@meframe/core 0.5.5 → 0.5.7

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 (26) hide show
  1. package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +1 -0
  2. package/dist/{medeo-fe/node_modules → node_modules}/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js +14 -14
  3. package/dist/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +1 -0
  4. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
  5. package/dist/orchestrator/ExportScheduler.js +13 -0
  6. package/dist/orchestrator/ExportScheduler.js.map +1 -1
  7. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  8. package/dist/stages/compose/VideoComposer.js +2 -0
  9. package/dist/stages/compose/VideoComposer.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-wrapper.d.ts.map +1 -1
  15. package/dist/stages/compose/text-utils/text-wrapper.js +38 -113
  16. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -1
  17. package/dist/stages/mux/MP4Muxer.js +1 -1
  18. package/dist/utils/mp4box.js +2 -3
  19. package/dist/workers/stages/export/{export.worker.C9m51RNP.js → export.worker.p7X_YtxQ.js} +117 -122
  20. package/dist/workers/stages/export/export.worker.p7X_YtxQ.js.map +1 -0
  21. package/dist/workers/worker-manifest.json +1 -1
  22. package/package.json +1 -1
  23. package/dist/medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +0 -1
  24. package/dist/medeo-fe/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +0 -1
  25. package/dist/workers/stages/export/export.worker.C9m51RNP.js.map +0 -1
  26. /package/dist/{medeo-fe/node_modules → node_modules}/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js +0 -0
@@ -16,129 +16,54 @@ function findAllBreakPoints(text) {
16
16
  breakPoints.push(chars.length);
17
17
  return breakPoints;
18
18
  }
19
- function evaluateBalance(lines, ctx, fontSize, fontFamily, fontWeight) {
20
- if (lines.length <= 1) return 0;
21
- const lengths = lines.map(
22
- (line) => measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)
23
- );
24
- const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
25
- return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);
26
- }
27
- function tryBreakPointsForMultipleLines(ctx, text, start, remainingLines, currentLines, maxWidth, fontSize, fontFamily, fontWeight, breakPoints) {
28
- let bestLines = [];
29
- let bestBalance = Infinity;
30
- if (remainingLines === 1) {
31
- const lastLine = text.slice(start).trim();
32
- const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);
33
- if (lastLineWidth <= maxWidth) {
34
- const allLines = [...currentLines, lastLine];
35
- const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);
36
- if (balance < bestBalance) {
37
- bestBalance = balance;
38
- bestLines = allLines;
39
- }
40
- } else {
41
- const words = lastLine.split(/\s+/);
42
- let currentLine = "";
43
- let tempLines = [...currentLines];
44
- for (const word of words) {
45
- const testLine = currentLine ? `${currentLine} ${word}` : word;
46
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
47
- if (lineWidth <= maxWidth) {
48
- currentLine = testLine;
49
- } else {
50
- if (currentLine) {
51
- tempLines.push(currentLine);
52
- currentLine = word;
53
- } else {
54
- tempLines.push(word);
55
- currentLine = "";
56
- }
57
- }
58
- }
59
- if (currentLine) {
60
- tempLines.push(currentLine);
61
- }
62
- const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);
63
- if (balance < bestBalance) {
64
- bestBalance = balance;
65
- bestLines = tempLines;
66
- }
67
- }
68
- return { bestLines, bestBalance };
69
- }
70
- let foundValidBreak = false;
71
- for (let i = 0; i < breakPoints.length; i++) {
19
+ function greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight) {
20
+ const breakPoints = findAllBreakPoints(text);
21
+ const lines = [];
22
+ let current = "";
23
+ let prev = breakPoints[0];
24
+ for (let i = 1; i < breakPoints.length; i++) {
72
25
  const bp = breakPoints[i];
73
- if (bp <= start || bp >= text.length) continue;
74
- const line = text.slice(start, bp).trim();
75
- const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
76
- if (lineWidth <= maxWidth) {
77
- foundValidBreak = true;
78
- const result = tryBreakPointsForMultipleLines(
79
- ctx,
80
- text,
81
- bp,
82
- remainingLines - 1,
83
- [...currentLines, line],
84
- maxWidth,
85
- fontSize,
86
- fontFamily,
87
- fontWeight,
88
- breakPoints
89
- );
90
- if (result.bestBalance < bestBalance) {
91
- bestBalance = result.bestBalance;
92
- bestLines = result.bestLines;
93
- }
26
+ if (bp <= prev) continue;
27
+ const segment = text.slice(prev, bp);
28
+ const candidate = current + segment;
29
+ const candidateWidth = measureTextWidth(ctx, candidate, fontSize, fontFamily, fontWeight);
30
+ if (candidateWidth <= maxWidth || current === "") {
31
+ current = candidate;
32
+ } else {
33
+ lines.push(current.trim());
34
+ current = segment;
94
35
  }
36
+ prev = bp;
95
37
  }
96
- if (!foundValidBreak) {
97
- const textPortion = text.slice(start);
98
- const words = textPortion.split(/\s+/);
99
- let currentLine = "";
100
- let tempLines = [...currentLines];
101
- for (const word of words) {
102
- const testLine = currentLine ? `${currentLine} ${word}` : word;
103
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
104
- if (lineWidth <= maxWidth) {
105
- currentLine = testLine;
106
- } else {
107
- if (currentLine) {
108
- tempLines.push(currentLine);
109
- currentLine = word;
110
- } else {
111
- tempLines.push(word);
112
- }
113
- }
114
- }
115
- if (currentLine) {
116
- tempLines.push(currentLine);
117
- }
118
- bestLines = tempLines;
38
+ if (current.trim()) {
39
+ lines.push(current.trim());
119
40
  }
120
- return { bestLines, bestBalance };
41
+ fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight);
42
+ return lines.length > 0 ? lines : [text];
43
+ }
44
+ function fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight) {
45
+ if (lines.length < 2) return;
46
+ const last = lines[lines.length - 1];
47
+ const lastWords = last.split(/\s+/).filter(Boolean);
48
+ if (lastWords.length > 1) return;
49
+ const prevLine = lines[lines.length - 2];
50
+ const prevWords = prevLine.split(/\s+/).filter(Boolean);
51
+ if (prevWords.length < 2) return;
52
+ const moved = prevWords.pop();
53
+ const newPrev = prevWords.join(" ");
54
+ const newLast = `${moved} ${last}`.trim();
55
+ const newPrevWidth = measureTextWidth(ctx, newPrev, fontSize, fontFamily, fontWeight);
56
+ const newLastWidth = measureTextWidth(ctx, newLast, fontSize, fontFamily, fontWeight);
57
+ if (newPrevWidth > maxWidth || newLastWidth > maxWidth) return;
58
+ lines[lines.length - 2] = newPrev;
59
+ lines[lines.length - 1] = newLast;
121
60
  }
122
61
  function wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
123
62
  const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);
124
63
  if (textWidth <= maxWidth) {
125
64
  return [text];
126
65
  }
127
- const estimatedLines = Math.ceil(textWidth / maxWidth);
128
- const breakPoints = findAllBreakPoints(text);
129
- const { bestLines } = tryBreakPointsForMultipleLines(
130
- ctx,
131
- text,
132
- 0,
133
- estimatedLines,
134
- [],
135
- maxWidth,
136
- fontSize,
137
- fontFamily,
138
- fontWeight,
139
- breakPoints
140
- );
141
- return bestLines.length > 0 ? bestLines : [text];
66
+ return greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
142
67
  }
143
68
  function formLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
144
69
  const result = [];
@@ -1 +1 @@
1
- {"version":3,"file":"text-wrapper.js","sources":["../../../../src/stages/compose/text-utils/text-wrapper.ts"],"sourcesContent":["import { measureTextWidth } from './text-metrics';\n\nfunction findAllBreakPoints(text: string): number[] {\n const breakPoints = [0];\n const chars = Array.from(text);\n\n for (let i = 1; i < chars.length - 1; i++) {\n if (/[、。!?,,!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i]!) &&\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i + 1]!)\n ) {\n breakPoints.push(i + 1);\n } else if (/[\\s\\-–—,.!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (/\\s/.test(chars[i]!) && /[a-zA-Z]/.test(chars[i + 1]!)) {\n breakPoints.push(i + 1);\n }\n }\n\n breakPoints.push(chars.length);\n return breakPoints;\n}\n\nfunction evaluateBalance(\n lines: string[],\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number\n): number {\n if (lines.length <= 1) return 0;\n const lengths = lines.map((line) =>\n measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)\n );\n const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;\n return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);\n}\n\nfunction tryBreakPointsForMultipleLines(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n start: number,\n remainingLines: number,\n currentLines: string[],\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number,\n breakPoints: number[]\n): {\n bestLines: string[];\n bestBalance: number;\n} {\n let bestLines: string[] = [];\n let bestBalance = Infinity;\n\n if (remainingLines === 1) {\n const lastLine = text.slice(start).trim();\n const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);\n\n if (lastLineWidth <= maxWidth) {\n const allLines = [...currentLines, lastLine];\n const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);\n\n if (balance < bestBalance) {\n bestBalance = balance;\n bestLines = allLines;\n }\n } else {\n const words = lastLine.split(/\\s+/);\n let currentLine = '';\n let tempLines = [...currentLines];\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word;\n const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);\n\n if (lineWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n tempLines.push(currentLine);\n currentLine = word;\n } else {\n tempLines.push(word);\n currentLine = '';\n }\n }\n }\n\n if (currentLine) {\n tempLines.push(currentLine);\n }\n\n const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);\n if (balance < bestBalance) {\n bestBalance = balance;\n bestLines = tempLines;\n }\n }\n return { bestLines, bestBalance };\n }\n\n let foundValidBreak = false;\n\n for (let i = 0; i < breakPoints.length; i++) {\n const bp = breakPoints[i]!;\n if (bp <= start || bp >= text.length) continue;\n\n const line = text.slice(start, bp).trim();\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n\n if (lineWidth <= maxWidth) {\n foundValidBreak = true;\n const result = tryBreakPointsForMultipleLines(\n ctx,\n text,\n bp,\n remainingLines - 1,\n [...currentLines, line],\n maxWidth,\n fontSize,\n fontFamily,\n fontWeight,\n breakPoints\n );\n if (result.bestBalance < bestBalance) {\n bestBalance = result.bestBalance;\n bestLines = result.bestLines;\n }\n }\n }\n\n if (!foundValidBreak) {\n const textPortion = text.slice(start);\n const words = textPortion.split(/\\s+/);\n let currentLine = '';\n let tempLines = [...currentLines];\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word;\n const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);\n\n if (lineWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n tempLines.push(currentLine);\n currentLine = word;\n } else {\n tempLines.push(word);\n }\n }\n }\n\n if (currentLine) {\n tempLines.push(currentLine);\n }\n\n bestLines = tempLines;\n }\n\n return { bestLines, bestBalance };\n}\n\nexport function wrapText(\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 textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);\n if (textWidth <= maxWidth) {\n return [text];\n }\n\n const estimatedLines = Math.ceil(textWidth / maxWidth);\n const breakPoints = findAllBreakPoints(text);\n\n const { bestLines } = tryBreakPointsForMultipleLines(\n ctx,\n text,\n 0,\n estimatedLines,\n [],\n maxWidth,\n fontSize,\n fontFamily,\n fontWeight,\n breakPoints\n );\n\n return bestLines.length > 0 ? bestLines : [text];\n}\n\nexport function formLinesWithWords(\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 result: string[] = [];\n let accumulatedWidth = 0;\n const spaceWidth = measureTextWidth(ctx, ' ', fontSize, fontFamily, fontWeight);\n let currentLine = '';\n\n for (const word of words) {\n let wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (needsSpace) {\n wordWidth += spaceWidth;\n }\n if (wordWidth + accumulatedWidth <= maxWidth) {\n currentLine += word + (needsSpace ? ' ' : '');\n accumulatedWidth += wordWidth;\n } else {\n if (currentLine) {\n result.push(currentLine);\n }\n currentLine = word + (needsSpace ? ' ' : '');\n accumulatedWidth = wordWidth;\n }\n }\n if (currentLine !== '') {\n result.push(currentLine);\n }\n return result;\n}\n\nexport function formEvenLinesWithWords(\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 let minWidth = maxWidth / 2;\n for (const word of words) {\n const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (wordWidth > minWidth) {\n minWidth = wordWidth;\n }\n }\n\n const leastLineNum = formLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n ).length;\n\n let bestDelta = maxWidth;\n let bestWidth = minWidth;\n for (let width = maxWidth; width >= minWidth; width -= 1) {\n const lines = formLinesWithWords(\n ctx,\n words,\n width,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n if (lines.length > leastLineNum) {\n break;\n }\n let minLineWidth = Infinity;\n let maxLineWidth = 0;\n for (const line of lines) {\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n if (lineWidth < minLineWidth) {\n minLineWidth = lineWidth;\n }\n if (lineWidth > maxLineWidth) {\n maxLineWidth = lineWidth;\n }\n }\n const delta = maxLineWidth - minLineWidth;\n if (delta < bestDelta) {\n bestDelta = delta;\n bestWidth = width;\n }\n }\n\n return formLinesWithWords(ctx, words, bestWidth, fontSize, needsSpace, fontFamily, fontWeight);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"text-wrapper.js","sources":["../../../../src/stages/compose/text-utils/text-wrapper.ts"],"sourcesContent":["import { measureTextWidth } from './text-metrics';\n\nfunction findAllBreakPoints(text: string): number[] {\n const breakPoints = [0];\n const chars = Array.from(text);\n\n for (let i = 1; i < chars.length - 1; i++) {\n if (/[、。!?,,!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i]!) &&\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i + 1]!)\n ) {\n breakPoints.push(i + 1);\n } else if (/[\\s\\-–—,.!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (/\\s/.test(chars[i]!) && /[a-zA-Z]/.test(chars[i + 1]!)) {\n breakPoints.push(i + 1);\n }\n }\n\n breakPoints.push(chars.length);\n return breakPoints;\n}\n\n// 线性贪心折行:从左到右把合法片段往当前行塞,塞不下就换行。\n// 复杂度 O(B)(B = 断点数),替代原 O(B^N) 的递归回溯。\n// 视觉缺陷:最后一行可能只剩一个词(\"孤儿\"),由 fixTailOrphan 修复。\nfunction greedyWrap(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number\n): string[] {\n const breakPoints = findAllBreakPoints(text);\n const lines: string[] = [];\n let current = '';\n let prev = breakPoints[0]!;\n\n for (let i = 1; i < breakPoints.length; i++) {\n const bp = breakPoints[i]!;\n if (bp <= prev) continue;\n\n const segment = text.slice(prev, bp);\n const candidate = current + segment;\n const candidateWidth = measureTextWidth(ctx, candidate, fontSize, fontFamily, fontWeight);\n\n // 能塞下就并入当前行;current === '' 兜底:单个 segment 已经超宽时也强制保留,\n // 避免死循环。这种超宽 segment(如超长无空格英文单词)由调用方处理或自然溢出。\n if (candidateWidth <= maxWidth || current === '') {\n current = candidate;\n } else {\n lines.push(current.trim());\n current = segment;\n }\n prev = bp;\n }\n\n if (current.trim()) {\n lines.push(current.trim());\n }\n\n fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight);\n\n return lines.length > 0 ? lines : [text];\n}\n\n// 尾行孤儿修正:贪心结束后若最后一行只剩 1 个词,\n// 从倒数第二行末尾挪 1 个词到最后一行,仅当移动后两行都不超宽时才接受。\n// 视觉上避免出现 \"断头\" 的最后一行。原地修改 lines。\nfunction fixTailOrphan(\n lines: string[],\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number\n): void {\n if (lines.length < 2) return;\n\n const last = lines[lines.length - 1]!;\n const lastWords = last.split(/\\s+/).filter(Boolean);\n if (lastWords.length > 1) return;\n\n const prevLine = lines[lines.length - 2]!;\n const prevWords = prevLine.split(/\\s+/).filter(Boolean);\n if (prevWords.length < 2) return;\n\n const moved = prevWords.pop()!;\n const newPrev = prevWords.join(' ');\n const newLast = `${moved} ${last}`.trim();\n\n // 移动后必须两行都不超宽,否则放弃修正、保留原贪心结果\n const newPrevWidth = measureTextWidth(ctx, newPrev, fontSize, fontFamily, fontWeight);\n const newLastWidth = measureTextWidth(ctx, newLast, fontSize, fontFamily, fontWeight);\n if (newPrevWidth > maxWidth || newLastWidth > maxWidth) return;\n\n lines[lines.length - 2] = newPrev;\n lines[lines.length - 1] = newLast;\n}\n\nexport function wrapText(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n // 整段不超宽直接单行返回(最常见的快路径)\n const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);\n if (textWidth <= maxWidth) {\n return [text];\n }\n\n return greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);\n}\n\nexport function formLinesWithWords(\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 result: string[] = [];\n let accumulatedWidth = 0;\n const spaceWidth = measureTextWidth(ctx, ' ', fontSize, fontFamily, fontWeight);\n let currentLine = '';\n\n for (const word of words) {\n let wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (needsSpace) {\n wordWidth += spaceWidth;\n }\n if (wordWidth + accumulatedWidth <= maxWidth) {\n currentLine += word + (needsSpace ? ' ' : '');\n accumulatedWidth += wordWidth;\n } else {\n if (currentLine) {\n result.push(currentLine);\n }\n currentLine = word + (needsSpace ? ' ' : '');\n accumulatedWidth = wordWidth;\n }\n }\n if (currentLine !== '') {\n result.push(currentLine);\n }\n return result;\n}\n\nexport function formEvenLinesWithWords(\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 let minWidth = maxWidth / 2;\n for (const word of words) {\n const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (wordWidth > minWidth) {\n minWidth = wordWidth;\n }\n }\n\n const leastLineNum = formLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n ).length;\n\n let bestDelta = maxWidth;\n let bestWidth = minWidth;\n for (let width = maxWidth; width >= minWidth; width -= 1) {\n const lines = formLinesWithWords(\n ctx,\n words,\n width,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n if (lines.length > leastLineNum) {\n break;\n }\n let minLineWidth = Infinity;\n let maxLineWidth = 0;\n for (const line of lines) {\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n if (lineWidth < minLineWidth) {\n minLineWidth = lineWidth;\n }\n if (lineWidth > maxLineWidth) {\n maxLineWidth = lineWidth;\n }\n }\n const delta = maxLineWidth - minLineWidth;\n if (delta < bestDelta) {\n bestDelta = delta;\n bestWidth = width;\n }\n }\n\n return formLinesWithWords(ctx, words, bestWidth, fontSize, needsSpace, fontFamily, fontWeight);\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { StreamTarget, ArrayBufferTarget, Muxer } from "../../medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js";
1
+ import { StreamTarget, ArrayBufferTarget, Muxer } from "../../node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js";
2
2
  import { checkBrowserCompatibility } from "../../utils/platform-utils.js";
3
3
  class MP4Muxer {
4
4
  muxer;
@@ -1,4 +1,4 @@
1
- import * as mp4box_all from "../medeo-fe/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js";
1
+ import * as mp4box_all from "../node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js";
2
2
  const lib = mp4box_all;
3
3
  const MP4Box = lib.default && typeof lib.default.createFile === "function" ? lib.default : lib;
4
4
  if (typeof MP4Box.createFile !== "function") {
@@ -11,7 +11,6 @@ if (typeof MP4Box.createFile !== "function") {
11
11
  );
12
12
  }
13
13
  export {
14
- MP4Box,
15
- MP4Box as default
14
+ MP4Box
16
15
  };
17
16
  //# sourceMappingURL=mp4box.js.map
@@ -558,129 +558,54 @@ function findAllBreakPoints(text) {
558
558
  breakPoints.push(chars.length);
559
559
  return breakPoints;
560
560
  }
561
- function evaluateBalance(lines, ctx, fontSize, fontFamily, fontWeight) {
562
- if (lines.length <= 1) return 0;
563
- const lengths = lines.map(
564
- (line) => measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)
565
- );
566
- const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
567
- return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);
568
- }
569
- function tryBreakPointsForMultipleLines(ctx, text, start, remainingLines, currentLines, maxWidth, fontSize, fontFamily, fontWeight, breakPoints) {
570
- let bestLines = [];
571
- let bestBalance = Infinity;
572
- if (remainingLines === 1) {
573
- const lastLine = text.slice(start).trim();
574
- const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);
575
- if (lastLineWidth <= maxWidth) {
576
- const allLines = [...currentLines, lastLine];
577
- const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);
578
- if (balance < bestBalance) {
579
- bestBalance = balance;
580
- bestLines = allLines;
581
- }
582
- } else {
583
- const words = lastLine.split(/\s+/);
584
- let currentLine = "";
585
- let tempLines = [...currentLines];
586
- for (const word of words) {
587
- const testLine = currentLine ? `${currentLine} ${word}` : word;
588
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
589
- if (lineWidth <= maxWidth) {
590
- currentLine = testLine;
591
- } else {
592
- if (currentLine) {
593
- tempLines.push(currentLine);
594
- currentLine = word;
595
- } else {
596
- tempLines.push(word);
597
- currentLine = "";
598
- }
599
- }
600
- }
601
- if (currentLine) {
602
- tempLines.push(currentLine);
603
- }
604
- const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);
605
- if (balance < bestBalance) {
606
- bestBalance = balance;
607
- bestLines = tempLines;
608
- }
609
- }
610
- return { bestLines, bestBalance };
611
- }
612
- let foundValidBreak = false;
613
- for (let i = 0; i < breakPoints.length; i++) {
561
+ function greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight) {
562
+ const breakPoints = findAllBreakPoints(text);
563
+ const lines = [];
564
+ let current = "";
565
+ let prev = breakPoints[0];
566
+ for (let i = 1; i < breakPoints.length; i++) {
614
567
  const bp = breakPoints[i];
615
- if (bp <= start || bp >= text.length) continue;
616
- const line = text.slice(start, bp).trim();
617
- const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
618
- if (lineWidth <= maxWidth) {
619
- foundValidBreak = true;
620
- const result = tryBreakPointsForMultipleLines(
621
- ctx,
622
- text,
623
- bp,
624
- remainingLines - 1,
625
- [...currentLines, line],
626
- maxWidth,
627
- fontSize,
628
- fontFamily,
629
- fontWeight,
630
- breakPoints
631
- );
632
- if (result.bestBalance < bestBalance) {
633
- bestBalance = result.bestBalance;
634
- bestLines = result.bestLines;
635
- }
636
- }
637
- }
638
- if (!foundValidBreak) {
639
- const textPortion = text.slice(start);
640
- const words = textPortion.split(/\s+/);
641
- let currentLine = "";
642
- let tempLines = [...currentLines];
643
- for (const word of words) {
644
- const testLine = currentLine ? `${currentLine} ${word}` : word;
645
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
646
- if (lineWidth <= maxWidth) {
647
- currentLine = testLine;
648
- } else {
649
- if (currentLine) {
650
- tempLines.push(currentLine);
651
- currentLine = word;
652
- } else {
653
- tempLines.push(word);
654
- }
655
- }
656
- }
657
- if (currentLine) {
658
- tempLines.push(currentLine);
659
- }
660
- bestLines = tempLines;
661
- }
662
- return { bestLines, bestBalance };
568
+ if (bp <= prev) continue;
569
+ const segment = text.slice(prev, bp);
570
+ const candidate = current + segment;
571
+ const candidateWidth = measureTextWidth(ctx, candidate, fontSize, fontFamily, fontWeight);
572
+ if (candidateWidth <= maxWidth || current === "") {
573
+ current = candidate;
574
+ } else {
575
+ lines.push(current.trim());
576
+ current = segment;
577
+ }
578
+ prev = bp;
579
+ }
580
+ if (current.trim()) {
581
+ lines.push(current.trim());
582
+ }
583
+ fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight);
584
+ return lines.length > 0 ? lines : [text];
585
+ }
586
+ function fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight) {
587
+ if (lines.length < 2) return;
588
+ const last = lines[lines.length - 1];
589
+ const lastWords = last.split(/\s+/).filter(Boolean);
590
+ if (lastWords.length > 1) return;
591
+ const prevLine = lines[lines.length - 2];
592
+ const prevWords = prevLine.split(/\s+/).filter(Boolean);
593
+ if (prevWords.length < 2) return;
594
+ const moved = prevWords.pop();
595
+ const newPrev = prevWords.join(" ");
596
+ const newLast = `${moved} ${last}`.trim();
597
+ const newPrevWidth = measureTextWidth(ctx, newPrev, fontSize, fontFamily, fontWeight);
598
+ const newLastWidth = measureTextWidth(ctx, newLast, fontSize, fontFamily, fontWeight);
599
+ if (newPrevWidth > maxWidth || newLastWidth > maxWidth) return;
600
+ lines[lines.length - 2] = newPrev;
601
+ lines[lines.length - 1] = newLast;
663
602
  }
664
603
  function wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
665
604
  const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);
666
605
  if (textWidth <= maxWidth) {
667
606
  return [text];
668
607
  }
669
- const estimatedLines = Math.ceil(textWidth / maxWidth);
670
- const breakPoints = findAllBreakPoints(text);
671
- const { bestLines } = tryBreakPointsForMultipleLines(
672
- ctx,
673
- text,
674
- 0,
675
- estimatedLines,
676
- [],
677
- maxWidth,
678
- fontSize,
679
- fontFamily,
680
- fontWeight,
681
- breakPoints
682
- );
683
- return bestLines.length > 0 ? bestLines : [text];
608
+ return greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
684
609
  }
685
610
  function formLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
686
611
  const result = [];
@@ -1048,6 +973,34 @@ function charProgress(relativeFrame, fps, charIndex, staggerMs, durationMs) {
1048
973
  const raw = (tMs - startMs) / durationMs;
1049
974
  return easeOutExpo(Math.min(1, raw));
1050
975
  }
976
+ const charSlotsCache = /* @__PURE__ */ new Map();
977
+ const staggerFinalRasterCache = /* @__PURE__ */ new Map();
978
+ function layoutCacheKey(layer, canvasWidth, canvasHeight) {
979
+ const ts = layer.fontConfig?.textStyle;
980
+ return [
981
+ layer.id,
982
+ layer.text,
983
+ layer.letterCase ?? "",
984
+ canvasWidth,
985
+ canvasHeight,
986
+ ts?.fontSize,
987
+ ts?.fontFamily,
988
+ ts?.fontWeight,
989
+ ts?.lineHeight,
990
+ JSON.stringify(layer.fontConfig?.globalPosition ?? null)
991
+ ].join("");
992
+ }
993
+ function staggerRasterKey(layer, canvasWidth, canvasHeight, preset) {
994
+ return `${layoutCacheKey(layer, canvasWidth, canvasHeight)}${preset}`;
995
+ }
996
+ function staggerEntranceEndMs(slotCount, preset) {
997
+ if (slotCount <= 0) return 0;
998
+ const lastIndex = slotCount - 1;
999
+ if (preset === "typewriter") {
1000
+ return lastIndex * DEFAULT_STAGGER_MS + DEFAULT_STAGGER_MS;
1001
+ }
1002
+ return lastIndex * DEFAULT_STAGGER_MS + DEFAULT_DURATION_MS;
1003
+ }
1051
1004
  function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
1052
1005
  const fontConfig = layer.fontConfig?.textStyle;
1053
1006
  if (!fontConfig) return null;
@@ -1060,8 +1013,8 @@ function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
1060
1013
  let lines;
1061
1014
  if (layer.wordTimings && layer.wordTimings.length > 0) {
1062
1015
  const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
1063
- const words = text.split(needsSpace ? /\s+/ : "");
1064
- lines = formEvenLinesWithWords(
1016
+ const words = needsSpace ? text.split(/\s+/) : Array.from(text);
1017
+ lines = getCachedEvenLinesWithWords(
1065
1018
  ctx,
1066
1019
  words,
1067
1020
  maxWidth,
@@ -1071,7 +1024,7 @@ function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
1071
1024
  fontWeight
1072
1025
  );
1073
1026
  } else {
1074
- lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
1027
+ lines = getCachedWrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
1075
1028
  }
1076
1029
  const totalHeight = lines.length * fontSize * lineHeight;
1077
1030
  const startY = calculateYPosition$3(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
@@ -1099,9 +1052,19 @@ function buildCharSlots(ctx, layer, canvasWidth, canvasHeight) {
1099
1052
  ctx.restore();
1100
1053
  return { slots, fontSize, lineHeight };
1101
1054
  }
1102
- function renderCaptionStaggerEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps, preset) {
1055
+ function getCachedCharSlots(ctx, layer, canvasWidth, canvasHeight) {
1056
+ const key = layoutCacheKey(layer, canvasWidth, canvasHeight);
1057
+ const cached = charSlotsCache.get(key);
1058
+ if (cached) {
1059
+ return cached;
1060
+ }
1103
1061
  const built = buildCharSlots(ctx, layer, canvasWidth, canvasHeight);
1104
- if (!built) return;
1062
+ if (built) {
1063
+ charSlotsCache.set(key, built);
1064
+ }
1065
+ return built;
1066
+ }
1067
+ function drawStaggerEntranceFrame(ctx, layer, built, relativeFrame, fps, preset) {
1105
1068
  const { slots, fontSize } = built;
1106
1069
  const fontConfig = layer.fontConfig.textStyle;
1107
1070
  const fontFamily = fontConfig.fontFamily;
@@ -1193,6 +1156,37 @@ function renderCaptionStaggerEntrance(ctx, layer, canvasWidth, canvasHeight, rel
1193
1156
  }
1194
1157
  ctx.restore();
1195
1158
  }
1159
+ function clearCaptionStaggerCache() {
1160
+ charSlotsCache.clear();
1161
+ for (const bitmap of staggerFinalRasterCache.values()) {
1162
+ bitmap.close();
1163
+ }
1164
+ staggerFinalRasterCache.clear();
1165
+ }
1166
+ function renderCaptionStaggerEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps, preset) {
1167
+ const built = getCachedCharSlots(ctx, layer, canvasWidth, canvasHeight);
1168
+ if (!built) return;
1169
+ const endMs = staggerEntranceEndMs(built.slots.length, preset);
1170
+ const endFrame = Math.ceil(endMs / 1e3 * fps);
1171
+ if (relativeFrame >= endFrame) {
1172
+ const rasterKey = staggerRasterKey(layer, canvasWidth, canvasHeight, preset);
1173
+ let bitmap = staggerFinalRasterCache.get(rasterKey);
1174
+ if (!bitmap) {
1175
+ const offscreen = new OffscreenCanvas(canvasWidth, canvasHeight);
1176
+ const offCtx = offscreen.getContext("2d");
1177
+ if (!offCtx) {
1178
+ drawStaggerEntranceFrame(ctx, layer, built, endFrame, fps, preset);
1179
+ return;
1180
+ }
1181
+ drawStaggerEntranceFrame(offCtx, layer, built, endFrame, fps, preset);
1182
+ bitmap = offscreen.transferToImageBitmap();
1183
+ staggerFinalRasterCache.set(rasterKey, bitmap);
1184
+ }
1185
+ ctx.drawImage(bitmap, 0, 0);
1186
+ return;
1187
+ }
1188
+ drawStaggerEntranceFrame(ctx, layer, built, relativeFrame, fps, preset);
1189
+ }
1196
1190
  function usToFrame$2(us, fps) {
1197
1191
  return Math.floor(us / (1e6 / fps));
1198
1192
  }
@@ -2394,6 +2388,7 @@ class VideoComposer {
2394
2388
  flush: async () => {
2395
2389
  this.filterProcessor.clearCache();
2396
2390
  clearTextLayoutCache();
2391
+ clearCaptionStaggerCache();
2397
2392
  }
2398
2393
  },
2399
2394
  {
@@ -3550,4 +3545,4 @@ const export_worker = null;
3550
3545
  export {
3551
3546
  export_worker as default
3552
3547
  };
3553
- //# sourceMappingURL=export.worker.C9m51RNP.js.map
3548
+ //# sourceMappingURL=export.worker.p7X_YtxQ.js.map