Skip to content

Commit 7aeea6f

Browse files
committed
wip: 完善canvas 内部逻辑
1 parent bbc8d9f commit 7aeea6f

File tree

5 files changed

+91
-79
lines changed

5 files changed

+91
-79
lines changed

src/components/player/CanvasPlayer.vue

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ let clipboard: fabric.Object | null = null
2525
const menuList = [
2626
{ key: 'flipX', shortkey: '⌘+C', text: '复制', callback: onCopy },
2727
{ key: 'flipX', shortkey: '⌘+V', text: '粘贴', callback: onPaste },
28-
{ key: 'flipX', shortkey: 'Del', text: '删除', callback: deleteElement },
28+
{ key: 'flipX', shortkey: 'Del', text: '删除', callback: onDelete },
2929
{ key: 'flipY', shortkey: '⌘+]', text: '上移一层', callback: () => setElementLayer('up') },
3030
{ key: 'flipY', shortkey: '⌘+[', text: '下移一层', callback: () => setElementLayer('down') },
3131
{ key: 'flipY', shortkey: '', text: '置于顶层', callback: () => {} },
@@ -37,6 +37,7 @@ const menuList = [
3737
3838
emitter.on('element:copy', onCopy)
3939
emitter.on('element:paste', onPaste)
40+
emitter.on('element:delete', onDelete)
4041
watch(playStatus, (v) => video[v ? 'play' : 'pause']())
4142
4243
// 初始化画布
@@ -78,8 +79,9 @@ function drawElements() {
7879
}
7980
8081
// 删除元素
81-
function deleteElement() {
82-
const activeObject = canvas.getActiveObject()
82+
// BUG 为啥删不了
83+
function onDelete(obj: fabric.Object) {
84+
const activeObject = obj || canvas.getActiveObject()
8385
if (!activeObject) return
8486
canvas.remove(activeObject)
8587
canvas.requestRenderAll()
@@ -155,55 +157,6 @@ function continuouslyRepaint() {
155157
fabric.util.requestAnimFrame(continuouslyRepaint)
156158
}
157159
158-
// 初始化控件
159-
// TODO 需要根据不同的元素类型,定制不同的控件功能以及显示
160-
function initControls() {
161-
// 修改控件显示
162-
fabric.Object.prototype.setControlsVisibility({
163-
mt: false, // 上边裁剪
164-
mb: false, // 下边裁剪
165-
ml: false, // 左侧裁剪
166-
mr: true, // 右侧裁剪
167-
bl: true, // 左下角缩放
168-
br: true, // 右下角缩放
169-
tl: true, // 左上角缩放
170-
tr: true, // 右上角缩放
171-
mtr: true // 旋转
172-
})
173-
// 修改控件颜色
174-
fabric.Object.prototype.set({
175-
borderColor: '#fff', // 控制边框颜色
176-
cornerColor: '#fff', // 控制控件颜色
177-
cornerStrokeColor: '#fff', // 控制控件边框颜色
178-
cornerSize: 10, // 控制控件大小
179-
cornerStyle: 'circle', // 控制控件形状
180-
transparentCorners: false // 控制控件是否透明
181-
})
182-
// 修改所有图片类型的 mr 控件的 actionHandler
183-
fabric.Object.prototype.controls.mr.actionHandler = (eventData, transform, x, y) => {
184-
const target = transform.target as fabric.Transform['target'] & {
185-
_originalElement: HTMLImageElement
186-
}
187-
const originWidth = target._originalElement.width
188-
const originHeight = target._originalElement.height
189-
// 这里应该有个原始大小的获取方法
190-
// const originSize = xxxx.getOriginalSize()
191-
// 右侧裁剪
192-
const w = target.width! * target.scaleX!
193-
const h = target.height! * target.scaleY!
194-
const newWidth = x - target.left!
195-
// 限制最大,最小宽
196-
if (x < target.left! || newWidth < 50) return false
197-
if (newWidth > originWidth) return false
198-
target.set({
199-
width: newWidth
200-
})
201-
target.setCoords()
202-
canvas.requestRenderAll()
203-
return true
204-
}
205-
}
206-
207160
// TODO 需完善窗口大小变化时,画布保持当前宽高比进行缩放
208161
function resizePlayer() {
209162
const width = container.value!.clientWidth
@@ -302,6 +255,7 @@ function flip(flipType: 'x' | 'y') {
302255
}
303256
304257
// 以元素的中心旋转 90°
258+
// BUG 未拖动元素时,旋转会导致元素偏移
305259
function rotate() {
306260
const activeObject = canvas.getActiveObject()
307261
if (!activeObject) return
@@ -336,6 +290,12 @@ function initContextMenu(e: fabric.IEvent<MouseEvent>) {
336290
}
337291
}
338292
293+
/**
294+
* 需要在初始化时,设置 fireMiddleClick: true
295+
* 左键:button 的值为 1
296+
* 中键(也就是点击滚轮),button 的值为 2
297+
* 右键:button 的值为 3
298+
*/
339299
function canvasOnMouseDown(e: fabric.IEvent<MouseEvent>) {
340300
switch (e.button) {
341301
case 1:
@@ -353,8 +313,6 @@ function canvasOnMouseDown(e: fabric.IEvent<MouseEvent>) {
353313
onMounted((): void => {
354314
// 初始化画布
355315
initCanvas()
356-
// 初始化控件
357-
initControls()
358316
359317
// oncopy事件禁用复制;
360318
document.oncopy = onCopy

src/components/player/ContextMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type Menu = {
55
key: string // key
66
text: string // 文本
77
shortkey?: string // 快捷键
8-
callback: () => void // 回调
8+
callback: Function // 回调
99
}
1010
1111
defineProps<{ menuList: Menu[] }>()

src/components/right-panel/RightPanel.vue

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
11
<script setup lang="ts">
2+
import type { fabric } from 'fabric'
3+
import { storeToRefs } from 'pinia'
4+
import { IconDelete } from '~/assets/icons/index'
5+
import { usePlayerStore } from '~/stores/player'
26
import emitter from '~/utils/bus'
37
4-
function copy() {
5-
emitter.emit('element:copy')
6-
}
7-
function paste() {
8-
emitter.emit('element:paste')
8+
const playerStore = usePlayerStore()
9+
const { elementList } = storeToRefs(playerStore)
10+
11+
function onDelete(item: fabric.Object) {
12+
emitter.emit('element:delete', item)
913
}
1014
</script>
1115
<template>
12-
<div class="right-panel w-[400px] bg-[#272836] flex justify-between">
13-
<div class="p-8">
14-
<!-- Open the modal using ID.showModal() method -->
15-
<button class="btn btn-sm" onclick="testModal.showModal()">open modal</button>
16-
<dialog id="testModal" class="modal">
17-
<div class="modal-box">
18-
<h3 class="font-bold text-lg">Hello!</h3>
19-
<p class="py-4">Press ESC key or click the button below to close</p>
20-
<div class="modal-action">
21-
<form method="dialog">
22-
<!-- if there is a button in form, it will close the modal -->
23-
<button class="btn">Close</button>
24-
</form>
25-
</div>
26-
</div>
27-
</dialog>
16+
<div class="right-panel w-[400px] bg-[#272836] flex justify-between text-white">
17+
<div class="p-4 w-full">
18+
<p class="">Elements:</p>
2819
<div class="my-4 flex flex-col gap-4">
29-
<button class="btn btn-sm" @click="copy">Copy Selected Objects</button>
30-
<button class="btn btn-sm" @click="paste">Paste Selected Objects</button>
20+
<div
21+
class="flex justify-between border border-[#3b3b4f] p-2 items-center rounded-md"
22+
v-for="(item, index) in elementList"
23+
:key="index"
24+
>
25+
<p class="leading-[16px] h-[16px] m-0 p-0">{{ item.type }} - {{ index + 1 }}</p>
26+
<button class="btn btn-sm btn-error" @click="onDelete(item)">
27+
<IconDelete fill="white" />
28+
</button>
29+
</div>
3130
</div>
3231
</div>
3332
<div class="w-[60px] bg-[#1c1c26] p-8"></div>

src/stores/player.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { fabric } from 'fabric'
12
import { defineStore } from 'pinia'
23
import { ref, computed } from 'vue'
34

@@ -12,10 +13,12 @@ export const usePlayerStore = defineStore('playerStore', () => {
1213
const playList = ref<string[]>([])
1314
// 轨道数
1415
const trackCount = computed(() => playList.value.length)
16+
// 元素列表
17+
const elementList = ref<fabric.Object[]>([])
1518

1619
const currentTime = ref<number>(0)
1720

1821
const duration = ref<number>(0)
1922

20-
return { playStatus, togglePlay, playList, trackCount, currentTime, duration }
23+
return { playStatus, togglePlay, playList, trackCount, currentTime, duration, elementList }
2124
})

src/utils/index.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,55 @@
1+
interface GetThumbnailBySeekOptions {
2+
url: string // 视频地址
3+
interval: number // 间隔多少秒抽一帧
4+
clipStart?: number // 裁剪开始时间
5+
clipEnd?: number // 裁剪结束时间
6+
}
7+
// 通过 video + canvas 对视频进行抽帧
8+
export async function getThumbnailBySeek(options: GetThumbnailBySeekOptions) {
9+
const { url, interval, clipStart, clipEnd } = options
10+
const video = document.createElement('video')
11+
// video.src = 'https://assets.fedtop.com/picbed/movie.mp4'
12+
video.src = url
13+
video.crossOrigin = 'anonymous' // 跨域, 防止污染
14+
video.preload = 'auto' // 不加会导致黑屏
15+
video.muted = true
16+
const canvas = document.createElement('canvas')
17+
const ctx = canvas.getContext('2d')!
18+
const thumbnails = []
19+
let clipDuration = 0
20+
if (clipEnd && clipStart) clipDuration = clipEnd - clipStart
21+
video.currentTime = clipStart || 0
22+
23+
const generateThumbnail = () => {
24+
// currentTime = parseFloat(currentTime.toFixed(4))
25+
if (video.currentTime < video.duration) {
26+
// 间隔
27+
const gap = (clipDuration || video.duration) / interval
28+
// 跳转到指定时间,触发seeked事件
29+
video.currentTime += gap
30+
} else {
31+
video.removeEventListener('seeked', seekedHandler)
32+
}
33+
}
34+
35+
const seekedHandler = () => {
36+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
37+
canvas.toBlob((blob) => {
38+
if (!blob) return
39+
const url = URL.createObjectURL(blob)
40+
thumbnails.push({ img: url, ts: video.currentTime })
41+
generateThumbnail()
42+
})
43+
}
44+
45+
video.addEventListener('seeked', seekedHandler)
46+
video.addEventListener('loadeddata', async () => {
47+
canvas.width = video.videoWidth
48+
canvas.height = video.videoHeight
49+
generateThumbnail()
50+
})
51+
}
52+
153
export function formatSeconds(seconds: number) {
254
seconds = Math.floor(seconds)
355
const hours = Math.floor(seconds / 3600)

0 commit comments

Comments
 (0)