@shotstack/shotstack-canvas 1.4.7 → 1.5.1
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 +121 -20
- package/dist/entry.node.d.cts +31 -2
- package/dist/entry.node.d.ts +31 -2
- package/dist/entry.node.js +121 -20
- package/dist/entry.web.d.ts +31 -2
- package/dist/entry.web.js +124 -19
- package/package.json +62 -62
package/dist/entry.node.cjs
CHANGED
|
@@ -132,11 +132,26 @@ var animationSchema = import_joi.default.object({
|
|
|
132
132
|
otherwise: import_joi.default.forbidden()
|
|
133
133
|
})
|
|
134
134
|
}).unknown(false);
|
|
135
|
+
var borderSchema = import_joi.default.object({
|
|
136
|
+
width: import_joi.default.number().min(0).default(0),
|
|
137
|
+
color: import_joi.default.string().pattern(HEX6).default("#000000"),
|
|
138
|
+
opacity: import_joi.default.number().min(0).max(1).default(1)
|
|
139
|
+
}).unknown(false);
|
|
135
140
|
var backgroundSchema = import_joi.default.object({
|
|
136
141
|
color: import_joi.default.string().pattern(HEX6).optional(),
|
|
137
142
|
opacity: import_joi.default.number().min(0).max(1).default(1),
|
|
138
|
-
borderRadius: import_joi.default.number().min(0).default(0)
|
|
143
|
+
borderRadius: import_joi.default.number().min(0).default(0),
|
|
144
|
+
border: borderSchema.optional()
|
|
139
145
|
}).unknown(false);
|
|
146
|
+
var paddingSchema = import_joi.default.alternatives().try(
|
|
147
|
+
import_joi.default.number().min(0).default(0),
|
|
148
|
+
import_joi.default.object({
|
|
149
|
+
top: import_joi.default.number().min(0).default(0),
|
|
150
|
+
right: import_joi.default.number().min(0).default(0),
|
|
151
|
+
bottom: import_joi.default.number().min(0).default(0),
|
|
152
|
+
left: import_joi.default.number().min(0).default(0)
|
|
153
|
+
}).unknown(false)
|
|
154
|
+
);
|
|
140
155
|
var customFontSchema = import_joi.default.object({
|
|
141
156
|
src: import_joi.default.string().uri().required(),
|
|
142
157
|
family: import_joi.default.string().required(),
|
|
@@ -154,11 +169,10 @@ var RichTextAssetSchema = import_joi.default.object({
|
|
|
154
169
|
stroke: strokeSchema.optional(),
|
|
155
170
|
shadow: shadowSchema.optional(),
|
|
156
171
|
background: backgroundSchema.optional(),
|
|
172
|
+
padding: paddingSchema.optional(),
|
|
157
173
|
align: alignmentSchema.optional(),
|
|
158
174
|
animation: animationSchema.optional(),
|
|
159
|
-
customFonts: import_joi.default.array().items(customFontSchema).optional()
|
|
160
|
-
cacheEnabled: import_joi.default.boolean().default(true),
|
|
161
|
-
pixelRatio: import_joi.default.number().min(1).max(3).default(CANVAS_CONFIG.DEFAULTS.pixelRatio)
|
|
175
|
+
customFonts: import_joi.default.array().items(customFontSchema).optional()
|
|
162
176
|
}).unknown(false);
|
|
163
177
|
|
|
164
178
|
// src/wasm/hb-loader.ts
|
|
@@ -828,15 +842,27 @@ function decorationGeometry(kind, p) {
|
|
|
828
842
|
}
|
|
829
843
|
|
|
830
844
|
// src/core/drawops.ts
|
|
845
|
+
function normalizePadding(padding) {
|
|
846
|
+
if (padding === void 0 || padding === null) {
|
|
847
|
+
return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
848
|
+
}
|
|
849
|
+
if (typeof padding === "number") {
|
|
850
|
+
return { top: padding, right: padding, bottom: padding, left: padding };
|
|
851
|
+
}
|
|
852
|
+
return padding;
|
|
853
|
+
}
|
|
831
854
|
async function buildDrawOps(p) {
|
|
832
855
|
const ops = [];
|
|
856
|
+
const padding = normalizePadding(p.padding);
|
|
857
|
+
const borderWidth = p.background?.border?.width ?? 0;
|
|
833
858
|
ops.push({
|
|
834
859
|
op: "BeginFrame",
|
|
835
860
|
width: p.canvas.width,
|
|
836
861
|
height: p.canvas.height,
|
|
837
862
|
pixelRatio: p.canvas.pixelRatio,
|
|
838
863
|
clear: true,
|
|
839
|
-
bg:
|
|
864
|
+
bg: void 0
|
|
865
|
+
// Background will be drawn as a separate layer with proper padding/border
|
|
840
866
|
});
|
|
841
867
|
if (p.lines.length === 0) return ops;
|
|
842
868
|
const upem = Math.max(1, await p.getUnitsPerEm());
|
|
@@ -846,33 +872,34 @@ async function buildDrawOps(p) {
|
|
|
846
872
|
let blockY;
|
|
847
873
|
switch (p.align.vertical) {
|
|
848
874
|
case "top":
|
|
849
|
-
blockY = p.font.size;
|
|
875
|
+
blockY = p.font.size + padding.top;
|
|
850
876
|
break;
|
|
851
877
|
case "bottom":
|
|
852
|
-
blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
|
|
878
|
+
blockY = p.textRect.height - (numLines - 1) * lineHeightPx + padding.top;
|
|
853
879
|
break;
|
|
854
880
|
case "middle":
|
|
855
881
|
default:
|
|
856
882
|
const capHeightRatio = 0.35;
|
|
857
883
|
const visualOffset = p.font.size * capHeightRatio;
|
|
858
|
-
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
|
|
884
|
+
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset + padding.top;
|
|
859
885
|
break;
|
|
860
886
|
}
|
|
861
887
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
862
888
|
const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
|
|
889
|
+
const textOps = [];
|
|
863
890
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
864
891
|
for (const line of p.lines) {
|
|
865
892
|
let lineX;
|
|
866
893
|
switch (p.align.horizontal) {
|
|
867
894
|
case "left":
|
|
868
|
-
lineX =
|
|
895
|
+
lineX = padding.left;
|
|
869
896
|
break;
|
|
870
897
|
case "right":
|
|
871
|
-
lineX = p.textRect.width - line.width;
|
|
898
|
+
lineX = p.textRect.width - line.width + padding.left;
|
|
872
899
|
break;
|
|
873
900
|
case "center":
|
|
874
901
|
default:
|
|
875
|
-
lineX = (p.textRect.width - line.width) / 2;
|
|
902
|
+
lineX = (p.textRect.width - line.width) / 2 + padding.left;
|
|
876
903
|
break;
|
|
877
904
|
}
|
|
878
905
|
let xCursor = lineX;
|
|
@@ -896,7 +923,7 @@ async function buildDrawOps(p) {
|
|
|
896
923
|
if (x2 > gMaxX) gMaxX = x2;
|
|
897
924
|
if (y2 > gMaxY) gMaxY = y2;
|
|
898
925
|
if (p.shadow && p.shadow.blur > 0) {
|
|
899
|
-
|
|
926
|
+
textOps.push({
|
|
900
927
|
isShadow: true,
|
|
901
928
|
op: "FillPath",
|
|
902
929
|
path,
|
|
@@ -907,7 +934,7 @@ async function buildDrawOps(p) {
|
|
|
907
934
|
});
|
|
908
935
|
}
|
|
909
936
|
if (p.stroke && p.stroke.width > 0) {
|
|
910
|
-
|
|
937
|
+
textOps.push({
|
|
911
938
|
op: "StrokePath",
|
|
912
939
|
path,
|
|
913
940
|
x: glyphX,
|
|
@@ -918,7 +945,7 @@ async function buildDrawOps(p) {
|
|
|
918
945
|
opacity: p.stroke.opacity
|
|
919
946
|
});
|
|
920
947
|
}
|
|
921
|
-
|
|
948
|
+
textOps.push({
|
|
922
949
|
op: "FillPath",
|
|
923
950
|
path,
|
|
924
951
|
x: glyphX,
|
|
@@ -935,7 +962,7 @@ async function buildDrawOps(p) {
|
|
|
935
962
|
lineWidth: line.width,
|
|
936
963
|
xStart: lineX
|
|
937
964
|
});
|
|
938
|
-
|
|
965
|
+
textOps.push({
|
|
939
966
|
op: "DecorationLine",
|
|
940
967
|
from: { x: deco.x1, y: deco.y },
|
|
941
968
|
to: { x: deco.x2, y: deco.y },
|
|
@@ -947,12 +974,46 @@ async function buildDrawOps(p) {
|
|
|
947
974
|
}
|
|
948
975
|
if (gMinX !== Infinity) {
|
|
949
976
|
const gbox = { x: gMinX, y: gMinY, w: Math.max(1, gMaxX - gMinX), h: Math.max(1, gMaxY - gMinY) };
|
|
950
|
-
for (const op of
|
|
977
|
+
for (const op of textOps) {
|
|
951
978
|
if (op.op === "FillPath" && !op.isShadow) {
|
|
952
979
|
op.gradientBBox = gbox;
|
|
953
980
|
}
|
|
954
981
|
}
|
|
955
982
|
}
|
|
983
|
+
if (p.background && (p.background.color || p.background.border)) {
|
|
984
|
+
const bgX = 0;
|
|
985
|
+
const bgY = 0;
|
|
986
|
+
const bgWidth = p.canvas.width;
|
|
987
|
+
const bgHeight = p.canvas.height;
|
|
988
|
+
if (p.background.color) {
|
|
989
|
+
ops.push({
|
|
990
|
+
op: "Rectangle",
|
|
991
|
+
x: bgX,
|
|
992
|
+
y: bgY,
|
|
993
|
+
width: bgWidth,
|
|
994
|
+
height: bgHeight,
|
|
995
|
+
fill: { kind: "solid", color: p.background.color, opacity: p.background.opacity },
|
|
996
|
+
borderRadius: p.background.borderRadius
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
if (p.background.border && p.background.border.width > 0) {
|
|
1000
|
+
const halfBorder = p.background.border.width / 2;
|
|
1001
|
+
ops.push({
|
|
1002
|
+
op: "RectangleStroke",
|
|
1003
|
+
x: bgX + halfBorder,
|
|
1004
|
+
y: bgY + halfBorder,
|
|
1005
|
+
width: bgWidth - p.background.border.width,
|
|
1006
|
+
height: bgHeight - p.background.border.width,
|
|
1007
|
+
stroke: {
|
|
1008
|
+
width: p.background.border.width,
|
|
1009
|
+
color: p.background.border.color,
|
|
1010
|
+
opacity: p.background.border.opacity
|
|
1011
|
+
},
|
|
1012
|
+
borderRadius: Math.max(0, p.background.borderRadius - halfBorder)
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
ops.push(...textOps);
|
|
956
1017
|
return ops;
|
|
957
1018
|
}
|
|
958
1019
|
function tokenizePath(d) {
|
|
@@ -1762,6 +1823,44 @@ async function createNodePainter(opts) {
|
|
|
1762
1823
|
});
|
|
1763
1824
|
continue;
|
|
1764
1825
|
}
|
|
1826
|
+
if (op.op === "Rectangle") {
|
|
1827
|
+
renderToBoth((context) => {
|
|
1828
|
+
context.save();
|
|
1829
|
+
const fill = makeGradientFromBBox(context, op.fill, {
|
|
1830
|
+
x: op.x,
|
|
1831
|
+
y: op.y,
|
|
1832
|
+
w: op.width,
|
|
1833
|
+
h: op.height
|
|
1834
|
+
});
|
|
1835
|
+
context.fillStyle = fill;
|
|
1836
|
+
if (op.borderRadius && op.borderRadius > 0) {
|
|
1837
|
+
context.beginPath();
|
|
1838
|
+
roundRectPath(context, op.x, op.y, op.width, op.height, op.borderRadius);
|
|
1839
|
+
context.fill();
|
|
1840
|
+
} else {
|
|
1841
|
+
context.fillRect(op.x, op.y, op.width, op.height);
|
|
1842
|
+
}
|
|
1843
|
+
context.restore();
|
|
1844
|
+
});
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
if (op.op === "RectangleStroke") {
|
|
1848
|
+
renderToBoth((context) => {
|
|
1849
|
+
context.save();
|
|
1850
|
+
const c = parseHex6(op.stroke.color, op.stroke.opacity);
|
|
1851
|
+
context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1852
|
+
context.lineWidth = op.stroke.width;
|
|
1853
|
+
if (op.borderRadius && op.borderRadius > 0) {
|
|
1854
|
+
context.beginPath();
|
|
1855
|
+
roundRectPath(context, op.x, op.y, op.width, op.height, op.borderRadius);
|
|
1856
|
+
context.stroke();
|
|
1857
|
+
} else {
|
|
1858
|
+
context.strokeRect(op.x, op.y, op.width, op.height);
|
|
1859
|
+
}
|
|
1860
|
+
context.restore();
|
|
1861
|
+
});
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1765
1864
|
}
|
|
1766
1865
|
if (needsAlphaExtraction) {
|
|
1767
1866
|
const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
@@ -2325,15 +2424,16 @@ async function createTextEngine(opts = {}) {
|
|
|
2325
2424
|
`Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
|
|
2326
2425
|
);
|
|
2327
2426
|
}
|
|
2427
|
+
const padding = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2328
2428
|
const textRect = {
|
|
2329
2429
|
x: 0,
|
|
2330
2430
|
y: 0,
|
|
2331
2431
|
width: asset.width ?? width,
|
|
2332
2432
|
height: asset.height ?? height
|
|
2333
2433
|
};
|
|
2334
|
-
const canvasW = asset.width ?? width;
|
|
2335
|
-
const canvasH = asset.height ?? height;
|
|
2336
|
-
const canvasPR =
|
|
2434
|
+
const canvasW = (asset.width ?? width) + padding.left + padding.right;
|
|
2435
|
+
const canvasH = (asset.height ?? height) + padding.top + padding.bottom;
|
|
2436
|
+
const canvasPR = pixelRatio;
|
|
2337
2437
|
let ops0;
|
|
2338
2438
|
try {
|
|
2339
2439
|
ops0 = await buildDrawOps({
|
|
@@ -2359,6 +2459,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2359
2459
|
vertical: asset.align?.vertical ?? "middle"
|
|
2360
2460
|
},
|
|
2361
2461
|
background: asset.background,
|
|
2462
|
+
padding: asset.padding,
|
|
2362
2463
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2363
2464
|
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2364
2465
|
});
|
|
@@ -2421,7 +2522,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2421
2522
|
fps,
|
|
2422
2523
|
duration: options.duration ?? 3,
|
|
2423
2524
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2424
|
-
pixelRatio
|
|
2525
|
+
pixelRatio,
|
|
2425
2526
|
hasAlpha: needsAlpha,
|
|
2426
2527
|
...options
|
|
2427
2528
|
};
|
package/dist/entry.node.d.cts
CHANGED
|
@@ -64,6 +64,17 @@ type RichTextValidated = Required<{
|
|
|
64
64
|
color?: string;
|
|
65
65
|
opacity: number;
|
|
66
66
|
borderRadius: number;
|
|
67
|
+
border?: {
|
|
68
|
+
width: number;
|
|
69
|
+
color: string;
|
|
70
|
+
opacity: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
padding?: number | {
|
|
74
|
+
top: number;
|
|
75
|
+
right: number;
|
|
76
|
+
bottom: number;
|
|
77
|
+
left: number;
|
|
67
78
|
};
|
|
68
79
|
align?: {
|
|
69
80
|
horizontal: "left" | "center" | "right";
|
|
@@ -83,8 +94,6 @@ type RichTextValidated = Required<{
|
|
|
83
94
|
style?: string;
|
|
84
95
|
originalFamily?: string;
|
|
85
96
|
}[];
|
|
86
|
-
cacheEnabled: boolean;
|
|
87
|
-
pixelRatio: number;
|
|
88
97
|
}>;
|
|
89
98
|
|
|
90
99
|
type RGBA = {
|
|
@@ -177,6 +186,26 @@ type DrawOp = {
|
|
|
177
186
|
width: number;
|
|
178
187
|
color: string;
|
|
179
188
|
opacity: number;
|
|
189
|
+
} | {
|
|
190
|
+
op: "Rectangle";
|
|
191
|
+
x: number;
|
|
192
|
+
y: number;
|
|
193
|
+
width: number;
|
|
194
|
+
height: number;
|
|
195
|
+
fill: GradientSpec;
|
|
196
|
+
borderRadius?: number;
|
|
197
|
+
} | {
|
|
198
|
+
op: "RectangleStroke";
|
|
199
|
+
x: number;
|
|
200
|
+
y: number;
|
|
201
|
+
width: number;
|
|
202
|
+
height: number;
|
|
203
|
+
stroke: {
|
|
204
|
+
width: number;
|
|
205
|
+
color: string;
|
|
206
|
+
opacity: number;
|
|
207
|
+
};
|
|
208
|
+
borderRadius?: number;
|
|
180
209
|
};
|
|
181
210
|
type EngineInit = {
|
|
182
211
|
width: number;
|
package/dist/entry.node.d.ts
CHANGED
|
@@ -64,6 +64,17 @@ type RichTextValidated = Required<{
|
|
|
64
64
|
color?: string;
|
|
65
65
|
opacity: number;
|
|
66
66
|
borderRadius: number;
|
|
67
|
+
border?: {
|
|
68
|
+
width: number;
|
|
69
|
+
color: string;
|
|
70
|
+
opacity: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
padding?: number | {
|
|
74
|
+
top: number;
|
|
75
|
+
right: number;
|
|
76
|
+
bottom: number;
|
|
77
|
+
left: number;
|
|
67
78
|
};
|
|
68
79
|
align?: {
|
|
69
80
|
horizontal: "left" | "center" | "right";
|
|
@@ -83,8 +94,6 @@ type RichTextValidated = Required<{
|
|
|
83
94
|
style?: string;
|
|
84
95
|
originalFamily?: string;
|
|
85
96
|
}[];
|
|
86
|
-
cacheEnabled: boolean;
|
|
87
|
-
pixelRatio: number;
|
|
88
97
|
}>;
|
|
89
98
|
|
|
90
99
|
type RGBA = {
|
|
@@ -177,6 +186,26 @@ type DrawOp = {
|
|
|
177
186
|
width: number;
|
|
178
187
|
color: string;
|
|
179
188
|
opacity: number;
|
|
189
|
+
} | {
|
|
190
|
+
op: "Rectangle";
|
|
191
|
+
x: number;
|
|
192
|
+
y: number;
|
|
193
|
+
width: number;
|
|
194
|
+
height: number;
|
|
195
|
+
fill: GradientSpec;
|
|
196
|
+
borderRadius?: number;
|
|
197
|
+
} | {
|
|
198
|
+
op: "RectangleStroke";
|
|
199
|
+
x: number;
|
|
200
|
+
y: number;
|
|
201
|
+
width: number;
|
|
202
|
+
height: number;
|
|
203
|
+
stroke: {
|
|
204
|
+
width: number;
|
|
205
|
+
color: string;
|
|
206
|
+
opacity: number;
|
|
207
|
+
};
|
|
208
|
+
borderRadius?: number;
|
|
180
209
|
};
|
|
181
210
|
type EngineInit = {
|
|
182
211
|
width: number;
|
package/dist/entry.node.js
CHANGED
|
@@ -94,11 +94,26 @@ var animationSchema = Joi.object({
|
|
|
94
94
|
otherwise: Joi.forbidden()
|
|
95
95
|
})
|
|
96
96
|
}).unknown(false);
|
|
97
|
+
var borderSchema = Joi.object({
|
|
98
|
+
width: Joi.number().min(0).default(0),
|
|
99
|
+
color: Joi.string().pattern(HEX6).default("#000000"),
|
|
100
|
+
opacity: Joi.number().min(0).max(1).default(1)
|
|
101
|
+
}).unknown(false);
|
|
97
102
|
var backgroundSchema = Joi.object({
|
|
98
103
|
color: Joi.string().pattern(HEX6).optional(),
|
|
99
104
|
opacity: Joi.number().min(0).max(1).default(1),
|
|
100
|
-
borderRadius: Joi.number().min(0).default(0)
|
|
105
|
+
borderRadius: Joi.number().min(0).default(0),
|
|
106
|
+
border: borderSchema.optional()
|
|
101
107
|
}).unknown(false);
|
|
108
|
+
var paddingSchema = Joi.alternatives().try(
|
|
109
|
+
Joi.number().min(0).default(0),
|
|
110
|
+
Joi.object({
|
|
111
|
+
top: Joi.number().min(0).default(0),
|
|
112
|
+
right: Joi.number().min(0).default(0),
|
|
113
|
+
bottom: Joi.number().min(0).default(0),
|
|
114
|
+
left: Joi.number().min(0).default(0)
|
|
115
|
+
}).unknown(false)
|
|
116
|
+
);
|
|
102
117
|
var customFontSchema = Joi.object({
|
|
103
118
|
src: Joi.string().uri().required(),
|
|
104
119
|
family: Joi.string().required(),
|
|
@@ -116,11 +131,10 @@ var RichTextAssetSchema = Joi.object({
|
|
|
116
131
|
stroke: strokeSchema.optional(),
|
|
117
132
|
shadow: shadowSchema.optional(),
|
|
118
133
|
background: backgroundSchema.optional(),
|
|
134
|
+
padding: paddingSchema.optional(),
|
|
119
135
|
align: alignmentSchema.optional(),
|
|
120
136
|
animation: animationSchema.optional(),
|
|
121
|
-
customFonts: Joi.array().items(customFontSchema).optional()
|
|
122
|
-
cacheEnabled: Joi.boolean().default(true),
|
|
123
|
-
pixelRatio: Joi.number().min(1).max(3).default(CANVAS_CONFIG.DEFAULTS.pixelRatio)
|
|
137
|
+
customFonts: Joi.array().items(customFontSchema).optional()
|
|
124
138
|
}).unknown(false);
|
|
125
139
|
|
|
126
140
|
// src/wasm/hb-loader.ts
|
|
@@ -789,15 +803,27 @@ function decorationGeometry(kind, p) {
|
|
|
789
803
|
}
|
|
790
804
|
|
|
791
805
|
// src/core/drawops.ts
|
|
806
|
+
function normalizePadding(padding) {
|
|
807
|
+
if (padding === void 0 || padding === null) {
|
|
808
|
+
return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
809
|
+
}
|
|
810
|
+
if (typeof padding === "number") {
|
|
811
|
+
return { top: padding, right: padding, bottom: padding, left: padding };
|
|
812
|
+
}
|
|
813
|
+
return padding;
|
|
814
|
+
}
|
|
792
815
|
async function buildDrawOps(p) {
|
|
793
816
|
const ops = [];
|
|
817
|
+
const padding = normalizePadding(p.padding);
|
|
818
|
+
const borderWidth = p.background?.border?.width ?? 0;
|
|
794
819
|
ops.push({
|
|
795
820
|
op: "BeginFrame",
|
|
796
821
|
width: p.canvas.width,
|
|
797
822
|
height: p.canvas.height,
|
|
798
823
|
pixelRatio: p.canvas.pixelRatio,
|
|
799
824
|
clear: true,
|
|
800
|
-
bg:
|
|
825
|
+
bg: void 0
|
|
826
|
+
// Background will be drawn as a separate layer with proper padding/border
|
|
801
827
|
});
|
|
802
828
|
if (p.lines.length === 0) return ops;
|
|
803
829
|
const upem = Math.max(1, await p.getUnitsPerEm());
|
|
@@ -807,33 +833,34 @@ async function buildDrawOps(p) {
|
|
|
807
833
|
let blockY;
|
|
808
834
|
switch (p.align.vertical) {
|
|
809
835
|
case "top":
|
|
810
|
-
blockY = p.font.size;
|
|
836
|
+
blockY = p.font.size + padding.top;
|
|
811
837
|
break;
|
|
812
838
|
case "bottom":
|
|
813
|
-
blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
|
|
839
|
+
blockY = p.textRect.height - (numLines - 1) * lineHeightPx + padding.top;
|
|
814
840
|
break;
|
|
815
841
|
case "middle":
|
|
816
842
|
default:
|
|
817
843
|
const capHeightRatio = 0.35;
|
|
818
844
|
const visualOffset = p.font.size * capHeightRatio;
|
|
819
|
-
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
|
|
845
|
+
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset + padding.top;
|
|
820
846
|
break;
|
|
821
847
|
}
|
|
822
848
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
823
849
|
const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
|
|
850
|
+
const textOps = [];
|
|
824
851
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
825
852
|
for (const line of p.lines) {
|
|
826
853
|
let lineX;
|
|
827
854
|
switch (p.align.horizontal) {
|
|
828
855
|
case "left":
|
|
829
|
-
lineX =
|
|
856
|
+
lineX = padding.left;
|
|
830
857
|
break;
|
|
831
858
|
case "right":
|
|
832
|
-
lineX = p.textRect.width - line.width;
|
|
859
|
+
lineX = p.textRect.width - line.width + padding.left;
|
|
833
860
|
break;
|
|
834
861
|
case "center":
|
|
835
862
|
default:
|
|
836
|
-
lineX = (p.textRect.width - line.width) / 2;
|
|
863
|
+
lineX = (p.textRect.width - line.width) / 2 + padding.left;
|
|
837
864
|
break;
|
|
838
865
|
}
|
|
839
866
|
let xCursor = lineX;
|
|
@@ -857,7 +884,7 @@ async function buildDrawOps(p) {
|
|
|
857
884
|
if (x2 > gMaxX) gMaxX = x2;
|
|
858
885
|
if (y2 > gMaxY) gMaxY = y2;
|
|
859
886
|
if (p.shadow && p.shadow.blur > 0) {
|
|
860
|
-
|
|
887
|
+
textOps.push({
|
|
861
888
|
isShadow: true,
|
|
862
889
|
op: "FillPath",
|
|
863
890
|
path,
|
|
@@ -868,7 +895,7 @@ async function buildDrawOps(p) {
|
|
|
868
895
|
});
|
|
869
896
|
}
|
|
870
897
|
if (p.stroke && p.stroke.width > 0) {
|
|
871
|
-
|
|
898
|
+
textOps.push({
|
|
872
899
|
op: "StrokePath",
|
|
873
900
|
path,
|
|
874
901
|
x: glyphX,
|
|
@@ -879,7 +906,7 @@ async function buildDrawOps(p) {
|
|
|
879
906
|
opacity: p.stroke.opacity
|
|
880
907
|
});
|
|
881
908
|
}
|
|
882
|
-
|
|
909
|
+
textOps.push({
|
|
883
910
|
op: "FillPath",
|
|
884
911
|
path,
|
|
885
912
|
x: glyphX,
|
|
@@ -896,7 +923,7 @@ async function buildDrawOps(p) {
|
|
|
896
923
|
lineWidth: line.width,
|
|
897
924
|
xStart: lineX
|
|
898
925
|
});
|
|
899
|
-
|
|
926
|
+
textOps.push({
|
|
900
927
|
op: "DecorationLine",
|
|
901
928
|
from: { x: deco.x1, y: deco.y },
|
|
902
929
|
to: { x: deco.x2, y: deco.y },
|
|
@@ -908,12 +935,46 @@ async function buildDrawOps(p) {
|
|
|
908
935
|
}
|
|
909
936
|
if (gMinX !== Infinity) {
|
|
910
937
|
const gbox = { x: gMinX, y: gMinY, w: Math.max(1, gMaxX - gMinX), h: Math.max(1, gMaxY - gMinY) };
|
|
911
|
-
for (const op of
|
|
938
|
+
for (const op of textOps) {
|
|
912
939
|
if (op.op === "FillPath" && !op.isShadow) {
|
|
913
940
|
op.gradientBBox = gbox;
|
|
914
941
|
}
|
|
915
942
|
}
|
|
916
943
|
}
|
|
944
|
+
if (p.background && (p.background.color || p.background.border)) {
|
|
945
|
+
const bgX = 0;
|
|
946
|
+
const bgY = 0;
|
|
947
|
+
const bgWidth = p.canvas.width;
|
|
948
|
+
const bgHeight = p.canvas.height;
|
|
949
|
+
if (p.background.color) {
|
|
950
|
+
ops.push({
|
|
951
|
+
op: "Rectangle",
|
|
952
|
+
x: bgX,
|
|
953
|
+
y: bgY,
|
|
954
|
+
width: bgWidth,
|
|
955
|
+
height: bgHeight,
|
|
956
|
+
fill: { kind: "solid", color: p.background.color, opacity: p.background.opacity },
|
|
957
|
+
borderRadius: p.background.borderRadius
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
if (p.background.border && p.background.border.width > 0) {
|
|
961
|
+
const halfBorder = p.background.border.width / 2;
|
|
962
|
+
ops.push({
|
|
963
|
+
op: "RectangleStroke",
|
|
964
|
+
x: bgX + halfBorder,
|
|
965
|
+
y: bgY + halfBorder,
|
|
966
|
+
width: bgWidth - p.background.border.width,
|
|
967
|
+
height: bgHeight - p.background.border.width,
|
|
968
|
+
stroke: {
|
|
969
|
+
width: p.background.border.width,
|
|
970
|
+
color: p.background.border.color,
|
|
971
|
+
opacity: p.background.border.opacity
|
|
972
|
+
},
|
|
973
|
+
borderRadius: Math.max(0, p.background.borderRadius - halfBorder)
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
ops.push(...textOps);
|
|
917
978
|
return ops;
|
|
918
979
|
}
|
|
919
980
|
function tokenizePath(d) {
|
|
@@ -1723,6 +1784,44 @@ async function createNodePainter(opts) {
|
|
|
1723
1784
|
});
|
|
1724
1785
|
continue;
|
|
1725
1786
|
}
|
|
1787
|
+
if (op.op === "Rectangle") {
|
|
1788
|
+
renderToBoth((context) => {
|
|
1789
|
+
context.save();
|
|
1790
|
+
const fill = makeGradientFromBBox(context, op.fill, {
|
|
1791
|
+
x: op.x,
|
|
1792
|
+
y: op.y,
|
|
1793
|
+
w: op.width,
|
|
1794
|
+
h: op.height
|
|
1795
|
+
});
|
|
1796
|
+
context.fillStyle = fill;
|
|
1797
|
+
if (op.borderRadius && op.borderRadius > 0) {
|
|
1798
|
+
context.beginPath();
|
|
1799
|
+
roundRectPath(context, op.x, op.y, op.width, op.height, op.borderRadius);
|
|
1800
|
+
context.fill();
|
|
1801
|
+
} else {
|
|
1802
|
+
context.fillRect(op.x, op.y, op.width, op.height);
|
|
1803
|
+
}
|
|
1804
|
+
context.restore();
|
|
1805
|
+
});
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
if (op.op === "RectangleStroke") {
|
|
1809
|
+
renderToBoth((context) => {
|
|
1810
|
+
context.save();
|
|
1811
|
+
const c = parseHex6(op.stroke.color, op.stroke.opacity);
|
|
1812
|
+
context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1813
|
+
context.lineWidth = op.stroke.width;
|
|
1814
|
+
if (op.borderRadius && op.borderRadius > 0) {
|
|
1815
|
+
context.beginPath();
|
|
1816
|
+
roundRectPath(context, op.x, op.y, op.width, op.height, op.borderRadius);
|
|
1817
|
+
context.stroke();
|
|
1818
|
+
} else {
|
|
1819
|
+
context.strokeRect(op.x, op.y, op.width, op.height);
|
|
1820
|
+
}
|
|
1821
|
+
context.restore();
|
|
1822
|
+
});
|
|
1823
|
+
continue;
|
|
1824
|
+
}
|
|
1726
1825
|
}
|
|
1727
1826
|
if (needsAlphaExtraction) {
|
|
1728
1827
|
const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
@@ -2286,15 +2385,16 @@ async function createTextEngine(opts = {}) {
|
|
|
2286
2385
|
`Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
|
|
2287
2386
|
);
|
|
2288
2387
|
}
|
|
2388
|
+
const padding = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2289
2389
|
const textRect = {
|
|
2290
2390
|
x: 0,
|
|
2291
2391
|
y: 0,
|
|
2292
2392
|
width: asset.width ?? width,
|
|
2293
2393
|
height: asset.height ?? height
|
|
2294
2394
|
};
|
|
2295
|
-
const canvasW = asset.width ?? width;
|
|
2296
|
-
const canvasH = asset.height ?? height;
|
|
2297
|
-
const canvasPR =
|
|
2395
|
+
const canvasW = (asset.width ?? width) + padding.left + padding.right;
|
|
2396
|
+
const canvasH = (asset.height ?? height) + padding.top + padding.bottom;
|
|
2397
|
+
const canvasPR = pixelRatio;
|
|
2298
2398
|
let ops0;
|
|
2299
2399
|
try {
|
|
2300
2400
|
ops0 = await buildDrawOps({
|
|
@@ -2320,6 +2420,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2320
2420
|
vertical: asset.align?.vertical ?? "middle"
|
|
2321
2421
|
},
|
|
2322
2422
|
background: asset.background,
|
|
2423
|
+
padding: asset.padding,
|
|
2323
2424
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2324
2425
|
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2325
2426
|
});
|
|
@@ -2382,7 +2483,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2382
2483
|
fps,
|
|
2383
2484
|
duration: options.duration ?? 3,
|
|
2384
2485
|
outputPath: options.outputPath ?? "output.mp4",
|
|
2385
|
-
pixelRatio
|
|
2486
|
+
pixelRatio,
|
|
2386
2487
|
hasAlpha: needsAlpha,
|
|
2387
2488
|
...options
|
|
2388
2489
|
};
|
package/dist/entry.web.d.ts
CHANGED
|
@@ -64,6 +64,17 @@ type RichTextValidated = Required<{
|
|
|
64
64
|
color?: string;
|
|
65
65
|
opacity: number;
|
|
66
66
|
borderRadius: number;
|
|
67
|
+
border?: {
|
|
68
|
+
width: number;
|
|
69
|
+
color: string;
|
|
70
|
+
opacity: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
padding?: number | {
|
|
74
|
+
top: number;
|
|
75
|
+
right: number;
|
|
76
|
+
bottom: number;
|
|
77
|
+
left: number;
|
|
67
78
|
};
|
|
68
79
|
align?: {
|
|
69
80
|
horizontal: "left" | "center" | "right";
|
|
@@ -83,8 +94,6 @@ type RichTextValidated = Required<{
|
|
|
83
94
|
style?: string;
|
|
84
95
|
originalFamily?: string;
|
|
85
96
|
}[];
|
|
86
|
-
cacheEnabled: boolean;
|
|
87
|
-
pixelRatio: number;
|
|
88
97
|
}>;
|
|
89
98
|
|
|
90
99
|
type RGBA = {
|
|
@@ -177,6 +186,26 @@ type DrawOp = {
|
|
|
177
186
|
width: number;
|
|
178
187
|
color: string;
|
|
179
188
|
opacity: number;
|
|
189
|
+
} | {
|
|
190
|
+
op: "Rectangle";
|
|
191
|
+
x: number;
|
|
192
|
+
y: number;
|
|
193
|
+
width: number;
|
|
194
|
+
height: number;
|
|
195
|
+
fill: GradientSpec;
|
|
196
|
+
borderRadius?: number;
|
|
197
|
+
} | {
|
|
198
|
+
op: "RectangleStroke";
|
|
199
|
+
x: number;
|
|
200
|
+
y: number;
|
|
201
|
+
width: number;
|
|
202
|
+
height: number;
|
|
203
|
+
stroke: {
|
|
204
|
+
width: number;
|
|
205
|
+
color: string;
|
|
206
|
+
opacity: number;
|
|
207
|
+
};
|
|
208
|
+
borderRadius?: number;
|
|
180
209
|
};
|
|
181
210
|
type EngineInit = {
|
|
182
211
|
width: number;
|
package/dist/entry.web.js
CHANGED
|
@@ -98,11 +98,26 @@ var animationSchema = Joi.object({
|
|
|
98
98
|
otherwise: Joi.forbidden()
|
|
99
99
|
})
|
|
100
100
|
}).unknown(false);
|
|
101
|
+
var borderSchema = Joi.object({
|
|
102
|
+
width: Joi.number().min(0).default(0),
|
|
103
|
+
color: Joi.string().pattern(HEX6).default("#000000"),
|
|
104
|
+
opacity: Joi.number().min(0).max(1).default(1)
|
|
105
|
+
}).unknown(false);
|
|
101
106
|
var backgroundSchema = Joi.object({
|
|
102
107
|
color: Joi.string().pattern(HEX6).optional(),
|
|
103
108
|
opacity: Joi.number().min(0).max(1).default(1),
|
|
104
|
-
borderRadius: Joi.number().min(0).default(0)
|
|
109
|
+
borderRadius: Joi.number().min(0).default(0),
|
|
110
|
+
border: borderSchema.optional()
|
|
105
111
|
}).unknown(false);
|
|
112
|
+
var paddingSchema = Joi.alternatives().try(
|
|
113
|
+
Joi.number().min(0).default(0),
|
|
114
|
+
Joi.object({
|
|
115
|
+
top: Joi.number().min(0).default(0),
|
|
116
|
+
right: Joi.number().min(0).default(0),
|
|
117
|
+
bottom: Joi.number().min(0).default(0),
|
|
118
|
+
left: Joi.number().min(0).default(0)
|
|
119
|
+
}).unknown(false)
|
|
120
|
+
);
|
|
106
121
|
var customFontSchema = Joi.object({
|
|
107
122
|
src: Joi.string().uri().required(),
|
|
108
123
|
family: Joi.string().required(),
|
|
@@ -120,11 +135,10 @@ var RichTextAssetSchema = Joi.object({
|
|
|
120
135
|
stroke: strokeSchema.optional(),
|
|
121
136
|
shadow: shadowSchema.optional(),
|
|
122
137
|
background: backgroundSchema.optional(),
|
|
138
|
+
padding: paddingSchema.optional(),
|
|
123
139
|
align: alignmentSchema.optional(),
|
|
124
140
|
animation: animationSchema.optional(),
|
|
125
|
-
customFonts: Joi.array().items(customFontSchema).optional()
|
|
126
|
-
cacheEnabled: Joi.boolean().default(true),
|
|
127
|
-
pixelRatio: Joi.number().min(1).max(3).default(CANVAS_CONFIG.DEFAULTS.pixelRatio)
|
|
141
|
+
customFonts: Joi.array().items(customFontSchema).optional()
|
|
128
142
|
}).unknown(false);
|
|
129
143
|
|
|
130
144
|
// src/wasm/hb-loader.ts
|
|
@@ -794,15 +808,27 @@ function decorationGeometry(kind, p) {
|
|
|
794
808
|
}
|
|
795
809
|
|
|
796
810
|
// src/core/drawops.ts
|
|
811
|
+
function normalizePadding(padding) {
|
|
812
|
+
if (padding === void 0 || padding === null) {
|
|
813
|
+
return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
814
|
+
}
|
|
815
|
+
if (typeof padding === "number") {
|
|
816
|
+
return { top: padding, right: padding, bottom: padding, left: padding };
|
|
817
|
+
}
|
|
818
|
+
return padding;
|
|
819
|
+
}
|
|
797
820
|
async function buildDrawOps(p) {
|
|
798
821
|
const ops = [];
|
|
822
|
+
const padding = normalizePadding(p.padding);
|
|
823
|
+
const borderWidth = p.background?.border?.width ?? 0;
|
|
799
824
|
ops.push({
|
|
800
825
|
op: "BeginFrame",
|
|
801
826
|
width: p.canvas.width,
|
|
802
827
|
height: p.canvas.height,
|
|
803
828
|
pixelRatio: p.canvas.pixelRatio,
|
|
804
829
|
clear: true,
|
|
805
|
-
bg:
|
|
830
|
+
bg: void 0
|
|
831
|
+
// Background will be drawn as a separate layer with proper padding/border
|
|
806
832
|
});
|
|
807
833
|
if (p.lines.length === 0) return ops;
|
|
808
834
|
const upem = Math.max(1, await p.getUnitsPerEm());
|
|
@@ -812,33 +838,34 @@ async function buildDrawOps(p) {
|
|
|
812
838
|
let blockY;
|
|
813
839
|
switch (p.align.vertical) {
|
|
814
840
|
case "top":
|
|
815
|
-
blockY = p.font.size;
|
|
841
|
+
blockY = p.font.size + padding.top;
|
|
816
842
|
break;
|
|
817
843
|
case "bottom":
|
|
818
|
-
blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
|
|
844
|
+
blockY = p.textRect.height - (numLines - 1) * lineHeightPx + padding.top;
|
|
819
845
|
break;
|
|
820
846
|
case "middle":
|
|
821
847
|
default:
|
|
822
848
|
const capHeightRatio = 0.35;
|
|
823
849
|
const visualOffset = p.font.size * capHeightRatio;
|
|
824
|
-
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
|
|
850
|
+
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset + padding.top;
|
|
825
851
|
break;
|
|
826
852
|
}
|
|
827
853
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
828
854
|
const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
|
|
855
|
+
const textOps = [];
|
|
829
856
|
let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
|
|
830
857
|
for (const line of p.lines) {
|
|
831
858
|
let lineX;
|
|
832
859
|
switch (p.align.horizontal) {
|
|
833
860
|
case "left":
|
|
834
|
-
lineX =
|
|
861
|
+
lineX = padding.left;
|
|
835
862
|
break;
|
|
836
863
|
case "right":
|
|
837
|
-
lineX = p.textRect.width - line.width;
|
|
864
|
+
lineX = p.textRect.width - line.width + padding.left;
|
|
838
865
|
break;
|
|
839
866
|
case "center":
|
|
840
867
|
default:
|
|
841
|
-
lineX = (p.textRect.width - line.width) / 2;
|
|
868
|
+
lineX = (p.textRect.width - line.width) / 2 + padding.left;
|
|
842
869
|
break;
|
|
843
870
|
}
|
|
844
871
|
let xCursor = lineX;
|
|
@@ -862,7 +889,7 @@ async function buildDrawOps(p) {
|
|
|
862
889
|
if (x2 > gMaxX) gMaxX = x2;
|
|
863
890
|
if (y2 > gMaxY) gMaxY = y2;
|
|
864
891
|
if (p.shadow && p.shadow.blur > 0) {
|
|
865
|
-
|
|
892
|
+
textOps.push({
|
|
866
893
|
isShadow: true,
|
|
867
894
|
op: "FillPath",
|
|
868
895
|
path,
|
|
@@ -873,7 +900,7 @@ async function buildDrawOps(p) {
|
|
|
873
900
|
});
|
|
874
901
|
}
|
|
875
902
|
if (p.stroke && p.stroke.width > 0) {
|
|
876
|
-
|
|
903
|
+
textOps.push({
|
|
877
904
|
op: "StrokePath",
|
|
878
905
|
path,
|
|
879
906
|
x: glyphX,
|
|
@@ -884,7 +911,7 @@ async function buildDrawOps(p) {
|
|
|
884
911
|
opacity: p.stroke.opacity
|
|
885
912
|
});
|
|
886
913
|
}
|
|
887
|
-
|
|
914
|
+
textOps.push({
|
|
888
915
|
op: "FillPath",
|
|
889
916
|
path,
|
|
890
917
|
x: glyphX,
|
|
@@ -901,7 +928,7 @@ async function buildDrawOps(p) {
|
|
|
901
928
|
lineWidth: line.width,
|
|
902
929
|
xStart: lineX
|
|
903
930
|
});
|
|
904
|
-
|
|
931
|
+
textOps.push({
|
|
905
932
|
op: "DecorationLine",
|
|
906
933
|
from: { x: deco.x1, y: deco.y },
|
|
907
934
|
to: { x: deco.x2, y: deco.y },
|
|
@@ -913,12 +940,46 @@ async function buildDrawOps(p) {
|
|
|
913
940
|
}
|
|
914
941
|
if (gMinX !== Infinity) {
|
|
915
942
|
const gbox = { x: gMinX, y: gMinY, w: Math.max(1, gMaxX - gMinX), h: Math.max(1, gMaxY - gMinY) };
|
|
916
|
-
for (const op of
|
|
943
|
+
for (const op of textOps) {
|
|
917
944
|
if (op.op === "FillPath" && !op.isShadow) {
|
|
918
945
|
op.gradientBBox = gbox;
|
|
919
946
|
}
|
|
920
947
|
}
|
|
921
948
|
}
|
|
949
|
+
if (p.background && (p.background.color || p.background.border)) {
|
|
950
|
+
const bgX = 0;
|
|
951
|
+
const bgY = 0;
|
|
952
|
+
const bgWidth = p.canvas.width;
|
|
953
|
+
const bgHeight = p.canvas.height;
|
|
954
|
+
if (p.background.color) {
|
|
955
|
+
ops.push({
|
|
956
|
+
op: "Rectangle",
|
|
957
|
+
x: bgX,
|
|
958
|
+
y: bgY,
|
|
959
|
+
width: bgWidth,
|
|
960
|
+
height: bgHeight,
|
|
961
|
+
fill: { kind: "solid", color: p.background.color, opacity: p.background.opacity },
|
|
962
|
+
borderRadius: p.background.borderRadius
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
if (p.background.border && p.background.border.width > 0) {
|
|
966
|
+
const halfBorder = p.background.border.width / 2;
|
|
967
|
+
ops.push({
|
|
968
|
+
op: "RectangleStroke",
|
|
969
|
+
x: bgX + halfBorder,
|
|
970
|
+
y: bgY + halfBorder,
|
|
971
|
+
width: bgWidth - p.background.border.width,
|
|
972
|
+
height: bgHeight - p.background.border.width,
|
|
973
|
+
stroke: {
|
|
974
|
+
width: p.background.border.width,
|
|
975
|
+
color: p.background.border.color,
|
|
976
|
+
opacity: p.background.border.opacity
|
|
977
|
+
},
|
|
978
|
+
borderRadius: Math.max(0, p.background.borderRadius - halfBorder)
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
ops.push(...textOps);
|
|
922
983
|
return ops;
|
|
923
984
|
}
|
|
924
985
|
function tokenizePath(d) {
|
|
@@ -1676,6 +1737,48 @@ function createWebPainter(canvas) {
|
|
|
1676
1737
|
ctx.restore();
|
|
1677
1738
|
continue;
|
|
1678
1739
|
}
|
|
1740
|
+
if (op.op === "Rectangle") {
|
|
1741
|
+
ctx.save();
|
|
1742
|
+
const fill = makeGradientFromBBox(ctx, op.fill, {
|
|
1743
|
+
x: op.x,
|
|
1744
|
+
y: op.y,
|
|
1745
|
+
w: op.width,
|
|
1746
|
+
h: op.height
|
|
1747
|
+
});
|
|
1748
|
+
ctx.fillStyle = fill;
|
|
1749
|
+
if (op.borderRadius && op.borderRadius > 0) {
|
|
1750
|
+
drawRoundedRect(ctx, op.x, op.y, op.width, op.height, op.borderRadius);
|
|
1751
|
+
} else {
|
|
1752
|
+
ctx.fillRect(op.x, op.y, op.width, op.height);
|
|
1753
|
+
}
|
|
1754
|
+
ctx.restore();
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
if (op.op === "RectangleStroke") {
|
|
1758
|
+
ctx.save();
|
|
1759
|
+
const c = parseHex6(op.stroke.color, op.stroke.opacity);
|
|
1760
|
+
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1761
|
+
ctx.lineWidth = op.stroke.width;
|
|
1762
|
+
if (op.borderRadius && op.borderRadius > 0) {
|
|
1763
|
+
const p = new Path2D();
|
|
1764
|
+
const r = op.borderRadius;
|
|
1765
|
+
const x = op.x;
|
|
1766
|
+
const y = op.y;
|
|
1767
|
+
const w = op.width;
|
|
1768
|
+
const h = op.height;
|
|
1769
|
+
p.moveTo(x + r, y);
|
|
1770
|
+
p.arcTo(x + w, y, x + w, y + h, r);
|
|
1771
|
+
p.arcTo(x + w, y + h, x, y + h, r);
|
|
1772
|
+
p.arcTo(x, y + h, x, y, r);
|
|
1773
|
+
p.arcTo(x, y, x + w, y, r);
|
|
1774
|
+
p.closePath();
|
|
1775
|
+
ctx.stroke(p);
|
|
1776
|
+
} else {
|
|
1777
|
+
ctx.strokeRect(op.x, op.y, op.width, op.height);
|
|
1778
|
+
}
|
|
1779
|
+
ctx.restore();
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1679
1782
|
}
|
|
1680
1783
|
}
|
|
1681
1784
|
};
|
|
@@ -2005,15 +2108,16 @@ async function createTextEngine(opts = {}) {
|
|
|
2005
2108
|
`Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
|
|
2006
2109
|
);
|
|
2007
2110
|
}
|
|
2111
|
+
const padding = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2008
2112
|
const textRect = {
|
|
2009
2113
|
x: 0,
|
|
2010
2114
|
y: 0,
|
|
2011
2115
|
width: asset.width ?? width,
|
|
2012
2116
|
height: asset.height ?? height
|
|
2013
2117
|
};
|
|
2014
|
-
const canvasW = asset.width ?? width;
|
|
2015
|
-
const canvasH = asset.height ?? height;
|
|
2016
|
-
const canvasPR =
|
|
2118
|
+
const canvasW = (asset.width ?? width) + padding.left + padding.right;
|
|
2119
|
+
const canvasH = (asset.height ?? height) + padding.top + padding.bottom;
|
|
2120
|
+
const canvasPR = pixelRatio;
|
|
2017
2121
|
let ops0;
|
|
2018
2122
|
try {
|
|
2019
2123
|
ops0 = await buildDrawOps({
|
|
@@ -2039,6 +2143,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2039
2143
|
vertical: asset.align?.vertical ?? "middle"
|
|
2040
2144
|
},
|
|
2041
2145
|
background: asset.background,
|
|
2146
|
+
padding: asset.padding,
|
|
2042
2147
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2043
2148
|
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2044
2149
|
});
|
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@shotstack/shotstack-canvas",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/entry.node.cjs",
|
|
7
|
-
"module": "./dist/entry.node.js",
|
|
8
|
-
"browser": "./dist/entry.web.js",
|
|
9
|
-
"types": "./dist/entry.node.d.ts",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"node": {
|
|
13
|
-
"import": "./dist/entry.node.js",
|
|
14
|
-
"require": "./dist/entry.node.cjs"
|
|
15
|
-
},
|
|
16
|
-
"browser": "./dist/entry.web.js",
|
|
17
|
-
"default": "./dist/entry.web.js"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"dist/**",
|
|
22
|
-
"scripts/postinstall.js",
|
|
23
|
-
"README.md",
|
|
24
|
-
"LICENSE"
|
|
25
|
-
],
|
|
26
|
-
"scripts": {
|
|
27
|
-
"dev": "tsup --watch",
|
|
28
|
-
"build": "tsup",
|
|
29
|
-
"postinstall": "node scripts/postinstall.js",
|
|
30
|
-
"vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
|
|
31
|
-
"example:node": "node examples/node-example.mjs",
|
|
32
|
-
"example:video": "node examples/node-video.mjs",
|
|
33
|
-
"example:web": "vite dev examples/web-example",
|
|
34
|
-
"prepublishOnly": "npm run build"
|
|
35
|
-
},
|
|
36
|
-
"publishConfig": {
|
|
37
|
-
"access": "public",
|
|
38
|
-
"registry": "https://registry.npmjs.org/"
|
|
39
|
-
},
|
|
40
|
-
"engines": {
|
|
41
|
-
"node": ">=18"
|
|
42
|
-
},
|
|
43
|
-
"sideEffects": false,
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"canvas": "npm:@napi-rs/canvas@^0.1.54",
|
|
46
|
-
"ffmpeg-static": "^5.2.0",
|
|
47
|
-
"fontkit": "^2.0.4",
|
|
48
|
-
"harfbuzzjs": "0.4.12",
|
|
49
|
-
"joi": "^17.13.3",
|
|
50
|
-
"opentype.js": "^1.3.4"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@types/fluent-ffmpeg": "2.1.27",
|
|
54
|
-
"@types/node": "^20.14.10",
|
|
55
|
-
"fluent-ffmpeg": "^2.1.3",
|
|
56
|
-
"tsup": "^8.2.3",
|
|
57
|
-
"typescript": "^5.5.3",
|
|
58
|
-
"vite": "^5.3.3",
|
|
59
|
-
"vite-plugin-top-level-await": "1.6.0",
|
|
60
|
-
"vite-plugin-wasm": "3.5.0"
|
|
61
|
-
}
|
|
62
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@shotstack/shotstack-canvas",
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/entry.node.cjs",
|
|
7
|
+
"module": "./dist/entry.node.js",
|
|
8
|
+
"browser": "./dist/entry.web.js",
|
|
9
|
+
"types": "./dist/entry.node.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"node": {
|
|
13
|
+
"import": "./dist/entry.node.js",
|
|
14
|
+
"require": "./dist/entry.node.cjs"
|
|
15
|
+
},
|
|
16
|
+
"browser": "./dist/entry.web.js",
|
|
17
|
+
"default": "./dist/entry.web.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/**",
|
|
22
|
+
"scripts/postinstall.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"postinstall": "node scripts/postinstall.js",
|
|
30
|
+
"vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
|
|
31
|
+
"example:node": "node examples/node-example.mjs",
|
|
32
|
+
"example:video": "node examples/node-video.mjs",
|
|
33
|
+
"example:web": "vite dev examples/web-example",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"canvas": "npm:@napi-rs/canvas@^0.1.54",
|
|
46
|
+
"ffmpeg-static": "^5.2.0",
|
|
47
|
+
"fontkit": "^2.0.4",
|
|
48
|
+
"harfbuzzjs": "0.4.12",
|
|
49
|
+
"joi": "^17.13.3",
|
|
50
|
+
"opentype.js": "^1.3.4"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/fluent-ffmpeg": "2.1.27",
|
|
54
|
+
"@types/node": "^20.14.10",
|
|
55
|
+
"fluent-ffmpeg": "^2.1.3",
|
|
56
|
+
"tsup": "^8.2.3",
|
|
57
|
+
"typescript": "^5.5.3",
|
|
58
|
+
"vite": "^5.3.3",
|
|
59
|
+
"vite-plugin-top-level-await": "1.6.0",
|
|
60
|
+
"vite-plugin-wasm": "3.5.0"
|
|
61
|
+
}
|
|
62
|
+
}
|