@shotstack/shotstack-canvas 1.1.0 → 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 +560 -283
- 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 +557 -282
- package/dist/entry.node.js.map +1 -1
- package/dist/entry.web.d.ts +20 -1
- package/dist/entry.web.js +525 -258
- package/dist/entry.web.js.map +1 -1
- package/package.json +10 -4
package/dist/entry.web.js
CHANGED
|
@@ -132,19 +132,28 @@ var RichTextAssetSchema = Joi.object({
|
|
|
132
132
|
var hbSingleton = null;
|
|
133
133
|
async function initHB(wasmBaseURL) {
|
|
134
134
|
if (hbSingleton) return hbSingleton;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
try {
|
|
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;
|
|
145
|
+
}
|
|
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
|
+
);
|
|
146
156
|
}
|
|
147
|
-
return hbSingleton;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
// src/core/font-registry.ts
|
|
@@ -167,7 +176,13 @@ var FontRegistry = class {
|
|
|
167
176
|
return;
|
|
168
177
|
}
|
|
169
178
|
this.initPromise = this._doInit();
|
|
170
|
-
|
|
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
|
+
}
|
|
171
186
|
}
|
|
172
187
|
async _doInit() {
|
|
173
188
|
try {
|
|
@@ -179,7 +194,13 @@ var FontRegistry = class {
|
|
|
179
194
|
}
|
|
180
195
|
async getHB() {
|
|
181
196
|
if (!this.hb) {
|
|
182
|
-
|
|
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
|
+
}
|
|
183
204
|
}
|
|
184
205
|
return this.hb;
|
|
185
206
|
}
|
|
@@ -187,48 +208,111 @@ var FontRegistry = class {
|
|
|
187
208
|
return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
|
|
188
209
|
}
|
|
189
210
|
async registerFromBytes(bytes, desc) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
228
|
}
|
|
202
229
|
async getFont(desc) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
}
|
|
208
244
|
}
|
|
209
245
|
async getFace(desc) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
255
|
}
|
|
214
256
|
async getUnitsPerEm(desc) {
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
}
|
|
217
265
|
}
|
|
218
266
|
async glyphPath(desc, glyphId) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
}
|
|
222
276
|
}
|
|
223
277
|
destroy() {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
}
|
|
232
316
|
}
|
|
233
317
|
};
|
|
234
318
|
|
|
@@ -250,112 +334,145 @@ var LayoutEngine = class {
|
|
|
250
334
|
}
|
|
251
335
|
}
|
|
252
336
|
async shapeFull(text, desc) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
}
|
|
265
364
|
}
|
|
266
365
|
async layout(params) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const shaped = await this.shapeFull(input, desc);
|
|
273
|
-
const face = await this.fonts.getFace(desc);
|
|
274
|
-
const upem = face?.upem || 1e3;
|
|
275
|
-
const scale = fontSize / upem;
|
|
276
|
-
const glyphs = shaped.map((g) => {
|
|
277
|
-
const charIndex = g.cl;
|
|
278
|
-
let char;
|
|
279
|
-
if (charIndex >= 0 && charIndex < input.length) {
|
|
280
|
-
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 [];
|
|
281
371
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
cluster: g.cl,
|
|
288
|
-
char
|
|
289
|
-
// This now correctly maps to the original character
|
|
290
|
-
};
|
|
291
|
-
});
|
|
292
|
-
const lines = [];
|
|
293
|
-
let currentLine = [];
|
|
294
|
-
let currentWidth = 0;
|
|
295
|
-
const spaceIndices = /* @__PURE__ */ new Set();
|
|
296
|
-
for (let i = 0; i < input.length; i++) {
|
|
297
|
-
if (input[i] === " ") {
|
|
298
|
-
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)}`);
|
|
299
377
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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);
|
|
312
410
|
}
|
|
313
|
-
currentLine = [];
|
|
314
|
-
currentWidth = 0;
|
|
315
|
-
lastBreakIndex = i;
|
|
316
|
-
continue;
|
|
317
411
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
} else {
|
|
331
|
-
lines.push({
|
|
332
|
-
glyphs: currentLine,
|
|
333
|
-
width: currentWidth,
|
|
334
|
-
y: 0
|
|
335
|
-
});
|
|
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
|
+
}
|
|
336
424
|
currentLine = [];
|
|
337
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;
|
|
338
456
|
}
|
|
339
|
-
lastBreakIndex = -1;
|
|
340
457
|
}
|
|
341
|
-
currentLine.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
458
|
+
if (currentLine.length > 0) {
|
|
459
|
+
lines.push({
|
|
460
|
+
glyphs: currentLine,
|
|
461
|
+
width: currentWidth,
|
|
462
|
+
y: 0
|
|
463
|
+
});
|
|
345
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)}`);
|
|
346
475
|
}
|
|
347
|
-
if (currentLine.length > 0) {
|
|
348
|
-
lines.push({
|
|
349
|
-
glyphs: currentLine,
|
|
350
|
-
width: currentWidth,
|
|
351
|
-
y: 0
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
const lineHeight = params.lineHeight * fontSize;
|
|
355
|
-
for (let i = 0; i < lines.length; i++) {
|
|
356
|
-
lines[i].y = (i + 1) * lineHeight;
|
|
357
|
-
}
|
|
358
|
-
return lines;
|
|
359
476
|
}
|
|
360
477
|
};
|
|
361
478
|
|
|
@@ -447,7 +564,6 @@ async function buildDrawOps(p) {
|
|
|
447
564
|
path,
|
|
448
565
|
x: glyphX + p.shadow.offsetX,
|
|
449
566
|
y: glyphY + p.shadow.offsetY,
|
|
450
|
-
// @ts-ignore scale propagated to painters
|
|
451
567
|
scale,
|
|
452
568
|
fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
|
|
453
569
|
});
|
|
@@ -458,7 +574,6 @@ async function buildDrawOps(p) {
|
|
|
458
574
|
path,
|
|
459
575
|
x: glyphX,
|
|
460
576
|
y: glyphY,
|
|
461
|
-
// @ts-ignore scale propagated to painters
|
|
462
577
|
scale,
|
|
463
578
|
width: p.stroke.width,
|
|
464
579
|
color: p.stroke.color,
|
|
@@ -470,7 +585,6 @@ async function buildDrawOps(p) {
|
|
|
470
585
|
path,
|
|
471
586
|
x: glyphX,
|
|
472
587
|
y: glyphY,
|
|
473
|
-
// @ts-ignore scale propagated to painters
|
|
474
588
|
scale,
|
|
475
589
|
fill
|
|
476
590
|
});
|
|
@@ -1013,7 +1127,8 @@ function createWebPainter(canvas) {
|
|
|
1013
1127
|
for (const op of ops) {
|
|
1014
1128
|
if (op.op === "BeginFrame") {
|
|
1015
1129
|
const dpr = op.pixelRatio;
|
|
1016
|
-
const w = op.width
|
|
1130
|
+
const w = op.width;
|
|
1131
|
+
const h = op.height;
|
|
1017
1132
|
if ("width" in canvas && "height" in canvas) {
|
|
1018
1133
|
canvas.width = Math.floor(w * dpr);
|
|
1019
1134
|
canvas.height = Math.floor(h * dpr);
|
|
@@ -1036,28 +1151,30 @@ function createWebPainter(canvas) {
|
|
|
1036
1151
|
continue;
|
|
1037
1152
|
}
|
|
1038
1153
|
if (op.op === "FillPath") {
|
|
1039
|
-
const
|
|
1154
|
+
const fillOp = op;
|
|
1155
|
+
const p = new Path2D(fillOp.path);
|
|
1040
1156
|
ctx.save();
|
|
1041
|
-
ctx.translate(
|
|
1042
|
-
const s =
|
|
1157
|
+
ctx.translate(fillOp.x, fillOp.y);
|
|
1158
|
+
const s = fillOp.scale ?? 1;
|
|
1043
1159
|
ctx.scale(s, -s);
|
|
1044
|
-
const bbox =
|
|
1045
|
-
const fill = makeGradientFromBBox(ctx,
|
|
1160
|
+
const bbox = fillOp.gradientBBox ?? globalBox;
|
|
1161
|
+
const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
|
|
1046
1162
|
ctx.fillStyle = fill;
|
|
1047
1163
|
ctx.fill(p);
|
|
1048
1164
|
ctx.restore();
|
|
1049
1165
|
continue;
|
|
1050
1166
|
}
|
|
1051
1167
|
if (op.op === "StrokePath") {
|
|
1052
|
-
const
|
|
1168
|
+
const strokeOp = op;
|
|
1169
|
+
const p = new Path2D(strokeOp.path);
|
|
1053
1170
|
ctx.save();
|
|
1054
|
-
ctx.translate(
|
|
1055
|
-
const s =
|
|
1171
|
+
ctx.translate(strokeOp.x, strokeOp.y);
|
|
1172
|
+
const s = strokeOp.scale ?? 1;
|
|
1056
1173
|
ctx.scale(s, -s);
|
|
1057
1174
|
const invAbs = 1 / Math.abs(s);
|
|
1058
|
-
const c = parseHex6(
|
|
1175
|
+
const c = parseHex6(strokeOp.color, strokeOp.opacity);
|
|
1059
1176
|
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1060
|
-
ctx.lineWidth =
|
|
1177
|
+
ctx.lineWidth = strokeOp.width * invAbs;
|
|
1061
1178
|
ctx.lineJoin = "round";
|
|
1062
1179
|
ctx.lineCap = "round";
|
|
1063
1180
|
ctx.stroke(p);
|
|
@@ -1097,17 +1214,20 @@ function makeGradientFromBBox(ctx, spec, box) {
|
|
|
1097
1214
|
const c = parseHex6(spec.color, spec.opacity);
|
|
1098
1215
|
return `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1099
1216
|
}
|
|
1100
|
-
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;
|
|
1101
1220
|
const addStops = (g) => {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
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);
|
|
1105
1225
|
g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
|
|
1106
1226
|
}
|
|
1107
1227
|
return g;
|
|
1108
1228
|
};
|
|
1109
1229
|
if (spec.kind === "linear") {
|
|
1110
|
-
const rad =
|
|
1230
|
+
const rad = spec.angle * Math.PI / 180;
|
|
1111
1231
|
const x1 = cx + Math.cos(rad + Math.PI) * r;
|
|
1112
1232
|
const y1 = cy + Math.sin(rad + Math.PI) * r;
|
|
1113
1233
|
const x2 = cx + Math.cos(rad) * r;
|
|
@@ -1118,27 +1238,42 @@ function makeGradientFromBBox(ctx, spec, box) {
|
|
|
1118
1238
|
}
|
|
1119
1239
|
}
|
|
1120
1240
|
function computeGlobalTextBounds(ops) {
|
|
1121
|
-
let minX = Infinity
|
|
1241
|
+
let minX = Infinity;
|
|
1242
|
+
let minY = Infinity;
|
|
1243
|
+
let maxX = -Infinity;
|
|
1244
|
+
let maxY = -Infinity;
|
|
1122
1245
|
for (const op of ops) {
|
|
1123
|
-
if (op.op !== "FillPath"
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
const
|
|
1127
|
-
const
|
|
1128
|
-
const
|
|
1129
|
-
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;
|
|
1130
1255
|
if (x1 < minX) minX = x1;
|
|
1131
1256
|
if (y1 < minY) minY = y1;
|
|
1132
1257
|
if (x2 > maxX) maxX = x2;
|
|
1133
1258
|
if (y2 > maxY) maxY = y2;
|
|
1134
1259
|
}
|
|
1135
|
-
if (minX === Infinity)
|
|
1136
|
-
|
|
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
|
+
};
|
|
1137
1269
|
}
|
|
1138
1270
|
function computePathBounds2(d) {
|
|
1139
1271
|
const tokens = tokenizePath2(d);
|
|
1140
1272
|
let i = 0;
|
|
1141
|
-
let minX = Infinity
|
|
1273
|
+
let minX = Infinity;
|
|
1274
|
+
let minY = Infinity;
|
|
1275
|
+
let maxX = -Infinity;
|
|
1276
|
+
let maxY = -Infinity;
|
|
1142
1277
|
const touch = (x, y) => {
|
|
1143
1278
|
if (x < minX) minX = x;
|
|
1144
1279
|
if (y < minY) minY = y;
|
|
@@ -1178,10 +1313,19 @@ function computePathBounds2(d) {
|
|
|
1178
1313
|
}
|
|
1179
1314
|
case "Z":
|
|
1180
1315
|
break;
|
|
1316
|
+
default:
|
|
1317
|
+
break;
|
|
1181
1318
|
}
|
|
1182
1319
|
}
|
|
1183
|
-
if (minX === Infinity)
|
|
1184
|
-
|
|
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
|
+
};
|
|
1185
1329
|
}
|
|
1186
1330
|
function tokenizePath2(d) {
|
|
1187
1331
|
return d.match(/[MLCQZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? [];
|
|
@@ -1189,11 +1333,37 @@ function tokenizePath2(d) {
|
|
|
1189
1333
|
|
|
1190
1334
|
// src/io/web.ts
|
|
1191
1335
|
async function fetchToArrayBuffer(url) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
+
}
|
|
1195
1357
|
}
|
|
1196
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
|
+
|
|
1197
1367
|
// src/env/entry.web.ts
|
|
1198
1368
|
async function createTextEngine(opts = {}) {
|
|
1199
1369
|
const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
|
|
@@ -1202,109 +1372,206 @@ async function createTextEngine(opts = {}) {
|
|
|
1202
1372
|
const wasmBaseURL = opts.wasmBaseURL;
|
|
1203
1373
|
const fonts = new FontRegistry(wasmBaseURL);
|
|
1204
1374
|
const layout = new LayoutEngine(fonts);
|
|
1205
|
-
|
|
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
|
+
}
|
|
1206
1382
|
async function ensureFonts(asset) {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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;
|
|
1215
1412
|
}
|
|
1413
|
+
throw new Error(`Failed to ensure fonts: ${String(err)}`);
|
|
1216
1414
|
}
|
|
1217
|
-
const main = asset.font ?? {
|
|
1218
|
-
family: "Roboto",
|
|
1219
|
-
weight: "400",
|
|
1220
|
-
style: "normal",
|
|
1221
|
-
size: 48,
|
|
1222
|
-
color: "#000000",
|
|
1223
|
-
opacity: 1
|
|
1224
|
-
};
|
|
1225
|
-
return main;
|
|
1226
1415
|
}
|
|
1227
1416
|
return {
|
|
1228
1417
|
validate(input) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
+
}
|
|
1235
1431
|
},
|
|
1236
1432
|
async registerFontFromUrl(url, desc) {
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
+
}
|
|
1239
1441
|
},
|
|
1240
1442
|
async registerFontFromFile(source, desc) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
+
);
|
|
1246
1470
|
}
|
|
1247
|
-
await fonts.registerFromBytes(bytes, desc);
|
|
1248
1471
|
},
|
|
1249
1472
|
async renderFrame(asset, tSeconds) {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
1298
1553
|
},
|
|
1299
1554
|
createRenderer(canvas) {
|
|
1300
|
-
|
|
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
|
+
}
|
|
1301
1562
|
},
|
|
1302
1563
|
destroy() {
|
|
1303
|
-
|
|
1564
|
+
try {
|
|
1565
|
+
fonts.destroy();
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
|
|
1568
|
+
}
|
|
1304
1569
|
}
|
|
1305
1570
|
};
|
|
1306
1571
|
}
|
|
1307
1572
|
export {
|
|
1308
|
-
createTextEngine
|
|
1573
|
+
createTextEngine,
|
|
1574
|
+
isGlyphFill2 as isGlyphFill,
|
|
1575
|
+
isShadowFill2 as isShadowFill
|
|
1309
1576
|
};
|
|
1310
1577
|
//# sourceMappingURL=entry.web.js.map
|