@shotstack/shotstack-canvas 1.4.0 → 1.4.2
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 +197 -42
- package/dist/entry.node.d.cts +1 -5
- package/dist/entry.node.d.ts +1 -5
- package/dist/entry.node.js +197 -42
- package/dist/entry.web.d.ts +1 -5
- package/dist/entry.web.js +199 -47
- package/package.json +1 -1
package/dist/entry.node.cjs
CHANGED
|
@@ -99,7 +99,6 @@ var fontSchema = import_joi.default.object({
|
|
|
99
99
|
family: import_joi.default.string().default(CANVAS_CONFIG.DEFAULTS.fontFamily),
|
|
100
100
|
size: import_joi.default.number().min(CANVAS_CONFIG.LIMITS.minFontSize).max(CANVAS_CONFIG.LIMITS.maxFontSize).default(CANVAS_CONFIG.DEFAULTS.fontSize),
|
|
101
101
|
weight: import_joi.default.alternatives().try(import_joi.default.string(), import_joi.default.number()).default("400"),
|
|
102
|
-
style: import_joi.default.string().valid("normal", "italic", "oblique").default("normal"),
|
|
103
102
|
color: import_joi.default.string().pattern(HEX6).default(CANVAS_CONFIG.DEFAULTS.color),
|
|
104
103
|
opacity: import_joi.default.number().min(0).max(1).default(1)
|
|
105
104
|
}).unknown(false);
|
|
@@ -119,7 +118,7 @@ var animationSchema = import_joi.default.object({
|
|
|
119
118
|
speed: import_joi.default.number().min(0.1).max(10).default(1),
|
|
120
119
|
duration: import_joi.default.number().min(CANVAS_CONFIG.LIMITS.minDuration).max(CANVAS_CONFIG.LIMITS.maxDuration).optional(),
|
|
121
120
|
style: import_joi.default.string().valid("character", "word").optional().when("preset", {
|
|
122
|
-
is: import_joi.default.valid("typewriter", "shift"),
|
|
121
|
+
is: import_joi.default.valid("typewriter", "shift", "fadeIn", "slideIn"),
|
|
123
122
|
then: import_joi.default.optional(),
|
|
124
123
|
otherwise: import_joi.default.forbidden()
|
|
125
124
|
}),
|
|
@@ -419,8 +418,7 @@ var FontRegistry = class _FontRegistry {
|
|
|
419
418
|
return this.hb;
|
|
420
419
|
}
|
|
421
420
|
key(desc) {
|
|
422
|
-
|
|
423
|
-
return `${desc.family}__${desc.weight ?? "400"}__${normalizedStyle}`;
|
|
421
|
+
return `${desc.family}__${desc.weight ?? "400"}`;
|
|
424
422
|
}
|
|
425
423
|
async registerFromBytes(bytes, desc) {
|
|
426
424
|
try {
|
|
@@ -469,14 +467,12 @@ var FontRegistry = class _FontRegistry {
|
|
|
469
467
|
try {
|
|
470
468
|
const bytes = await loader({
|
|
471
469
|
family: desc.family,
|
|
472
|
-
weight: desc.weight ?? "400"
|
|
473
|
-
style: desc.style ?? "normal"
|
|
470
|
+
weight: desc.weight ?? "400"
|
|
474
471
|
});
|
|
475
472
|
if (!bytes) return false;
|
|
476
473
|
await this.registerFromBytes(bytes, {
|
|
477
474
|
family: desc.family,
|
|
478
|
-
weight: desc.weight ?? "400"
|
|
479
|
-
style: desc.style ?? "normal"
|
|
475
|
+
weight: desc.weight ?? "400"
|
|
480
476
|
});
|
|
481
477
|
return true;
|
|
482
478
|
} catch {
|
|
@@ -733,13 +729,11 @@ var LayoutEngine = class {
|
|
|
733
729
|
const glyph = glyphs[i];
|
|
734
730
|
const glyphWidth = glyph.xAdvance;
|
|
735
731
|
if (glyph.char === "\n") {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
});
|
|
742
|
-
}
|
|
732
|
+
lines.push({
|
|
733
|
+
glyphs: currentLine,
|
|
734
|
+
width: currentWidth,
|
|
735
|
+
y: 0
|
|
736
|
+
});
|
|
743
737
|
currentLine = [];
|
|
744
738
|
currentWidth = 0;
|
|
745
739
|
lastBreakIndex = i;
|
|
@@ -998,7 +992,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
998
992
|
if (!p.anim || !p.anim.preset) return ops;
|
|
999
993
|
const { preset } = p.anim;
|
|
1000
994
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1001
|
-
|
|
995
|
+
let autoDuration = Math.max(0.3, totalGlyphs / (30 * p.anim.speed));
|
|
996
|
+
if (p.clipDuration && !p.anim.duration) {
|
|
997
|
+
autoDuration = Math.min(autoDuration, p.clipDuration * 0.9);
|
|
998
|
+
}
|
|
999
|
+
const duration = p.anim.duration ?? autoDuration;
|
|
1002
1000
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
1003
1001
|
switch (preset) {
|
|
1004
1002
|
case "typewriter":
|
|
@@ -1012,9 +1010,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
1012
1010
|
duration
|
|
1013
1011
|
);
|
|
1014
1012
|
case "fadeIn":
|
|
1015
|
-
return applyFadeInAnimation(ops, progress);
|
|
1013
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
1016
1014
|
case "slideIn":
|
|
1017
|
-
return applySlideInAnimation(ops, progress, p.anim.direction ?? "left", p.fontSize);
|
|
1015
|
+
return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
|
|
1018
1016
|
case "shift":
|
|
1019
1017
|
return applyShiftAnimation(
|
|
1020
1018
|
ops,
|
|
@@ -1035,7 +1033,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
1035
1033
|
duration
|
|
1036
1034
|
);
|
|
1037
1035
|
case "movingLetters":
|
|
1038
|
-
return applyMovingLettersAnimation(ops,
|
|
1036
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1039
1037
|
default:
|
|
1040
1038
|
return ops;
|
|
1041
1039
|
}
|
|
@@ -1217,20 +1215,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1217
1215
|
}
|
|
1218
1216
|
return result;
|
|
1219
1217
|
}
|
|
1220
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1218
|
+
function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
1219
|
+
const byWord = style === "word";
|
|
1220
|
+
const byCharacter = style === "character";
|
|
1221
|
+
if (!byWord && !byCharacter) {
|
|
1222
|
+
const alpha = easeOutQuad(progress);
|
|
1223
|
+
const scale = 0.95 + 0.05 * alpha;
|
|
1224
|
+
return scaleAndFade(ops, alpha, scale);
|
|
1225
|
+
}
|
|
1226
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1227
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1228
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1229
|
+
if (totalUnits === 0) return ops;
|
|
1230
|
+
const result = [];
|
|
1231
|
+
for (const op of ops) {
|
|
1232
|
+
if (op.op === "BeginFrame") {
|
|
1233
|
+
result.push(op);
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const windowDuration = 0.3;
|
|
1238
|
+
const overlapFactor = 0.7;
|
|
1239
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1240
|
+
const windowFor = (unitIdx) => {
|
|
1241
|
+
const startTime = unitIdx * staggerDelay;
|
|
1242
|
+
const startF = startTime / duration;
|
|
1243
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1244
|
+
return { startF, endF };
|
|
1245
|
+
};
|
|
1246
|
+
let glyphIndex = 0;
|
|
1247
|
+
for (const op of ops) {
|
|
1248
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1249
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
let unitIndex;
|
|
1253
|
+
if (!byWord) {
|
|
1254
|
+
unitIndex = glyphIndex;
|
|
1255
|
+
} else {
|
|
1256
|
+
let wordIndex = -1, acc = 0;
|
|
1257
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1258
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1259
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1260
|
+
wordIndex = i;
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
acc += gcount;
|
|
1264
|
+
}
|
|
1265
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1266
|
+
}
|
|
1267
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1268
|
+
if (progress <= startF) {
|
|
1269
|
+
const animated = { ...op };
|
|
1270
|
+
if (op.op === "FillPath") {
|
|
1271
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1272
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1273
|
+
} else {
|
|
1274
|
+
animated.opacity = 0;
|
|
1275
|
+
}
|
|
1276
|
+
result.push(animated);
|
|
1277
|
+
} else if (progress >= endF) {
|
|
1278
|
+
result.push(op);
|
|
1279
|
+
} else {
|
|
1280
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1281
|
+
const ease = easeOutQuad(Math.min(1, local));
|
|
1282
|
+
const animated = { ...op };
|
|
1283
|
+
if (op.op === "FillPath") {
|
|
1284
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1285
|
+
if (animated.fill.kind === "solid")
|
|
1286
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1287
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1288
|
+
} else {
|
|
1289
|
+
animated.opacity = animated.opacity * ease;
|
|
1290
|
+
}
|
|
1291
|
+
result.push(animated);
|
|
1292
|
+
}
|
|
1293
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1294
|
+
}
|
|
1295
|
+
return result;
|
|
1224
1296
|
}
|
|
1225
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1226
|
-
const
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1297
|
+
function applySlideInAnimation(ops, lines, progress, direction, fontSize, style, duration) {
|
|
1298
|
+
const byWord = style === "word";
|
|
1299
|
+
const byCharacter = style === "character";
|
|
1300
|
+
if (!byWord && !byCharacter) {
|
|
1301
|
+
const easeProgress = easeOutCubic(progress);
|
|
1302
|
+
const shift = shiftFor(1 - easeProgress, direction, fontSize * 2);
|
|
1303
|
+
const alpha = easeOutQuad(progress);
|
|
1304
|
+
return translateGlyphOps(ops, shift.dx, shift.dy, alpha);
|
|
1305
|
+
}
|
|
1306
|
+
const startOffsets = {
|
|
1307
|
+
left: { x: fontSize * 2, y: 0 },
|
|
1308
|
+
right: { x: -fontSize * 2, y: 0 },
|
|
1309
|
+
up: { x: 0, y: fontSize * 2 },
|
|
1310
|
+
down: { x: 0, y: -fontSize * 2 }
|
|
1311
|
+
};
|
|
1312
|
+
const offset = startOffsets[direction];
|
|
1313
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1314
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1315
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1316
|
+
if (totalUnits === 0) return ops;
|
|
1317
|
+
const result = [];
|
|
1318
|
+
for (const op of ops) {
|
|
1319
|
+
if (op.op === "BeginFrame") {
|
|
1320
|
+
result.push(op);
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const windowDuration = 0.3;
|
|
1325
|
+
const overlapFactor = 0.7;
|
|
1326
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1327
|
+
const windowFor = (unitIdx) => {
|
|
1328
|
+
const startTime = unitIdx * staggerDelay;
|
|
1329
|
+
const startF = startTime / duration;
|
|
1330
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1331
|
+
return { startF, endF };
|
|
1332
|
+
};
|
|
1333
|
+
let glyphIndex = 0;
|
|
1334
|
+
for (const op of ops) {
|
|
1335
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1336
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
let unitIndex;
|
|
1340
|
+
if (!byWord) {
|
|
1341
|
+
unitIndex = glyphIndex;
|
|
1342
|
+
} else {
|
|
1343
|
+
let wordIndex = -1, acc = 0;
|
|
1344
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1345
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1346
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1347
|
+
wordIndex = i;
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
acc += gcount;
|
|
1351
|
+
}
|
|
1352
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1353
|
+
}
|
|
1354
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1355
|
+
if (progress <= startF) {
|
|
1356
|
+
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1357
|
+
if (op.op === "FillPath") {
|
|
1358
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1359
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1360
|
+
} else {
|
|
1361
|
+
animated.opacity = 0;
|
|
1362
|
+
}
|
|
1363
|
+
result.push(animated);
|
|
1364
|
+
} else if (progress >= endF) {
|
|
1365
|
+
result.push(op);
|
|
1366
|
+
} else {
|
|
1367
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1368
|
+
const ease = easeOutCubic(Math.min(1, local));
|
|
1369
|
+
const dx = offset.x * (1 - ease);
|
|
1370
|
+
const dy = offset.y * (1 - ease);
|
|
1371
|
+
const animated = { ...op, x: op.x + dx, y: op.y + dy };
|
|
1372
|
+
if (op.op === "FillPath") {
|
|
1373
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1374
|
+
if (animated.fill.kind === "solid")
|
|
1375
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1376
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1377
|
+
} else {
|
|
1378
|
+
animated.opacity = animated.opacity * ease;
|
|
1379
|
+
}
|
|
1380
|
+
result.push(animated);
|
|
1381
|
+
}
|
|
1382
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1383
|
+
}
|
|
1384
|
+
return result;
|
|
1230
1385
|
}
|
|
1231
|
-
function applyMovingLettersAnimation(ops,
|
|
1386
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1232
1387
|
const amp = fontSize * 0.3;
|
|
1233
|
-
return waveTransform(ops, direction, amp,
|
|
1388
|
+
return waveTransform(ops, direction, amp, time);
|
|
1234
1389
|
}
|
|
1235
1390
|
function getWordSegments(lines) {
|
|
1236
1391
|
const segments = [];
|
|
@@ -1388,14 +1543,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1388
1543
|
return op;
|
|
1389
1544
|
});
|
|
1390
1545
|
}
|
|
1391
|
-
function waveTransform(ops, dir, amp,
|
|
1546
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1392
1547
|
let glyphIndex = 0;
|
|
1548
|
+
const fadeInDuration = 0.5;
|
|
1549
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1393
1550
|
return ops.map((op) => {
|
|
1394
1551
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1395
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1552
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1396
1553
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1397
1554
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1398
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1399
1555
|
if (op.op === "FillPath") {
|
|
1400
1556
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1401
1557
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -2060,8 +2216,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2060
2216
|
const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
|
|
2061
2217
|
await fonts.registerFromBytes(bytes, {
|
|
2062
2218
|
family: cf.family,
|
|
2063
|
-
weight: cf.weight ?? "400"
|
|
2064
|
-
style: cf.style ?? "normal"
|
|
2219
|
+
weight: cf.weight ?? "400"
|
|
2065
2220
|
});
|
|
2066
2221
|
} catch (err) {
|
|
2067
2222
|
throw new Error(
|
|
@@ -2073,7 +2228,6 @@ async function createTextEngine(opts = {}) {
|
|
|
2073
2228
|
const main = asset.font ?? {
|
|
2074
2229
|
family: "Roboto",
|
|
2075
2230
|
weight: "400",
|
|
2076
|
-
style: "normal",
|
|
2077
2231
|
size: 48,
|
|
2078
2232
|
color: "#000000",
|
|
2079
2233
|
opacity: 1
|
|
@@ -2122,13 +2276,13 @@ async function createTextEngine(opts = {}) {
|
|
|
2122
2276
|
);
|
|
2123
2277
|
}
|
|
2124
2278
|
},
|
|
2125
|
-
async renderFrame(asset, tSeconds) {
|
|
2279
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
2126
2280
|
try {
|
|
2127
2281
|
const main = await ensureFonts(asset);
|
|
2128
|
-
const desc = { family: main.family, weight: `${main.weight}
|
|
2282
|
+
const desc = { family: main.family, weight: `${main.weight}` };
|
|
2129
2283
|
let lines;
|
|
2130
2284
|
try {
|
|
2131
|
-
const emojiDesc = { family: "NotoEmoji", weight: "400"
|
|
2285
|
+
const emojiDesc = { family: "NotoEmoji", weight: "400" };
|
|
2132
2286
|
let emojiAvailable = false;
|
|
2133
2287
|
try {
|
|
2134
2288
|
await fonts.getFace(emojiDesc);
|
|
@@ -2169,7 +2323,6 @@ async function createTextEngine(opts = {}) {
|
|
|
2169
2323
|
family: main.family,
|
|
2170
2324
|
size: main.size,
|
|
2171
2325
|
weight: `${main.weight}`,
|
|
2172
|
-
style: main.style,
|
|
2173
2326
|
color: main.color,
|
|
2174
2327
|
opacity: main.opacity
|
|
2175
2328
|
},
|
|
@@ -2197,6 +2350,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2197
2350
|
const ops = applyAnimation(ops0, lines, {
|
|
2198
2351
|
t: tSeconds,
|
|
2199
2352
|
fontSize: main.size,
|
|
2353
|
+
clipDuration,
|
|
2200
2354
|
anim: asset.animation ? {
|
|
2201
2355
|
preset: asset.animation.preset,
|
|
2202
2356
|
speed: asset.animation.speed,
|
|
@@ -2244,14 +2398,15 @@ async function createTextEngine(opts = {}) {
|
|
|
2244
2398
|
width: asset.width ?? width,
|
|
2245
2399
|
height: asset.height ?? height,
|
|
2246
2400
|
fps,
|
|
2247
|
-
duration:
|
|
2401
|
+
duration: options.duration ?? 3,
|
|
2248
2402
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2249
2403
|
pixelRatio: asset.pixelRatio ?? pixelRatio,
|
|
2250
2404
|
hasAlpha: needsAlpha,
|
|
2251
2405
|
...options
|
|
2252
2406
|
};
|
|
2407
|
+
const clipDuration = finalOptions.duration;
|
|
2253
2408
|
const frameGenerator = async (time) => {
|
|
2254
|
-
return this.renderFrame(asset, time);
|
|
2409
|
+
return this.renderFrame(asset, time, clipDuration);
|
|
2255
2410
|
};
|
|
2256
2411
|
const actualOutputPath = await videoGenerator.generateVideo(frameGenerator, finalOptions);
|
|
2257
2412
|
return actualOutputPath;
|
package/dist/entry.node.d.cts
CHANGED
|
@@ -31,7 +31,6 @@ type RichTextValidated = Required<{
|
|
|
31
31
|
family: string;
|
|
32
32
|
size: number;
|
|
33
33
|
weight: string | number;
|
|
34
|
-
style: "normal" | "italic" | "oblique";
|
|
35
34
|
color: string;
|
|
36
35
|
opacity: number;
|
|
37
36
|
};
|
|
@@ -124,7 +123,6 @@ type Glyph = {
|
|
|
124
123
|
fontDesc?: {
|
|
125
124
|
family: string;
|
|
126
125
|
weight?: string | number;
|
|
127
|
-
style?: string;
|
|
128
126
|
};
|
|
129
127
|
};
|
|
130
128
|
type ShapedLine = {
|
|
@@ -232,14 +230,12 @@ declare function createTextEngine(opts?: {
|
|
|
232
230
|
registerFontFromFile(path: string, desc: {
|
|
233
231
|
family: string;
|
|
234
232
|
weight?: string | number;
|
|
235
|
-
style?: string;
|
|
236
233
|
}): Promise<void>;
|
|
237
234
|
registerFontFromUrl(url: string, desc: {
|
|
238
235
|
family: string;
|
|
239
236
|
weight?: string | number;
|
|
240
|
-
style?: string;
|
|
241
237
|
}): Promise<void>;
|
|
242
|
-
renderFrame(asset: RichTextValidated, tSeconds: number): Promise<DrawOp[]>;
|
|
238
|
+
renderFrame(asset: RichTextValidated, tSeconds: number, clipDuration?: number): Promise<DrawOp[]>;
|
|
243
239
|
createRenderer(p: {
|
|
244
240
|
width?: number;
|
|
245
241
|
height?: number;
|
package/dist/entry.node.d.ts
CHANGED
|
@@ -31,7 +31,6 @@ type RichTextValidated = Required<{
|
|
|
31
31
|
family: string;
|
|
32
32
|
size: number;
|
|
33
33
|
weight: string | number;
|
|
34
|
-
style: "normal" | "italic" | "oblique";
|
|
35
34
|
color: string;
|
|
36
35
|
opacity: number;
|
|
37
36
|
};
|
|
@@ -124,7 +123,6 @@ type Glyph = {
|
|
|
124
123
|
fontDesc?: {
|
|
125
124
|
family: string;
|
|
126
125
|
weight?: string | number;
|
|
127
|
-
style?: string;
|
|
128
126
|
};
|
|
129
127
|
};
|
|
130
128
|
type ShapedLine = {
|
|
@@ -232,14 +230,12 @@ declare function createTextEngine(opts?: {
|
|
|
232
230
|
registerFontFromFile(path: string, desc: {
|
|
233
231
|
family: string;
|
|
234
232
|
weight?: string | number;
|
|
235
|
-
style?: string;
|
|
236
233
|
}): Promise<void>;
|
|
237
234
|
registerFontFromUrl(url: string, desc: {
|
|
238
235
|
family: string;
|
|
239
236
|
weight?: string | number;
|
|
240
|
-
style?: string;
|
|
241
237
|
}): Promise<void>;
|
|
242
|
-
renderFrame(asset: RichTextValidated, tSeconds: number): Promise<DrawOp[]>;
|
|
238
|
+
renderFrame(asset: RichTextValidated, tSeconds: number, clipDuration?: number): Promise<DrawOp[]>;
|
|
243
239
|
createRenderer(p: {
|
|
244
240
|
width?: number;
|
|
245
241
|
height?: number;
|
package/dist/entry.node.js
CHANGED
|
@@ -61,7 +61,6 @@ var fontSchema = Joi.object({
|
|
|
61
61
|
family: Joi.string().default(CANVAS_CONFIG.DEFAULTS.fontFamily),
|
|
62
62
|
size: Joi.number().min(CANVAS_CONFIG.LIMITS.minFontSize).max(CANVAS_CONFIG.LIMITS.maxFontSize).default(CANVAS_CONFIG.DEFAULTS.fontSize),
|
|
63
63
|
weight: Joi.alternatives().try(Joi.string(), Joi.number()).default("400"),
|
|
64
|
-
style: Joi.string().valid("normal", "italic", "oblique").default("normal"),
|
|
65
64
|
color: Joi.string().pattern(HEX6).default(CANVAS_CONFIG.DEFAULTS.color),
|
|
66
65
|
opacity: Joi.number().min(0).max(1).default(1)
|
|
67
66
|
}).unknown(false);
|
|
@@ -81,7 +80,7 @@ var animationSchema = Joi.object({
|
|
|
81
80
|
speed: Joi.number().min(0.1).max(10).default(1),
|
|
82
81
|
duration: Joi.number().min(CANVAS_CONFIG.LIMITS.minDuration).max(CANVAS_CONFIG.LIMITS.maxDuration).optional(),
|
|
83
82
|
style: Joi.string().valid("character", "word").optional().when("preset", {
|
|
84
|
-
is: Joi.valid("typewriter", "shift"),
|
|
83
|
+
is: Joi.valid("typewriter", "shift", "fadeIn", "slideIn"),
|
|
85
84
|
then: Joi.optional(),
|
|
86
85
|
otherwise: Joi.forbidden()
|
|
87
86
|
}),
|
|
@@ -380,8 +379,7 @@ var FontRegistry = class _FontRegistry {
|
|
|
380
379
|
return this.hb;
|
|
381
380
|
}
|
|
382
381
|
key(desc) {
|
|
383
|
-
|
|
384
|
-
return `${desc.family}__${desc.weight ?? "400"}__${normalizedStyle}`;
|
|
382
|
+
return `${desc.family}__${desc.weight ?? "400"}`;
|
|
385
383
|
}
|
|
386
384
|
async registerFromBytes(bytes, desc) {
|
|
387
385
|
try {
|
|
@@ -430,14 +428,12 @@ var FontRegistry = class _FontRegistry {
|
|
|
430
428
|
try {
|
|
431
429
|
const bytes = await loader({
|
|
432
430
|
family: desc.family,
|
|
433
|
-
weight: desc.weight ?? "400"
|
|
434
|
-
style: desc.style ?? "normal"
|
|
431
|
+
weight: desc.weight ?? "400"
|
|
435
432
|
});
|
|
436
433
|
if (!bytes) return false;
|
|
437
434
|
await this.registerFromBytes(bytes, {
|
|
438
435
|
family: desc.family,
|
|
439
|
-
weight: desc.weight ?? "400"
|
|
440
|
-
style: desc.style ?? "normal"
|
|
436
|
+
weight: desc.weight ?? "400"
|
|
441
437
|
});
|
|
442
438
|
return true;
|
|
443
439
|
} catch {
|
|
@@ -694,13 +690,11 @@ var LayoutEngine = class {
|
|
|
694
690
|
const glyph = glyphs[i];
|
|
695
691
|
const glyphWidth = glyph.xAdvance;
|
|
696
692
|
if (glyph.char === "\n") {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
});
|
|
703
|
-
}
|
|
693
|
+
lines.push({
|
|
694
|
+
glyphs: currentLine,
|
|
695
|
+
width: currentWidth,
|
|
696
|
+
y: 0
|
|
697
|
+
});
|
|
704
698
|
currentLine = [];
|
|
705
699
|
currentWidth = 0;
|
|
706
700
|
lastBreakIndex = i;
|
|
@@ -959,7 +953,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
959
953
|
if (!p.anim || !p.anim.preset) return ops;
|
|
960
954
|
const { preset } = p.anim;
|
|
961
955
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
962
|
-
|
|
956
|
+
let autoDuration = Math.max(0.3, totalGlyphs / (30 * p.anim.speed));
|
|
957
|
+
if (p.clipDuration && !p.anim.duration) {
|
|
958
|
+
autoDuration = Math.min(autoDuration, p.clipDuration * 0.9);
|
|
959
|
+
}
|
|
960
|
+
const duration = p.anim.duration ?? autoDuration;
|
|
963
961
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
964
962
|
switch (preset) {
|
|
965
963
|
case "typewriter":
|
|
@@ -973,9 +971,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
973
971
|
duration
|
|
974
972
|
);
|
|
975
973
|
case "fadeIn":
|
|
976
|
-
return applyFadeInAnimation(ops, progress);
|
|
974
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
977
975
|
case "slideIn":
|
|
978
|
-
return applySlideInAnimation(ops, progress, p.anim.direction ?? "left", p.fontSize);
|
|
976
|
+
return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
|
|
979
977
|
case "shift":
|
|
980
978
|
return applyShiftAnimation(
|
|
981
979
|
ops,
|
|
@@ -996,7 +994,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
996
994
|
duration
|
|
997
995
|
);
|
|
998
996
|
case "movingLetters":
|
|
999
|
-
return applyMovingLettersAnimation(ops,
|
|
997
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1000
998
|
default:
|
|
1001
999
|
return ops;
|
|
1002
1000
|
}
|
|
@@ -1178,20 +1176,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1178
1176
|
}
|
|
1179
1177
|
return result;
|
|
1180
1178
|
}
|
|
1181
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1182
|
-
const
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1179
|
+
function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
1180
|
+
const byWord = style === "word";
|
|
1181
|
+
const byCharacter = style === "character";
|
|
1182
|
+
if (!byWord && !byCharacter) {
|
|
1183
|
+
const alpha = easeOutQuad(progress);
|
|
1184
|
+
const scale = 0.95 + 0.05 * alpha;
|
|
1185
|
+
return scaleAndFade(ops, alpha, scale);
|
|
1186
|
+
}
|
|
1187
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1188
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1189
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1190
|
+
if (totalUnits === 0) return ops;
|
|
1191
|
+
const result = [];
|
|
1192
|
+
for (const op of ops) {
|
|
1193
|
+
if (op.op === "BeginFrame") {
|
|
1194
|
+
result.push(op);
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const windowDuration = 0.3;
|
|
1199
|
+
const overlapFactor = 0.7;
|
|
1200
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1201
|
+
const windowFor = (unitIdx) => {
|
|
1202
|
+
const startTime = unitIdx * staggerDelay;
|
|
1203
|
+
const startF = startTime / duration;
|
|
1204
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1205
|
+
return { startF, endF };
|
|
1206
|
+
};
|
|
1207
|
+
let glyphIndex = 0;
|
|
1208
|
+
for (const op of ops) {
|
|
1209
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1210
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
let unitIndex;
|
|
1214
|
+
if (!byWord) {
|
|
1215
|
+
unitIndex = glyphIndex;
|
|
1216
|
+
} else {
|
|
1217
|
+
let wordIndex = -1, acc = 0;
|
|
1218
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1219
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1220
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1221
|
+
wordIndex = i;
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
acc += gcount;
|
|
1225
|
+
}
|
|
1226
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1227
|
+
}
|
|
1228
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1229
|
+
if (progress <= startF) {
|
|
1230
|
+
const animated = { ...op };
|
|
1231
|
+
if (op.op === "FillPath") {
|
|
1232
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1233
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1234
|
+
} else {
|
|
1235
|
+
animated.opacity = 0;
|
|
1236
|
+
}
|
|
1237
|
+
result.push(animated);
|
|
1238
|
+
} else if (progress >= endF) {
|
|
1239
|
+
result.push(op);
|
|
1240
|
+
} else {
|
|
1241
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1242
|
+
const ease = easeOutQuad(Math.min(1, local));
|
|
1243
|
+
const animated = { ...op };
|
|
1244
|
+
if (op.op === "FillPath") {
|
|
1245
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1246
|
+
if (animated.fill.kind === "solid")
|
|
1247
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1248
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1249
|
+
} else {
|
|
1250
|
+
animated.opacity = animated.opacity * ease;
|
|
1251
|
+
}
|
|
1252
|
+
result.push(animated);
|
|
1253
|
+
}
|
|
1254
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1255
|
+
}
|
|
1256
|
+
return result;
|
|
1185
1257
|
}
|
|
1186
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1258
|
+
function applySlideInAnimation(ops, lines, progress, direction, fontSize, style, duration) {
|
|
1259
|
+
const byWord = style === "word";
|
|
1260
|
+
const byCharacter = style === "character";
|
|
1261
|
+
if (!byWord && !byCharacter) {
|
|
1262
|
+
const easeProgress = easeOutCubic(progress);
|
|
1263
|
+
const shift = shiftFor(1 - easeProgress, direction, fontSize * 2);
|
|
1264
|
+
const alpha = easeOutQuad(progress);
|
|
1265
|
+
return translateGlyphOps(ops, shift.dx, shift.dy, alpha);
|
|
1266
|
+
}
|
|
1267
|
+
const startOffsets = {
|
|
1268
|
+
left: { x: fontSize * 2, y: 0 },
|
|
1269
|
+
right: { x: -fontSize * 2, y: 0 },
|
|
1270
|
+
up: { x: 0, y: fontSize * 2 },
|
|
1271
|
+
down: { x: 0, y: -fontSize * 2 }
|
|
1272
|
+
};
|
|
1273
|
+
const offset = startOffsets[direction];
|
|
1274
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1275
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1276
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1277
|
+
if (totalUnits === 0) return ops;
|
|
1278
|
+
const result = [];
|
|
1279
|
+
for (const op of ops) {
|
|
1280
|
+
if (op.op === "BeginFrame") {
|
|
1281
|
+
result.push(op);
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const windowDuration = 0.3;
|
|
1286
|
+
const overlapFactor = 0.7;
|
|
1287
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1288
|
+
const windowFor = (unitIdx) => {
|
|
1289
|
+
const startTime = unitIdx * staggerDelay;
|
|
1290
|
+
const startF = startTime / duration;
|
|
1291
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1292
|
+
return { startF, endF };
|
|
1293
|
+
};
|
|
1294
|
+
let glyphIndex = 0;
|
|
1295
|
+
for (const op of ops) {
|
|
1296
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1297
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
let unitIndex;
|
|
1301
|
+
if (!byWord) {
|
|
1302
|
+
unitIndex = glyphIndex;
|
|
1303
|
+
} else {
|
|
1304
|
+
let wordIndex = -1, acc = 0;
|
|
1305
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1306
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1307
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1308
|
+
wordIndex = i;
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
acc += gcount;
|
|
1312
|
+
}
|
|
1313
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1314
|
+
}
|
|
1315
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1316
|
+
if (progress <= startF) {
|
|
1317
|
+
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1318
|
+
if (op.op === "FillPath") {
|
|
1319
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1320
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1321
|
+
} else {
|
|
1322
|
+
animated.opacity = 0;
|
|
1323
|
+
}
|
|
1324
|
+
result.push(animated);
|
|
1325
|
+
} else if (progress >= endF) {
|
|
1326
|
+
result.push(op);
|
|
1327
|
+
} else {
|
|
1328
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1329
|
+
const ease = easeOutCubic(Math.min(1, local));
|
|
1330
|
+
const dx = offset.x * (1 - ease);
|
|
1331
|
+
const dy = offset.y * (1 - ease);
|
|
1332
|
+
const animated = { ...op, x: op.x + dx, y: op.y + dy };
|
|
1333
|
+
if (op.op === "FillPath") {
|
|
1334
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1335
|
+
if (animated.fill.kind === "solid")
|
|
1336
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1337
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1338
|
+
} else {
|
|
1339
|
+
animated.opacity = animated.opacity * ease;
|
|
1340
|
+
}
|
|
1341
|
+
result.push(animated);
|
|
1342
|
+
}
|
|
1343
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1344
|
+
}
|
|
1345
|
+
return result;
|
|
1191
1346
|
}
|
|
1192
|
-
function applyMovingLettersAnimation(ops,
|
|
1347
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1193
1348
|
const amp = fontSize * 0.3;
|
|
1194
|
-
return waveTransform(ops, direction, amp,
|
|
1349
|
+
return waveTransform(ops, direction, amp, time);
|
|
1195
1350
|
}
|
|
1196
1351
|
function getWordSegments(lines) {
|
|
1197
1352
|
const segments = [];
|
|
@@ -1349,14 +1504,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1349
1504
|
return op;
|
|
1350
1505
|
});
|
|
1351
1506
|
}
|
|
1352
|
-
function waveTransform(ops, dir, amp,
|
|
1507
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1353
1508
|
let glyphIndex = 0;
|
|
1509
|
+
const fadeInDuration = 0.5;
|
|
1510
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1354
1511
|
return ops.map((op) => {
|
|
1355
1512
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1356
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1513
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1357
1514
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1358
1515
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1359
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1360
1516
|
if (op.op === "FillPath") {
|
|
1361
1517
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1362
1518
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -2021,8 +2177,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2021
2177
|
const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
|
|
2022
2178
|
await fonts.registerFromBytes(bytes, {
|
|
2023
2179
|
family: cf.family,
|
|
2024
|
-
weight: cf.weight ?? "400"
|
|
2025
|
-
style: cf.style ?? "normal"
|
|
2180
|
+
weight: cf.weight ?? "400"
|
|
2026
2181
|
});
|
|
2027
2182
|
} catch (err) {
|
|
2028
2183
|
throw new Error(
|
|
@@ -2034,7 +2189,6 @@ async function createTextEngine(opts = {}) {
|
|
|
2034
2189
|
const main = asset.font ?? {
|
|
2035
2190
|
family: "Roboto",
|
|
2036
2191
|
weight: "400",
|
|
2037
|
-
style: "normal",
|
|
2038
2192
|
size: 48,
|
|
2039
2193
|
color: "#000000",
|
|
2040
2194
|
opacity: 1
|
|
@@ -2083,13 +2237,13 @@ async function createTextEngine(opts = {}) {
|
|
|
2083
2237
|
);
|
|
2084
2238
|
}
|
|
2085
2239
|
},
|
|
2086
|
-
async renderFrame(asset, tSeconds) {
|
|
2240
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
2087
2241
|
try {
|
|
2088
2242
|
const main = await ensureFonts(asset);
|
|
2089
|
-
const desc = { family: main.family, weight: `${main.weight}
|
|
2243
|
+
const desc = { family: main.family, weight: `${main.weight}` };
|
|
2090
2244
|
let lines;
|
|
2091
2245
|
try {
|
|
2092
|
-
const emojiDesc = { family: "NotoEmoji", weight: "400"
|
|
2246
|
+
const emojiDesc = { family: "NotoEmoji", weight: "400" };
|
|
2093
2247
|
let emojiAvailable = false;
|
|
2094
2248
|
try {
|
|
2095
2249
|
await fonts.getFace(emojiDesc);
|
|
@@ -2130,7 +2284,6 @@ async function createTextEngine(opts = {}) {
|
|
|
2130
2284
|
family: main.family,
|
|
2131
2285
|
size: main.size,
|
|
2132
2286
|
weight: `${main.weight}`,
|
|
2133
|
-
style: main.style,
|
|
2134
2287
|
color: main.color,
|
|
2135
2288
|
opacity: main.opacity
|
|
2136
2289
|
},
|
|
@@ -2158,6 +2311,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2158
2311
|
const ops = applyAnimation(ops0, lines, {
|
|
2159
2312
|
t: tSeconds,
|
|
2160
2313
|
fontSize: main.size,
|
|
2314
|
+
clipDuration,
|
|
2161
2315
|
anim: asset.animation ? {
|
|
2162
2316
|
preset: asset.animation.preset,
|
|
2163
2317
|
speed: asset.animation.speed,
|
|
@@ -2205,14 +2359,15 @@ async function createTextEngine(opts = {}) {
|
|
|
2205
2359
|
width: asset.width ?? width,
|
|
2206
2360
|
height: asset.height ?? height,
|
|
2207
2361
|
fps,
|
|
2208
|
-
duration:
|
|
2362
|
+
duration: options.duration ?? 3,
|
|
2209
2363
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2210
2364
|
pixelRatio: asset.pixelRatio ?? pixelRatio,
|
|
2211
2365
|
hasAlpha: needsAlpha,
|
|
2212
2366
|
...options
|
|
2213
2367
|
};
|
|
2368
|
+
const clipDuration = finalOptions.duration;
|
|
2214
2369
|
const frameGenerator = async (time) => {
|
|
2215
|
-
return this.renderFrame(asset, time);
|
|
2370
|
+
return this.renderFrame(asset, time, clipDuration);
|
|
2216
2371
|
};
|
|
2217
2372
|
const actualOutputPath = await videoGenerator.generateVideo(frameGenerator, finalOptions);
|
|
2218
2373
|
return actualOutputPath;
|
package/dist/entry.web.d.ts
CHANGED
|
@@ -31,7 +31,6 @@ type RichTextValidated = Required<{
|
|
|
31
31
|
family: string;
|
|
32
32
|
size: number;
|
|
33
33
|
weight: string | number;
|
|
34
|
-
style: "normal" | "italic" | "oblique";
|
|
35
34
|
color: string;
|
|
36
35
|
opacity: number;
|
|
37
36
|
};
|
|
@@ -124,7 +123,6 @@ type Glyph = {
|
|
|
124
123
|
fontDesc?: {
|
|
125
124
|
family: string;
|
|
126
125
|
weight?: string | number;
|
|
127
|
-
style?: string;
|
|
128
126
|
};
|
|
129
127
|
};
|
|
130
128
|
type ShapedLine = {
|
|
@@ -215,14 +213,12 @@ declare function createTextEngine(opts?: {
|
|
|
215
213
|
registerFontFromUrl(url: string, desc: {
|
|
216
214
|
family: string;
|
|
217
215
|
weight?: string | number;
|
|
218
|
-
style?: string;
|
|
219
216
|
}): Promise<void>;
|
|
220
217
|
registerFontFromFile(source: string | Blob, desc: {
|
|
221
218
|
family: string;
|
|
222
219
|
weight?: string | number;
|
|
223
|
-
style?: string;
|
|
224
220
|
}): Promise<void>;
|
|
225
|
-
renderFrame(asset: RichTextValidated, tSeconds: number): Promise<DrawOp[]>;
|
|
221
|
+
renderFrame(asset: RichTextValidated, tSeconds: number, clipDuration?: number): Promise<DrawOp[]>;
|
|
226
222
|
createRenderer(canvas: HTMLCanvasElement | OffscreenCanvas): {
|
|
227
223
|
render(ops: DrawOp[]): Promise<void>;
|
|
228
224
|
};
|
package/dist/entry.web.js
CHANGED
|
@@ -65,7 +65,6 @@ var fontSchema = Joi.object({
|
|
|
65
65
|
family: Joi.string().default(CANVAS_CONFIG.DEFAULTS.fontFamily),
|
|
66
66
|
size: Joi.number().min(CANVAS_CONFIG.LIMITS.minFontSize).max(CANVAS_CONFIG.LIMITS.maxFontSize).default(CANVAS_CONFIG.DEFAULTS.fontSize),
|
|
67
67
|
weight: Joi.alternatives().try(Joi.string(), Joi.number()).default("400"),
|
|
68
|
-
style: Joi.string().valid("normal", "italic", "oblique").default("normal"),
|
|
69
68
|
color: Joi.string().pattern(HEX6).default(CANVAS_CONFIG.DEFAULTS.color),
|
|
70
69
|
opacity: Joi.number().min(0).max(1).default(1)
|
|
71
70
|
}).unknown(false);
|
|
@@ -85,7 +84,7 @@ var animationSchema = Joi.object({
|
|
|
85
84
|
speed: Joi.number().min(0.1).max(10).default(1),
|
|
86
85
|
duration: Joi.number().min(CANVAS_CONFIG.LIMITS.minDuration).max(CANVAS_CONFIG.LIMITS.maxDuration).optional(),
|
|
87
86
|
style: Joi.string().valid("character", "word").optional().when("preset", {
|
|
88
|
-
is: Joi.valid("typewriter", "shift"),
|
|
87
|
+
is: Joi.valid("typewriter", "shift", "fadeIn", "slideIn"),
|
|
89
88
|
then: Joi.optional(),
|
|
90
89
|
otherwise: Joi.forbidden()
|
|
91
90
|
}),
|
|
@@ -383,8 +382,7 @@ var _FontRegistry = class _FontRegistry {
|
|
|
383
382
|
return this.hb;
|
|
384
383
|
}
|
|
385
384
|
key(desc) {
|
|
386
|
-
|
|
387
|
-
return `${desc.family}__${desc.weight ?? "400"}__${normalizedStyle}`;
|
|
385
|
+
return `${desc.family}__${desc.weight ?? "400"}`;
|
|
388
386
|
}
|
|
389
387
|
async registerFromBytes(bytes, desc) {
|
|
390
388
|
try {
|
|
@@ -433,14 +431,12 @@ var _FontRegistry = class _FontRegistry {
|
|
|
433
431
|
try {
|
|
434
432
|
const bytes = await loader({
|
|
435
433
|
family: desc.family,
|
|
436
|
-
weight: desc.weight ?? "400"
|
|
437
|
-
style: desc.style ?? "normal"
|
|
434
|
+
weight: desc.weight ?? "400"
|
|
438
435
|
});
|
|
439
436
|
if (!bytes) return false;
|
|
440
437
|
await this.registerFromBytes(bytes, {
|
|
441
438
|
family: desc.family,
|
|
442
|
-
weight: desc.weight ?? "400"
|
|
443
|
-
style: desc.style ?? "normal"
|
|
439
|
+
weight: desc.weight ?? "400"
|
|
444
440
|
});
|
|
445
441
|
return true;
|
|
446
442
|
} catch {
|
|
@@ -699,13 +695,11 @@ var LayoutEngine = class {
|
|
|
699
695
|
const glyph = glyphs[i];
|
|
700
696
|
const glyphWidth = glyph.xAdvance;
|
|
701
697
|
if (glyph.char === "\n") {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
});
|
|
708
|
-
}
|
|
698
|
+
lines.push({
|
|
699
|
+
glyphs: currentLine,
|
|
700
|
+
width: currentWidth,
|
|
701
|
+
y: 0
|
|
702
|
+
});
|
|
709
703
|
currentLine = [];
|
|
710
704
|
currentWidth = 0;
|
|
711
705
|
lastBreakIndex = i;
|
|
@@ -964,7 +958,11 @@ function applyAnimation(ops, lines, p) {
|
|
|
964
958
|
if (!p.anim || !p.anim.preset) return ops;
|
|
965
959
|
const { preset } = p.anim;
|
|
966
960
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
967
|
-
|
|
961
|
+
let autoDuration = Math.max(0.3, totalGlyphs / (30 * p.anim.speed));
|
|
962
|
+
if (p.clipDuration && !p.anim.duration) {
|
|
963
|
+
autoDuration = Math.min(autoDuration, p.clipDuration * 0.9);
|
|
964
|
+
}
|
|
965
|
+
const duration = p.anim.duration ?? autoDuration;
|
|
968
966
|
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
969
967
|
switch (preset) {
|
|
970
968
|
case "typewriter":
|
|
@@ -978,9 +976,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
978
976
|
duration
|
|
979
977
|
);
|
|
980
978
|
case "fadeIn":
|
|
981
|
-
return applyFadeInAnimation(ops, progress);
|
|
979
|
+
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
982
980
|
case "slideIn":
|
|
983
|
-
return applySlideInAnimation(ops, progress, p.anim.direction ?? "left", p.fontSize);
|
|
981
|
+
return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
|
|
984
982
|
case "shift":
|
|
985
983
|
return applyShiftAnimation(
|
|
986
984
|
ops,
|
|
@@ -1001,7 +999,7 @@ function applyAnimation(ops, lines, p) {
|
|
|
1001
999
|
duration
|
|
1002
1000
|
);
|
|
1003
1001
|
case "movingLetters":
|
|
1004
|
-
return applyMovingLettersAnimation(ops,
|
|
1002
|
+
return applyMovingLettersAnimation(ops, p.t, p.anim.direction ?? "up", p.fontSize);
|
|
1005
1003
|
default:
|
|
1006
1004
|
return ops;
|
|
1007
1005
|
}
|
|
@@ -1183,20 +1181,177 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1183
1181
|
}
|
|
1184
1182
|
return result;
|
|
1185
1183
|
}
|
|
1186
|
-
function applyFadeInAnimation(ops, progress) {
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1184
|
+
function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
1185
|
+
const byWord = style === "word";
|
|
1186
|
+
const byCharacter = style === "character";
|
|
1187
|
+
if (!byWord && !byCharacter) {
|
|
1188
|
+
const alpha = easeOutQuad(progress);
|
|
1189
|
+
const scale = 0.95 + 0.05 * alpha;
|
|
1190
|
+
return scaleAndFade(ops, alpha, scale);
|
|
1191
|
+
}
|
|
1192
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1193
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1194
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1195
|
+
if (totalUnits === 0) return ops;
|
|
1196
|
+
const result = [];
|
|
1197
|
+
for (const op of ops) {
|
|
1198
|
+
if (op.op === "BeginFrame") {
|
|
1199
|
+
result.push(op);
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const windowDuration = 0.3;
|
|
1204
|
+
const overlapFactor = 0.7;
|
|
1205
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1206
|
+
const windowFor = (unitIdx) => {
|
|
1207
|
+
const startTime = unitIdx * staggerDelay;
|
|
1208
|
+
const startF = startTime / duration;
|
|
1209
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1210
|
+
return { startF, endF };
|
|
1211
|
+
};
|
|
1212
|
+
let glyphIndex = 0;
|
|
1213
|
+
for (const op of ops) {
|
|
1214
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1215
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
let unitIndex;
|
|
1219
|
+
if (!byWord) {
|
|
1220
|
+
unitIndex = glyphIndex;
|
|
1221
|
+
} else {
|
|
1222
|
+
let wordIndex = -1, acc = 0;
|
|
1223
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1224
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1225
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1226
|
+
wordIndex = i;
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
acc += gcount;
|
|
1230
|
+
}
|
|
1231
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1232
|
+
}
|
|
1233
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1234
|
+
if (progress <= startF) {
|
|
1235
|
+
const animated = { ...op };
|
|
1236
|
+
if (op.op === "FillPath") {
|
|
1237
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1238
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1239
|
+
} else {
|
|
1240
|
+
animated.opacity = 0;
|
|
1241
|
+
}
|
|
1242
|
+
result.push(animated);
|
|
1243
|
+
} else if (progress >= endF) {
|
|
1244
|
+
result.push(op);
|
|
1245
|
+
} else {
|
|
1246
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1247
|
+
const ease = easeOutQuad(Math.min(1, local));
|
|
1248
|
+
const animated = { ...op };
|
|
1249
|
+
if (op.op === "FillPath") {
|
|
1250
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1251
|
+
if (animated.fill.kind === "solid")
|
|
1252
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1253
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1254
|
+
} else {
|
|
1255
|
+
animated.opacity = animated.opacity * ease;
|
|
1256
|
+
}
|
|
1257
|
+
result.push(animated);
|
|
1258
|
+
}
|
|
1259
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1260
|
+
}
|
|
1261
|
+
return result;
|
|
1190
1262
|
}
|
|
1191
|
-
function applySlideInAnimation(ops, progress, direction, fontSize) {
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1263
|
+
function applySlideInAnimation(ops, lines, progress, direction, fontSize, style, duration) {
|
|
1264
|
+
const byWord = style === "word";
|
|
1265
|
+
const byCharacter = style === "character";
|
|
1266
|
+
if (!byWord && !byCharacter) {
|
|
1267
|
+
const easeProgress = easeOutCubic(progress);
|
|
1268
|
+
const shift = shiftFor(1 - easeProgress, direction, fontSize * 2);
|
|
1269
|
+
const alpha = easeOutQuad(progress);
|
|
1270
|
+
return translateGlyphOps(ops, shift.dx, shift.dy, alpha);
|
|
1271
|
+
}
|
|
1272
|
+
const startOffsets = {
|
|
1273
|
+
left: { x: fontSize * 2, y: 0 },
|
|
1274
|
+
right: { x: -fontSize * 2, y: 0 },
|
|
1275
|
+
up: { x: 0, y: fontSize * 2 },
|
|
1276
|
+
down: { x: 0, y: -fontSize * 2 }
|
|
1277
|
+
};
|
|
1278
|
+
const offset = startOffsets[direction];
|
|
1279
|
+
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
1280
|
+
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1281
|
+
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
1282
|
+
if (totalUnits === 0) return ops;
|
|
1283
|
+
const result = [];
|
|
1284
|
+
for (const op of ops) {
|
|
1285
|
+
if (op.op === "BeginFrame") {
|
|
1286
|
+
result.push(op);
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const windowDuration = 0.3;
|
|
1291
|
+
const overlapFactor = 0.7;
|
|
1292
|
+
const staggerDelay = duration * overlapFactor / Math.max(1, totalUnits - 1);
|
|
1293
|
+
const windowFor = (unitIdx) => {
|
|
1294
|
+
const startTime = unitIdx * staggerDelay;
|
|
1295
|
+
const startF = startTime / duration;
|
|
1296
|
+
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
1297
|
+
return { startF, endF };
|
|
1298
|
+
};
|
|
1299
|
+
let glyphIndex = 0;
|
|
1300
|
+
for (const op of ops) {
|
|
1301
|
+
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
1302
|
+
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
let unitIndex;
|
|
1306
|
+
if (!byWord) {
|
|
1307
|
+
unitIndex = glyphIndex;
|
|
1308
|
+
} else {
|
|
1309
|
+
let wordIndex = -1, acc = 0;
|
|
1310
|
+
for (let i = 0; i < wordSegments.length; i++) {
|
|
1311
|
+
const gcount = wordSegments[i].glyphCount;
|
|
1312
|
+
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
1313
|
+
wordIndex = i;
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
acc += gcount;
|
|
1317
|
+
}
|
|
1318
|
+
unitIndex = Math.max(0, wordIndex);
|
|
1319
|
+
}
|
|
1320
|
+
const { startF, endF } = windowFor(unitIndex);
|
|
1321
|
+
if (progress <= startF) {
|
|
1322
|
+
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1323
|
+
if (op.op === "FillPath") {
|
|
1324
|
+
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
1325
|
+
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
1326
|
+
} else {
|
|
1327
|
+
animated.opacity = 0;
|
|
1328
|
+
}
|
|
1329
|
+
result.push(animated);
|
|
1330
|
+
} else if (progress >= endF) {
|
|
1331
|
+
result.push(op);
|
|
1332
|
+
} else {
|
|
1333
|
+
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
1334
|
+
const ease = easeOutCubic(Math.min(1, local));
|
|
1335
|
+
const dx = offset.x * (1 - ease);
|
|
1336
|
+
const dy = offset.y * (1 - ease);
|
|
1337
|
+
const animated = { ...op, x: op.x + dx, y: op.y + dy };
|
|
1338
|
+
if (op.op === "FillPath") {
|
|
1339
|
+
const targetOpacity = animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
1340
|
+
if (animated.fill.kind === "solid")
|
|
1341
|
+
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1342
|
+
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
1343
|
+
} else {
|
|
1344
|
+
animated.opacity = animated.opacity * ease;
|
|
1345
|
+
}
|
|
1346
|
+
result.push(animated);
|
|
1347
|
+
}
|
|
1348
|
+
if (isGlyphFill(op)) glyphIndex++;
|
|
1349
|
+
}
|
|
1350
|
+
return result;
|
|
1196
1351
|
}
|
|
1197
|
-
function applyMovingLettersAnimation(ops,
|
|
1352
|
+
function applyMovingLettersAnimation(ops, time, direction, fontSize) {
|
|
1198
1353
|
const amp = fontSize * 0.3;
|
|
1199
|
-
return waveTransform(ops, direction, amp,
|
|
1354
|
+
return waveTransform(ops, direction, amp, time);
|
|
1200
1355
|
}
|
|
1201
1356
|
function getWordSegments(lines) {
|
|
1202
1357
|
const segments = [];
|
|
@@ -1354,14 +1509,15 @@ function translateGlyphOps(ops, dx, dy, alpha = 1) {
|
|
|
1354
1509
|
return op;
|
|
1355
1510
|
});
|
|
1356
1511
|
}
|
|
1357
|
-
function waveTransform(ops, dir, amp,
|
|
1512
|
+
function waveTransform(ops, dir, amp, time) {
|
|
1358
1513
|
let glyphIndex = 0;
|
|
1514
|
+
const fadeInDuration = 0.5;
|
|
1515
|
+
const waveAlpha = Math.min(1, time / fadeInDuration);
|
|
1359
1516
|
return ops.map((op) => {
|
|
1360
1517
|
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1361
|
-
const phase = Math.sin(glyphIndex / 5 * Math.PI +
|
|
1518
|
+
const phase = Math.sin(glyphIndex / 5 * Math.PI + time * Math.PI * 2);
|
|
1362
1519
|
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
1363
1520
|
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
1364
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
1365
1521
|
if (op.op === "FillPath") {
|
|
1366
1522
|
if (!isShadowFill(op)) glyphIndex++;
|
|
1367
1523
|
const out = { ...op, x: op.x + dx, y: op.y + dy };
|
|
@@ -1683,8 +1839,7 @@ async function createTextEngine(opts = {}) {
|
|
|
1683
1839
|
FontRegistry.setFallbackLoader(async (desc) => {
|
|
1684
1840
|
const family = (desc.family ?? "Roboto").toLowerCase();
|
|
1685
1841
|
const weight = `${desc.weight ?? "400"}`;
|
|
1686
|
-
|
|
1687
|
-
if (family === "roboto" && weight === "400" && style === "normal") {
|
|
1842
|
+
if (family === "roboto" && weight === "400") {
|
|
1688
1843
|
return fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
|
|
1689
1844
|
}
|
|
1690
1845
|
return void 0;
|
|
@@ -1703,8 +1858,7 @@ async function createTextEngine(opts = {}) {
|
|
|
1703
1858
|
const bytes = await fetchToArrayBuffer(cf.src);
|
|
1704
1859
|
await fonts.registerFromBytes(bytes, {
|
|
1705
1860
|
family: cf.family,
|
|
1706
|
-
weight: cf.weight ?? "400"
|
|
1707
|
-
style: cf.style ?? "normal"
|
|
1861
|
+
weight: cf.weight ?? "400"
|
|
1708
1862
|
});
|
|
1709
1863
|
} catch (err) {
|
|
1710
1864
|
throw new Error(
|
|
@@ -1716,27 +1870,25 @@ async function createTextEngine(opts = {}) {
|
|
|
1716
1870
|
const main = asset.font ?? {
|
|
1717
1871
|
family: "Roboto",
|
|
1718
1872
|
weight: "400",
|
|
1719
|
-
style: "normal",
|
|
1720
1873
|
size: 48,
|
|
1721
1874
|
color: "#000000",
|
|
1722
1875
|
opacity: 1
|
|
1723
1876
|
};
|
|
1724
|
-
const desc = { family: main.family, weight: `${main.weight}
|
|
1877
|
+
const desc = { family: main.family, weight: `${main.weight}` };
|
|
1725
1878
|
const ensureFace = async () => {
|
|
1726
1879
|
try {
|
|
1727
1880
|
await fonts.getFace(desc);
|
|
1728
1881
|
} catch {
|
|
1729
|
-
const wantsDefaultRoboto = (main.family || "Roboto").toLowerCase() === "roboto" && `${main.weight}` === "400"
|
|
1882
|
+
const wantsDefaultRoboto = (main.family || "Roboto").toLowerCase() === "roboto" && `${main.weight}` === "400";
|
|
1730
1883
|
if (wantsDefaultRoboto) {
|
|
1731
1884
|
const bytes = await fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
|
|
1732
1885
|
await fonts.registerFromBytes(bytes, {
|
|
1733
1886
|
family: "Roboto",
|
|
1734
|
-
weight: "400"
|
|
1735
|
-
style: "normal"
|
|
1887
|
+
weight: "400"
|
|
1736
1888
|
});
|
|
1737
1889
|
} else {
|
|
1738
1890
|
throw new Error(
|
|
1739
|
-
`Font not registered for ${desc.family}__${desc.weight}
|
|
1891
|
+
`Font not registered for ${desc.family}__${desc.weight}`
|
|
1740
1892
|
);
|
|
1741
1893
|
}
|
|
1742
1894
|
}
|
|
@@ -1804,13 +1956,13 @@ async function createTextEngine(opts = {}) {
|
|
|
1804
1956
|
);
|
|
1805
1957
|
}
|
|
1806
1958
|
},
|
|
1807
|
-
async renderFrame(asset, tSeconds) {
|
|
1959
|
+
async renderFrame(asset, tSeconds, clipDuration) {
|
|
1808
1960
|
try {
|
|
1809
1961
|
const main = await ensureFonts(asset);
|
|
1810
|
-
const desc = { family: main.family, weight: `${main.weight}
|
|
1962
|
+
const desc = { family: main.family, weight: `${main.weight}` };
|
|
1811
1963
|
let lines;
|
|
1812
1964
|
try {
|
|
1813
|
-
const emojiDesc = { family: "NotoEmoji", weight: "400"
|
|
1965
|
+
const emojiDesc = { family: "NotoEmoji", weight: "400" };
|
|
1814
1966
|
let emojiAvailable = false;
|
|
1815
1967
|
try {
|
|
1816
1968
|
await fonts.getFace(emojiDesc);
|
|
@@ -1851,7 +2003,6 @@ async function createTextEngine(opts = {}) {
|
|
|
1851
2003
|
family: main.family,
|
|
1852
2004
|
size: main.size,
|
|
1853
2005
|
weight: `${main.weight}`,
|
|
1854
|
-
style: main.style,
|
|
1855
2006
|
color: main.color,
|
|
1856
2007
|
opacity: main.opacity
|
|
1857
2008
|
},
|
|
@@ -1879,6 +2030,7 @@ async function createTextEngine(opts = {}) {
|
|
|
1879
2030
|
const ops = applyAnimation(ops0, lines, {
|
|
1880
2031
|
t: tSeconds,
|
|
1881
2032
|
fontSize: main.size,
|
|
2033
|
+
clipDuration,
|
|
1882
2034
|
anim: asset.animation ? {
|
|
1883
2035
|
preset: asset.animation.preset,
|
|
1884
2036
|
speed: asset.animation.speed,
|
package/package.json
CHANGED