@shotstack/shotstack-canvas 1.3.9 → 1.4.1
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/entry.node.cjs +193 -30
- package/dist/entry.node.d.cts +1 -1
- package/dist/entry.node.d.ts +1 -1
- package/dist/entry.node.js +193 -30
- package/dist/entry.web.d.ts +1 -1
- package/dist/entry.web.js +190 -28
- package/package.json +1 -1
package/dist/entry.node.cjs
CHANGED
|
@@ -119,7 +119,7 @@ var animationSchema = import_joi.default.object({
|
|
|
119
119
|
speed: import_joi.default.number().min(0.1).max(10).default(1),
|
|
120
120
|
duration: import_joi.default.number().min(CANVAS_CONFIG.LIMITS.minDuration).max(CANVAS_CONFIG.LIMITS.maxDuration).optional(),
|
|
121
121
|
style: import_joi.default.string().valid("character", "word").optional().when("preset", {
|
|
122
|
-
is: import_joi.default.valid("typewriter", "shift"),
|
|
122
|
+
is: import_joi.default.valid("typewriter", "shift", "fadeIn", "slideIn"),
|
|
123
123
|
then: import_joi.default.optional(),
|
|
124
124
|
otherwise: import_joi.default.forbidden()
|
|
125
125
|
}),
|
|
@@ -419,7 +419,8 @@ var FontRegistry = class _FontRegistry {
|
|
|
419
419
|
return this.hb;
|
|
420
420
|
}
|
|
421
421
|
key(desc) {
|
|
422
|
-
|
|
422
|
+
const normalizedStyle = desc.style === "oblique" ? "italic" : desc.style ?? "normal";
|
|
423
|
+
return `${desc.family}__${desc.weight ?? "400"}__${normalizedStyle}`;
|
|
423
424
|
}
|
|
424
425
|
async registerFromBytes(bytes, desc) {
|
|
425
426
|
try {
|
|
@@ -732,13 +733,11 @@ var LayoutEngine = class {
|
|
|
732
733
|
const glyph = glyphs[i];
|
|
733
734
|
const glyphWidth = glyph.xAdvance;
|
|
734
735
|
if (glyph.char === "\n") {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
});
|
|
741
|
-
}
|
|
736
|
+
lines.push({
|
|
737
|
+
glyphs: currentLine,
|
|
738
|
+
width: currentWidth,
|
|
739
|
+
y: 0
|
|
740
|
+
});
|
|
742
741
|
currentLine = [];
|
|
743
742
|
currentWidth = 0;
|
|
744
743
|
lastBreakIndex = i;
|
|
@@ -997,7 +996,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
997
996
|
if (!p.anim || !p.anim.preset) return ops;
|
|
998
997
|
const { preset } = p.anim;
|
|
999
998
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1000
|
-
|
|
999
|
+
let autoDuration = Math.max(0.3, totalGlyphs / (30 * p.anim.speed));
|
|
1000
|
+
if (p.clipDuration && !p.anim.duration) {
|
|
1001
|
+
autoDuration = Math.min(autoDuration, p.clipDuration * 0.9);
|
|
1002
|
+
}
|
|
1003
|
+
const duration = p.anim.duration ?? autoDuration;
|
|
1001
1004
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
1002
1005
|
switch (preset) {
|
|
1003
1006
|
case "typewriter":
|
|
@@ -1011,9 +1014,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
1011
1014
|
duration
|
|
1012
1015
|
);
|
|
1013
1016
|
case "fadeIn":
|
|
1014
|
-
return applyFadeInAnimation(ops, progress);
|
|
1017
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
1015
1018
|
case "slideIn":
|
|
1016
|
-
return applySlideInAnimation(ops, progress, p.anim.direction ?? "left", p.fontSize);
|
|
1019
|
+
return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
|
|
1017
1020
|
case "shift":
|
|
1018
1021
|
return applyShiftAnimation(
|
|
1019
1022
|
ops,
|
|
@@ -1034,7 +1037,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
1034
1037
|
duration
|
|
1035
1038
|
);
|
|
1036
1039
|
case "movingLetters":
|
|
1037
|
-
return applyMovingLettersAnimation(ops,
|
|
1040
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1038
1041
|
default:
|
|
1039
1042
|
return ops;
|
|
1040
1043
|
}
|
|
@@ -1216,20 +1219,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1216
1219
|
}
|
|
1217
1220
|
return result;
|
|
1218
1221
|
}
|
|
1219
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1220
|
-
const
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1222
|
+
function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
1223
|
+
const byWord = style === "word";
|
|
1224
|
+
const byCharacter = style === "character";
|
|
1225
|
+
if (!byWord && !byCharacter) {
|
|
1226
|
+
const alpha = easeOutQuad(progress);
|
|
1227
|
+
const scale = 0.95 + 0.05 * alpha;
|
|
1228
|
+
return scaleAndFade(ops, alpha, scale);
|
|
1229
|
+
}
|
|
1230
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1231
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1232
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1233
|
+
if (totalUnits === 0) return ops;
|
|
1234
|
+
const result = [];
|
|
1235
|
+
for (const op of ops) {
|
|
1236
|
+
if (op.op === "BeginFrame") {
|
|
1237
|
+
result.push(op);
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const windowDuration = 0.3;
|
|
1242
|
+
const overlapFactor = 0.7;
|
|
1243
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1244
|
+
const windowFor = (unitIdx) => {
|
|
1245
|
+
const startTime = unitIdx * staggerDelay;
|
|
1246
|
+
const startF = startTime / duration;
|
|
1247
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1248
|
+
return { startF, endF };
|
|
1249
|
+
};
|
|
1250
|
+
let glyphIndex = 0;
|
|
1251
|
+
for (const op of ops) {
|
|
1252
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1253
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
let unitIndex;
|
|
1257
|
+
if (!byWord) {
|
|
1258
|
+
unitIndex = glyphIndex;
|
|
1259
|
+
} else {
|
|
1260
|
+
let wordIndex = -1, acc = 0;
|
|
1261
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1262
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1263
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1264
|
+
wordIndex = i;
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
acc += gcount;
|
|
1268
|
+
}
|
|
1269
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1270
|
+
}
|
|
1271
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1272
|
+
if (progress <= startF) {
|
|
1273
|
+
const animated = { ...op };
|
|
1274
|
+
if (op.op === "FillPath") {
|
|
1275
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1276
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1277
|
+
} else {
|
|
1278
|
+
animated.opacity = 0;
|
|
1279
|
+
}
|
|
1280
|
+
result.push(animated);
|
|
1281
|
+
} else if (progress >= endF) {
|
|
1282
|
+
result.push(op);
|
|
1283
|
+
} else {
|
|
1284
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1285
|
+
const ease = easeOutQuad(Math.min(1, local));
|
|
1286
|
+
const animated = { ...op };
|
|
1287
|
+
if (op.op === "FillPath") {
|
|
1288
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1289
|
+
if (animated.fill.kind === "solid")
|
|
1290
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1291
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1292
|
+
} else {
|
|
1293
|
+
animated.opacity = animated.opacity * ease;
|
|
1294
|
+
}
|
|
1295
|
+
result.push(animated);
|
|
1296
|
+
}
|
|
1297
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1298
|
+
}
|
|
1299
|
+
return result;
|
|
1223
1300
|
}
|
|
1224
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1301
|
+
function applySlideInAnimation(ops, lines, progress, direction, fontSize, style, duration) {
|
|
1302
|
+
const byWord = style === "word";
|
|
1303
|
+
const byCharacter = style === "character";
|
|
1304
|
+
if (!byWord && !byCharacter) {
|
|
1305
|
+
const easeProgress = easeOutCubic(progress);
|
|
1306
|
+
const shift = shiftFor(1 - easeProgress, direction, fontSize * 2);
|
|
1307
|
+
const alpha = easeOutQuad(progress);
|
|
1308
|
+
return translateGlyphOps(ops, shift.dx, shift.dy, alpha);
|
|
1309
|
+
}
|
|
1310
|
+
const startOffsets = {
|
|
1311
|
+
left: { x: fontSize * 2, y: 0 },
|
|
1312
|
+
right: { x: -fontSize * 2, y: 0 },
|
|
1313
|
+
up: { x: 0, y: fontSize * 2 },
|
|
1314
|
+
down: { x: 0, y: -fontSize * 2 }
|
|
1315
|
+
};
|
|
1316
|
+
const offset = startOffsets[direction];
|
|
1317
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1318
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1319
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1320
|
+
if (totalUnits === 0) return ops;
|
|
1321
|
+
const result = [];
|
|
1322
|
+
for (const op of ops) {
|
|
1323
|
+
if (op.op === "BeginFrame") {
|
|
1324
|
+
result.push(op);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const windowDuration = 0.3;
|
|
1329
|
+
const overlapFactor = 0.7;
|
|
1330
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1331
|
+
const windowFor = (unitIdx) => {
|
|
1332
|
+
const startTime = unitIdx * staggerDelay;
|
|
1333
|
+
const startF = startTime / duration;
|
|
1334
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1335
|
+
return { startF, endF };
|
|
1336
|
+
};
|
|
1337
|
+
let glyphIndex = 0;
|
|
1338
|
+
for (const op of ops) {
|
|
1339
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1340
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
let unitIndex;
|
|
1344
|
+
if (!byWord) {
|
|
1345
|
+
unitIndex = glyphIndex;
|
|
1346
|
+
} else {
|
|
1347
|
+
let wordIndex = -1, acc = 0;
|
|
1348
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1349
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1350
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1351
|
+
wordIndex = i;
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
acc += gcount;
|
|
1355
|
+
}
|
|
1356
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1357
|
+
}
|
|
1358
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1359
|
+
if (progress <= startF) {
|
|
1360
|
+
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1361
|
+
if (op.op === "FillPath") {
|
|
1362
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1363
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1364
|
+
} else {
|
|
1365
|
+
animated.opacity = 0;
|
|
1366
|
+
}
|
|
1367
|
+
result.push(animated);
|
|
1368
|
+
} else if (progress >= endF) {
|
|
1369
|
+
result.push(op);
|
|
1370
|
+
} else {
|
|
1371
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1372
|
+
const ease = easeOutCubic(Math.min(1, local));
|
|
1373
|
+
const dx = offset.x * (1 - ease);
|
|
1374
|
+
const dy = offset.y * (1 - ease);
|
|
1375
|
+
const animated = { ...op, x: op.x + dx, y: op.y + dy };
|
|
1376
|
+
if (op.op === "FillPath") {
|
|
1377
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1378
|
+
if (animated.fill.kind === "solid")
|
|
1379
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1380
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1381
|
+
} else {
|
|
1382
|
+
animated.opacity = animated.opacity * ease;
|
|
1383
|
+
}
|
|
1384
|
+
result.push(animated);
|
|
1385
|
+
}
|
|
1386
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1387
|
+
}
|
|
1388
|
+
return result;
|
|
1229
1389
|
}
|
|
1230
|
-
function applyMovingLettersAnimation(ops,
|
|
1390
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1231
1391
|
const amp = fontSize * 0.3;
|
|
1232
|
-
return waveTransform(ops, direction, amp,
|
|
1392
|
+
return waveTransform(ops, direction, amp, time);
|
|
1233
1393
|
}
|
|
1234
1394
|
function getWordSegments(lines) {
|
|
1235
1395
|
const segments = [];
|
|
@@ -1387,14 +1547,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1387
1547
|
return op;
|
|
1388
1548
|
});
|
|
1389
1549
|
}
|
|
1390
|
-
function waveTransform(ops, dir, amp,
|
|
1550
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1391
1551
|
let glyphIndex = 0;
|
|
1552
|
+
const fadeInDuration = 0.5;
|
|
1553
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1392
1554
|
return ops.map((op) => {
|
|
1393
1555
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1394
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1556
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1395
1557
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1396
1558
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1397
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1398
1559
|
if (op.op === "FillPath") {
|
|
1399
1560
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1400
1561
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -2121,7 +2282,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2121
2282
|
);
|
|
2122
2283
|
}
|
|
2123
2284
|
},
|
|
2124
|
-
async renderFrame(asset, tSeconds) {
|
|
2285
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
2125
2286
|
try {
|
|
2126
2287
|
const main = await ensureFonts(asset);
|
|
2127
2288
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
@@ -2196,6 +2357,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2196
2357
|
const ops = applyAnimation(ops0, lines, {
|
|
2197
2358
|
t: tSeconds,
|
|
2198
2359
|
fontSize: main.size,
|
|
2360
|
+
clipDuration,
|
|
2199
2361
|
anim: asset.animation ? {
|
|
2200
2362
|
preset: asset.animation.preset,
|
|
2201
2363
|
speed: asset.animation.speed,
|
|
@@ -2243,14 +2405,15 @@ async function createTextEngine(opts = {}) {
|
|
|
2243
2405
|
width: asset.width ?? width,
|
|
2244
2406
|
height: asset.height ?? height,
|
|
2245
2407
|
fps,
|
|
2246
|
-
duration:
|
|
2408
|
+
duration: options.duration ?? 3,
|
|
2247
2409
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2248
2410
|
pixelRatio: asset.pixelRatio ?? pixelRatio,
|
|
2249
2411
|
hasAlpha: needsAlpha,
|
|
2250
2412
|
...options
|
|
2251
2413
|
};
|
|
2414
|
+
const clipDuration = finalOptions.duration;
|
|
2252
2415
|
const frameGenerator = async (time) => {
|
|
2253
|
-
return this.renderFrame(asset, time);
|
|
2416
|
+
return this.renderFrame(asset, time, clipDuration);
|
|
2254
2417
|
};
|
|
2255
2418
|
const actualOutputPath = await videoGenerator.generateVideo(frameGenerator, finalOptions);
|
|
2256
2419
|
return actualOutputPath;
|
package/dist/entry.node.d.cts
CHANGED
|
@@ -239,7 +239,7 @@ declare function createTextEngine(opts?: {
|
|
|
239
239
|
weight?: string | number;
|
|
240
240
|
style?: string;
|
|
241
241
|
}): Promise<void>;
|
|
242
|
-
renderFrame(asset: RichTextValidated, tSeconds: number): Promise<DrawOp[]>;
|
|
242
|
+
renderFrame(asset: RichTextValidated, tSeconds: number, clipDuration?: number): Promise<DrawOp[]>;
|
|
243
243
|
createRenderer(p: {
|
|
244
244
|
width?: number;
|
|
245
245
|
height?: number;
|
package/dist/entry.node.d.ts
CHANGED
|
@@ -239,7 +239,7 @@ declare function createTextEngine(opts?: {
|
|
|
239
239
|
weight?: string | number;
|
|
240
240
|
style?: string;
|
|
241
241
|
}): Promise<void>;
|
|
242
|
-
renderFrame(asset: RichTextValidated, tSeconds: number): Promise<DrawOp[]>;
|
|
242
|
+
renderFrame(asset: RichTextValidated, tSeconds: number, clipDuration?: number): Promise<DrawOp[]>;
|
|
243
243
|
createRenderer(p: {
|
|
244
244
|
width?: number;
|
|
245
245
|
height?: number;
|
package/dist/entry.node.js
CHANGED
|
@@ -81,7 +81,7 @@ var animationSchema = Joi.object({
|
|
|
81
81
|
speed: Joi.number().min(0.1).max(10).default(1),
|
|
82
82
|
duration: Joi.number().min(CANVAS_CONFIG.LIMITS.minDuration).max(CANVAS_CONFIG.LIMITS.maxDuration).optional(),
|
|
83
83
|
style: Joi.string().valid("character", "word").optional().when("preset", {
|
|
84
|
-
is: Joi.valid("typewriter", "shift"),
|
|
84
|
+
is: Joi.valid("typewriter", "shift", "fadeIn", "slideIn"),
|
|
85
85
|
then: Joi.optional(),
|
|
86
86
|
otherwise: Joi.forbidden()
|
|
87
87
|
}),
|
|
@@ -380,7 +380,8 @@ var FontRegistry = class _FontRegistry {
|
|
|
380
380
|
return this.hb;
|
|
381
381
|
}
|
|
382
382
|
key(desc) {
|
|
383
|
-
|
|
383
|
+
const normalizedStyle = desc.style === "oblique" ? "italic" : desc.style ?? "normal";
|
|
384
|
+
return `${desc.family}__${desc.weight ?? "400"}__${normalizedStyle}`;
|
|
384
385
|
}
|
|
385
386
|
async registerFromBytes(bytes, desc) {
|
|
386
387
|
try {
|
|
@@ -693,13 +694,11 @@ var LayoutEngine = class {
|
|
|
693
694
|
const glyph = glyphs[i];
|
|
694
695
|
const glyphWidth = glyph.xAdvance;
|
|
695
696
|
if (glyph.char === "\n") {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
});
|
|
702
|
-
}
|
|
697
|
+
lines.push({
|
|
698
|
+
glyphs: currentLine,
|
|
699
|
+
width: currentWidth,
|
|
700
|
+
y: 0
|
|
701
|
+
});
|
|
703
702
|
currentLine = [];
|
|
704
703
|
currentWidth = 0;
|
|
705
704
|
lastBreakIndex = i;
|
|
@@ -958,7 +957,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
958
957
|
if (!p.anim || !p.anim.preset) return ops;
|
|
959
958
|
const { preset } = p.anim;
|
|
960
959
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
961
|
-
|
|
960
|
+
let autoDuration = Math.max(0.3, totalGlyphs / (30 * p.anim.speed));
|
|
961
|
+
if (p.clipDuration && !p.anim.duration) {
|
|
962
|
+
autoDuration = Math.min(autoDuration, p.clipDuration * 0.9);
|
|
963
|
+
}
|
|
964
|
+
const duration = p.anim.duration ?? autoDuration;
|
|
962
965
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
963
966
|
switch (preset) {
|
|
964
967
|
case "typewriter":
|
|
@@ -972,9 +975,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
972
975
|
duration
|
|
973
976
|
);
|
|
974
977
|
case "fadeIn":
|
|
975
|
-
return applyFadeInAnimation(ops, progress);
|
|
978
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
976
979
|
case "slideIn":
|
|
977
|
-
return applySlideInAnimation(ops, progress, p.anim.direction ?? "left", p.fontSize);
|
|
980
|
+
return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
|
|
978
981
|
case "shift":
|
|
979
982
|
return applyShiftAnimation(
|
|
980
983
|
ops,
|
|
@@ -995,7 +998,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
995
998
|
duration
|
|
996
999
|
);
|
|
997
1000
|
case "movingLetters":
|
|
998
|
-
return applyMovingLettersAnimation(ops,
|
|
1001
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
999
1002
|
default:
|
|
1000
1003
|
return ops;
|
|
1001
1004
|
}
|
|
@@ -1177,20 +1180,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1177
1180
|
}
|
|
1178
1181
|
return result;
|
|
1179
1182
|
}
|
|
1180
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1181
|
-
const
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1183
|
+
function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
1184
|
+
const byWord = style === "word";
|
|
1185
|
+
const byCharacter = style === "character";
|
|
1186
|
+
if (!byWord && !byCharacter) {
|
|
1187
|
+
const alpha = easeOutQuad(progress);
|
|
1188
|
+
const scale = 0.95 + 0.05 * alpha;
|
|
1189
|
+
return scaleAndFade(ops, alpha, scale);
|
|
1190
|
+
}
|
|
1191
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1192
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1193
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1194
|
+
if (totalUnits === 0) return ops;
|
|
1195
|
+
const result = [];
|
|
1196
|
+
for (const op of ops) {
|
|
1197
|
+
if (op.op === "BeginFrame") {
|
|
1198
|
+
result.push(op);
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const windowDuration = 0.3;
|
|
1203
|
+
const overlapFactor = 0.7;
|
|
1204
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1205
|
+
const windowFor = (unitIdx) => {
|
|
1206
|
+
const startTime = unitIdx * staggerDelay;
|
|
1207
|
+
const startF = startTime / duration;
|
|
1208
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1209
|
+
return { startF, endF };
|
|
1210
|
+
};
|
|
1211
|
+
let glyphIndex = 0;
|
|
1212
|
+
for (const op of ops) {
|
|
1213
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1214
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
let unitIndex;
|
|
1218
|
+
if (!byWord) {
|
|
1219
|
+
unitIndex = glyphIndex;
|
|
1220
|
+
} else {
|
|
1221
|
+
let wordIndex = -1, acc = 0;
|
|
1222
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1223
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1224
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1225
|
+
wordIndex = i;
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
acc += gcount;
|
|
1229
|
+
}
|
|
1230
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1231
|
+
}
|
|
1232
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1233
|
+
if (progress <= startF) {
|
|
1234
|
+
const animated = { ...op };
|
|
1235
|
+
if (op.op === "FillPath") {
|
|
1236
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1237
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1238
|
+
} else {
|
|
1239
|
+
animated.opacity = 0;
|
|
1240
|
+
}
|
|
1241
|
+
result.push(animated);
|
|
1242
|
+
} else if (progress >= endF) {
|
|
1243
|
+
result.push(op);
|
|
1244
|
+
} else {
|
|
1245
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1246
|
+
const ease = easeOutQuad(Math.min(1, local));
|
|
1247
|
+
const animated = { ...op };
|
|
1248
|
+
if (op.op === "FillPath") {
|
|
1249
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1250
|
+
if (animated.fill.kind === "solid")
|
|
1251
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1252
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1253
|
+
} else {
|
|
1254
|
+
animated.opacity = animated.opacity * ease;
|
|
1255
|
+
}
|
|
1256
|
+
result.push(animated);
|
|
1257
|
+
}
|
|
1258
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1259
|
+
}
|
|
1260
|
+
return result;
|
|
1184
1261
|
}
|
|
1185
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1186
|
-
const
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1262
|
+
function applySlideInAnimation(ops, lines, progress, direction, fontSize, style, duration) {
|
|
1263
|
+
const byWord = style === "word";
|
|
1264
|
+
const byCharacter = style === "character";
|
|
1265
|
+
if (!byWord && !byCharacter) {
|
|
1266
|
+
const easeProgress = easeOutCubic(progress);
|
|
1267
|
+
const shift = shiftFor(1 - easeProgress, direction, fontSize * 2);
|
|
1268
|
+
const alpha = easeOutQuad(progress);
|
|
1269
|
+
return translateGlyphOps(ops, shift.dx, shift.dy, alpha);
|
|
1270
|
+
}
|
|
1271
|
+
const startOffsets = {
|
|
1272
|
+
left: { x: fontSize * 2, y: 0 },
|
|
1273
|
+
right: { x: -fontSize * 2, y: 0 },
|
|
1274
|
+
up: { x: 0, y: fontSize * 2 },
|
|
1275
|
+
down: { x: 0, y: -fontSize * 2 }
|
|
1276
|
+
};
|
|
1277
|
+
const offset = startOffsets[direction];
|
|
1278
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1279
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1280
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1281
|
+
if (totalUnits === 0) return ops;
|
|
1282
|
+
const result = [];
|
|
1283
|
+
for (const op of ops) {
|
|
1284
|
+
if (op.op === "BeginFrame") {
|
|
1285
|
+
result.push(op);
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const windowDuration = 0.3;
|
|
1290
|
+
const overlapFactor = 0.7;
|
|
1291
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1292
|
+
const windowFor = (unitIdx) => {
|
|
1293
|
+
const startTime = unitIdx * staggerDelay;
|
|
1294
|
+
const startF = startTime / duration;
|
|
1295
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1296
|
+
return { startF, endF };
|
|
1297
|
+
};
|
|
1298
|
+
let glyphIndex = 0;
|
|
1299
|
+
for (const op of ops) {
|
|
1300
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1301
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
let unitIndex;
|
|
1305
|
+
if (!byWord) {
|
|
1306
|
+
unitIndex = glyphIndex;
|
|
1307
|
+
} else {
|
|
1308
|
+
let wordIndex = -1, acc = 0;
|
|
1309
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1310
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1311
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1312
|
+
wordIndex = i;
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
acc += gcount;
|
|
1316
|
+
}
|
|
1317
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1318
|
+
}
|
|
1319
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1320
|
+
if (progress <= startF) {
|
|
1321
|
+
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1322
|
+
if (op.op === "FillPath") {
|
|
1323
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1324
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1325
|
+
} else {
|
|
1326
|
+
animated.opacity = 0;
|
|
1327
|
+
}
|
|
1328
|
+
result.push(animated);
|
|
1329
|
+
} else if (progress >= endF) {
|
|
1330
|
+
result.push(op);
|
|
1331
|
+
} else {
|
|
1332
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1333
|
+
const ease = easeOutCubic(Math.min(1, local));
|
|
1334
|
+
const dx = offset.x * (1 - ease);
|
|
1335
|
+
const dy = offset.y * (1 - ease);
|
|
1336
|
+
const animated = { ...op, x: op.x + dx, y: op.y + dy };
|
|
1337
|
+
if (op.op === "FillPath") {
|
|
1338
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1339
|
+
if (animated.fill.kind === "solid")
|
|
1340
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1341
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1342
|
+
} else {
|
|
1343
|
+
animated.opacity = animated.opacity * ease;
|
|
1344
|
+
}
|
|
1345
|
+
result.push(animated);
|
|
1346
|
+
}
|
|
1347
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1348
|
+
}
|
|
1349
|
+
return result;
|
|
1190
1350
|
}
|
|
1191
|
-
function applyMovingLettersAnimation(ops,
|
|
1351
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1192
1352
|
const amp = fontSize * 0.3;
|
|
1193
|
-
return waveTransform(ops, direction, amp,
|
|
1353
|
+
return waveTransform(ops, direction, amp, time);
|
|
1194
1354
|
}
|
|
1195
1355
|
function getWordSegments(lines) {
|
|
1196
1356
|
const segments = [];
|
|
@@ -1348,14 +1508,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1348
1508
|
return op;
|
|
1349
1509
|
});
|
|
1350
1510
|
}
|
|
1351
|
-
function waveTransform(ops, dir, amp,
|
|
1511
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1352
1512
|
let glyphIndex = 0;
|
|
1513
|
+
const fadeInDuration = 0.5;
|
|
1514
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1353
1515
|
return ops.map((op) => {
|
|
1354
1516
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1355
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1517
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1356
1518
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1357
1519
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1358
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1359
1520
|
if (op.op === "FillPath") {
|
|
1360
1521
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1361
1522
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -2082,7 +2243,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2082
2243
|
);
|
|
2083
2244
|
}
|
|
2084
2245
|
},
|
|
2085
|
-
async renderFrame(asset, tSeconds) {
|
|
2246
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
2086
2247
|
try {
|
|
2087
2248
|
const main = await ensureFonts(asset);
|
|
2088
2249
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
@@ -2157,6 +2318,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2157
2318
|
const ops = applyAnimation(ops0, lines, {
|
|
2158
2319
|
t: tSeconds,
|
|
2159
2320
|
fontSize: main.size,
|
|
2321
|
+
clipDuration,
|
|
2160
2322
|
anim: asset.animation ? {
|
|
2161
2323
|
preset: asset.animation.preset,
|
|
2162
2324
|
speed: asset.animation.speed,
|
|
@@ -2204,14 +2366,15 @@ async function createTextEngine(opts = {}) {
|
|
|
2204
2366
|
width: asset.width ?? width,
|
|
2205
2367
|
height: asset.height ?? height,
|
|
2206
2368
|
fps,
|
|
2207
|
-
duration:
|
|
2369
|
+
duration: options.duration ?? 3,
|
|
2208
2370
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2209
2371
|
pixelRatio: asset.pixelRatio ?? pixelRatio,
|
|
2210
2372
|
hasAlpha: needsAlpha,
|
|
2211
2373
|
...options
|
|
2212
2374
|
};
|
|
2375
|
+
const clipDuration = finalOptions.duration;
|
|
2213
2376
|
const frameGenerator = async (time) => {
|
|
2214
|
-
return this.renderFrame(asset, time);
|
|
2377
|
+
return this.renderFrame(asset, time, clipDuration);
|
|
2215
2378
|
};
|
|
2216
2379
|
const actualOutputPath = await videoGenerator.generateVideo(frameGenerator, finalOptions);
|
|
2217
2380
|
return actualOutputPath;
|
package/dist/entry.web.d.ts
CHANGED
|
@@ -222,7 +222,7 @@ declare function createTextEngine(opts?: {
|
|
|
222
222
|
weight?: string | number;
|
|
223
223
|
style?: string;
|
|
224
224
|
}): Promise<void>;
|
|
225
|
-
renderFrame(asset: RichTextValidated, tSeconds: number): Promise<DrawOp[]>;
|
|
225
|
+
renderFrame(asset: RichTextValidated, tSeconds: number, clipDuration?: number): Promise<DrawOp[]>;
|
|
226
226
|
createRenderer(canvas: HTMLCanvasElement | OffscreenCanvas): {
|
|
227
227
|
render(ops: DrawOp[]): Promise<void>;
|
|
228
228
|
};
|
package/dist/entry.web.js
CHANGED
|
@@ -85,7 +85,7 @@ var animationSchema = Joi.object({
|
|
|
85
85
|
speed: Joi.number().min(0.1).max(10).default(1),
|
|
86
86
|
duration: Joi.number().min(CANVAS_CONFIG.LIMITS.minDuration).max(CANVAS_CONFIG.LIMITS.maxDuration).optional(),
|
|
87
87
|
style: Joi.string().valid("character", "word").optional().when("preset", {
|
|
88
|
-
is: Joi.valid("typewriter", "shift"),
|
|
88
|
+
is: Joi.valid("typewriter", "shift", "fadeIn", "slideIn"),
|
|
89
89
|
then: Joi.optional(),
|
|
90
90
|
otherwise: Joi.forbidden()
|
|
91
91
|
}),
|
|
@@ -383,7 +383,8 @@ var _FontRegistry = class _FontRegistry {
|
|
|
383
383
|
return this.hb;
|
|
384
384
|
}
|
|
385
385
|
key(desc) {
|
|
386
|
-
|
|
386
|
+
const normalizedStyle = desc.style === "oblique" ? "italic" : desc.style ?? "normal";
|
|
387
|
+
return `${desc.family}__${desc.weight ?? "400"}__${normalizedStyle}`;
|
|
387
388
|
}
|
|
388
389
|
async registerFromBytes(bytes, desc) {
|
|
389
390
|
try {
|
|
@@ -698,13 +699,11 @@ var LayoutEngine = class {
|
|
|
698
699
|
const glyph = glyphs[i];
|
|
699
700
|
const glyphWidth = glyph.xAdvance;
|
|
700
701
|
if (glyph.char === "\n") {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
});
|
|
707
|
-
}
|
|
702
|
+
lines.push({
|
|
703
|
+
glyphs: currentLine,
|
|
704
|
+
width: currentWidth,
|
|
705
|
+
y: 0
|
|
706
|
+
});
|
|
708
707
|
currentLine = [];
|
|
709
708
|
currentWidth = 0;
|
|
710
709
|
lastBreakIndex = i;
|
|
@@ -963,7 +962,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
963
962
|
if (!p.anim || !p.anim.preset) return ops;
|
|
964
963
|
const { preset } = p.anim;
|
|
965
964
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
966
|
-
|
|
965
|
+
let autoDuration = Math.max(0.3, totalGlyphs / (30 * p.anim.speed));
|
|
966
|
+
if (p.clipDuration && !p.anim.duration) {
|
|
967
|
+
autoDuration = Math.min(autoDuration, p.clipDuration * 0.9);
|
|
968
|
+
}
|
|
969
|
+
const duration = p.anim.duration ?? autoDuration;
|
|
967
970
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
968
971
|
switch (preset) {
|
|
969
972
|
case "typewriter":
|
|
@@ -977,9 +980,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
977
980
|
duration
|
|
978
981
|
);
|
|
979
982
|
case "fadeIn":
|
|
980
|
-
return applyFadeInAnimation(ops, progress);
|
|
983
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
981
984
|
case "slideIn":
|
|
982
|
-
return applySlideInAnimation(ops, progress, p.anim.direction ?? "left", p.fontSize);
|
|
985
|
+
return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
|
|
983
986
|
case "shift":
|
|
984
987
|
return applyShiftAnimation(
|
|
985
988
|
ops,
|
|
@@ -1000,7 +1003,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
1000
1003
|
duration
|
|
1001
1004
|
);
|
|
1002
1005
|
case "movingLetters":
|
|
1003
|
-
return applyMovingLettersAnimation(ops,
|
|
1006
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1004
1007
|
default:
|
|
1005
1008
|
return ops;
|
|
1006
1009
|
}
|
|
@@ -1182,20 +1185,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1182
1185
|
}
|
|
1183
1186
|
return result;
|
|
1184
1187
|
}
|
|
1185
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1186
|
-
const
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1188
|
+
function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
1189
|
+
const byWord = style === "word";
|
|
1190
|
+
const byCharacter = style === "character";
|
|
1191
|
+
if (!byWord && !byCharacter) {
|
|
1192
|
+
const alpha = easeOutQuad(progress);
|
|
1193
|
+
const scale = 0.95 + 0.05 * alpha;
|
|
1194
|
+
return scaleAndFade(ops, alpha, scale);
|
|
1195
|
+
}
|
|
1196
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1197
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1198
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1199
|
+
if (totalUnits === 0) return ops;
|
|
1200
|
+
const result = [];
|
|
1201
|
+
for (const op of ops) {
|
|
1202
|
+
if (op.op === "BeginFrame") {
|
|
1203
|
+
result.push(op);
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const windowDuration = 0.3;
|
|
1208
|
+
const overlapFactor = 0.7;
|
|
1209
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1210
|
+
const windowFor = (unitIdx) => {
|
|
1211
|
+
const startTime = unitIdx * staggerDelay;
|
|
1212
|
+
const startF = startTime / duration;
|
|
1213
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1214
|
+
return { startF, endF };
|
|
1215
|
+
};
|
|
1216
|
+
let glyphIndex = 0;
|
|
1217
|
+
for (const op of ops) {
|
|
1218
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1219
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
let unitIndex;
|
|
1223
|
+
if (!byWord) {
|
|
1224
|
+
unitIndex = glyphIndex;
|
|
1225
|
+
} else {
|
|
1226
|
+
let wordIndex = -1, acc = 0;
|
|
1227
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1228
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1229
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1230
|
+
wordIndex = i;
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
acc += gcount;
|
|
1234
|
+
}
|
|
1235
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1236
|
+
}
|
|
1237
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1238
|
+
if (progress <= startF) {
|
|
1239
|
+
const animated = { ...op };
|
|
1240
|
+
if (op.op === "FillPath") {
|
|
1241
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1242
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1243
|
+
} else {
|
|
1244
|
+
animated.opacity = 0;
|
|
1245
|
+
}
|
|
1246
|
+
result.push(animated);
|
|
1247
|
+
} else if (progress >= endF) {
|
|
1248
|
+
result.push(op);
|
|
1249
|
+
} else {
|
|
1250
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1251
|
+
const ease = easeOutQuad(Math.min(1, local));
|
|
1252
|
+
const animated = { ...op };
|
|
1253
|
+
if (op.op === "FillPath") {
|
|
1254
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1255
|
+
if (animated.fill.kind === "solid")
|
|
1256
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1257
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1258
|
+
} else {
|
|
1259
|
+
animated.opacity = animated.opacity * ease;
|
|
1260
|
+
}
|
|
1261
|
+
result.push(animated);
|
|
1262
|
+
}
|
|
1263
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1264
|
+
}
|
|
1265
|
+
return result;
|
|
1189
1266
|
}
|
|
1190
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1191
|
-
const
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1267
|
+
function applySlideInAnimation(ops, lines, progress, direction, fontSize, style, duration) {
|
|
1268
|
+
const byWord = style === "word";
|
|
1269
|
+
const byCharacter = style === "character";
|
|
1270
|
+
if (!byWord && !byCharacter) {
|
|
1271
|
+
const easeProgress = easeOutCubic(progress);
|
|
1272
|
+
const shift = shiftFor(1 - easeProgress, direction, fontSize * 2);
|
|
1273
|
+
const alpha = easeOutQuad(progress);
|
|
1274
|
+
return translateGlyphOps(ops, shift.dx, shift.dy, alpha);
|
|
1275
|
+
}
|
|
1276
|
+
const startOffsets = {
|
|
1277
|
+
left: { x: fontSize * 2, y: 0 },
|
|
1278
|
+
right: { x: -fontSize * 2, y: 0 },
|
|
1279
|
+
up: { x: 0, y: fontSize * 2 },
|
|
1280
|
+
down: { x: 0, y: -fontSize * 2 }
|
|
1281
|
+
};
|
|
1282
|
+
const offset = startOffsets[direction];
|
|
1283
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1284
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1285
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1286
|
+
if (totalUnits === 0) return ops;
|
|
1287
|
+
const result = [];
|
|
1288
|
+
for (const op of ops) {
|
|
1289
|
+
if (op.op === "BeginFrame") {
|
|
1290
|
+
result.push(op);
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const windowDuration = 0.3;
|
|
1295
|
+
const overlapFactor = 0.7;
|
|
1296
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1297
|
+
const windowFor = (unitIdx) => {
|
|
1298
|
+
const startTime = unitIdx * staggerDelay;
|
|
1299
|
+
const startF = startTime / duration;
|
|
1300
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1301
|
+
return { startF, endF };
|
|
1302
|
+
};
|
|
1303
|
+
let glyphIndex = 0;
|
|
1304
|
+
for (const op of ops) {
|
|
1305
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1306
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
let unitIndex;
|
|
1310
|
+
if (!byWord) {
|
|
1311
|
+
unitIndex = glyphIndex;
|
|
1312
|
+
} else {
|
|
1313
|
+
let wordIndex = -1, acc = 0;
|
|
1314
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1315
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1316
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1317
|
+
wordIndex = i;
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
acc += gcount;
|
|
1321
|
+
}
|
|
1322
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1323
|
+
}
|
|
1324
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1325
|
+
if (progress <= startF) {
|
|
1326
|
+
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1327
|
+
if (op.op === "FillPath") {
|
|
1328
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1329
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1330
|
+
} else {
|
|
1331
|
+
animated.opacity = 0;
|
|
1332
|
+
}
|
|
1333
|
+
result.push(animated);
|
|
1334
|
+
} else if (progress >= endF) {
|
|
1335
|
+
result.push(op);
|
|
1336
|
+
} else {
|
|
1337
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1338
|
+
const ease = easeOutCubic(Math.min(1, local));
|
|
1339
|
+
const dx = offset.x * (1 - ease);
|
|
1340
|
+
const dy = offset.y * (1 - ease);
|
|
1341
|
+
const animated = { ...op, x: op.x + dx, y: op.y + dy };
|
|
1342
|
+
if (op.op === "FillPath") {
|
|
1343
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1344
|
+
if (animated.fill.kind === "solid")
|
|
1345
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1346
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1347
|
+
} else {
|
|
1348
|
+
animated.opacity = animated.opacity * ease;
|
|
1349
|
+
}
|
|
1350
|
+
result.push(animated);
|
|
1351
|
+
}
|
|
1352
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1353
|
+
}
|
|
1354
|
+
return result;
|
|
1195
1355
|
}
|
|
1196
|
-
function applyMovingLettersAnimation(ops,
|
|
1356
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1197
1357
|
const amp = fontSize * 0.3;
|
|
1198
|
-
return waveTransform(ops, direction, amp,
|
|
1358
|
+
return waveTransform(ops, direction, amp, time);
|
|
1199
1359
|
}
|
|
1200
1360
|
function getWordSegments(lines) {
|
|
1201
1361
|
const segments = [];
|
|
@@ -1353,14 +1513,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1353
1513
|
return op;
|
|
1354
1514
|
});
|
|
1355
1515
|
}
|
|
1356
|
-
function waveTransform(ops, dir, amp,
|
|
1516
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1357
1517
|
let glyphIndex = 0;
|
|
1518
|
+
const fadeInDuration = 0.5;
|
|
1519
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1358
1520
|
return ops.map((op) => {
|
|
1359
1521
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1360
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1522
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1361
1523
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1362
1524
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1363
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1364
1525
|
if (op.op === "FillPath") {
|
|
1365
1526
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1366
1527
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -1803,7 +1964,7 @@ async function createTextEngine(opts = {}) {
|
|
|
1803
1964
|
);
|
|
1804
1965
|
}
|
|
1805
1966
|
},
|
|
1806
|
-
async renderFrame(asset, tSeconds) {
|
|
1967
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
1807
1968
|
try {
|
|
1808
1969
|
const main = await ensureFonts(asset);
|
|
1809
1970
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
@@ -1878,6 +2039,7 @@ async function createTextEngine(opts = {}) {
|
|
|
1878
2039
|
const ops = applyAnimation(ops0, lines, {
|
|
1879
2040
|
t: tSeconds,
|
|
1880
2041
|
fontSize: main.size,
|
|
2042
|
+
clipDuration,
|
|
1881
2043
|
anim: asset.animation ? {
|
|
1882
2044
|
preset: asset.animation.preset,
|
|
1883
2045
|
speed: asset.animation.speed,
|
package/package.json
CHANGED