@shotstack/shotstack-canvas 1.0.2
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/.eslintrc.cjs +7 -0
- package/.prettierrc +1 -0
- package/README.md +13 -0
- package/assets/wasm/hb.wasm +0 -0
- package/package.json +48 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/vendor-harfbuzz.js +64 -0
- package/src/config/canvas-constants.ts +30 -0
- package/src/core/animations.ts +570 -0
- package/src/core/colors.ts +11 -0
- package/src/core/decoration.ts +9 -0
- package/src/core/drawops.ts +206 -0
- package/src/core/font-registry.ts +77 -0
- package/src/core/gradients.ts +12 -0
- package/src/core/layout.ts +184 -0
- package/src/core/utils.ts +3 -0
- package/src/core/video-generator.ts +157 -0
- package/src/env/entry.node.ts +167 -0
- package/src/env/entry.web.ts +146 -0
- package/src/index.ts +1 -0
- package/src/io/node.ts +45 -0
- package/src/io/web.ts +5 -0
- package/src/painters/node.ts +290 -0
- package/src/painters/web.ts +224 -0
- package/src/schema/asset-schema.ts +166 -0
- package/src/types.ts +36 -0
- package/src/wasm/hb-loader.ts +31 -0
- package/tsconfig.base.json +22 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +52 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { DrawOp, GradientSpec } from "../types";
|
|
2
|
+
import { parseHex6 } from "../core/colors";
|
|
3
|
+
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
+
|
|
6
|
+
export function createWebPainter(canvas: HTMLCanvasElement | OffscreenCanvas) {
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
const ctx: CanvasRenderingContext2D = (canvas as any).getContext("2d");
|
|
9
|
+
if (!ctx) throw new Error("2D context unavailable");
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
async render(ops: DrawOp[]) {
|
|
13
|
+
const globalBox = computeGlobalTextBounds(ops);
|
|
14
|
+
|
|
15
|
+
for (const op of ops) {
|
|
16
|
+
if (op.op === "BeginFrame") {
|
|
17
|
+
const dpr = op.pixelRatio;
|
|
18
|
+
const w = op.width,
|
|
19
|
+
h = op.height;
|
|
20
|
+
|
|
21
|
+
if ("width" in canvas && "height" in canvas) {
|
|
22
|
+
(canvas as any).width = Math.floor(w * dpr);
|
|
23
|
+
(canvas as any).height = Math.floor(h * dpr);
|
|
24
|
+
}
|
|
25
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
26
|
+
|
|
27
|
+
if (op.clear) ctx.clearRect(0, 0, w, h);
|
|
28
|
+
if (op.bg) {
|
|
29
|
+
const { color, opacity, radius } = op.bg;
|
|
30
|
+
if (color) {
|
|
31
|
+
const c = parseHex6(color, opacity);
|
|
32
|
+
ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
33
|
+
if (radius && radius > 0) {
|
|
34
|
+
drawRoundedRect(ctx, 0, 0, w, h, radius);
|
|
35
|
+
ctx.fill();
|
|
36
|
+
} else {
|
|
37
|
+
ctx.fillRect(0, 0, w, h);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (op.op === "FillPath") {
|
|
45
|
+
const p = new Path2D(op.path);
|
|
46
|
+
ctx.save();
|
|
47
|
+
ctx.translate(op.x, op.y);
|
|
48
|
+
|
|
49
|
+
const s = (op as any).scale ?? 1;
|
|
50
|
+
ctx.scale(s, -s);
|
|
51
|
+
|
|
52
|
+
const bbox = (op as any).gradientBBox ?? globalBox;
|
|
53
|
+
const fill = makeGradientFromBBox(ctx, op.fill, bbox);
|
|
54
|
+
ctx.fillStyle = fill as any;
|
|
55
|
+
ctx.fill(p);
|
|
56
|
+
ctx.restore();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (op.op === "StrokePath") {
|
|
61
|
+
const p = new Path2D(op.path);
|
|
62
|
+
ctx.save();
|
|
63
|
+
ctx.translate(op.x, op.y);
|
|
64
|
+
|
|
65
|
+
const s = (op as any).scale ?? 1;
|
|
66
|
+
ctx.scale(s, -s);
|
|
67
|
+
const invAbs = 1 / Math.abs(s);
|
|
68
|
+
|
|
69
|
+
const c = parseHex6((op as any).color, (op as any).opacity);
|
|
70
|
+
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
71
|
+
ctx.lineWidth = (op as any).width * invAbs;
|
|
72
|
+
ctx.lineJoin = "round";
|
|
73
|
+
ctx.lineCap = "round";
|
|
74
|
+
ctx.stroke(p);
|
|
75
|
+
ctx.restore();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (op.op === "DecorationLine") {
|
|
80
|
+
ctx.save();
|
|
81
|
+
const c = parseHex6((op as any).color, (op as any).opacity);
|
|
82
|
+
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
83
|
+
ctx.lineWidth = (op as any).width;
|
|
84
|
+
ctx.beginPath();
|
|
85
|
+
ctx.moveTo((op as any).from.x, (op as any).from.y);
|
|
86
|
+
ctx.lineTo((op as any).to.x, (op as any).to.y);
|
|
87
|
+
ctx.stroke();
|
|
88
|
+
ctx.restore();
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function drawRoundedRect(
|
|
97
|
+
ctx: CanvasRenderingContext2D,
|
|
98
|
+
x: number,
|
|
99
|
+
y: number,
|
|
100
|
+
w: number,
|
|
101
|
+
h: number,
|
|
102
|
+
r: number
|
|
103
|
+
) {
|
|
104
|
+
const p = new Path2D();
|
|
105
|
+
p.moveTo(x + r, y);
|
|
106
|
+
p.arcTo(x + w, y, x + w, y + h, r);
|
|
107
|
+
p.arcTo(x + w, y + h, x, y + h, r);
|
|
108
|
+
p.arcTo(x, y + h, x, y, r);
|
|
109
|
+
p.arcTo(x, y, x + w, y, r);
|
|
110
|
+
p.closePath();
|
|
111
|
+
ctx.save();
|
|
112
|
+
ctx.fill(p);
|
|
113
|
+
ctx.restore();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function makeGradientFromBBox(
|
|
117
|
+
ctx: CanvasRenderingContext2D,
|
|
118
|
+
spec: GradientSpec,
|
|
119
|
+
box: { x: number; y: number; w: number; h: number }
|
|
120
|
+
): CanvasGradient | string {
|
|
121
|
+
if (spec.kind === "solid") {
|
|
122
|
+
const c = parseHex6((spec as any).color, (spec as any).opacity);
|
|
123
|
+
return `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
124
|
+
}
|
|
125
|
+
const cx = box.x + box.w / 2,
|
|
126
|
+
cy = box.y + box.h / 2,
|
|
127
|
+
r = Math.max(box.w, box.h) / 2;
|
|
128
|
+
const addStops = (g: CanvasGradient) => {
|
|
129
|
+
const op = (spec as any).opacity ?? 1;
|
|
130
|
+
for (const s of (spec as any).stops) {
|
|
131
|
+
const c = parseHex6(s.color, op);
|
|
132
|
+
g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
|
|
133
|
+
}
|
|
134
|
+
return g;
|
|
135
|
+
};
|
|
136
|
+
if (spec.kind === "linear") {
|
|
137
|
+
const rad = (((spec as any).angle || 0) * Math.PI) / 180;
|
|
138
|
+
const x1 = cx + Math.cos(rad + Math.PI) * r;
|
|
139
|
+
const y1 = cy + Math.sin(rad + Math.PI) * r;
|
|
140
|
+
const x2 = cx + Math.cos(rad) * r;
|
|
141
|
+
const y2 = cy + Math.sin(rad) * r;
|
|
142
|
+
return addStops(ctx.createLinearGradient(x1, y1, x2, y2));
|
|
143
|
+
} else {
|
|
144
|
+
return addStops(ctx.createRadialGradient(cx, cy, 0, cx, cy, r));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function computeGlobalTextBounds(ops: DrawOp[]) {
|
|
149
|
+
let minX = Infinity,
|
|
150
|
+
minY = Infinity,
|
|
151
|
+
maxX = -Infinity,
|
|
152
|
+
maxY = -Infinity;
|
|
153
|
+
for (const op of ops) {
|
|
154
|
+
if (op.op !== "FillPath" || (op as any).isShadow) continue;
|
|
155
|
+
const b = computePathBounds(op.path);
|
|
156
|
+
const s = (op as any).scale ?? 1;
|
|
157
|
+
const x1 = op.x + s * b.x;
|
|
158
|
+
const x2 = op.x + s * (b.x + b.w);
|
|
159
|
+
const y1 = op.y - s * (b.y + b.h);
|
|
160
|
+
const y2 = op.y - s * b.y;
|
|
161
|
+
if (x1 < minX) minX = x1;
|
|
162
|
+
if (y1 < minY) minY = y1;
|
|
163
|
+
if (x2 > maxX) maxX = x2;
|
|
164
|
+
if (y2 > maxY) maxY = y2;
|
|
165
|
+
}
|
|
166
|
+
if (minX === Infinity) return { x: 0, y: 0, w: 1, h: 1 };
|
|
167
|
+
return { x: minX, y: minY, w: Math.max(1, maxX - minX), h: Math.max(1, maxY - minY) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function computePathBounds(d: string) {
|
|
171
|
+
const tokens = tokenizePath(d);
|
|
172
|
+
let i = 0;
|
|
173
|
+
let minX = Infinity,
|
|
174
|
+
minY = Infinity,
|
|
175
|
+
maxX = -Infinity,
|
|
176
|
+
maxY = -Infinity;
|
|
177
|
+
const touch = (x: number, y: number) => {
|
|
178
|
+
if (x < minX) minX = x;
|
|
179
|
+
if (y < minY) minY = y;
|
|
180
|
+
if (x > maxX) maxX = x;
|
|
181
|
+
if (y > maxY) maxY = y;
|
|
182
|
+
};
|
|
183
|
+
while (i < tokens.length) {
|
|
184
|
+
const t = tokens[i++];
|
|
185
|
+
switch (t) {
|
|
186
|
+
case "M":
|
|
187
|
+
case "L": {
|
|
188
|
+
const x = parseFloat(tokens[i++]);
|
|
189
|
+
const y = parseFloat(tokens[i++]);
|
|
190
|
+
touch(x, y);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "C": {
|
|
194
|
+
const c1x = parseFloat(tokens[i++]);
|
|
195
|
+
const c1y = parseFloat(tokens[i++]);
|
|
196
|
+
const c2x = parseFloat(tokens[i++]);
|
|
197
|
+
const c2y = parseFloat(tokens[i++]);
|
|
198
|
+
const x = parseFloat(tokens[i++]);
|
|
199
|
+
const y = parseFloat(tokens[i++]);
|
|
200
|
+
touch(c1x, c1y);
|
|
201
|
+
touch(c2x, c2y);
|
|
202
|
+
touch(x, y);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "Q": {
|
|
206
|
+
const cx = parseFloat(tokens[i++]);
|
|
207
|
+
const cy = parseFloat(tokens[i++]);
|
|
208
|
+
const x = parseFloat(tokens[i++]);
|
|
209
|
+
const y = parseFloat(tokens[i++]);
|
|
210
|
+
touch(cx, cy);
|
|
211
|
+
touch(x, y);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "Z":
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (minX === Infinity) return { x: 0, y: 0, w: 0, h: 0 };
|
|
219
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function tokenizePath(d: string): string[] {
|
|
223
|
+
return d.match(/[MLCQZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? [];
|
|
224
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Joi from "joi";
|
|
2
|
+
import { CANVAS_CONFIG } from "../config/canvas-constants";
|
|
3
|
+
|
|
4
|
+
const HEX6 = /^#[A-Fa-f0-9]{6}$/;
|
|
5
|
+
|
|
6
|
+
const gradientSchema = Joi.object({
|
|
7
|
+
type: Joi.string().valid("linear", "radial").default("linear"),
|
|
8
|
+
angle: Joi.number().min(0).max(360).default(0),
|
|
9
|
+
stops: Joi.array()
|
|
10
|
+
.items(
|
|
11
|
+
Joi.object({
|
|
12
|
+
offset: Joi.number().min(0).max(1).required(),
|
|
13
|
+
color: Joi.string().pattern(HEX6).required()
|
|
14
|
+
}).unknown(false)
|
|
15
|
+
)
|
|
16
|
+
.min(2)
|
|
17
|
+
.required()
|
|
18
|
+
}).unknown(false);
|
|
19
|
+
|
|
20
|
+
const shadowSchema = Joi.object({
|
|
21
|
+
offsetX: Joi.number().default(0),
|
|
22
|
+
offsetY: Joi.number().default(0),
|
|
23
|
+
blur: Joi.number().min(0).default(0),
|
|
24
|
+
color: Joi.string().pattern(HEX6).default("#000000"),
|
|
25
|
+
opacity: Joi.number().min(0).max(1).default(0.5)
|
|
26
|
+
}).unknown(false);
|
|
27
|
+
|
|
28
|
+
const strokeSchema = Joi.object({
|
|
29
|
+
width: Joi.number().min(0).default(0),
|
|
30
|
+
color: Joi.string().pattern(HEX6).default("#000000"),
|
|
31
|
+
opacity: Joi.number().min(0).max(1).default(1)
|
|
32
|
+
}).unknown(false);
|
|
33
|
+
|
|
34
|
+
const fontSchema = Joi.object({
|
|
35
|
+
family: Joi.string().default(CANVAS_CONFIG.DEFAULTS.fontFamily),
|
|
36
|
+
size: Joi.number()
|
|
37
|
+
.min(CANVAS_CONFIG.LIMITS.minFontSize)
|
|
38
|
+
.max(CANVAS_CONFIG.LIMITS.maxFontSize)
|
|
39
|
+
.default(CANVAS_CONFIG.DEFAULTS.fontSize),
|
|
40
|
+
weight: Joi.alternatives().try(Joi.string(), Joi.number()).default("400"),
|
|
41
|
+
style: Joi.string().valid("normal", "italic", "oblique").default("normal"),
|
|
42
|
+
color: Joi.string().pattern(HEX6).default(CANVAS_CONFIG.DEFAULTS.color),
|
|
43
|
+
opacity: Joi.number().min(0).max(1).default(1)
|
|
44
|
+
}).unknown(false);
|
|
45
|
+
|
|
46
|
+
const styleSchema = Joi.object({
|
|
47
|
+
letterSpacing: Joi.number().default(0),
|
|
48
|
+
lineHeight: Joi.number().min(0).max(10).default(1.2),
|
|
49
|
+
textTransform: Joi.string().valid("none", "uppercase", "lowercase", "capitalize").default("none"),
|
|
50
|
+
textDecoration: Joi.string().valid("none", "underline", "line-through").default("none"),
|
|
51
|
+
gradient: gradientSchema.optional()
|
|
52
|
+
}).unknown(false);
|
|
53
|
+
|
|
54
|
+
const alignmentSchema = Joi.object({
|
|
55
|
+
horizontal: Joi.string()
|
|
56
|
+
.valid("left", "center", "right")
|
|
57
|
+
.default(CANVAS_CONFIG.DEFAULTS.textAlign),
|
|
58
|
+
vertical: Joi.string().valid("top", "middle", "bottom").default("middle")
|
|
59
|
+
}).unknown(false);
|
|
60
|
+
|
|
61
|
+
const animationSchema = Joi.object({
|
|
62
|
+
preset: Joi.string().valid(...CANVAS_CONFIG.ANIMATION_TYPES as readonly string[]),
|
|
63
|
+
speed: Joi.number().min(0.1).max(10).default(1),
|
|
64
|
+
duration: Joi.number()
|
|
65
|
+
.min(CANVAS_CONFIG.LIMITS.minDuration)
|
|
66
|
+
.max(CANVAS_CONFIG.LIMITS.maxDuration)
|
|
67
|
+
.optional(),
|
|
68
|
+
style: Joi.string()
|
|
69
|
+
.valid("character", "word")
|
|
70
|
+
.optional()
|
|
71
|
+
.when("preset", {
|
|
72
|
+
is: Joi.valid("typewriter", "shift"),
|
|
73
|
+
then: Joi.optional(),
|
|
74
|
+
otherwise: Joi.forbidden()
|
|
75
|
+
}),
|
|
76
|
+
direction: Joi.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.when("preset", {
|
|
79
|
+
switch: [
|
|
80
|
+
{ is: "ascend", then: Joi.valid("up", "down") },
|
|
81
|
+
{ is: "shift", then: Joi.valid("left", "right", "up", "down") },
|
|
82
|
+
{ is: "slideIn", then: Joi.valid("left", "right", "up", "down") },
|
|
83
|
+
{ is: "movingLetters", then: Joi.valid("left", "right", "up", "down") }
|
|
84
|
+
],
|
|
85
|
+
otherwise: Joi.forbidden()
|
|
86
|
+
})
|
|
87
|
+
}).unknown(false);
|
|
88
|
+
|
|
89
|
+
const backgroundSchema = Joi.object({
|
|
90
|
+
color: Joi.string().pattern(HEX6).optional(),
|
|
91
|
+
opacity: Joi.number().min(0).max(1).default(1),
|
|
92
|
+
borderRadius: Joi.number().min(0).default(0)
|
|
93
|
+
}).unknown(false);
|
|
94
|
+
|
|
95
|
+
const customFontSchema = Joi.object({
|
|
96
|
+
src: Joi.string().uri().required(),
|
|
97
|
+
family: Joi.string().required(),
|
|
98
|
+
weight: Joi.alternatives().try(Joi.string(), Joi.number()).optional(),
|
|
99
|
+
style: Joi.string().optional(),
|
|
100
|
+
originalFamily: Joi.string().optional()
|
|
101
|
+
}).unknown(false);
|
|
102
|
+
|
|
103
|
+
export const RichTextAssetSchema = Joi.object({
|
|
104
|
+
type: Joi.string().valid("rich-text").required(),
|
|
105
|
+
text: Joi.string().allow("").max(CANVAS_CONFIG.LIMITS.maxTextLength).default(""),
|
|
106
|
+
width: Joi.number()
|
|
107
|
+
.min(CANVAS_CONFIG.LIMITS.minWidth)
|
|
108
|
+
.max(CANVAS_CONFIG.LIMITS.maxWidth)
|
|
109
|
+
.default(CANVAS_CONFIG.DEFAULTS.width)
|
|
110
|
+
.optional(),
|
|
111
|
+
height: Joi.number()
|
|
112
|
+
.min(CANVAS_CONFIG.LIMITS.minHeight)
|
|
113
|
+
.max(CANVAS_CONFIG.LIMITS.maxHeight)
|
|
114
|
+
.default(CANVAS_CONFIG.DEFAULTS.height)
|
|
115
|
+
.optional(),
|
|
116
|
+
font: fontSchema.optional(),
|
|
117
|
+
style: styleSchema.optional(),
|
|
118
|
+
stroke: strokeSchema.optional(),
|
|
119
|
+
shadow: shadowSchema.optional(),
|
|
120
|
+
background: backgroundSchema.optional(),
|
|
121
|
+
align: alignmentSchema.optional(),
|
|
122
|
+
animation: animationSchema.optional(),
|
|
123
|
+
customFonts: Joi.array().items(customFontSchema).optional(),
|
|
124
|
+
cacheEnabled: Joi.boolean().default(true),
|
|
125
|
+
pixelRatio: Joi.number().min(1).max(3).default(CANVAS_CONFIG.DEFAULTS.pixelRatio)
|
|
126
|
+
}).unknown(false);
|
|
127
|
+
|
|
128
|
+
export type RichTextValidated = Required<{
|
|
129
|
+
type: "rich-text";
|
|
130
|
+
text: string;
|
|
131
|
+
width?: number;
|
|
132
|
+
height?: number;
|
|
133
|
+
font?: {
|
|
134
|
+
family: string;
|
|
135
|
+
size: number;
|
|
136
|
+
weight: string | number;
|
|
137
|
+
style: "normal" | "italic" | "oblique";
|
|
138
|
+
color: string;
|
|
139
|
+
opacity: number;
|
|
140
|
+
};
|
|
141
|
+
style?: {
|
|
142
|
+
letterSpacing: number;
|
|
143
|
+
lineHeight: number;
|
|
144
|
+
textTransform: "none" | "uppercase" | "lowercase" | "capitalize";
|
|
145
|
+
textDecoration: "none" | "underline" | "line-through";
|
|
146
|
+
gradient?: {
|
|
147
|
+
type: "linear" | "radial";
|
|
148
|
+
angle: number;
|
|
149
|
+
stops: { offset: number; color: string }[];
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
stroke?: { width: number; color: string; opacity: number };
|
|
153
|
+
shadow?: { offsetX: number; offsetY: number; blur: number; color: string; opacity: number };
|
|
154
|
+
background?: { color?: string; opacity: number; borderRadius: number };
|
|
155
|
+
align?: { horizontal: "left" | "center" | "right"; vertical: "top" | "middle" | "bottom" };
|
|
156
|
+
animation?: {
|
|
157
|
+
preset: typeof CANVAS_CONFIG.ANIMATION_TYPES[number];
|
|
158
|
+
speed: number;
|
|
159
|
+
duration?: number;
|
|
160
|
+
style?: "character" | "word";
|
|
161
|
+
direction?: "left" | "right" | "up" | "down";
|
|
162
|
+
};
|
|
163
|
+
customFonts?: { src: string; family: string; weight?: string | number; style?: string; originalFamily?: string }[];
|
|
164
|
+
cacheEnabled: boolean;
|
|
165
|
+
pixelRatio: number;
|
|
166
|
+
}>;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RichTextValidated } from "./schema/asset-schema";
|
|
2
|
+
|
|
3
|
+
export type RGBA = { r: number; g: number; b: number; a: number };
|
|
4
|
+
|
|
5
|
+
export type GradientSpec =
|
|
6
|
+
| { kind: "solid"; color: string; opacity: number }
|
|
7
|
+
| { kind: "linear"; angle: number; stops: { offset: number; color: string }[]; opacity: number }
|
|
8
|
+
| { kind: "radial"; stops: { offset: number; color: string }[]; opacity: number };
|
|
9
|
+
|
|
10
|
+
export type Glyph = {
|
|
11
|
+
id: number;
|
|
12
|
+
xAdvance: number;
|
|
13
|
+
xOffset: number;
|
|
14
|
+
yOffset: number;
|
|
15
|
+
cluster: number;
|
|
16
|
+
char?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ShapedLine = {
|
|
20
|
+
glyphs: Glyph[];
|
|
21
|
+
width: number;
|
|
22
|
+
y: number; // baseline y
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type DrawOp =
|
|
26
|
+
| { op: "BeginFrame"; width: number; height: number; pixelRatio: number; clear: boolean; bg?: { color?: string; opacity: number; radius: number } }
|
|
27
|
+
| { op: "FillPath"; path: string; x: number; y: number; fill: GradientSpec }
|
|
28
|
+
| { op: "StrokePath"; path: string; x: number; y: number; width: number; color: string; opacity: number }
|
|
29
|
+
| { op: "DecorationLine"; from: { x: number; y: number }; to: { x: number; y: number }; width: number; color: string; opacity: number }
|
|
30
|
+
;
|
|
31
|
+
|
|
32
|
+
export type EngineInit = { width: number; height: number; pixelRatio?: number; fps?: number };
|
|
33
|
+
|
|
34
|
+
export type Renderer = { render(ops: DrawOp[]): Promise<void>; toPNG?: () => Promise<Buffer> };
|
|
35
|
+
|
|
36
|
+
export type ValidAsset = RichTextValidated;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/wasm/hb-loader.ts
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
|
|
4
|
+
let hbSingleton: any | null = null;
|
|
5
|
+
|
|
6
|
+
export async function initHB(wasmBaseURL?: string): Promise<any> {
|
|
7
|
+
if (hbSingleton) return hbSingleton;
|
|
8
|
+
|
|
9
|
+
// Simply import harfbuzzjs and let it handle WASM loading
|
|
10
|
+
const harfbuzzjs = await import('harfbuzzjs');
|
|
11
|
+
|
|
12
|
+
// harfbuzzjs default export is a Promise that resolves to the hb object
|
|
13
|
+
const hbPromise = harfbuzzjs.default;
|
|
14
|
+
|
|
15
|
+
if (typeof hbPromise === 'function') {
|
|
16
|
+
hbSingleton = await hbPromise();
|
|
17
|
+
} else if (hbPromise && typeof hbPromise.then === 'function') {
|
|
18
|
+
hbSingleton = await hbPromise;
|
|
19
|
+
} else {
|
|
20
|
+
hbSingleton = hbPromise;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate the API
|
|
24
|
+
if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
|
|
25
|
+
throw new Error("Failed to initialize HarfBuzz: invalid API");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return hbSingleton;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type HB = any;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compileOnSave": false,
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["ES2022", "DOM"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"skipLibCheck": false,
|
|
14
|
+
"allowJs": false,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationMap": true,
|
|
17
|
+
"sourceMap": true,
|
|
18
|
+
"esModuleInterop": false,
|
|
19
|
+
"resolveJsonModule": true,
|
|
20
|
+
"types": []
|
|
21
|
+
}
|
|
22
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "scripts/**/*"]
|
|
16
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig([
|
|
4
|
+
// Node build
|
|
5
|
+
{
|
|
6
|
+
name: 'node',
|
|
7
|
+
entry: { 'entry.node': 'src/env/entry.node.ts' },
|
|
8
|
+
format: ['esm', 'cjs'],
|
|
9
|
+
dts: true,
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
clean: true,
|
|
12
|
+
platform: 'node',
|
|
13
|
+
target: 'node18',
|
|
14
|
+
// Mark these as external to avoid bundling issues
|
|
15
|
+
external: [
|
|
16
|
+
'canvas',
|
|
17
|
+
'harfbuzzjs',
|
|
18
|
+
'ffmpeg-static',
|
|
19
|
+
'fluent-ffmpeg',
|
|
20
|
+
'child_process',
|
|
21
|
+
'stream',
|
|
22
|
+
'path',
|
|
23
|
+
'fs',
|
|
24
|
+
'node:fs',
|
|
25
|
+
'node:fs/promises',
|
|
26
|
+
'node:path',
|
|
27
|
+
'node:http',
|
|
28
|
+
'node:https',
|
|
29
|
+
'node:stream',
|
|
30
|
+
'node:child_process'
|
|
31
|
+
],
|
|
32
|
+
esbuildOptions(options) {
|
|
33
|
+
options.mainFields = ['module', 'main'];
|
|
34
|
+
options.conditions = ['node', 'import'];
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
// Web build
|
|
38
|
+
{
|
|
39
|
+
name: 'web',
|
|
40
|
+
entry: { 'entry.web': 'src/env/entry.web.ts' },
|
|
41
|
+
format: ['esm'],
|
|
42
|
+
dts: true,
|
|
43
|
+
sourcemap: true,
|
|
44
|
+
platform: 'browser',
|
|
45
|
+
target: 'es2020',
|
|
46
|
+
external: ['harfbuzzjs'],
|
|
47
|
+
esbuildOptions(options) {
|
|
48
|
+
options.mainFields = ['browser', 'module', 'main'];
|
|
49
|
+
options.conditions = ['browser', 'import'];
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]);
|