@webav/av-canvas
Version:
Combine Text, Image, Video, Audio, UserMedia, DisplayMedia to generate MediaStream. With [AVRcorder](../av-recorder/README.md) you can output MP4 streams and save them as local files or push them to the server.
1 lines • 68.1 kB
Source Map (JSON)
{"version":3,"file":"av-canvas.umd.cjs","sources":["../src/types.ts","../src/utils.ts","../src/sprites/sprite-manager.ts","../src/sprites/render-ctrl.ts","../src/sprites/sprite-op.ts","../src/av-canvas.ts"],"sourcesContent":["import { Rect } from '@webav/av-cliper';\n\n/**\n * 二维坐标系中的点\n */\nexport interface IPoint {\n x: number;\n y: number;\n}\n\n/**\n * 分辨率(尺寸)\n */\nexport interface IResolution {\n width: number;\n height: number;\n}\n\n/**\n * 画布分辨率与实际宽高的比例\n */\nexport interface ICvsRatio {\n w: number;\n h: number;\n}\n\n/**\n * 控制点:上、下、左、右,左上、左下、右上、右下、旋转\n * 当 Rect 只允许等比例缩放时({@link Rect.fixedAspectRatio} = true),缺少 t、b、l、r 四个控制点\n */\nexport type RectCtrls = Partial<Record<'t' | 'b' | 'l' | 'r', Rect>> &\n Record<'lt' | 'lb' | 'rt' | 'rb' | 'rotate', Rect>;\n\nexport const CTRL_KEYS = [\n 't',\n 'b',\n 'l',\n 'r',\n 'lt',\n 'lb',\n 'rt',\n 'rb',\n 'rotate',\n] as const;\n\nexport type TCtrlKey = (typeof CTRL_KEYS)[number];\n","import { Rect } from '@webav/av-cliper';\nimport { ICvsRatio, RectCtrls } from './types';\n\nexport function createEl(tagName: string): HTMLElement {\n return document.createElement(tagName);\n}\n\nconst rectGetterCache = new WeakMap<\n HTMLCanvasElement,\n (rect: Rect) => RectCtrls\n>();\n/**\n * 根据 canvas 创建该画布上的 Sprite 控制点生成器\n * 因为控制点的大小需要根据画布的大小动态调整\n */\nexport function getRectCtrls(cvsEl: HTMLCanvasElement, rect: Rect) {\n if (rectGetterCache.has(cvsEl)) {\n return rectGetterCache.get(cvsEl)!(rect);\n }\n\n let ctrlSize = 10;\n const cvsResizeOb = new ResizeObserver((entries) => {\n const fisrtEntry = entries[0];\n if (fisrtEntry == null) return;\n ctrlSize = 10 / (fisrtEntry.contentRect.width / cvsEl.width);\n });\n cvsResizeOb.observe(cvsEl);\n function rectCtrlsGetter(rect: Rect): RectCtrls {\n const { w, h } = rect;\n // 控制点元素大小, 以 分辨率 为基准\n const sz = ctrlSize;\n // half size\n const hfSz = sz / 2;\n const hfW = w / 2;\n const hfH = h / 2;\n // rotate size\n const rtSz = sz * 1.5;\n const hfRtSz = rtSz / 2;\n // ctrl 坐标是相对于 sprite 中心点\n const tblr = rect.fixedAspectRatio\n ? {}\n : {\n t: new Rect(-hfSz, -hfH - hfSz, sz, sz, rect),\n b: new Rect(-hfSz, hfH - hfSz, sz, sz, rect),\n l: new Rect(-hfW - hfSz, -hfSz, sz, sz, rect),\n r: new Rect(hfW - hfSz, -hfSz, sz, sz, rect),\n };\n return {\n ...tblr,\n lt: new Rect(-hfW - hfSz, -hfH - hfSz, sz, sz, rect),\n lb: new Rect(-hfW - hfSz, hfH - hfSz, sz, sz, rect),\n rt: new Rect(hfW - hfSz, -hfH - hfSz, sz, sz, rect),\n rb: new Rect(hfW - hfSz, hfH - hfSz, sz, sz, rect),\n rotate: new Rect(-hfRtSz, -hfH - sz * 2 - hfRtSz, rtSz, rtSz, rect),\n };\n }\n rectGetterCache.set(cvsEl, rectCtrlsGetter);\n return rectCtrlsGetter(rect);\n}\n\n// 复用 canvas 比例的获取,避免重复 observer\nconst cvsRatioCache = new WeakMap<HTMLCanvasElement, ICvsRatio>();\nexport function getCvsRatio(cvsEl: HTMLCanvasElement): ICvsRatio {\n if (cvsRatioCache.has(cvsEl)) {\n return cvsRatioCache.get(cvsEl)!;\n }\n\n const cvsRatio = {\n w: cvsEl.clientWidth / cvsEl.width,\n h: cvsEl.clientHeight / cvsEl.height,\n };\n const observer = new ResizeObserver(() => {\n cvsRatio.w = cvsEl.clientWidth / cvsEl.width;\n cvsRatio.h = cvsEl.clientHeight / cvsEl.height;\n });\n observer.observe(cvsEl);\n cvsRatioCache.set(cvsEl, cvsRatio);\n return cvsRatio;\n}\n","import { VisibleSprite } from '@webav/av-cliper';\nimport { EventTool } from '@webav/internal-utils';\n\nexport enum ESpriteManagerEvt {\n ActiveSpriteChange = 'activeSpriteChange',\n AddSprite = 'addSprite',\n}\n\nexport class SpriteManager {\n #sprites: VisibleSprite[] = [];\n\n #activeSprite: VisibleSprite | null = null;\n\n #evtTool = new EventTool<{\n [ESpriteManagerEvt.AddSprite]: (s: VisibleSprite) => void;\n [ESpriteManagerEvt.ActiveSpriteChange]: (s: VisibleSprite | null) => void;\n }>();\n\n on = this.#evtTool.on;\n\n get activeSprite(): VisibleSprite | null {\n return this.#activeSprite;\n }\n set activeSprite(s: VisibleSprite | null) {\n if (s === this.#activeSprite || s?.interactable === 'disabled') return;\n this.#activeSprite = s;\n this.#evtTool.emit(ESpriteManagerEvt.ActiveSpriteChange, s);\n }\n\n activeSpriteByCoord(x: number, y: number): void {\n this.activeSprite =\n this.getSprites()\n // 排在后面的层级更高\n .reverse()\n .find(\n (s) =>\n s.visible && s.interactable !== 'disabled' && s.rect.checkHit(x, y),\n ) ?? null;\n }\n\n async addSprite(vs: VisibleSprite): Promise<void> {\n await vs.ready;\n this.#sprites.push(vs);\n this.#sprites = this.#sprites.sort((a, b) => a.zIndex - b.zIndex);\n vs.on('propsChange', (props) => {\n if (props.zIndex == null) return;\n this.#sprites = this.#sprites.sort((a, b) => a.zIndex - b.zIndex);\n });\n\n this.#evtTool.emit(ESpriteManagerEvt.AddSprite, vs);\n }\n\n removeSprite(spr: VisibleSprite): void {\n if (this.#activeSprite === spr) this.activeSprite = null;\n this.#sprites = this.#sprites.filter((s) => s !== spr);\n spr.destroy();\n }\n\n getSprites(filter: { time: boolean } = { time: true }): VisibleSprite[] {\n return this.#sprites.filter(\n (s) =>\n s.visible &&\n (filter.time\n ? this.#renderTime >= s.time.offset &&\n this.#renderTime <= s.time.offset + s.time.duration\n : true),\n );\n }\n\n #renderTime = 0;\n updateRenderTime(time: number) {\n this.#renderTime = time;\n\n // 避免素材不可见,但渲染了素材边框控制点\n const as = this.activeSprite;\n if (\n as != null &&\n (time < as.time.offset || time > as.time.offset + as.time.duration)\n ) {\n this.activeSprite = null;\n }\n }\n\n destroy(): void {\n this.#evtTool.destroy();\n this.#sprites.forEach((s) => s.destroy());\n this.#sprites = [];\n }\n}\n","import { VisibleSprite } from '@webav/av-cliper';\nimport { CTRL_KEYS, TCtrlKey } from '../types';\nimport { createEl, getCvsRatio, getRectCtrls } from '../utils';\nimport { ESpriteManagerEvt, SpriteManager } from './sprite-manager';\n\nconst CloseSvg = `\n<svg t=\"1756779136804\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1456\" width=\"16\" height=\"16\">\n<path d=\"M1022.793875 170.063604L852.730271 0 511.396938 341.333333 170.063604 0 0 170.063604l341.333333 341.333334L0 852.730271l170.063604 170.063604 341.333334-340.127208 341.333333 340.127208 170.063604-170.063604-340.127208-341.333333 340.127208-341.333334z\" fill=\"#bfbfbf\" p-id=\"1457\"></path>\n</svg>\n`;\n\nexport function renderCtrls(\n container: HTMLElement,\n cvsEl: HTMLCanvasElement,\n sprMng: SpriteManager,\n): () => void {\n const cvsRatio = getCvsRatio(cvsEl);\n const observer = new ResizeObserver(() => {\n if (sprMng.activeSprite == null) return;\n syncCtrlElPos(sprMng.activeSprite, cvsEl, rectEl, ctrlsEl);\n });\n observer.observe(cvsEl);\n\n let lastActSprEvtClear = () => {};\n const { rectEl, ctrlsEl } = createRectAndCtrlEl(container);\n\n // 添加点击事件处理\n rectEl.addEventListener('pointerdown', (evt) => {\n // 如果点击的是控制点,不处理点击穿透\n if (Object.values(ctrlsEl).includes(evt.target as HTMLElement)) {\n return;\n }\n\n // 获取相对于 canvas 的坐标,可能需要激活更上层的 sprite\n const cvsRect = cvsEl.getBoundingClientRect();\n const x = (evt.clientX - cvsRect.left) / cvsRatio.w;\n const y = (evt.clientY - cvsRect.top) / cvsRatio.h;\n sprMng.activeSpriteByCoord(x, y);\n });\n\n const offSprChange = sprMng.on(ESpriteManagerEvt.ActiveSpriteChange, (s) => {\n // 每次变更,需要清理上一个事件监听器\n lastActSprEvtClear();\n if (s == null) {\n rectEl.style.display = 'none';\n return;\n }\n syncCtrlElPos(s, cvsEl, rectEl, ctrlsEl);\n lastActSprEvtClear = s.on('propsChange', () => {\n syncCtrlElPos(s, cvsEl, rectEl, ctrlsEl);\n });\n });\n\n return () => {\n observer.disconnect();\n offSprChange();\n rectEl.remove();\n lastActSprEvtClear();\n };\n}\n\nfunction createRectAndCtrlEl(container: HTMLElement): {\n rectEl: HTMLElement;\n ctrlsEl: Record<TCtrlKey, HTMLElement>;\n} {\n const rectEl = createEl('div');\n rectEl.classList.add('sprite-rect');\n rectEl.style.cssText = `\n position: absolute;\n z-index: 3;\n pointer-events: auto;\n border: 1px solid #eee;\n box-sizing: border-box;\n display: none;\n cursor: move;\n `;\n const ctrlsEl = Object.fromEntries(\n CTRL_KEYS.map((k) => {\n const d = createEl('div');\n d.classList.add(`ctrl-key-${k}`);\n d.style.cssText = `\n display: none;\n position: absolute;\n border: 1px solid #3ee;\n border-radius: 50%;\n box-sizing: border-box;\n background-color: #fff;\n pointer-events: auto;\n cursor: ${k === 'rotate' ? 'crosshair' : 'default'};\n user-select: none;\n `;\n return [k, d];\n }),\n ) as Record<TCtrlKey, HTMLElement>;\n\n Object.values(ctrlsEl).forEach((d) => rectEl.appendChild(d));\n container.appendChild(rectEl);\n return {\n rectEl,\n ctrlsEl,\n };\n}\n\nfunction syncCtrlElPos(\n s: VisibleSprite,\n cvsEl: HTMLCanvasElement,\n rectEl: HTMLElement,\n ctrlsEl: Record<TCtrlKey, HTMLElement>,\n): void {\n if (s.interactable === 'disabled') {\n rectEl.style.display = 'none';\n return;\n }\n rectEl.style.display = '';\n\n const cvsRatio = getCvsRatio(cvsEl);\n const { x, y, w, h, angle } = s.rect;\n\n Object.assign(rectEl.style, {\n left: `${x * cvsRatio.w}px`,\n top: `${y * cvsRatio.h}px`,\n width: `${w * cvsRatio.w}px`,\n height: `${h * cvsRatio.h}px`,\n transform: `rotate(${angle}rad)`,\n });\n\n const ctrlPosMap = getRectCtrls(cvsEl, s.rect);\n\n for (const k in ctrlsEl) {\n const key = k as TCtrlKey;\n const el = ctrlsEl[key];\n const pos = ctrlPosMap[key];\n\n if (pos == null) {\n el.style.display = 'none';\n continue;\n }\n\n const baseStyle: Record<string, string> = {\n width: `${pos.w * cvsRatio.w}px`,\n height: `${pos.h * cvsRatio.h}px`,\n transform: `translate(${pos.x * cvsRatio.w}px, ${pos.y * cvsRatio.h}px)`,\n left: '50%',\n top: '50%',\n };\n\n let customStyle: Record<string, string> = { display: 'none' };\n el.innerHTML = '';\n\n switch (s.interactable) {\n case 'interactive':\n customStyle = {\n display: 'block',\n backgroundColor: '#fff',\n border: '1px solid #3ee',\n };\n break;\n case 'selectable':\n if (key !== 'rotate') {\n customStyle = {\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n backgroundColor: 'transparent',\n border: 'none',\n };\n el.innerHTML = CloseSvg;\n }\n break;\n }\n\n Object.assign(el.style, baseStyle, customStyle);\n }\n}\n","import { Rect } from '@webav/av-cliper';\nimport { debounce } from '@webav/internal-utils';\nimport { CTRL_KEYS, ICvsRatio, IPoint, TCtrlKey } from '../types';\nimport { createEl, getCvsRatio } from '../utils';\nimport { ESpriteManagerEvt, SpriteManager } from './sprite-manager';\n\n/**\n * 鼠标点击,激活 sprite\n */\nexport function activeSprite(\n cvsEl: HTMLCanvasElement,\n sprMng: SpriteManager,\n): () => void {\n const onCvsMouseDown = (evt: MouseEvent): void => {\n if (evt.button !== 0) return;\n // 如果点击的是控制元素,不处理选择逻辑\n if ((evt.target as HTMLElement) !== cvsEl) return;\n\n const cvsRatio = getCvsRatio(cvsEl);\n const { offsetX, offsetY } = evt;\n const ofx = offsetX / cvsRatio.w;\n const ofy = offsetY / cvsRatio.h;\n\n sprMng.activeSpriteByCoord(ofx, ofy);\n };\n\n cvsEl.addEventListener('pointerdown', onCvsMouseDown);\n\n return () => {\n cvsEl.removeEventListener('pointerdown', onCvsMouseDown);\n };\n}\n\n/**\n * 让sprite可以被拖拽移动、缩放和旋转\n */\nexport function draggabelSprite(\n cvsEl: HTMLCanvasElement,\n sprMng: SpriteManager,\n container: HTMLElement,\n): () => void {\n let startX = 0;\n let startY = 0;\n let startRect: Rect | null = null;\n\n const refline = createRefline(cvsEl, container);\n\n // 查找控制 sprite 的 DOM 元素,在 renderCtrls 中创建并添加到 container 中\n const rectEl = container.querySelector('.sprite-rect') as HTMLElement;\n if (!rectEl) throw Error('sprite-rect DOM Node not found');\n\n // 移动sprite的处理函数\n const onRectMouseDown = (evt: MouseEvent): void => {\n const hitSpr = sprMng.activeSprite;\n if (\n evt.button !== 0 ||\n hitSpr == null ||\n hitSpr.interactable !== 'interactive'\n )\n return;\n\n const { clientX, clientY } = evt;\n\n startRect = hitSpr.rect.clone();\n refline.magneticEffect(hitSpr.rect.x, hitSpr.rect.y, hitSpr.rect);\n\n startX = clientX;\n startY = clientY;\n window.addEventListener('pointermove', onMouseMove);\n window.addEventListener('pointerup', clearWindowEvt);\n\n evt.stopPropagation();\n };\n\n const cvsRatio = getCvsRatio(cvsEl);\n const onMouseMove = (evt: MouseEvent): void => {\n const hitSpr = sprMng.activeSprite;\n if (\n hitSpr == null ||\n hitSpr.interactable !== 'interactive' ||\n startRect == null\n )\n return;\n\n const { clientX, clientY } = evt;\n let expectX = startRect.x + (clientX - startX) / cvsRatio.w;\n let expectY = startRect.y + (clientY - startY) / cvsRatio.h;\n\n updateRectWithSafeMargin(\n hitSpr.rect,\n cvsEl,\n refline.magneticEffect(expectX, expectY, hitSpr.rect),\n );\n };\n\n const clearWindowEvt = (): void => {\n refline.hide();\n window.removeEventListener('pointermove', onMouseMove);\n window.removeEventListener('pointerup', clearWindowEvt);\n };\n\n // 初始设置\n rectEl.addEventListener('pointerdown', onRectMouseDown);\n cvsEl.addEventListener('pointerdown', onRectMouseDown);\n const offCtrlEvt = setupCtrlEvents(cvsEl, rectEl, sprMng);\n\n return () => {\n refline.destroy();\n clearWindowEvt();\n rectEl.removeEventListener('pointerdown', onRectMouseDown);\n cvsEl.removeEventListener('pointerdown', onRectMouseDown);\n offCtrlEvt();\n };\n}\n\n// 为控制点添加事件处理\nfunction setupCtrlEvents(\n cvsEl: HTMLCanvasElement,\n rectEl: HTMLElement,\n sprMng: SpriteManager,\n) {\n // 获取所有控制点元素\n const ctrlElements = Array.from(rectEl.children) as HTMLElement[];\n\n const cvsRatio = getCvsRatio(cvsEl);\n // 鼠标按下对应的节点,进行对应的操作(旋转、缩放)\n ctrlElements.forEach((ctrlEl, index) => {\n const ctrlKey = CTRL_KEYS[index];\n ctrlEl.addEventListener('pointerdown', (evt: MouseEvent) => {\n const hitSpr = sprMng.activeSprite;\n if (\n evt.button !== 0 ||\n hitSpr == null ||\n hitSpr.interactable !== 'interactive'\n )\n return;\n\n const { clientX, clientY } = evt;\n\n if (ctrlKey === 'rotate') {\n rotateRect(\n hitSpr.rect,\n cntMap2Outer(hitSpr.rect.center, cvsRatio, cvsEl),\n );\n } else {\n scaleRect({\n sprRect: hitSpr.rect,\n ctrlKey,\n startX: clientX,\n startY: clientY,\n cvsRatio,\n cvsEl,\n });\n }\n\n evt.stopPropagation();\n });\n });\n\n ctrlElements[CTRL_KEYS.indexOf('rotate')].style.cursor = 'crosshair';\n\n // 根据角度,动态调整每个控制节点的鼠标样式\n const curStyles = [\n 'ns-resize',\n 'nesw-resize',\n 'ew-resize',\n 'nwse-resize',\n 'ns-resize',\n 'nesw-resize',\n 'ew-resize',\n 'nwse-resize',\n ];\n const curInitIdx = {\n t: 0,\n rt: 1,\n r: 2,\n rb: 3,\n b: 4,\n lb: 5,\n l: 6,\n lt: 7,\n };\n\n let offPropsEvt = () => {};\n const offActSprEvt = sprMng.on(ESpriteManagerEvt.ActiveSpriteChange, (s) => {\n offPropsEvt();\n if (s == null) return;\n\n const updateCursorStyle = debounce(function () {\n const { angle } = s.rect;\n const oa = angle < 0 ? angle + 2 * Math.PI : angle;\n\n ctrlElements.forEach((ctrlEl, index) => {\n const ctrlKey = CTRL_KEYS[index];\n if (ctrlKey === 'rotate') return;\n // 每个控制点的初始样式(idx) + 旋转角度导致的偏移,即为新鼠标样式\n // 每旋转45°,偏移+1,以此在curStyles中循环\n const idx =\n (curInitIdx[ctrlKey] +\n Math.floor((oa + Math.PI / 8) / (Math.PI / 4))) %\n 8;\n ctrlEl.style.cursor = curStyles[idx];\n });\n }, 300);\n\n offPropsEvt = s.on('propsChange', (props) => {\n if (props.rect?.angle == null) return;\n updateCursorStyle();\n });\n\n updateCursorStyle();\n });\n return () => {\n offPropsEvt();\n offActSprEvt();\n };\n}\n\n/**\n * 缩放 sprite\n */\nfunction scaleRect({\n sprRect,\n startX,\n startY,\n ctrlKey,\n cvsRatio,\n cvsEl,\n}: {\n sprRect: Rect;\n startX: number;\n startY: number;\n ctrlKey: TCtrlKey;\n cvsRatio: ICvsRatio;\n cvsEl: HTMLCanvasElement;\n}): void {\n const startRect = sprRect.clone();\n\n const onMouseMove = (evt: MouseEvent): void => {\n const { clientX, clientY } = evt;\n const deltaX = (clientX - startX) / cvsRatio.w;\n const deltaY = (clientY - startY) / cvsRatio.h;\n\n // 对角线上的点是等比例缩放,key 的长度为 2\n const scaler = ctrlKey.length === 1 ? stretchScale : fixedRatioScale;\n const { x, y, w, h } = startRect;\n // rect 对角线角度\n const diagonalAngle = Math.atan2(h, w);\n const { incW, incH, incS, rotateAngle } = scaler({\n deltaX,\n deltaY,\n angle: sprRect.angle,\n ctrlKey,\n diagonalAngle,\n });\n\n // 最小宽高缩放限定\n const minSize = 10;\n let newW = w;\n let newH = h;\n // 中心点缩放时,宽高增量是原来的两倍\n let newIncW = startRect.fixedScaleCenter ? incW * 2 : incW;\n let newIncH = startRect.fixedScaleCenter ? incH * 2 : incH;\n // 最小长度缩放限定\n let newIncS = incS;\n // 起始对角线长度\n const startS = Math.sqrt(h ** 2 + w ** 2);\n // 最小对角线长度\n const minS = Math.sqrt((minSize * (h / w)) ** 2 + minSize ** 2);\n switch (ctrlKey) {\n // 非等比例缩放时,变化的增量范围 由原宽高跟 minSize 的差值决定\n // 非等比例缩放时,根据ctrlKey的不同,固定宽高中的一个,另一个根据增量计算,并考虑最小值限定\n case 'l':\n newW = Math.max(w + newIncW, minSize);\n newIncS = Math.min(incS, w - minSize);\n break;\n case 'r':\n newW = Math.max(w + newIncW, minSize);\n newIncS = Math.max(incS, minSize - w);\n break;\n case 'b':\n newH = Math.max(h + newIncH, minSize);\n newIncS = Math.min(incS, h - minSize);\n break;\n case 't':\n newH = Math.max(h + newIncH, minSize);\n newIncS = Math.max(incS, minSize - h);\n break;\n // 等比例缩放时,变化(对角线长度)的增量范围由原对角线长度跟 minSize 对角线的差值决定\n // 等比例缩放时,某一边达到最小值时保持宽高比例不变\n case 'lt':\n case 'lb':\n newW = Math.max(w + newIncW, minSize);\n newH = newW === minSize ? (h / w) * newW : h + newIncH;\n newIncS = Math.min(incS, startS - minS);\n break;\n case 'rt':\n case 'rb':\n newW = Math.max(w + newIncW, minSize);\n newH = newW === minSize ? (h / w) * newW : h + newIncH;\n newIncS = Math.max(incS, minS - startS);\n break;\n }\n let newX = x;\n let newY = y;\n if (startRect.fixedScaleCenter) {\n newX = x + w / 2 - newW / 2;\n newY = y + h / 2 - newH / 2;\n } else {\n const newCenterX = (newIncS / 2) * Math.cos(rotateAngle) + x + w / 2;\n const newCenterY = (newIncS / 2) * Math.sin(rotateAngle) + y + h / 2;\n newX = newCenterX - newW / 2;\n newY = newCenterY - newH / 2;\n }\n\n updateRectWithSafeMargin(sprRect, cvsEl, {\n x: newX,\n y: newY,\n w: newW,\n h: newH,\n });\n };\n\n const clearWindowEvt = (): void => {\n window.removeEventListener('pointermove', onMouseMove);\n window.removeEventListener('pointerup', clearWindowEvt);\n };\n window.addEventListener('pointermove', onMouseMove);\n window.addEventListener('pointerup', clearWindowEvt);\n}\n\n/**\n * 拉伸缩放, 上t 下b 左l 右r\n */\nfunction stretchScale({\n deltaX,\n deltaY,\n angle,\n ctrlKey,\n}: {\n deltaX: number;\n deltaY: number;\n angle: number;\n ctrlKey: TCtrlKey;\n}): {\n incW: number;\n incH: number;\n incS: number;\n rotateAngle: number;\n} {\n // 计算矩形增加的宽度\n let incS = 0;\n let incW = 0;\n let incH = 0;\n let rotateAngle = angle;\n if (ctrlKey === 'l' || ctrlKey === 'r') {\n incS = deltaX * Math.cos(angle) + deltaY * Math.sin(angle);\n // l 缩放是反向的\n incW = incS * (ctrlKey === 'l' ? -1 : 1);\n } else if (ctrlKey === 't' || ctrlKey === 'b') {\n // 计算矩形增加的宽度,旋转坐标系让x轴与角度重合,鼠标位置在x轴的投影(x值)即为增加的高度\n rotateAngle = angle - Math.PI / 2;\n incS = deltaX * Math.cos(rotateAngle) + deltaY * Math.sin(rotateAngle);\n incH = incS * (ctrlKey === 'b' ? -1 : 1);\n }\n\n return { incW, incH, incS, rotateAngle };\n}\n\n/**\n * 等比例缩放\n */\nfunction fixedRatioScale({\n deltaX,\n deltaY,\n angle,\n ctrlKey,\n diagonalAngle,\n}: {\n deltaX: number;\n deltaY: number;\n angle: number;\n ctrlKey: TCtrlKey;\n diagonalAngle: number;\n}): {\n incW: number;\n incH: number;\n incS: number;\n rotateAngle: number;\n} {\n // 坐标系旋转角度, lb->rt的对角线的初始角度为负数,所以需要乘以-1\n const rotateAngle =\n (ctrlKey === 'lt' || ctrlKey === 'rb' ? 1 : -1) * diagonalAngle + angle;\n // 旋转坐标系让x轴与对角线重合,鼠标位置在x轴的投影(x值)即为增加的长度\n const incS = deltaX * Math.cos(rotateAngle) + deltaY * Math.sin(rotateAngle);\n // lb lt 缩放值是反向\n const coefficient = ctrlKey === 'lt' || ctrlKey === 'lb' ? -1 : 1;\n // 等比例缩放,增加宽高等于长度乘以对应的角度函数\n // 因为等比例缩放,中心及被拖拽的点,一定在对角线上\n const incW = incS * Math.cos(diagonalAngle) * coefficient;\n const incH = incS * Math.sin(diagonalAngle) * coefficient;\n\n return { incW, incH, incS, rotateAngle };\n}\n\n/**\n * 监听拖拽事件,将鼠标坐标转换为旋转角度\n * 旋转时,rect的坐标不变\n */\nfunction rotateRect(rect: Rect, outCnt: IPoint): void {\n const onMove = ({ clientX, clientY }: MouseEvent): void => {\n // 映射为 中心点坐标系\n const x = clientX - outCnt.x;\n const y = clientY - outCnt.y;\n // 旋转控制点在正上方,与 x 轴是 -90°, 所以需要加上 Math.PI / 2\n const angle = Math.atan2(y, x) + Math.PI / 2;\n rect.angle = angle;\n };\n const clear = (): void => {\n window.removeEventListener('pointermove', onMove);\n window.removeEventListener('pointerup', clear);\n };\n window.addEventListener('pointermove', onMove);\n window.addEventListener('pointerup', clear);\n}\n\n/**\n * canvas 内部(resolution)坐标映射成外部(DOM)坐标\n */\nfunction cntMap2Outer(\n cnt: IPoint,\n cvsRatio: ICvsRatio,\n cvsEl: HTMLElement,\n): IPoint {\n const x = cnt.x * cvsRatio.w;\n const y = cnt.y * cvsRatio.h;\n\n const { left, top } = cvsEl.getBoundingClientRect();\n return {\n x: x + left,\n y: y + top,\n };\n}\n\n/**\n * 限制安全范围,避免 sprite 完全超出边界\n */\nfunction updateRectWithSafeMargin(\n rect: Rect,\n cvsEl: HTMLCanvasElement,\n value: Partial<Pick<Rect, 'x' | 'y' | 'w' | 'h'>>,\n) {\n const newState = { x: rect.x, y: rect.y, w: rect.w, h: rect.h, ...value };\n const safeWidth = cvsEl.width * 0.05;\n const safeHeight = cvsEl.height * 0.05;\n if (newState.x < -newState.w + safeWidth) {\n newState.x = -newState.w + safeWidth;\n } else if (newState.x > cvsEl.width - safeWidth) {\n newState.x = cvsEl.width - safeWidth;\n }\n if (newState.y < -newState.h + safeHeight) {\n newState.y = -newState.h + safeHeight;\n } else if (newState.y > cvsEl.height - safeHeight) {\n newState.y = cvsEl.height - safeHeight;\n }\n rect.x = newState.x;\n rect.y = newState.y;\n rect.w = newState.w;\n rect.h = newState.h;\n}\n\n/**\n * 创建四周+中线参考线, 靠近具有磁吸效果\n */\nfunction createRefline(cvsEl: HTMLCanvasElement, container: HTMLElement) {\n const reflineBaseCSS = `display: none; position: absolute;`;\n const baseSettings = { w: 0, h: 0, x: 0, y: 0 } as const;\n const reflineSettings: Record<\n 'top' | 'bottom' | 'left' | 'right' | 'vertMiddle' | 'horMiddle',\n {\n // 四周加中线参考线,它们的坐标、宽高只能是 0 | 50 | 100\n w: 0 | 50 | 100;\n h: 0 | 50 | 100;\n x: 0 | 50 | 100;\n y: 0 | 50 | 100;\n ref: { prop: 'x' | 'y'; val: (rect: Rect) => number };\n }\n > = {\n vertMiddle: {\n ...baseSettings,\n h: 100,\n x: 50,\n ref: { prop: 'x', val: ({ w }) => (cvsEl.width - w) / 2 },\n },\n horMiddle: {\n ...baseSettings,\n w: 100,\n y: 50,\n ref: { prop: 'y', val: ({ h }) => (cvsEl.height - h) / 2 },\n },\n top: {\n ...baseSettings,\n w: 100,\n ref: { prop: 'y', val: () => 0 },\n },\n bottom: {\n ...baseSettings,\n w: 100,\n y: 100,\n ref: { prop: 'y', val: ({ h }) => cvsEl.height - h },\n },\n left: {\n ...baseSettings,\n h: 100,\n ref: { prop: 'x', val: () => 0 },\n },\n right: {\n ...baseSettings,\n h: 100,\n x: 100,\n ref: { prop: 'x', val: ({ w }) => cvsEl.width - w },\n },\n } as const;\n\n const lineWrap = createEl('div');\n lineWrap.style.cssText = `\n position: absolute;\n z-index: 4;\n top: 0; left: 0;\n width: 100%; height: 100%;\n pointer-events: none;\n box-sizing: border-box;\n `;\n const reflineEls = Object.fromEntries(\n Object.entries(reflineSettings).map(([key, { w, h, x, y }]) => {\n const lineEl = createEl('div');\n lineEl.style.cssText = `\n ${reflineBaseCSS}\n border-${w > 0 ? 'top' : 'left'}: 1px solid #3ee;\n top: ${y}%; left: ${x}%;\n ${x === 100 ? 'margin-left: -1px' : ''};\n ${y === 100 ? 'margin-top: -1px' : ''};\n width: ${w}%; height: ${h}%;\n `;\n lineWrap.appendChild(lineEl);\n return [key, lineEl];\n }),\n ) as Record<keyof typeof reflineSettings, HTMLDivElement>;\n container.appendChild(lineWrap);\n\n const magneticDistance = 6 / (900 / cvsEl.width);\n return {\n magneticEffect(expectX: number, expectY: number, rect: Rect) {\n const retVal = { x: expectX, y: expectY };\n\n // 记录每个维度(x,y)的最小距离和对应的参考线\n const minDist = { x: magneticDistance, y: magneticDistance };\n\n type RefLineKey = keyof typeof reflineEls;\n const activeReflines = { x: '', y: '' } as {\n x: RefLineKey | '';\n y: RefLineKey | '';\n };\n\n // 隐藏所有参考线,稍后会显示激活的参考线\n Object.values(reflineEls).forEach((el) => (el.style.display = 'none'));\n\n // 遍历所有参考线\n for (const reflineKey in reflineSettings) {\n const { prop, val } =\n reflineSettings[reflineKey as keyof typeof reflineSettings].ref;\n const refVal = val(rect);\n const currentVal = prop === 'x' ? expectX : expectY;\n\n // 计算与参考线的距离\n const dist = Math.abs(currentVal - refVal);\n\n // 在磁吸范围内,且比当前记录的最小距离更近\n if (dist <= magneticDistance && dist < minDist[prop]) {\n minDist[prop] = dist;\n retVal[prop] = refVal;\n activeReflines[prop] = reflineKey as RefLineKey;\n }\n }\n\n // 显示激活的参考线\n if (activeReflines.x) {\n reflineEls[activeReflines.x].style.display = 'block';\n }\n if (activeReflines.y) {\n reflineEls[activeReflines.y].style.display = 'block';\n }\n\n return retVal;\n },\n hide() {\n Object.values(reflineEls).forEach((el) => (el.style.display = 'none'));\n },\n destroy() {\n lineWrap.remove();\n },\n };\n}\n","import {\n Combinator,\n ICombinatorOpts,\n Log,\n MediaStreamClip,\n OffscreenSprite,\n Rect,\n VisibleSprite,\n} from '@webav/av-cliper';\nimport { EventTool, throttle, workerTimer } from '@webav/internal-utils';\nimport { renderCtrls } from './sprites/render-ctrl';\nimport { ESpriteManagerEvt, SpriteManager } from './sprites/sprite-manager';\nimport { activeSprite, draggabelSprite } from './sprites/sprite-op';\nimport { IResolution } from './types';\nimport { createEl, getRectCtrls } from './utils';\n\n/**\n * 默认的音频设置,⚠️ 不要变更它的值 ⚠️\n */\nconst DEFAULT_AUDIO_CONF = {\n sampleRate: 48000,\n channelCount: 2,\n codec: 'mp4a.40.2',\n} as const;\n\nfunction createInitCvsEl(resolution: IResolution): HTMLCanvasElement {\n const cvsEl = createEl('canvas') as HTMLCanvasElement;\n cvsEl.style.cssText = `\n width: 100%;\n height: 100%;\n display: block;\n touch-action: none;\n `;\n cvsEl.width = resolution.width;\n cvsEl.height = resolution.height;\n\n return cvsEl;\n}\n\n/**\n *\n * 一个可交互的画布,让用户添加各种素材,支持基础交互(拖拽、缩放、旋转、时间偏移)\n *\n * 用于在 Web 环境中实现视频剪辑、直播推流工作台功能\n *\n * @description\n *\n - 添加/删除素材(视频、音频、图片、文字)\n - 分割(裁剪)素材\n - 控制素材在视频中的空间属性(坐标、旋转、缩放)\n - 控制素材在视频中的时间属性(偏移、时长)\n - 实时预览播放\n - 纯浏览器环境生成视频\n\n * @see [直播录制](https://webav-tech.github.io/WebAV/demo/4_2-recorder-avcanvas)\n * @see [视频剪辑](https://webav-tech.github.io/WebAV/demo/6_4-video-editor)\n * @example\n * const avCvs = new AVCanvas(document.querySelector('#app'), {\n * bgColor: '#333',\n * width: 1920,\n * height: 1080,\n * });\n *\n */\nexport class AVCanvas {\n #cvsEl: HTMLCanvasElement;\n\n #spriteManager: SpriteManager;\n\n #cvsCtx: CanvasRenderingContext2D;\n\n #destroyed = false;\n\n #clears: Array<() => void> = [];\n #stopRender: () => void;\n\n #evtTool = new EventTool<{\n timeupdate: (time: number) => void;\n paused: () => void;\n playing: () => void;\n activeSpriteChange: (sprite: VisibleSprite | null) => void;\n }>();\n on = this.#evtTool.on;\n\n #opts;\n /**\n * 预览帧生成中\n */\n #waitingPreviewFrame = false;\n\n /**\n * 创建 `AVCanvas` 类的实例。\n * @param attchEl - 要添加画布的元素。\n * @param opts - 画布的选项\n * @param opts.bgColor - 画布的背景颜色。\n * @param opts.width - 画布的宽度。\n * @param opts.height - 画布的高度。\n */\n constructor(\n attchEl: HTMLElement,\n opts: {\n bgColor: string;\n } & IResolution,\n ) {\n this.#opts = opts;\n this.#cvsEl = createInitCvsEl(opts);\n const ctx = this.#cvsEl.getContext('2d', { alpha: false });\n if (ctx == null) throw Error('canvas context is null');\n this.#cvsCtx = ctx;\n const container = createEl('div');\n container.style.cssText = 'width: 100%; height: 100%; position: relative;';\n container.appendChild(this.#cvsEl);\n attchEl.appendChild(container);\n\n createEmptyOscillatorNode(this.#audioCtx).connect(this.#captureAudioDest);\n\n // 创建 this.#cvsEl 时自动设置 ctrlSize 初始值\n // 避免首次渲染时 ctrls 节点大小不符合期望,所以这里不需要它的返回值\n getRectCtrls(this.#cvsEl, { x: 0, y: 0, w: 0, h: 0 } as Rect);\n\n this.#spriteManager = new SpriteManager();\n\n this.#clears.push(\n // 鼠标样式、控制 sprite 依赖 activeSprite,\n // activeSprite 需要在他们之前监听到 mousedown 事件 (代码顺序需要靠前)\n activeSprite(this.#cvsEl, this.#spriteManager),\n renderCtrls(container, this.#cvsEl, this.#spriteManager),\n draggabelSprite(this.#cvsEl, this.#spriteManager, container),\n this.#spriteManager.on(ESpriteManagerEvt.AddSprite, (s) => {\n const { rect } = s;\n // 默认居中\n if (rect.x === 0 && rect.y === 0) {\n rect.x = (this.#cvsEl.width - rect.w) / 2;\n rect.y = (this.#cvsEl.height - rect.h) / 2;\n }\n }),\n EventTool.forwardEvent(this.#spriteManager, this.#evtTool, [\n ESpriteManagerEvt.ActiveSpriteChange,\n ]),\n );\n\n let lastRenderTime = this.#renderTime;\n let start = performance.now();\n let runCnt = 0;\n const expectFrameTime = 1000 / 30;\n this.#stopRender = workerTimer(() => {\n // workerTimer 会略快于真实时钟,使用真实时间(performance.now)作为基准\n // 跳过部分运行帧修正时间,避免导致音画不同步\n if ((performance.now() - start) / (expectFrameTime * runCnt) < 1) {\n return;\n }\n // 如果正在准备下一次预览画面,则暂时跳过渲染;避免多个素材之间来回 seek 导致闪烁\n if (this.#waitingPreviewFrame) return;\n\n runCnt += 1;\n this.#cvsCtx.fillStyle = opts.bgColor;\n this.#cvsCtx.fillRect(0, 0, this.#cvsEl.width, this.#cvsEl.height);\n this.#render();\n\n if (lastRenderTime !== this.#renderTime) {\n lastRenderTime = this.#renderTime;\n this.#evtTool.emit('timeupdate', Math.round(lastRenderTime));\n }\n }, expectFrameTime);\n\n // ;(window as any).cvsEl = this.#cvsEl\n }\n\n #renderTime = 0e6;\n #updateRenderTime(time: number) {\n this.#renderTime = time;\n this.#spriteManager.updateRenderTime(time);\n this.#autoPreFrame.updateTime(time);\n }\n\n #pause() {\n if (this.#playState.step === 0) return;\n this.#playState.step = 0;\n this.#evtTool.emit('paused');\n this.#audioCtx.suspend();\n for (const asn of this.#playingAudioCache) {\n asn.stop();\n asn.disconnect();\n }\n this.#playingAudioCache.clear();\n this.#autoPreFrame.reset();\n }\n\n #audioCtx = new AudioContext();\n #captureAudioDest = this.#audioCtx.createMediaStreamDestination();\n\n #playingAudioCache: Set<AudioBufferSourceNode> = new Set();\n #render() {\n const cvsCtx = this.#cvsCtx;\n let ts = this.#renderTime;\n const { start, end, step, audioPlayAt } = this.#playState;\n ts += step;\n if (step !== 0 && ts >= start && ts < end) {\n this.#updateRenderTime(ts);\n } else {\n this.#pause();\n }\n\n const ctxDestAudioData: Float32Array[][] = [];\n for (const s of this.#spriteManager.getSprites()) {\n cvsCtx.save();\n const { audio } = s.render(cvsCtx, ts - s.time.offset);\n cvsCtx.restore();\n\n ctxDestAudioData.push(audio);\n }\n cvsCtx.resetTransform();\n\n if (step !== 0) {\n const curAudioTime = Math.max(this.#audioCtx.currentTime, audioPlayAt);\n const audioSourceArr = convertPCM2AudioSource(\n ctxDestAudioData,\n this.#audioCtx,\n );\n\n let addTime = 0;\n for (const ads of audioSourceArr) {\n ads.start(curAudioTime);\n ads.connect(this.#audioCtx.destination);\n ads.connect(this.#captureAudioDest);\n\n this.#playingAudioCache.add(ads);\n ads.onended = () => {\n ads.disconnect();\n this.#playingAudioCache.delete(ads);\n };\n addTime = Math.max(addTime, ads.buffer?.duration ?? 0);\n }\n this.#playState.audioPlayAt = curAudioTime + addTime;\n }\n }\n\n #playState = {\n start: 0,\n end: 0,\n // paused state when step equal 0\n step: 0,\n // step: (1000 / 30) * 1000,\n audioPlayAt: 0,\n };\n /**\n * 每 33ms 更新一次画布,绘制已添加的 Sprite\n * @param opts - 播放选项\n * @param opts.start - 开始播放的时间(单位:微秒)\n * @param [opts.end] - 结束播放的时间(单位:微秒)。如果未指定,则播放到最后一个 Sprite 的结束时间\n * @param [opts.playbackRate] - 播放速率。1 表示正常速度,2 表示两倍速度,0.5 表示半速等。如果未指定,则默认为 1\n * @throws 如果开始时间大于等于结束时间或小于 0,则抛出错误\n */\n play(opts: { start: number; end?: number; playbackRate?: number }) {\n const spriteTimes = this.#spriteManager\n .getSprites({ time: false })\n .map((s) => s.time.offset + s.time.duration);\n const end =\n opts.end ??\n (spriteTimes.length > 0 ? Math.max(...spriteTimes) : Infinity);\n\n if (opts.start >= end || opts.start < 0) {\n throw Error(\n `Invalid time parameter, ${JSON.stringify({ start: opts.start, end })}`,\n );\n }\n\n this.#updateRenderTime(opts.start);\n this.#autoPreFrame.reset();\n\n this.#playState.start = opts.start;\n this.#playState.end = end;\n // AVCanvas 30FPS,将播放速率转换成步长\n this.#playState.step = (opts.playbackRate ?? 1) * (1000 / 30) * 1000;\n this.#audioCtx.resume();\n this.#playState.audioPlayAt = 0;\n\n this.#evtTool.emit('playing');\n Log.info('AVCanvs play by:', this.#playState);\n }\n\n #autoPreFrame = (() => {\n const readyVS = new Set<VisibleSprite>();\n return {\n reset() {\n readyVS.clear();\n },\n updateTime: throttle((curTime: number) => {\n const sprs = this.#spriteManager.getSprites({ time: false });\n // 匹配接下来 1s 内即将要播放的 Sprite\n const matchPreSprs = sprs.filter((vs) => {\n const { offset } = vs.time;\n return offset > curTime && offset - 1e6 <= curTime;\n });\n for (const vs of matchPreSprs) {\n if (!readyVS.has(vs)) vs.preFrame(0);\n readyVS.add(vs);\n }\n }, 500),\n };\n })();\n\n /**\n * 暂停播放,画布内容不再更新\n */\n pause() {\n this.#pause();\n }\n\n /**\n * 预览 `AVCanvas` 指定时间的图像帧\n */\n async previewFrame(time: number) {\n this.#pause();\n this.#updateRenderTime(time);\n this.#waitingPreviewFrame = true;\n try {\n await Promise.all(\n this.#spriteManager.getSprites({ time: false }).map((vs) => {\n if (\n time >= vs.time.offset &&\n time <= vs.time.offset + vs.time.duration\n ) {\n return vs.preFrame(time - vs.time.offset);\n }\n return null;\n }),\n );\n } finally {\n this.#waitingPreviewFrame = false;\n }\n }\n\n /**\n * 获取当前帧的截图图像 返回的是一个base64\n */\n captureImage(): string {\n return this.#cvsEl.toDataURL();\n }\n\n get activeSprite() {\n return this.#spriteManager.activeSprite;\n }\n set activeSprite(s: VisibleSprite | null) {\n this.#spriteManager.activeSprite = s;\n }\n\n #sprMapAudioNode = new WeakMap<VisibleSprite, AudioNode>();\n /**\n * 添加 {@link VisibleSprite}\n * @param args {@link VisibleSprite}\n * @example\n * const sprite = new VisibleSprite(\n * new ImgClip({\n * type: 'image/gif',\n * stream: (await fetch('https://xx.gif')).body!,\n * }),\n * );\n */\n addSprite: SpriteManager['addSprite'] = async (vs) => {\n if (this.#audioCtx.state === 'suspended')\n this.#audioCtx.resume().catch(Log.error);\n\n const clip = vs.getClip();\n if (clip instanceof MediaStreamClip && clip.audioTrack != null) {\n const audioNode = this.#audioCtx.createMediaStreamSource(\n new MediaStream([clip.audioTrack]),\n );\n audioNode.connect(this.#captureAudioDest);\n this.#sprMapAudioNode.set(vs, audioNode);\n }\n await this.#spriteManager.addSprite(vs);\n };\n /**\n * 删除 {@link VisibleSprite}\n * @param args\n * @returns\n * @example\n * const sprite = new VisibleSprite();\n * avCvs.removeSprite(sprite);\n */\n removeSprite: SpriteManager['removeSprite'] = (vs) => {\n this.#sprMapAudioNode.get(vs)?.disconnect();\n this.#spriteManager.removeSprite(vs);\n };\n\n /**\n * 销毁实例\n */\n destroy(): void {\n if (this.#destroyed) return;\n this.#destroyed = true;\n\n this.#audioCtx.close();\n this.#captureAudioDest.disconnect();\n this.#evtTool.destroy();\n this.#stopRender();\n this.#cvsEl.parentElement?.remove();\n this.#clears.forEach((fn) => fn());\n this.#playingAudioCache.clear();\n this.#spriteManager.destroy();\n }\n\n /**\n * 合成所有素材的图像与音频,返回实时媒体流 `MediaStream`\n *\n * 可用于 WebRTC 推流,或由 {@link [AVRecorder](../../av-recorder/classes/AVRecorder.html)} 录制生成视频文件\n *\n * @see [直播录制](https://webav-tech.github.io/WebAV/demo/4_2-recorder-avcanvas)\n *\n */\n captureStream(): MediaStream {\n if (this.#audioCtx.state === 'suspended') {\n this.#audioCtx.resume().catch(Log.error);\n }\n\n const ms = new MediaStream(\n this.#cvsEl\n .captureStream()\n .getTracks()\n .concat(this.#captureAudioDest.stream.getTracks()),\n );\n Log.info(\n 'AVCanvas.captureStream, tracks:',\n ms.getTracks().map((t) => t.kind),\n );\n return ms;\n }\n\n /**\n * 创建一个视频合成器 {@link [Combinator](../../av-cliper/classes/Combinator.html)} 实例,用于将当前画布添加的 Sprite 导出为视频文件流\n *\n * @param opts - 创建 Combinator 的可选参数\n * @throws 如果没有添加素材,会抛出错误\n *\n * @example\n * avCvs.createCombinator().output() // => ReadableStream\n *\n * @see [视频剪辑](https://webav-tech.github.io/WebAV/demo/6_4-video-editor)\n */\n async createCombinator(opts: ICombinatorOpts = {}) {\n Log.info('AVCanvas.createCombinator, opts:', opts);\n\n const com = new Combinator({ ...this.#opts, ...opts });\n const sprites = this.#spriteManager.getSprites({ time: false });\n if (sprites.length === 0) throw Error('No sprite added');\n\n for (const vs of sprites) {\n const os = new OffscreenSprite(vs.getClip());\n os.time = { ...vs.time };\n vs.copyStateTo(os);\n await com.addSprite(os);\n }\n return com;\n }\n}\n\nfunction convertPCM2AudioSource(pcmData: Float32Array[][], ctx: AudioContext) {\n const asArr: AudioBufferSourceNode[] = [];\n if (pcmData.length === 0) return asArr;\n\n for (const [chan0Buf, chan1Buf] of pcmData) {\n if (chan0Buf == null) continue;\n if (chan0Buf.length <= 0) continue;\n\n const buf = ctx.createBuffer(\n 2,\n chan0Buf.length,\n DEFAULT_AUDIO_CONF.sampleRate,\n );\n buf.copyToChannel(chan0Buf, 0);\n buf.copyToChannel(chan1Buf ?? chan0Buf, 1);\n const audioSource = ctx.createBufferSource();\n audioSource.buffer = buf;\n asArr.push(audioSource);\n }\n return asArr;\n}\n\n/**\n * 空背景音,让 dest 能持续收到音频数据,否则时间会异常偏移\n */\nfunction createEmptyOscillatorNode(ctx: AudioContext) {\n const osc = ctx.createOscillator();\n const real = new Float32Array([0, 0]);\n const imag = new Float32Array([0, 0]);\n const wave = ctx.createPeriodicWave(real, imag, {\n disableNormalization: true,\n });\n osc.setPeriodicWave(wave);\n osc.start();\n return osc;\n}\n"],"names":["CTRL_KEYS","createEl","tagName","rectGetterCache","getRectCtrls","cvsEl","rect","ctrlSize","entries","fisrtEntry","rectCtrlsGetter","w","sz","hfSz","hfW","hfH","rtSz","hfRtSz","Rect","cvsRatioCache","getCvsRatio","cvsRatio","ESpriteManagerEvt","SpriteManager","#sprites","#activeSprite","#evtTool","EventTool","s","x","y","vs","a","b","props","spr","filter","#renderTime","time","as","CloseSvg","renderCtrls","container","sprMng","observer","syncCtrlElPos","rectEl","ctrlsEl","lastActSprEvtClear","createRectAndCtrlEl","evt","cvsRect","offSprChange","k","d","h","angle","ctrlPosMap","key","el","pos","baseStyle","customStyle","activeSprite","onCvsMouseDown","offsetX","offsetY","ofx","ofy","draggabelSprite","startX","startY","startRect","refline","createRefline","onRectMouseDown","hitSpr","clientX","clientY","onMouseMove","clearWindowEvt","expectX","expectY","updateRectWithSafeMargin","offCtrlEvt","setupCtrlEvents","ctrlElements","ctrlEl","index","ctrlKey","rotateRect","cntMap2Outer","scaleRect","curStyles","curInitIdx","offPropsEvt","offActSprEvt","updateCursorStyle","debounce","oa","idx","sprRect","deltaX","deltaY","scaler","stretchScale","fixedRatioScale","diagonalAngle","incW","incH","incS","rotateAngle","minSize","newW","newH","newIncW","newIncH","newIncS","startS","minS","newX","newY","newCenterX","newCenterY","coefficient","outCnt","onMove","clear","cnt","left","top","value","newState","safeWidth","safeHeight","reflineBaseCSS","baseSettings","reflineSettings","lineWrap","reflineEls","lineEl","magneticDistance","retVal","minDist","activeReflines","reflineKey","prop","val","refVal","dist","DEFAULT_AUDIO_CONF","createInitCvsEl","resolution","AVCanvas","#cvsEl","#spriteManager","#cvsCtx","#destroyed","#clears","#stopRender","#opts","#waitingPreviewFrame","attchEl","opts","ctx","createEmptyOscillatorNode","#audioCtx","#captureAudioDest","lastRenderTime","start","runCnt","expectFrameTime","workerTimer","#render","#updateRenderTime","#autoPreFrame","#pause","#playState","asn","#playingAudioCache","cvsCtx","ts","end","step","audioPlayAt","ctxDestAudioData","audio","curAudioTime","audioSourceArr","convertPCM2AudioSource","addTime","ads","spriteTimes","Log","readyVS","throttle","curTime","matchPreSprs","offset","#sprMapAudioNode","clip","MediaStreamClip","audioNode","fn","ms","t","com","Combinator","sprites","os","OffscreenSprite","pcmData","asArr","chan0Buf","chan1Buf","buf","audioSource","osc","real","imag","wave"],"mappings":"2WAiCO,MAAMA,EAAY,CACvB,IACA,IACA,IACA,IACA,KACA,KACA,KACA,KACA,QACF,ECxCO,SAASC,EAASC,EAA8B,CACrD,OAAO,SAAS,cAAcA,CAAO,CACvC,CAEA,MAAMC,MAAsB,QAQrB,SAASC,EAAaC,EAA0BC,EAAY,CACjE,GAAIH,EAAgB,IAAIE,CAAK,EAC3B,OAAOF,EAAgB,IAAIE,CAAK,EAAGC,CAAI,EAGzC,IAAIC,EAAW,GACK,IAAI,eAAgBC,GAAY,CAClD,MAAMC,EAAaD,EAAQ,CAAC,EACxBC,GAAc,OAClBF,EAAW,IAAME,EAAW,YAAY,MAAQJ,EAAM,OACxD,CAAC,EACW,QAAQA,CAAK,EACzB,SAASK,EAAgBJ,EAAuB,CAC9C,KAAM,CAAE,EAAAK,EAAG,CAAA,EAAML,EAEXM,EAAKL,EAELM,EAAOD,EAAK,EACZE,EAAMH,EAAI,EACVI,EAAM,EAAI,EAEVC,EAAOJ,EAAK,IACZK,EAASD,EAAO,EAUtB,MAAO,CACL,GATWV,EAAK,iBACd,GACA,CACE,EAAG,IAAIY,OAAK,CAACL,EAAM,CAACE,EAAMF,EAAMD,EAAIA,EAAIN,CAAI,EAC5C,EAAG,IAAIY,EAAAA,KAAK,CAACL,EAAME,EAAMF,EAAMD,EAAIA,EAAIN,CAAI,EAC3C,EAAG,IAAIY,OAAK,CAACJ,EAAMD,EAAM,CAACA,EAAMD,EAAIA,EAAIN,CAAI,EAC5C,EAAG,IAAIY,EAAAA,KAAKJ,EAAMD,EAAM,CAACA,EAAMD,EAAIA,EAAIN,CAAI,CAAA,EAI/C,GAAI,IAAIY,EAAAA,KAAK,CAACJ,EAAMD,EAAM,CAACE,EAAMF,EAAMD,EAAIA,EAAIN,CAAI,EACnD,GAAI,IAAIY,OAAK,CAACJ,EAAMD,EAAME,EAAMF,EAAMD,EAAIA,EAAIN,CAAI,EAClD,GAAI,IAAIY,OAAKJ,EAAMD,EAAM,CAACE,EAAMF,EAAMD,EAAIA,EAAIN,CAAI,EAClD,GAAI,IAAIY,OAAKJ,EAAMD,EAAME,EAAMF,EAAMD,EAAIA,EAAIN,CAAI,EACjD,OAAQ,IAAIY,EAAAA,KAAK,CAACD,EAAQ,CAACF,EAAMH,EAAK,EAAIK,EAAQD,EAAMA,EAAMV,CAAI,CAAA,CAEtE,CACA,OAAAH,EAAgB,IAAIE,EAAOK,CAAe,EACnCA,EAAgBJ,CAAI,CAC7B,CAGA,MAAMa,MAAoB,QACnB,SAASC,EAAYf,EAAqC,CAC/D,GAAIc,EAAc,IAAId,CAAK,EACzB,OAAOc,EAAc,IAAId,CAAK,EAGhC,MAAMgB,EAAW,CACf,EAAGhB,EAAM,YAAcA,EAAM,MAC7B,EAAGA,EAAM,aAAeA,EAAM,MAAA,EAMhC,OAJiB,IAAI,eAAe,IAAM,CACxCgB,EAAS,EAAIhB,EAAM,YAAcA,EAAM,MACvCgB,EAAS,EAAIhB,EAAM,aAAeA,EAAM,MAC1C,CAAC,EACQ,QAAQA,CAAK,EACtBc,EAAc,IAAId,EAAOgB,CAAQ,EAC1BA,CACT,CC3EO,IAAKC,GAAAA,IACVA,EAAA,mBAAqB,qBACrBA,EAAA,UAAY,YAFFA,IAAAA,GAAA,CAAA,CAAA,EAKL,MAAMC,CAAc,CACzBC,GAA4B,CAAA,EAE5BC,GAAsC,KAEtCC,GAAW,IAAIC,EAAAA,UAKf,GAAK,KAAKD,GAAS,GAEnB,IAAI,cAAqC,CACvC,OAAO,KAAKD,EACd,CACA,IAAI,aAAaG,EAAyB,CACpCA,IAAM,KAAKH,IAAiBG,GAAG,eAAiB,aACpD,KAAKH,GAAgBG,EACrB,KAAKF,GAAS,KAAK,qBAAsCE,CAAC,EAC5D,CAEA,oBAAoBC,EAAWC,EAAiB,CAC9C,KAAK,aACH,KAAK,WAAA,EAEF,UACA,KACEF,GACCA,EAAE,SAAWA,EAAE,eAAiB,YAAcA,EAAE,KAAK,SAASC,EAAGC,CAAC,CAAA,GACjE,IACX,CAEA,MAAM,UAAUC,EAAkC,CAChD,MAAMA,EAAG,MACT,KAAKP,GAAS,KAAKO,CAAE,EACrB,KAAKP,GAAW,KAAKA,GAAS,KAAK,CAACQ,EAAGC,IAAMD,EAAE,OAASC,EAAE,MAAM,EAChEF,EAAG,GAAG,cAAgBG,GAAU,CAC1BA,EAAM,QAAU,OACpB,KAAKV,GAAW,KAAKA,GAAS,KAAK,CAACQ,EAAGC,IAAMD,EAAE,OAASC,EAAE,MAAM,EAClE,CAAC,EAED,KAAKP,GAAS,KAAK,YAA6BK,CAAE,CACpD,CAEA,aAAaI,EAA0B,CACjC,KAAKV,KAAkBU,IAAK,KAAK,aAAe,MACpD,KAAKX,GAAW,KAAKA,GAAS,OAAQI,GAAMA,IAAMO,CAAG,EACrDA,EAAI,QAAA,CACN,CAEA,WAAWC,EAA4B,CAAE,KAAM,IAAyB,CACtE,OAAO,KAAKZ,GAAS,OAClBI,GACCA,EAAE,UACDQ,EAAO,KACJ,KAAKC,IAAeT,EAAE,KAAK,QAC3B,KAAKS,IAAeT,EAAE,KAAK,OAASA,EAAE,KAAK,SAC3C,GAAA,CAEV,CAEAS,GAAc,EACd,iBAAiBC,EAAc,CAC7B,KAAKD,GAAcC,EAGnB,MAAMC,EAAK,KAAK,aAEdA,GAAM,OACLD,EAAOC,EAAG,KAAK,QAAUD,EAAOC,EAAG,KAAK,OAASA,EAAG,KAAK,YAE1D,KAAK,aAAe,KAExB,CAEA,SAAgB,CACd,KAAKb,GAAS,QAAA,EACd,KAAKF,GAAS,QAASI,GAAMA,EAAE,SAAS,EACxC,KAAKJ,GAAW,CAAA,CAClB,CACF,CCnFA,MAAMgB,EAAW;AAAA;AAAA;AAAA;AAAA,EAMV,SAASC,EACdC,EACArC,EACAsC,EACY,CACZ,MAAMtB,EAAWD,EAAYf,CAAK,EAC5BuC,EAAW,IAAI,eAAe,IAAM,CACpCD,EAAO,cAAgB,MAC3BE,EAAcF,EAAO,aAActC,EAAOyC,EAAQC,CAAO,CAC3D,CAAC,EACDH,EAAS,QAAQvC,CAAK,EAEtB,IAAI2C,EAAqB,IAAM,CAAC,EAChC,KAAM,CAAE,OAAAF,EAAQ,QAAAC,GAAYE,EAAoBP,CAAS,EAGzDI,EAAO,iBAAiB,cAAgBI,GAAQ,CAE9C,GAAI,OAAO,OAAOH,CAAO,EAAE,SAASG,EAAI,MAAqB,EAC3D,OAIF,MAAMC,EAAU9C,EAAM,sBAAA,EAChBwB,GAAKqB,EAAI,QAAUC,EAAQ,MAAQ9B,EAAS,EAC5CS,GAAKoB,EAAI,QAAUC,EAAQ,KAAO9B,EAAS,EACjDsB,EAAO,oBAAoBd,EAAGC,CAAC,CACjC,CAAC,EAED,MAAMsB,EAAeT,EAAO,GAAGrB,EAAkB,mBAAqBM,GAAM,CAG1E,GADAoB,EAAA,EACIpB,GAAK,KAAM,CACbkB,EAAO,MAAM,QAAU,OACvB,MACF,CACAD,EAAcjB,EAAGvB,EAAOyC,EAAQC,CAAO,EACvCC,EAAqBpB,EAAE,GAAG,cAAe,IAAM,CAC7CiB,EAAcjB,EAAGvB,EAAOyC,EAAQC,CAAO,CACzC,CAAC,CACH,CAAC,EAED,MAAO,IAAM,CACXH,EAAS,WAAA,EACTQ,EAAA,EACAN,EAAO,OAAA,EACPE,EAAA,CACF,CACF,CAEA,SAASC,EAAoBP,EAG3B,CACA,MAAMI,EAAS7C,EAAS,KAAK,EAC7B6C,EAAO,UAAU,IAAI,aAAa,EAClCA,EAAO,MAAM,QAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASvB,MAAMC,EAAU,OAAO,YACrB/C,EAAU,IAAKqD,GAAM,CACnB,MAAMC,EAAIrD,EAAS,KAAK,EACxB,OAAAqD,EAAE,UAAU,IAAI,YAAYD,CAAC,EAAE,EAC/BC,EAAE,MAAM,QAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAQND,IAAM,SAAW,YAAc,SAAS;AAAA;AAAA,QAG7C,CAACA,EAAGC,CAAC,CACd,CAAC,CAAA,EAGH,cAAO,OAAOP,CAAO,EAAE,QAASO,GAAMR,EAAO,YAAYQ,CAAC,CAAC,EAC3DZ,EAAU,YAAYI,CAAM,EACrB,CACL,OAAAA,EACA,QAAAC,CAAA,CAEJ,CAEA,SAASF,EACPjB,EACAvB,EACAyC,EACAC,EACM,CACN,GAAInB,EAAE,eAAiB,WAAY,CACjCkB,EAAO,MAAM,QAAU,OACvB,MACF,CACAA,EAAO,MAAM,QAAU,GAEvB,MAAMzB,EAAWD,EAAYf,CAAK,EAC5B,CAAE,EAAAwB,EAAG,EAAAC,EAAG,EAAAnB,EAAG,EAAA4C,EAAG,MAAAC,CAAA,EAAU5B,EAAE,KAEhC,OAAO,OAAOkB,EAAO,MAAO,CAC1B,KAAM,GAAGjB,EAAIR,EAAS,CAAC,KACvB,IAAK,GAAGS,EAAIT,EAAS,CAAC,KACtB,MAAO,GAAGV,EAAIU,EAAS,CAAC,KACxB,OAAQ,GAAGkC,EAAIlC,EAAS,CAAC,KACzB,UAAW,UAAUmC,CAAK,MAAA,CAC3B,EAED,MAAMC,EAAarD,EAAaC,EAAOuB,EAAE,IAAI,EAE7C,UAAWyB,KAAKN,EAAS,CACvB,MAAMW,EAAML,EACNM,EAAKZ,EAAQW,CAAG,EAChBE,EAAMH,EAAWC,CAAG,EAE1B,GAAIE,GAAO,KAAM,CACfD,EAAG,MAAM,QAAU,OACnB,QACF,CAEA,MAAME,EAAoC,CACxC,MAAO,GAAGD,EAAI,EAAIvC,EAAS,CAAC,KAC5B,OAAQ,GAAGuC,EAAI,EAAIvC,EAAS,CAAC,KAC7B,UAAW,aAAauC,EAAI,EAAIvC,EAAS,CAAC,OAAOuC,EAAI,EAAIvC,EAAS,CAAC,MACnE,KAAM,MACN,IAAK,KAAA,EAGP,IAAIyC,EAAsC,CAAE,QAAS,MAAA,EAGrD,OAFAH,EAAG,UAAY,GAEP/B,EAAE,aAAA,CACR,IAAK,cACHkC,EAAc,CACZ,QAAS,QACT,gBAAiB,OACjB,OAAQ,gBAAA,EAEV,MACF,IAAK,aACCJ,IAAQ,WACVI,EAAc,CACZ,QAAS,OACT,eAAgB,SAChB,WAAY,SACZ,gBAAiB,cACjB,OAAQ,MAAA,EAEVH,EAAG,UAAYnB,GAEjB,KAAA,CAGJ,OAAO,OAAOmB,EAAG,MAAOE,EAAWC,CAAW,CAChD,CACF,CCpKO,SAASC,EACd1D,EACAsC,EACY,CACZ,MAAMqB,EAAkBd,GAA0B,CAGhD,GAFIA,EAAI,SAAW,GAEdA,EAAI,SAA2B7C,EAAO,OAE3C,MAAMgB,EAAWD,EAAYf,CAAK,EAC5B,CAAE,QAAA4D,EAAS,QAAAC,CAAA,EAAYhB,EACvBiB,EAAMF,EAAU5C,EAAS,EACzB+C,EAAMF,EAAU7C,EAAS,EAE/BsB,EAAO,oBAAoBwB,EAAKC,CAAG,CACrC,EAEA,OAAA/D,EAAM,iBAAiB,cAAe2D,CAAc,EAE7C,IAAM,CACX3D,EAAM,oBAAoB,cAAe2D,CAAc,CACzD,CACF,CAKO,SAASK,EACdhE,EACAsC,EACAD,EACY,CACZ,IAAI4B,EAAS,EACTC,EAAS,EACTC,EAAyB,KAE7B,MAAMC,EAAUC,GAAcrE,EAAOqC,CAAS,EAGxCI,EAASJ,EAAU,cAAc,cAAc,EACrD,GAAI,CAACI,EAAQ,MAAM,MAAM,gCAAgC,EAGzD,MAAM6B,EAAmBzB,GAA0B,CACjD,MAAM0B,EAASjC,EAAO,aACtB,GACEO,EAAI,SAAW,GACf0B,GAAU,MACVA,EAAO,eAAiB,cAExB,OAEF,KAAM,CAAE,QAAAC,EAAS,QAAAC,CAAA,EAAY5B,EAE7B