@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.
@@ -823,10 +823,9 @@ var FontRegistry = class _FontRegistry {
823
823
  return;
824
824
  }
825
825
  try {
826
- const moduleName = "canvas";
827
826
  const canvasMod = await import(
828
827
  /* @vite-ignore */
829
- moduleName
828
+ "canvas"
830
829
  );
831
830
  const GlobalFonts = canvasMod.GlobalFonts;
832
831
  if (GlobalFonts && typeof GlobalFonts.register === "function") {
@@ -884,6 +883,96 @@ var FontRegistry = class _FontRegistry {
884
883
  }
885
884
  };
886
885
 
886
+ // src/core/bidi.ts
887
+ import bidiFactory from "bidi-js";
888
+ var bidiInstance = null;
889
+ function getBidi() {
890
+ if (!bidiInstance) {
891
+ bidiInstance = bidiFactory();
892
+ }
893
+ return bidiInstance;
894
+ }
895
+ var RTL_SCRIPT_REGEX = /[֐-׿؀-ۿ܀-ݏݐ-ݿހ-޿߀-߿ࠀ-࠿ࡀ-࡟ࡠ-࡯ࢠ-ࣿיִ-ﭏﭐ-﷿ﹰ-𐴀-𐴿𐹠-𐹿𞠀-𞣟𞤀-𞥟𞱰-𞲿𞴀-𞵏𞸀-𞻿]/u;
896
+ function containsRTLCharacters(text) {
897
+ return RTL_SCRIPT_REGEX.test(text);
898
+ }
899
+ function detectParagraphDirection(text) {
900
+ const bidi = getBidi();
901
+ const { paragraphs } = bidi.getEmbeddingLevels(text);
902
+ if (paragraphs.length === 0) {
903
+ return "ltr";
904
+ }
905
+ return paragraphs[0].level % 2 === 1 ? "rtl" : "ltr";
906
+ }
907
+ function detectParagraphDirectionFromWords(words) {
908
+ const combined = words.join(" ");
909
+ return detectParagraphDirection(combined);
910
+ }
911
+ function reorderWordsForLine(wordTexts, paragraphDirection) {
912
+ if (wordTexts.length <= 1) {
913
+ return wordTexts.map((_, i) => i);
914
+ }
915
+ const lineText = wordTexts.join(" ");
916
+ const bidi = getBidi();
917
+ const { levels } = bidi.getEmbeddingLevels(lineText, paragraphDirection);
918
+ const wordLevels = [];
919
+ let charIndex = 0;
920
+ for (let i = 0; i < wordTexts.length; i++) {
921
+ const wordLevel = levels[charIndex];
922
+ wordLevels.push(wordLevel);
923
+ charIndex += wordTexts[i].length;
924
+ if (i < wordTexts.length - 1) {
925
+ charIndex += 1;
926
+ }
927
+ }
928
+ const indices = wordTexts.map((_, i) => i);
929
+ const maxLevel = Math.max(...wordLevels);
930
+ const minOddLevel = paragraphDirection === "rtl" ? 1 : Math.min(...wordLevels.filter((l) => l % 2 === 1), maxLevel + 1);
931
+ for (let level = maxLevel; level >= minOddLevel; level--) {
932
+ let start = 0;
933
+ while (start < indices.length) {
934
+ if (wordLevels[indices[start]] >= level) {
935
+ let end = start;
936
+ while (end + 1 < indices.length && wordLevels[indices[end + 1]] >= level) {
937
+ end++;
938
+ }
939
+ if (start < end) {
940
+ let left = start;
941
+ let right = end;
942
+ while (left < right) {
943
+ const tmp = indices[left];
944
+ indices[left] = indices[right];
945
+ indices[right] = tmp;
946
+ left++;
947
+ right--;
948
+ }
949
+ }
950
+ start = end + 1;
951
+ } else {
952
+ start++;
953
+ }
954
+ }
955
+ }
956
+ return indices;
957
+ }
958
+ function getVisibleText(text, visibleCharacters, isRTL) {
959
+ if (visibleCharacters < 0 || visibleCharacters >= text.length) {
960
+ return text;
961
+ }
962
+ if (visibleCharacters === 0) {
963
+ return "";
964
+ }
965
+ return text.slice(0, visibleCharacters);
966
+ }
967
+ function mirrorAnimationDirection(direction, isRTL) {
968
+ if (!isRTL) {
969
+ return direction;
970
+ }
971
+ if (direction === "left") return "right";
972
+ if (direction === "right") return "left";
973
+ return direction;
974
+ }
975
+
887
976
  // src/core/layout.ts
888
977
  function isEmoji(char) {
889
978
  const code = char.codePointAt(0);
@@ -913,7 +1002,7 @@ var LayoutEngine = class {
913
1002
  return t;
914
1003
  }
915
1004
  }
916
- async shapeFull(text, desc) {
1005
+ async shapeFull(text, desc, direction) {
917
1006
  try {
918
1007
  const hb = await this.fonts.getHB();
919
1008
  const buffer = hb.createBuffer();
@@ -952,7 +1041,8 @@ var LayoutEngine = class {
952
1041
  let shaped;
953
1042
  try {
954
1043
  if (!emojiFallback) {
955
- shaped = await this.shapeFull(input, desc);
1044
+ const textDirection = containsRTLCharacters(input) ? "rtl" : void 0;
1045
+ shaped = await this.shapeFull(input, desc, textDirection);
956
1046
  } else {
957
1047
  const chars = Array.from(input);
958
1048
  const runs = [];
@@ -973,7 +1063,8 @@ var LayoutEngine = class {
973
1063
  shaped = [];
974
1064
  for (const run of runs) {
975
1065
  const runFont = run.isEmoji ? emojiFallback : desc;
976
- const runShaped = await this.shapeFull(run.text, runFont);
1066
+ const runDirection = containsRTLCharacters(run.text) ? "rtl" : void 0;
1067
+ const runShaped = await this.shapeFull(run.text, runFont, runDirection);
977
1068
  for (const glyph of runShaped) {
978
1069
  glyph.cl += run.startIndex;
979
1070
  glyph.fontDesc = runFont;
@@ -1074,9 +1165,11 @@ var LayoutEngine = class {
1074
1165
  y: 0
1075
1166
  });
1076
1167
  }
1168
+ const textIsRTL = containsRTLCharacters(input);
1077
1169
  const lineHeight = params.lineHeight * fontSize;
1078
1170
  for (let i = 0; i < lines.length; i++) {
1079
1171
  lines[i].y = (i + 1) * lineHeight;
1172
+ lines[i].isRTL = textIsRTL;
1080
1173
  }
1081
1174
  return lines;
1082
1175
  } catch (err) {
@@ -1115,6 +1208,14 @@ function normalizePadding(padding) {
1115
1208
  }
1116
1209
  return padding;
1117
1210
  }
1211
+ function resolveHorizontalAlign(align, isRTL) {
1212
+ if (!isRTL || align === "center") {
1213
+ return align;
1214
+ }
1215
+ if (align === "left") return "right";
1216
+ if (align === "right") return "left";
1217
+ return align;
1218
+ }
1118
1219
  async function buildDrawOps(p) {
1119
1220
  const ops = [];
1120
1221
  const padding = normalizePadding(p.padding);
@@ -1155,7 +1256,8 @@ async function buildDrawOps(p) {
1155
1256
  let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
1156
1257
  for (const line of p.lines) {
1157
1258
  let lineX;
1158
- switch (p.align.horizontal) {
1259
+ const effectiveAlign = resolveHorizontalAlign(p.align.horizontal, line.isRTL);
1260
+ switch (effectiveAlign) {
1159
1261
  case "left":
1160
1262
  lineX = 0;
1161
1263
  break;
@@ -1283,7 +1385,7 @@ async function buildDrawOps(p) {
1283
1385
  const contentWidth = p.contentRect?.width ?? p.canvas.width;
1284
1386
  const contentHeight = p.contentRect?.height ?? p.canvas.height;
1285
1387
  const borderWidth2 = p.border?.width ?? 0;
1286
- const borderRadius = p.border?.radius ?? 0;
1388
+ const borderRadius = p.border?.radius ?? p.background?.borderRadius ?? 0;
1287
1389
  const halfBorder = borderWidth2 / 2;
1288
1390
  const canvasCenterX = p.canvas.width / 2;
1289
1391
  const canvasCenterY = p.canvas.height / 2;
@@ -1401,7 +1503,15 @@ function applyAnimation(ops, lines, p) {
1401
1503
  case "fadeIn":
1402
1504
  return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
1403
1505
  case "slideIn":
1404
- return applySlideInAnimation(ops, lines, progress, p.anim.direction ?? "left", p.fontSize, p.anim.style, duration);
1506
+ return applySlideInAnimation(
1507
+ ops,
1508
+ lines,
1509
+ progress,
1510
+ p.anim.direction ?? "left",
1511
+ p.fontSize,
1512
+ p.anim.style,
1513
+ duration
1514
+ );
1405
1515
  case "shift":
1406
1516
  return applyShiftAnimation(
1407
1517
  ops,
@@ -1429,6 +1539,9 @@ function applyAnimation(ops, lines, p) {
1429
1539
  }
1430
1540
  var isShadowFill = (op) => op.op === "FillPath" && op.isShadow === true;
1431
1541
  var isGlyphFill = (op) => op.op === "FillPath" && !op.isShadow === true;
1542
+ function isRTLLines(lines) {
1543
+ return lines.length > 0 && lines[0].isRTL === true;
1544
+ }
1432
1545
  function getTextColorFromOps(ops) {
1433
1546
  for (const op of ops) {
1434
1547
  if (op.op === "FillPath") {
@@ -1448,28 +1561,32 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
1448
1561
  const totalWords = wordSegments.length;
1449
1562
  const visibleWords = Math.floor(progress * totalWords);
1450
1563
  if (visibleWords === 0) {
1451
- return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
1564
+ return ops.filter(
1565
+ (x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
1566
+ );
1452
1567
  }
1453
1568
  let totalVisibleGlyphs = 0;
1454
1569
  for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
1455
1570
  totalVisibleGlyphs += wordSegments[i].glyphCount;
1456
1571
  }
1457
- const visibleOpsRaw = sliceGlyphOps(ops, totalVisibleGlyphs);
1572
+ const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, totalVisibleGlyphs) : sliceGlyphOps(ops, totalVisibleGlyphs);
1458
1573
  const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
1459
1574
  if (progress < 1 && totalVisibleGlyphs > 0) {
1460
- return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time);
1575
+ return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time, isRTLLines(lines));
1461
1576
  }
1462
1577
  return visibleOps;
1463
1578
  } else {
1464
1579
  const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
1465
1580
  const visibleGlyphs = Math.floor(progress * totalGlyphs);
1466
1581
  if (visibleGlyphs === 0) {
1467
- return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
1582
+ return ops.filter(
1583
+ (x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
1584
+ );
1468
1585
  }
1469
- const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
1586
+ const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, visibleGlyphs) : sliceGlyphOps(ops, visibleGlyphs);
1470
1587
  const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
1471
1588
  if (progress < 1 && visibleGlyphs > 0) {
1472
- return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time);
1589
+ return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time, isRTLLines(lines));
1473
1590
  }
1474
1591
  return visibleOps;
1475
1592
  }
@@ -1500,7 +1617,8 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
1500
1617
  acc += gcount;
1501
1618
  }
1502
1619
  if (wordIndex >= 0) {
1503
- const startF = wordIndex / Math.max(1, totalWords) * (duration / duration);
1620
+ const effectiveWordIndex = isRTLLines(lines) ? Math.max(0, totalWords - 1 - wordIndex) : wordIndex;
1621
+ const startF = effectiveWordIndex / Math.max(1, totalWords) * (duration / duration);
1504
1622
  const endF = Math.min(1, startF + 0.3);
1505
1623
  if (progress >= endF) {
1506
1624
  result.push(op);
@@ -1580,7 +1698,8 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
1580
1698
  }
1581
1699
  unitIndex = Math.max(0, wordIndex);
1582
1700
  }
1583
- const { startF, endF } = windowFor(unitIndex);
1701
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
1702
+ const { startF, endF } = windowFor(effectiveUnit);
1584
1703
  if (progress <= startF) {
1585
1704
  const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
1586
1705
  if (op.op === "FillPath") {
@@ -1661,7 +1780,8 @@ function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
1661
1780
  }
1662
1781
  unitIndex = Math.max(0, wordIndex);
1663
1782
  }
1664
- const { startF, endF } = windowFor(unitIndex);
1783
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
1784
+ const { startF, endF } = windowFor(effectiveUnit);
1665
1785
  if (progress <= startF) {
1666
1786
  const animated = { ...op };
1667
1787
  if (op.op === "FillPath") {
@@ -1748,7 +1868,8 @@ function applySlideInAnimation(ops, lines, progress, direction, fontSize, style,
1748
1868
  }
1749
1869
  unitIndex = Math.max(0, wordIndex);
1750
1870
  }
1751
- const { startF, endF } = windowFor(unitIndex);
1871
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
1872
+ const { startF, endF } = windowFor(effectiveUnit);
1752
1873
  if (progress <= startF) {
1753
1874
  const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
1754
1875
  if (op.op === "FillPath") {
@@ -1815,6 +1936,43 @@ function segmentLineBySpaces(line) {
1815
1936
  if (current.length) words.push(current);
1816
1937
  return words;
1817
1938
  }
1939
+ function sliceGlyphOpsFromEnd(ops, maxGlyphs) {
1940
+ let totalGlyphs = 0;
1941
+ for (const op of ops) {
1942
+ if (op.op === "FillPath" && !isShadowFill(op)) totalGlyphs++;
1943
+ }
1944
+ const skipCount = Math.max(0, totalGlyphs - maxGlyphs);
1945
+ const result = [];
1946
+ let glyphCount = 0;
1947
+ let foundGlyphs = false;
1948
+ for (const op of ops) {
1949
+ if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
1950
+ result.push(op);
1951
+ continue;
1952
+ }
1953
+ if (op.op === "FillPath" && !isShadowFill(op)) {
1954
+ if (glyphCount >= skipCount) {
1955
+ result.push(op);
1956
+ foundGlyphs = true;
1957
+ }
1958
+ glyphCount++;
1959
+ continue;
1960
+ }
1961
+ if (op.op === "StrokePath") {
1962
+ if (glyphCount >= skipCount) result.push(op);
1963
+ continue;
1964
+ }
1965
+ if (op.op === "FillPath" && isShadowFill(op)) {
1966
+ if (glyphCount >= skipCount) result.push(op);
1967
+ continue;
1968
+ }
1969
+ if (op.op === "DecorationLine" && foundGlyphs) {
1970
+ result.push(op);
1971
+ continue;
1972
+ }
1973
+ }
1974
+ return result;
1975
+ }
1818
1976
  function sliceGlyphOps(ops, maxGlyphs) {
1819
1977
  const result = [];
1820
1978
  let glyphCount = 0;
@@ -1847,27 +2005,30 @@ function sliceGlyphOps(ops, maxGlyphs) {
1847
2005
  }
1848
2006
  return result;
1849
2007
  }
1850
- function addTypewriterCursor(ops, glyphCount, fontSize, time) {
2008
+ function addTypewriterCursor(ops, glyphCount, fontSize, time, isRTL = false) {
1851
2009
  if (glyphCount === 0) return ops;
1852
2010
  const blinkRate = 1;
1853
2011
  const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
1854
2012
  const alwaysShowCursor = true;
1855
2013
  if (!alwaysShowCursor && !cursorVisible) return ops;
1856
2014
  let last = null;
2015
+ let first = null;
1857
2016
  let count = 0;
1858
2017
  for (const op of ops) {
1859
2018
  if (op.op === "FillPath" && !isShadowFill(op)) {
1860
2019
  count++;
2020
+ if (count === 1) first = op;
1861
2021
  if (count === glyphCount) {
1862
2022
  last = op;
1863
2023
  break;
1864
2024
  }
1865
2025
  }
1866
2026
  }
1867
- if (last && last.op === "FillPath") {
2027
+ const cursorAnchor = isRTL && first ? first : last;
2028
+ if (cursorAnchor && cursorAnchor.op === "FillPath") {
1868
2029
  const color = getTextColorFromOps(ops);
1869
- const cursorX = last.x + fontSize * 0.5;
1870
- const cursorY = last.y;
2030
+ const cursorX = isRTL && first ? first.x - fontSize * 0.15 : cursorAnchor.x + fontSize * 0.5;
2031
+ const cursorY = cursorAnchor.y;
1871
2032
  const cursorWidth = Math.max(3, fontSize / 15);
1872
2033
  const cursorOp = {
1873
2034
  op: "DecorationLine",
@@ -2227,7 +2388,7 @@ function calculateNoneState(ctx) {
2227
2388
  isActive: isWordActive(ctx)
2228
2389
  };
2229
2390
  }
2230
- function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48) {
2391
+ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
2231
2392
  const safeSpeed = config.speed > 0 ? config.speed : 1;
2232
2393
  const ctx = {
2233
2394
  wordStart,
@@ -2250,9 +2411,11 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
2250
2411
  case "fade":
2251
2412
  partialState = calculateFadeState(ctx, safeSpeed);
2252
2413
  break;
2253
- case "slide":
2254
- partialState = calculateSlideState(ctx, config.direction, safeSpeed, fontSize);
2414
+ case "slide": {
2415
+ const slideDir = mirrorAnimationDirection(config.direction, isRTL);
2416
+ partialState = calculateSlideState(ctx, slideDir, config.speed, fontSize);
2255
2417
  break;
2418
+ }
2256
2419
  case "bounce":
2257
2420
  partialState = calculateBounceState(ctx, safeSpeed, fontSize);
2258
2421
  break;
@@ -2280,7 +2443,8 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
2280
2443
  config,
2281
2444
  activeScale,
2282
2445
  word.text.length,
2283
- fontSize
2446
+ fontSize,
2447
+ word.isRTL
2284
2448
  );
2285
2449
  states.set(word.wordIndex, state);
2286
2450
  }
@@ -2424,7 +2588,7 @@ function extractActiveScale(asset) {
2424
2588
  }
2425
2589
  function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
2426
2590
  const isActive = animState.isActive;
2427
- const displayText = animState.visibleCharacters >= 0 && animState.visibleCharacters < word.text.length ? word.text.slice(0, animState.visibleCharacters) : word.text;
2591
+ const displayText = getVisibleText(word.text, animState.visibleCharacters, word.isRTL);
2428
2592
  return {
2429
2593
  op: "DrawCaptionWord",
2430
2594
  text: displayText,
@@ -2879,7 +3043,7 @@ async function createNodePainter(opts) {
2879
3043
  renderToBoth((context) => {
2880
3044
  for (const wordOp of captionWordOps) {
2881
3045
  if (!wordOp.background) continue;
2882
- const wordDisplayText = wordOp.visibleCharacters >= 0 && wordOp.visibleCharacters < wordOp.text.length ? wordOp.text.slice(0, wordOp.visibleCharacters) : wordOp.text;
3046
+ const wordDisplayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
2883
3047
  if (wordDisplayText.length === 0) continue;
2884
3048
  context.save();
2885
3049
  const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
@@ -2913,7 +3077,7 @@ async function createNodePainter(opts) {
2913
3077
  context.restore();
2914
3078
  }
2915
3079
  for (const wordOp of captionWordOps) {
2916
- const displayText = wordOp.visibleCharacters >= 0 && wordOp.visibleCharacters < wordOp.text.length ? wordOp.text.slice(0, wordOp.visibleCharacters) : wordOp.text;
3080
+ const displayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
2917
3081
  if (displayText.length === 0) continue;
2918
3082
  context.save();
2919
3083
  const tx = Math.round(wordOp.x + wordOp.transform.translateX);
@@ -4445,9 +4609,8 @@ function extractSvgDimensions(svgString) {
4445
4609
 
4446
4610
  // src/core/rich-caption-layout.ts
4447
4611
  import { LRUCache } from "lru-cache";
4448
- var RTL_RANGES = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
4449
4612
  function isRTLText(text) {
4450
- return RTL_RANGES.test(text);
4613
+ return containsRTLCharacters(text);
4451
4614
  }
4452
4615
  var WordTimingStore = class {
4453
4616
  startTimes;
@@ -4706,6 +4869,8 @@ var CaptionLayoutEngine = class {
4706
4869
  return (config.frameHeight - totalHeight) / 2 + config.fontSize;
4707
4870
  }
4708
4871
  };
4872
+ const allWordTexts = store.words.slice(0, store.length);
4873
+ const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
4709
4874
  const calculateLineX = (lineWidth) => {
4710
4875
  switch (config.horizontalAlign) {
4711
4876
  case "left":
@@ -4723,8 +4888,11 @@ var CaptionLayoutEngine = class {
4723
4888
  const line = group.lines[lineIdx];
4724
4889
  line.x = calculateLineX(line.width);
4725
4890
  line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
4891
+ const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
4892
+ const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
4726
4893
  let xCursor = line.x;
4727
- for (const wordIdx of line.wordIndices) {
4894
+ for (const visualIdx of visualOrder) {
4895
+ const wordIdx = line.wordIndices[visualIdx];
4728
4896
  store.xPositions[wordIdx] = xCursor;
4729
4897
  store.yPositions[wordIdx] = line.y;
4730
4898
  xCursor += store.widths[wordIdx] + spaceWidth;
@@ -4734,7 +4902,8 @@ var CaptionLayoutEngine = class {
4734
4902
  return {
4735
4903
  store,
4736
4904
  groups,
4737
- shapedWords
4905
+ shapedWords,
4906
+ paragraphDirection
4738
4907
  };
4739
4908
  }
4740
4909
  getVisibleWordsAtTime(layout, timeMs) {
@@ -6205,7 +6374,7 @@ async function createTextEngine(opts = {}) {
6205
6374
  horizontal: asset.align?.horizontal ?? "center",
6206
6375
  vertical: asset.align?.vertical ?? "middle"
6207
6376
  },
6208
- background: asset.background,
6377
+ 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,
6209
6378
  border: asset.border,
6210
6379
  padding: asset.padding,
6211
6380
  glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
@@ -6260,7 +6429,8 @@ async function createTextEngine(opts = {}) {
6260
6429
  try {
6261
6430
  const hasBackground = !!asset.background?.color;
6262
6431
  const hasAnimation = !!asset.animation?.preset;
6263
- const hasBorderRadius = (asset.border?.radius ?? 0) > 0;
6432
+ const bgBorderRadius = typeof asset.background?.borderRadius === "number" ? asset.background.borderRadius : Number(asset.background?.borderRadius) || 0;
6433
+ const hasBorderRadius = (asset.border?.radius ?? 0) > 0 || bgBorderRadius > 0;
6264
6434
  const needsAlpha = !hasBackground && hasAnimation || hasBorderRadius;
6265
6435
  console.log(
6266
6436
  `\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, BorderRadius=${hasBorderRadius}, Alpha=${needsAlpha}`
@@ -6318,6 +6488,7 @@ export {
6318
6488
  calculateAnimationStatesForGroup,
6319
6489
  commandsToPathString,
6320
6490
  computeSimplePathBounds,
6491
+ containsRTLCharacters,
6321
6492
  createDefaultGeneratorConfig,
6322
6493
  createFrameSchedule,
6323
6494
  createNodePainter,
@@ -6325,6 +6496,8 @@ export {
6325
6496
  createRichCaptionRenderer,
6326
6497
  createTextEngine,
6327
6498
  createVideoEncoder,
6499
+ detectParagraphDirection,
6500
+ detectParagraphDirectionFromWords,
6328
6501
  detectPlatform,
6329
6502
  detectSubtitleFormat,
6330
6503
  extractCaptionPadding,
@@ -6336,10 +6509,12 @@ export {
6336
6509
  getDrawCaptionWordOps,
6337
6510
  getEncoderCapabilities,
6338
6511
  getEncoderWarning,
6512
+ getVisibleText,
6339
6513
  groupWordsByPause,
6340
6514
  isDrawCaptionWordOp,
6341
6515
  isRTLText,
6342
6516
  isWebCodecsH264Supported,
6517
+ mirrorAnimationDirection,
6343
6518
  normalizePath,
6344
6519
  normalizePathString,
6345
6520
  parseSubtitleToWords,
@@ -6347,6 +6522,7 @@ export {
6347
6522
  quadraticToCubic,
6348
6523
  renderSvgAssetToPng,
6349
6524
  renderSvgToPng,
6525
+ reorderWordsForLine,
6350
6526
  richCaptionAssetSchema,
6351
6527
  shapeToSvgString,
6352
6528
  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";
@@ -1274,4 +1284,4 @@ declare function createTextEngine(opts?: {
1274
1284
  destroy(): void;
1275
1285
  }>;
1276
1286
 
1277
- 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, MediaRecorderFallback, type MoveToCommand, 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, 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, WebCodecsEncoder, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, createDefaultGeneratorConfig, createFrameSchedule, createMediaRecorderFallback, createTextEngine, createVideoEncoder, createWebCodecsEncoder, createWebPainter, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, groupWordsByPause, initResvg, isDrawCaptionWordOp, isMediaRecorderSupported, isRTLText, isWebCodecsH264Supported, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, richCaptionAssetSchema, shapeToSvgString };
1287
+ 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, MediaRecorderFallback, type MoveToCommand, 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, 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, WebCodecsEncoder, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createMediaRecorderFallback, createTextEngine, createVideoEncoder, createWebCodecsEncoder, createWebPainter, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, initResvg, isDrawCaptionWordOp, isMediaRecorderSupported, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };