@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.
- package/dist/entry.node.cjs +216 -33
- package/dist/entry.node.d.cts +11 -1
- package/dist/entry.node.d.ts +11 -1
- package/dist/entry.node.js +210 -33
- package/dist/entry.web.d.ts +11 -1
- package/dist/entry.web.js +978 -33
- package/package.json +66 -65
package/dist/entry.node.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2027
|
+
const cursorAnchor = isRTL && first ? first : last;
|
|
2028
|
+
if (cursorAnchor && cursorAnchor.op === "FillPath") {
|
|
1867
2029
|
const color = getTextColorFromOps(ops);
|
|
1868
|
-
const cursorX =
|
|
1869
|
-
const cursorY =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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,
|
package/dist/entry.web.d.ts
CHANGED
|
@@ -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 };
|