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