@shotstack/shotstack-canvas 1.0.9 → 1.1.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 +558 -294
- package/dist/entry.node.cjs.map +1 -1
- package/dist/entry.node.d.cts +20 -1
- package/dist/entry.node.d.ts +20 -1
- package/dist/entry.node.js +555 -292
- package/dist/entry.node.js.map +1 -1
- package/dist/entry.web.d.ts +20 -1
- package/dist/entry.web.js +523 -268
- package/dist/entry.web.js.map +1 -1
- package/package.json +10 -4
- /package/{assets/wasm → public}/hb.wasm +0 -0
package/dist/entry.web.js
CHANGED
|
@@ -132,31 +132,28 @@ var RichTextAssetSchema = Joi.object({
|
|
|
132
132
|
var hbSingleton = null;
|
|
133
133
|
async function initHB(wasmBaseURL) {
|
|
134
134
|
if (hbSingleton) return hbSingleton;
|
|
135
|
-
const harfbuzzjs = await import("harfbuzzjs");
|
|
136
|
-
const factory = harfbuzzjs.default;
|
|
137
|
-
let hbUrlFromImport;
|
|
138
135
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (path.endsWith(".wasm")) {
|
|
149
|
-
return `${base}/${path}`.replace(/([^:])\/{2,}/g, "$1/");
|
|
136
|
+
const mod = await import("harfbuzzjs");
|
|
137
|
+
const candidate = mod.default;
|
|
138
|
+
let hb;
|
|
139
|
+
if (typeof candidate === "function") {
|
|
140
|
+
hb = await candidate();
|
|
141
|
+
} else if (candidate && typeof candidate.then === "function") {
|
|
142
|
+
hb = await candidate;
|
|
143
|
+
} else {
|
|
144
|
+
hb = candidate;
|
|
150
145
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
|
|
147
|
+
throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
|
|
148
|
+
}
|
|
149
|
+
void wasmBaseURL;
|
|
150
|
+
hbSingleton = hb;
|
|
151
|
+
return hbSingleton;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Failed to initialize HarfBuzz: ${err instanceof Error ? err.message : String(err)}`
|
|
155
|
+
);
|
|
158
156
|
}
|
|
159
|
-
return hbSingleton;
|
|
160
157
|
}
|
|
161
158
|
|
|
162
159
|
// src/core/font-registry.ts
|
|
@@ -179,7 +176,13 @@ var FontRegistry = class {
|
|
|
179
176
|
return;
|
|
180
177
|
}
|
|
181
178
|
this.initPromise = this._doInit();
|
|
182
|
-
|
|
179
|
+
try {
|
|
180
|
+
await this.initPromise;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Failed to initialize FontRegistry: ${err instanceof Error ? err.message : String(err)}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
183
186
|
}
|
|
184
187
|
async _doInit() {
|
|
185
188
|
try {
|
|
@@ -191,7 +194,13 @@ var FontRegistry = class {
|
|
|
191
194
|
}
|
|
192
195
|
async getHB() {
|
|
193
196
|
if (!this.hb) {
|
|
194
|
-
|
|
197
|
+
try {
|
|
198
|
+
await this.init();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Failed to get HarfBuzz instance: ${err instanceof Error ? err.message : String(err)}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
195
204
|
}
|
|
196
205
|
return this.hb;
|
|
197
206
|
}
|
|
@@ -199,48 +208,111 @@ var FontRegistry = class {
|
|
|
199
208
|
return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
|
|
200
209
|
}
|
|
201
210
|
async registerFromBytes(bytes, desc) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
try {
|
|
212
|
+
if (!this.hb) await this.init();
|
|
213
|
+
const k = this.key(desc);
|
|
214
|
+
if (this.fonts.has(k)) return;
|
|
215
|
+
const blob = this.hb.createBlob(bytes);
|
|
216
|
+
const face = this.hb.createFace(blob, 0);
|
|
217
|
+
const font = this.hb.createFont(face);
|
|
218
|
+
const upem = face.upem || 1e3;
|
|
219
|
+
font.setScale(upem, upem);
|
|
220
|
+
this.blobs.set(k, blob);
|
|
221
|
+
this.faces.set(k, face);
|
|
222
|
+
this.fonts.set(k, font);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Failed to register font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
213
228
|
}
|
|
214
229
|
async getFont(desc) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
230
|
+
try {
|
|
231
|
+
if (!this.hb) await this.init();
|
|
232
|
+
const k = this.key(desc);
|
|
233
|
+
const f = this.fonts.get(k);
|
|
234
|
+
if (!f) throw new Error(`Font not registered for ${k}`);
|
|
235
|
+
return f;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err instanceof Error && err.message.includes("Font not registered")) {
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Failed to get font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
220
244
|
}
|
|
221
245
|
async getFace(desc) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
246
|
+
try {
|
|
247
|
+
if (!this.hb) await this.init();
|
|
248
|
+
const k = this.key(desc);
|
|
249
|
+
return this.faces.get(k);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Failed to get face for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
225
255
|
}
|
|
226
256
|
async getUnitsPerEm(desc) {
|
|
227
|
-
|
|
228
|
-
|
|
257
|
+
try {
|
|
258
|
+
const face = await this.getFace(desc);
|
|
259
|
+
return face?.upem || 1e3;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Failed to get units per em for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
229
265
|
}
|
|
230
266
|
async glyphPath(desc, glyphId) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
267
|
+
try {
|
|
268
|
+
const font = await this.getFont(desc);
|
|
269
|
+
const path = font.glyphToPath(glyphId);
|
|
270
|
+
return path && path !== "" ? path : "M 0 0";
|
|
271
|
+
} catch (err) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Failed to get glyph path for glyph ${glyphId} in font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
234
276
|
}
|
|
235
277
|
destroy() {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
278
|
+
try {
|
|
279
|
+
for (const [, f] of this.fonts) {
|
|
280
|
+
try {
|
|
281
|
+
f.destroy();
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error(
|
|
284
|
+
`Error destroying font: ${err instanceof Error ? err.message : String(err)}`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const [, f] of this.faces) {
|
|
289
|
+
try {
|
|
290
|
+
f.destroy();
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(
|
|
293
|
+
`Error destroying face: ${err instanceof Error ? err.message : String(err)}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const [, b] of this.blobs) {
|
|
298
|
+
try {
|
|
299
|
+
b.destroy();
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(
|
|
302
|
+
`Error destroying blob: ${err instanceof Error ? err.message : String(err)}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this.fonts.clear();
|
|
307
|
+
this.faces.clear();
|
|
308
|
+
this.blobs.clear();
|
|
309
|
+
this.hb = void 0;
|
|
310
|
+
this.initPromise = void 0;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.error(
|
|
313
|
+
`Error during FontRegistry cleanup: ${err instanceof Error ? err.message : String(err)}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
244
316
|
}
|
|
245
317
|
};
|
|
246
318
|
|
|
@@ -262,112 +334,145 @@ var LayoutEngine = class {
|
|
|
262
334
|
}
|
|
263
335
|
}
|
|
264
336
|
async shapeFull(text, desc) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
337
|
+
try {
|
|
338
|
+
const hb = await this.fonts.getHB();
|
|
339
|
+
const buffer = hb.createBuffer();
|
|
340
|
+
try {
|
|
341
|
+
buffer.addText(text);
|
|
342
|
+
buffer.guessSegmentProperties();
|
|
343
|
+
const font = await this.fonts.getFont(desc);
|
|
344
|
+
const face = await this.fonts.getFace(desc);
|
|
345
|
+
const upem = face?.upem || 1e3;
|
|
346
|
+
font.setScale(upem, upem);
|
|
347
|
+
hb.shape(font, buffer);
|
|
348
|
+
const result = buffer.json();
|
|
349
|
+
return result;
|
|
350
|
+
} finally {
|
|
351
|
+
try {
|
|
352
|
+
buffer.destroy();
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error(
|
|
355
|
+
`Error destroying HarfBuzz buffer: ${err instanceof Error ? err.message : String(err)}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Failed to shape text with font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
277
364
|
}
|
|
278
365
|
async layout(params) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const shaped = await this.shapeFull(input, desc);
|
|
285
|
-
const face = await this.fonts.getFace(desc);
|
|
286
|
-
const upem = face?.upem || 1e3;
|
|
287
|
-
const scale = fontSize / upem;
|
|
288
|
-
const glyphs = shaped.map((g) => {
|
|
289
|
-
const charIndex = g.cl;
|
|
290
|
-
let char;
|
|
291
|
-
if (charIndex >= 0 && charIndex < input.length) {
|
|
292
|
-
char = input[charIndex];
|
|
366
|
+
try {
|
|
367
|
+
const { textTransform, desc, fontSize, letterSpacing, width } = params;
|
|
368
|
+
const input = this.transformText(params.text, textTransform);
|
|
369
|
+
if (!input || input.length === 0) {
|
|
370
|
+
return [];
|
|
293
371
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
cluster: g.cl,
|
|
300
|
-
char
|
|
301
|
-
// This now correctly maps to the original character
|
|
302
|
-
};
|
|
303
|
-
});
|
|
304
|
-
const lines = [];
|
|
305
|
-
let currentLine = [];
|
|
306
|
-
let currentWidth = 0;
|
|
307
|
-
const spaceIndices = /* @__PURE__ */ new Set();
|
|
308
|
-
for (let i = 0; i < input.length; i++) {
|
|
309
|
-
if (input[i] === " ") {
|
|
310
|
-
spaceIndices.add(i);
|
|
372
|
+
let shaped;
|
|
373
|
+
try {
|
|
374
|
+
shaped = await this.shapeFull(input, desc);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
311
377
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
378
|
+
let upem;
|
|
379
|
+
try {
|
|
380
|
+
const face = await this.fonts.getFace(desc);
|
|
381
|
+
upem = face?.upem || 1e3;
|
|
382
|
+
} catch (err) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Failed to get font metrics: ${err instanceof Error ? err.message : String(err)}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const scale = fontSize / upem;
|
|
388
|
+
const glyphs = shaped.map((g) => {
|
|
389
|
+
const charIndex = g.cl;
|
|
390
|
+
let char;
|
|
391
|
+
if (charIndex >= 0 && charIndex < input.length) {
|
|
392
|
+
char = input[charIndex];
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
id: g.g,
|
|
396
|
+
xAdvance: g.ax * scale + letterSpacing,
|
|
397
|
+
xOffset: g.dx * scale,
|
|
398
|
+
yOffset: -g.dy * scale,
|
|
399
|
+
cluster: g.cl,
|
|
400
|
+
char
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
const lines = [];
|
|
404
|
+
let currentLine = [];
|
|
405
|
+
let currentWidth = 0;
|
|
406
|
+
const spaceIndices = /* @__PURE__ */ new Set();
|
|
407
|
+
for (let i = 0; i < input.length; i++) {
|
|
408
|
+
if (input[i] === " ") {
|
|
409
|
+
spaceIndices.add(i);
|
|
324
410
|
}
|
|
325
|
-
currentLine = [];
|
|
326
|
-
currentWidth = 0;
|
|
327
|
-
lastBreakIndex = i;
|
|
328
|
-
continue;
|
|
329
411
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
} else {
|
|
343
|
-
lines.push({
|
|
344
|
-
glyphs: currentLine,
|
|
345
|
-
width: currentWidth,
|
|
346
|
-
y: 0
|
|
347
|
-
});
|
|
412
|
+
let lastBreakIndex = -1;
|
|
413
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
414
|
+
const glyph = glyphs[i];
|
|
415
|
+
const glyphWidth = glyph.xAdvance;
|
|
416
|
+
if (glyph.char === "\n") {
|
|
417
|
+
if (currentLine.length > 0) {
|
|
418
|
+
lines.push({
|
|
419
|
+
glyphs: currentLine,
|
|
420
|
+
width: currentWidth,
|
|
421
|
+
y: 0
|
|
422
|
+
});
|
|
423
|
+
}
|
|
348
424
|
currentLine = [];
|
|
349
425
|
currentWidth = 0;
|
|
426
|
+
lastBreakIndex = i;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (currentWidth + glyphWidth > width && currentLine.length > 0) {
|
|
430
|
+
if (lastBreakIndex > -1) {
|
|
431
|
+
const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
|
|
432
|
+
const nextLine = currentLine.splice(breakPoint);
|
|
433
|
+
const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
|
|
434
|
+
lines.push({
|
|
435
|
+
glyphs: currentLine,
|
|
436
|
+
width: lineWidth,
|
|
437
|
+
y: 0
|
|
438
|
+
});
|
|
439
|
+
currentLine = nextLine;
|
|
440
|
+
currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
|
|
441
|
+
} else {
|
|
442
|
+
lines.push({
|
|
443
|
+
glyphs: currentLine,
|
|
444
|
+
width: currentWidth,
|
|
445
|
+
y: 0
|
|
446
|
+
});
|
|
447
|
+
currentLine = [];
|
|
448
|
+
currentWidth = 0;
|
|
449
|
+
}
|
|
450
|
+
lastBreakIndex = -1;
|
|
451
|
+
}
|
|
452
|
+
currentLine.push(glyph);
|
|
453
|
+
currentWidth += glyphWidth;
|
|
454
|
+
if (spaceIndices.has(glyph.cluster)) {
|
|
455
|
+
lastBreakIndex = i;
|
|
350
456
|
}
|
|
351
|
-
lastBreakIndex = -1;
|
|
352
457
|
}
|
|
353
|
-
currentLine.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
458
|
+
if (currentLine.length > 0) {
|
|
459
|
+
lines.push({
|
|
460
|
+
glyphs: currentLine,
|
|
461
|
+
width: currentWidth,
|
|
462
|
+
y: 0
|
|
463
|
+
});
|
|
357
464
|
}
|
|
465
|
+
const lineHeight = params.lineHeight * fontSize;
|
|
466
|
+
for (let i = 0; i < lines.length; i++) {
|
|
467
|
+
lines[i].y = (i + 1) * lineHeight;
|
|
468
|
+
}
|
|
469
|
+
return lines;
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (err instanceof Error) {
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
throw new Error(`Layout failed: ${String(err)}`);
|
|
358
475
|
}
|
|
359
|
-
if (currentLine.length > 0) {
|
|
360
|
-
lines.push({
|
|
361
|
-
glyphs: currentLine,
|
|
362
|
-
width: currentWidth,
|
|
363
|
-
y: 0
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
const lineHeight = params.lineHeight * fontSize;
|
|
367
|
-
for (let i = 0; i < lines.length; i++) {
|
|
368
|
-
lines[i].y = (i + 1) * lineHeight;
|
|
369
|
-
}
|
|
370
|
-
return lines;
|
|
371
476
|
}
|
|
372
477
|
};
|
|
373
478
|
|
|
@@ -459,7 +564,6 @@ async function buildDrawOps(p) {
|
|
|
459
564
|
path,
|
|
460
565
|
x: glyphX + p.shadow.offsetX,
|
|
461
566
|
y: glyphY + p.shadow.offsetY,
|
|
462
|
-
// @ts-ignore scale propagated to painters
|
|
463
567
|
scale,
|
|
464
568
|
fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
|
|
465
569
|
});
|
|
@@ -470,7 +574,6 @@ async function buildDrawOps(p) {
|
|
|
470
574
|
path,
|
|
471
575
|
x: glyphX,
|
|
472
576
|
y: glyphY,
|
|
473
|
-
// @ts-ignore scale propagated to painters
|
|
474
577
|
scale,
|
|
475
578
|
width: p.stroke.width,
|
|
476
579
|
color: p.stroke.color,
|
|
@@ -482,7 +585,6 @@ async function buildDrawOps(p) {
|
|
|
482
585
|
path,
|
|
483
586
|
x: glyphX,
|
|
484
587
|
y: glyphY,
|
|
485
|
-
// @ts-ignore scale propagated to painters
|
|
486
588
|
scale,
|
|
487
589
|
fill
|
|
488
590
|
});
|
|
@@ -1025,7 +1127,8 @@ function createWebPainter(canvas) {
|
|
|
1025
1127
|
for (const op of ops) {
|
|
1026
1128
|
if (op.op === "BeginFrame") {
|
|
1027
1129
|
const dpr = op.pixelRatio;
|
|
1028
|
-
const w = op.width
|
|
1130
|
+
const w = op.width;
|
|
1131
|
+
const h = op.height;
|
|
1029
1132
|
if ("width" in canvas && "height" in canvas) {
|
|
1030
1133
|
canvas.width = Math.floor(w * dpr);
|
|
1031
1134
|
canvas.height = Math.floor(h * dpr);
|
|
@@ -1048,28 +1151,30 @@ function createWebPainter(canvas) {
|
|
|
1048
1151
|
continue;
|
|
1049
1152
|
}
|
|
1050
1153
|
if (op.op === "FillPath") {
|
|
1051
|
-
const
|
|
1154
|
+
const fillOp = op;
|
|
1155
|
+
const p = new Path2D(fillOp.path);
|
|
1052
1156
|
ctx.save();
|
|
1053
|
-
ctx.translate(
|
|
1054
|
-
const s =
|
|
1157
|
+
ctx.translate(fillOp.x, fillOp.y);
|
|
1158
|
+
const s = fillOp.scale ?? 1;
|
|
1055
1159
|
ctx.scale(s, -s);
|
|
1056
|
-
const bbox =
|
|
1057
|
-
const fill = makeGradientFromBBox(ctx,
|
|
1160
|
+
const bbox = fillOp.gradientBBox ?? globalBox;
|
|
1161
|
+
const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
|
|
1058
1162
|
ctx.fillStyle = fill;
|
|
1059
1163
|
ctx.fill(p);
|
|
1060
1164
|
ctx.restore();
|
|
1061
1165
|
continue;
|
|
1062
1166
|
}
|
|
1063
1167
|
if (op.op === "StrokePath") {
|
|
1064
|
-
const
|
|
1168
|
+
const strokeOp = op;
|
|
1169
|
+
const p = new Path2D(strokeOp.path);
|
|
1065
1170
|
ctx.save();
|
|
1066
|
-
ctx.translate(
|
|
1067
|
-
const s =
|
|
1171
|
+
ctx.translate(strokeOp.x, strokeOp.y);
|
|
1172
|
+
const s = strokeOp.scale ?? 1;
|
|
1068
1173
|
ctx.scale(s, -s);
|
|
1069
1174
|
const invAbs = 1 / Math.abs(s);
|
|
1070
|
-
const c = parseHex6(
|
|
1175
|
+
const c = parseHex6(strokeOp.color, strokeOp.opacity);
|
|
1071
1176
|
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1072
|
-
ctx.lineWidth =
|
|
1177
|
+
ctx.lineWidth = strokeOp.width * invAbs;
|
|
1073
1178
|
ctx.lineJoin = "round";
|
|
1074
1179
|
ctx.lineCap = "round";
|
|
1075
1180
|
ctx.stroke(p);
|
|
@@ -1109,17 +1214,20 @@ function makeGradientFromBBox(ctx, spec, box) {
|
|
|
1109
1214
|
const c = parseHex6(spec.color, spec.opacity);
|
|
1110
1215
|
return `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1111
1216
|
}
|
|
1112
|
-
const cx = box.x + box.w / 2
|
|
1217
|
+
const cx = box.x + box.w / 2;
|
|
1218
|
+
const cy = box.y + box.h / 2;
|
|
1219
|
+
const r = Math.max(box.w, box.h) / 2;
|
|
1113
1220
|
const addStops = (g) => {
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1221
|
+
const opacity = spec.kind === "linear" || spec.kind === "radial" ? spec.opacity : 1;
|
|
1222
|
+
const stops = spec.kind === "linear" || spec.kind === "radial" ? spec.stops : [];
|
|
1223
|
+
for (const s of stops) {
|
|
1224
|
+
const c = parseHex6(s.color, opacity);
|
|
1117
1225
|
g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
|
|
1118
1226
|
}
|
|
1119
1227
|
return g;
|
|
1120
1228
|
};
|
|
1121
1229
|
if (spec.kind === "linear") {
|
|
1122
|
-
const rad =
|
|
1230
|
+
const rad = spec.angle * Math.PI / 180;
|
|
1123
1231
|
const x1 = cx + Math.cos(rad + Math.PI) * r;
|
|
1124
1232
|
const y1 = cy + Math.sin(rad + Math.PI) * r;
|
|
1125
1233
|
const x2 = cx + Math.cos(rad) * r;
|
|
@@ -1130,27 +1238,42 @@ function makeGradientFromBBox(ctx, spec, box) {
|
|
|
1130
1238
|
}
|
|
1131
1239
|
}
|
|
1132
1240
|
function computeGlobalTextBounds(ops) {
|
|
1133
|
-
let minX = Infinity
|
|
1241
|
+
let minX = Infinity;
|
|
1242
|
+
let minY = Infinity;
|
|
1243
|
+
let maxX = -Infinity;
|
|
1244
|
+
let maxY = -Infinity;
|
|
1134
1245
|
for (const op of ops) {
|
|
1135
|
-
if (op.op !== "FillPath"
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
const
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1246
|
+
if (op.op !== "FillPath") continue;
|
|
1247
|
+
const fillOp = op;
|
|
1248
|
+
if (fillOp.isShadow) continue;
|
|
1249
|
+
const b = computePathBounds2(fillOp.path);
|
|
1250
|
+
const s = fillOp.scale ?? 1;
|
|
1251
|
+
const x1 = fillOp.x + s * b.x;
|
|
1252
|
+
const x2 = fillOp.x + s * (b.x + b.w);
|
|
1253
|
+
const y1 = fillOp.y - s * (b.y + b.h);
|
|
1254
|
+
const y2 = fillOp.y - s * b.y;
|
|
1142
1255
|
if (x1 < minX) minX = x1;
|
|
1143
1256
|
if (y1 < minY) minY = y1;
|
|
1144
1257
|
if (x2 > maxX) maxX = x2;
|
|
1145
1258
|
if (y2 > maxY) maxY = y2;
|
|
1146
1259
|
}
|
|
1147
|
-
if (minX === Infinity)
|
|
1148
|
-
|
|
1260
|
+
if (minX === Infinity) {
|
|
1261
|
+
return { x: 0, y: 0, w: 1, h: 1 };
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
x: minX,
|
|
1265
|
+
y: minY,
|
|
1266
|
+
w: Math.max(1, maxX - minX),
|
|
1267
|
+
h: Math.max(1, maxY - minY)
|
|
1268
|
+
};
|
|
1149
1269
|
}
|
|
1150
1270
|
function computePathBounds2(d) {
|
|
1151
1271
|
const tokens = tokenizePath2(d);
|
|
1152
1272
|
let i = 0;
|
|
1153
|
-
let minX = Infinity
|
|
1273
|
+
let minX = Infinity;
|
|
1274
|
+
let minY = Infinity;
|
|
1275
|
+
let maxX = -Infinity;
|
|
1276
|
+
let maxY = -Infinity;
|
|
1154
1277
|
const touch = (x, y) => {
|
|
1155
1278
|
if (x < minX) minX = x;
|
|
1156
1279
|
if (y < minY) minY = y;
|
|
@@ -1190,10 +1313,19 @@ function computePathBounds2(d) {
|
|
|
1190
1313
|
}
|
|
1191
1314
|
case "Z":
|
|
1192
1315
|
break;
|
|
1316
|
+
default:
|
|
1317
|
+
break;
|
|
1193
1318
|
}
|
|
1194
1319
|
}
|
|
1195
|
-
if (minX === Infinity)
|
|
1196
|
-
|
|
1320
|
+
if (minX === Infinity) {
|
|
1321
|
+
return { x: 0, y: 0, w: 0, h: 0 };
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
x: minX,
|
|
1325
|
+
y: minY,
|
|
1326
|
+
w: maxX - minX,
|
|
1327
|
+
h: maxY - minY
|
|
1328
|
+
};
|
|
1197
1329
|
}
|
|
1198
1330
|
function tokenizePath2(d) {
|
|
1199
1331
|
return d.match(/[MLCQZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? [];
|
|
@@ -1201,11 +1333,37 @@ function tokenizePath2(d) {
|
|
|
1201
1333
|
|
|
1202
1334
|
// src/io/web.ts
|
|
1203
1335
|
async function fetchToArrayBuffer(url) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1336
|
+
try {
|
|
1337
|
+
const res = await fetch(url);
|
|
1338
|
+
if (!res.ok) {
|
|
1339
|
+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
return await res.arrayBuffer();
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
throw new Error(
|
|
1345
|
+
`Failed to read response body as ArrayBuffer from ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
if (err instanceof Error) {
|
|
1350
|
+
if (err.message.includes("Failed to fetch") || err.message.includes("Failed to read")) {
|
|
1351
|
+
throw err;
|
|
1352
|
+
}
|
|
1353
|
+
throw new Error(`Failed to fetch ${url}: ${err.message}`);
|
|
1354
|
+
}
|
|
1355
|
+
throw new Error(`Failed to fetch ${url}: ${String(err)}`);
|
|
1356
|
+
}
|
|
1207
1357
|
}
|
|
1208
1358
|
|
|
1359
|
+
// src/types.ts
|
|
1360
|
+
var isShadowFill2 = (op) => {
|
|
1361
|
+
return op.op === "FillPath" && op.isShadow === true;
|
|
1362
|
+
};
|
|
1363
|
+
var isGlyphFill2 = (op) => {
|
|
1364
|
+
return op.op === "FillPath" && op.isShadow !== true;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1209
1367
|
// src/env/entry.web.ts
|
|
1210
1368
|
async function createTextEngine(opts = {}) {
|
|
1211
1369
|
const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
|
|
@@ -1214,109 +1372,206 @@ async function createTextEngine(opts = {}) {
|
|
|
1214
1372
|
const wasmBaseURL = opts.wasmBaseURL;
|
|
1215
1373
|
const fonts = new FontRegistry(wasmBaseURL);
|
|
1216
1374
|
const layout = new LayoutEngine(fonts);
|
|
1217
|
-
|
|
1375
|
+
try {
|
|
1376
|
+
await fonts.init();
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
throw new Error(
|
|
1379
|
+
`Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1218
1382
|
async function ensureFonts(asset) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1383
|
+
try {
|
|
1384
|
+
if (asset.customFonts) {
|
|
1385
|
+
for (const cf of asset.customFonts) {
|
|
1386
|
+
try {
|
|
1387
|
+
const bytes = await fetchToArrayBuffer(cf.src);
|
|
1388
|
+
await fonts.registerFromBytes(bytes, {
|
|
1389
|
+
family: cf.family,
|
|
1390
|
+
weight: cf.weight ?? "400",
|
|
1391
|
+
style: cf.style ?? "normal"
|
|
1392
|
+
});
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
`Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const main = asset.font ?? {
|
|
1401
|
+
family: "Roboto",
|
|
1402
|
+
weight: "400",
|
|
1403
|
+
style: "normal",
|
|
1404
|
+
size: 48,
|
|
1405
|
+
color: "#000000",
|
|
1406
|
+
opacity: 1
|
|
1407
|
+
};
|
|
1408
|
+
return main;
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
if (err instanceof Error) {
|
|
1411
|
+
throw err;
|
|
1227
1412
|
}
|
|
1413
|
+
throw new Error(`Failed to ensure fonts: ${String(err)}`);
|
|
1228
1414
|
}
|
|
1229
|
-
const main = asset.font ?? {
|
|
1230
|
-
family: "Roboto",
|
|
1231
|
-
weight: "400",
|
|
1232
|
-
style: "normal",
|
|
1233
|
-
size: 48,
|
|
1234
|
-
color: "#000000",
|
|
1235
|
-
opacity: 1
|
|
1236
|
-
};
|
|
1237
|
-
return main;
|
|
1238
1415
|
}
|
|
1239
1416
|
return {
|
|
1240
1417
|
validate(input) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1418
|
+
try {
|
|
1419
|
+
const { value, error } = RichTextAssetSchema.validate(input, {
|
|
1420
|
+
abortEarly: false,
|
|
1421
|
+
convert: true
|
|
1422
|
+
});
|
|
1423
|
+
if (error) throw error;
|
|
1424
|
+
return { value };
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
if (err instanceof Error) {
|
|
1427
|
+
throw new Error(`Validation failed: ${err.message}`);
|
|
1428
|
+
}
|
|
1429
|
+
throw new Error(`Validation failed: ${String(err)}`);
|
|
1430
|
+
}
|
|
1247
1431
|
},
|
|
1248
1432
|
async registerFontFromUrl(url, desc) {
|
|
1249
|
-
|
|
1250
|
-
|
|
1433
|
+
try {
|
|
1434
|
+
const bytes = await fetchToArrayBuffer(url);
|
|
1435
|
+
await fonts.registerFromBytes(bytes, desc);
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
throw new Error(
|
|
1438
|
+
`Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1251
1441
|
},
|
|
1252
1442
|
async registerFontFromFile(source, desc) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1443
|
+
try {
|
|
1444
|
+
let bytes;
|
|
1445
|
+
if (typeof source === "string") {
|
|
1446
|
+
try {
|
|
1447
|
+
bytes = await fetchToArrayBuffer(source);
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
throw new Error(
|
|
1450
|
+
`Failed to fetch font from ${source}: ${err instanceof Error ? err.message : String(err)}`
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
} else {
|
|
1454
|
+
try {
|
|
1455
|
+
bytes = await source.arrayBuffer();
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
`Failed to read Blob as ArrayBuffer: ${err instanceof Error ? err.message : String(err)}`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
await fonts.registerFromBytes(bytes, desc);
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
if (err instanceof Error) {
|
|
1465
|
+
throw err;
|
|
1466
|
+
}
|
|
1467
|
+
throw new Error(
|
|
1468
|
+
`Failed to register font "${desc.family}" from ${typeof source === "string" ? source : "Blob"}: ${String(err)}`
|
|
1469
|
+
);
|
|
1258
1470
|
}
|
|
1259
|
-
await fonts.registerFromBytes(bytes, desc);
|
|
1260
1471
|
},
|
|
1261
1472
|
async renderFrame(asset, tSeconds) {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1473
|
+
try {
|
|
1474
|
+
const main = await ensureFonts(asset);
|
|
1475
|
+
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
1476
|
+
let lines;
|
|
1477
|
+
try {
|
|
1478
|
+
lines = await layout.layout({
|
|
1479
|
+
text: asset.text,
|
|
1480
|
+
width: asset.width ?? width,
|
|
1481
|
+
letterSpacing: asset.style?.letterSpacing ?? 0,
|
|
1482
|
+
fontSize: main.size,
|
|
1483
|
+
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
1484
|
+
desc,
|
|
1485
|
+
textTransform: asset.style?.textTransform ?? "none"
|
|
1486
|
+
});
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
throw new Error(
|
|
1489
|
+
`Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
const textRect = {
|
|
1493
|
+
x: 0,
|
|
1494
|
+
y: 0,
|
|
1495
|
+
width: asset.width ?? width,
|
|
1496
|
+
height: asset.height ?? height
|
|
1497
|
+
};
|
|
1498
|
+
let ops0;
|
|
1499
|
+
try {
|
|
1500
|
+
ops0 = await buildDrawOps({
|
|
1501
|
+
canvas: { width, height, pixelRatio },
|
|
1502
|
+
textRect,
|
|
1503
|
+
lines,
|
|
1504
|
+
font: {
|
|
1505
|
+
family: main.family,
|
|
1506
|
+
size: main.size,
|
|
1507
|
+
weight: `${main.weight}`,
|
|
1508
|
+
style: main.style,
|
|
1509
|
+
color: main.color,
|
|
1510
|
+
opacity: main.opacity
|
|
1511
|
+
},
|
|
1512
|
+
style: {
|
|
1513
|
+
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
1514
|
+
textDecoration: asset.style?.textDecoration ?? "none",
|
|
1515
|
+
gradient: asset.style?.gradient
|
|
1516
|
+
},
|
|
1517
|
+
stroke: asset.stroke,
|
|
1518
|
+
shadow: asset.shadow,
|
|
1519
|
+
align: asset.align ?? { horizontal: "left", vertical: "middle" },
|
|
1520
|
+
background: asset.background,
|
|
1521
|
+
glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
|
|
1522
|
+
getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
|
|
1523
|
+
});
|
|
1524
|
+
} catch (err) {
|
|
1525
|
+
throw new Error(
|
|
1526
|
+
`Failed to build draw operations: ${err instanceof Error ? err.message : String(err)}`
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
const ops = applyAnimation(ops0, lines, {
|
|
1531
|
+
t: tSeconds,
|
|
1532
|
+
fontSize: main.size,
|
|
1533
|
+
anim: asset.animation ? {
|
|
1534
|
+
preset: asset.animation.preset,
|
|
1535
|
+
speed: asset.animation.speed,
|
|
1536
|
+
duration: asset.animation.duration,
|
|
1537
|
+
style: asset.animation.style,
|
|
1538
|
+
direction: asset.animation.direction
|
|
1539
|
+
} : void 0
|
|
1540
|
+
});
|
|
1541
|
+
return ops;
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
throw new Error(
|
|
1544
|
+
`Failed to apply animation: ${err instanceof Error ? err.message : String(err)}`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
if (err instanceof Error) {
|
|
1549
|
+
throw err;
|
|
1550
|
+
}
|
|
1551
|
+
throw new Error(`Failed to render frame at time ${tSeconds}s: ${String(err)}`);
|
|
1552
|
+
}
|
|
1310
1553
|
},
|
|
1311
1554
|
createRenderer(canvas) {
|
|
1312
|
-
|
|
1555
|
+
try {
|
|
1556
|
+
return createWebPainter(canvas);
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
throw new Error(
|
|
1559
|
+
`Failed to create renderer: ${err instanceof Error ? err.message : String(err)}`
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1313
1562
|
},
|
|
1314
1563
|
destroy() {
|
|
1315
|
-
|
|
1564
|
+
try {
|
|
1565
|
+
fonts.destroy();
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
|
|
1568
|
+
}
|
|
1316
1569
|
}
|
|
1317
1570
|
};
|
|
1318
1571
|
}
|
|
1319
1572
|
export {
|
|
1320
|
-
createTextEngine
|
|
1573
|
+
createTextEngine,
|
|
1574
|
+
isGlyphFill2 as isGlyphFill,
|
|
1575
|
+
isShadowFill2 as isShadowFill
|
|
1321
1576
|
};
|
|
1322
1577
|
//# sourceMappingURL=entry.web.js.map
|