@shotstack/shotstack-canvas 1.5.6 → 1.5.8
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 +68 -24
- package/dist/entry.node.d.cts +1 -0
- package/dist/entry.node.d.ts +1 -0
- package/dist/entry.node.js +68 -24
- package/dist/entry.web.d.ts +1 -0
- package/dist/entry.web.js +45 -14
- package/package.json +1 -1
package/dist/entry.node.cjs
CHANGED
|
@@ -100,7 +100,8 @@ var fontSchema = import_joi.default.object({
|
|
|
100
100
|
size: import_joi.default.number().min(CANVAS_CONFIG.LIMITS.minFontSize).max(CANVAS_CONFIG.LIMITS.maxFontSize).default(CANVAS_CONFIG.DEFAULTS.fontSize),
|
|
101
101
|
weight: import_joi.default.alternatives().try(import_joi.default.string(), import_joi.default.number()).default("400"),
|
|
102
102
|
color: import_joi.default.string().pattern(HEX6).default(CANVAS_CONFIG.DEFAULTS.color),
|
|
103
|
-
opacity: import_joi.default.number().min(0).max(1).default(1)
|
|
103
|
+
opacity: import_joi.default.number().min(0).max(1).default(1),
|
|
104
|
+
background: import_joi.default.string().pattern(HEX6).optional()
|
|
104
105
|
}).unknown(false);
|
|
105
106
|
var styleSchema = import_joi.default.object({
|
|
106
107
|
letterSpacing: import_joi.default.number().default(0),
|
|
@@ -854,7 +855,7 @@ function normalizePadding(padding) {
|
|
|
854
855
|
async function buildDrawOps(p) {
|
|
855
856
|
const ops = [];
|
|
856
857
|
const padding = normalizePadding(p.padding);
|
|
857
|
-
const borderWidth = p.
|
|
858
|
+
const borderWidth = p.border?.width ?? 0;
|
|
858
859
|
ops.push({
|
|
859
860
|
op: "BeginFrame",
|
|
860
861
|
width: p.canvas.width,
|
|
@@ -887,6 +888,7 @@ async function buildDrawOps(p) {
|
|
|
887
888
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
888
889
|
const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
|
|
889
890
|
const textOps = [];
|
|
891
|
+
const highlighterOps = [];
|
|
890
892
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
891
893
|
for (const line of p.lines) {
|
|
892
894
|
let lineX;
|
|
@@ -905,6 +907,20 @@ async function buildDrawOps(p) {
|
|
|
905
907
|
let xCursor = lineX;
|
|
906
908
|
const lineIndex = p.lines.indexOf(line);
|
|
907
909
|
const baselineY = blockY + lineIndex * lineHeightPx;
|
|
910
|
+
if (p.font.background) {
|
|
911
|
+
const highlightPadding = p.font.size * 0.15;
|
|
912
|
+
const verticalPadding = p.font.size * 0.12;
|
|
913
|
+
const highlightHeight = p.font.size * 0.92 + verticalPadding * 2;
|
|
914
|
+
const highlightY = baselineY - p.font.size * 0.78 - verticalPadding;
|
|
915
|
+
highlighterOps.push({
|
|
916
|
+
op: "Rectangle",
|
|
917
|
+
x: lineX - highlightPadding,
|
|
918
|
+
y: highlightY,
|
|
919
|
+
width: line.width + highlightPadding * 2,
|
|
920
|
+
height: highlightHeight,
|
|
921
|
+
fill: { kind: "solid", color: p.font.background, opacity: 1 }
|
|
922
|
+
});
|
|
923
|
+
}
|
|
908
924
|
for (const glyph of line.glyphs) {
|
|
909
925
|
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
910
926
|
if (!path || path === "M 0 0") {
|
|
@@ -1018,6 +1034,7 @@ async function buildDrawOps(p) {
|
|
|
1018
1034
|
});
|
|
1019
1035
|
}
|
|
1020
1036
|
}
|
|
1037
|
+
ops.push(...highlighterOps);
|
|
1021
1038
|
ops.push(...textOps);
|
|
1022
1039
|
return ops;
|
|
1023
1040
|
}
|
|
@@ -1145,7 +1162,9 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1145
1162
|
const wordSegments = getWordSegments(lines);
|
|
1146
1163
|
const totalWords = wordSegments.length;
|
|
1147
1164
|
const visibleWords = Math.floor(progress * totalWords);
|
|
1148
|
-
if (visibleWords === 0)
|
|
1165
|
+
if (visibleWords === 0) {
|
|
1166
|
+
return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
|
|
1167
|
+
}
|
|
1149
1168
|
let totalVisibleGlyphs = 0;
|
|
1150
1169
|
for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
|
|
1151
1170
|
totalVisibleGlyphs += wordSegments[i].glyphCount;
|
|
@@ -1159,7 +1178,9 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1159
1178
|
} else {
|
|
1160
1179
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1161
1180
|
const visibleGlyphs = Math.floor(progress * totalGlyphs);
|
|
1162
|
-
if (visibleGlyphs === 0)
|
|
1181
|
+
if (visibleGlyphs === 0) {
|
|
1182
|
+
return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
|
|
1183
|
+
}
|
|
1163
1184
|
const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
|
|
1164
1185
|
const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
1165
1186
|
if (progress < 1 && visibleGlyphs > 0) {
|
|
@@ -1175,8 +1196,10 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
|
|
|
1175
1196
|
const result = [];
|
|
1176
1197
|
let glyphIndex = 0;
|
|
1177
1198
|
for (const op of ops) {
|
|
1178
|
-
if (op.op === "BeginFrame") {
|
|
1199
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1179
1200
|
result.push(op);
|
|
1201
|
+
}
|
|
1202
|
+
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1180
1203
|
break;
|
|
1181
1204
|
}
|
|
1182
1205
|
}
|
|
@@ -1235,8 +1258,10 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1235
1258
|
if (totalUnits === 0) return ops;
|
|
1236
1259
|
const result = [];
|
|
1237
1260
|
for (const op of ops) {
|
|
1238
|
-
if (op.op === "BeginFrame") {
|
|
1261
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1239
1262
|
result.push(op);
|
|
1263
|
+
}
|
|
1264
|
+
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1240
1265
|
break;
|
|
1241
1266
|
}
|
|
1242
1267
|
}
|
|
@@ -1510,7 +1535,7 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
1510
1535
|
let glyphCount = 0;
|
|
1511
1536
|
let foundGlyphs = false;
|
|
1512
1537
|
for (const op of ops) {
|
|
1513
|
-
if (op.op === "BeginFrame") {
|
|
1538
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1514
1539
|
result.push(op);
|
|
1515
1540
|
continue;
|
|
1516
1541
|
}
|
|
@@ -1538,9 +1563,11 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
1538
1563
|
return result;
|
|
1539
1564
|
}
|
|
1540
1565
|
function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
1541
|
-
|
|
1566
|
+
if (glyphCount === 0) return ops;
|
|
1567
|
+
const blinkRate = 1;
|
|
1542
1568
|
const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
|
|
1543
|
-
|
|
1569
|
+
const alwaysShowCursor = true;
|
|
1570
|
+
if (!alwaysShowCursor && !cursorVisible) return ops;
|
|
1544
1571
|
let last = null;
|
|
1545
1572
|
let count = 0;
|
|
1546
1573
|
for (const op of ops) {
|
|
@@ -1556,13 +1583,16 @@ function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
|
1556
1583
|
const color = getTextColorFromOps(ops);
|
|
1557
1584
|
const cursorX = last.x + fontSize * 0.5;
|
|
1558
1585
|
const cursorY = last.y;
|
|
1586
|
+
const cursorWidth = Math.max(3, fontSize / 15);
|
|
1559
1587
|
const cursorOp = {
|
|
1560
1588
|
op: "DecorationLine",
|
|
1561
|
-
from: { x: cursorX, y: cursorY - fontSize * 0.
|
|
1562
|
-
|
|
1563
|
-
|
|
1589
|
+
from: { x: cursorX, y: cursorY - fontSize * 0.75 },
|
|
1590
|
+
// Slightly taller
|
|
1591
|
+
to: { x: cursorX, y: cursorY + fontSize * 0.15 },
|
|
1592
|
+
width: cursorWidth,
|
|
1564
1593
|
color,
|
|
1565
|
-
opacity: 1
|
|
1594
|
+
opacity: alwaysShowCursor ? 1 : cursorVisible ? 1 : 0
|
|
1595
|
+
// Always visible or blink
|
|
1566
1596
|
};
|
|
1567
1597
|
return [...ops, cursorOp];
|
|
1568
1598
|
}
|
|
@@ -2288,16 +2318,29 @@ var VideoGenerator = class {
|
|
|
2288
2318
|
});
|
|
2289
2319
|
try {
|
|
2290
2320
|
const painter = await createNodePainter({ width, height, pixelRatio });
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
const
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2321
|
+
const CHUNK_SIZE = 30;
|
|
2322
|
+
for (let chunkStart = 0; chunkStart < totalFrames; chunkStart += CHUNK_SIZE) {
|
|
2323
|
+
const chunkEnd = Math.min(chunkStart + CHUNK_SIZE, totalFrames);
|
|
2324
|
+
const chunkSize = chunkEnd - chunkStart;
|
|
2325
|
+
const chunkOps = [];
|
|
2326
|
+
for (let frame = chunkStart; frame < chunkEnd; frame++) {
|
|
2327
|
+
const t = frame / (totalFrames - 1) * duration;
|
|
2328
|
+
chunkOps.push(await frameGenerator(t));
|
|
2329
|
+
}
|
|
2330
|
+
for (let i = 0; i < chunkSize; i++) {
|
|
2331
|
+
const frame = chunkStart + i;
|
|
2332
|
+
await painter.render(chunkOps[i]);
|
|
2333
|
+
const png = await painter.toPNG();
|
|
2334
|
+
const ok = ffmpeg.stdin.write(png);
|
|
2335
|
+
if (!ok) await new Promise((r) => ffmpeg.stdin.once("drain", r));
|
|
2336
|
+
if (frame % Math.max(1, Math.floor(fps / 2)) === 0) {
|
|
2337
|
+
const pct = Math.round((frame + 1) / totalFrames * 100);
|
|
2338
|
+
console.log(`\u{1F39E}\uFE0F Rendering frames: ${pct}%`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
chunkOps.length = 0;
|
|
2342
|
+
if (global.gc && chunkEnd < totalFrames) {
|
|
2343
|
+
global.gc();
|
|
2301
2344
|
}
|
|
2302
2345
|
}
|
|
2303
2346
|
ffmpeg.stdin.end();
|
|
@@ -2452,7 +2495,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2452
2495
|
size: main.size,
|
|
2453
2496
|
weight: `${main.weight}`,
|
|
2454
2497
|
color: main.color,
|
|
2455
|
-
opacity: main.opacity
|
|
2498
|
+
opacity: main.opacity,
|
|
2499
|
+
background: main.background
|
|
2456
2500
|
},
|
|
2457
2501
|
style: {
|
|
2458
2502
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
package/dist/entry.node.d.cts
CHANGED
package/dist/entry.node.d.ts
CHANGED
package/dist/entry.node.js
CHANGED
|
@@ -62,7 +62,8 @@ var fontSchema = Joi.object({
|
|
|
62
62
|
size: Joi.number().min(CANVAS_CONFIG.LIMITS.minFontSize).max(CANVAS_CONFIG.LIMITS.maxFontSize).default(CANVAS_CONFIG.DEFAULTS.fontSize),
|
|
63
63
|
weight: Joi.alternatives().try(Joi.string(), Joi.number()).default("400"),
|
|
64
64
|
color: Joi.string().pattern(HEX6).default(CANVAS_CONFIG.DEFAULTS.color),
|
|
65
|
-
opacity: Joi.number().min(0).max(1).default(1)
|
|
65
|
+
opacity: Joi.number().min(0).max(1).default(1),
|
|
66
|
+
background: Joi.string().pattern(HEX6).optional()
|
|
66
67
|
}).unknown(false);
|
|
67
68
|
var styleSchema = Joi.object({
|
|
68
69
|
letterSpacing: Joi.number().default(0),
|
|
@@ -815,7 +816,7 @@ function normalizePadding(padding) {
|
|
|
815
816
|
async function buildDrawOps(p) {
|
|
816
817
|
const ops = [];
|
|
817
818
|
const padding = normalizePadding(p.padding);
|
|
818
|
-
const borderWidth = p.
|
|
819
|
+
const borderWidth = p.border?.width ?? 0;
|
|
819
820
|
ops.push({
|
|
820
821
|
op: "BeginFrame",
|
|
821
822
|
width: p.canvas.width,
|
|
@@ -848,6 +849,7 @@ async function buildDrawOps(p) {
|
|
|
848
849
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
849
850
|
const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
|
|
850
851
|
const textOps = [];
|
|
852
|
+
const highlighterOps = [];
|
|
851
853
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
852
854
|
for (const line of p.lines) {
|
|
853
855
|
let lineX;
|
|
@@ -866,6 +868,20 @@ async function buildDrawOps(p) {
|
|
|
866
868
|
let xCursor = lineX;
|
|
867
869
|
const lineIndex = p.lines.indexOf(line);
|
|
868
870
|
const baselineY = blockY + lineIndex * lineHeightPx;
|
|
871
|
+
if (p.font.background) {
|
|
872
|
+
const highlightPadding = p.font.size * 0.15;
|
|
873
|
+
const verticalPadding = p.font.size * 0.12;
|
|
874
|
+
const highlightHeight = p.font.size * 0.92 + verticalPadding * 2;
|
|
875
|
+
const highlightY = baselineY - p.font.size * 0.78 - verticalPadding;
|
|
876
|
+
highlighterOps.push({
|
|
877
|
+
op: "Rectangle",
|
|
878
|
+
x: lineX - highlightPadding,
|
|
879
|
+
y: highlightY,
|
|
880
|
+
width: line.width + highlightPadding * 2,
|
|
881
|
+
height: highlightHeight,
|
|
882
|
+
fill: { kind: "solid", color: p.font.background, opacity: 1 }
|
|
883
|
+
});
|
|
884
|
+
}
|
|
869
885
|
for (const glyph of line.glyphs) {
|
|
870
886
|
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
871
887
|
if (!path || path === "M 0 0") {
|
|
@@ -979,6 +995,7 @@ async function buildDrawOps(p) {
|
|
|
979
995
|
});
|
|
980
996
|
}
|
|
981
997
|
}
|
|
998
|
+
ops.push(...highlighterOps);
|
|
982
999
|
ops.push(...textOps);
|
|
983
1000
|
return ops;
|
|
984
1001
|
}
|
|
@@ -1106,7 +1123,9 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1106
1123
|
const wordSegments = getWordSegments(lines);
|
|
1107
1124
|
const totalWords = wordSegments.length;
|
|
1108
1125
|
const visibleWords = Math.floor(progress * totalWords);
|
|
1109
|
-
if (visibleWords === 0)
|
|
1126
|
+
if (visibleWords === 0) {
|
|
1127
|
+
return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
|
|
1128
|
+
}
|
|
1110
1129
|
let totalVisibleGlyphs = 0;
|
|
1111
1130
|
for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
|
|
1112
1131
|
totalVisibleGlyphs += wordSegments[i].glyphCount;
|
|
@@ -1120,7 +1139,9 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1120
1139
|
} else {
|
|
1121
1140
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1122
1141
|
const visibleGlyphs = Math.floor(progress * totalGlyphs);
|
|
1123
|
-
if (visibleGlyphs === 0)
|
|
1142
|
+
if (visibleGlyphs === 0) {
|
|
1143
|
+
return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
|
|
1144
|
+
}
|
|
1124
1145
|
const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
|
|
1125
1146
|
const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
1126
1147
|
if (progress < 1 && visibleGlyphs > 0) {
|
|
@@ -1136,8 +1157,10 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
|
|
|
1136
1157
|
const result = [];
|
|
1137
1158
|
let glyphIndex = 0;
|
|
1138
1159
|
for (const op of ops) {
|
|
1139
|
-
if (op.op === "BeginFrame") {
|
|
1160
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1140
1161
|
result.push(op);
|
|
1162
|
+
}
|
|
1163
|
+
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1141
1164
|
break;
|
|
1142
1165
|
}
|
|
1143
1166
|
}
|
|
@@ -1196,8 +1219,10 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1196
1219
|
if (totalUnits === 0) return ops;
|
|
1197
1220
|
const result = [];
|
|
1198
1221
|
for (const op of ops) {
|
|
1199
|
-
if (op.op === "BeginFrame") {
|
|
1222
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1200
1223
|
result.push(op);
|
|
1224
|
+
}
|
|
1225
|
+
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1201
1226
|
break;
|
|
1202
1227
|
}
|
|
1203
1228
|
}
|
|
@@ -1471,7 +1496,7 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
1471
1496
|
let glyphCount = 0;
|
|
1472
1497
|
let foundGlyphs = false;
|
|
1473
1498
|
for (const op of ops) {
|
|
1474
|
-
if (op.op === "BeginFrame") {
|
|
1499
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1475
1500
|
result.push(op);
|
|
1476
1501
|
continue;
|
|
1477
1502
|
}
|
|
@@ -1499,9 +1524,11 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
1499
1524
|
return result;
|
|
1500
1525
|
}
|
|
1501
1526
|
function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
1502
|
-
|
|
1527
|
+
if (glyphCount === 0) return ops;
|
|
1528
|
+
const blinkRate = 1;
|
|
1503
1529
|
const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
|
|
1504
|
-
|
|
1530
|
+
const alwaysShowCursor = true;
|
|
1531
|
+
if (!alwaysShowCursor && !cursorVisible) return ops;
|
|
1505
1532
|
let last = null;
|
|
1506
1533
|
let count = 0;
|
|
1507
1534
|
for (const op of ops) {
|
|
@@ -1517,13 +1544,16 @@ function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
|
1517
1544
|
const color = getTextColorFromOps(ops);
|
|
1518
1545
|
const cursorX = last.x + fontSize * 0.5;
|
|
1519
1546
|
const cursorY = last.y;
|
|
1547
|
+
const cursorWidth = Math.max(3, fontSize / 15);
|
|
1520
1548
|
const cursorOp = {
|
|
1521
1549
|
op: "DecorationLine",
|
|
1522
|
-
from: { x: cursorX, y: cursorY - fontSize * 0.
|
|
1523
|
-
|
|
1524
|
-
|
|
1550
|
+
from: { x: cursorX, y: cursorY - fontSize * 0.75 },
|
|
1551
|
+
// Slightly taller
|
|
1552
|
+
to: { x: cursorX, y: cursorY + fontSize * 0.15 },
|
|
1553
|
+
width: cursorWidth,
|
|
1525
1554
|
color,
|
|
1526
|
-
opacity: 1
|
|
1555
|
+
opacity: alwaysShowCursor ? 1 : cursorVisible ? 1 : 0
|
|
1556
|
+
// Always visible or blink
|
|
1527
1557
|
};
|
|
1528
1558
|
return [...ops, cursorOp];
|
|
1529
1559
|
}
|
|
@@ -2249,16 +2279,29 @@ var VideoGenerator = class {
|
|
|
2249
2279
|
});
|
|
2250
2280
|
try {
|
|
2251
2281
|
const painter = await createNodePainter({ width, height, pixelRatio });
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2282
|
+
const CHUNK_SIZE = 30;
|
|
2283
|
+
for (let chunkStart = 0; chunkStart < totalFrames; chunkStart += CHUNK_SIZE) {
|
|
2284
|
+
const chunkEnd = Math.min(chunkStart + CHUNK_SIZE, totalFrames);
|
|
2285
|
+
const chunkSize = chunkEnd - chunkStart;
|
|
2286
|
+
const chunkOps = [];
|
|
2287
|
+
for (let frame = chunkStart; frame < chunkEnd; frame++) {
|
|
2288
|
+
const t = frame / (totalFrames - 1) * duration;
|
|
2289
|
+
chunkOps.push(await frameGenerator(t));
|
|
2290
|
+
}
|
|
2291
|
+
for (let i = 0; i < chunkSize; i++) {
|
|
2292
|
+
const frame = chunkStart + i;
|
|
2293
|
+
await painter.render(chunkOps[i]);
|
|
2294
|
+
const png = await painter.toPNG();
|
|
2295
|
+
const ok = ffmpeg.stdin.write(png);
|
|
2296
|
+
if (!ok) await new Promise((r) => ffmpeg.stdin.once("drain", r));
|
|
2297
|
+
if (frame % Math.max(1, Math.floor(fps / 2)) === 0) {
|
|
2298
|
+
const pct = Math.round((frame + 1) / totalFrames * 100);
|
|
2299
|
+
console.log(`\u{1F39E}\uFE0F Rendering frames: ${pct}%`);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
chunkOps.length = 0;
|
|
2303
|
+
if (global.gc && chunkEnd < totalFrames) {
|
|
2304
|
+
global.gc();
|
|
2262
2305
|
}
|
|
2263
2306
|
}
|
|
2264
2307
|
ffmpeg.stdin.end();
|
|
@@ -2413,7 +2456,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2413
2456
|
size: main.size,
|
|
2414
2457
|
weight: `${main.weight}`,
|
|
2415
2458
|
color: main.color,
|
|
2416
|
-
opacity: main.opacity
|
|
2459
|
+
opacity: main.opacity,
|
|
2460
|
+
background: main.background
|
|
2417
2461
|
},
|
|
2418
2462
|
style: {
|
|
2419
2463
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
package/dist/entry.web.d.ts
CHANGED
package/dist/entry.web.js
CHANGED
|
@@ -66,7 +66,8 @@ var fontSchema = Joi.object({
|
|
|
66
66
|
size: Joi.number().min(CANVAS_CONFIG.LIMITS.minFontSize).max(CANVAS_CONFIG.LIMITS.maxFontSize).default(CANVAS_CONFIG.DEFAULTS.fontSize),
|
|
67
67
|
weight: Joi.alternatives().try(Joi.string(), Joi.number()).default("400"),
|
|
68
68
|
color: Joi.string().pattern(HEX6).default(CANVAS_CONFIG.DEFAULTS.color),
|
|
69
|
-
opacity: Joi.number().min(0).max(1).default(1)
|
|
69
|
+
opacity: Joi.number().min(0).max(1).default(1),
|
|
70
|
+
background: Joi.string().pattern(HEX6).optional()
|
|
70
71
|
}).unknown(false);
|
|
71
72
|
var styleSchema = Joi.object({
|
|
72
73
|
letterSpacing: Joi.number().default(0),
|
|
@@ -820,7 +821,7 @@ function normalizePadding(padding) {
|
|
|
820
821
|
async function buildDrawOps(p) {
|
|
821
822
|
const ops = [];
|
|
822
823
|
const padding = normalizePadding(p.padding);
|
|
823
|
-
const borderWidth = p.
|
|
824
|
+
const borderWidth = p.border?.width ?? 0;
|
|
824
825
|
ops.push({
|
|
825
826
|
op: "BeginFrame",
|
|
826
827
|
width: p.canvas.width,
|
|
@@ -853,6 +854,7 @@ async function buildDrawOps(p) {
|
|
|
853
854
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
854
855
|
const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
|
|
855
856
|
const textOps = [];
|
|
857
|
+
const highlighterOps = [];
|
|
856
858
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
857
859
|
for (const line of p.lines) {
|
|
858
860
|
let lineX;
|
|
@@ -871,6 +873,20 @@ async function buildDrawOps(p) {
|
|
|
871
873
|
let xCursor = lineX;
|
|
872
874
|
const lineIndex = p.lines.indexOf(line);
|
|
873
875
|
const baselineY = blockY + lineIndex * lineHeightPx;
|
|
876
|
+
if (p.font.background) {
|
|
877
|
+
const highlightPadding = p.font.size * 0.15;
|
|
878
|
+
const verticalPadding = p.font.size * 0.12;
|
|
879
|
+
const highlightHeight = p.font.size * 0.92 + verticalPadding * 2;
|
|
880
|
+
const highlightY = baselineY - p.font.size * 0.78 - verticalPadding;
|
|
881
|
+
highlighterOps.push({
|
|
882
|
+
op: "Rectangle",
|
|
883
|
+
x: lineX - highlightPadding,
|
|
884
|
+
y: highlightY,
|
|
885
|
+
width: line.width + highlightPadding * 2,
|
|
886
|
+
height: highlightHeight,
|
|
887
|
+
fill: { kind: "solid", color: p.font.background, opacity: 1 }
|
|
888
|
+
});
|
|
889
|
+
}
|
|
874
890
|
for (const glyph of line.glyphs) {
|
|
875
891
|
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
876
892
|
if (!path || path === "M 0 0") {
|
|
@@ -984,6 +1000,7 @@ async function buildDrawOps(p) {
|
|
|
984
1000
|
});
|
|
985
1001
|
}
|
|
986
1002
|
}
|
|
1003
|
+
ops.push(...highlighterOps);
|
|
987
1004
|
ops.push(...textOps);
|
|
988
1005
|
return ops;
|
|
989
1006
|
}
|
|
@@ -1111,7 +1128,9 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1111
1128
|
const wordSegments = getWordSegments(lines);
|
|
1112
1129
|
const totalWords = wordSegments.length;
|
|
1113
1130
|
const visibleWords = Math.floor(progress * totalWords);
|
|
1114
|
-
if (visibleWords === 0)
|
|
1131
|
+
if (visibleWords === 0) {
|
|
1132
|
+
return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
|
|
1133
|
+
}
|
|
1115
1134
|
let totalVisibleGlyphs = 0;
|
|
1116
1135
|
for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
|
|
1117
1136
|
totalVisibleGlyphs += wordSegments[i].glyphCount;
|
|
@@ -1125,7 +1144,9 @@ function applyTypewriterAnimation(ops, lines, progress, style, fontSize, time, d
|
|
|
1125
1144
|
} else {
|
|
1126
1145
|
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
1127
1146
|
const visibleGlyphs = Math.floor(progress * totalGlyphs);
|
|
1128
|
-
if (visibleGlyphs === 0)
|
|
1147
|
+
if (visibleGlyphs === 0) {
|
|
1148
|
+
return ops.filter((x) => x.op === "BeginFrame" || x.op === "Rectangle" || x.op === "RectangleStroke");
|
|
1149
|
+
}
|
|
1129
1150
|
const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
|
|
1130
1151
|
const visibleOps = progress >= DECORATION_DONE_THRESHOLD ? visibleOpsRaw : visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
1131
1152
|
if (progress < 1 && visibleGlyphs > 0) {
|
|
@@ -1141,8 +1162,10 @@ function applyAscendAnimation(ops, lines, progress, direction, fontSize, duratio
|
|
|
1141
1162
|
const result = [];
|
|
1142
1163
|
let glyphIndex = 0;
|
|
1143
1164
|
for (const op of ops) {
|
|
1144
|
-
if (op.op === "BeginFrame") {
|
|
1165
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1145
1166
|
result.push(op);
|
|
1167
|
+
}
|
|
1168
|
+
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1146
1169
|
break;
|
|
1147
1170
|
}
|
|
1148
1171
|
}
|
|
@@ -1201,8 +1224,10 @@ function applyShiftAnimation(ops, lines, progress, direction, fontSize, style, d
|
|
|
1201
1224
|
if (totalUnits === 0) return ops;
|
|
1202
1225
|
const result = [];
|
|
1203
1226
|
for (const op of ops) {
|
|
1204
|
-
if (op.op === "BeginFrame") {
|
|
1227
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1205
1228
|
result.push(op);
|
|
1229
|
+
}
|
|
1230
|
+
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
1206
1231
|
break;
|
|
1207
1232
|
}
|
|
1208
1233
|
}
|
|
@@ -1476,7 +1501,7 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
1476
1501
|
let glyphCount = 0;
|
|
1477
1502
|
let foundGlyphs = false;
|
|
1478
1503
|
for (const op of ops) {
|
|
1479
|
-
if (op.op === "BeginFrame") {
|
|
1504
|
+
if (op.op === "BeginFrame" || op.op === "Rectangle" || op.op === "RectangleStroke") {
|
|
1480
1505
|
result.push(op);
|
|
1481
1506
|
continue;
|
|
1482
1507
|
}
|
|
@@ -1504,9 +1529,11 @@ function sliceGlyphOps(ops, maxGlyphs) {
|
|
|
1504
1529
|
return result;
|
|
1505
1530
|
}
|
|
1506
1531
|
function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
1507
|
-
|
|
1532
|
+
if (glyphCount === 0) return ops;
|
|
1533
|
+
const blinkRate = 1;
|
|
1508
1534
|
const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
|
|
1509
|
-
|
|
1535
|
+
const alwaysShowCursor = true;
|
|
1536
|
+
if (!alwaysShowCursor && !cursorVisible) return ops;
|
|
1510
1537
|
let last = null;
|
|
1511
1538
|
let count = 0;
|
|
1512
1539
|
for (const op of ops) {
|
|
@@ -1522,13 +1549,16 @@ function addTypewriterCursor(ops, glyphCount, fontSize, time) {
|
|
|
1522
1549
|
const color = getTextColorFromOps(ops);
|
|
1523
1550
|
const cursorX = last.x + fontSize * 0.5;
|
|
1524
1551
|
const cursorY = last.y;
|
|
1552
|
+
const cursorWidth = Math.max(3, fontSize / 15);
|
|
1525
1553
|
const cursorOp = {
|
|
1526
1554
|
op: "DecorationLine",
|
|
1527
|
-
from: { x: cursorX, y: cursorY - fontSize * 0.
|
|
1528
|
-
|
|
1529
|
-
|
|
1555
|
+
from: { x: cursorX, y: cursorY - fontSize * 0.75 },
|
|
1556
|
+
// Slightly taller
|
|
1557
|
+
to: { x: cursorX, y: cursorY + fontSize * 0.15 },
|
|
1558
|
+
width: cursorWidth,
|
|
1530
1559
|
color,
|
|
1531
|
-
opacity: 1
|
|
1560
|
+
opacity: alwaysShowCursor ? 1 : cursorVisible ? 1 : 0
|
|
1561
|
+
// Always visible or blink
|
|
1532
1562
|
};
|
|
1533
1563
|
return [...ops, cursorOp];
|
|
1534
1564
|
}
|
|
@@ -2137,7 +2167,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2137
2167
|
size: main.size,
|
|
2138
2168
|
weight: `${main.weight}`,
|
|
2139
2169
|
color: main.color,
|
|
2140
|
-
opacity: main.opacity
|
|
2170
|
+
opacity: main.opacity,
|
|
2171
|
+
background: main.background
|
|
2141
2172
|
},
|
|
2142
2173
|
style: {
|
|
2143
2174
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
package/package.json
CHANGED