@shotstack/shotstack-canvas 2.0.12 → 2.0.14

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.
@@ -371,6 +371,7 @@ __export(entry_node_exports, {
371
371
  calculateAnimationStatesForGroup: () => calculateAnimationStatesForGroup,
372
372
  commandsToPathString: () => commandsToPathString,
373
373
  computeSimplePathBounds: () => computeSimplePathBounds,
374
+ containsRTLCharacters: () => containsRTLCharacters,
374
375
  createDefaultGeneratorConfig: () => createDefaultGeneratorConfig,
375
376
  createFrameSchedule: () => createFrameSchedule,
376
377
  createNodePainter: () => createNodePainter,
@@ -378,6 +379,8 @@ __export(entry_node_exports, {
378
379
  createRichCaptionRenderer: () => createRichCaptionRenderer,
379
380
  createTextEngine: () => createTextEngine,
380
381
  createVideoEncoder: () => createVideoEncoder,
382
+ detectParagraphDirection: () => detectParagraphDirection,
383
+ detectParagraphDirectionFromWords: () => detectParagraphDirectionFromWords,
381
384
  detectPlatform: () => detectPlatform,
382
385
  detectSubtitleFormat: () => detectSubtitleFormat,
383
386
  extractCaptionPadding: () => extractCaptionPadding,
@@ -389,10 +392,12 @@ __export(entry_node_exports, {
389
392
  getDrawCaptionWordOps: () => getDrawCaptionWordOps,
390
393
  getEncoderCapabilities: () => getEncoderCapabilities,
391
394
  getEncoderWarning: () => getEncoderWarning,
395
+ getVisibleText: () => getVisibleText,
392
396
  groupWordsByPause: () => groupWordsByPause,
393
397
  isDrawCaptionWordOp: () => isDrawCaptionWordOp,
394
398
  isRTLText: () => isRTLText,
395
399
  isWebCodecsH264Supported: () => isWebCodecsH264Supported,
400
+ mirrorAnimationDirection: () => mirrorAnimationDirection,
396
401
  normalizePath: () => normalizePath,
397
402
  normalizePathString: () => normalizePathString,
398
403
  parseSubtitleToWords: () => parseSubtitleToWords,
@@ -400,6 +405,7 @@ __export(entry_node_exports, {
400
405
  quadraticToCubic: () => quadraticToCubic,
401
406
  renderSvgAssetToPng: () => renderSvgAssetToPng,
402
407
  renderSvgToPng: () => renderSvgToPng,
408
+ reorderWordsForLine: () => reorderWordsForLine,
403
409
  richCaptionAssetSchema: () => richCaptionAssetSchema,
404
410
  shapeToSvgString: () => shapeToSvgString,
405
411
  svgAssetSchema: () => import_zod2.svgAssetSchema,
@@ -1220,10 +1226,9 @@ var FontRegistry = class _FontRegistry {
1220
1226
  return;
1221
1227
  }
1222
1228
  try {
1223
- const moduleName = "canvas";
1224
1229
  const canvasMod = await import(
1225
1230
  /* @vite-ignore */
1226
- moduleName
1231
+ "canvas"
1227
1232
  );
1228
1233
  const GlobalFonts = canvasMod.GlobalFonts;
1229
1234
  if (GlobalFonts && typeof GlobalFonts.register === "function") {
@@ -1281,6 +1286,96 @@ var FontRegistry = class _FontRegistry {
1281
1286
  }
1282
1287
  };
1283
1288
 
1289
+ // src/core/bidi.ts
1290
+ var import_bidi_js = __toESM(require("bidi-js"), 1);
1291
+ var bidiInstance = null;
1292
+ function getBidi() {
1293
+ if (!bidiInstance) {
1294
+ bidiInstance = (0, import_bidi_js.default)();
1295
+ }
1296
+ return bidiInstance;
1297
+ }
1298
+ var RTL_SCRIPT_REGEX = /[֐-׿؀-ۿ܀-ݏݐ-ݿހ-޿߀-߿ࠀ-࠿ࡀ-࡟ࡠ-࡯ࢠ-ࣿיִ-ﭏﭐ-﷿ﹰ-𐴀-𐴿𐹠-𐹿𞠀-𞣟𞤀-𞥟𞱰-𞲿𞴀-𞵏𞸀-𞻿]/u;
1299
+ function containsRTLCharacters(text) {
1300
+ return RTL_SCRIPT_REGEX.test(text);
1301
+ }
1302
+ function detectParagraphDirection(text) {
1303
+ const bidi = getBidi();
1304
+ const { paragraphs } = bidi.getEmbeddingLevels(text);
1305
+ if (paragraphs.length === 0) {
1306
+ return "ltr";
1307
+ }
1308
+ return paragraphs[0].level % 2 === 1 ? "rtl" : "ltr";
1309
+ }
1310
+ function detectParagraphDirectionFromWords(words) {
1311
+ const combined = words.join(" ");
1312
+ return detectParagraphDirection(combined);
1313
+ }
1314
+ function reorderWordsForLine(wordTexts, paragraphDirection) {
1315
+ if (wordTexts.length <= 1) {
1316
+ return wordTexts.map((_, i) => i);
1317
+ }
1318
+ const lineText = wordTexts.join(" ");
1319
+ const bidi = getBidi();
1320
+ const { levels } = bidi.getEmbeddingLevels(lineText, paragraphDirection);
1321
+ const wordLevels = [];
1322
+ let charIndex = 0;
1323
+ for (let i = 0; i < wordTexts.length; i++) {
1324
+ const wordLevel = levels[charIndex];
1325
+ wordLevels.push(wordLevel);
1326
+ charIndex += wordTexts[i].length;
1327
+ if (i < wordTexts.length - 1) {
1328
+ charIndex += 1;
1329
+ }
1330
+ }
1331
+ const indices = wordTexts.map((_, i) => i);
1332
+ const maxLevel = Math.max(...wordLevels);
1333
+ const minOddLevel = paragraphDirection === "rtl" ? 1 : Math.min(...wordLevels.filter((l) => l % 2 === 1), maxLevel + 1);
1334
+ for (let level = maxLevel; level >= minOddLevel; level--) {
1335
+ let start = 0;
1336
+ while (start < indices.length) {
1337
+ if (wordLevels[indices[start]] >= level) {
1338
+ let end = start;
1339
+ while (end + 1 < indices.length && wordLevels[indices[end + 1]] >= level) {
1340
+ end++;
1341
+ }
1342
+ if (start < end) {
1343
+ let left = start;
1344
+ let right = end;
1345
+ while (left < right) {
1346
+ const tmp = indices[left];
1347
+ indices[left] = indices[right];
1348
+ indices[right] = tmp;
1349
+ left++;
1350
+ right--;
1351
+ }
1352
+ }
1353
+ start = end + 1;
1354
+ } else {
1355
+ start++;
1356
+ }
1357
+ }
1358
+ }
1359
+ return indices;
1360
+ }
1361
+ function getVisibleText(text, visibleCharacters, isRTL) {
1362
+ if (visibleCharacters < 0 || visibleCharacters >= text.length) {
1363
+ return text;
1364
+ }
1365
+ if (visibleCharacters === 0) {
1366
+ return "";
1367
+ }
1368
+ return text.slice(0, visibleCharacters);
1369
+ }
1370
+ function mirrorAnimationDirection(direction, isRTL) {
1371
+ if (!isRTL) {
1372
+ return direction;
1373
+ }
1374
+ if (direction === "left") return "right";
1375
+ if (direction === "right") return "left";
1376
+ return direction;
1377
+ }
1378
+
1284
1379
  // src/core/layout.ts
1285
1380
  function isEmoji(char) {
1286
1381
  const code = char.codePointAt(0);
@@ -1310,7 +1405,7 @@ var LayoutEngine = class {
1310
1405
  return t;
1311
1406
  }
1312
1407
  }
1313
- async shapeFull(text, desc) {
1408
+ async shapeFull(text, desc, direction) {
1314
1409
  try {
1315
1410
  const hb = await this.fonts.getHB();
1316
1411
  const buffer = hb.createBuffer();
@@ -1349,7 +1444,8 @@ var LayoutEngine = class {
1349
1444
  let shaped;
1350
1445
  try {
1351
1446
  if (!emojiFallback) {
1352
- shaped = await this.shapeFull(input, desc);
1447
+ const textDirection = containsRTLCharacters(input) ? "rtl" : void 0;
1448
+ shaped = await this.shapeFull(input, desc, textDirection);
1353
1449
  } else {
1354
1450
  const chars = Array.from(input);
1355
1451
  const runs = [];
@@ -1370,7 +1466,8 @@ var LayoutEngine = class {
1370
1466
  shaped = [];
1371
1467
  for (const run of runs) {
1372
1468
  const runFont = run.isEmoji ? emojiFallback : desc;
1373
- const runShaped = await this.shapeFull(run.text, runFont);
1469
+ const runDirection = containsRTLCharacters(run.text) ? "rtl" : void 0;
1470
+ const runShaped = await this.shapeFull(run.text, runFont, runDirection);
1374
1471
  for (const glyph of runShaped) {
1375
1472
  glyph.cl += run.startIndex;
1376
1473
  glyph.fontDesc = runFont;
@@ -1471,9 +1568,11 @@ var LayoutEngine = class {
1471
1568
  y: 0
1472
1569
  });
1473
1570
  }
1571
+ const textIsRTL = containsRTLCharacters(input);
1474
1572
  const lineHeight = params.lineHeight * fontSize;
1475
1573
  for (let i = 0; i < lines.length; i++) {
1476
1574
  lines[i].y = (i + 1) * lineHeight;
1575
+ lines[i].isRTL = textIsRTL;
1477
1576
  }
1478
1577
  return lines;
1479
1578
  } catch (err) {
@@ -1512,6 +1611,14 @@ function normalizePadding(padding) {
1512
1611
  }
1513
1612
  return padding;
1514
1613
  }
1614
+ function resolveHorizontalAlign(align, isRTL) {
1615
+ if (!isRTL || align === "center") {
1616
+ return align;
1617
+ }
1618
+ if (align === "left") return "right";
1619
+ if (align === "right") return "left";
1620
+ return align;
1621
+ }
1515
1622
  async function buildDrawOps(p) {
1516
1623
  const ops = [];
1517
1624
  const padding = normalizePadding(p.padding);
@@ -1552,7 +1659,8 @@ async function buildDrawOps(p) {
1552
1659
  let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
1553
1660
  for (const line of p.lines) {
1554
1661
  let lineX;
1555
- switch (p.align.horizontal) {
1662
+ const effectiveAlign = resolveHorizontalAlign(p.align.horizontal, line.isRTL);
1663
+ switch (effectiveAlign) {
1556
1664
  case "left":
1557
1665
  lineX = 0;
1558
1666
  break;
@@ -1680,7 +1788,7 @@ async function buildDrawOps(p) {
1680
1788
  const contentWidth = p.contentRect?.width ?? p.canvas.width;
1681
1789
  const contentHeight = p.contentRect?.height ?? p.canvas.height;
1682
1790
  const borderWidth2 = p.border?.width ?? 0;
1683
- const borderRadius = p.border?.radius ?? 0;
1791
+ const borderRadius = p.border?.radius ?? p.background?.borderRadius ?? 0;
1684
1792
  const halfBorder = borderWidth2 / 2;
1685
1793
  const canvasCenterX = p.canvas.width / 2;
1686
1794
  const canvasCenterY = p.canvas.height / 2;
@@ -1798,7 +1906,15 @@ function applyAnimation(ops, lines, p) {
1798
1906
  case "fadeIn":
1799
1907
  return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
1800
1908
  case "slideIn":
1801
- return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
1909
+ return applySlideInAnimation(
1910
+ ops,
1911
+ lines,
1912
+ progress,
1913
+ p.anim.direction ?? "left",
1914
+ p.fontSize,
1915
+ p.anim.style,
1916
+ duration
1917
+ );
1802
1918
  case "shift":
1803
1919
  return applyShiftAnimation(
1804
1920
  ops,
@@ -1826,6 +1942,9 @@ function applyAnimation(ops, lines, p) {
1826
1942
  }
1827
1943
  var isShadowFill = (op) => op.op === "FillPath" && op.isShadow === true;
1828
1944
  var isGlyphFill = (op) => op.op === "FillPath" && !op.isShadow === true;
1945
+ function isRTLLines(lines) {
1946
+ return lines.length > 0 && lines[0].isRTL === true;
1947
+ }
1829
1948
  function getTextColorFromOps(ops) {
1830
1949
  for (const op of ops) {
1831
1950
  if (op.op === "FillPath") {
@@ -1845,28 +1964,32 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
1845
1964
  const totalWords = wordSegments.length;
1846
1965
  const visibleWords = Math.floor(progress * totalWords);
1847
1966
  if (visibleWords === 0) {
1848
- return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
1967
+ return ops.filter(
1968
+ (x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
1969
+ );
1849
1970
  }
1850
1971
  let totalVisibleGlyphs = 0;
1851
1972
  for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
1852
1973
  totalVisibleGlyphs += wordSegments[i].glyphCount;
1853
1974
  }
1854
- const visibleOpsRaw = sliceGlyphOps(ops, totalVisibleGlyphs);
1975
+ const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, totalVisibleGlyphs) : sliceGlyphOps(ops, totalVisibleGlyphs);
1855
1976
  const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
1856
1977
  if (progress < 1 && totalVisibleGlyphs > 0) {
1857
- return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time);
1978
+ return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time, isRTLLines(lines));
1858
1979
  }
1859
1980
  return visibleOps;
1860
1981
  } else {
1861
1982
  const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
1862
1983
  const visibleGlyphs = Math.floor(progress * totalGlyphs);
1863
1984
  if (visibleGlyphs === 0) {
1864
- return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
1985
+ return ops.filter(
1986
+ (x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
1987
+ );
1865
1988
  }
1866
- const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
1989
+ const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, visibleGlyphs) : sliceGlyphOps(ops, visibleGlyphs);
1867
1990
  const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
1868
1991
  if (progress < 1 && visibleGlyphs > 0) {
1869
- return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time);
1992
+ return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time, isRTLLines(lines));
1870
1993
  }
1871
1994
  return visibleOps;
1872
1995
  }
@@ -1897,7 +2020,8 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
1897
2020
  acc += gcount;
1898
2021
  }
1899
2022
  if (wordIndex >= 0) {
1900
- const startF = wordIndex / Math.max(1, totalWords) * (duration / duration);
2023
+ const effectiveWordIndex = isRTLLines(lines) ? Math.max(0, totalWords - 1 - wordIndex) : wordIndex;
2024
+ const startF = effectiveWordIndex / Math.max(1, totalWords) * (duration / duration);
1901
2025
  const endF = Math.min(1, startF + 0.3);
1902
2026
  if (progress >= endF) {
1903
2027
  result.push(op);
@@ -1977,7 +2101,8 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
1977
2101
  }
1978
2102
  unitIndex = Math.max(0, wordIndex);
1979
2103
  }
1980
- const { startF, endF } = windowFor(unitIndex);
2104
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
2105
+ const { startF, endF } = windowFor(effectiveUnit);
1981
2106
  if (progress <= startF) {
1982
2107
  const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
1983
2108
  if (op.op === "FillPath") {
@@ -2058,7 +2183,8 @@ function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
2058
2183
  }
2059
2184
  unitIndex = Math.max(0, wordIndex);
2060
2185
  }
2061
- const { startF, endF } = windowFor(unitIndex);
2186
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
2187
+ const { startF, endF } = windowFor(effectiveUnit);
2062
2188
  if (progress <= startF) {
2063
2189
  const animated = { ...op };
2064
2190
  if (op.op === "FillPath") {
@@ -2145,7 +2271,8 @@ function applySlideInAnimation(ops, lines, progress, direction, fontSize, style,
2145
2271
  }
2146
2272
  unitIndex = Math.max(0, wordIndex);
2147
2273
  }
2148
- const { startF, endF } = windowFor(unitIndex);
2274
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
2275
+ const { startF, endF } = windowFor(effectiveUnit);
2149
2276
  if (progress <= startF) {
2150
2277
  const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
2151
2278
  if (op.op === "FillPath") {
@@ -2212,6 +2339,43 @@ function segmentLineBySpaces(line) {
2212
2339
  if (current.length) words.push(current);
2213
2340
  return words;
2214
2341
  }
2342
+ function sliceGlyphOpsFromEnd(ops, maxGlyphs) {
2343
+ let totalGlyphs = 0;
2344
+ for (const op of ops) {
2345
+ if (op.op === "FillPath" && !isShadowFill(op)) totalGlyphs++;
2346
+ }
2347
+ const skipCount = Math.max(0, totalGlyphs - maxGlyphs);
2348
+ const result = [];
2349
+ let glyphCount = 0;
2350
+ let foundGlyphs = false;
2351
+ for (const op of ops) {
2352
+ if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
2353
+ result.push(op);
2354
+ continue;
2355
+ }
2356
+ if (op.op === "FillPath" && !isShadowFill(op)) {
2357
+ if (glyphCount >= skipCount) {
2358
+ result.push(op);
2359
+ foundGlyphs = true;
2360
+ }
2361
+ glyphCount++;
2362
+ continue;
2363
+ }
2364
+ if (op.op === "StrokePath") {
2365
+ if (glyphCount >= skipCount) result.push(op);
2366
+ continue;
2367
+ }
2368
+ if (op.op === "FillPath" && isShadowFill(op)) {
2369
+ if (glyphCount >= skipCount) result.push(op);
2370
+ continue;
2371
+ }
2372
+ if (op.op === "DecorationLine" && foundGlyphs) {
2373
+ result.push(op);
2374
+ continue;
2375
+ }
2376
+ }
2377
+ return result;
2378
+ }
2215
2379
  function sliceGlyphOps(ops, maxGlyphs) {
2216
2380
  const result = [];
2217
2381
  let glyphCount = 0;
@@ -2244,27 +2408,30 @@ function sliceGlyphOps(ops, maxGlyphs) {
2244
2408
  }
2245
2409
  return result;
2246
2410
  }
2247
- function addTypewriterCursor(ops, glyphCount, fontSize, time) {
2411
+ function addTypewriterCursor(ops, glyphCount, fontSize, time, isRTL = false) {
2248
2412
  if (glyphCount === 0) return ops;
2249
2413
  const blinkRate = 1;
2250
2414
  const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
2251
2415
  const alwaysShowCursor = true;
2252
2416
  if (!alwaysShowCursor && !cursorVisible) return ops;
2253
2417
  let last = null;
2418
+ let first = null;
2254
2419
  let count = 0;
2255
2420
  for (const op of ops) {
2256
2421
  if (op.op === "FillPath" && !isShadowFill(op)) {
2257
2422
  count++;
2423
+ if (count === 1) first = op;
2258
2424
  if (count === glyphCount) {
2259
2425
  last = op;
2260
2426
  break;
2261
2427
  }
2262
2428
  }
2263
2429
  }
2264
- if (last && last.op === "FillPath") {
2430
+ const cursorAnchor = isRTL && first ? first : last;
2431
+ if (cursorAnchor && cursorAnchor.op === "FillPath") {
2265
2432
  const color = getTextColorFromOps(ops);
2266
- const cursorX = last.x + fontSize * 0.5;
2267
- const cursorY = last.y;
2433
+ const cursorX = isRTL && first ? first.x - fontSize * 0.15 : cursorAnchor.x + fontSize * 0.5;
2434
+ const cursorY = cursorAnchor.y;
2268
2435
  const cursorWidth = Math.max(3, fontSize / 15);
2269
2436
  const cursorOp = {
2270
2437
  op: "DecorationLine",
@@ -2624,7 +2791,7 @@ function calculateNoneState(ctx) {
2624
2791
  isActive: isWordActive(ctx)
2625
2792
  };
2626
2793
  }
2627
- function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48) {
2794
+ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
2628
2795
  const safeSpeed = config.speed > 0 ? config.speed : 1;
2629
2796
  const ctx = {
2630
2797
  wordStart,
@@ -2647,9 +2814,11 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
2647
2814
  case "fade":
2648
2815
  partialState = calculateFadeState(ctx, safeSpeed);
2649
2816
  break;
2650
- case "slide":
2651
- partialState = calculateSlideState(ctx, config.direction, safeSpeed, fontSize);
2817
+ case "slide": {
2818
+ const slideDir = mirrorAnimationDirection(config.direction, isRTL);
2819
+ partialState = calculateSlideState(ctx, slideDir, config.speed, fontSize);
2652
2820
  break;
2821
+ }
2653
2822
  case "bounce":
2654
2823
  partialState = calculateBounceState(ctx, safeSpeed, fontSize);
2655
2824
  break;
@@ -2677,7 +2846,8 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
2677
2846
  config,
2678
2847
  activeScale,
2679
2848
  word.text.length,
2680
- fontSize
2849
+ fontSize,
2850
+ word.isRTL
2681
2851
  );
2682
2852
  states.set(word.wordIndex, state);
2683
2853
  }
@@ -2821,7 +2991,7 @@ function extractActiveScale(asset) {
2821
2991
  }
2822
2992
  function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
2823
2993
  const isActive = animState.isActive;
2824
- const displayText = animState.visibleCharacters >= 0 && animState.visibleCharacters < word.text.length ? word.text.slice(0, animState.visibleCharacters) : word.text;
2994
+ const displayText = getVisibleText(word.text, animState.visibleCharacters, word.isRTL);
2825
2995
  return {
2826
2996
  op: "DrawCaptionWord",
2827
2997
  text: displayText,
@@ -3276,7 +3446,7 @@ async function createNodePainter(opts) {
3276
3446
  renderToBoth((context) => {
3277
3447
  for (const wordOp of captionWordOps) {
3278
3448
  if (!wordOp.background) continue;
3279
- const wordDisplayText = wordOp.visibleCharacters >= 0 && wordOp.visibleCharacters < wordOp.text.length ? wordOp.text.slice(0, wordOp.visibleCharacters) : wordOp.text;
3449
+ const wordDisplayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
3280
3450
  if (wordDisplayText.length === 0) continue;
3281
3451
  context.save();
3282
3452
  const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
@@ -3310,7 +3480,7 @@ async function createNodePainter(opts) {
3310
3480
  context.restore();
3311
3481
  }
3312
3482
  for (const wordOp of captionWordOps) {
3313
- const displayText = wordOp.visibleCharacters >= 0 && wordOp.visibleCharacters < wordOp.text.length ? wordOp.text.slice(0, wordOp.visibleCharacters) : wordOp.text;
3483
+ const displayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
3314
3484
  if (displayText.length === 0) continue;
3315
3485
  context.save();
3316
3486
  const tx = Math.round(wordOp.x + wordOp.transform.translateX);
@@ -4842,9 +5012,8 @@ function extractSvgDimensions(svgString) {
4842
5012
 
4843
5013
  // src/core/rich-caption-layout.ts
4844
5014
  var import_lru_cache = require("lru-cache");
4845
- var RTL_RANGES = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
4846
5015
  function isRTLText(text) {
4847
- return RTL_RANGES.test(text);
5016
+ return containsRTLCharacters(text);
4848
5017
  }
4849
5018
  var WordTimingStore = class {
4850
5019
  startTimes;
@@ -5103,6 +5272,8 @@ var CaptionLayoutEngine = class {
5103
5272
  return (config.frameHeight - totalHeight) / 2 + config.fontSize;
5104
5273
  }
5105
5274
  };
5275
+ const allWordTexts = store.words.slice(0, store.length);
5276
+ const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
5106
5277
  const calculateLineX = (lineWidth) => {
5107
5278
  switch (config.horizontalAlign) {
5108
5279
  case "left":
@@ -5120,8 +5291,11 @@ var CaptionLayoutEngine = class {
5120
5291
  const line = group.lines[lineIdx];
5121
5292
  line.x = calculateLineX(line.width);
5122
5293
  line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
5294
+ const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
5295
+ const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
5123
5296
  let xCursor = line.x;
5124
- for (const wordIdx of line.wordIndices) {
5297
+ for (const visualIdx of visualOrder) {
5298
+ const wordIdx = line.wordIndices[visualIdx];
5125
5299
  store.xPositions[wordIdx] = xCursor;
5126
5300
  store.yPositions[wordIdx] = line.y;
5127
5301
  xCursor += store.widths[wordIdx] + spaceWidth;
@@ -5131,7 +5305,8 @@ var CaptionLayoutEngine = class {
5131
5305
  return {
5132
5306
  store,
5133
5307
  groups,
5134
- shapedWords
5308
+ shapedWords,
5309
+ paragraphDirection
5135
5310
  };
5136
5311
  }
5137
5312
  getVisibleWordsAtTime(layout, timeMs) {
@@ -6602,7 +6777,7 @@ async function createTextEngine(opts = {}) {
6602
6777
  horizontal: asset.align?.horizontal ?? "center",
6603
6778
  vertical: asset.align?.vertical ?? "middle"
6604
6779
  },
6605
- background: asset.background,
6780
+ background: asset.background ? { color: asset.background.color, opacity: asset.background.opacity, borderRadius: typeof asset.background.borderRadius === "number" ? asset.background.borderRadius : Number(asset.background.borderRadius) || 0 } : void 0,
6606
6781
  border: asset.border,
6607
6782
  padding: asset.padding,
6608
6783
  glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
@@ -6657,7 +6832,8 @@ async function createTextEngine(opts = {}) {
6657
6832
  try {
6658
6833
  const hasBackground = !!asset.background?.color;
6659
6834
  const hasAnimation = !!asset.animation?.preset;
6660
- const hasBorderRadius = (asset.border?.radius ?? 0) > 0;
6835
+ const bgBorderRadius = typeof asset.background?.borderRadius === "number" ? asset.background.borderRadius : Number(asset.background?.borderRadius) || 0;
6836
+ const hasBorderRadius = (asset.border?.radius ?? 0) > 0 || bgBorderRadius > 0;
6661
6837
  const needsAlpha = !hasBackground && hasAnimation || hasBorderRadius;
6662
6838
  console.log(
6663
6839
  `\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, BorderRadius=${hasBorderRadius}, Alpha=${needsAlpha}`
@@ -6716,6 +6892,7 @@ async function createTextEngine(opts = {}) {
6716
6892
  calculateAnimationStatesForGroup,
6717
6893
  commandsToPathString,
6718
6894
  computeSimplePathBounds,
6895
+ containsRTLCharacters,
6719
6896
  createDefaultGeneratorConfig,
6720
6897
  createFrameSchedule,
6721
6898
  createNodePainter,
@@ -6723,6 +6900,8 @@ async function createTextEngine(opts = {}) {
6723
6900
  createRichCaptionRenderer,
6724
6901
  createTextEngine,
6725
6902
  createVideoEncoder,
6903
+ detectParagraphDirection,
6904
+ detectParagraphDirectionFromWords,
6726
6905
  detectPlatform,
6727
6906
  detectSubtitleFormat,
6728
6907
  extractCaptionPadding,
@@ -6734,10 +6913,12 @@ async function createTextEngine(opts = {}) {
6734
6913
  getDrawCaptionWordOps,
6735
6914
  getEncoderCapabilities,
6736
6915
  getEncoderWarning,
6916
+ getVisibleText,
6737
6917
  groupWordsByPause,
6738
6918
  isDrawCaptionWordOp,
6739
6919
  isRTLText,
6740
6920
  isWebCodecsH264Supported,
6921
+ mirrorAnimationDirection,
6741
6922
  normalizePath,
6742
6923
  normalizePathString,
6743
6924
  parseSubtitleToWords,
@@ -6745,6 +6926,7 @@ async function createTextEngine(opts = {}) {
6745
6926
  quadraticToCubic,
6746
6927
  renderSvgAssetToPng,
6747
6928
  renderSvgToPng,
6929
+ reorderWordsForLine,
6748
6930
  richCaptionAssetSchema,
6749
6931
  shapeToSvgString,
6750
6932
  svgAssetSchema,
@@ -594,6 +594,14 @@ declare class FontRegistry {
594
594
  destroy(): void;
595
595
  }
596
596
 
597
+ declare function containsRTLCharacters(text: string): boolean;
598
+ type ParagraphDirection = "ltr" | "rtl";
599
+ declare function detectParagraphDirection(text: string): ParagraphDirection;
600
+ declare function detectParagraphDirectionFromWords(words: string[]): ParagraphDirection;
601
+ declare function reorderWordsForLine(wordTexts: string[], paragraphDirection: ParagraphDirection): number[];
602
+ declare function getVisibleText(text: string, visibleCharacters: number, isRTL: boolean): string;
603
+ declare function mirrorAnimationDirection(direction: "left" | "right" | "up" | "down", isRTL: boolean): "left" | "right" | "up" | "down";
604
+
597
605
  interface WordTiming {
598
606
  text: string;
599
607
  start: number;
@@ -658,6 +666,7 @@ interface CaptionLayout {
658
666
  store: WordTimingStore;
659
667
  groups: CaptionGroup[];
660
668
  shapedWords: ShapedWord[];
669
+ paragraphDirection: ParagraphDirection;
661
670
  }
662
671
  declare function isRTLText(text: string): boolean;
663
672
  declare class WordTimingStore {
@@ -771,6 +780,7 @@ type ShapedLine = {
771
780
  glyphs: Glyph[];
772
781
  width: number;
773
782
  y: number;
783
+ isRTL?: boolean;
774
784
  };
775
785
  type DrawOp = {
776
786
  op: "BeginFrame";
@@ -1331,4 +1341,4 @@ declare function createTextEngine(opts?: {
1331
1341
  destroy(): void;
1332
1342
  }>;
1333
1343
 
1334
- export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, richCaptionAssetSchema, shapeToSvgString };
1344
+ export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };
@@ -594,6 +594,14 @@ declare class FontRegistry {
594
594
  destroy(): void;
595
595
  }
596
596
 
597
+ declare function containsRTLCharacters(text: string): boolean;
598
+ type ParagraphDirection = "ltr" | "rtl";
599
+ declare function detectParagraphDirection(text: string): ParagraphDirection;
600
+ declare function detectParagraphDirectionFromWords(words: string[]): ParagraphDirection;
601
+ declare function reorderWordsForLine(wordTexts: string[], paragraphDirection: ParagraphDirection): number[];
602
+ declare function getVisibleText(text: string, visibleCharacters: number, isRTL: boolean): string;
603
+ declare function mirrorAnimationDirection(direction: "left" | "right" | "up" | "down", isRTL: boolean): "left" | "right" | "up" | "down";
604
+
597
605
  interface WordTiming {
598
606
  text: string;
599
607
  start: number;
@@ -658,6 +666,7 @@ interface CaptionLayout {
658
666
  store: WordTimingStore;
659
667
  groups: CaptionGroup[];
660
668
  shapedWords: ShapedWord[];
669
+ paragraphDirection: ParagraphDirection;
661
670
  }
662
671
  declare function isRTLText(text: string): boolean;
663
672
  declare class WordTimingStore {
@@ -771,6 +780,7 @@ type ShapedLine = {
771
780
  glyphs: Glyph[];
772
781
  width: number;
773
782
  y: number;
783
+ isRTL?: boolean;
774
784
  };
775
785
  type DrawOp = {
776
786
  op: "BeginFrame";
@@ -1331,4 +1341,4 @@ declare function createTextEngine(opts?: {
1331
1341
  destroy(): void;
1332
1342
  }>;
1333
1343
 
1334
- export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, richCaptionAssetSchema, shapeToSvgString };
1344
+ export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };