@meframe/core 0.0.18 → 0.0.20
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/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +2 -2
- package/dist/Meframe.js.map +1 -1
- package/dist/config/types.d.ts +4 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.d.ts +1 -4
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js +7 -40
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/controllers/types.d.ts +0 -4
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +2 -0
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/patch.js +1 -25
- package/dist/model/patch.js.map +1 -1
- package/dist/model/types.d.ts +18 -0
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts +1 -0
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +36 -10
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +0 -1
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +2 -1
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +0 -2
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/LayerRenderer.d.ts +4 -7
- package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +1 -9
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts +1 -0
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/font-system/FontManager.d.ts +11 -0
- package/dist/stages/compose/font-system/FontManager.d.ts.map +1 -0
- package/dist/stages/compose/font-system/FontManager.js +69 -0
- package/dist/stages/compose/font-system/FontManager.js.map +1 -0
- package/dist/stages/compose/font-system/font-templates.d.ts +12 -0
- package/dist/stages/compose/font-system/font-templates.d.ts.map +1 -0
- package/dist/stages/compose/font-system/font-templates.js +384 -0
- package/dist/stages/compose/font-system/font-templates.js.map +1 -0
- package/dist/stages/compose/font-system/index.d.ts +5 -0
- package/dist/stages/compose/font-system/index.d.ts.map +1 -0
- package/dist/stages/compose/font-system/types.d.ts +60 -0
- package/dist/stages/compose/font-system/types.d.ts.map +1 -0
- package/dist/stages/compose/instructions.d.ts +50 -0
- package/dist/stages/compose/instructions.d.ts.map +1 -1
- package/dist/stages/compose/text-renderers/animation-utils.d.ts +16 -0
- package/dist/stages/compose/text-renderers/animation-utils.d.ts.map +1 -0
- package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +5 -0
- package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -0
- package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +4 -0
- package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -0
- package/dist/stages/compose/text-renderers/index.d.ts +6 -0
- package/dist/stages/compose/text-renderers/index.d.ts.map +1 -0
- package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +4 -0
- package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -0
- package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +4 -0
- package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -0
- package/dist/stages/compose/text-utils/index.d.ts +4 -0
- package/dist/stages/compose/text-utils/index.d.ts.map +1 -0
- package/dist/stages/compose/text-utils/locale-detector.d.ts +5 -0
- package/dist/stages/compose/text-utils/locale-detector.d.ts.map +1 -0
- package/dist/stages/compose/text-utils/text-metrics.d.ts +3 -0
- package/dist/stages/compose/text-utils/text-metrics.d.ts.map +1 -0
- package/dist/stages/compose/text-utils/text-wrapper.d.ts +4 -0
- package/dist/stages/compose/text-utils/text-wrapper.d.ts.map +1 -0
- package/dist/stages/compose/types.d.ts +51 -1
- package/dist/stages/compose/types.d.ts.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.js +10 -12
- package/dist/stages/mux/MuxManager.js.map +1 -1
- package/dist/workers/MP4Demuxer.js +8 -1
- package/dist/workers/MP4Demuxer.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +838 -78
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/stages/demux/aac-esds-extractor.d.ts +0 -7
- package/dist/stages/demux/aac-esds-extractor.d.ts.map +0 -1
|
@@ -12,16 +12,794 @@ function frameDurationFromFps(fps) {
|
|
|
12
12
|
const duration = MICROSECONDS_PER_SECOND / normalized;
|
|
13
13
|
return Math.max(Math.round(duration), 1);
|
|
14
14
|
}
|
|
15
|
+
function measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight = 400) {
|
|
16
|
+
ctx.save();
|
|
17
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
18
|
+
const metrics = ctx.measureText(text);
|
|
19
|
+
ctx.restore();
|
|
20
|
+
return metrics.width;
|
|
21
|
+
}
|
|
22
|
+
function getLetterCaseText(text, letterCase) {
|
|
23
|
+
if (letterCase === "upper") {
|
|
24
|
+
return text.toUpperCase();
|
|
25
|
+
}
|
|
26
|
+
if (letterCase === "lower") {
|
|
27
|
+
return text.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
function findAllBreakPoints(text) {
|
|
32
|
+
const breakPoints = [0];
|
|
33
|
+
const chars = Array.from(text);
|
|
34
|
+
for (let i = 1; i < chars.length - 1; i++) {
|
|
35
|
+
if (/[、。!?,,!?;:]/.test(chars[i])) {
|
|
36
|
+
breakPoints.push(i + 1);
|
|
37
|
+
} else if (/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(chars[i]) && /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(chars[i + 1])) {
|
|
38
|
+
breakPoints.push(i + 1);
|
|
39
|
+
} else if (/[\s\-–—,.!?;:]/.test(chars[i])) {
|
|
40
|
+
breakPoints.push(i + 1);
|
|
41
|
+
} else if (/\s/.test(chars[i]) && /[a-zA-Z]/.test(chars[i + 1])) {
|
|
42
|
+
breakPoints.push(i + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
breakPoints.push(chars.length);
|
|
46
|
+
return breakPoints;
|
|
47
|
+
}
|
|
48
|
+
function evaluateBalance(lines, ctx, fontSize, fontFamily, fontWeight) {
|
|
49
|
+
if (lines.length <= 1) return 0;
|
|
50
|
+
const lengths = lines.map(
|
|
51
|
+
(line) => measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)
|
|
52
|
+
);
|
|
53
|
+
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
54
|
+
return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);
|
|
55
|
+
}
|
|
56
|
+
function tryBreakPointsForMultipleLines(ctx, text, start, remainingLines, currentLines, maxWidth, fontSize, fontFamily, fontWeight, breakPoints) {
|
|
57
|
+
let bestLines = [];
|
|
58
|
+
let bestBalance = Infinity;
|
|
59
|
+
if (remainingLines === 1) {
|
|
60
|
+
const lastLine = text.slice(start).trim();
|
|
61
|
+
const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);
|
|
62
|
+
if (lastLineWidth <= maxWidth) {
|
|
63
|
+
const allLines = [...currentLines, lastLine];
|
|
64
|
+
const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);
|
|
65
|
+
if (balance < bestBalance) {
|
|
66
|
+
bestBalance = balance;
|
|
67
|
+
bestLines = allLines;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
const words = lastLine.split(/\s+/);
|
|
71
|
+
let currentLine = "";
|
|
72
|
+
let tempLines = [...currentLines];
|
|
73
|
+
for (const word of words) {
|
|
74
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
75
|
+
const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
|
|
76
|
+
if (lineWidth <= maxWidth) {
|
|
77
|
+
currentLine = testLine;
|
|
78
|
+
} else {
|
|
79
|
+
if (currentLine) {
|
|
80
|
+
tempLines.push(currentLine);
|
|
81
|
+
currentLine = word;
|
|
82
|
+
} else {
|
|
83
|
+
tempLines.push(word);
|
|
84
|
+
currentLine = "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (currentLine) {
|
|
89
|
+
tempLines.push(currentLine);
|
|
90
|
+
}
|
|
91
|
+
const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);
|
|
92
|
+
if (balance < bestBalance) {
|
|
93
|
+
bestBalance = balance;
|
|
94
|
+
bestLines = tempLines;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { bestLines, bestBalance };
|
|
98
|
+
}
|
|
99
|
+
let foundValidBreak = false;
|
|
100
|
+
for (let i = 0; i < breakPoints.length; i++) {
|
|
101
|
+
const bp = breakPoints[i];
|
|
102
|
+
if (bp <= start || bp >= text.length) continue;
|
|
103
|
+
const line = text.slice(start, bp).trim();
|
|
104
|
+
const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
|
|
105
|
+
if (lineWidth <= maxWidth) {
|
|
106
|
+
foundValidBreak = true;
|
|
107
|
+
const result = tryBreakPointsForMultipleLines(
|
|
108
|
+
ctx,
|
|
109
|
+
text,
|
|
110
|
+
bp,
|
|
111
|
+
remainingLines - 1,
|
|
112
|
+
[...currentLines, line],
|
|
113
|
+
maxWidth,
|
|
114
|
+
fontSize,
|
|
115
|
+
fontFamily,
|
|
116
|
+
fontWeight,
|
|
117
|
+
breakPoints
|
|
118
|
+
);
|
|
119
|
+
if (result.bestBalance < bestBalance) {
|
|
120
|
+
bestBalance = result.bestBalance;
|
|
121
|
+
bestLines = result.bestLines;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!foundValidBreak) {
|
|
126
|
+
const textPortion = text.slice(start);
|
|
127
|
+
const words = textPortion.split(/\s+/);
|
|
128
|
+
let currentLine = "";
|
|
129
|
+
let tempLines = [...currentLines];
|
|
130
|
+
for (const word of words) {
|
|
131
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
132
|
+
const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
|
|
133
|
+
if (lineWidth <= maxWidth) {
|
|
134
|
+
currentLine = testLine;
|
|
135
|
+
} else {
|
|
136
|
+
if (currentLine) {
|
|
137
|
+
tempLines.push(currentLine);
|
|
138
|
+
currentLine = word;
|
|
139
|
+
} else {
|
|
140
|
+
tempLines.push(word);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (currentLine) {
|
|
145
|
+
tempLines.push(currentLine);
|
|
146
|
+
}
|
|
147
|
+
bestLines = tempLines;
|
|
148
|
+
}
|
|
149
|
+
return { bestLines, bestBalance };
|
|
150
|
+
}
|
|
151
|
+
function wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
|
|
152
|
+
const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);
|
|
153
|
+
if (textWidth <= maxWidth) {
|
|
154
|
+
return [text];
|
|
155
|
+
}
|
|
156
|
+
const estimatedLines = Math.ceil(textWidth / maxWidth);
|
|
157
|
+
const breakPoints = findAllBreakPoints(text);
|
|
158
|
+
const { bestLines } = tryBreakPointsForMultipleLines(
|
|
159
|
+
ctx,
|
|
160
|
+
text,
|
|
161
|
+
0,
|
|
162
|
+
estimatedLines,
|
|
163
|
+
[],
|
|
164
|
+
maxWidth,
|
|
165
|
+
fontSize,
|
|
166
|
+
fontFamily,
|
|
167
|
+
fontWeight,
|
|
168
|
+
breakPoints
|
|
169
|
+
);
|
|
170
|
+
return bestLines.length > 0 ? bestLines : [text];
|
|
171
|
+
}
|
|
172
|
+
function formLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
|
|
173
|
+
const result = [];
|
|
174
|
+
let accumulatedWidth = 0;
|
|
175
|
+
const spaceWidth = measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight);
|
|
176
|
+
let currentLine = "";
|
|
177
|
+
for (const word of words) {
|
|
178
|
+
let wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
|
|
179
|
+
if (needsSpace) {
|
|
180
|
+
wordWidth += spaceWidth;
|
|
181
|
+
}
|
|
182
|
+
if (wordWidth + accumulatedWidth <= maxWidth) {
|
|
183
|
+
currentLine += word + (needsSpace ? " " : "");
|
|
184
|
+
accumulatedWidth += wordWidth;
|
|
185
|
+
} else {
|
|
186
|
+
if (currentLine) {
|
|
187
|
+
result.push(currentLine);
|
|
188
|
+
}
|
|
189
|
+
currentLine = word + (needsSpace ? " " : "");
|
|
190
|
+
accumulatedWidth = wordWidth;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (currentLine !== "") {
|
|
194
|
+
result.push(currentLine);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
function formEvenLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
|
|
199
|
+
let minWidth = maxWidth / 2;
|
|
200
|
+
for (const word of words) {
|
|
201
|
+
const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
|
|
202
|
+
if (wordWidth > minWidth) {
|
|
203
|
+
minWidth = wordWidth;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const leastLineNum = formLinesWithWords(
|
|
207
|
+
ctx,
|
|
208
|
+
words,
|
|
209
|
+
maxWidth,
|
|
210
|
+
fontSize,
|
|
211
|
+
needsSpace,
|
|
212
|
+
fontFamily,
|
|
213
|
+
fontWeight
|
|
214
|
+
).length;
|
|
215
|
+
let bestDelta = maxWidth;
|
|
216
|
+
let bestWidth = minWidth;
|
|
217
|
+
for (let width = maxWidth; width >= minWidth; width -= 1) {
|
|
218
|
+
const lines = formLinesWithWords(
|
|
219
|
+
ctx,
|
|
220
|
+
words,
|
|
221
|
+
width,
|
|
222
|
+
fontSize,
|
|
223
|
+
needsSpace,
|
|
224
|
+
fontFamily,
|
|
225
|
+
fontWeight
|
|
226
|
+
);
|
|
227
|
+
if (lines.length > leastLineNum) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
let minLineWidth = Infinity;
|
|
231
|
+
let maxLineWidth = 0;
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
|
|
234
|
+
if (lineWidth < minLineWidth) {
|
|
235
|
+
minLineWidth = lineWidth;
|
|
236
|
+
}
|
|
237
|
+
if (lineWidth > maxLineWidth) {
|
|
238
|
+
maxLineWidth = lineWidth;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const delta = maxLineWidth - minLineWidth;
|
|
242
|
+
if (delta < bestDelta) {
|
|
243
|
+
bestDelta = delta;
|
|
244
|
+
bestWidth = width;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return formLinesWithWords(ctx, words, bestWidth, fontSize, needsSpace, fontFamily, fontWeight);
|
|
248
|
+
}
|
|
249
|
+
function springEasing(frame, config) {
|
|
250
|
+
const { damping, mass, stiffness, overshootClamping = false } = config;
|
|
251
|
+
if (frame <= 0) return 0;
|
|
252
|
+
const zeta = damping / (2 * Math.sqrt(mass * stiffness));
|
|
253
|
+
const omega = Math.sqrt(stiffness / mass);
|
|
254
|
+
const t = frame / 60;
|
|
255
|
+
let value;
|
|
256
|
+
if (zeta < 1) {
|
|
257
|
+
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
258
|
+
const A = 1;
|
|
259
|
+
const B = zeta * omega / omegaD;
|
|
260
|
+
value = 1 - Math.exp(-zeta * omega * t) * (A * Math.cos(omegaD * t) + B * Math.sin(omegaD * t));
|
|
261
|
+
} else if (zeta === 1) {
|
|
262
|
+
value = 1 - Math.exp(-omega * t) * (1 + omega * t);
|
|
263
|
+
} else {
|
|
264
|
+
const r1 = -omega * (zeta - Math.sqrt(zeta * zeta - 1));
|
|
265
|
+
const r2 = -omega * (zeta + Math.sqrt(zeta * zeta - 1));
|
|
266
|
+
const A = r2 / (r2 - r1);
|
|
267
|
+
const B = 1 - A;
|
|
268
|
+
value = 1 - A * Math.exp(r1 * t) - B * Math.exp(r2 * t);
|
|
269
|
+
}
|
|
270
|
+
if (overshootClamping && value > 1) {
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
return Math.max(0, value);
|
|
274
|
+
}
|
|
275
|
+
function interpolate(value, inputRange, outputRange, options) {
|
|
276
|
+
const { extrapolateLeft = "extend", extrapolateRight = "extend" } = options || {};
|
|
277
|
+
const [inputMin, inputMax] = inputRange;
|
|
278
|
+
const [outputMin, outputMax] = outputRange;
|
|
279
|
+
if (value < inputMin) {
|
|
280
|
+
if (extrapolateLeft === "clamp") {
|
|
281
|
+
return outputMin;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (value > inputMax) {
|
|
285
|
+
if (extrapolateRight === "clamp") {
|
|
286
|
+
return outputMax;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const inputDelta = inputMax - inputMin;
|
|
290
|
+
if (inputDelta === 0) {
|
|
291
|
+
return outputMin;
|
|
292
|
+
}
|
|
293
|
+
const progress = (value - inputMin) / inputDelta;
|
|
294
|
+
const outputDelta = outputMax - outputMin;
|
|
295
|
+
return outputMin + progress * outputDelta;
|
|
296
|
+
}
|
|
297
|
+
function parseRgb(color) {
|
|
298
|
+
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
299
|
+
if (match) {
|
|
300
|
+
return {
|
|
301
|
+
r: parseInt(match[1], 10),
|
|
302
|
+
g: parseInt(match[2], 10),
|
|
303
|
+
b: parseInt(match[3], 10)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
function interpolateColor(color1, color2, progress) {
|
|
309
|
+
const rgb1 = parseRgb(color1);
|
|
310
|
+
const rgb2 = parseRgb(color2);
|
|
311
|
+
if (!rgb1 || !rgb2) {
|
|
312
|
+
return color1;
|
|
313
|
+
}
|
|
314
|
+
const r = Math.round(interpolate(progress, [0, 1], [rgb1.r, rgb2.r]));
|
|
315
|
+
const g = Math.round(interpolate(progress, [0, 1], [rgb1.g, rgb2.g]));
|
|
316
|
+
const b = Math.round(interpolate(progress, [0, 1], [rgb1.b, rgb2.b]));
|
|
317
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
318
|
+
}
|
|
319
|
+
const CJK_REGEX = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\u3400-\u4dbf\uac00-\ud7af]/g;
|
|
320
|
+
function needsSpaceBetweenWords(locale, text) {
|
|
321
|
+
if (text) {
|
|
322
|
+
const cjkMatches = text.match(CJK_REGEX);
|
|
323
|
+
const cjkCount = cjkMatches ? cjkMatches.length : 0;
|
|
324
|
+
if (cjkCount > 0 && cjkCount / text.length >= 0.6) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
return !["zh-CN", "ja-JP", "ko-KR"].includes(locale);
|
|
330
|
+
}
|
|
331
|
+
function calculateYPosition$3(canvasHeight, totalHeight, globalPosition) {
|
|
332
|
+
if (!globalPosition) {
|
|
333
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
334
|
+
}
|
|
335
|
+
if (globalPosition.top) {
|
|
336
|
+
const topPercent = parseFloat(globalPosition.top) / 100;
|
|
337
|
+
return canvasHeight * topPercent;
|
|
338
|
+
}
|
|
339
|
+
if (globalPosition.bottom) {
|
|
340
|
+
const bottomPercent = parseFloat(globalPosition.bottom) / 100;
|
|
341
|
+
return canvasHeight * (1 - bottomPercent) - totalHeight;
|
|
342
|
+
}
|
|
343
|
+
if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
|
|
344
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
345
|
+
}
|
|
346
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
347
|
+
}
|
|
348
|
+
function renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame) {
|
|
349
|
+
const fontConfig = layer.fontConfig?.textStyle;
|
|
350
|
+
if (!fontConfig) return;
|
|
351
|
+
const fontSize = fontConfig.fontSize;
|
|
352
|
+
const fontFamily = fontConfig.fontFamily;
|
|
353
|
+
const fontWeight = fontConfig.fontWeight;
|
|
354
|
+
const fill = fontConfig.fill;
|
|
355
|
+
const stroke = fontConfig.stroke;
|
|
356
|
+
const strokeWidth = fontConfig.strokeWidth || 0;
|
|
357
|
+
const lineHeight = fontConfig.lineHeight || 1.2;
|
|
358
|
+
const maxWidth = canvasWidth * 0.64;
|
|
359
|
+
const text = getLetterCaseText(layer.text, layer.letterCase);
|
|
360
|
+
let lines;
|
|
361
|
+
if (layer.wordTimings && layer.wordTimings.length > 0) {
|
|
362
|
+
const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
|
|
363
|
+
const words = text.split(needsSpace ? /\s+/ : "");
|
|
364
|
+
lines = formEvenLinesWithWords(
|
|
365
|
+
ctx,
|
|
366
|
+
words,
|
|
367
|
+
maxWidth,
|
|
368
|
+
fontSize,
|
|
369
|
+
needsSpace,
|
|
370
|
+
fontFamily,
|
|
371
|
+
fontWeight
|
|
372
|
+
);
|
|
373
|
+
} else {
|
|
374
|
+
lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
|
|
375
|
+
}
|
|
376
|
+
ctx.save();
|
|
377
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
378
|
+
ctx.textAlign = "center";
|
|
379
|
+
ctx.textBaseline = "middle";
|
|
380
|
+
ctx.lineJoin = "round";
|
|
381
|
+
ctx.lineCap = "round";
|
|
382
|
+
const totalHeight = lines.length * fontSize * lineHeight;
|
|
383
|
+
const startY = calculateYPosition$3(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
|
|
384
|
+
for (let i = 0; i < lines.length; i++) {
|
|
385
|
+
const line = lines[i];
|
|
386
|
+
const y = startY + i * fontSize * lineHeight + fontSize / 2;
|
|
387
|
+
if (stroke && strokeWidth > 0) {
|
|
388
|
+
ctx.strokeStyle = stroke;
|
|
389
|
+
ctx.lineWidth = strokeWidth;
|
|
390
|
+
ctx.strokeText(line, canvasWidth / 2, y);
|
|
391
|
+
}
|
|
392
|
+
ctx.fillStyle = fill;
|
|
393
|
+
ctx.fillText(line, canvasWidth / 2, y);
|
|
394
|
+
}
|
|
395
|
+
ctx.restore();
|
|
396
|
+
}
|
|
397
|
+
function renderTextWithEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame) {
|
|
398
|
+
const fontConfig = layer.fontConfig?.textStyle;
|
|
399
|
+
if (!fontConfig) return;
|
|
400
|
+
const entrance = springEasing(relativeFrame, {
|
|
401
|
+
damping: 200,
|
|
402
|
+
mass: 1,
|
|
403
|
+
stiffness: 300
|
|
404
|
+
});
|
|
405
|
+
const opacity = interpolate(entrance, [0, 1], [0, 1]);
|
|
406
|
+
const scale = interpolate(entrance, [0, 1], [0.9, 1]);
|
|
407
|
+
ctx.save();
|
|
408
|
+
ctx.globalAlpha = opacity;
|
|
409
|
+
ctx.translate(canvasWidth / 2, canvasHeight / 2);
|
|
410
|
+
ctx.scale(scale, scale);
|
|
411
|
+
ctx.translate(-canvasWidth / 2, -canvasHeight / 2);
|
|
412
|
+
renderBasicText(ctx, layer, canvasWidth, canvasHeight);
|
|
413
|
+
ctx.restore();
|
|
414
|
+
}
|
|
415
|
+
function usToFrame$2(us, fps) {
|
|
416
|
+
return Math.floor(us / (1e6 / fps));
|
|
417
|
+
}
|
|
418
|
+
function calculateYPosition$2(canvasHeight, totalHeight, globalPosition) {
|
|
419
|
+
if (!globalPosition) {
|
|
420
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
421
|
+
}
|
|
422
|
+
if (globalPosition.top) {
|
|
423
|
+
const topPercent = parseFloat(globalPosition.top) / 100;
|
|
424
|
+
return canvasHeight * topPercent;
|
|
425
|
+
}
|
|
426
|
+
if (globalPosition.bottom) {
|
|
427
|
+
const bottomPercent = parseFloat(globalPosition.bottom) / 100;
|
|
428
|
+
return canvasHeight * (1 - bottomPercent) - totalHeight;
|
|
429
|
+
}
|
|
430
|
+
if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
|
|
431
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
432
|
+
}
|
|
433
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
434
|
+
}
|
|
435
|
+
function renderWordByWord(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
|
|
436
|
+
const fontConfig = layer.fontConfig?.textStyle;
|
|
437
|
+
if (!fontConfig) return;
|
|
438
|
+
const fontSize = fontConfig.fontSize;
|
|
439
|
+
const fontFamily = fontConfig.fontFamily;
|
|
440
|
+
const fontWeight = fontConfig.fontWeight;
|
|
441
|
+
const fill = fontConfig.fill;
|
|
442
|
+
const stroke = fontConfig.stroke;
|
|
443
|
+
const strokeWidth = fontConfig.strokeWidth || 0;
|
|
444
|
+
const lineHeight = fontConfig.lineHeight || 1.2;
|
|
445
|
+
const highlightFill = layer.animation?.highlightTextStyle?.fill || "rgb(255, 215, 0)";
|
|
446
|
+
const highlightStroke = layer.animation?.highlightTextStyle?.stroke || stroke;
|
|
447
|
+
const maxWidth = canvasWidth * 0.64;
|
|
448
|
+
const text = getLetterCaseText(layer.text, layer.letterCase);
|
|
449
|
+
const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
|
|
450
|
+
const words = text.split(needsSpace ? /\s+/ : "");
|
|
451
|
+
const lines = formEvenLinesWithWords(
|
|
452
|
+
ctx,
|
|
453
|
+
words,
|
|
454
|
+
maxWidth,
|
|
455
|
+
fontSize,
|
|
456
|
+
needsSpace,
|
|
457
|
+
fontFamily,
|
|
458
|
+
fontWeight
|
|
459
|
+
);
|
|
460
|
+
const wordPositions = [];
|
|
461
|
+
let wordIndex = 0;
|
|
462
|
+
const totalHeight = lines.length * fontSize * lineHeight;
|
|
463
|
+
const startY = calculateYPosition$2(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
|
|
464
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
465
|
+
const line = lines[lineIndex];
|
|
466
|
+
const lineWords = line.split(needsSpace ? /\s+/ : "");
|
|
467
|
+
const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
|
|
468
|
+
const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
|
|
469
|
+
let currentX = canvasWidth / 2 - lineWidth / 2;
|
|
470
|
+
for (const word of lineWords) {
|
|
471
|
+
const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
|
|
472
|
+
const wordTimingUs = layer.wordTimings?.[wordIndex];
|
|
473
|
+
const wordTiming = wordTimingUs ? {
|
|
474
|
+
startFrame: usToFrame$2(wordTimingUs.startUs, fps),
|
|
475
|
+
endFrame: usToFrame$2(wordTimingUs.endUs, fps)
|
|
476
|
+
} : void 0;
|
|
477
|
+
wordPositions.push({
|
|
478
|
+
text: word,
|
|
479
|
+
x: currentX + wordWidth / 2,
|
|
480
|
+
y,
|
|
481
|
+
lineIndex,
|
|
482
|
+
wordIndex,
|
|
483
|
+
timing: wordTiming
|
|
484
|
+
});
|
|
485
|
+
currentX += wordWidth + (needsSpace ? measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight) : 0);
|
|
486
|
+
wordIndex++;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
ctx.save();
|
|
490
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
491
|
+
ctx.textAlign = "center";
|
|
492
|
+
ctx.textBaseline = "middle";
|
|
493
|
+
ctx.lineJoin = "round";
|
|
494
|
+
ctx.lineCap = "round";
|
|
495
|
+
for (const wordPos of wordPositions) {
|
|
496
|
+
let currentFill = fill;
|
|
497
|
+
let currentStroke = stroke;
|
|
498
|
+
if (wordPos.timing) {
|
|
499
|
+
const { startFrame, endFrame } = wordPos.timing;
|
|
500
|
+
if (relativeFrame >= startFrame && relativeFrame <= endFrame) {
|
|
501
|
+
const transitionProgressIn = interpolate(
|
|
502
|
+
relativeFrame,
|
|
503
|
+
[startFrame, startFrame + 3],
|
|
504
|
+
[0, 1],
|
|
505
|
+
{
|
|
506
|
+
extrapolateLeft: "clamp",
|
|
507
|
+
extrapolateRight: "clamp"
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
currentFill = interpolateColor(fill, highlightFill, transitionProgressIn);
|
|
511
|
+
if (stroke && highlightStroke) {
|
|
512
|
+
currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressIn);
|
|
513
|
+
}
|
|
514
|
+
} else if (relativeFrame > endFrame) {
|
|
515
|
+
const transitionProgressOut = interpolate(relativeFrame, [endFrame, endFrame + 3], [1, 0], {
|
|
516
|
+
extrapolateLeft: "clamp",
|
|
517
|
+
extrapolateRight: "clamp"
|
|
518
|
+
});
|
|
519
|
+
currentFill = interpolateColor(fill, highlightFill, transitionProgressOut);
|
|
520
|
+
if (stroke && highlightStroke) {
|
|
521
|
+
currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressOut);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (currentStroke && strokeWidth > 0) {
|
|
526
|
+
ctx.strokeStyle = currentStroke;
|
|
527
|
+
ctx.lineWidth = strokeWidth;
|
|
528
|
+
ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);
|
|
529
|
+
}
|
|
530
|
+
ctx.fillStyle = currentFill;
|
|
531
|
+
ctx.fillText(wordPos.text, wordPos.x, wordPos.y);
|
|
532
|
+
}
|
|
533
|
+
ctx.restore();
|
|
534
|
+
}
|
|
535
|
+
function usToFrame$1(us, fps) {
|
|
536
|
+
return Math.floor(us / (1e6 / fps));
|
|
537
|
+
}
|
|
538
|
+
function calculateYPosition$1(canvasHeight, totalHeight, globalPosition) {
|
|
539
|
+
if (!globalPosition) {
|
|
540
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
541
|
+
}
|
|
542
|
+
if (globalPosition.top) {
|
|
543
|
+
const topPercent = parseFloat(globalPosition.top) / 100;
|
|
544
|
+
return canvasHeight * topPercent;
|
|
545
|
+
}
|
|
546
|
+
if (globalPosition.bottom) {
|
|
547
|
+
const bottomPercent = parseFloat(globalPosition.bottom) / 100;
|
|
548
|
+
return canvasHeight * (1 - bottomPercent) - totalHeight;
|
|
549
|
+
}
|
|
550
|
+
if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
|
|
551
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
552
|
+
}
|
|
553
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
554
|
+
}
|
|
555
|
+
function renderCharacterKTV(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
|
|
556
|
+
const fontConfig = layer.fontConfig?.textStyle;
|
|
557
|
+
if (!fontConfig) return;
|
|
558
|
+
const fontSize = fontConfig.fontSize;
|
|
559
|
+
const fontFamily = fontConfig.fontFamily;
|
|
560
|
+
const fontWeight = fontConfig.fontWeight;
|
|
561
|
+
const baseFill = fontConfig.fill;
|
|
562
|
+
const stroke = fontConfig.stroke;
|
|
563
|
+
const strokeWidth = fontConfig.strokeWidth || 0;
|
|
564
|
+
const lineHeight = fontConfig.lineHeight || 1.2;
|
|
565
|
+
const highlightFill = layer.animation?.highlightTextStyle?.fill || "rgb(255, 215, 0)";
|
|
566
|
+
const glowColor = layer.animation?.glowColor || "#ffffff";
|
|
567
|
+
const glowIntensity = layer.animation?.glowIntensity || 3;
|
|
568
|
+
const transitionFrames = layer.animation?.transitionFrames || 10;
|
|
569
|
+
const maxWidth = canvasWidth * 0.9;
|
|
570
|
+
const text = getLetterCaseText(layer.text, layer.letterCase);
|
|
571
|
+
const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
|
|
572
|
+
const characterTimings = [];
|
|
573
|
+
if (layer.wordTimings && layer.wordTimings.length > 0) {
|
|
574
|
+
let charIndex = 0;
|
|
575
|
+
for (let wordIndex = 0; wordIndex < layer.wordTimings.length; wordIndex++) {
|
|
576
|
+
const word = layer.wordTimings[wordIndex];
|
|
577
|
+
const wordChars = word.text.split("");
|
|
578
|
+
const wordStartFrame = usToFrame$1(word.startUs, fps);
|
|
579
|
+
const wordEndFrame = usToFrame$1(word.endUs, fps);
|
|
580
|
+
const framesPerChar = wordChars.length > 0 ? (wordEndFrame - wordStartFrame) / wordChars.length : 0;
|
|
581
|
+
for (let i = 0; i < wordChars.length; i++) {
|
|
582
|
+
const charStartFrame = Math.floor(wordStartFrame + i * framesPerChar);
|
|
583
|
+
characterTimings.push({
|
|
584
|
+
char: wordChars[i],
|
|
585
|
+
index: charIndex,
|
|
586
|
+
startFrame: charStartFrame
|
|
587
|
+
});
|
|
588
|
+
charIndex++;
|
|
589
|
+
}
|
|
590
|
+
if (needsSpace && wordIndex < layer.wordTimings.length - 1) {
|
|
591
|
+
const nextWord = layer.wordTimings[wordIndex + 1];
|
|
592
|
+
const nextWordFirstChar = nextWord?.text?.[0] || "";
|
|
593
|
+
const isPunctuation = /[.,!?;:)]/.test(nextWordFirstChar);
|
|
594
|
+
if (!isPunctuation) {
|
|
595
|
+
const spaceStartFrame = characterTimings[characterTimings.length - 1]?.startFrame || 0;
|
|
596
|
+
characterTimings.push({
|
|
597
|
+
char: " ",
|
|
598
|
+
index: charIndex,
|
|
599
|
+
startFrame: spaceStartFrame
|
|
600
|
+
});
|
|
601
|
+
charIndex++;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
const totalFrames = 100;
|
|
607
|
+
const chars = text.split("");
|
|
608
|
+
const framesPerChar = chars.length > 0 ? totalFrames / chars.length : 0;
|
|
609
|
+
for (let i = 0; i < chars.length; i++) {
|
|
610
|
+
characterTimings.push({
|
|
611
|
+
char: chars[i],
|
|
612
|
+
index: i,
|
|
613
|
+
startFrame: Math.floor(i * framesPerChar)
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const fullText = characterTimings.map((ct) => ct.char).join("");
|
|
618
|
+
const fullTextLines = wrapText(ctx, fullText, maxWidth, fontSize, fontFamily, fontWeight);
|
|
619
|
+
ctx.save();
|
|
620
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
621
|
+
ctx.textAlign = "left";
|
|
622
|
+
ctx.textBaseline = "middle";
|
|
623
|
+
ctx.lineJoin = "round";
|
|
624
|
+
ctx.lineCap = "round";
|
|
625
|
+
const totalHeight = fullTextLines.length * fontSize * lineHeight;
|
|
626
|
+
const startY = calculateYPosition$1(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
|
|
627
|
+
let charIndexInText = 0;
|
|
628
|
+
for (let lineIndex = 0; lineIndex < fullTextLines.length; lineIndex++) {
|
|
629
|
+
const line = fullTextLines[lineIndex];
|
|
630
|
+
const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
|
|
631
|
+
const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
|
|
632
|
+
let currentX = canvasWidth / 2 - lineWidth / 2;
|
|
633
|
+
for (let i = 0; i < line.length; i++) {
|
|
634
|
+
const char = line[i];
|
|
635
|
+
const timing = characterTimings[charIndexInText];
|
|
636
|
+
if (timing) {
|
|
637
|
+
const hasScanned = relativeFrame >= timing.startFrame;
|
|
638
|
+
const isCurrentlySinging = hasScanned && relativeFrame < timing.startFrame + transitionFrames;
|
|
639
|
+
ctx.fillStyle = hasScanned ? highlightFill : baseFill;
|
|
640
|
+
if (isCurrentlySinging) {
|
|
641
|
+
ctx.shadowColor = glowColor;
|
|
642
|
+
ctx.shadowBlur = glowIntensity * 10;
|
|
643
|
+
} else {
|
|
644
|
+
ctx.shadowBlur = 0;
|
|
645
|
+
}
|
|
646
|
+
if (stroke && strokeWidth > 0) {
|
|
647
|
+
ctx.strokeStyle = stroke;
|
|
648
|
+
ctx.lineWidth = strokeWidth;
|
|
649
|
+
ctx.strokeText(char, currentX, y);
|
|
650
|
+
}
|
|
651
|
+
ctx.fillText(char, currentX, y);
|
|
652
|
+
ctx.shadowBlur = 0;
|
|
653
|
+
}
|
|
654
|
+
currentX += measureTextWidth(ctx, char, fontSize, fontFamily, fontWeight);
|
|
655
|
+
charIndexInText++;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
ctx.restore();
|
|
659
|
+
}
|
|
660
|
+
function usToFrame(us, fps) {
|
|
661
|
+
return Math.floor(us / (1e6 / fps));
|
|
662
|
+
}
|
|
663
|
+
function calculateYPosition(canvasHeight, totalHeight, globalPosition) {
|
|
664
|
+
if (!globalPosition) {
|
|
665
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
666
|
+
}
|
|
667
|
+
if (globalPosition.top) {
|
|
668
|
+
const topPercent = parseFloat(globalPosition.top) / 100;
|
|
669
|
+
return canvasHeight * topPercent;
|
|
670
|
+
}
|
|
671
|
+
if (globalPosition.bottom) {
|
|
672
|
+
const bottomPercent = parseFloat(globalPosition.bottom) / 100;
|
|
673
|
+
return canvasHeight * (1 - bottomPercent) - totalHeight;
|
|
674
|
+
}
|
|
675
|
+
if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
|
|
676
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
677
|
+
}
|
|
678
|
+
return canvasHeight / 2 - totalHeight / 2;
|
|
679
|
+
}
|
|
680
|
+
function renderWordByWordFancy(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
|
|
681
|
+
const fontConfig = layer.fontConfig?.textStyle;
|
|
682
|
+
if (!fontConfig) return;
|
|
683
|
+
const fontSize = fontConfig.fontSize;
|
|
684
|
+
const fontFamily = fontConfig.fontFamily;
|
|
685
|
+
const fontWeight = fontConfig.fontWeight;
|
|
686
|
+
const fill = fontConfig.fill;
|
|
687
|
+
const stroke = fontConfig.stroke;
|
|
688
|
+
const strokeWidth = fontConfig.strokeWidth || 0;
|
|
689
|
+
const lineHeight = fontConfig.lineHeight || 1.2;
|
|
690
|
+
const highlightBackgroundColor = layer.animation?.highlightColor || "rgb(255, 215, 0)";
|
|
691
|
+
const maxWidth = canvasWidth * 0.64;
|
|
692
|
+
const text = getLetterCaseText(layer.text, layer.letterCase);
|
|
693
|
+
const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
|
|
694
|
+
const words = text.split(needsSpace ? /\s+/ : "");
|
|
695
|
+
const lines = formEvenLinesWithWords(
|
|
696
|
+
ctx,
|
|
697
|
+
words,
|
|
698
|
+
maxWidth,
|
|
699
|
+
fontSize,
|
|
700
|
+
needsSpace,
|
|
701
|
+
fontFamily,
|
|
702
|
+
fontWeight
|
|
703
|
+
);
|
|
704
|
+
const wordPositions = [];
|
|
705
|
+
let wordIndex = 0;
|
|
706
|
+
const totalHeight = lines.length * fontSize * lineHeight;
|
|
707
|
+
const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
|
|
708
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
709
|
+
const line = lines[lineIndex];
|
|
710
|
+
const lineWords = line.split(needsSpace ? /\s+/ : "");
|
|
711
|
+
const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
|
|
712
|
+
const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
|
|
713
|
+
let currentX = canvasWidth / 2 - lineWidth / 2;
|
|
714
|
+
for (const word of lineWords) {
|
|
715
|
+
const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
|
|
716
|
+
const wordTimingUs = layer.wordTimings?.[wordIndex];
|
|
717
|
+
const wordTiming = wordTimingUs ? {
|
|
718
|
+
startFrame: usToFrame(wordTimingUs.startUs, fps),
|
|
719
|
+
endFrame: usToFrame(wordTimingUs.endUs, fps)
|
|
720
|
+
} : void 0;
|
|
721
|
+
wordPositions.push({
|
|
722
|
+
text: word,
|
|
723
|
+
x: currentX + wordWidth / 2,
|
|
724
|
+
y,
|
|
725
|
+
width: wordWidth,
|
|
726
|
+
lineIndex,
|
|
727
|
+
wordIndex,
|
|
728
|
+
timing: wordTiming
|
|
729
|
+
});
|
|
730
|
+
currentX += wordWidth + (needsSpace ? measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight) : 0);
|
|
731
|
+
wordIndex++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
ctx.save();
|
|
735
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
736
|
+
ctx.textAlign = "center";
|
|
737
|
+
ctx.textBaseline = "middle";
|
|
738
|
+
ctx.lineJoin = "round";
|
|
739
|
+
ctx.lineCap = "round";
|
|
740
|
+
for (const wordPos of wordPositions) {
|
|
741
|
+
let backgroundOpacity = 0;
|
|
742
|
+
let backgroundScale = 0.8;
|
|
743
|
+
if (wordPos.timing) {
|
|
744
|
+
const { startFrame, endFrame } = wordPos.timing;
|
|
745
|
+
const preStartFrames = 3;
|
|
746
|
+
const isActive = relativeFrame >= startFrame && relativeFrame <= endFrame;
|
|
747
|
+
if (isActive) {
|
|
748
|
+
const scaleSpringIn = springEasing(relativeFrame - (startFrame - preStartFrames), {
|
|
749
|
+
damping: 6,
|
|
750
|
+
mass: 0.35,
|
|
751
|
+
stiffness: 200,
|
|
752
|
+
overshootClamping: false
|
|
753
|
+
});
|
|
754
|
+
const inProgress = interpolate(relativeFrame, [startFrame, startFrame + 1], [0, 1], {
|
|
755
|
+
extrapolateLeft: "clamp",
|
|
756
|
+
extrapolateRight: "clamp"
|
|
757
|
+
});
|
|
758
|
+
backgroundOpacity = 0.9 * inProgress;
|
|
759
|
+
backgroundScale = 0.8 + scaleSpringIn * 0.45;
|
|
760
|
+
} else if (relativeFrame > endFrame) {
|
|
761
|
+
backgroundOpacity = 0;
|
|
762
|
+
backgroundScale = 0.8;
|
|
763
|
+
}
|
|
764
|
+
if (backgroundOpacity > 0) {
|
|
765
|
+
const padding = 8;
|
|
766
|
+
const bgWidth = (wordPos.width + padding * 2) * backgroundScale;
|
|
767
|
+
const bgHeight = (fontSize + padding) * backgroundScale;
|
|
768
|
+
ctx.save();
|
|
769
|
+
ctx.globalAlpha = backgroundOpacity;
|
|
770
|
+
ctx.fillStyle = highlightBackgroundColor;
|
|
771
|
+
ctx.beginPath();
|
|
772
|
+
ctx.roundRect(wordPos.x - bgWidth / 2, wordPos.y - bgHeight / 2, bgWidth, bgHeight, 8);
|
|
773
|
+
ctx.fill();
|
|
774
|
+
ctx.restore();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (stroke && strokeWidth > 0) {
|
|
778
|
+
ctx.strokeStyle = stroke;
|
|
779
|
+
ctx.lineWidth = strokeWidth;
|
|
780
|
+
ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);
|
|
781
|
+
}
|
|
782
|
+
ctx.fillStyle = fill;
|
|
783
|
+
ctx.fillText(wordPos.text, wordPos.x, wordPos.y);
|
|
784
|
+
}
|
|
785
|
+
ctx.restore();
|
|
786
|
+
}
|
|
15
787
|
class LayerRenderer {
|
|
16
788
|
ctx;
|
|
17
789
|
width;
|
|
18
790
|
height;
|
|
19
|
-
|
|
791
|
+
currentFrame = 0;
|
|
792
|
+
fps = 30;
|
|
793
|
+
constructor(ctx, width, height, fps = 30) {
|
|
20
794
|
this.ctx = ctx;
|
|
21
795
|
this.width = width;
|
|
22
796
|
this.height = height;
|
|
797
|
+
this.fps = fps;
|
|
23
798
|
this.ensureHighQualityRendering();
|
|
24
799
|
}
|
|
800
|
+
setCurrentFrame(frame) {
|
|
801
|
+
this.currentFrame = frame;
|
|
802
|
+
}
|
|
25
803
|
ensureHighQualityRendering() {
|
|
26
804
|
this.ctx.imageSmoothingEnabled = true;
|
|
27
805
|
this.ctx.imageSmoothingQuality = "high";
|
|
@@ -211,73 +989,30 @@ class LayerRenderer {
|
|
|
211
989
|
}
|
|
212
990
|
}
|
|
213
991
|
async renderTextLayer(layer) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
if (layer.strokeColor && layer.strokeWidth && layer.strokeWidth > 0) {
|
|
237
|
-
this.drawEnhancedStroke(layer.text, x, y, layer.strokeColor, layer.strokeWidth);
|
|
238
|
-
}
|
|
239
|
-
this.ctx.fillText(layer.text, x, y);
|
|
240
|
-
if (layer.shadow) {
|
|
241
|
-
this.ctx.shadowColor = "transparent";
|
|
242
|
-
this.ctx.shadowOffsetX = 0;
|
|
243
|
-
this.ctx.shadowOffsetY = 0;
|
|
244
|
-
this.ctx.shadowBlur = 0;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Draw enhanced multi-layer stroke for better text visibility
|
|
249
|
-
*/
|
|
250
|
-
drawEnhancedStroke(text, x, y, strokeColor, strokeWidth) {
|
|
251
|
-
this.ctx.save();
|
|
252
|
-
this.ctx.strokeStyle = strokeColor;
|
|
253
|
-
this.ctx.lineJoin = "round";
|
|
254
|
-
this.ctx.lineCap = "round";
|
|
255
|
-
this.ctx.miterLimit = 2;
|
|
256
|
-
const layers = [1.1, 1];
|
|
257
|
-
layers.forEach((multiplier) => {
|
|
258
|
-
this.ctx.lineWidth = strokeWidth * multiplier;
|
|
259
|
-
this.ctx.strokeText(text, x, y);
|
|
260
|
-
});
|
|
261
|
-
this.ctx.restore();
|
|
262
|
-
}
|
|
263
|
-
calculateTextX(align) {
|
|
264
|
-
switch (align) {
|
|
265
|
-
case "center":
|
|
266
|
-
return this.width / 2;
|
|
267
|
-
case "right":
|
|
268
|
-
return this.width;
|
|
269
|
-
default:
|
|
270
|
-
return 0;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
calculateTextY(align, fontSize = 16) {
|
|
274
|
-
switch (align) {
|
|
275
|
-
case "middle":
|
|
276
|
-
return this.height / 2;
|
|
277
|
-
case "bottom":
|
|
278
|
-
return this.height * 0.85;
|
|
992
|
+
const animationType = layer.animation?.type;
|
|
993
|
+
switch (animationType) {
|
|
994
|
+
case "wordByWord":
|
|
995
|
+
renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
|
|
996
|
+
break;
|
|
997
|
+
case "characterKTV":
|
|
998
|
+
renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
|
|
999
|
+
break;
|
|
1000
|
+
case "wordByWordFancy":
|
|
1001
|
+
renderWordByWordFancy(
|
|
1002
|
+
this.ctx,
|
|
1003
|
+
layer,
|
|
1004
|
+
this.width,
|
|
1005
|
+
this.height,
|
|
1006
|
+
this.currentFrame,
|
|
1007
|
+
this.fps
|
|
1008
|
+
);
|
|
1009
|
+
break;
|
|
1010
|
+
case "fade":
|
|
1011
|
+
renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);
|
|
1012
|
+
break;
|
|
279
1013
|
default:
|
|
280
|
-
|
|
1014
|
+
renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);
|
|
1015
|
+
break;
|
|
281
1016
|
}
|
|
282
1017
|
}
|
|
283
1018
|
applyMask(mask) {
|
|
@@ -731,8 +1466,14 @@ class VideoComposer {
|
|
|
731
1466
|
}
|
|
732
1467
|
this.ctx = ctx;
|
|
733
1468
|
this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
|
|
1469
|
+
this.loadFonts();
|
|
734
1470
|
this.ctx.imageSmoothingQuality = "high";
|
|
735
|
-
this.layerRenderer = new LayerRenderer(
|
|
1471
|
+
this.layerRenderer = new LayerRenderer(
|
|
1472
|
+
ctx,
|
|
1473
|
+
this.config.width,
|
|
1474
|
+
this.config.height,
|
|
1475
|
+
this.config.fps
|
|
1476
|
+
);
|
|
736
1477
|
this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);
|
|
737
1478
|
this.filterProcessor = new FilterProcessor();
|
|
738
1479
|
this.timelineContext = this.config.timeline;
|
|
@@ -756,7 +1497,8 @@ class VideoComposer {
|
|
|
756
1497
|
clipStartUs: 0,
|
|
757
1498
|
clipDurationUs: Infinity,
|
|
758
1499
|
compositionFps: 30
|
|
759
|
-
}
|
|
1500
|
+
},
|
|
1501
|
+
fonts: config.fonts ?? []
|
|
760
1502
|
};
|
|
761
1503
|
}
|
|
762
1504
|
createStreams(_instruction) {
|
|
@@ -795,6 +1537,9 @@ class VideoComposer {
|
|
|
795
1537
|
throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);
|
|
796
1538
|
}
|
|
797
1539
|
this.clearCanvas();
|
|
1540
|
+
const frameDurationUs = 1e6 / this.config.fps;
|
|
1541
|
+
const relativeFrame = Math.floor(request.timeUs / frameDurationUs);
|
|
1542
|
+
this.layerRenderer.setCurrentFrame(relativeFrame);
|
|
798
1543
|
if (request.transition) {
|
|
799
1544
|
this.ctx.save();
|
|
800
1545
|
this.transitionProcessor.applyTransition(this.ctx, request.transition);
|
|
@@ -870,6 +1615,22 @@ class VideoComposer {
|
|
|
870
1615
|
});
|
|
871
1616
|
return frame;
|
|
872
1617
|
}
|
|
1618
|
+
async loadFonts() {
|
|
1619
|
+
if (!this.config.fonts || this.config.fonts.length === 0) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
for (const font of this.config.fonts) {
|
|
1623
|
+
try {
|
|
1624
|
+
const fontFace = new FontFace(font.family, `url(${font.url})`);
|
|
1625
|
+
await fontFace.load();
|
|
1626
|
+
if ("fonts" in self) {
|
|
1627
|
+
self.fonts.add(fontFace);
|
|
1628
|
+
}
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
console.warn(`[VideoComposer] Failed to load font ${font.family}:`, error);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
873
1634
|
updateConfig(config) {
|
|
874
1635
|
Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
|
|
875
1636
|
if (config.width || config.height) {
|
|
@@ -884,6 +1645,9 @@ class VideoComposer {
|
|
|
884
1645
|
if (config.timeline) {
|
|
885
1646
|
this.timelineContext = config.timeline;
|
|
886
1647
|
}
|
|
1648
|
+
if (config.fonts) {
|
|
1649
|
+
this.loadFonts();
|
|
1650
|
+
}
|
|
887
1651
|
}
|
|
888
1652
|
dispose() {
|
|
889
1653
|
this.filterProcessor.clearCache();
|
|
@@ -1001,15 +1765,11 @@ function materializeLayer(layer, frame, imageMap, globalTimeUs) {
|
|
|
1001
1765
|
...baseLayer,
|
|
1002
1766
|
type: "text",
|
|
1003
1767
|
text: payload.text,
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
strokeWidth: payload.strokeWidth,
|
|
1010
|
-
lineHeight: payload.lineHeight,
|
|
1011
|
-
textAlign: payload.align,
|
|
1012
|
-
verticalAlign: "bottom"
|
|
1768
|
+
localeCode: payload.localeCode,
|
|
1769
|
+
fontConfig: payload.fontConfig,
|
|
1770
|
+
animation: payload.animation,
|
|
1771
|
+
wordTimings: payload.wordTimings,
|
|
1772
|
+
letterCase: payload.letterCase
|
|
1013
1773
|
};
|
|
1014
1774
|
}
|
|
1015
1775
|
if (layer.type === "image") {
|