@meframe/core 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +25 -22
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/VideoComposer.js +4 -0
- package/dist/stages/compose/VideoComposer.js.map +1 -1
- package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
- package/dist/stages/compose/text-renderers/basic-text-renderer.js +45 -20
- package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -1
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.d.ts +2 -0
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.d.ts.map +1 -1
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.js +78 -7
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.js.map +1 -1
- package/dist/stages/compose/text-utils/text-layout-cache.d.ts +7 -0
- package/dist/stages/compose/text-utils/text-layout-cache.d.ts.map +1 -0
- package/dist/stages/compose/text-utils/text-layout-cache.js +89 -0
- package/dist/stages/compose/text-utils/text-layout-cache.js.map +1 -0
- package/dist/utils/time-utils.d.ts.map +1 -1
- package/dist/utils/time-utils.js +10 -6
- package/dist/utils/time-utils.js.map +1 -1
- package/dist/workers/stages/export/{export.worker.Dztm6GuN.js → export.worker.CPqXBEVe.js} +205 -28
- package/dist/workers/stages/export/export.worker.CPqXBEVe.js.map +1 -0
- package/dist/workers/worker-manifest.json +1 -1
- package/package.json +1 -1
- package/dist/workers/stages/export/export.worker.Dztm6GuN.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"caption-stagger-entrance-renderer.js","sources":["../../../../src/stages/compose/text-renderers/caption-stagger-entrance-renderer.ts"],"sourcesContent":["import type { TextLayer } from '../types';\nimport { wrapText, formEvenLinesWithWords } from '../text-utils/text-wrapper';\nimport { getLetterCaseText, measureTextWidth } from '../text-utils/text-metrics';\nimport { needsSpaceBetweenWords } from '../text-utils/locale-detector';\n\n/** Matches medeo-web caption preview (`caption-anime-effects` + anime.js defaults). */\nconst DEFAULT_DURATION_MS = 800;\nconst DEFAULT_STAGGER_MS = 50;\nconst SLIDE_BASE_PX = 50;\nconst LETTER_SPREAD_BASE_PX = 20;\nconst BLUR_START_PX = 10;\nconst FONT_REF_PX = 40;\n\nfunction easeOutExpo(t: number): number {\n if (t <= 0) return 0;\n if (t >= 1) return 1;\n return 1 - Math.pow(2, -10 * t);\n}\n\nfunction calculateYPosition(\n canvasHeight: number,\n totalHeight: number,\n globalPosition?: {\n position?: 'absolute';\n top?: string;\n bottom?: string;\n left?: string;\n right?: string;\n display?: string;\n alignItems?: string;\n justifyContent?: string;\n }\n): number {\n if (!globalPosition) {\n return canvasHeight / 2 - totalHeight / 2;\n }\n if (globalPosition.top) {\n const topPercent = parseFloat(globalPosition.top) / 100;\n return canvasHeight * topPercent;\n }\n if (globalPosition.bottom) {\n const bottomPercent = parseFloat(globalPosition.bottom) / 100;\n return canvasHeight * (1 - bottomPercent) - totalHeight;\n }\n if (globalPosition.justifyContent === 'center' || globalPosition.alignItems === 'center') {\n return canvasHeight / 2 - totalHeight / 2;\n }\n return canvasHeight / 2 - totalHeight / 2;\n}\n\nexport function charProgress(\n relativeFrame: number,\n fps: number,\n charIndex: number,\n staggerMs: number,\n durationMs: number\n): number {\n const tMs = (relativeFrame / fps) * 1000;\n const startMs = charIndex * staggerMs;\n if (tMs <= startMs) return 0;\n const raw = (tMs - startMs) / durationMs;\n return easeOutExpo(Math.min(1, raw));\n}\n\nexport type CaptionStaggerPreset =\n | 'fade'\n | 'slideUp'\n | 'scale'\n | 'rotateScale'\n | 'blur'\n | 'flip3d'\n | 'typewriter'\n | 'letterSpread';\n\ninterface CharSlot {\n ch: string;\n x: number;\n y: number;\n globalIndex: number;\n}\n\nfunction buildCharSlots(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number\n): { slots: CharSlot[]; fontSize: number; lineHeight: number } | null {\n const fontConfig = layer.fontConfig?.textStyle;\n if (!fontConfig) return null;\n\n const fontSize = fontConfig.fontSize;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const lineHeight = fontConfig.lineHeight || 1.2;\n const maxWidth = canvasWidth * 0.64;\n const text = getLetterCaseText(layer.text, layer.letterCase);\n\n let lines: string[];\n if (layer.wordTimings && layer.wordTimings.length > 0) {\n const needsSpace = needsSpaceBetweenWords(layer.localeCode || 'en-US', text);\n const words = text.split(needsSpace ? /\\s+/ : '');\n lines = formEvenLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n } else {\n lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);\n }\n\n const totalHeight = lines.length * fontSize * lineHeight;\n const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n\n const slots: CharSlot[] = [];\n let globalIndex = 0;\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex]!;\n const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n let cx = canvasWidth / 2 - lineWidth / 2;\n\n for (const ch of Array.from(line)) {\n const cw = measureTextWidth(ctx, ch, fontSize, fontFamily, fontWeight);\n slots.push({\n ch,\n x: cx + cw / 2,\n y,\n globalIndex,\n });\n globalIndex++;\n cx += cw;\n }\n }\n\n ctx.restore();\n return { slots, fontSize, lineHeight };\n}\n\n/**\n * Per-character stagger entrance aligned with medeo-web preview (anime.js easeOutExpo, 800ms, 50ms stagger).\n */\nexport function renderCaptionStaggerEntrance(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n relativeFrame: number,\n fps: number,\n preset: CaptionStaggerPreset\n): void {\n const built = buildCharSlots(ctx, layer, canvasWidth, canvasHeight);\n if (!built) return;\n\n const { slots, fontSize } = built;\n const fontConfig = layer.fontConfig!.textStyle!;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const fill = fontConfig.fill;\n const stroke = fontConfig.stroke;\n const strokeWidth = fontConfig.strokeWidth || 0;\n\n const scalePx = fontSize / FONT_REF_PX;\n const staggerMs = DEFAULT_STAGGER_MS;\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n\n const tMsGlobal = (relativeFrame / fps) * 1000;\n\n for (const slot of slots) {\n let p: number;\n if (preset === 'typewriter') {\n const startMs = slot.globalIndex * staggerMs;\n p = tMsGlobal >= startMs ? 1 : 0;\n } else {\n p = charProgress(relativeFrame, fps, slot.globalIndex, staggerMs, DEFAULT_DURATION_MS);\n }\n if (p <= 0 && preset !== 'blur') continue;\n\n const slidePx = SLIDE_BASE_PX * scalePx;\n const spreadPx = LETTER_SPREAD_BASE_PX * scalePx;\n\n ctx.save();\n\n let opacity = p;\n let tx = 0;\n let ty = 0;\n let rot = 0;\n let sc = 1;\n let sy = 1;\n let blurPx = 0;\n\n switch (preset) {\n case 'fade':\n opacity = p;\n break;\n case 'slideUp':\n opacity = p;\n ty = (1 - p) * slidePx;\n break;\n case 'scale':\n opacity = p;\n sc = Math.max(0.04, p);\n break;\n case 'rotateScale':\n opacity = p;\n rot = ((1 - p) * 45 * Math.PI) / 180;\n sc = 0.5 + p * 0.5;\n break;\n case 'blur':\n opacity = p;\n blurPx = (1 - p) * BLUR_START_PX;\n break;\n case 'flip3d':\n opacity = p;\n sy = Math.max(0.04, p);\n sc = 1;\n break;\n case 'typewriter':\n opacity = p >= 1 ? 1 : p;\n break;\n case 'letterSpread':\n opacity = p;\n tx = (1 - p) * spreadPx * slot.globalIndex;\n break;\n default:\n break;\n }\n\n ctx.globalAlpha = opacity;\n if (blurPx > 0.01) {\n ctx.filter = `blur(${blurPx}px)`;\n }\n\n ctx.translate(slot.x + tx, slot.y + ty);\n ctx.rotate(rot);\n if (preset === 'flip3d') {\n ctx.scale(1, sy);\n } else {\n ctx.scale(sc, sc);\n }\n\n if (stroke && strokeWidth > 0) {\n ctx.strokeStyle = stroke;\n ctx.lineWidth = strokeWidth;\n ctx.strokeText(slot.ch, 0, 0);\n }\n ctx.fillStyle = fill;\n ctx.fillText(slot.ch, 0, 0);\n\n ctx.restore();\n }\n\n ctx.restore();\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"caption-stagger-entrance-renderer.js","sources":["../../../../src/stages/compose/text-renderers/caption-stagger-entrance-renderer.ts"],"sourcesContent":["import type { TextLayer } from '../types';\nimport { getCachedEvenLinesWithWords, getCachedWrapText } from '../text-utils/text-layout-cache';\nimport { getLetterCaseText, measureTextWidth } from '../text-utils/text-metrics';\nimport { needsSpaceBetweenWords } from '../text-utils/locale-detector';\n\n/** Matches medeo-web caption preview (`caption-anime-effects` + anime.js defaults). */\nconst DEFAULT_DURATION_MS = 800;\nconst DEFAULT_STAGGER_MS = 50;\nconst SLIDE_BASE_PX = 50;\nconst LETTER_SPREAD_BASE_PX = 20;\nconst BLUR_START_PX = 10;\nconst FONT_REF_PX = 40;\n\nfunction easeOutExpo(t: number): number {\n if (t <= 0) return 0;\n if (t >= 1) return 1;\n return 1 - Math.pow(2, -10 * t);\n}\n\nfunction calculateYPosition(\n canvasHeight: number,\n totalHeight: number,\n globalPosition?: {\n position?: 'absolute';\n top?: string;\n bottom?: string;\n left?: string;\n right?: string;\n display?: string;\n alignItems?: string;\n justifyContent?: string;\n }\n): number {\n if (!globalPosition) {\n return canvasHeight / 2 - totalHeight / 2;\n }\n if (globalPosition.top) {\n const topPercent = parseFloat(globalPosition.top) / 100;\n return canvasHeight * topPercent;\n }\n if (globalPosition.bottom) {\n const bottomPercent = parseFloat(globalPosition.bottom) / 100;\n return canvasHeight * (1 - bottomPercent) - totalHeight;\n }\n if (globalPosition.justifyContent === 'center' || globalPosition.alignItems === 'center') {\n return canvasHeight / 2 - totalHeight / 2;\n }\n return canvasHeight / 2 - totalHeight / 2;\n}\n\nexport function charProgress(\n relativeFrame: number,\n fps: number,\n charIndex: number,\n staggerMs: number,\n durationMs: number\n): number {\n const tMs = (relativeFrame / fps) * 1000;\n const startMs = charIndex * staggerMs;\n if (tMs <= startMs) return 0;\n const raw = (tMs - startMs) / durationMs;\n return easeOutExpo(Math.min(1, raw));\n}\n\nexport type CaptionStaggerPreset =\n | 'fade'\n | 'slideUp'\n | 'scale'\n | 'rotateScale'\n | 'blur'\n | 'flip3d'\n | 'typewriter'\n | 'letterSpread';\n\ninterface CharSlot {\n ch: string;\n x: number;\n y: number;\n globalIndex: number;\n}\n\ninterface CharSlotsLayout {\n slots: CharSlot[];\n fontSize: number;\n lineHeight: number;\n}\n\nconst charSlotsCache = new Map<string, CharSlotsLayout>();\nconst staggerFinalRasterCache = new Map<string, ImageBitmap>();\n\nfunction layoutCacheKey(layer: TextLayer, canvasWidth: number, canvasHeight: number): string {\n const ts = layer.fontConfig?.textStyle;\n return [\n layer.id,\n layer.text,\n layer.letterCase ?? '',\n canvasWidth,\n canvasHeight,\n ts?.fontSize,\n ts?.fontFamily,\n ts?.fontWeight,\n ts?.lineHeight,\n JSON.stringify(layer.fontConfig?.globalPosition ?? null),\n ].join('\\x1f');\n}\n\nfunction staggerRasterKey(\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n preset: CaptionStaggerPreset\n): string {\n return `${layoutCacheKey(layer, canvasWidth, canvasHeight)}\\x1f${preset}`;\n}\n\nexport function staggerEntranceEndMs(slotCount: number, preset: CaptionStaggerPreset): number {\n if (slotCount <= 0) return 0;\n const lastIndex = slotCount - 1;\n if (preset === 'typewriter') {\n return lastIndex * DEFAULT_STAGGER_MS + DEFAULT_STAGGER_MS;\n }\n return lastIndex * DEFAULT_STAGGER_MS + DEFAULT_DURATION_MS;\n}\n\nfunction buildCharSlots(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number\n): CharSlotsLayout | null {\n const fontConfig = layer.fontConfig?.textStyle;\n if (!fontConfig) return null;\n\n const fontSize = fontConfig.fontSize;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const lineHeight = fontConfig.lineHeight || 1.2;\n const maxWidth = canvasWidth * 0.64;\n const text = getLetterCaseText(layer.text, layer.letterCase);\n\n let lines: string[];\n if (layer.wordTimings && layer.wordTimings.length > 0) {\n const needsSpace = needsSpaceBetweenWords(layer.localeCode || 'en-US', text);\n const words = needsSpace ? text.split(/\\s+/) : Array.from(text);\n lines = getCachedEvenLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n } else {\n lines = getCachedWrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);\n }\n\n const totalHeight = lines.length * fontSize * lineHeight;\n const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n\n const slots: CharSlot[] = [];\n let globalIndex = 0;\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex]!;\n const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n let cx = canvasWidth / 2 - lineWidth / 2;\n\n for (const ch of Array.from(line)) {\n const cw = measureTextWidth(ctx, ch, fontSize, fontFamily, fontWeight);\n slots.push({\n ch,\n x: cx + cw / 2,\n y,\n globalIndex,\n });\n globalIndex++;\n cx += cw;\n }\n }\n\n ctx.restore();\n return { slots, fontSize, lineHeight };\n}\n\nfunction getCachedCharSlots(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number\n): CharSlotsLayout | null {\n const key = layoutCacheKey(layer, canvasWidth, canvasHeight);\n const cached = charSlotsCache.get(key);\n if (cached) {\n return cached;\n }\n const built = buildCharSlots(ctx, layer, canvasWidth, canvasHeight);\n if (built) {\n charSlotsCache.set(key, built);\n }\n return built;\n}\n\nfunction drawStaggerEntranceFrame(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n built: CharSlotsLayout,\n relativeFrame: number,\n fps: number,\n preset: CaptionStaggerPreset\n): void {\n const { slots, fontSize } = built;\n const fontConfig = layer.fontConfig!.textStyle!;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const fill = fontConfig.fill;\n const stroke = fontConfig.stroke;\n const strokeWidth = fontConfig.strokeWidth || 0;\n\n const scalePx = fontSize / FONT_REF_PX;\n const staggerMs = DEFAULT_STAGGER_MS;\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n\n const tMsGlobal = (relativeFrame / fps) * 1000;\n\n for (const slot of slots) {\n let p: number;\n if (preset === 'typewriter') {\n const startMs = slot.globalIndex * staggerMs;\n p = tMsGlobal >= startMs ? 1 : 0;\n } else {\n p = charProgress(relativeFrame, fps, slot.globalIndex, staggerMs, DEFAULT_DURATION_MS);\n }\n if (p <= 0 && preset !== 'blur') continue;\n\n const slidePx = SLIDE_BASE_PX * scalePx;\n const spreadPx = LETTER_SPREAD_BASE_PX * scalePx;\n\n ctx.save();\n\n let opacity = p;\n let tx = 0;\n let ty = 0;\n let rot = 0;\n let sc = 1;\n let sy = 1;\n let blurPx = 0;\n\n switch (preset) {\n case 'fade':\n opacity = p;\n break;\n case 'slideUp':\n opacity = p;\n ty = (1 - p) * slidePx;\n break;\n case 'scale':\n opacity = p;\n sc = Math.max(0.04, p);\n break;\n case 'rotateScale':\n opacity = p;\n rot = ((1 - p) * 45 * Math.PI) / 180;\n sc = 0.5 + p * 0.5;\n break;\n case 'blur':\n opacity = p;\n blurPx = (1 - p) * BLUR_START_PX;\n break;\n case 'flip3d':\n opacity = p;\n sy = Math.max(0.04, p);\n sc = 1;\n break;\n case 'typewriter':\n opacity = p >= 1 ? 1 : p;\n break;\n case 'letterSpread':\n opacity = p;\n tx = (1 - p) * spreadPx * slot.globalIndex;\n break;\n default:\n break;\n }\n\n ctx.globalAlpha = opacity;\n if (blurPx > 0.01) {\n ctx.filter = `blur(${blurPx}px)`;\n }\n\n ctx.translate(slot.x + tx, slot.y + ty);\n ctx.rotate(rot);\n if (preset === 'flip3d') {\n ctx.scale(1, sy);\n } else {\n ctx.scale(sc, sc);\n }\n\n if (stroke && strokeWidth > 0) {\n ctx.strokeStyle = stroke;\n ctx.lineWidth = strokeWidth;\n ctx.strokeText(slot.ch, 0, 0);\n }\n ctx.fillStyle = fill;\n ctx.fillText(slot.ch, 0, 0);\n\n ctx.restore();\n }\n\n ctx.restore();\n}\n\nexport function clearCaptionStaggerCache(): void {\n charSlotsCache.clear();\n for (const bitmap of staggerFinalRasterCache.values()) {\n bitmap.close();\n }\n staggerFinalRasterCache.clear();\n}\n\n/**\n * Per-character stagger entrance aligned with medeo-web preview (anime.js easeOutExpo, 800ms, 50ms stagger).\n */\nexport function renderCaptionStaggerEntrance(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n relativeFrame: number,\n fps: number,\n preset: CaptionStaggerPreset\n): void {\n const built = getCachedCharSlots(ctx, layer, canvasWidth, canvasHeight);\n if (!built) return;\n\n const endMs = staggerEntranceEndMs(built.slots.length, preset);\n const endFrame = Math.ceil((endMs / 1000) * fps);\n\n if (relativeFrame >= endFrame) {\n const rasterKey = staggerRasterKey(layer, canvasWidth, canvasHeight, preset);\n let bitmap = staggerFinalRasterCache.get(rasterKey);\n if (!bitmap) {\n const offscreen = new OffscreenCanvas(canvasWidth, canvasHeight);\n const offCtx = offscreen.getContext('2d');\n if (!offCtx) {\n drawStaggerEntranceFrame(ctx, layer, built, endFrame, fps, preset);\n return;\n }\n drawStaggerEntranceFrame(offCtx, layer, built, endFrame, fps, preset);\n bitmap = offscreen.transferToImageBitmap();\n staggerFinalRasterCache.set(rasterKey, bitmap);\n }\n ctx.drawImage(bitmap, 0, 0);\n return;\n }\n\n drawStaggerEntranceFrame(ctx, layer, built, relativeFrame, fps, preset);\n}\n"],"names":[],"mappings":";;;AAMA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AACtB,MAAM,wBAAwB;AAC9B,MAAM,gBAAgB;AACtB,MAAM,cAAc;AAEpB,SAAS,YAAY,GAAmB;AACtC,MAAI,KAAK,EAAG,QAAO;AACnB,MAAI,KAAK,EAAG,QAAO;AACnB,SAAO,IAAI,KAAK,IAAI,GAAG,MAAM,CAAC;AAChC;AAEA,SAAS,mBACP,cACA,aACA,gBAUQ;AACR,MAAI,CAAC,gBAAgB;AACnB,WAAO,eAAe,IAAI,cAAc;AAAA,EAC1C;AACA,MAAI,eAAe,KAAK;AACtB,UAAM,aAAa,WAAW,eAAe,GAAG,IAAI;AACpD,WAAO,eAAe;AAAA,EACxB;AACA,MAAI,eAAe,QAAQ;AACzB,UAAM,gBAAgB,WAAW,eAAe,MAAM,IAAI;AAC1D,WAAO,gBAAgB,IAAI,iBAAiB;AAAA,EAC9C;AACA,MAAI,eAAe,mBAAmB,YAAY,eAAe,eAAe,UAAU;AACxF,WAAO,eAAe,IAAI,cAAc;AAAA,EAC1C;AACA,SAAO,eAAe,IAAI,cAAc;AAC1C;AAEO,SAAS,aACd,eACA,KACA,WACA,WACA,YACQ;AACR,QAAM,MAAO,gBAAgB,MAAO;AACpC,QAAM,UAAU,YAAY;AAC5B,MAAI,OAAO,QAAS,QAAO;AAC3B,QAAM,OAAO,MAAM,WAAW;AAC9B,SAAO,YAAY,KAAK,IAAI,GAAG,GAAG,CAAC;AACrC;AAyBA,MAAM,qCAAqB,IAAA;AAC3B,MAAM,8CAA8B,IAAA;AAEpC,SAAS,eAAe,OAAkB,aAAqB,cAA8B;AAC3F,QAAM,KAAK,MAAM,YAAY;AAC7B,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM,cAAc;AAAA,IACpB;AAAA,IACA;AAAA,IACA,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,KAAK,UAAU,MAAM,YAAY,kBAAkB,IAAI;AAAA,EAAA,EACvD,KAAK,GAAM;AACf;AAEA,SAAS,iBACP,OACA,aACA,cACA,QACQ;AACR,SAAO,GAAG,eAAe,OAAO,aAAa,YAAY,CAAC,IAAO,MAAM;AACzE;AAEO,SAAS,qBAAqB,WAAmB,QAAsC;AAC5F,MAAI,aAAa,EAAG,QAAO;AAC3B,QAAM,YAAY,YAAY;AAC9B,MAAI,WAAW,cAAc;AAC3B,WAAO,YAAY,qBAAqB;AAAA,EAC1C;AACA,SAAO,YAAY,qBAAqB;AAC1C;AAEA,SAAS,eACP,KACA,OACA,aACA,cACwB;AACxB,QAAM,aAAa,MAAM,YAAY;AACrC,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,WAAW,WAAW;AAC5B,QAAM,aAAa,WAAW;AAC9B,QAAM,aAAa,WAAW;AAC9B,QAAM,aAAa,WAAW,cAAc;AAC5C,QAAM,WAAW,cAAc;AAC/B,QAAM,OAAO,kBAAkB,MAAM,MAAM,MAAM,UAAU;AAE3D,MAAI;AACJ,MAAI,MAAM,eAAe,MAAM,YAAY,SAAS,GAAG;AACrD,UAAM,aAAa,uBAAuB,MAAM,cAAc,SAAS,IAAI;AAC3E,UAAM,QAAQ,aAAa,KAAK,MAAM,KAAK,IAAI,MAAM,KAAK,IAAI;AAC9D,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ,OAAO;AACL,YAAQ,kBAAkB,KAAK,MAAM,UAAU,UAAU,YAAY,UAAU;AAAA,EACjF;AAEA,QAAM,cAAc,MAAM,SAAS,WAAW;AAC9C,QAAM,SAAS,mBAAmB,cAAc,aAAa,MAAM,YAAY,cAAc;AAE7F,MAAI,KAAA;AACJ,MAAI,OAAO,GAAG,UAAU,IAAI,QAAQ,MAAM,UAAU;AAEpD,QAAM,QAAoB,CAAA;AAC1B,MAAI,cAAc;AAElB,WAAS,YAAY,GAAG,YAAY,MAAM,QAAQ,aAAa;AAC7D,UAAM,OAAO,MAAM,SAAS;AAC5B,UAAM,IAAI,SAAS,YAAY,WAAW,aAAa,WAAW;AAClE,UAAM,YAAY,iBAAiB,KAAK,MAAM,UAAU,YAAY,UAAU;AAC9E,QAAI,KAAK,cAAc,IAAI,YAAY;AAEvC,eAAW,MAAM,MAAM,KAAK,IAAI,GAAG;AACjC,YAAM,KAAK,iBAAiB,KAAK,IAAI,UAAU,YAAY,UAAU;AACrE,YAAM,KAAK;AAAA,QACT;AAAA,QACA,GAAG,KAAK,KAAK;AAAA,QACb;AAAA,QACA;AAAA,MAAA,CACD;AACD;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,QAAA;AACJ,SAAO,EAAE,OAAO,UAAU,WAAA;AAC5B;AAEA,SAAS,mBACP,KACA,OACA,aACA,cACwB;AACxB,QAAM,MAAM,eAAe,OAAO,aAAa,YAAY;AAC3D,QAAM,SAAS,eAAe,IAAI,GAAG;AACrC,MAAI,QAAQ;AACV,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,eAAe,KAAK,OAAO,aAAa,YAAY;AAClE,MAAI,OAAO;AACT,mBAAe,IAAI,KAAK,KAAK;AAAA,EAC/B;AACA,SAAO;AACT;AAEA,SAAS,yBACP,KACA,OACA,OACA,eACA,KACA,QACM;AACN,QAAM,EAAE,OAAO,SAAA,IAAa;AAC5B,QAAM,aAAa,MAAM,WAAY;AACrC,QAAM,aAAa,WAAW;AAC9B,QAAM,aAAa,WAAW;AAC9B,QAAM,OAAO,WAAW;AACxB,QAAM,SAAS,WAAW;AAC1B,QAAM,cAAc,WAAW,eAAe;AAE9C,QAAM,UAAU,WAAW;AAC3B,QAAM,YAAY;AAElB,MAAI,KAAA;AACJ,MAAI,OAAO,GAAG,UAAU,IAAI,QAAQ,MAAM,UAAU;AACpD,MAAI,YAAY;AAChB,MAAI,eAAe;AACnB,MAAI,WAAW;AACf,MAAI,UAAU;AAEd,QAAM,YAAa,gBAAgB,MAAO;AAE1C,aAAW,QAAQ,OAAO;AACxB,QAAI;AACJ,QAAI,WAAW,cAAc;AAC3B,YAAM,UAAU,KAAK,cAAc;AACnC,UAAI,aAAa,UAAU,IAAI;AAAA,IACjC,OAAO;AACL,UAAI,aAAa,eAAe,KAAK,KAAK,aAAa,WAAW,mBAAmB;AAAA,IACvF;AACA,QAAI,KAAK,KAAK,WAAW,OAAQ;AAEjC,UAAM,UAAU,gBAAgB;AAChC,UAAM,WAAW,wBAAwB;AAEzC,QAAI,KAAA;AAEJ,QAAI,UAAU;AACd,QAAI,KAAK;AACT,QAAI,KAAK;AACT,QAAI,MAAM;AACV,QAAI,KAAK;AACT,QAAI,KAAK;AACT,QAAI,SAAS;AAEb,YAAQ,QAAA;AAAA,MACN,KAAK;AACH,kBAAU;AACV;AAAA,MACF,KAAK;AACH,kBAAU;AACV,cAAM,IAAI,KAAK;AACf;AAAA,MACF,KAAK;AACH,kBAAU;AACV,aAAK,KAAK,IAAI,MAAM,CAAC;AACrB;AAAA,MACF,KAAK;AACH,kBAAU;AACV,eAAQ,IAAI,KAAK,KAAK,KAAK,KAAM;AACjC,aAAK,MAAM,IAAI;AACf;AAAA,MACF,KAAK;AACH,kBAAU;AACV,kBAAU,IAAI,KAAK;AACnB;AAAA,MACF,KAAK;AACH,kBAAU;AACV,aAAK,KAAK,IAAI,MAAM,CAAC;AACrB,aAAK;AACL;AAAA,MACF,KAAK;AACH,kBAAU,KAAK,IAAI,IAAI;AACvB;AAAA,MACF,KAAK;AACH,kBAAU;AACV,cAAM,IAAI,KAAK,WAAW,KAAK;AAC/B;AAAA,IAEA;AAGJ,QAAI,cAAc;AAClB,QAAI,SAAS,MAAM;AACjB,UAAI,SAAS,QAAQ,MAAM;AAAA,IAC7B;AAEA,QAAI,UAAU,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE;AACtC,QAAI,OAAO,GAAG;AACd,QAAI,WAAW,UAAU;AACvB,UAAI,MAAM,GAAG,EAAE;AAAA,IACjB,OAAO;AACL,UAAI,MAAM,IAAI,EAAE;AAAA,IAClB;AAEA,QAAI,UAAU,cAAc,GAAG;AAC7B,UAAI,cAAc;AAClB,UAAI,YAAY;AAChB,UAAI,WAAW,KAAK,IAAI,GAAG,CAAC;AAAA,IAC9B;AACA,QAAI,YAAY;AAChB,QAAI,SAAS,KAAK,IAAI,GAAG,CAAC;AAE1B,QAAI,QAAA;AAAA,EACN;AAEA,MAAI,QAAA;AACN;AAEO,SAAS,2BAAiC;AAC/C,iBAAe,MAAA;AACf,aAAW,UAAU,wBAAwB,UAAU;AACrD,WAAO,MAAA;AAAA,EACT;AACA,0BAAwB,MAAA;AAC1B;AAKO,SAAS,6BACd,KACA,OACA,aACA,cACA,eACA,KACA,QACM;AACN,QAAM,QAAQ,mBAAmB,KAAK,OAAO,aAAa,YAAY;AACtE,MAAI,CAAC,MAAO;AAEZ,QAAM,QAAQ,qBAAqB,MAAM,MAAM,QAAQ,MAAM;AAC7D,QAAM,WAAW,KAAK,KAAM,QAAQ,MAAQ,GAAG;AAE/C,MAAI,iBAAiB,UAAU;AAC7B,UAAM,YAAY,iBAAiB,OAAO,aAAa,cAAc,MAAM;AAC3E,QAAI,SAAS,wBAAwB,IAAI,SAAS;AAClD,QAAI,CAAC,QAAQ;AACX,YAAM,YAAY,IAAI,gBAAgB,aAAa,YAAY;AAC/D,YAAM,SAAS,UAAU,WAAW,IAAI;AACxC,UAAI,CAAC,QAAQ;AACX,iCAAyB,KAAK,OAAO,OAAO,UAAU,KAAK,MAAM;AACjE;AAAA,MACF;AACA,+BAAyB,QAAQ,OAAO,OAAO,UAAU,KAAK,MAAM;AACpE,eAAS,UAAU,sBAAA;AACnB,8BAAwB,IAAI,WAAW,MAAM;AAAA,IAC/C;AACA,QAAI,UAAU,QAAQ,GAAG,CAAC;AAC1B;AAAA,EACF;AAEA,2BAAyB,KAAK,OAAO,OAAO,eAAe,KAAK,MAAM;AACxE;"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TextLayer } from '../types';
|
|
2
|
+
|
|
3
|
+
export declare function getCachedWrapText(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, text: string, maxWidth: number, fontSize: number, fontFamily: string, fontWeight?: string | number): string[];
|
|
4
|
+
export declare function getCachedEvenLinesWithWords(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, words: string[], maxWidth: number, fontSize: number, needsSpace: boolean, fontFamily: string, fontWeight?: string | number): string[];
|
|
5
|
+
export declare function getCachedBasicTextRaster(layer: TextLayer, canvasWidth: number, canvasHeight: number, draw: (ctx: OffscreenCanvasRenderingContext2D) => void): ImageBitmap | null;
|
|
6
|
+
export declare function clearTextLayoutCache(): void;
|
|
7
|
+
//# sourceMappingURL=text-layout-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-layout-cache.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-utils/text-layout-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAU1C,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CAQV;AAED,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CAuBV;AAuBD,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,CAAC,GAAG,EAAE,iCAAiC,KAAK,IAAI,GACrD,WAAW,GAAG,IAAI,CAiBpB;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAO3C"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { formEvenLinesWithWords, wrapText } from "./text-wrapper.js";
|
|
2
|
+
const wrapTextCache = /* @__PURE__ */ new Map();
|
|
3
|
+
const evenLinesCache = /* @__PURE__ */ new Map();
|
|
4
|
+
function cacheKey(parts) {
|
|
5
|
+
return parts.join("");
|
|
6
|
+
}
|
|
7
|
+
function getCachedWrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
|
|
8
|
+
const key = cacheKey([text, maxWidth, fontSize, fontFamily, fontWeight]);
|
|
9
|
+
let lines = wrapTextCache.get(key);
|
|
10
|
+
if (!lines) {
|
|
11
|
+
lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
|
|
12
|
+
wrapTextCache.set(key, lines);
|
|
13
|
+
}
|
|
14
|
+
return lines;
|
|
15
|
+
}
|
|
16
|
+
function getCachedEvenLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
|
|
17
|
+
const key = cacheKey([
|
|
18
|
+
words.join(""),
|
|
19
|
+
maxWidth,
|
|
20
|
+
fontSize,
|
|
21
|
+
needsSpace,
|
|
22
|
+
fontFamily,
|
|
23
|
+
fontWeight
|
|
24
|
+
]);
|
|
25
|
+
let lines = evenLinesCache.get(key);
|
|
26
|
+
if (!lines) {
|
|
27
|
+
lines = formEvenLinesWithWords(
|
|
28
|
+
ctx,
|
|
29
|
+
words,
|
|
30
|
+
maxWidth,
|
|
31
|
+
fontSize,
|
|
32
|
+
needsSpace,
|
|
33
|
+
fontFamily,
|
|
34
|
+
fontWeight
|
|
35
|
+
);
|
|
36
|
+
evenLinesCache.set(key, lines);
|
|
37
|
+
}
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
const basicTextRasterCache = /* @__PURE__ */ new Map();
|
|
41
|
+
function basicTextRasterKey(layer, canvasWidth, canvasHeight) {
|
|
42
|
+
const ts = layer.fontConfig?.textStyle;
|
|
43
|
+
return [
|
|
44
|
+
layer.id,
|
|
45
|
+
layer.text,
|
|
46
|
+
layer.letterCase ?? "",
|
|
47
|
+
canvasWidth,
|
|
48
|
+
canvasHeight,
|
|
49
|
+
ts?.fontSize,
|
|
50
|
+
ts?.fontFamily,
|
|
51
|
+
ts?.fontWeight,
|
|
52
|
+
ts?.fill,
|
|
53
|
+
ts?.stroke,
|
|
54
|
+
ts?.strokeWidth,
|
|
55
|
+
ts?.lineHeight,
|
|
56
|
+
JSON.stringify(layer.fontConfig?.globalPosition ?? null)
|
|
57
|
+
].join("");
|
|
58
|
+
}
|
|
59
|
+
function getCachedBasicTextRaster(layer, canvasWidth, canvasHeight, draw) {
|
|
60
|
+
const key = basicTextRasterKey(layer, canvasWidth, canvasHeight);
|
|
61
|
+
const cached = basicTextRasterCache.get(key);
|
|
62
|
+
if (cached) {
|
|
63
|
+
return cached;
|
|
64
|
+
}
|
|
65
|
+
const offscreen = new OffscreenCanvas(canvasWidth, canvasHeight);
|
|
66
|
+
const offCtx = offscreen.getContext("2d");
|
|
67
|
+
if (!offCtx) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
draw(offCtx);
|
|
71
|
+
const bitmap = offscreen.transferToImageBitmap();
|
|
72
|
+
basicTextRasterCache.set(key, bitmap);
|
|
73
|
+
return bitmap;
|
|
74
|
+
}
|
|
75
|
+
function clearTextLayoutCache() {
|
|
76
|
+
wrapTextCache.clear();
|
|
77
|
+
evenLinesCache.clear();
|
|
78
|
+
for (const bitmap of basicTextRasterCache.values()) {
|
|
79
|
+
bitmap.close();
|
|
80
|
+
}
|
|
81
|
+
basicTextRasterCache.clear();
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
clearTextLayoutCache,
|
|
85
|
+
getCachedBasicTextRaster,
|
|
86
|
+
getCachedEvenLinesWithWords,
|
|
87
|
+
getCachedWrapText
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=text-layout-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-layout-cache.js","sources":["../../../../src/stages/compose/text-utils/text-layout-cache.ts"],"sourcesContent":["import type { TextLayer } from '../types';\nimport { formEvenLinesWithWords, wrapText } from './text-wrapper';\n\nconst wrapTextCache = new Map<string, string[]>();\nconst evenLinesCache = new Map<string, string[]>();\n\nfunction cacheKey(parts: (string | number | boolean)[]): string {\n return parts.join('\\x1f');\n}\n\nexport function getCachedWrapText(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n const key = cacheKey([text, maxWidth, fontSize, fontFamily, fontWeight]);\n let lines = wrapTextCache.get(key);\n if (!lines) {\n lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);\n wrapTextCache.set(key, lines);\n }\n return lines;\n}\n\nexport function getCachedEvenLinesWithWords(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n words: string[],\n maxWidth: number,\n fontSize: number,\n needsSpace: boolean,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n const key = cacheKey([\n words.join('\\x1e'),\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight,\n ]);\n let lines = evenLinesCache.get(key);\n if (!lines) {\n lines = formEvenLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n evenLinesCache.set(key, lines);\n }\n return lines;\n}\n\nconst basicTextRasterCache = new Map<string, ImageBitmap>();\n\nfunction basicTextRasterKey(layer: TextLayer, canvasWidth: number, canvasHeight: number): string {\n const ts = layer.fontConfig?.textStyle;\n return [\n layer.id,\n layer.text,\n layer.letterCase ?? '',\n canvasWidth,\n canvasHeight,\n ts?.fontSize,\n ts?.fontFamily,\n ts?.fontWeight,\n ts?.fill,\n ts?.stroke,\n ts?.strokeWidth,\n ts?.lineHeight,\n JSON.stringify(layer.fontConfig?.globalPosition ?? null),\n ].join('\\x1f');\n}\n\nexport function getCachedBasicTextRaster(\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n draw: (ctx: OffscreenCanvasRenderingContext2D) => void\n): ImageBitmap | null {\n const key = basicTextRasterKey(layer, canvasWidth, canvasHeight);\n const cached = basicTextRasterCache.get(key);\n if (cached) {\n return cached;\n }\n\n const offscreen = new OffscreenCanvas(canvasWidth, canvasHeight);\n const offCtx = offscreen.getContext('2d');\n if (!offCtx) {\n return null;\n }\n\n draw(offCtx);\n const bitmap = offscreen.transferToImageBitmap();\n basicTextRasterCache.set(key, bitmap);\n return bitmap;\n}\n\nexport function clearTextLayoutCache(): void {\n wrapTextCache.clear();\n evenLinesCache.clear();\n for (const bitmap of basicTextRasterCache.values()) {\n bitmap.close();\n }\n basicTextRasterCache.clear();\n}\n"],"names":[],"mappings":";AAGA,MAAM,oCAAoB,IAAA;AAC1B,MAAM,qCAAqB,IAAA;AAE3B,SAAS,SAAS,OAA8C;AAC9D,SAAO,MAAM,KAAK,GAAM;AAC1B;AAEO,SAAS,kBACd,KACA,MACA,UACA,UACA,YACA,aAA8B,KACpB;AACV,QAAM,MAAM,SAAS,CAAC,MAAM,UAAU,UAAU,YAAY,UAAU,CAAC;AACvE,MAAI,QAAQ,cAAc,IAAI,GAAG;AACjC,MAAI,CAAC,OAAO;AACV,YAAQ,SAAS,KAAK,MAAM,UAAU,UAAU,YAAY,UAAU;AACtE,kBAAc,IAAI,KAAK,KAAK;AAAA,EAC9B;AACA,SAAO;AACT;AAEO,SAAS,4BACd,KACA,OACA,UACA,UACA,YACA,YACA,aAA8B,KACpB;AACV,QAAM,MAAM,SAAS;AAAA,IACnB,MAAM,KAAK,GAAM;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AACD,MAAI,QAAQ,eAAe,IAAI,GAAG;AAClC,MAAI,CAAC,OAAO;AACV,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAEF,mBAAe,IAAI,KAAK,KAAK;AAAA,EAC/B;AACA,SAAO;AACT;AAEA,MAAM,2CAA2B,IAAA;AAEjC,SAAS,mBAAmB,OAAkB,aAAqB,cAA8B;AAC/F,QAAM,KAAK,MAAM,YAAY;AAC7B,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM,cAAc;AAAA,IACpB;AAAA,IACA;AAAA,IACA,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,KAAK,UAAU,MAAM,YAAY,kBAAkB,IAAI;AAAA,EAAA,EACvD,KAAK,GAAM;AACf;AAEO,SAAS,yBACd,OACA,aACA,cACA,MACoB;AACpB,QAAM,MAAM,mBAAmB,OAAO,aAAa,YAAY;AAC/D,QAAM,SAAS,qBAAqB,IAAI,GAAG;AAC3C,MAAI,QAAQ;AACV,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,IAAI,gBAAgB,aAAa,YAAY;AAC/D,QAAM,SAAS,UAAU,WAAW,IAAI;AACxC,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,OAAK,MAAM;AACX,QAAM,SAAS,UAAU,sBAAA;AACzB,uBAAqB,IAAI,KAAK,MAAM;AACpC,SAAO;AACT;AAEO,SAAS,uBAA6B;AAC3C,gBAAc,MAAA;AACd,iBAAe,MAAA;AACf,aAAW,UAAU,qBAAqB,UAAU;AAClD,WAAO,MAAA;AAAA,EACT;AACA,uBAAqB,MAAA;AACvB;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-utils.d.ts","sourceRoot":"","sources":["../../src/utils/time-utils.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAC5D,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,eAAO,MAAM,uBAAuB,UAAY,CAAC;AAIjD,eAAO,MAAM,0BAA0B,EAAE,MAAe,CAAC;AAEzD,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,wBAAgB,oBAAoB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,GAAG,CAAC,EAAE,MAAM,EACZ,QAAQ,GAAE,gBAA4B,GACrC,MAAM,CAqBR;AAED,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,GAAG,CAAC,EAAE,MAAM,EACZ,QAAQ,GAAE,gBAA4B,GACrC,MAAM,CAIR;AAED,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,eAAe,EAAE,MAAM,EACvB,WAAW,GAAE,MAAmC,GAC/C,OAAO,CAeT;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,EAC5C,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"time-utils.d.ts","sourceRoot":"","sources":["../../src/utils/time-utils.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAC5D,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,eAAO,MAAM,uBAAuB,UAAY,CAAC;AAIjD,eAAO,MAAM,0BAA0B,EAAE,MAAe,CAAC;AAEzD,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,wBAAgB,oBAAoB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,GAAG,CAAC,EAAE,MAAM,EACZ,QAAQ,GAAE,gBAA4B,GACrC,MAAM,CAqBR;AAED,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,GAAG,CAAC,EAAE,MAAM,EACZ,QAAQ,GAAE,gBAA4B,GACrC,MAAM,CAIR;AAED,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,eAAe,EAAE,MAAM,EACvB,WAAW,GAAE,MAAmC,GAC/C,OAAO,CAeT;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,EAC5C,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CA2BtC;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,GACvB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CAMtC"}
|
package/dist/utils/time-utils.js
CHANGED
|
@@ -9,14 +9,18 @@ function computeGOPAlignedWindows(gopIndex, trimStartUs, trimEndUs, maxDurationU
|
|
|
9
9
|
gopBoundaries.push(trimEndUs);
|
|
10
10
|
let windowStart = trimStartUs;
|
|
11
11
|
for (const boundary of gopBoundaries) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
while (windowStart < boundary) {
|
|
13
|
+
const spanToBoundary = boundary - windowStart;
|
|
14
|
+
if (spanToBoundary <= maxDurationUs) {
|
|
15
|
+
windows.push({ startUs: windowStart, endUs: boundary });
|
|
16
|
+
windowStart = boundary;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
const endUs = windowStart + maxDurationUs;
|
|
20
|
+
windows.push({ startUs: windowStart, endUs });
|
|
21
|
+
windowStart = endUs;
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
|
-
if (windowStart < trimEndUs) {
|
|
18
|
-
windows.push({ startUs: windowStart, endUs: trimEndUs });
|
|
19
|
-
}
|
|
20
24
|
return windows;
|
|
21
25
|
}
|
|
22
26
|
function computeFixedWindows(trimStartUs, trimEndUs, windowDurationUs) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-utils.js","sources":["../../src/utils/time-utils.ts"],"sourcesContent":["export type QuantizeStrategy = 'nearest' | 'floor' | 'ceil';\ntype TimeUs = number;\nexport const MICROSECONDS_PER_SECOND = 1_000_000;\n\nconst DEFAULT_FPS = 30;\n\nexport const DEFAULT_FRAME_TOLERANCE_US: TimeUs = 33_333;\n\nexport function normalizeFps(value?: number): number {\n if (!Number.isFinite(value) || (value as number) <= 0) {\n return DEFAULT_FPS;\n }\n return value as number;\n}\n\nexport function frameDurationFromFps(fps?: number): TimeUs {\n const normalized = normalizeFps(fps);\n const duration = MICROSECONDS_PER_SECOND / normalized;\n return Math.max(Math.round(duration), 1);\n}\n\nexport function frameIndexFromTimestamp(\n baseTimestampUs: TimeUs,\n timestampUs: TimeUs,\n fps?: number,\n strategy: QuantizeStrategy = 'nearest'\n): number {\n const frameDurationUs = frameDurationFromFps(fps);\n if (frameDurationUs <= 0) {\n return 0;\n }\n\n const delta = timestampUs - baseTimestampUs;\n const rawIndex = delta / frameDurationUs;\n\n if (!Number.isFinite(rawIndex)) {\n return 0;\n }\n\n switch (strategy) {\n case 'floor':\n return Math.floor(rawIndex);\n case 'ceil':\n return Math.ceil(rawIndex);\n default:\n return Math.round(rawIndex);\n }\n}\n\nexport function quantizeTimestampToFrame(\n timestampUs: TimeUs,\n baseTimestampUs: TimeUs,\n fps?: number,\n strategy: QuantizeStrategy = 'nearest'\n): TimeUs {\n const frameDurationUs = frameDurationFromFps(fps);\n const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);\n return baseTimestampUs + index * frameDurationUs;\n}\n\nexport function isTimestampWithinFrame(\n targetTimeUs: TimeUs,\n frameTimestampUs: TimeUs,\n frameDurationUs: TimeUs,\n toleranceUs: TimeUs = DEFAULT_FRAME_TOLERANCE_US\n): boolean {\n if (!Number.isFinite(frameTimestampUs)) {\n return false;\n }\n\n const delta = Math.abs(targetTimeUs - frameTimestampUs);\n if (delta <= toleranceUs) {\n return true;\n }\n\n if (frameDurationUs > 0 && frameTimestampUs <= targetTimeUs) {\n return targetTimeUs < frameTimestampUs + frameDurationUs;\n }\n\n return false;\n}\n\n/**\n * Group consecutive GOPs into windows that respect maxDurationUs.\n * First window starts at trimStartUs (may not align with GOP);\n * subsequent windows start at GOP boundaries (zero leading-GOP overhead).\n */\nexport function computeGOPAlignedWindows(\n gopIndex: readonly { startTimeUs: number }[],\n trimStartUs: TimeUs,\n trimEndUs: TimeUs,\n maxDurationUs: TimeUs\n): { startUs: TimeUs; endUs: TimeUs }[] {\n const windows: { startUs: TimeUs; endUs: TimeUs }[] = [];\n\n const gopBoundaries: TimeUs[] = [];\n for (const gop of gopIndex) {\n if (gop.startTimeUs > trimStartUs && gop.startTimeUs < trimEndUs) {\n gopBoundaries.push(gop.startTimeUs);\n }\n }\n gopBoundaries.push(trimEndUs);\n\n let windowStart = trimStartUs;\n for (const boundary of gopBoundaries) {\n
|
|
1
|
+
{"version":3,"file":"time-utils.js","sources":["../../src/utils/time-utils.ts"],"sourcesContent":["export type QuantizeStrategy = 'nearest' | 'floor' | 'ceil';\ntype TimeUs = number;\nexport const MICROSECONDS_PER_SECOND = 1_000_000;\n\nconst DEFAULT_FPS = 30;\n\nexport const DEFAULT_FRAME_TOLERANCE_US: TimeUs = 33_333;\n\nexport function normalizeFps(value?: number): number {\n if (!Number.isFinite(value) || (value as number) <= 0) {\n return DEFAULT_FPS;\n }\n return value as number;\n}\n\nexport function frameDurationFromFps(fps?: number): TimeUs {\n const normalized = normalizeFps(fps);\n const duration = MICROSECONDS_PER_SECOND / normalized;\n return Math.max(Math.round(duration), 1);\n}\n\nexport function frameIndexFromTimestamp(\n baseTimestampUs: TimeUs,\n timestampUs: TimeUs,\n fps?: number,\n strategy: QuantizeStrategy = 'nearest'\n): number {\n const frameDurationUs = frameDurationFromFps(fps);\n if (frameDurationUs <= 0) {\n return 0;\n }\n\n const delta = timestampUs - baseTimestampUs;\n const rawIndex = delta / frameDurationUs;\n\n if (!Number.isFinite(rawIndex)) {\n return 0;\n }\n\n switch (strategy) {\n case 'floor':\n return Math.floor(rawIndex);\n case 'ceil':\n return Math.ceil(rawIndex);\n default:\n return Math.round(rawIndex);\n }\n}\n\nexport function quantizeTimestampToFrame(\n timestampUs: TimeUs,\n baseTimestampUs: TimeUs,\n fps?: number,\n strategy: QuantizeStrategy = 'nearest'\n): TimeUs {\n const frameDurationUs = frameDurationFromFps(fps);\n const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);\n return baseTimestampUs + index * frameDurationUs;\n}\n\nexport function isTimestampWithinFrame(\n targetTimeUs: TimeUs,\n frameTimestampUs: TimeUs,\n frameDurationUs: TimeUs,\n toleranceUs: TimeUs = DEFAULT_FRAME_TOLERANCE_US\n): boolean {\n if (!Number.isFinite(frameTimestampUs)) {\n return false;\n }\n\n const delta = Math.abs(targetTimeUs - frameTimestampUs);\n if (delta <= toleranceUs) {\n return true;\n }\n\n if (frameDurationUs > 0 && frameTimestampUs <= targetTimeUs) {\n return targetTimeUs < frameTimestampUs + frameDurationUs;\n }\n\n return false;\n}\n\n/**\n * Group consecutive GOPs into windows that respect maxDurationUs.\n * First window starts at trimStartUs (may not align with GOP);\n * subsequent windows start at GOP boundaries (zero leading-GOP overhead).\n */\nexport function computeGOPAlignedWindows(\n gopIndex: readonly { startTimeUs: number }[],\n trimStartUs: TimeUs,\n trimEndUs: TimeUs,\n maxDurationUs: TimeUs\n): { startUs: TimeUs; endUs: TimeUs }[] {\n const windows: { startUs: TimeUs; endUs: TimeUs }[] = [];\n\n const gopBoundaries: TimeUs[] = [];\n for (const gop of gopIndex) {\n if (gop.startTimeUs > trimStartUs && gop.startTimeUs < trimEndUs) {\n gopBoundaries.push(gop.startTimeUs);\n }\n }\n gopBoundaries.push(trimEndUs);\n\n let windowStart = trimStartUs;\n for (const boundary of gopBoundaries) {\n while (windowStart < boundary) {\n const spanToBoundary = boundary - windowStart;\n if (spanToBoundary <= maxDurationUs) {\n windows.push({ startUs: windowStart, endUs: boundary });\n windowStart = boundary;\n break;\n }\n const endUs = windowStart + maxDurationUs;\n windows.push({ startUs: windowStart, endUs: endUs });\n windowStart = endUs;\n }\n }\n\n return windows;\n}\n\nexport function computeFixedWindows(\n trimStartUs: TimeUs,\n trimEndUs: TimeUs,\n windowDurationUs: TimeUs\n): { startUs: TimeUs; endUs: TimeUs }[] {\n const windows: { startUs: TimeUs; endUs: TimeUs }[] = [];\n for (let ws = trimStartUs; ws < trimEndUs; ws += windowDurationUs) {\n windows.push({ startUs: ws, endUs: Math.min(ws + windowDurationUs, trimEndUs) });\n }\n return windows;\n}\n"],"names":[],"mappings":"AAuFO,SAAS,yBACd,UACA,aACA,WACA,eACsC;AACtC,QAAM,UAAgD,CAAA;AAEtD,QAAM,gBAA0B,CAAA;AAChC,aAAW,OAAO,UAAU;AAC1B,QAAI,IAAI,cAAc,eAAe,IAAI,cAAc,WAAW;AAChE,oBAAc,KAAK,IAAI,WAAW;AAAA,IACpC;AAAA,EACF;AACA,gBAAc,KAAK,SAAS;AAE5B,MAAI,cAAc;AAClB,aAAW,YAAY,eAAe;AACpC,WAAO,cAAc,UAAU;AAC7B,YAAM,iBAAiB,WAAW;AAClC,UAAI,kBAAkB,eAAe;AACnC,gBAAQ,KAAK,EAAE,SAAS,aAAa,OAAO,UAAU;AACtD,sBAAc;AACd;AAAA,MACF;AACA,YAAM,QAAQ,cAAc;AAC5B,cAAQ,KAAK,EAAE,SAAS,aAAa,OAAc;AACnD,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,oBACd,aACA,WACA,kBACsC;AACtC,QAAM,UAAgD,CAAA;AACtD,WAAS,KAAK,aAAa,KAAK,WAAW,MAAM,kBAAkB;AACjE,YAAQ,KAAK,EAAE,SAAS,IAAI,OAAO,KAAK,IAAI,KAAK,kBAAkB,SAAS,EAAA,CAAG;AAAA,EACjF;AACA,SAAO;AACT;"}
|
|
@@ -759,6 +759,87 @@ function formEvenLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, font
|
|
|
759
759
|
}
|
|
760
760
|
return formLinesWithWords(ctx, words, bestWidth, fontSize, needsSpace, fontFamily, fontWeight);
|
|
761
761
|
}
|
|
762
|
+
const wrapTextCache = /* @__PURE__ */ new Map();
|
|
763
|
+
const evenLinesCache = /* @__PURE__ */ new Map();
|
|
764
|
+
function cacheKey(parts) {
|
|
765
|
+
return parts.join("");
|
|
766
|
+
}
|
|
767
|
+
function getCachedWrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
|
|
768
|
+
const key = cacheKey([text, maxWidth, fontSize, fontFamily, fontWeight]);
|
|
769
|
+
let lines = wrapTextCache.get(key);
|
|
770
|
+
if (!lines) {
|
|
771
|
+
lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
|
|
772
|
+
wrapTextCache.set(key, lines);
|
|
773
|
+
}
|
|
774
|
+
return lines;
|
|
775
|
+
}
|
|
776
|
+
function getCachedEvenLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
|
|
777
|
+
const key = cacheKey([
|
|
778
|
+
words.join(""),
|
|
779
|
+
maxWidth,
|
|
780
|
+
fontSize,
|
|
781
|
+
needsSpace,
|
|
782
|
+
fontFamily,
|
|
783
|
+
fontWeight
|
|
784
|
+
]);
|
|
785
|
+
let lines = evenLinesCache.get(key);
|
|
786
|
+
if (!lines) {
|
|
787
|
+
lines = formEvenLinesWithWords(
|
|
788
|
+
ctx,
|
|
789
|
+
words,
|
|
790
|
+
maxWidth,
|
|
791
|
+
fontSize,
|
|
792
|
+
needsSpace,
|
|
793
|
+
fontFamily,
|
|
794
|
+
fontWeight
|
|
795
|
+
);
|
|
796
|
+
evenLinesCache.set(key, lines);
|
|
797
|
+
}
|
|
798
|
+
return lines;
|
|
799
|
+
}
|
|
800
|
+
const basicTextRasterCache = /* @__PURE__ */ new Map();
|
|
801
|
+
function basicTextRasterKey(layer, canvasWidth, canvasHeight) {
|
|
802
|
+
const ts = layer.fontConfig?.textStyle;
|
|
803
|
+
return [
|
|
804
|
+
layer.id,
|
|
805
|
+
layer.text,
|
|
806
|
+
layer.letterCase ?? "",
|
|
807
|
+
canvasWidth,
|
|
808
|
+
canvasHeight,
|
|
809
|
+
ts?.fontSize,
|
|
810
|
+
ts?.fontFamily,
|
|
811
|
+
ts?.fontWeight,
|
|
812
|
+
ts?.fill,
|
|
813
|
+
ts?.stroke,
|
|
814
|
+
ts?.strokeWidth,
|
|
815
|
+
ts?.lineHeight,
|
|
816
|
+
JSON.stringify(layer.fontConfig?.globalPosition ?? null)
|
|
817
|
+
].join("");
|
|
818
|
+
}
|
|
819
|
+
function getCachedBasicTextRaster(layer, canvasWidth, canvasHeight, draw) {
|
|
820
|
+
const key = basicTextRasterKey(layer, canvasWidth, canvasHeight);
|
|
821
|
+
const cached = basicTextRasterCache.get(key);
|
|
822
|
+
if (cached) {
|
|
823
|
+
return cached;
|
|
824
|
+
}
|
|
825
|
+
const offscreen = new OffscreenCanvas(canvasWidth, canvasHeight);
|
|
826
|
+
const offCtx = offscreen.getContext("2d");
|
|
827
|
+
if (!offCtx) {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
draw(offCtx);
|
|
831
|
+
const bitmap = offscreen.transferToImageBitmap();
|
|
832
|
+
basicTextRasterCache.set(key, bitmap);
|
|
833
|
+
return bitmap;
|
|
834
|
+
}
|
|
835
|
+
function clearTextLayoutCache() {
|
|
836
|
+
wrapTextCache.clear();
|
|
837
|
+
evenLinesCache.clear();
|
|
838
|
+
for (const bitmap of basicTextRasterCache.values()) {
|
|
839
|
+
bitmap.close();
|
|
840
|
+
}
|
|
841
|
+
basicTextRasterCache.clear();
|
|
842
|
+
}
|
|
762
843
|
function springEasing(frame, config) {
|
|
763
844
|
const { damping, mass, stiffness, overshootClamping = false } = config;
|
|
764
845
|
if (frame <= 0) return 0;
|
|
@@ -858,7 +939,7 @@ function calculateYPosition$4(canvasHeight, totalHeight, globalPosition) {
|
|
|
858
939
|
}
|
|
859
940
|
return canvasHeight / 2 - totalHeight / 2;
|
|
860
941
|
}
|
|
861
|
-
function
|
|
942
|
+
function drawBasicTextLines(ctx, layer, canvasWidth, canvasHeight, lines) {
|
|
862
943
|
const fontConfig = layer.fontConfig?.textStyle;
|
|
863
944
|
if (!fontConfig) return;
|
|
864
945
|
const fontSize = fontConfig.fontSize;
|
|
@@ -868,24 +949,6 @@ function renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame)
|
|
|
868
949
|
const stroke = fontConfig.stroke;
|
|
869
950
|
const strokeWidth = fontConfig.strokeWidth || 0;
|
|
870
951
|
const lineHeight = fontConfig.lineHeight || 1.2;
|
|
871
|
-
const maxWidth = canvasWidth * 0.64;
|
|
872
|
-
const text = getLetterCaseText(layer.text, layer.letterCase);
|
|
873
|
-
let lines;
|
|
874
|
-
if (layer.wordTimings && layer.wordTimings.length > 0) {
|
|
875
|
-
const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
|
|
876
|
-
const words = text.split(needsSpace ? /\s+/ : "");
|
|
877
|
-
lines = formEvenLinesWithWords(
|
|
878
|
-
ctx,
|
|
879
|
-
words,
|
|
880
|
-
maxWidth,
|
|
881
|
-
fontSize,
|
|
882
|
-
needsSpace,
|
|
883
|
-
fontFamily,
|
|
884
|
-
fontWeight
|
|
885
|
-
);
|
|
886
|
-
} else {
|
|
887
|
-
lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
|
|
888
|
-
}
|
|
889
952
|
ctx.save();
|
|
890
953
|
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
891
954
|
ctx.textAlign = "center";
|
|
@@ -907,6 +970,49 @@ function renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame)
|
|
|
907
970
|
}
|
|
908
971
|
ctx.restore();
|
|
909
972
|
}
|
|
973
|
+
function resolveBasicTextLines(ctx, layer, canvasWidth) {
|
|
974
|
+
const fontConfig = layer.fontConfig?.textStyle;
|
|
975
|
+
if (!fontConfig) return [];
|
|
976
|
+
const maxWidth = canvasWidth * 0.64;
|
|
977
|
+
const text = getLetterCaseText(layer.text, layer.letterCase);
|
|
978
|
+
if (layer.wordTimings && layer.wordTimings.length > 0) {
|
|
979
|
+
const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
|
|
980
|
+
const words = needsSpace ? text.split(/\s+/) : Array.from(text);
|
|
981
|
+
return getCachedEvenLinesWithWords(
|
|
982
|
+
ctx,
|
|
983
|
+
words,
|
|
984
|
+
maxWidth,
|
|
985
|
+
fontConfig.fontSize,
|
|
986
|
+
needsSpace,
|
|
987
|
+
fontConfig.fontFamily,
|
|
988
|
+
fontConfig.fontWeight
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
return getCachedWrapText(
|
|
992
|
+
ctx,
|
|
993
|
+
text,
|
|
994
|
+
maxWidth,
|
|
995
|
+
fontConfig.fontSize,
|
|
996
|
+
fontConfig.fontFamily,
|
|
997
|
+
fontConfig.fontWeight
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
function renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame) {
|
|
1001
|
+
const fontConfig = layer.fontConfig?.textStyle;
|
|
1002
|
+
if (!fontConfig) return;
|
|
1003
|
+
const raster = getCachedBasicTextRaster(layer, canvasWidth, canvasHeight, (offCtx) => {
|
|
1004
|
+
const lines2 = resolveBasicTextLines(offCtx, layer, canvasWidth);
|
|
1005
|
+
if (lines2.length === 0) return;
|
|
1006
|
+
drawBasicTextLines(offCtx, layer, canvasWidth, canvasHeight, lines2);
|
|
1007
|
+
});
|
|
1008
|
+
if (raster) {
|
|
1009
|
+
ctx.drawImage(raster, 0, 0);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const lines = resolveBasicTextLines(ctx, layer, canvasWidth);
|
|
1013
|
+
if (lines.length === 0) return;
|
|
1014
|
+
drawBasicTextLines(ctx, layer, canvasWidth, canvasHeight, lines);
|
|
1015
|
+
}
|
|
910
1016
|
const DEFAULT_DURATION_MS = 800;
|
|
911
1017
|
const DEFAULT_STAGGER_MS = 50;
|
|
912
1018
|
const SLIDE_BASE_PX = 50;
|
|
@@ -942,6 +1048,34 @@ function charProgress(relativeFrame, fps, charIndex, staggerMs, durationMs) {
|
|
|
942
1048
|
const raw = (tMs - startMs) / durationMs;
|
|
943
1049
|
return easeOutExpo(Math.min(1, raw));
|
|
944
1050
|
}
|
|
1051
|
+
const charSlotsCache = /* @__PURE__ */ new Map();
|
|
1052
|
+
const staggerFinalRasterCache = /* @__PURE__ */ new Map();
|
|
1053
|
+
function layoutCacheKey(layer, canvasWidth, canvasHeight) {
|
|
1054
|
+
const ts = layer.fontConfig?.textStyle;
|
|
1055
|
+
return [
|
|
1056
|
+
layer.id,
|
|
1057
|
+
layer.text,
|
|
1058
|
+
layer.letterCase ?? "",
|
|
1059
|
+
canvasWidth,
|
|
1060
|
+
canvasHeight,
|
|
1061
|
+
ts?.fontSize,
|
|
1062
|
+
ts?.fontFamily,
|
|
1063
|
+
ts?.fontWeight,
|
|
1064
|
+
ts?.lineHeight,
|
|
1065
|
+
JSON.stringify(layer.fontConfig?.globalPosition ?? null)
|
|
1066
|
+
].join("");
|
|
1067
|
+
}
|
|
1068
|
+
function staggerRasterKey(layer, canvasWidth, canvasHeight, preset) {
|
|
1069
|
+
return `${layoutCacheKey(layer, canvasWidth, canvasHeight)}${preset}`;
|
|
1070
|
+
}
|
|
1071
|
+
function staggerEntranceEndMs(slotCount, preset) {
|
|
1072
|
+
if (slotCount <= 0) return 0;
|
|
1073
|
+
const lastIndex = slotCount - 1;
|
|
1074
|
+
if (preset === "typewriter") {
|
|
1075
|
+
return lastIndex * DEFAULT_STAGGER_MS + DEFAULT_STAGGER_MS;
|
|
1076
|
+
}
|
|
1077
|
+
return lastIndex * DEFAULT_STAGGER_MS + DEFAULT_DURATION_MS;
|
|
1078
|
+
}
|
|
945
1079
|
function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
|
|
946
1080
|
const fontConfig = layer.fontConfig?.textStyle;
|
|
947
1081
|
if (!fontConfig) return null;
|
|
@@ -954,8 +1088,8 @@ function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
|
|
|
954
1088
|
let lines;
|
|
955
1089
|
if (layer.wordTimings && layer.wordTimings.length > 0) {
|
|
956
1090
|
const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
|
|
957
|
-
const words = text.split(
|
|
958
|
-
lines =
|
|
1091
|
+
const words = needsSpace ? text.split(/\s+/) : Array.from(text);
|
|
1092
|
+
lines = getCachedEvenLinesWithWords(
|
|
959
1093
|
ctx,
|
|
960
1094
|
words,
|
|
961
1095
|
maxWidth,
|
|
@@ -965,7 +1099,7 @@ function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
|
|
|
965
1099
|
fontWeight
|
|
966
1100
|
);
|
|
967
1101
|
} else {
|
|
968
|
-
lines =
|
|
1102
|
+
lines = getCachedWrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
|
|
969
1103
|
}
|
|
970
1104
|
const totalHeight = lines.length * fontSize * lineHeight;
|
|
971
1105
|
const startY = calculateYPosition$3(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
|
|
@@ -993,9 +1127,19 @@ function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
|
|
|
993
1127
|
ctx.restore();
|
|
994
1128
|
return { slots, fontSize, lineHeight };
|
|
995
1129
|
}
|
|
996
|
-
function
|
|
1130
|
+
function getCachedCharSlots(ctx, layer, canvasWidth, canvasHeight) {
|
|
1131
|
+
const key = layoutCacheKey(layer, canvasWidth, canvasHeight);
|
|
1132
|
+
const cached = charSlotsCache.get(key);
|
|
1133
|
+
if (cached) {
|
|
1134
|
+
return cached;
|
|
1135
|
+
}
|
|
997
1136
|
const built = buildCharSlots(ctx, layer, canvasWidth, canvasHeight);
|
|
998
|
-
if (
|
|
1137
|
+
if (built) {
|
|
1138
|
+
charSlotsCache.set(key, built);
|
|
1139
|
+
}
|
|
1140
|
+
return built;
|
|
1141
|
+
}
|
|
1142
|
+
function drawStaggerEntranceFrame(ctx, layer, built, relativeFrame, fps, preset) {
|
|
999
1143
|
const { slots, fontSize } = built;
|
|
1000
1144
|
const fontConfig = layer.fontConfig.textStyle;
|
|
1001
1145
|
const fontFamily = fontConfig.fontFamily;
|
|
@@ -1087,6 +1231,37 @@ function renderCaptionStaggerEntrance(ctx, layer, canvasWidth, canvasHeight, rel
|
|
|
1087
1231
|
}
|
|
1088
1232
|
ctx.restore();
|
|
1089
1233
|
}
|
|
1234
|
+
function clearCaptionStaggerCache() {
|
|
1235
|
+
charSlotsCache.clear();
|
|
1236
|
+
for (const bitmap of staggerFinalRasterCache.values()) {
|
|
1237
|
+
bitmap.close();
|
|
1238
|
+
}
|
|
1239
|
+
staggerFinalRasterCache.clear();
|
|
1240
|
+
}
|
|
1241
|
+
function renderCaptionStaggerEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps, preset) {
|
|
1242
|
+
const built = getCachedCharSlots(ctx, layer, canvasWidth, canvasHeight);
|
|
1243
|
+
if (!built) return;
|
|
1244
|
+
const endMs = staggerEntranceEndMs(built.slots.length, preset);
|
|
1245
|
+
const endFrame = Math.ceil(endMs / 1e3 * fps);
|
|
1246
|
+
if (relativeFrame >= endFrame) {
|
|
1247
|
+
const rasterKey = staggerRasterKey(layer, canvasWidth, canvasHeight, preset);
|
|
1248
|
+
let bitmap = staggerFinalRasterCache.get(rasterKey);
|
|
1249
|
+
if (!bitmap) {
|
|
1250
|
+
const offscreen = new OffscreenCanvas(canvasWidth, canvasHeight);
|
|
1251
|
+
const offCtx = offscreen.getContext("2d");
|
|
1252
|
+
if (!offCtx) {
|
|
1253
|
+
drawStaggerEntranceFrame(ctx, layer, built, endFrame, fps, preset);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
drawStaggerEntranceFrame(offCtx, layer, built, endFrame, fps, preset);
|
|
1257
|
+
bitmap = offscreen.transferToImageBitmap();
|
|
1258
|
+
staggerFinalRasterCache.set(rasterKey, bitmap);
|
|
1259
|
+
}
|
|
1260
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
drawStaggerEntranceFrame(ctx, layer, built, relativeFrame, fps, preset);
|
|
1264
|
+
}
|
|
1090
1265
|
function usToFrame$2(us, fps) {
|
|
1091
1266
|
return Math.floor(us / (1e6 / fps));
|
|
1092
1267
|
}
|
|
@@ -1979,11 +2154,11 @@ class FilterProcessor {
|
|
|
1979
2154
|
ctx.filter = "none";
|
|
1980
2155
|
return;
|
|
1981
2156
|
}
|
|
1982
|
-
const
|
|
1983
|
-
let filterString = this.filterCache.get(
|
|
2157
|
+
const cacheKey2 = this.generateCacheKey(filters);
|
|
2158
|
+
let filterString = this.filterCache.get(cacheKey2);
|
|
1984
2159
|
if (!filterString) {
|
|
1985
2160
|
filterString = this.buildFilterString(filters);
|
|
1986
|
-
this.filterCache.set(
|
|
2161
|
+
this.filterCache.set(cacheKey2, filterString);
|
|
1987
2162
|
}
|
|
1988
2163
|
ctx.filter = filterString;
|
|
1989
2164
|
}
|
|
@@ -2287,6 +2462,8 @@ class VideoComposer {
|
|
|
2287
2462
|
},
|
|
2288
2463
|
flush: async () => {
|
|
2289
2464
|
this.filterProcessor.clearCache();
|
|
2465
|
+
clearTextLayoutCache();
|
|
2466
|
+
clearCaptionStaggerCache();
|
|
2290
2467
|
}
|
|
2291
2468
|
},
|
|
2292
2469
|
{
|
|
@@ -3443,4 +3620,4 @@ const export_worker = null;
|
|
|
3443
3620
|
export {
|
|
3444
3621
|
export_worker as default
|
|
3445
3622
|
};
|
|
3446
|
-
//# sourceMappingURL=export.worker.
|
|
3623
|
+
//# sourceMappingURL=export.worker.CPqXBEVe.js.map
|