@shotstack/shotstack-canvas 2.0.13 → 2.0.15
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 +310 -70
- package/dist/entry.node.d.cts +12 -1
- package/dist/entry.node.d.ts +12 -1
- package/dist/entry.node.js +304 -70
- package/dist/entry.web.d.ts +12 -1
- package/dist/entry.web.js +1071 -70
- package/package.json +66 -65
package/dist/entry.node.js
CHANGED
|
@@ -883,18 +883,136 @@ 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
|
|
977
|
+
import bidiFactory2 from "bidi-js";
|
|
887
978
|
function isEmoji(char) {
|
|
888
979
|
const code = char.codePointAt(0);
|
|
889
980
|
if (!code) return false;
|
|
890
|
-
return code >= 127744 && code <= 129535 ||
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
981
|
+
return code >= 127744 && code <= 129535 || code >= 9728 && code <= 9983 || code >= 9984 && code <= 10175 || code >= 65024 && code <= 65039 || code >= 128512 && code <= 128591 || code >= 128640 && code <= 128767 || code >= 129280 && code <= 129535 || code >= 129648 && code <= 129791;
|
|
982
|
+
}
|
|
983
|
+
function reorderRunsVisually(runs) {
|
|
984
|
+
if (runs.length <= 1) return runs;
|
|
985
|
+
const result = runs.slice();
|
|
986
|
+
const runLevels = result.map((r) => r.level);
|
|
987
|
+
const maxLevel = Math.max(...runLevels);
|
|
988
|
+
const minLevel = Math.min(...runLevels);
|
|
989
|
+
const minOddLevel = minLevel % 2 === 1 ? minLevel : minLevel + 1;
|
|
990
|
+
for (let level = maxLevel; level >= minOddLevel; level--) {
|
|
991
|
+
let start = 0;
|
|
992
|
+
while (start < result.length) {
|
|
993
|
+
if (runLevels[result.indexOf(result[start])] >= level) {
|
|
994
|
+
let end = start;
|
|
995
|
+
while (end + 1 < result.length && result[end + 1].level >= level) {
|
|
996
|
+
end++;
|
|
997
|
+
}
|
|
998
|
+
if (start < end) {
|
|
999
|
+
let left = start;
|
|
1000
|
+
let right = end;
|
|
1001
|
+
while (left < right) {
|
|
1002
|
+
const tmp = result[left];
|
|
1003
|
+
result[left] = result[right];
|
|
1004
|
+
result[right] = tmp;
|
|
1005
|
+
left++;
|
|
1006
|
+
right--;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
start = end + 1;
|
|
1010
|
+
} else {
|
|
1011
|
+
start++;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return result;
|
|
898
1016
|
}
|
|
899
1017
|
var LayoutEngine = class {
|
|
900
1018
|
constructor(fonts) {
|
|
@@ -912,13 +1030,16 @@ var LayoutEngine = class {
|
|
|
912
1030
|
return t;
|
|
913
1031
|
}
|
|
914
1032
|
}
|
|
915
|
-
async shapeFull(text, desc) {
|
|
1033
|
+
async shapeFull(text, desc, direction) {
|
|
916
1034
|
try {
|
|
917
1035
|
const hb = await this.fonts.getHB();
|
|
918
1036
|
const buffer = hb.createBuffer();
|
|
919
1037
|
try {
|
|
920
1038
|
buffer.addText(text);
|
|
921
1039
|
buffer.guessSegmentProperties();
|
|
1040
|
+
if (direction) {
|
|
1041
|
+
buffer.setDirection(direction);
|
|
1042
|
+
}
|
|
922
1043
|
const font = await this.fonts.getFont(desc);
|
|
923
1044
|
const face = await this.fonts.getFace(desc);
|
|
924
1045
|
const upem = face?.upem || 1e3;
|
|
@@ -941,6 +1062,64 @@ var LayoutEngine = class {
|
|
|
941
1062
|
);
|
|
942
1063
|
}
|
|
943
1064
|
}
|
|
1065
|
+
splitIntoBidiRuns(input, levels, desc, emojiFallback) {
|
|
1066
|
+
const runs = [];
|
|
1067
|
+
if (input.length === 0) return runs;
|
|
1068
|
+
let runStart = 0;
|
|
1069
|
+
let runLevel = levels[0];
|
|
1070
|
+
let runIsEmoji = emojiFallback ? isEmoji(input[0]) : false;
|
|
1071
|
+
for (let i = 1; i <= input.length; i++) {
|
|
1072
|
+
const atEnd = i === input.length;
|
|
1073
|
+
const charLevel = atEnd ? -1 : levels[i];
|
|
1074
|
+
const charIsEmoji = !atEnd && emojiFallback ? isEmoji(String.fromCodePoint(input.codePointAt(i) ?? 0)) : false;
|
|
1075
|
+
if (atEnd || charLevel !== runLevel || emojiFallback && charIsEmoji !== runIsEmoji) {
|
|
1076
|
+
const text = input.slice(runStart, i);
|
|
1077
|
+
const font = runIsEmoji && emojiFallback ? emojiFallback : desc;
|
|
1078
|
+
runs.push({ text, startIndex: runStart, endIndex: i, level: runLevel, font });
|
|
1079
|
+
if (!atEnd) {
|
|
1080
|
+
runStart = i;
|
|
1081
|
+
runLevel = charLevel;
|
|
1082
|
+
runIsEmoji = charIsEmoji;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return runs;
|
|
1087
|
+
}
|
|
1088
|
+
async shapeWithBidi(input, desc, emojiFallback) {
|
|
1089
|
+
const hasRTL = containsRTLCharacters(input);
|
|
1090
|
+
const hasLTR = /[a-zA-Z0-9]/.test(input);
|
|
1091
|
+
const hasMixedDirection = hasRTL && hasLTR;
|
|
1092
|
+
if (!hasMixedDirection && !emojiFallback) {
|
|
1093
|
+
const textDirection = hasRTL ? "rtl" : void 0;
|
|
1094
|
+
return this.shapeFull(input, desc, textDirection);
|
|
1095
|
+
}
|
|
1096
|
+
const bidi = bidiFactory2();
|
|
1097
|
+
const { levels } = bidi.getEmbeddingLevels(input);
|
|
1098
|
+
const bidiRuns = this.splitIntoBidiRuns(input, levels, desc, emojiFallback);
|
|
1099
|
+
const shapedRuns = [];
|
|
1100
|
+
for (const run of bidiRuns) {
|
|
1101
|
+
const runDirection = run.level % 2 === 1 ? "rtl" : "ltr";
|
|
1102
|
+
const runShaped = await this.shapeFull(run.text, run.font, runDirection);
|
|
1103
|
+
for (const glyph of runShaped) {
|
|
1104
|
+
glyph.cl += run.startIndex;
|
|
1105
|
+
if (run.font !== desc) {
|
|
1106
|
+
glyph.fontDesc = run.font;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
shapedRuns.push({
|
|
1110
|
+
glyphs: runShaped,
|
|
1111
|
+
startIndex: run.startIndex,
|
|
1112
|
+
endIndex: run.endIndex,
|
|
1113
|
+
level: run.level
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
const visualRuns = reorderRunsVisually(shapedRuns);
|
|
1117
|
+
const visualGlyphs = [];
|
|
1118
|
+
for (const run of visualRuns) {
|
|
1119
|
+
visualGlyphs.push(...run.glyphs);
|
|
1120
|
+
}
|
|
1121
|
+
return visualGlyphs;
|
|
1122
|
+
}
|
|
944
1123
|
async layout(params) {
|
|
945
1124
|
try {
|
|
946
1125
|
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
@@ -950,36 +1129,7 @@ var LayoutEngine = class {
|
|
|
950
1129
|
}
|
|
951
1130
|
let shaped;
|
|
952
1131
|
try {
|
|
953
|
-
|
|
954
|
-
shaped = await this.shapeFull(input, desc);
|
|
955
|
-
} else {
|
|
956
|
-
const chars = Array.from(input);
|
|
957
|
-
const runs = [];
|
|
958
|
-
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
959
|
-
for (let i = 0; i < chars.length; i++) {
|
|
960
|
-
const char = chars[i];
|
|
961
|
-
const charIsEmoji = isEmoji(char);
|
|
962
|
-
if (i === 0) {
|
|
963
|
-
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
964
|
-
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
965
|
-
currentRun.text += char;
|
|
966
|
-
} else {
|
|
967
|
-
runs.push(currentRun);
|
|
968
|
-
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
if (currentRun.text) runs.push(currentRun);
|
|
972
|
-
shaped = [];
|
|
973
|
-
for (const run of runs) {
|
|
974
|
-
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
975
|
-
const runShaped = await this.shapeFull(run.text, runFont);
|
|
976
|
-
for (const glyph of runShaped) {
|
|
977
|
-
glyph.cl += run.startIndex;
|
|
978
|
-
glyph.fontDesc = runFont;
|
|
979
|
-
}
|
|
980
|
-
shaped.push(...runShaped);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
1132
|
+
shaped = await this.shapeWithBidi(input, desc, emojiFallback);
|
|
983
1133
|
} catch (err) {
|
|
984
1134
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
985
1135
|
}
|
|
@@ -1010,7 +1160,6 @@ var LayoutEngine = class {
|
|
|
1010
1160
|
cluster: g.cl,
|
|
1011
1161
|
char,
|
|
1012
1162
|
fontDesc: g.fontDesc
|
|
1013
|
-
// Preserve font descriptor
|
|
1014
1163
|
};
|
|
1015
1164
|
});
|
|
1016
1165
|
const lines = [];
|
|
@@ -1073,9 +1222,11 @@ var LayoutEngine = class {
|
|
|
1073
1222
|
y: 0
|
|
1074
1223
|
});
|
|
1075
1224
|
}
|
|
1225
|
+
const textIsRTL = containsRTLCharacters(input);
|
|
1076
1226
|
const lineHeight = params.lineHeight * fontSize;
|
|
1077
1227
|
for (let i = 0; i < lines.length; i++) {
|
|
1078
1228
|
lines[i].y = (i + 1) * lineHeight;
|
|
1229
|
+
lines[i].isRTL = textIsRTL;
|
|
1079
1230
|
}
|
|
1080
1231
|
return lines;
|
|
1081
1232
|
} catch (err) {
|
|
@@ -1114,6 +1265,14 @@ function normalizePadding(padding) {
|
|
|
1114
1265
|
}
|
|
1115
1266
|
return padding;
|
|
1116
1267
|
}
|
|
1268
|
+
function resolveHorizontalAlign(align, isRTL) {
|
|
1269
|
+
if (!isRTL || align === "center") {
|
|
1270
|
+
return align;
|
|
1271
|
+
}
|
|
1272
|
+
if (align === "left") return "right";
|
|
1273
|
+
if (align === "right") return "left";
|
|
1274
|
+
return align;
|
|
1275
|
+
}
|
|
1117
1276
|
async function buildDrawOps(p) {
|
|
1118
1277
|
const ops = [];
|
|
1119
1278
|
const padding = normalizePadding(p.padding);
|
|
@@ -1154,7 +1313,8 @@ async function buildDrawOps(p) {
|
|
|
1154
1313
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
1155
1314
|
for (const line of p.lines) {
|
|
1156
1315
|
let lineX;
|
|
1157
|
-
|
|
1316
|
+
const effectiveAlign = resolveHorizontalAlign(p.align.horizontal, line.isRTL);
|
|
1317
|
+
switch (effectiveAlign) {
|
|
1158
1318
|
case "left":
|
|
1159
1319
|
lineX = 0;
|
|
1160
1320
|
break;
|
|
@@ -1282,7 +1442,7 @@ async function buildDrawOps(p) {
|
|
|
1282
1442
|
const contentWidth = p.contentRect?.width ?? p.canvas.width;
|
|
1283
1443
|
const contentHeight = p.contentRect?.height ?? p.canvas.height;
|
|
1284
1444
|
const borderWidth2 = p.border?.width ?? 0;
|
|
1285
|
-
const borderRadius = p.border?.radius ?? 0;
|
|
1445
|
+
const borderRadius = p.border?.radius ?? p.background?.borderRadius ?? 0;
|
|
1286
1446
|
const halfBorder = borderWidth2 / 2;
|
|
1287
1447
|
const canvasCenterX = p.canvas.width / 2;
|
|
1288
1448
|
const canvasCenterY = p.canvas.height / 2;
|
|
@@ -1400,7 +1560,15 @@ function applyAnimation(ops, lines, p) {
|
|
|
1400
1560
|
case "fadeIn":
|
|
1401
1561
|
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
1402
1562
|
case "slideIn":
|
|
1403
|
-
return applySlideInAnimation(
|
|
1563
|
+
return applySlideInAnimation(
|
|
1564
|
+
ops,
|
|
1565
|
+
lines,
|
|
1566
|
+
progress,
|
|
1567
|
+
p.anim.direction ?? "left",
|
|
1568
|
+
p.fontSize,
|
|
1569
|
+
p.anim.style,
|
|
1570
|
+
duration
|
|
1571
|
+
);
|
|
1404
1572
|
case "shift":
|
|
1405
1573
|
return applyShiftAnimation(
|
|
1406
1574
|
ops,
|
|
@@ -1428,6 +1596,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
1428
1596
|
}
|
|
1429
1597
|
var isShadowFill = (op) => op.op === "FillPath" && op.isShadow === true;
|
|
1430
1598
|
var isGlyphFill = (op) => op.op === "FillPath" && !op.isShadow === true;
|
|
1599
|
+
function isRTLLines(lines) {
|
|
1600
|
+
return lines.length > 0 && lines[0].isRTL === true;
|
|
1601
|
+
}
|
|
1431
1602
|
function getTextColorFromOps(ops) {
|
|
1432
1603
|
for (const op of ops) {
|
|
1433
1604
|
if (op.op === "FillPath") {
|
|
@@ -1447,28 +1618,32 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1447
1618
|
const totalWords = wordSegments.length;
|
|
1448
1619
|
const visibleWords = Math.floor(progress * totalWords);
|
|
1449
1620
|
if (visibleWords === 0) {
|
|
1450
|
-
return ops.filter(
|
|
1621
|
+
return ops.filter(
|
|
1622
|
+
(x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
|
|
1623
|
+
);
|
|
1451
1624
|
}
|
|
1452
1625
|
let totalVisibleGlyphs = 0;
|
|
1453
1626
|
for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
|
|
1454
1627
|
totalVisibleGlyphs += wordSegments[i].glyphCount;
|
|
1455
1628
|
}
|
|
1456
|
-
const visibleOpsRaw = sliceGlyphOps(ops, totalVisibleGlyphs);
|
|
1629
|
+
const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, totalVisibleGlyphs) : sliceGlyphOps(ops, totalVisibleGlyphs);
|
|
1457
1630
|
const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
1458
1631
|
if (progress < 1 && totalVisibleGlyphs > 0) {
|
|
1459
|
-
return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time);
|
|
1632
|
+
return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time, isRTLLines(lines));
|
|
1460
1633
|
}
|
|
1461
1634
|
return visibleOps;
|
|
1462
1635
|
} else {
|
|
1463
1636
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1464
1637
|
const visibleGlyphs = Math.floor(progress * totalGlyphs);
|
|
1465
1638
|
if (visibleGlyphs === 0) {
|
|
1466
|
-
return ops.filter(
|
|
1639
|
+
return ops.filter(
|
|
1640
|
+
(x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
|
|
1641
|
+
);
|
|
1467
1642
|
}
|
|
1468
|
-
const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
|
|
1643
|
+
const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, visibleGlyphs) : sliceGlyphOps(ops, visibleGlyphs);
|
|
1469
1644
|
const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
1470
1645
|
if (progress < 1 && visibleGlyphs > 0) {
|
|
1471
|
-
return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time);
|
|
1646
|
+
return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time, isRTLLines(lines));
|
|
1472
1647
|
}
|
|
1473
1648
|
return visibleOps;
|
|
1474
1649
|
}
|
|
@@ -1499,7 +1674,8 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
|
|
|
1499
1674
|
acc += gcount;
|
|
1500
1675
|
}
|
|
1501
1676
|
if (wordIndex >= 0) {
|
|
1502
|
-
const
|
|
1677
|
+
const effectiveWordIndex = isRTLLines(lines) ? Math.max(0, totalWords - 1 - wordIndex) : wordIndex;
|
|
1678
|
+
const startF = effectiveWordIndex / Math.max(1, totalWords) * (duration / duration);
|
|
1503
1679
|
const endF = Math.min(1, startF + 0.3);
|
|
1504
1680
|
if (progress >= endF) {
|
|
1505
1681
|
result.push(op);
|
|
@@ -1579,7 +1755,8 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1579
1755
|
}
|
|
1580
1756
|
unitIndex = Math.max(0, wordIndex);
|
|
1581
1757
|
}
|
|
1582
|
-
const
|
|
1758
|
+
const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
|
|
1759
|
+
const { startF, endF } = windowFor(effectiveUnit);
|
|
1583
1760
|
if (progress <= startF) {
|
|
1584
1761
|
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1585
1762
|
if (op.op === "FillPath") {
|
|
@@ -1660,7 +1837,8 @@ function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
|
1660
1837
|
}
|
|
1661
1838
|
unitIndex = Math.max(0, wordIndex);
|
|
1662
1839
|
}
|
|
1663
|
-
const
|
|
1840
|
+
const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
|
|
1841
|
+
const { startF, endF } = windowFor(effectiveUnit);
|
|
1664
1842
|
if (progress <= startF) {
|
|
1665
1843
|
const animated = { ...op };
|
|
1666
1844
|
if (op.op === "FillPath") {
|
|
@@ -1747,7 +1925,8 @@ function applySlideInAnimation(ops, lines, progress, direction, fontSize, style,
|
|
|
1747
1925
|
}
|
|
1748
1926
|
unitIndex = Math.max(0, wordIndex);
|
|
1749
1927
|
}
|
|
1750
|
-
const
|
|
1928
|
+
const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
|
|
1929
|
+
const { startF, endF } = windowFor(effectiveUnit);
|
|
1751
1930
|
if (progress <= startF) {
|
|
1752
1931
|
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1753
1932
|
if (op.op === "FillPath") {
|
|
@@ -1814,6 +1993,43 @@ function segmentLineBySpaces(line) {
|
|
|
1814
1993
|
if (current.length) words.push(current);
|
|
1815
1994
|
return words;
|
|
1816
1995
|
}
|
|
1996
|
+
function sliceGlyphOpsFromEnd(ops, maxGlyphs) {
|
|
1997
|
+
let totalGlyphs = 0;
|
|
1998
|
+
for (const op of ops) {
|
|
1999
|
+
if (op.op === "FillPath" && !isShadowFill(op)) totalGlyphs++;
|
|
2000
|
+
}
|
|
2001
|
+
const skipCount = Math.max(0, totalGlyphs - maxGlyphs);
|
|
2002
|
+
const result = [];
|
|
2003
|
+
let glyphCount = 0;
|
|
2004
|
+
let foundGlyphs = false;
|
|
2005
|
+
for (const op of ops) {
|
|
2006
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
2007
|
+
result.push(op);
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
if (op.op === "FillPath" && !isShadowFill(op)) {
|
|
2011
|
+
if (glyphCount >= skipCount) {
|
|
2012
|
+
result.push(op);
|
|
2013
|
+
foundGlyphs = true;
|
|
2014
|
+
}
|
|
2015
|
+
glyphCount++;
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
if (op.op === "StrokePath") {
|
|
2019
|
+
if (glyphCount >= skipCount) result.push(op);
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
if (op.op === "FillPath" && isShadowFill(op)) {
|
|
2023
|
+
if (glyphCount >= skipCount) result.push(op);
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
if (op.op === "DecorationLine" && foundGlyphs) {
|
|
2027
|
+
result.push(op);
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return result;
|
|
2032
|
+
}
|
|
1817
2033
|
function sliceGlyphOps(ops, maxGlyphs) {
|
|
1818
2034
|
const result = [];
|
|
1819
2035
|
let glyphCount = 0;
|
|
@@ -1846,27 +2062,30 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
1846
2062
|
}
|
|
1847
2063
|
return result;
|
|
1848
2064
|
}
|
|
1849
|
-
function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
2065
|
+
function addTypewriterCursor(ops, glyphCount, fontSize, time, isRTL = false) {
|
|
1850
2066
|
if (glyphCount === 0) return ops;
|
|
1851
2067
|
const blinkRate = 1;
|
|
1852
2068
|
const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
|
|
1853
2069
|
const alwaysShowCursor = true;
|
|
1854
2070
|
if (!alwaysShowCursor && !cursorVisible) return ops;
|
|
1855
2071
|
let last = null;
|
|
2072
|
+
let first = null;
|
|
1856
2073
|
let count = 0;
|
|
1857
2074
|
for (const op of ops) {
|
|
1858
2075
|
if (op.op === "FillPath" && !isShadowFill(op)) {
|
|
1859
2076
|
count++;
|
|
2077
|
+
if (count === 1) first = op;
|
|
1860
2078
|
if (count === glyphCount) {
|
|
1861
2079
|
last = op;
|
|
1862
2080
|
break;
|
|
1863
2081
|
}
|
|
1864
2082
|
}
|
|
1865
2083
|
}
|
|
1866
|
-
|
|
2084
|
+
const cursorAnchor = isRTL && first ? first : last;
|
|
2085
|
+
if (cursorAnchor && cursorAnchor.op === "FillPath") {
|
|
1867
2086
|
const color = getTextColorFromOps(ops);
|
|
1868
|
-
const cursorX =
|
|
1869
|
-
const cursorY =
|
|
2087
|
+
const cursorX = isRTL && first ? first.x - fontSize * 0.15 : cursorAnchor.x + fontSize * 0.5;
|
|
2088
|
+
const cursorY = cursorAnchor.y;
|
|
1870
2089
|
const cursorWidth = Math.max(3, fontSize / 15);
|
|
1871
2090
|
const cursorOp = {
|
|
1872
2091
|
op: "DecorationLine",
|
|
@@ -2226,7 +2445,7 @@ function calculateNoneState(ctx) {
|
|
|
2226
2445
|
isActive: isWordActive(ctx)
|
|
2227
2446
|
};
|
|
2228
2447
|
}
|
|
2229
|
-
function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48) {
|
|
2448
|
+
function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
|
|
2230
2449
|
const safeSpeed = config.speed > 0 ? config.speed : 1;
|
|
2231
2450
|
const ctx = {
|
|
2232
2451
|
wordStart,
|
|
@@ -2249,9 +2468,11 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
|
|
|
2249
2468
|
case "fade":
|
|
2250
2469
|
partialState = calculateFadeState(ctx, safeSpeed);
|
|
2251
2470
|
break;
|
|
2252
|
-
case "slide":
|
|
2253
|
-
|
|
2471
|
+
case "slide": {
|
|
2472
|
+
const slideDir = mirrorAnimationDirection(config.direction, isRTL);
|
|
2473
|
+
partialState = calculateSlideState(ctx, slideDir, config.speed, fontSize);
|
|
2254
2474
|
break;
|
|
2475
|
+
}
|
|
2255
2476
|
case "bounce":
|
|
2256
2477
|
partialState = calculateBounceState(ctx, safeSpeed, fontSize);
|
|
2257
2478
|
break;
|
|
@@ -2279,7 +2500,8 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
|
|
|
2279
2500
|
config,
|
|
2280
2501
|
activeScale,
|
|
2281
2502
|
word.text.length,
|
|
2282
|
-
fontSize
|
|
2503
|
+
fontSize,
|
|
2504
|
+
word.isRTL
|
|
2283
2505
|
);
|
|
2284
2506
|
states.set(word.wordIndex, state);
|
|
2285
2507
|
}
|
|
@@ -2423,7 +2645,7 @@ function extractActiveScale(asset) {
|
|
|
2423
2645
|
}
|
|
2424
2646
|
function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
|
|
2425
2647
|
const isActive = animState.isActive;
|
|
2426
|
-
const displayText =
|
|
2648
|
+
const displayText = getVisibleText(word.text, animState.visibleCharacters, word.isRTL);
|
|
2427
2649
|
return {
|
|
2428
2650
|
op: "DrawCaptionWord",
|
|
2429
2651
|
text: displayText,
|
|
@@ -2878,7 +3100,7 @@ async function createNodePainter(opts) {
|
|
|
2878
3100
|
renderToBoth((context) => {
|
|
2879
3101
|
for (const wordOp of captionWordOps) {
|
|
2880
3102
|
if (!wordOp.background) continue;
|
|
2881
|
-
const wordDisplayText = wordOp.
|
|
3103
|
+
const wordDisplayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
|
|
2882
3104
|
if (wordDisplayText.length === 0) continue;
|
|
2883
3105
|
context.save();
|
|
2884
3106
|
const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
|
|
@@ -2912,7 +3134,7 @@ async function createNodePainter(opts) {
|
|
|
2912
3134
|
context.restore();
|
|
2913
3135
|
}
|
|
2914
3136
|
for (const wordOp of captionWordOps) {
|
|
2915
|
-
const displayText = wordOp.
|
|
3137
|
+
const displayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
|
|
2916
3138
|
if (displayText.length === 0) continue;
|
|
2917
3139
|
context.save();
|
|
2918
3140
|
const tx = Math.round(wordOp.x + wordOp.transform.translateX);
|
|
@@ -4444,9 +4666,8 @@ function extractSvgDimensions(svgString) {
|
|
|
4444
4666
|
|
|
4445
4667
|
// src/core/rich-caption-layout.ts
|
|
4446
4668
|
import { LRUCache } from "lru-cache";
|
|
4447
|
-
var RTL_RANGES = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
|
4448
4669
|
function isRTLText(text) {
|
|
4449
|
-
return
|
|
4670
|
+
return containsRTLCharacters(text);
|
|
4450
4671
|
}
|
|
4451
4672
|
var WordTimingStore = class {
|
|
4452
4673
|
startTimes;
|
|
@@ -4705,6 +4926,8 @@ var CaptionLayoutEngine = class {
|
|
|
4705
4926
|
return (config.frameHeight - totalHeight) / 2 + config.fontSize;
|
|
4706
4927
|
}
|
|
4707
4928
|
};
|
|
4929
|
+
const allWordTexts = store.words.slice(0, store.length);
|
|
4930
|
+
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
4708
4931
|
const calculateLineX = (lineWidth) => {
|
|
4709
4932
|
switch (config.horizontalAlign) {
|
|
4710
4933
|
case "left":
|
|
@@ -4722,8 +4945,11 @@ var CaptionLayoutEngine = class {
|
|
|
4722
4945
|
const line = group.lines[lineIdx];
|
|
4723
4946
|
line.x = calculateLineX(line.width);
|
|
4724
4947
|
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
4948
|
+
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
4949
|
+
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
4725
4950
|
let xCursor = line.x;
|
|
4726
|
-
for (const
|
|
4951
|
+
for (const visualIdx of visualOrder) {
|
|
4952
|
+
const wordIdx = line.wordIndices[visualIdx];
|
|
4727
4953
|
store.xPositions[wordIdx] = xCursor;
|
|
4728
4954
|
store.yPositions[wordIdx] = line.y;
|
|
4729
4955
|
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
@@ -4733,7 +4959,8 @@ var CaptionLayoutEngine = class {
|
|
|
4733
4959
|
return {
|
|
4734
4960
|
store,
|
|
4735
4961
|
groups,
|
|
4736
|
-
shapedWords
|
|
4962
|
+
shapedWords,
|
|
4963
|
+
paragraphDirection
|
|
4737
4964
|
};
|
|
4738
4965
|
}
|
|
4739
4966
|
getVisibleWordsAtTime(layout, timeMs) {
|
|
@@ -6204,7 +6431,7 @@ async function createTextEngine(opts = {}) {
|
|
|
6204
6431
|
horizontal: asset.align?.horizontal ?? "center",
|
|
6205
6432
|
vertical: asset.align?.vertical ?? "middle"
|
|
6206
6433
|
},
|
|
6207
|
-
background: asset.background,
|
|
6434
|
+
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
6435
|
border: asset.border,
|
|
6209
6436
|
padding: asset.padding,
|
|
6210
6437
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
@@ -6259,7 +6486,8 @@ async function createTextEngine(opts = {}) {
|
|
|
6259
6486
|
try {
|
|
6260
6487
|
const hasBackground = !!asset.background?.color;
|
|
6261
6488
|
const hasAnimation = !!asset.animation?.preset;
|
|
6262
|
-
const
|
|
6489
|
+
const bgBorderRadius = typeof asset.background?.borderRadius === "number" ? asset.background.borderRadius : Number(asset.background?.borderRadius) || 0;
|
|
6490
|
+
const hasBorderRadius = (asset.border?.radius ?? 0) > 0 || bgBorderRadius > 0;
|
|
6263
6491
|
const needsAlpha = !hasBackground && hasAnimation || hasBorderRadius;
|
|
6264
6492
|
console.log(
|
|
6265
6493
|
`\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, BorderRadius=${hasBorderRadius}, Alpha=${needsAlpha}`
|
|
@@ -6317,6 +6545,7 @@ export {
|
|
|
6317
6545
|
calculateAnimationStatesForGroup,
|
|
6318
6546
|
commandsToPathString,
|
|
6319
6547
|
computeSimplePathBounds,
|
|
6548
|
+
containsRTLCharacters,
|
|
6320
6549
|
createDefaultGeneratorConfig,
|
|
6321
6550
|
createFrameSchedule,
|
|
6322
6551
|
createNodePainter,
|
|
@@ -6324,6 +6553,8 @@ export {
|
|
|
6324
6553
|
createRichCaptionRenderer,
|
|
6325
6554
|
createTextEngine,
|
|
6326
6555
|
createVideoEncoder,
|
|
6556
|
+
detectParagraphDirection,
|
|
6557
|
+
detectParagraphDirectionFromWords,
|
|
6327
6558
|
detectPlatform,
|
|
6328
6559
|
detectSubtitleFormat,
|
|
6329
6560
|
extractCaptionPadding,
|
|
@@ -6335,10 +6566,12 @@ export {
|
|
|
6335
6566
|
getDrawCaptionWordOps,
|
|
6336
6567
|
getEncoderCapabilities,
|
|
6337
6568
|
getEncoderWarning,
|
|
6569
|
+
getVisibleText,
|
|
6338
6570
|
groupWordsByPause,
|
|
6339
6571
|
isDrawCaptionWordOp,
|
|
6340
6572
|
isRTLText,
|
|
6341
6573
|
isWebCodecsH264Supported,
|
|
6574
|
+
mirrorAnimationDirection,
|
|
6342
6575
|
normalizePath,
|
|
6343
6576
|
normalizePathString,
|
|
6344
6577
|
parseSubtitleToWords,
|
|
@@ -6346,6 +6579,7 @@ export {
|
|
|
6346
6579
|
quadraticToCubic,
|
|
6347
6580
|
renderSvgAssetToPng,
|
|
6348
6581
|
renderSvgToPng,
|
|
6582
|
+
reorderWordsForLine,
|
|
6349
6583
|
richCaptionAssetSchema,
|
|
6350
6584
|
shapeToSvgString,
|
|
6351
6585
|
svgAssetSchema,
|