@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.cjs
CHANGED
|
@@ -371,6 +371,7 @@ __export(entry_node_exports, {
|
|
|
371
371
|
calculateAnimationStatesForGroup: () => calculateAnimationStatesForGroup,
|
|
372
372
|
commandsToPathString: () => commandsToPathString,
|
|
373
373
|
computeSimplePathBounds: () => computeSimplePathBounds,
|
|
374
|
+
containsRTLCharacters: () => containsRTLCharacters,
|
|
374
375
|
createDefaultGeneratorConfig: () => createDefaultGeneratorConfig,
|
|
375
376
|
createFrameSchedule: () => createFrameSchedule,
|
|
376
377
|
createNodePainter: () => createNodePainter,
|
|
@@ -378,6 +379,8 @@ __export(entry_node_exports, {
|
|
|
378
379
|
createRichCaptionRenderer: () => createRichCaptionRenderer,
|
|
379
380
|
createTextEngine: () => createTextEngine,
|
|
380
381
|
createVideoEncoder: () => createVideoEncoder,
|
|
382
|
+
detectParagraphDirection: () => detectParagraphDirection,
|
|
383
|
+
detectParagraphDirectionFromWords: () => detectParagraphDirectionFromWords,
|
|
381
384
|
detectPlatform: () => detectPlatform,
|
|
382
385
|
detectSubtitleFormat: () => detectSubtitleFormat,
|
|
383
386
|
extractCaptionPadding: () => extractCaptionPadding,
|
|
@@ -389,10 +392,12 @@ __export(entry_node_exports, {
|
|
|
389
392
|
getDrawCaptionWordOps: () => getDrawCaptionWordOps,
|
|
390
393
|
getEncoderCapabilities: () => getEncoderCapabilities,
|
|
391
394
|
getEncoderWarning: () => getEncoderWarning,
|
|
395
|
+
getVisibleText: () => getVisibleText,
|
|
392
396
|
groupWordsByPause: () => groupWordsByPause,
|
|
393
397
|
isDrawCaptionWordOp: () => isDrawCaptionWordOp,
|
|
394
398
|
isRTLText: () => isRTLText,
|
|
395
399
|
isWebCodecsH264Supported: () => isWebCodecsH264Supported,
|
|
400
|
+
mirrorAnimationDirection: () => mirrorAnimationDirection,
|
|
396
401
|
normalizePath: () => normalizePath,
|
|
397
402
|
normalizePathString: () => normalizePathString,
|
|
398
403
|
parseSubtitleToWords: () => parseSubtitleToWords,
|
|
@@ -400,6 +405,7 @@ __export(entry_node_exports, {
|
|
|
400
405
|
quadraticToCubic: () => quadraticToCubic,
|
|
401
406
|
renderSvgAssetToPng: () => renderSvgAssetToPng,
|
|
402
407
|
renderSvgToPng: () => renderSvgToPng,
|
|
408
|
+
reorderWordsForLine: () => reorderWordsForLine,
|
|
403
409
|
richCaptionAssetSchema: () => richCaptionAssetSchema,
|
|
404
410
|
shapeToSvgString: () => shapeToSvgString,
|
|
405
411
|
svgAssetSchema: () => import_zod2.svgAssetSchema,
|
|
@@ -1280,18 +1286,136 @@ var FontRegistry = class _FontRegistry {
|
|
|
1280
1286
|
}
|
|
1281
1287
|
};
|
|
1282
1288
|
|
|
1289
|
+
// src/core/bidi.ts
|
|
1290
|
+
var import_bidi_js = __toESM(require("bidi-js"), 1);
|
|
1291
|
+
var bidiInstance = null;
|
|
1292
|
+
function getBidi() {
|
|
1293
|
+
if (!bidiInstance) {
|
|
1294
|
+
bidiInstance = (0, import_bidi_js.default)();
|
|
1295
|
+
}
|
|
1296
|
+
return bidiInstance;
|
|
1297
|
+
}
|
|
1298
|
+
var RTL_SCRIPT_REGEX = /[--ۿ܀-ݏݐ-ݿހ-߀-߿ࠀ-ࡀ-ࡠ-ࢠ-ࣿיִ-ﭏﭐ-﷿ﹰ-𐴀-𐹠-𞠀-𞤀-𞥟--𞸀-]/u;
|
|
1299
|
+
function containsRTLCharacters(text) {
|
|
1300
|
+
return RTL_SCRIPT_REGEX.test(text);
|
|
1301
|
+
}
|
|
1302
|
+
function detectParagraphDirection(text) {
|
|
1303
|
+
const bidi = getBidi();
|
|
1304
|
+
const { paragraphs } = bidi.getEmbeddingLevels(text);
|
|
1305
|
+
if (paragraphs.length === 0) {
|
|
1306
|
+
return "ltr";
|
|
1307
|
+
}
|
|
1308
|
+
return paragraphs[0].level % 2 === 1 ? "rtl" : "ltr";
|
|
1309
|
+
}
|
|
1310
|
+
function detectParagraphDirectionFromWords(words) {
|
|
1311
|
+
const combined = words.join(" ");
|
|
1312
|
+
return detectParagraphDirection(combined);
|
|
1313
|
+
}
|
|
1314
|
+
function reorderWordsForLine(wordTexts, paragraphDirection) {
|
|
1315
|
+
if (wordTexts.length <= 1) {
|
|
1316
|
+
return wordTexts.map((_, i) => i);
|
|
1317
|
+
}
|
|
1318
|
+
const lineText = wordTexts.join(" ");
|
|
1319
|
+
const bidi = getBidi();
|
|
1320
|
+
const { levels } = bidi.getEmbeddingLevels(lineText, paragraphDirection);
|
|
1321
|
+
const wordLevels = [];
|
|
1322
|
+
let charIndex = 0;
|
|
1323
|
+
for (let i = 0; i < wordTexts.length; i++) {
|
|
1324
|
+
const wordLevel = levels[charIndex];
|
|
1325
|
+
wordLevels.push(wordLevel);
|
|
1326
|
+
charIndex += wordTexts[i].length;
|
|
1327
|
+
if (i < wordTexts.length - 1) {
|
|
1328
|
+
charIndex += 1;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const indices = wordTexts.map((_, i) => i);
|
|
1332
|
+
const maxLevel = Math.max(...wordLevels);
|
|
1333
|
+
const minOddLevel = paragraphDirection === "rtl" ? 1 : Math.min(...wordLevels.filter((l) => l % 2 === 1), maxLevel + 1);
|
|
1334
|
+
for (let level = maxLevel; level >= minOddLevel; level--) {
|
|
1335
|
+
let start = 0;
|
|
1336
|
+
while (start < indices.length) {
|
|
1337
|
+
if (wordLevels[indices[start]] >= level) {
|
|
1338
|
+
let end = start;
|
|
1339
|
+
while (end + 1 < indices.length && wordLevels[indices[end + 1]] >= level) {
|
|
1340
|
+
end++;
|
|
1341
|
+
}
|
|
1342
|
+
if (start < end) {
|
|
1343
|
+
let left = start;
|
|
1344
|
+
let right = end;
|
|
1345
|
+
while (left < right) {
|
|
1346
|
+
const tmp = indices[left];
|
|
1347
|
+
indices[left] = indices[right];
|
|
1348
|
+
indices[right] = tmp;
|
|
1349
|
+
left++;
|
|
1350
|
+
right--;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
start = end + 1;
|
|
1354
|
+
} else {
|
|
1355
|
+
start++;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return indices;
|
|
1360
|
+
}
|
|
1361
|
+
function getVisibleText(text, visibleCharacters, isRTL) {
|
|
1362
|
+
if (visibleCharacters < 0 || visibleCharacters >= text.length) {
|
|
1363
|
+
return text;
|
|
1364
|
+
}
|
|
1365
|
+
if (visibleCharacters === 0) {
|
|
1366
|
+
return "";
|
|
1367
|
+
}
|
|
1368
|
+
return text.slice(0, visibleCharacters);
|
|
1369
|
+
}
|
|
1370
|
+
function mirrorAnimationDirection(direction, isRTL) {
|
|
1371
|
+
if (!isRTL) {
|
|
1372
|
+
return direction;
|
|
1373
|
+
}
|
|
1374
|
+
if (direction === "left") return "right";
|
|
1375
|
+
if (direction === "right") return "left";
|
|
1376
|
+
return direction;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1283
1379
|
// src/core/layout.ts
|
|
1380
|
+
var import_bidi_js2 = __toESM(require("bidi-js"), 1);
|
|
1284
1381
|
function isEmoji(char) {
|
|
1285
1382
|
const code = char.codePointAt(0);
|
|
1286
1383
|
if (!code) return false;
|
|
1287
|
-
return code >= 127744 && code <= 129535 ||
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1384
|
+
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;
|
|
1385
|
+
}
|
|
1386
|
+
function reorderRunsVisually(runs) {
|
|
1387
|
+
if (runs.length <= 1) return runs;
|
|
1388
|
+
const result = runs.slice();
|
|
1389
|
+
const runLevels = result.map((r) => r.level);
|
|
1390
|
+
const maxLevel = Math.max(...runLevels);
|
|
1391
|
+
const minLevel = Math.min(...runLevels);
|
|
1392
|
+
const minOddLevel = minLevel % 2 === 1 ? minLevel : minLevel + 1;
|
|
1393
|
+
for (let level = maxLevel; level >= minOddLevel; level--) {
|
|
1394
|
+
let start = 0;
|
|
1395
|
+
while (start < result.length) {
|
|
1396
|
+
if (runLevels[result.indexOf(result[start])] >= level) {
|
|
1397
|
+
let end = start;
|
|
1398
|
+
while (end + 1 < result.length && result[end + 1].level >= level) {
|
|
1399
|
+
end++;
|
|
1400
|
+
}
|
|
1401
|
+
if (start < end) {
|
|
1402
|
+
let left = start;
|
|
1403
|
+
let right = end;
|
|
1404
|
+
while (left < right) {
|
|
1405
|
+
const tmp = result[left];
|
|
1406
|
+
result[left] = result[right];
|
|
1407
|
+
result[right] = tmp;
|
|
1408
|
+
left++;
|
|
1409
|
+
right--;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
start = end + 1;
|
|
1413
|
+
} else {
|
|
1414
|
+
start++;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return result;
|
|
1295
1419
|
}
|
|
1296
1420
|
var LayoutEngine = class {
|
|
1297
1421
|
constructor(fonts) {
|
|
@@ -1309,13 +1433,16 @@ var LayoutEngine = class {
|
|
|
1309
1433
|
return t;
|
|
1310
1434
|
}
|
|
1311
1435
|
}
|
|
1312
|
-
async shapeFull(text, desc) {
|
|
1436
|
+
async shapeFull(text, desc, direction) {
|
|
1313
1437
|
try {
|
|
1314
1438
|
const hb = await this.fonts.getHB();
|
|
1315
1439
|
const buffer = hb.createBuffer();
|
|
1316
1440
|
try {
|
|
1317
1441
|
buffer.addText(text);
|
|
1318
1442
|
buffer.guessSegmentProperties();
|
|
1443
|
+
if (direction) {
|
|
1444
|
+
buffer.setDirection(direction);
|
|
1445
|
+
}
|
|
1319
1446
|
const font = await this.fonts.getFont(desc);
|
|
1320
1447
|
const face = await this.fonts.getFace(desc);
|
|
1321
1448
|
const upem = face?.upem || 1e3;
|
|
@@ -1338,6 +1465,64 @@ var LayoutEngine = class {
|
|
|
1338
1465
|
);
|
|
1339
1466
|
}
|
|
1340
1467
|
}
|
|
1468
|
+
splitIntoBidiRuns(input, levels, desc, emojiFallback) {
|
|
1469
|
+
const runs = [];
|
|
1470
|
+
if (input.length === 0) return runs;
|
|
1471
|
+
let runStart = 0;
|
|
1472
|
+
let runLevel = levels[0];
|
|
1473
|
+
let runIsEmoji = emojiFallback ? isEmoji(input[0]) : false;
|
|
1474
|
+
for (let i = 1; i <= input.length; i++) {
|
|
1475
|
+
const atEnd = i === input.length;
|
|
1476
|
+
const charLevel = atEnd ? -1 : levels[i];
|
|
1477
|
+
const charIsEmoji = !atEnd && emojiFallback ? isEmoji(String.fromCodePoint(input.codePointAt(i) ?? 0)) : false;
|
|
1478
|
+
if (atEnd || charLevel !== runLevel || emojiFallback && charIsEmoji !== runIsEmoji) {
|
|
1479
|
+
const text = input.slice(runStart, i);
|
|
1480
|
+
const font = runIsEmoji && emojiFallback ? emojiFallback : desc;
|
|
1481
|
+
runs.push({ text, startIndex: runStart, endIndex: i, level: runLevel, font });
|
|
1482
|
+
if (!atEnd) {
|
|
1483
|
+
runStart = i;
|
|
1484
|
+
runLevel = charLevel;
|
|
1485
|
+
runIsEmoji = charIsEmoji;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return runs;
|
|
1490
|
+
}
|
|
1491
|
+
async shapeWithBidi(input, desc, emojiFallback) {
|
|
1492
|
+
const hasRTL = containsRTLCharacters(input);
|
|
1493
|
+
const hasLTR = /[a-zA-Z0-9]/.test(input);
|
|
1494
|
+
const hasMixedDirection = hasRTL && hasLTR;
|
|
1495
|
+
if (!hasMixedDirection && !emojiFallback) {
|
|
1496
|
+
const textDirection = hasRTL ? "rtl" : void 0;
|
|
1497
|
+
return this.shapeFull(input, desc, textDirection);
|
|
1498
|
+
}
|
|
1499
|
+
const bidi = (0, import_bidi_js2.default)();
|
|
1500
|
+
const { levels } = bidi.getEmbeddingLevels(input);
|
|
1501
|
+
const bidiRuns = this.splitIntoBidiRuns(input, levels, desc, emojiFallback);
|
|
1502
|
+
const shapedRuns = [];
|
|
1503
|
+
for (const run of bidiRuns) {
|
|
1504
|
+
const runDirection = run.level % 2 === 1 ? "rtl" : "ltr";
|
|
1505
|
+
const runShaped = await this.shapeFull(run.text, run.font, runDirection);
|
|
1506
|
+
for (const glyph of runShaped) {
|
|
1507
|
+
glyph.cl += run.startIndex;
|
|
1508
|
+
if (run.font !== desc) {
|
|
1509
|
+
glyph.fontDesc = run.font;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
shapedRuns.push({
|
|
1513
|
+
glyphs: runShaped,
|
|
1514
|
+
startIndex: run.startIndex,
|
|
1515
|
+
endIndex: run.endIndex,
|
|
1516
|
+
level: run.level
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
const visualRuns = reorderRunsVisually(shapedRuns);
|
|
1520
|
+
const visualGlyphs = [];
|
|
1521
|
+
for (const run of visualRuns) {
|
|
1522
|
+
visualGlyphs.push(...run.glyphs);
|
|
1523
|
+
}
|
|
1524
|
+
return visualGlyphs;
|
|
1525
|
+
}
|
|
1341
1526
|
async layout(params) {
|
|
1342
1527
|
try {
|
|
1343
1528
|
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
@@ -1347,36 +1532,7 @@ var LayoutEngine = class {
|
|
|
1347
1532
|
}
|
|
1348
1533
|
let shaped;
|
|
1349
1534
|
try {
|
|
1350
|
-
|
|
1351
|
-
shaped = await this.shapeFull(input, desc);
|
|
1352
|
-
} else {
|
|
1353
|
-
const chars = Array.from(input);
|
|
1354
|
-
const runs = [];
|
|
1355
|
-
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
1356
|
-
for (let i = 0; i < chars.length; i++) {
|
|
1357
|
-
const char = chars[i];
|
|
1358
|
-
const charIsEmoji = isEmoji(char);
|
|
1359
|
-
if (i === 0) {
|
|
1360
|
-
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
1361
|
-
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
1362
|
-
currentRun.text += char;
|
|
1363
|
-
} else {
|
|
1364
|
-
runs.push(currentRun);
|
|
1365
|
-
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
if (currentRun.text) runs.push(currentRun);
|
|
1369
|
-
shaped = [];
|
|
1370
|
-
for (const run of runs) {
|
|
1371
|
-
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
1372
|
-
const runShaped = await this.shapeFull(run.text, runFont);
|
|
1373
|
-
for (const glyph of runShaped) {
|
|
1374
|
-
glyph.cl += run.startIndex;
|
|
1375
|
-
glyph.fontDesc = runFont;
|
|
1376
|
-
}
|
|
1377
|
-
shaped.push(...runShaped);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1535
|
+
shaped = await this.shapeWithBidi(input, desc, emojiFallback);
|
|
1380
1536
|
} catch (err) {
|
|
1381
1537
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1382
1538
|
}
|
|
@@ -1407,7 +1563,6 @@ var LayoutEngine = class {
|
|
|
1407
1563
|
cluster: g.cl,
|
|
1408
1564
|
char,
|
|
1409
1565
|
fontDesc: g.fontDesc
|
|
1410
|
-
// Preserve font descriptor
|
|
1411
1566
|
};
|
|
1412
1567
|
});
|
|
1413
1568
|
const lines = [];
|
|
@@ -1470,9 +1625,11 @@ var LayoutEngine = class {
|
|
|
1470
1625
|
y: 0
|
|
1471
1626
|
});
|
|
1472
1627
|
}
|
|
1628
|
+
const textIsRTL = containsRTLCharacters(input);
|
|
1473
1629
|
const lineHeight = params.lineHeight * fontSize;
|
|
1474
1630
|
for (let i = 0; i < lines.length; i++) {
|
|
1475
1631
|
lines[i].y = (i + 1) * lineHeight;
|
|
1632
|
+
lines[i].isRTL = textIsRTL;
|
|
1476
1633
|
}
|
|
1477
1634
|
return lines;
|
|
1478
1635
|
} catch (err) {
|
|
@@ -1511,6 +1668,14 @@ function normalizePadding(padding) {
|
|
|
1511
1668
|
}
|
|
1512
1669
|
return padding;
|
|
1513
1670
|
}
|
|
1671
|
+
function resolveHorizontalAlign(align, isRTL) {
|
|
1672
|
+
if (!isRTL || align === "center") {
|
|
1673
|
+
return align;
|
|
1674
|
+
}
|
|
1675
|
+
if (align === "left") return "right";
|
|
1676
|
+
if (align === "right") return "left";
|
|
1677
|
+
return align;
|
|
1678
|
+
}
|
|
1514
1679
|
async function buildDrawOps(p) {
|
|
1515
1680
|
const ops = [];
|
|
1516
1681
|
const padding = normalizePadding(p.padding);
|
|
@@ -1551,7 +1716,8 @@ async function buildDrawOps(p) {
|
|
|
1551
1716
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
1552
1717
|
for (const line of p.lines) {
|
|
1553
1718
|
let lineX;
|
|
1554
|
-
|
|
1719
|
+
const effectiveAlign = resolveHorizontalAlign(p.align.horizontal, line.isRTL);
|
|
1720
|
+
switch (effectiveAlign) {
|
|
1555
1721
|
case "left":
|
|
1556
1722
|
lineX = 0;
|
|
1557
1723
|
break;
|
|
@@ -1679,7 +1845,7 @@ async function buildDrawOps(p) {
|
|
|
1679
1845
|
const contentWidth = p.contentRect?.width ?? p.canvas.width;
|
|
1680
1846
|
const contentHeight = p.contentRect?.height ?? p.canvas.height;
|
|
1681
1847
|
const borderWidth2 = p.border?.width ?? 0;
|
|
1682
|
-
const borderRadius = p.border?.radius ?? 0;
|
|
1848
|
+
const borderRadius = p.border?.radius ?? p.background?.borderRadius ?? 0;
|
|
1683
1849
|
const halfBorder = borderWidth2 / 2;
|
|
1684
1850
|
const canvasCenterX = p.canvas.width / 2;
|
|
1685
1851
|
const canvasCenterY = p.canvas.height / 2;
|
|
@@ -1797,7 +1963,15 @@ function applyAnimation(ops, lines, p) {
|
|
|
1797
1963
|
case "fadeIn":
|
|
1798
1964
|
return applyFadeInAnimation(ops, lines, progress, p.anim.style, p.fontSize, duration);
|
|
1799
1965
|
case "slideIn":
|
|
1800
|
-
return applySlideInAnimation(
|
|
1966
|
+
return applySlideInAnimation(
|
|
1967
|
+
ops,
|
|
1968
|
+
lines,
|
|
1969
|
+
progress,
|
|
1970
|
+
p.anim.direction ?? "left",
|
|
1971
|
+
p.fontSize,
|
|
1972
|
+
p.anim.style,
|
|
1973
|
+
duration
|
|
1974
|
+
);
|
|
1801
1975
|
case "shift":
|
|
1802
1976
|
return applyShiftAnimation(
|
|
1803
1977
|
ops,
|
|
@@ -1825,6 +1999,9 @@ function applyAnimation(ops, lines, p) {
|
|
|
1825
1999
|
}
|
|
1826
2000
|
var isShadowFill = (op) => op.op === "FillPath" && op.isShadow === true;
|
|
1827
2001
|
var isGlyphFill = (op) => op.op === "FillPath" && !op.isShadow === true;
|
|
2002
|
+
function isRTLLines(lines) {
|
|
2003
|
+
return lines.length > 0 && lines[0].isRTL === true;
|
|
2004
|
+
}
|
|
1828
2005
|
function getTextColorFromOps(ops) {
|
|
1829
2006
|
for (const op of ops) {
|
|
1830
2007
|
if (op.op === "FillPath") {
|
|
@@ -1844,28 +2021,32 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1844
2021
|
const totalWords = wordSegments.length;
|
|
1845
2022
|
const visibleWords = Math.floor(progress * totalWords);
|
|
1846
2023
|
if (visibleWords === 0) {
|
|
1847
|
-
return ops.filter(
|
|
2024
|
+
return ops.filter(
|
|
2025
|
+
(x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
|
|
2026
|
+
);
|
|
1848
2027
|
}
|
|
1849
2028
|
let totalVisibleGlyphs = 0;
|
|
1850
2029
|
for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
|
|
1851
2030
|
totalVisibleGlyphs += wordSegments[i].glyphCount;
|
|
1852
2031
|
}
|
|
1853
|
-
const visibleOpsRaw = sliceGlyphOps(ops, totalVisibleGlyphs);
|
|
2032
|
+
const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, totalVisibleGlyphs) : sliceGlyphOps(ops, totalVisibleGlyphs);
|
|
1854
2033
|
const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
1855
2034
|
if (progress < 1 && totalVisibleGlyphs > 0) {
|
|
1856
|
-
return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time);
|
|
2035
|
+
return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time, isRTLLines(lines));
|
|
1857
2036
|
}
|
|
1858
2037
|
return visibleOps;
|
|
1859
2038
|
} else {
|
|
1860
2039
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1861
2040
|
const visibleGlyphs = Math.floor(progress * totalGlyphs);
|
|
1862
2041
|
if (visibleGlyphs === 0) {
|
|
1863
|
-
return ops.filter(
|
|
2042
|
+
return ops.filter(
|
|
2043
|
+
(x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke"
|
|
2044
|
+
);
|
|
1864
2045
|
}
|
|
1865
|
-
const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
|
|
2046
|
+
const visibleOpsRaw = isRTLLines(lines) ? sliceGlyphOpsFromEnd(ops, visibleGlyphs) : sliceGlyphOps(ops, visibleGlyphs);
|
|
1866
2047
|
const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
1867
2048
|
if (progress < 1 && visibleGlyphs > 0) {
|
|
1868
|
-
return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time);
|
|
2049
|
+
return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time, isRTLLines(lines));
|
|
1869
2050
|
}
|
|
1870
2051
|
return visibleOps;
|
|
1871
2052
|
}
|
|
@@ -1896,7 +2077,8 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
|
|
|
1896
2077
|
acc += gcount;
|
|
1897
2078
|
}
|
|
1898
2079
|
if (wordIndex >= 0) {
|
|
1899
|
-
const
|
|
2080
|
+
const effectiveWordIndex = isRTLLines(lines) ? Math.max(0, totalWords - 1 - wordIndex) : wordIndex;
|
|
2081
|
+
const startF = effectiveWordIndex / Math.max(1, totalWords) * (duration / duration);
|
|
1900
2082
|
const endF = Math.min(1, startF + 0.3);
|
|
1901
2083
|
if (progress >= endF) {
|
|
1902
2084
|
result.push(op);
|
|
@@ -1976,7 +2158,8 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1976
2158
|
}
|
|
1977
2159
|
unitIndex = Math.max(0, wordIndex);
|
|
1978
2160
|
}
|
|
1979
|
-
const
|
|
2161
|
+
const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
|
|
2162
|
+
const { startF, endF } = windowFor(effectiveUnit);
|
|
1980
2163
|
if (progress <= startF) {
|
|
1981
2164
|
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
1982
2165
|
if (op.op === "FillPath") {
|
|
@@ -2057,7 +2240,8 @@ function applyFadeInAnimation(ops, lines, progress, style, fontSize, duration) {
|
|
|
2057
2240
|
}
|
|
2058
2241
|
unitIndex = Math.max(0, wordIndex);
|
|
2059
2242
|
}
|
|
2060
|
-
const
|
|
2243
|
+
const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
|
|
2244
|
+
const { startF, endF } = windowFor(effectiveUnit);
|
|
2061
2245
|
if (progress <= startF) {
|
|
2062
2246
|
const animated = { ...op };
|
|
2063
2247
|
if (op.op === "FillPath") {
|
|
@@ -2144,7 +2328,8 @@ function applySlideInAnimation(ops, lines, progress, direction, fontSize, style,
|
|
|
2144
2328
|
}
|
|
2145
2329
|
unitIndex = Math.max(0, wordIndex);
|
|
2146
2330
|
}
|
|
2147
|
-
const
|
|
2331
|
+
const effectiveUnit = isRTLLines(lines) ? Math.max(0, totalUnits - 1 - unitIndex) : unitIndex;
|
|
2332
|
+
const { startF, endF } = windowFor(effectiveUnit);
|
|
2148
2333
|
if (progress <= startF) {
|
|
2149
2334
|
const animated = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
2150
2335
|
if (op.op === "FillPath") {
|
|
@@ -2211,6 +2396,43 @@ function segmentLineBySpaces(line) {
|
|
|
2211
2396
|
if (current.length) words.push(current);
|
|
2212
2397
|
return words;
|
|
2213
2398
|
}
|
|
2399
|
+
function sliceGlyphOpsFromEnd(ops, maxGlyphs) {
|
|
2400
|
+
let totalGlyphs = 0;
|
|
2401
|
+
for (const op of ops) {
|
|
2402
|
+
if (op.op === "FillPath" && !isShadowFill(op)) totalGlyphs++;
|
|
2403
|
+
}
|
|
2404
|
+
const skipCount = Math.max(0, totalGlyphs - maxGlyphs);
|
|
2405
|
+
const result = [];
|
|
2406
|
+
let glyphCount = 0;
|
|
2407
|
+
let foundGlyphs = false;
|
|
2408
|
+
for (const op of ops) {
|
|
2409
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
2410
|
+
result.push(op);
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
if (op.op === "FillPath" && !isShadowFill(op)) {
|
|
2414
|
+
if (glyphCount >= skipCount) {
|
|
2415
|
+
result.push(op);
|
|
2416
|
+
foundGlyphs = true;
|
|
2417
|
+
}
|
|
2418
|
+
glyphCount++;
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
if (op.op === "StrokePath") {
|
|
2422
|
+
if (glyphCount >= skipCount) result.push(op);
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (op.op === "FillPath" && isShadowFill(op)) {
|
|
2426
|
+
if (glyphCount >= skipCount) result.push(op);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
if (op.op === "DecorationLine" && foundGlyphs) {
|
|
2430
|
+
result.push(op);
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return result;
|
|
2435
|
+
}
|
|
2214
2436
|
function sliceGlyphOps(ops, maxGlyphs) {
|
|
2215
2437
|
const result = [];
|
|
2216
2438
|
let glyphCount = 0;
|
|
@@ -2243,27 +2465,30 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
2243
2465
|
}
|
|
2244
2466
|
return result;
|
|
2245
2467
|
}
|
|
2246
|
-
function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
2468
|
+
function addTypewriterCursor(ops, glyphCount, fontSize, time, isRTL = false) {
|
|
2247
2469
|
if (glyphCount === 0) return ops;
|
|
2248
2470
|
const blinkRate = 1;
|
|
2249
2471
|
const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
|
|
2250
2472
|
const alwaysShowCursor = true;
|
|
2251
2473
|
if (!alwaysShowCursor && !cursorVisible) return ops;
|
|
2252
2474
|
let last = null;
|
|
2475
|
+
let first = null;
|
|
2253
2476
|
let count = 0;
|
|
2254
2477
|
for (const op of ops) {
|
|
2255
2478
|
if (op.op === "FillPath" && !isShadowFill(op)) {
|
|
2256
2479
|
count++;
|
|
2480
|
+
if (count === 1) first = op;
|
|
2257
2481
|
if (count === glyphCount) {
|
|
2258
2482
|
last = op;
|
|
2259
2483
|
break;
|
|
2260
2484
|
}
|
|
2261
2485
|
}
|
|
2262
2486
|
}
|
|
2263
|
-
|
|
2487
|
+
const cursorAnchor = isRTL && first ? first : last;
|
|
2488
|
+
if (cursorAnchor && cursorAnchor.op === "FillPath") {
|
|
2264
2489
|
const color = getTextColorFromOps(ops);
|
|
2265
|
-
const cursorX =
|
|
2266
|
-
const cursorY =
|
|
2490
|
+
const cursorX = isRTL && first ? first.x - fontSize * 0.15 : cursorAnchor.x + fontSize * 0.5;
|
|
2491
|
+
const cursorY = cursorAnchor.y;
|
|
2267
2492
|
const cursorWidth = Math.max(3, fontSize / 15);
|
|
2268
2493
|
const cursorOp = {
|
|
2269
2494
|
op: "DecorationLine",
|
|
@@ -2623,7 +2848,7 @@ function calculateNoneState(ctx) {
|
|
|
2623
2848
|
isActive: isWordActive(ctx)
|
|
2624
2849
|
};
|
|
2625
2850
|
}
|
|
2626
|
-
function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48) {
|
|
2851
|
+
function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
|
|
2627
2852
|
const safeSpeed = config.speed > 0 ? config.speed : 1;
|
|
2628
2853
|
const ctx = {
|
|
2629
2854
|
wordStart,
|
|
@@ -2646,9 +2871,11 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
|
|
|
2646
2871
|
case "fade":
|
|
2647
2872
|
partialState = calculateFadeState(ctx, safeSpeed);
|
|
2648
2873
|
break;
|
|
2649
|
-
case "slide":
|
|
2650
|
-
|
|
2874
|
+
case "slide": {
|
|
2875
|
+
const slideDir = mirrorAnimationDirection(config.direction, isRTL);
|
|
2876
|
+
partialState = calculateSlideState(ctx, slideDir, config.speed, fontSize);
|
|
2651
2877
|
break;
|
|
2878
|
+
}
|
|
2652
2879
|
case "bounce":
|
|
2653
2880
|
partialState = calculateBounceState(ctx, safeSpeed, fontSize);
|
|
2654
2881
|
break;
|
|
@@ -2676,7 +2903,8 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
|
|
|
2676
2903
|
config,
|
|
2677
2904
|
activeScale,
|
|
2678
2905
|
word.text.length,
|
|
2679
|
-
fontSize
|
|
2906
|
+
fontSize,
|
|
2907
|
+
word.isRTL
|
|
2680
2908
|
);
|
|
2681
2909
|
states.set(word.wordIndex, state);
|
|
2682
2910
|
}
|
|
@@ -2820,7 +3048,7 @@ function extractActiveScale(asset) {
|
|
|
2820
3048
|
}
|
|
2821
3049
|
function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
|
|
2822
3050
|
const isActive = animState.isActive;
|
|
2823
|
-
const displayText =
|
|
3051
|
+
const displayText = getVisibleText(word.text, animState.visibleCharacters, word.isRTL);
|
|
2824
3052
|
return {
|
|
2825
3053
|
op: "DrawCaptionWord",
|
|
2826
3054
|
text: displayText,
|
|
@@ -3275,7 +3503,7 @@ async function createNodePainter(opts) {
|
|
|
3275
3503
|
renderToBoth((context) => {
|
|
3276
3504
|
for (const wordOp of captionWordOps) {
|
|
3277
3505
|
if (!wordOp.background) continue;
|
|
3278
|
-
const wordDisplayText = wordOp.
|
|
3506
|
+
const wordDisplayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
|
|
3279
3507
|
if (wordDisplayText.length === 0) continue;
|
|
3280
3508
|
context.save();
|
|
3281
3509
|
const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
|
|
@@ -3309,7 +3537,7 @@ async function createNodePainter(opts) {
|
|
|
3309
3537
|
context.restore();
|
|
3310
3538
|
}
|
|
3311
3539
|
for (const wordOp of captionWordOps) {
|
|
3312
|
-
const displayText = wordOp.
|
|
3540
|
+
const displayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
|
|
3313
3541
|
if (displayText.length === 0) continue;
|
|
3314
3542
|
context.save();
|
|
3315
3543
|
const tx = Math.round(wordOp.x + wordOp.transform.translateX);
|
|
@@ -4841,9 +5069,8 @@ function extractSvgDimensions(svgString) {
|
|
|
4841
5069
|
|
|
4842
5070
|
// src/core/rich-caption-layout.ts
|
|
4843
5071
|
var import_lru_cache = require("lru-cache");
|
|
4844
|
-
var RTL_RANGES = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
|
4845
5072
|
function isRTLText(text) {
|
|
4846
|
-
return
|
|
5073
|
+
return containsRTLCharacters(text);
|
|
4847
5074
|
}
|
|
4848
5075
|
var WordTimingStore = class {
|
|
4849
5076
|
startTimes;
|
|
@@ -5102,6 +5329,8 @@ var CaptionLayoutEngine = class {
|
|
|
5102
5329
|
return (config.frameHeight - totalHeight) / 2 + config.fontSize;
|
|
5103
5330
|
}
|
|
5104
5331
|
};
|
|
5332
|
+
const allWordTexts = store.words.slice(0, store.length);
|
|
5333
|
+
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
5105
5334
|
const calculateLineX = (lineWidth) => {
|
|
5106
5335
|
switch (config.horizontalAlign) {
|
|
5107
5336
|
case "left":
|
|
@@ -5119,8 +5348,11 @@ var CaptionLayoutEngine = class {
|
|
|
5119
5348
|
const line = group.lines[lineIdx];
|
|
5120
5349
|
line.x = calculateLineX(line.width);
|
|
5121
5350
|
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
5351
|
+
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
5352
|
+
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
5122
5353
|
let xCursor = line.x;
|
|
5123
|
-
for (const
|
|
5354
|
+
for (const visualIdx of visualOrder) {
|
|
5355
|
+
const wordIdx = line.wordIndices[visualIdx];
|
|
5124
5356
|
store.xPositions[wordIdx] = xCursor;
|
|
5125
5357
|
store.yPositions[wordIdx] = line.y;
|
|
5126
5358
|
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
@@ -5130,7 +5362,8 @@ var CaptionLayoutEngine = class {
|
|
|
5130
5362
|
return {
|
|
5131
5363
|
store,
|
|
5132
5364
|
groups,
|
|
5133
|
-
shapedWords
|
|
5365
|
+
shapedWords,
|
|
5366
|
+
paragraphDirection
|
|
5134
5367
|
};
|
|
5135
5368
|
}
|
|
5136
5369
|
getVisibleWordsAtTime(layout, timeMs) {
|
|
@@ -6601,7 +6834,7 @@ async function createTextEngine(opts = {}) {
|
|
|
6601
6834
|
horizontal: asset.align?.horizontal ?? "center",
|
|
6602
6835
|
vertical: asset.align?.vertical ?? "middle"
|
|
6603
6836
|
},
|
|
6604
|
-
background: asset.background,
|
|
6837
|
+
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,
|
|
6605
6838
|
border: asset.border,
|
|
6606
6839
|
padding: asset.padding,
|
|
6607
6840
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
@@ -6656,7 +6889,8 @@ async function createTextEngine(opts = {}) {
|
|
|
6656
6889
|
try {
|
|
6657
6890
|
const hasBackground = !!asset.background?.color;
|
|
6658
6891
|
const hasAnimation = !!asset.animation?.preset;
|
|
6659
|
-
const
|
|
6892
|
+
const bgBorderRadius = typeof asset.background?.borderRadius === "number" ? asset.background.borderRadius : Number(asset.background?.borderRadius) || 0;
|
|
6893
|
+
const hasBorderRadius = (asset.border?.radius ?? 0) > 0 || bgBorderRadius > 0;
|
|
6660
6894
|
const needsAlpha = !hasBackground && hasAnimation || hasBorderRadius;
|
|
6661
6895
|
console.log(
|
|
6662
6896
|
`\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, BorderRadius=${hasBorderRadius}, Alpha=${needsAlpha}`
|
|
@@ -6715,6 +6949,7 @@ async function createTextEngine(opts = {}) {
|
|
|
6715
6949
|
calculateAnimationStatesForGroup,
|
|
6716
6950
|
commandsToPathString,
|
|
6717
6951
|
computeSimplePathBounds,
|
|
6952
|
+
containsRTLCharacters,
|
|
6718
6953
|
createDefaultGeneratorConfig,
|
|
6719
6954
|
createFrameSchedule,
|
|
6720
6955
|
createNodePainter,
|
|
@@ -6722,6 +6957,8 @@ async function createTextEngine(opts = {}) {
|
|
|
6722
6957
|
createRichCaptionRenderer,
|
|
6723
6958
|
createTextEngine,
|
|
6724
6959
|
createVideoEncoder,
|
|
6960
|
+
detectParagraphDirection,
|
|
6961
|
+
detectParagraphDirectionFromWords,
|
|
6725
6962
|
detectPlatform,
|
|
6726
6963
|
detectSubtitleFormat,
|
|
6727
6964
|
extractCaptionPadding,
|
|
@@ -6733,10 +6970,12 @@ async function createTextEngine(opts = {}) {
|
|
|
6733
6970
|
getDrawCaptionWordOps,
|
|
6734
6971
|
getEncoderCapabilities,
|
|
6735
6972
|
getEncoderWarning,
|
|
6973
|
+
getVisibleText,
|
|
6736
6974
|
groupWordsByPause,
|
|
6737
6975
|
isDrawCaptionWordOp,
|
|
6738
6976
|
isRTLText,
|
|
6739
6977
|
isWebCodecsH264Supported,
|
|
6978
|
+
mirrorAnimationDirection,
|
|
6740
6979
|
normalizePath,
|
|
6741
6980
|
normalizePathString,
|
|
6742
6981
|
parseSubtitleToWords,
|
|
@@ -6744,6 +6983,7 @@ async function createTextEngine(opts = {}) {
|
|
|
6744
6983
|
quadraticToCubic,
|
|
6745
6984
|
renderSvgAssetToPng,
|
|
6746
6985
|
renderSvgToPng,
|
|
6986
|
+
reorderWordsForLine,
|
|
6747
6987
|
richCaptionAssetSchema,
|
|
6748
6988
|
shapeToSvgString,
|
|
6749
6989
|
svgAssetSchema,
|