@shotstack/shotstack-canvas 1.3.9 → 1.4.1

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