@shotstack/shotstack-canvas 2.0.13 → 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.
@@ -883,6 +883,96 @@ var FontRegistry = class _FontRegistry {
883
883
  }
884
884
  };
885
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
+
886
976
  // src/core/layout.ts
887
977
  function isEmoji(char) {
888
978
  const code = char.codePointAt(0);
@@ -912,7 +1002,7 @@ var LayoutEngine = class {
912
1002
  return t;
913
1003
  }
914
1004
  }
915
- async shapeFull(text, desc) {
1005
+ async shapeFull(text, desc, direction) {
916
1006
  try {
917
1007
  const hb = await this.fonts.getHB();
918
1008
  const buffer = hb.createBuffer();
@@ -951,7 +1041,8 @@ var LayoutEngine = class {
951
1041
  let shaped;
952
1042
  try {
953
1043
  if (!emojiFallback) {
954
- shaped = await this.shapeFull(input, desc);
1044
+ const textDirection = containsRTLCharacters(input) ? "rtl" : void 0;
1045
+ shaped = await this.shapeFull(input, desc, textDirection);
955
1046
  } else {
956
1047
  const chars = Array.from(input);
957
1048
  const runs = [];
@@ -972,7 +1063,8 @@ var LayoutEngine = class {
972
1063
  shaped = [];
973
1064
  for (const run of runs) {
974
1065
  const runFont = run.isEmoji ? emojiFallback : desc;
975
- 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);
976
1068
  for (const glyph of runShaped) {
977
1069
  glyph.cl += run.startIndex;
978
1070
  glyph.fontDesc = runFont;
@@ -1073,9 +1165,11 @@ var LayoutEngine = class {
1073
1165
  y: 0
1074
1166
  });
1075
1167
  }
1168
+ const textIsRTL = containsRTLCharacters(input);
1076
1169
  const lineHeight = params.lineHeight * fontSize;
1077
1170
  for (let i = 0; i < lines.length; i++) {
1078
1171
  lines[i].y = (i + 1) * lineHeight;
1172
+ lines[i].isRTL = textIsRTL;
1079
1173
  }
1080
1174
  return lines;
1081
1175
  } catch (err) {
@@ -1114,6 +1208,14 @@ function normalizePadding(padding) {
1114
1208
  }
1115
1209
  return padding;
1116
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
+ }
1117
1219
  async function buildDrawOps(p) {
1118
1220
  const ops = [];
1119
1221
  const padding = normalizePadding(p.padding);
@@ -1154,7 +1256,8 @@ async function buildDrawOps(p) {
1154
1256
  let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
1155
1257
  for (const line of p.lines) {
1156
1258
  let lineX;
1157
- switch (p.align.horizontal) {
1259
+ const effectiveAlign = resolveHorizontalAlign(p.align.horizontal, line.isRTL);
1260
+ switch (effectiveAlign) {
1158
1261
  case "left":
1159
1262
  lineX = 0;
1160
1263
  break;
@@ -1282,7 +1385,7 @@ async function buildDrawOps(p) {
1282
1385
  const contentWidth = p.contentRect?.width ?? p.canvas.width;
1283
1386
  const contentHeight = p.contentRect?.height ?? p.canvas.height;
1284
1387
  const borderWidth2 = p.border?.width ?? 0;
1285
- const borderRadius = p.border?.radius ?? 0;
1388
+ const borderRadius = p.border?.radius ?? p.background?.borderRadius ?? 0;
1286
1389
  const halfBorder = borderWidth2 / 2;
1287
1390
  const canvasCenterX = p.canvas.width / 2;
1288
1391
  const canvasCenterY = p.canvas.height / 2;
@@ -1400,7 +1503,15 @@ function applyAnimation(ops, lines, p) {
1400
1503
  case "fadeIn":
1401
1504
  return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
1402
1505
  case "slideIn":
1403
- 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
+ );
1404
1515
  case "shift":
1405
1516
  return applyShiftAnimation(
1406
1517
  ops,
@@ -1428,6 +1539,9 @@ function applyAnimation(ops, lines, p) {
1428
1539
  }
1429
1540
  var isShadowFill = (op) => op.op === "FillPath" && op.isShadow === true;
1430
1541
  var isGlyphFill = (op) => op.op === "FillPath" && !op.isShadow === true;
1542
+ function isRTLLines(lines) {
1543
+ return lines.length > 0 && lines[0].isRTL === true;
1544
+ }
1431
1545
  function getTextColorFromOps(ops) {
1432
1546
  for (const op of ops) {
1433
1547
  if (op.op === "FillPath") {
@@ -1447,28 +1561,32 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
1447
1561
  const totalWords = wordSegments.length;
1448
1562
  const visibleWords = Math.floor(progress * totalWords);
1449
1563
  if (visibleWords === 0) {
1450
- 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
+ );
1451
1567
  }
1452
1568
  let totalVisibleGlyphs = 0;
1453
1569
  for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
1454
1570
  totalVisibleGlyphs += wordSegments[i].glyphCount;
1455
1571
  }
1456
- const visibleOpsRaw = sliceGlyphOps(ops, totalVisibleGlyphs);
1572
+ const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, totalVisibleGlyphs) : sliceGlyphOps(ops, totalVisibleGlyphs);
1457
1573
  const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
1458
1574
  if (progress < 1 && totalVisibleGlyphs > 0) {
1459
- return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time);
1575
+ return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time, isRTLLines(lines));
1460
1576
  }
1461
1577
  return visibleOps;
1462
1578
  } else {
1463
1579
  const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
1464
1580
  const visibleGlyphs = Math.floor(progress * totalGlyphs);
1465
1581
  if (visibleGlyphs === 0) {
1466
- 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
+ );
1467
1585
  }
1468
- const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
1586
+ const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, visibleGlyphs) : sliceGlyphOps(ops, visibleGlyphs);
1469
1587
  const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
1470
1588
  if (progress < 1 && visibleGlyphs > 0) {
1471
- return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time);
1589
+ return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time, isRTLLines(lines));
1472
1590
  }
1473
1591
  return visibleOps;
1474
1592
  }
@@ -1499,7 +1617,8 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
1499
1617
  acc += gcount;
1500
1618
  }
1501
1619
  if (wordIndex >= 0) {
1502
- 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);
1503
1622
  const endF = Math.min(1, startF + 0.3);
1504
1623
  if (progress >= endF) {
1505
1624
  result.push(op);
@@ -1579,7 +1698,8 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
1579
1698
  }
1580
1699
  unitIndex = Math.max(0, wordIndex);
1581
1700
  }
1582
- const { startF, endF } = windowFor(unitIndex);
1701
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
1702
+ const { startF, endF } = windowFor(effectiveUnit);
1583
1703
  if (progress <= startF) {
1584
1704
  const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
1585
1705
  if (op.op === "FillPath") {
@@ -1660,7 +1780,8 @@ function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
1660
1780
  }
1661
1781
  unitIndex = Math.max(0, wordIndex);
1662
1782
  }
1663
- const { startF, endF } = windowFor(unitIndex);
1783
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
1784
+ const { startF, endF } = windowFor(effectiveUnit);
1664
1785
  if (progress <= startF) {
1665
1786
  const animated = { ...op };
1666
1787
  if (op.op === "FillPath") {
@@ -1747,7 +1868,8 @@ function applySlideInAnimation(ops, lines, progress, direction, fontSize, style,
1747
1868
  }
1748
1869
  unitIndex = Math.max(0, wordIndex);
1749
1870
  }
1750
- const { startF, endF } = windowFor(unitIndex);
1871
+ const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
1872
+ const { startF, endF } = windowFor(effectiveUnit);
1751
1873
  if (progress <= startF) {
1752
1874
  const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
1753
1875
  if (op.op === "FillPath") {
@@ -1814,6 +1936,43 @@ function segmentLineBySpaces(line) {
1814
1936
  if (current.length) words.push(current);
1815
1937
  return words;
1816
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
+ }
1817
1976
  function sliceGlyphOps(ops, maxGlyphs) {
1818
1977
  const result = [];
1819
1978
  let glyphCount = 0;
@@ -1846,27 +2005,30 @@ function sliceGlyphOps(ops, maxGlyphs) {
1846
2005
  }
1847
2006
  return result;
1848
2007
  }
1849
- function addTypewriterCursor(ops, glyphCount, fontSize, time) {
2008
+ function addTypewriterCursor(ops, glyphCount, fontSize, time, isRTL = false) {
1850
2009
  if (glyphCount === 0) return ops;
1851
2010
  const blinkRate = 1;
1852
2011
  const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
1853
2012
  const alwaysShowCursor = true;
1854
2013
  if (!alwaysShowCursor && !cursorVisible) return ops;
1855
2014
  let last = null;
2015
+ let first = null;
1856
2016
  let count = 0;
1857
2017
  for (const op of ops) {
1858
2018
  if (op.op === "FillPath" && !isShadowFill(op)) {
1859
2019
  count++;
2020
+ if (count === 1) first = op;
1860
2021
  if (count === glyphCount) {
1861
2022
  last = op;
1862
2023
  break;
1863
2024
  }
1864
2025
  }
1865
2026
  }
1866
- if (last && last.op === "FillPath") {
2027
+ const cursorAnchor = isRTL && first ? first : last;
2028
+ if (cursorAnchor && cursorAnchor.op === "FillPath") {
1867
2029
  const color = getTextColorFromOps(ops);
1868
- const cursorX = last.x + fontSize * 0.5;
1869
- const cursorY = last.y;
2030
+ const cursorX = isRTL && first ? first.x - fontSize * 0.15 : cursorAnchor.x + fontSize * 0.5;
2031
+ const cursorY = cursorAnchor.y;
1870
2032
  const cursorWidth = Math.max(3, fontSize / 15);
1871
2033
  const cursorOp = {
1872
2034
  op: "DecorationLine",
@@ -2226,7 +2388,7 @@ function calculateNoneState(ctx) {
2226
2388
  isActive: isWordActive(ctx)
2227
2389
  };
2228
2390
  }
2229
- 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) {
2230
2392
  const safeSpeed = config.speed > 0 ? config.speed : 1;
2231
2393
  const ctx = {
2232
2394
  wordStart,
@@ -2249,9 +2411,11 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
2249
2411
  case "fade":
2250
2412
  partialState = calculateFadeState(ctx, safeSpeed);
2251
2413
  break;
2252
- case "slide":
2253
- 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);
2254
2417
  break;
2418
+ }
2255
2419
  case "bounce":
2256
2420
  partialState = calculateBounceState(ctx, safeSpeed, fontSize);
2257
2421
  break;
@@ -2279,7 +2443,8 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
2279
2443
  config,
2280
2444
  activeScale,
2281
2445
  word.text.length,
2282
- fontSize
2446
+ fontSize,
2447
+ word.isRTL
2283
2448
  );
2284
2449
  states.set(word.wordIndex, state);
2285
2450
  }
@@ -2423,7 +2588,7 @@ function extractActiveScale(asset) {
2423
2588
  }
2424
2589
  function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
2425
2590
  const isActive = animState.isActive;
2426
- 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);
2427
2592
  return {
2428
2593
  op: "DrawCaptionWord",
2429
2594
  text: displayText,
@@ -2878,7 +3043,7 @@ async function createNodePainter(opts) {
2878
3043
  renderToBoth((context) => {
2879
3044
  for (const wordOp of captionWordOps) {
2880
3045
  if (!wordOp.background) continue;
2881
- 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);
2882
3047
  if (wordDisplayText.length === 0) continue;
2883
3048
  context.save();
2884
3049
  const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
@@ -2912,7 +3077,7 @@ async function createNodePainter(opts) {
2912
3077
  context.restore();
2913
3078
  }
2914
3079
  for (const wordOp of captionWordOps) {
2915
- 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);
2916
3081
  if (displayText.length === 0) continue;
2917
3082
  context.save();
2918
3083
  const tx = Math.round(wordOp.x + wordOp.transform.translateX);
@@ -4444,9 +4609,8 @@ function extractSvgDimensions(svgString) {
4444
4609
 
4445
4610
  // src/core/rich-caption-layout.ts
4446
4611
  import { LRUCache } from "lru-cache";
4447
- var RTL_RANGES = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
4448
4612
  function isRTLText(text) {
4449
- return RTL_RANGES.test(text);
4613
+ return containsRTLCharacters(text);
4450
4614
  }
4451
4615
  var WordTimingStore = class {
4452
4616
  startTimes;
@@ -4705,6 +4869,8 @@ var CaptionLayoutEngine = class {
4705
4869
  return (config.frameHeight - totalHeight) / 2 + config.fontSize;
4706
4870
  }
4707
4871
  };
4872
+ const allWordTexts = store.words.slice(0, store.length);
4873
+ const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
4708
4874
  const calculateLineX = (lineWidth) => {
4709
4875
  switch (config.horizontalAlign) {
4710
4876
  case "left":
@@ -4722,8 +4888,11 @@ var CaptionLayoutEngine = class {
4722
4888
  const line = group.lines[lineIdx];
4723
4889
  line.x = calculateLineX(line.width);
4724
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);
4725
4893
  let xCursor = line.x;
4726
- for (const wordIdx of line.wordIndices) {
4894
+ for (const visualIdx of visualOrder) {
4895
+ const wordIdx = line.wordIndices[visualIdx];
4727
4896
  store.xPositions[wordIdx] = xCursor;
4728
4897
  store.yPositions[wordIdx] = line.y;
4729
4898
  xCursor += store.widths[wordIdx] + spaceWidth;
@@ -4733,7 +4902,8 @@ var CaptionLayoutEngine = class {
4733
4902
  return {
4734
4903
  store,
4735
4904
  groups,
4736
- shapedWords
4905
+ shapedWords,
4906
+ paragraphDirection
4737
4907
  };
4738
4908
  }
4739
4909
  getVisibleWordsAtTime(layout, timeMs) {
@@ -6204,7 +6374,7 @@ async function createTextEngine(opts = {}) {
6204
6374
  horizontal: asset.align?.horizontal ?? "center",
6205
6375
  vertical: asset.align?.vertical ?? "middle"
6206
6376
  },
6207
- 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,
6208
6378
  border: asset.border,
6209
6379
  padding: asset.padding,
6210
6380
  glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
@@ -6259,7 +6429,8 @@ async function createTextEngine(opts = {}) {
6259
6429
  try {
6260
6430
  const hasBackground = !!asset.background?.color;
6261
6431
  const hasAnimation = !!asset.animation?.preset;
6262
- 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;
6263
6434
  const needsAlpha = !hasBackground && hasAnimation || hasBorderRadius;
6264
6435
  console.log(
6265
6436
  `\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, BorderRadius=${hasBorderRadius}, Alpha=${needsAlpha}`
@@ -6317,6 +6488,7 @@ export {
6317
6488
  calculateAnimationStatesForGroup,
6318
6489
  commandsToPathString,
6319
6490
  computeSimplePathBounds,
6491
+ containsRTLCharacters,
6320
6492
  createDefaultGeneratorConfig,
6321
6493
  createFrameSchedule,
6322
6494
  createNodePainter,
@@ -6324,6 +6496,8 @@ export {
6324
6496
  createRichCaptionRenderer,
6325
6497
  createTextEngine,
6326
6498
  createVideoEncoder,
6499
+ detectParagraphDirection,
6500
+ detectParagraphDirectionFromWords,
6327
6501
  detectPlatform,
6328
6502
  detectSubtitleFormat,
6329
6503
  extractCaptionPadding,
@@ -6335,10 +6509,12 @@ export {
6335
6509
  getDrawCaptionWordOps,
6336
6510
  getEncoderCapabilities,
6337
6511
  getEncoderWarning,
6512
+ getVisibleText,
6338
6513
  groupWordsByPause,
6339
6514
  isDrawCaptionWordOp,
6340
6515
  isRTLText,
6341
6516
  isWebCodecsH264Supported,
6517
+ mirrorAnimationDirection,
6342
6518
  normalizePath,
6343
6519
  normalizePathString,
6344
6520
  parseSubtitleToWords,
@@ -6346,6 +6522,7 @@ export {
6346
6522
  quadraticToCubic,
6347
6523
  renderSvgAssetToPng,
6348
6524
  renderSvgToPng,
6525
+ reorderWordsForLine,
6349
6526
  richCaptionAssetSchema,
6350
6527
  shapeToSvgString,
6351
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 };