@shotstack/shotstack-canvas 1.4.0 → 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 +191 -29
- package/dist/entry.node.d.cts +1 -1
- package/dist/entry.node.d.ts +1 -1
- package/dist/entry.node.js +191 -29
- package/dist/entry.web.d.ts +1 -1
- package/dist/entry.web.js +188 -27
- 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
|
}),
|
|
@@ -733,13 +733,11 @@ var LayoutEngine = class {
|
|
|
733
733
|
const glyph = glyphs[i];
|
|
734
734
|
const glyphWidth = glyph.xAdvance;
|
|
735
735
|
if (glyph.char === "\n") {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
});
|
|
742
|
-
}
|
|
736
|
+
lines.push({
|
|
737
|
+
glyphs: currentLine,
|
|
738
|
+
width: currentWidth,
|
|
739
|
+
y: 0
|
|
740
|
+
});
|
|
743
741
|
currentLine = [];
|
|
744
742
|
currentWidth = 0;
|
|
745
743
|
lastBreakIndex = i;
|
|
@@ -998,7 +996,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
998
996
|
if (!p.anim || !p.anim.preset) return ops;
|
|
999
997
|
const { preset } = p.anim;
|
|
1000
998
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1001
|
-
|
|
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;
|
|
1002
1004
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
1003
1005
|
switch (preset) {
|
|
1004
1006
|
case "typewriter":
|
|
@@ -1012,9 +1014,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
1012
1014
|
duration
|
|
1013
1015
|
);
|
|
1014
1016
|
case "fadeIn":
|
|
1015
|
-
return applyFadeInAnimation(ops, progress);
|
|
1017
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
1016
1018
|
case "slideIn":
|
|
1017
|
-
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);
|
|
1018
1020
|
case "shift":
|
|
1019
1021
|
return applyShiftAnimation(
|
|
1020
1022
|
ops,
|
|
@@ -1035,7 +1037,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
1035
1037
|
duration
|
|
1036
1038
|
);
|
|
1037
1039
|
case "movingLetters":
|
|
1038
|
-
return applyMovingLettersAnimation(ops,
|
|
1040
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1039
1041
|
default:
|
|
1040
1042
|
return ops;
|
|
1041
1043
|
}
|
|
@@ -1217,20 +1219,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1217
1219
|
}
|
|
1218
1220
|
return result;
|
|
1219
1221
|
}
|
|
1220
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
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;
|
|
1224
1300
|
}
|
|
1225
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1226
|
-
const
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
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;
|
|
1230
1389
|
}
|
|
1231
|
-
function applyMovingLettersAnimation(ops,
|
|
1390
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1232
1391
|
const amp = fontSize * 0.3;
|
|
1233
|
-
return waveTransform(ops, direction, amp,
|
|
1392
|
+
return waveTransform(ops, direction, amp, time);
|
|
1234
1393
|
}
|
|
1235
1394
|
function getWordSegments(lines) {
|
|
1236
1395
|
const segments = [];
|
|
@@ -1388,14 +1547,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1388
1547
|
return op;
|
|
1389
1548
|
});
|
|
1390
1549
|
}
|
|
1391
|
-
function waveTransform(ops, dir, amp,
|
|
1550
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1392
1551
|
let glyphIndex = 0;
|
|
1552
|
+
const fadeInDuration = 0.5;
|
|
1553
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1393
1554
|
return ops.map((op) => {
|
|
1394
1555
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1395
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1556
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1396
1557
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1397
1558
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1398
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1399
1559
|
if (op.op === "FillPath") {
|
|
1400
1560
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1401
1561
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -2122,7 +2282,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2122
2282
|
);
|
|
2123
2283
|
}
|
|
2124
2284
|
},
|
|
2125
|
-
async renderFrame(asset, tSeconds) {
|
|
2285
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
2126
2286
|
try {
|
|
2127
2287
|
const main = await ensureFonts(asset);
|
|
2128
2288
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
@@ -2197,6 +2357,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2197
2357
|
const ops = applyAnimation(ops0, lines, {
|
|
2198
2358
|
t: tSeconds,
|
|
2199
2359
|
fontSize: main.size,
|
|
2360
|
+
clipDuration,
|
|
2200
2361
|
anim: asset.animation ? {
|
|
2201
2362
|
preset: asset.animation.preset,
|
|
2202
2363
|
speed: asset.animation.speed,
|
|
@@ -2244,14 +2405,15 @@ async function createTextEngine(opts = {}) {
|
|
|
2244
2405
|
width: asset.width ?? width,
|
|
2245
2406
|
height: asset.height ?? height,
|
|
2246
2407
|
fps,
|
|
2247
|
-
duration:
|
|
2408
|
+
duration: options.duration ?? 3,
|
|
2248
2409
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2249
2410
|
pixelRatio: asset.pixelRatio ?? pixelRatio,
|
|
2250
2411
|
hasAlpha: needsAlpha,
|
|
2251
2412
|
...options
|
|
2252
2413
|
};
|
|
2414
|
+
const clipDuration = finalOptions.duration;
|
|
2253
2415
|
const frameGenerator = async (time) => {
|
|
2254
|
-
return this.renderFrame(asset, time);
|
|
2416
|
+
return this.renderFrame(asset, time, clipDuration);
|
|
2255
2417
|
};
|
|
2256
2418
|
const actualOutputPath = await videoGenerator.generateVideo(frameGenerator, finalOptions);
|
|
2257
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
|
}),
|
|
@@ -694,13 +694,11 @@ var LayoutEngine = class {
|
|
|
694
694
|
const glyph = glyphs[i];
|
|
695
695
|
const glyphWidth = glyph.xAdvance;
|
|
696
696
|
if (glyph.char === "\n") {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
});
|
|
703
|
-
}
|
|
697
|
+
lines.push({
|
|
698
|
+
glyphs: currentLine,
|
|
699
|
+
width: currentWidth,
|
|
700
|
+
y: 0
|
|
701
|
+
});
|
|
704
702
|
currentLine = [];
|
|
705
703
|
currentWidth = 0;
|
|
706
704
|
lastBreakIndex = i;
|
|
@@ -959,7 +957,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
959
957
|
if (!p.anim || !p.anim.preset) return ops;
|
|
960
958
|
const { preset } = p.anim;
|
|
961
959
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
962
|
-
|
|
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;
|
|
963
965
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
964
966
|
switch (preset) {
|
|
965
967
|
case "typewriter":
|
|
@@ -973,9 +975,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
973
975
|
duration
|
|
974
976
|
);
|
|
975
977
|
case "fadeIn":
|
|
976
|
-
return applyFadeInAnimation(ops, progress);
|
|
978
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
977
979
|
case "slideIn":
|
|
978
|
-
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);
|
|
979
981
|
case "shift":
|
|
980
982
|
return applyShiftAnimation(
|
|
981
983
|
ops,
|
|
@@ -996,7 +998,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
996
998
|
duration
|
|
997
999
|
);
|
|
998
1000
|
case "movingLetters":
|
|
999
|
-
return applyMovingLettersAnimation(ops,
|
|
1001
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1000
1002
|
default:
|
|
1001
1003
|
return ops;
|
|
1002
1004
|
}
|
|
@@ -1178,20 +1180,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1178
1180
|
}
|
|
1179
1181
|
return result;
|
|
1180
1182
|
}
|
|
1181
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1182
|
-
const
|
|
1183
|
-
const
|
|
1184
|
-
|
|
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;
|
|
1185
1261
|
}
|
|
1186
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
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;
|
|
1191
1350
|
}
|
|
1192
|
-
function applyMovingLettersAnimation(ops,
|
|
1351
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1193
1352
|
const amp = fontSize * 0.3;
|
|
1194
|
-
return waveTransform(ops, direction, amp,
|
|
1353
|
+
return waveTransform(ops, direction, amp, time);
|
|
1195
1354
|
}
|
|
1196
1355
|
function getWordSegments(lines) {
|
|
1197
1356
|
const segments = [];
|
|
@@ -1349,14 +1508,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1349
1508
|
return op;
|
|
1350
1509
|
});
|
|
1351
1510
|
}
|
|
1352
|
-
function waveTransform(ops, dir, amp,
|
|
1511
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1353
1512
|
let glyphIndex = 0;
|
|
1513
|
+
const fadeInDuration = 0.5;
|
|
1514
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1354
1515
|
return ops.map((op) => {
|
|
1355
1516
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1356
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1517
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1357
1518
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1358
1519
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1359
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1360
1520
|
if (op.op === "FillPath") {
|
|
1361
1521
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1362
1522
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -2083,7 +2243,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2083
2243
|
);
|
|
2084
2244
|
}
|
|
2085
2245
|
},
|
|
2086
|
-
async renderFrame(asset, tSeconds) {
|
|
2246
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
2087
2247
|
try {
|
|
2088
2248
|
const main = await ensureFonts(asset);
|
|
2089
2249
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
@@ -2158,6 +2318,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2158
2318
|
const ops = applyAnimation(ops0, lines, {
|
|
2159
2319
|
t: tSeconds,
|
|
2160
2320
|
fontSize: main.size,
|
|
2321
|
+
clipDuration,
|
|
2161
2322
|
anim: asset.animation ? {
|
|
2162
2323
|
preset: asset.animation.preset,
|
|
2163
2324
|
speed: asset.animation.speed,
|
|
@@ -2205,14 +2366,15 @@ async function createTextEngine(opts = {}) {
|
|
|
2205
2366
|
width: asset.width ?? width,
|
|
2206
2367
|
height: asset.height ?? height,
|
|
2207
2368
|
fps,
|
|
2208
|
-
duration:
|
|
2369
|
+
duration: options.duration ?? 3,
|
|
2209
2370
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2210
2371
|
pixelRatio: asset.pixelRatio ?? pixelRatio,
|
|
2211
2372
|
hasAlpha: needsAlpha,
|
|
2212
2373
|
...options
|
|
2213
2374
|
};
|
|
2375
|
+
const clipDuration = finalOptions.duration;
|
|
2214
2376
|
const frameGenerator = async (time) => {
|
|
2215
|
-
return this.renderFrame(asset, time);
|
|
2377
|
+
return this.renderFrame(asset, time, clipDuration);
|
|
2216
2378
|
};
|
|
2217
2379
|
const actualOutputPath = await videoGenerator.generateVideo(frameGenerator, finalOptions);
|
|
2218
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
|
}),
|
|
@@ -699,13 +699,11 @@ var LayoutEngine = class {
|
|
|
699
699
|
const glyph = glyphs[i];
|
|
700
700
|
const glyphWidth = glyph.xAdvance;
|
|
701
701
|
if (glyph.char === "\n") {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
});
|
|
708
|
-
}
|
|
702
|
+
lines.push({
|
|
703
|
+
glyphs: currentLine,
|
|
704
|
+
width: currentWidth,
|
|
705
|
+
y: 0
|
|
706
|
+
});
|
|
709
707
|
currentLine = [];
|
|
710
708
|
currentWidth = 0;
|
|
711
709
|
lastBreakIndex = i;
|
|
@@ -964,7 +962,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
964
962
|
if (!p.anim || !p.anim.preset) return ops;
|
|
965
963
|
const { preset } = p.anim;
|
|
966
964
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
967
|
-
|
|
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;
|
|
968
970
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
969
971
|
switch (preset) {
|
|
970
972
|
case "typewriter":
|
|
@@ -978,9 +980,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
978
980
|
duration
|
|
979
981
|
);
|
|
980
982
|
case "fadeIn":
|
|
981
|
-
return applyFadeInAnimation(ops, progress);
|
|
983
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
982
984
|
case "slideIn":
|
|
983
|
-
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);
|
|
984
986
|
case "shift":
|
|
985
987
|
return applyShiftAnimation(
|
|
986
988
|
ops,
|
|
@@ -1001,7 +1003,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
1001
1003
|
duration
|
|
1002
1004
|
);
|
|
1003
1005
|
case "movingLetters":
|
|
1004
|
-
return applyMovingLettersAnimation(ops,
|
|
1006
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1005
1007
|
default:
|
|
1006
1008
|
return ops;
|
|
1007
1009
|
}
|
|
@@ -1183,20 +1185,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1183
1185
|
}
|
|
1184
1186
|
return result;
|
|
1185
1187
|
}
|
|
1186
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1189
|
-
|
|
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;
|
|
1190
1266
|
}
|
|
1191
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
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;
|
|
1196
1355
|
}
|
|
1197
|
-
function applyMovingLettersAnimation(ops,
|
|
1356
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1198
1357
|
const amp = fontSize * 0.3;
|
|
1199
|
-
return waveTransform(ops, direction, amp,
|
|
1358
|
+
return waveTransform(ops, direction, amp, time);
|
|
1200
1359
|
}
|
|
1201
1360
|
function getWordSegments(lines) {
|
|
1202
1361
|
const segments = [];
|
|
@@ -1354,14 +1513,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1354
1513
|
return op;
|
|
1355
1514
|
});
|
|
1356
1515
|
}
|
|
1357
|
-
function waveTransform(ops, dir, amp,
|
|
1516
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1358
1517
|
let glyphIndex = 0;
|
|
1518
|
+
const fadeInDuration = 0.5;
|
|
1519
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1359
1520
|
return ops.map((op) => {
|
|
1360
1521
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1361
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1522
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1362
1523
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1363
1524
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1364
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1365
1525
|
if (op.op === "FillPath") {
|
|
1366
1526
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1367
1527
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -1804,7 +1964,7 @@ async function createTextEngine(opts = {}) {
|
|
|
1804
1964
|
);
|
|
1805
1965
|
}
|
|
1806
1966
|
},
|
|
1807
|
-
async renderFrame(asset, tSeconds) {
|
|
1967
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
1808
1968
|
try {
|
|
1809
1969
|
const main = await ensureFonts(asset);
|
|
1810
1970
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
@@ -1879,6 +2039,7 @@ async function createTextEngine(opts = {}) {
|
|
|
1879
2039
|
const ops = applyAnimation(ops0, lines, {
|
|
1880
2040
|
t: tSeconds,
|
|
1881
2041
|
fontSize: main.size,
|
|
2042
|
+
clipDuration,
|
|
1882
2043
|
anim: asset.animation ? {
|
|
1883
2044
|
preset: asset.animation.preset,
|
|
1884
2045
|
speed: asset.animation.speed,
|
package/package.json
CHANGED