@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 • 66.5 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 { RectCtrls } from './types';\n\nexport function createEl(tagName: string): HTMLElement {\n return document.createElement(tagName);\n}\n\n/**\n * 根据 canvas 创建该画布上的 Sprite 控制点生成器\n * 因为控制点的大小需要根据画布的大小动态调整\n */\nexport function createCtrlsGetter(cvsEl: HTMLCanvasElement) {\n let ctrlSize = 16;\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 return {\n rectCtrlsGetter,\n destroy: () => {\n cvsResizeOb.disconnect();\n },\n };\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) return;\n this.#activeSprite = s;\n this.#evtTool.emit(ESpriteManagerEvt.ActiveSpriteChange, s);\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 { CTRL_KEYS, ICvsRatio, RectCtrls, TCtrlKey } from '../types';\nimport { createEl } from '../utils';\nimport { VisibleSprite, Rect } from '@webav/av-cliper';\nimport { ESpriteManagerEvt, SpriteManager } from './sprite-manager';\n\nexport function renderCtrls(\n container: HTMLElement,\n cvsEl: HTMLCanvasElement,\n sprMng: SpriteManager,\n rectCtrlsGetter: (rect: Rect) => RectCtrls,\n): () => void {\n const cvsRatio = {\n w: cvsEl.clientWidth / cvsEl.width,\n h: cvsEl.clientHeight / cvsEl.height,\n };\n\n const observer = new ResizeObserver(() => {\n cvsRatio.w = cvsEl.clientWidth / cvsEl.width;\n cvsRatio.h = cvsEl.clientHeight / cvsEl.height;\n\n if (sprMng.activeSprite == null) return;\n syncCtrlElPos(\n sprMng.activeSprite,\n rectEl,\n ctrlsEl,\n cvsRatio,\n rectCtrlsGetter,\n );\n });\n\n observer.observe(cvsEl);\n\n let lastActSprEvtClear = () => {};\n const { rectEl, ctrlsEl } = createRectAndCtrlEl(container);\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, rectEl, ctrlsEl, cvsRatio, rectCtrlsGetter);\n lastActSprEvtClear = s.on('propsChange', () => {\n syncCtrlElPos(s, rectEl, ctrlsEl, cvsRatio, rectCtrlsGetter);\n });\n rectEl.style.display = '';\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.style.cssText = `\n position: absolute;\n pointer-events: none;\n border: 1px solid #eee;\n box-sizing: border-box;\n display: none;\n `;\n const ctrlsEl = Object.fromEntries(\n CTRL_KEYS.map((k) => {\n const d = createEl('div');\n d.style.cssText = `\n display: none;\n position: absolute;\n border: 1px solid #3ee; border-radius: 50%;\n box-sizing: border-box;\n background-color: #fff;\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 rectEl: HTMLElement,\n ctrlsEl: Record<TCtrlKey, HTMLElement>,\n cvsRatio: ICvsRatio,\n rectCtrlsGetter: (rect: Rect) => RectCtrls,\n): void {\n const { x, y, w, h, angle } = s.rect;\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 rotate: `${angle}rad`,\n });\n Object.entries(rectCtrlsGetter(s.rect)).forEach(([k, { x, y, w, h }]) => {\n // ctrl 是相对中心点定位的\n Object.assign(ctrlsEl[k as TCtrlKey].style, {\n display: 'block',\n left: '50%',\n top: '50%',\n width: `${w * cvsRatio.w}px`,\n height: `${h * cvsRatio.h}px`,\n // border 1px, 所以要 -1\n transform: `translate(${x * cvsRatio.w}px, ${y * cvsRatio.h}px)`,\n });\n });\n}\n","import { ESpriteManagerEvt, SpriteManager } from './sprite-manager';\nimport { ICvsRatio, IPoint, RectCtrls, TCtrlKey } from '../types';\nimport { VisibleSprite, Rect } from '@webav/av-cliper';\nimport { createEl } from '../utils';\n\n/**\n * 鼠标点击,激活 sprite\n */\nexport function activeSprite(\n cvsEl: HTMLCanvasElement,\n sprMng: SpriteManager,\n rectCtrlsGetter: (rect: Rect) => RectCtrls,\n): () => void {\n const cvsRatio = {\n w: cvsEl.clientWidth / cvsEl.width,\n h: cvsEl.clientHeight / cvsEl.height,\n };\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\n const onCvsMouseDown = (evt: MouseEvent): void => {\n if (evt.button !== 0) return;\n const { offsetX, offsetY } = evt;\n const ofx = offsetX / cvsRatio.w;\n const ofy = offsetY / cvsRatio.h;\n if (sprMng.activeSprite != null) {\n const [ctrlKey] =\n (Object.entries(rectCtrlsGetter(sprMng.activeSprite.rect)).find(\n ([, rect]) => rect.checkHit(ofx, ofy),\n ) as [TCtrlKey, Rect]) ?? [];\n if (ctrlKey != null) return;\n }\n sprMng.activeSprite =\n sprMng\n .getSprites()\n // 排在后面的层级更高\n .reverse()\n .find((s) => s.visible && s.rect.checkHit(ofx, ofy)) ?? null;\n };\n\n cvsEl.addEventListener('pointerdown', onCvsMouseDown);\n\n return () => {\n observer.disconnect();\n cvsEl.removeEventListener('pointerdown', onCvsMouseDown);\n };\n}\n\n/**\n * 让canvas中的sprite可以被拖拽移动\n */\nexport function draggabelSprite(\n cvsEl: HTMLCanvasElement,\n sprMng: SpriteManager,\n container: HTMLElement,\n rectCtrlsGetter: (rect: Rect) => RectCtrls,\n): () => void {\n const cvsRatio = {\n w: cvsEl.clientWidth / cvsEl.width,\n h: cvsEl.clientHeight / cvsEl.height,\n };\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\n let startX = 0;\n let startY = 0;\n let startRect: Rect | null = null;\n\n const refline = createRefline(cvsEl, container);\n\n let hitSpr: VisibleSprite | null = null;\n // sprMng.activeSprite 在 av-canvas.ts -> activeSprite 中被赋值\n const onCvsMouseDown = (evt: MouseEvent): void => {\n // 鼠标左键才能拖拽移动\n if (evt.button !== 0 || sprMng.activeSprite == null) return;\n hitSpr = sprMng.activeSprite;\n const { offsetX, offsetY, clientX, clientY } = evt;\n // 如果已有激活 sprite,先判定是否命中其 ctrls\n if (\n hitRectCtrls({\n rect: hitSpr.rect,\n offsetX,\n offsetY,\n clientX,\n clientY,\n cvsRatio,\n cvsEl,\n rectCtrlsGetter,\n })\n ) {\n // 命中 ctrl 是缩放 sprite,略过后续移动 sprite 逻辑\n return;\n }\n\n startRect = hitSpr.rect.clone();\n\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\n const onMouseMove = (evt: MouseEvent): void => {\n if (hitSpr == null || startRect == null) 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 cvsEl.addEventListener('pointerdown', onCvsMouseDown);\n\n const clearWindowEvt = (): void => {\n refline.hide();\n window.removeEventListener('pointermove', onMouseMove);\n window.removeEventListener('pointerup', clearWindowEvt);\n };\n\n return () => {\n observer.disconnect();\n refline.destroy();\n clearWindowEvt();\n cvsEl.removeEventListener('pointerdown', onCvsMouseDown);\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\nfunction hitRectCtrls({\n rect,\n cvsRatio,\n offsetX,\n offsetY,\n clientX,\n clientY,\n cvsEl,\n rectCtrlsGetter,\n}: {\n rect: Rect;\n cvsRatio: ICvsRatio;\n offsetX: number;\n offsetY: number;\n clientX: number;\n clientY: number;\n cvsEl: HTMLCanvasElement;\n rectCtrlsGetter: (rect: Rect) => RectCtrls;\n}): boolean {\n // 将鼠标点击偏移坐标映射成 canvas 坐,\n const ofx = offsetX / cvsRatio.w;\n const ofy = offsetY / cvsRatio.h;\n const [k] =\n (Object.entries(rectCtrlsGetter(rect)).find(([, rect]) =>\n rect.checkHit(ofx, ofy),\n ) as [TCtrlKey, Rect]) ?? [];\n\n if (k == null) return false;\n if (k === 'rotate') {\n rotateRect(rect, cntMap2Outer(rect.center, cvsRatio, cvsEl));\n } else {\n scaleRect({\n sprRect: rect,\n ctrlKey: k,\n startX: clientX,\n startY: clientY,\n cvsRatio,\n cvsEl,\n });\n }\n // 命中 ctrl 后续是缩放 sprite,略过移动 sprite 逻辑\n return true;\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 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 let reflineKey: keyof typeof reflineSettings;\n let correctionState = { x: false, y: false };\n for (reflineKey in reflineSettings) {\n const { prop, val } = reflineSettings[reflineKey].ref;\n if (correctionState[prop]) continue;\n\n const refVal = val(rect);\n if (\n Math.abs(rect[prop] - refVal) <= magneticDistance &&\n Math.abs(rect[prop] - (prop === 'x' ? expectX : expectY)) <=\n magneticDistance\n ) {\n retVal[prop] = refVal;\n reflineEls[reflineKey].style.display = 'block';\n correctionState[prop] = true;\n } else {\n reflineEls[reflineKey].style.display = 'none';\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\n/**\n * 根据当前位置(sprite & ctrls),动态调整鼠标样式\n */\nexport function dynamicCusor(\n cvsEl: HTMLCanvasElement,\n sprMng: SpriteManager,\n rectCtrlsGetter: (rect: Rect) => RectCtrls,\n): () => void {\n const cvsRatio = {\n w: cvsEl.clientWidth / cvsEl.width,\n h: cvsEl.clientHeight / cvsEl.height,\n };\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\n const cvsStyle = cvsEl.style;\n\n let actSpr = sprMng.activeSprite;\n sprMng.on(ESpriteManagerEvt.ActiveSpriteChange, (s) => {\n actSpr = s;\n if (s == null) cvsStyle.cursor = '';\n });\n // 鼠标按下时,在操作过程中,不需要变换鼠标样式\n let isMSDown = false;\n const onDown = ({ offsetX, offsetY }: MouseEvent): void => {\n isMSDown = true;\n // 将鼠标点击偏移坐标映射成 canvas 坐,\n const ofx = offsetX / cvsRatio.w;\n const ofy = offsetY / cvsRatio.h;\n // 直接选中 sprite 时,需要改变鼠标样式为 move\n if (actSpr?.rect.checkHit(ofx, ofy) === true && cvsStyle.cursor === '') {\n cvsStyle.cursor = 'move';\n }\n };\n const onWindowUp = (): void => {\n isMSDown = false;\n };\n\n // 八个 ctrl 点位对应的鼠标样式,构成循环\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 = { t: 0, rt: 1, r: 2, rb: 3, b: 4, lb: 5, l: 6, lt: 7 };\n\n const onMove = (evt: MouseEvent): void => {\n // 按下之后,不再变化,因为可能是在拖拽控制点\n if (actSpr == null || isMSDown) return;\n const { offsetX, offsetY } = evt;\n const ofx = offsetX / cvsRatio.w;\n const ofy = offsetY / cvsRatio.h;\n const [ctrlKey] =\n (Object.entries(rectCtrlsGetter(actSpr.rect)).find(([, rect]) =>\n rect.checkHit(ofx, ofy),\n ) as [TCtrlKey, Rect]) ?? [];\n\n if (ctrlKey != null) {\n if (ctrlKey === 'rotate') {\n cvsStyle.cursor = 'crosshair';\n return;\n }\n // 旋转后,控制点的箭头指向也需要修正\n const angle = actSpr.rect.angle;\n const oa = angle < 0 ? angle + 2 * Math.PI : angle;\n // 每个控制点的初始样式(idx) + 旋转角度导致的偏移,即为新鼠标样式\n // 每旋转45°,偏移+1,以此在curStyles中循环\n const idx =\n (curInitIdx[ctrlKey] + Math.floor((oa + Math.PI / 8) / (Math.PI / 4))) %\n 8;\n cvsStyle.cursor = curStyles[idx];\n return;\n }\n if (actSpr.rect.checkHit(ofx, ofy)) {\n cvsStyle.cursor = 'move';\n return;\n }\n // 未命中 ctrls、sprite,重置为默认鼠标样式\n cvsStyle.cursor = '';\n };\n\n cvsEl.addEventListener('pointermove', onMove);\n cvsEl.addEventListener('pointerdown', onDown);\n window.addEventListener('pointerup', onWindowUp);\n\n return () => {\n observer.disconnect();\n cvsEl.removeEventListener('pointermove', onMove);\n cvsEl.removeEventListener('pointerdown', onDown);\n window.removeEventListener('pointerup', onWindowUp);\n };\n}\n","import {\n Log,\n Combinator,\n OffscreenSprite,\n VisibleSprite,\n MediaStreamClip,\n ICombinatorOpts,\n} from '@webav/av-cliper';\nimport { renderCtrls } from './sprites/render-ctrl';\nimport { ESpriteManagerEvt, SpriteManager } from './sprites/sprite-manager';\nimport {\n activeSprite,\n draggabelSprite,\n dynamicCusor,\n} from './sprites/sprite-op';\nimport { IResolution } from './types';\nimport { createCtrlsGetter, createEl } from './utils';\nimport { workerTimer, EventTool } from '@webav/internal-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 * 创建 `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 =\n 'width: 100%; height: 100%; position: relative; overflow: hidden;';\n container.appendChild(this.#cvsEl);\n attchEl.appendChild(container);\n\n createEmptyOscillatorNode(this.#audioCtx).connect(this.#captureAudioDest);\n\n this.#spriteManager = new SpriteManager();\n\n const { rectCtrlsGetter, destroy: ctrlGetterDestroy } = createCtrlsGetter(\n this.#cvsEl,\n );\n this.#clears.push(\n ctrlGetterDestroy,\n // 鼠标样式、控制 sprite 依赖 activeSprite,\n // activeSprite 需要在他们之前监听到 mousedown 事件 (代码顺序需要靠前)\n activeSprite(this.#cvsEl, this.#spriteManager, rectCtrlsGetter),\n dynamicCusor(this.#cvsEl, this.#spriteManager, rectCtrlsGetter),\n draggabelSprite(\n this.#cvsEl,\n this.#spriteManager,\n container,\n rectCtrlsGetter,\n ),\n renderCtrls(container, this.#cvsEl, this.#spriteManager, rectCtrlsGetter),\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 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 }\n\n #pause() {\n const emitPaused = this.#playState.step !== 0;\n this.#playState.step = 0;\n if (emitPaused) {\n this.#evtTool.emit('paused');\n this.#audioCtx.suspend();\n }\n for (const asn of this.#playingAudioCache) {\n asn.stop();\n asn.disconnect();\n }\n this.#playingAudioCache.clear();\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 if (step !== 0 && ts >= start && ts < end) {\n ts += step;\n } else {\n this.#pause();\n }\n this.#updateRenderTime(ts);\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.#spriteManager.getSprites({ time: false }).forEach((vs) => {\n const { offset, duration } = vs.time;\n const selfOffset = this.#renderTime - offset;\n vs.preFrame(selfOffset > 0 && selfOffset < duration ? selfOffset : 0);\n });\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 /**\n * 暂停播放,画布内容不再更新\n */\n pause() {\n this.#pause();\n }\n\n /**\n * 预览 `AVCanvas` 指定时间的图像帧\n */\n previewFrame(time: number) {\n this.#updateRenderTime(time);\n this.#pause();\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","createCtrlsGetter","cvsEl","ctrlSize","cvsResizeOb","entries","fisrtEntry","rectCtrlsGetter","rect","w","sz","hfSz","hfW","hfH","rtSz","hfRtSz","Rect","ESpriteManagerEvt","SpriteManager","__privateAdd","_sprites","_activeSprite","_evtTool","EventTool","__publicField","__privateGet","_renderTime","s","__privateSet","vs","a","b","props","spr","filter","time","as","renderCtrls","container","sprMng","cvsRatio","observer","syncCtrlElPos","rectEl","ctrlsEl","lastActSprEvtClear","createRectAndCtrlEl","offSprChange","k","d","x","y","h","angle","activeSprite","onCvsMouseDown","evt","offsetX","offsetY","ofx","ofy","ctrlKey","draggabelSprite","startX","startY","startRect","refline","createRefline","hitSpr","clientX","clientY","hitRectCtrls","onMouseMove","clearWindowEvt","expectX","expectY","updateRectWithSafeMargin","scaleRect","sprRect","deltaX","deltaY","scaler","stretchScale","fixedRatioScale","diagonalAngle","incW","incH","incS","rotateAngle","minSize","newW","newH","newIncW","newIncH","newIncS","startS","minS","newX","newY","newCenterX","newCenterY","coefficient","rotateRect","cntMap2Outer","outCnt","onMove","clear","cnt","left","top","value","newState","safeWidth","safeHeight","reflineBaseCSS","baseSettings","reflineSettings","lineWrap","reflineEls","key","lineEl","magneticDistance","retVal","reflineKey","correctionState","prop","val","refVal","el","dynamicCusor","cvsStyle","actSpr","isMSDown","onDown","onWindowUp","curStyles","curInitIdx","oa","idx","DEFAULT_AUDIO_CONF","createInitCvsEl","resolution","AVCanvas","attchEl","opts","_AVCanvas_instances","_cvsEl","_spriteManager","_cvsCtx","_destroyed","_clears","_stopRender","_opts","_audioCtx","_captureAudioDest","_playingAudioCache","_playState","_sprMapAudioNode","Log","clip","MediaStreamClip","audioNode","_a","ctx","createEmptyOscillatorNode","ctrlGetterDestroy","lastRenderTime","start","runCnt","expectFrameTime","workerTimer","__privateMethod","render_fn","spriteTimes","end","updateRenderTime_fn","offset","duration","selfOffset","pause_fn","fn","ms","t","com","Combinator","sprites","os","OffscreenSprite","emitPaused","asn","cvsCtx","ts","step","audioPlayAt","ctxDestAudioData","audio","curAudioTime","audioSourceArr","convertPCM2AudioSource","addTime","ads","pcmData","asArr","chan0Buf","chan1Buf","buf","audioSource","osc","real","imag","wave"],"mappings":"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