@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.
Files changed (25) hide show
  1. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
  2. package/dist/orchestrator/ExportScheduler.js +25 -22
  3. package/dist/orchestrator/ExportScheduler.js.map +1 -1
  4. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  5. package/dist/stages/compose/VideoComposer.js +4 -0
  6. package/dist/stages/compose/VideoComposer.js.map +1 -1
  7. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  8. package/dist/stages/compose/text-renderers/basic-text-renderer.js +45 -20
  9. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -1
  10. package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.d.ts +2 -0
  11. package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.d.ts.map +1 -1
  12. package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.js +78 -7
  13. package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.js.map +1 -1
  14. package/dist/stages/compose/text-utils/text-layout-cache.d.ts +7 -0
  15. package/dist/stages/compose/text-utils/text-layout-cache.d.ts.map +1 -0
  16. package/dist/stages/compose/text-utils/text-layout-cache.js +89 -0
  17. package/dist/stages/compose/text-utils/text-layout-cache.js.map +1 -0
  18. package/dist/utils/time-utils.d.ts.map +1 -1
  19. package/dist/utils/time-utils.js +10 -6
  20. package/dist/utils/time-utils.js.map +1 -1
  21. package/dist/workers/stages/export/{export.worker.Dztm6GuN.js → export.worker.CPqXBEVe.js} +205 -28
  22. package/dist/workers/stages/export/export.worker.CPqXBEVe.js.map +1 -0
  23. package/dist/workers/worker-manifest.json +1 -1
  24. package/package.json +1 -1
  25. 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":"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"}
@@ -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,CAwBtC;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"}
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"}
@@ -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
- if (boundary - windowStart >= maxDurationUs) {
13
- windows.push({ startUs: windowStart, endUs: boundary });
14
- windowStart = boundary;
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 if (boundary - windowStart >= maxDurationUs) {\n windows.push({ startUs: windowStart, endUs: boundary });\n windowStart = boundary;\n }\n }\n\n if (windowStart < trimEndUs) {\n windows.push({ startUs: windowStart, endUs: trimEndUs });\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,QAAI,WAAW,eAAe,eAAe;AAC3C,cAAQ,KAAK,EAAE,SAAS,aAAa,OAAO,UAAU;AACtD,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,cAAc,WAAW;AAC3B,YAAQ,KAAK,EAAE,SAAS,aAAa,OAAO,WAAW;AAAA,EACzD;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;"}
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 renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame) {
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(needsSpace ? /\s+/ : "");
958
- lines = formEvenLinesWithWords(
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 = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
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 renderCaptionStaggerEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps, preset) {
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 (!built) return;
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 cacheKey = this.generateCacheKey(filters);
1983
- let filterString = this.filterCache.get(cacheKey);
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(cacheKey, filterString);
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.Dztm6GuN.js.map
3623
+ //# sourceMappingURL=export.worker.CPqXBEVe.js.map