@effing/canvas 0.41.1 → 0.43.0
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/README.md +50 -37
- package/dist/index.d.ts +15 -13
- package/dist/index.js +707 -1271
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createCanvas as _createCanvas } from "@
|
|
2
|
+
import { createCanvas as _createCanvas } from "@effing/skia";
|
|
3
3
|
import {
|
|
4
4
|
Canvas,
|
|
5
5
|
GlobalFonts as GlobalFonts2,
|
|
@@ -13,10 +13,10 @@ import {
|
|
|
13
13
|
FillType,
|
|
14
14
|
StrokeJoin,
|
|
15
15
|
StrokeCap
|
|
16
|
-
} from "@
|
|
16
|
+
} from "@effing/skia";
|
|
17
17
|
|
|
18
18
|
// src/image.ts
|
|
19
|
-
import { loadImage as nativeLoadImage } from "@
|
|
19
|
+
import { loadImage as nativeLoadImage } from "@effing/skia";
|
|
20
20
|
function loadImage(source, options) {
|
|
21
21
|
const remoteUrl = typeof source === "string" && isRemoteUrl(source) ? source : source instanceof URL && isRemoteUrl(source.href) ? source.href : void 0;
|
|
22
22
|
if (remoteUrl !== void 0) {
|
|
@@ -56,7 +56,7 @@ async function loadRemoteImage(url, userAgent) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/lottie.ts
|
|
59
|
-
import { LottieAnimation } from "@
|
|
59
|
+
import { LottieAnimation } from "@effing/skia";
|
|
60
60
|
function loadLottie(data, options) {
|
|
61
61
|
const jsonString = typeof data === "string" ? data : data.toString("utf-8");
|
|
62
62
|
return LottieAnimation.loadFromData(jsonString, {
|
|
@@ -130,6 +130,7 @@ var INHERITABLE_PROPS = [
|
|
|
130
130
|
"letterSpacing",
|
|
131
131
|
"whiteSpace",
|
|
132
132
|
"wordBreak",
|
|
133
|
+
"overflowWrap",
|
|
133
134
|
"textOverflow",
|
|
134
135
|
"lineClamp",
|
|
135
136
|
"textBoxTrim",
|
|
@@ -320,180 +321,39 @@ function resolveLengthUnits(transform, viewportWidth, viewportHeight, fontSize,
|
|
|
320
321
|
);
|
|
321
322
|
}
|
|
322
323
|
|
|
323
|
-
// src/jsx/
|
|
324
|
-
import {
|
|
325
|
-
|
|
326
|
-
// src/jsx/font-metrics.ts
|
|
327
|
-
import { inflateSync } from "zlib";
|
|
328
|
-
function fontMetricsToPx(metrics, fontSize) {
|
|
329
|
-
return {
|
|
330
|
-
ascent: metrics.ascender / metrics.unitsPerEm * fontSize,
|
|
331
|
-
descent: -metrics.descender / metrics.unitsPerEm * fontSize
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
var WOFF_SIGNATURE = 2001684038;
|
|
335
|
-
function readWoffTable(view, entry) {
|
|
336
|
-
const compressed = entry.compLength < entry.origLength;
|
|
337
|
-
if (compressed) {
|
|
338
|
-
try {
|
|
339
|
-
const compData = new Uint8Array(
|
|
340
|
-
view.buffer,
|
|
341
|
-
view.byteOffset + entry.offset,
|
|
342
|
-
entry.compLength
|
|
343
|
-
);
|
|
344
|
-
const decompressed = inflateSync(compData);
|
|
345
|
-
return new DataView(
|
|
346
|
-
decompressed.buffer,
|
|
347
|
-
decompressed.byteOffset,
|
|
348
|
-
decompressed.byteLength
|
|
349
|
-
);
|
|
350
|
-
} catch {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return new DataView(
|
|
355
|
-
view.buffer,
|
|
356
|
-
view.byteOffset + entry.offset,
|
|
357
|
-
entry.origLength
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
function parseWoff(view) {
|
|
361
|
-
if (view.byteLength < 44) return null;
|
|
362
|
-
const numTables = view.getUint16(12);
|
|
363
|
-
if (view.byteLength < 44 + numTables * 20) return null;
|
|
364
|
-
let headEntry = null;
|
|
365
|
-
let hheaEntry = null;
|
|
366
|
-
for (let i = 0; i < numTables; i++) {
|
|
367
|
-
const r = 44 + i * 20;
|
|
368
|
-
const tag = String.fromCharCode(view.getUint8(r)) + String.fromCharCode(view.getUint8(r + 1)) + String.fromCharCode(view.getUint8(r + 2)) + String.fromCharCode(view.getUint8(r + 3));
|
|
369
|
-
const offset = view.getUint32(r + 4);
|
|
370
|
-
const compLength = view.getUint32(r + 8);
|
|
371
|
-
const origLength = view.getUint32(r + 12);
|
|
372
|
-
if (tag === "head") headEntry = { offset, compLength, origLength };
|
|
373
|
-
else if (tag === "hhea") hheaEntry = { offset, compLength, origLength };
|
|
374
|
-
}
|
|
375
|
-
if (!headEntry || !hheaEntry) return null;
|
|
376
|
-
const headView = readWoffTable(view, headEntry);
|
|
377
|
-
const hheaView = readWoffTable(view, hheaEntry);
|
|
378
|
-
if (!headView || !hheaView) return null;
|
|
379
|
-
if (headView.byteLength < 20 || hheaView.byteLength < 8) return null;
|
|
380
|
-
const unitsPerEm = headView.getUint16(18);
|
|
381
|
-
const ascender = hheaView.getInt16(4);
|
|
382
|
-
const descender = hheaView.getInt16(6);
|
|
383
|
-
return { unitsPerEm, ascender, descender };
|
|
384
|
-
}
|
|
385
|
-
function parseSfnt(view) {
|
|
386
|
-
const numTables = view.getUint16(4);
|
|
387
|
-
if (view.byteLength < 12 + numTables * 16) return null;
|
|
388
|
-
let headOffset = -1;
|
|
389
|
-
let hheaOffset = -1;
|
|
390
|
-
for (let i = 0; i < numTables; i++) {
|
|
391
|
-
const recordOffset = 12 + i * 16;
|
|
392
|
-
const tag = String.fromCharCode(view.getUint8(recordOffset)) + String.fromCharCode(view.getUint8(recordOffset + 1)) + String.fromCharCode(view.getUint8(recordOffset + 2)) + String.fromCharCode(view.getUint8(recordOffset + 3));
|
|
393
|
-
const offset = view.getUint32(recordOffset + 8);
|
|
394
|
-
if (tag === "head") headOffset = offset;
|
|
395
|
-
else if (tag === "hhea") hheaOffset = offset;
|
|
396
|
-
}
|
|
397
|
-
if (headOffset < 0 || hheaOffset < 0) return null;
|
|
398
|
-
if (view.byteLength < headOffset + 20) return null;
|
|
399
|
-
const unitsPerEm = view.getUint16(headOffset + 18);
|
|
400
|
-
if (view.byteLength < hheaOffset + 8) return null;
|
|
401
|
-
const ascender = view.getInt16(hheaOffset + 4);
|
|
402
|
-
const descender = view.getInt16(hheaOffset + 6);
|
|
403
|
-
return { unitsPerEm, ascender, descender };
|
|
404
|
-
}
|
|
405
|
-
function parseFontMetrics(data) {
|
|
406
|
-
const buf = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
407
|
-
const view = new DataView(buf);
|
|
408
|
-
if (buf.byteLength < 12) return null;
|
|
409
|
-
const signature = view.getUint32(0);
|
|
410
|
-
if (signature === WOFF_SIGNATURE) {
|
|
411
|
-
return parseWoff(view);
|
|
412
|
-
}
|
|
413
|
-
return parseSfnt(view);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// src/jsx/font.ts
|
|
417
|
-
var registeredFonts = /* @__PURE__ */ new Set();
|
|
418
|
-
var metricsCache = /* @__PURE__ */ new Map();
|
|
419
|
-
function registerFont(font) {
|
|
420
|
-
const key = `${font.name}:${font.weight}:${font.style}`;
|
|
421
|
-
if (registeredFonts.has(key)) return;
|
|
422
|
-
const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
|
|
423
|
-
GlobalFonts.register(buffer, font.name);
|
|
424
|
-
const metrics = parseFontMetrics(font.data);
|
|
425
|
-
if (metrics) {
|
|
426
|
-
metricsCache.set(key, metrics);
|
|
427
|
-
}
|
|
428
|
-
registeredFonts.add(key);
|
|
429
|
-
}
|
|
430
|
-
function getFontMetrics(family, weight, style) {
|
|
431
|
-
const families = family.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, ""));
|
|
432
|
-
for (const name of families) {
|
|
433
|
-
const exact = metricsCache.get(`${name}:${weight}:${style}`);
|
|
434
|
-
if (exact) return exact;
|
|
435
|
-
for (const [key, metrics] of metricsCache) {
|
|
436
|
-
if (key.startsWith(`${name}:`)) return metrics;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
function registerFontFromPath(path, nameAlias) {
|
|
442
|
-
GlobalFonts.registerFromPath(path, nameAlias ?? "");
|
|
443
|
-
}
|
|
444
|
-
function registeredFamilies() {
|
|
445
|
-
return GlobalFonts.families.map((f) => f.family);
|
|
446
|
-
}
|
|
447
|
-
function ensureFontsRegistered(fonts) {
|
|
448
|
-
for (const font of fonts) {
|
|
449
|
-
registerFont(font);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
324
|
+
// src/jsx/text/native.ts
|
|
325
|
+
import { Paragraph } from "@effing/skia/extensions";
|
|
452
326
|
|
|
453
327
|
// src/jsx/language.ts
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
// src/jsx/text/linebreak.ts
|
|
480
|
-
import LineBreaker from "linebreak";
|
|
481
|
-
function findBreakOpportunities(text) {
|
|
482
|
-
const breaker = new LineBreaker(text);
|
|
483
|
-
const opportunities = [];
|
|
484
|
-
let bk = breaker.nextBreak();
|
|
485
|
-
while (bk) {
|
|
486
|
-
opportunities.push({
|
|
487
|
-
position: bk.position,
|
|
488
|
-
required: bk.required ?? false
|
|
489
|
-
});
|
|
490
|
-
bk = breaker.nextBreak();
|
|
491
|
-
}
|
|
492
|
-
return opportunities;
|
|
328
|
+
var UNASSIGNED_PICTOGRAPHIC = String.raw`(?=\p{Cn})\p{Extended_Pictographic}`;
|
|
329
|
+
var EMOJI_CHAR = String.raw`[\p{Emoji}\p{Extended_Pictographic}]`;
|
|
330
|
+
var KEYCAP_BASE = "[0-9#*]";
|
|
331
|
+
var TEXT_EMOJI_CHAR = String.raw`[0-9#*\u00A9\u00AE\u2122]`;
|
|
332
|
+
var TAGS = String.raw`[\u{E0020}-\u{E007E}]+\u{E007F}`;
|
|
333
|
+
var PRESENTED_EMOJI = [
|
|
334
|
+
// Keycap: 1️⃣, and the unqualified 1⃣.
|
|
335
|
+
String.raw`${KEYCAP_BASE}\uFE0F?\u20E3`,
|
|
336
|
+
// Flag: a pair of regional indicators.
|
|
337
|
+
String.raw`\p{Regional_Indicator}{2}`,
|
|
338
|
+
// Emoji presentation sequence (©️), modifier sequence (👍🏽, ☝🏽) or tag
|
|
339
|
+
// sequence (🏴).
|
|
340
|
+
String.raw`(?!${KEYCAP_BASE})${EMOJI_CHAR}(?:\uFE0F|\p{Emoji_Modifier}|${TAGS})`,
|
|
341
|
+
// An emoji character on its own, emoji presentation by default (🌍, ⭐) or
|
|
342
|
+
// text (❤, ☎, 🕵), but not one that stays text.
|
|
343
|
+
String.raw`(?!${TEXT_EMOJI_CHAR})\p{Emoji}`,
|
|
344
|
+
UNASSIGNED_PICTOGRAPHIC
|
|
345
|
+
].join("|");
|
|
346
|
+
var ZWJ_ELEMENT = `(?:${PRESENTED_EMOJI}|(?!${KEYCAP_BASE})${EMOJI_CHAR})`;
|
|
347
|
+
var EMOJI_RE = new RegExp(
|
|
348
|
+
`^(?:${PRESENTED_EMOJI}|${ZWJ_ELEMENT}(?:\\u200D${ZWJ_ELEMENT})+)$`,
|
|
349
|
+
"u"
|
|
350
|
+
);
|
|
351
|
+
function isEmoji(grapheme) {
|
|
352
|
+
return EMOJI_RE.test(grapheme);
|
|
493
353
|
}
|
|
494
354
|
|
|
495
355
|
// src/jsx/text/measure.ts
|
|
496
|
-
import { createCanvas } from "@
|
|
356
|
+
import { createCanvas } from "@effing/skia";
|
|
497
357
|
var scratchCtx = null;
|
|
498
358
|
function getScratchCtx() {
|
|
499
359
|
if (!scratchCtx) {
|
|
@@ -520,49 +380,36 @@ function quoteFontFamily(family) {
|
|
|
520
380
|
if (!family || GENERIC_FAMILIES.has(family)) return family;
|
|
521
381
|
return `"${family}"`;
|
|
522
382
|
}
|
|
383
|
+
function quoteFontFamilies(fontFamily) {
|
|
384
|
+
return fontFamily.split(",").map((f) => quoteFontFamily(f.trim())).join(", ");
|
|
385
|
+
}
|
|
523
386
|
function setFont(ctx, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal") {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
height: ascent + descent
|
|
387
|
+
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${quoteFontFamilies(fontFamily)}`;
|
|
388
|
+
}
|
|
389
|
+
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, fontAscent, fontDescent) {
|
|
390
|
+
const c = getScratchCtx();
|
|
391
|
+
const measure = (text) => {
|
|
392
|
+
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
393
|
+
const previous = c.textRendering;
|
|
394
|
+
c.textRendering = "geometricPrecision";
|
|
395
|
+
try {
|
|
396
|
+
return c.measureText(text);
|
|
397
|
+
} finally {
|
|
398
|
+
c.textRendering = previous;
|
|
399
|
+
}
|
|
538
400
|
};
|
|
539
|
-
}
|
|
540
|
-
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx, fontMetrics) {
|
|
541
|
-
const c = ctx ?? getScratchCtx();
|
|
542
|
-
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
543
|
-
const refMetrics = c.measureText("M");
|
|
544
|
-
let fontAscent;
|
|
545
|
-
let fontDescent;
|
|
546
|
-
if (fontMetrics) {
|
|
547
|
-
const px = fontMetricsToPx(fontMetrics, fontSize);
|
|
548
|
-
fontAscent = px.ascent;
|
|
549
|
-
fontDescent = px.descent;
|
|
550
|
-
} else {
|
|
551
|
-
fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
|
|
552
|
-
fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
|
|
553
|
-
}
|
|
554
401
|
const parts = edge.trim().split(/\s+/);
|
|
555
402
|
const overEdge = parts[0] ?? "text";
|
|
556
403
|
const underEdge = parts.length > 1 ? parts[1] : overEdge;
|
|
557
404
|
let targetAscent;
|
|
558
405
|
switch (overEdge) {
|
|
559
406
|
case "cap": {
|
|
560
|
-
const capMetrics =
|
|
407
|
+
const capMetrics = measure("H");
|
|
561
408
|
targetAscent = capMetrics.actualBoundingBoxAscent ?? fontSize * 0.7;
|
|
562
409
|
break;
|
|
563
410
|
}
|
|
564
411
|
case "ex": {
|
|
565
|
-
const exMetrics =
|
|
412
|
+
const exMetrics = measure("x");
|
|
566
413
|
targetAscent = exMetrics.actualBoundingBoxAscent ?? fontSize * 0.5;
|
|
567
414
|
break;
|
|
568
415
|
}
|
|
@@ -593,423 +440,364 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
|
|
|
593
440
|
underTrim: Math.max(0, underTrim)
|
|
594
441
|
};
|
|
595
442
|
}
|
|
596
|
-
function measureWord(word, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx, letterSpacing = 0) {
|
|
597
|
-
const base = measureText(
|
|
598
|
-
word,
|
|
599
|
-
fontSize,
|
|
600
|
-
fontFamily,
|
|
601
|
-
fontWeight,
|
|
602
|
-
fontStyle,
|
|
603
|
-
ctx
|
|
604
|
-
).width;
|
|
605
|
-
if (letterSpacing === 0 || word.length === 0) return base;
|
|
606
|
-
return base + letterSpacing * word.length;
|
|
607
|
-
}
|
|
608
443
|
|
|
609
|
-
// src/jsx/text/
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
444
|
+
// src/jsx/text/native.ts
|
|
445
|
+
function toWeight(weight) {
|
|
446
|
+
if (typeof weight === "number") return weight;
|
|
447
|
+
if (weight === "bold") return 700;
|
|
448
|
+
const parsed = parseInt(String(weight ?? "400"), 10);
|
|
449
|
+
return isNaN(parsed) ? 400 : parsed;
|
|
450
|
+
}
|
|
451
|
+
function toFontStyle(fontStyle) {
|
|
452
|
+
if (fontStyle?.startsWith("italic")) return "italic";
|
|
453
|
+
if (fontStyle?.startsWith("oblique")) return "oblique";
|
|
454
|
+
return "normal";
|
|
455
|
+
}
|
|
456
|
+
var TEXT_ALIGNS = /* @__PURE__ */ new Set([
|
|
457
|
+
"left",
|
|
458
|
+
"right",
|
|
459
|
+
"center",
|
|
460
|
+
"justify",
|
|
461
|
+
"start",
|
|
462
|
+
"end"
|
|
463
|
+
]);
|
|
464
|
+
function toTextAlign(textAlign) {
|
|
465
|
+
return textAlign !== void 0 && TEXT_ALIGNS.has(textAlign) ? textAlign : "left";
|
|
466
|
+
}
|
|
467
|
+
function toWordBreak(style) {
|
|
468
|
+
const { wordBreak } = style;
|
|
469
|
+
return wordBreak === "break-all" || wordBreak === "keep-all" ? wordBreak : "normal";
|
|
470
|
+
}
|
|
471
|
+
function toOverflowWrap(style) {
|
|
472
|
+
const { overflowWrap, wordBreak } = style;
|
|
473
|
+
return overflowWrap === "break-word" || overflowWrap === "anywhere" || wordBreak === "break-word" ? "break-word" : "normal";
|
|
474
|
+
}
|
|
475
|
+
var MIN_WIDTH = 0.01;
|
|
476
|
+
var MIN_FONT_SIZE = 1e-3;
|
|
477
|
+
var EMOJI_DROP = 0.1;
|
|
478
|
+
var MAX_PENDING_PARAGRAPHS = 2e3;
|
|
479
|
+
var pendingParagraphs = 0;
|
|
480
|
+
var turnWatched = false;
|
|
481
|
+
function countParagraph() {
|
|
482
|
+
pendingParagraphs++;
|
|
483
|
+
if (turnWatched) return;
|
|
484
|
+
turnWatched = true;
|
|
485
|
+
setImmediate(() => {
|
|
486
|
+
pendingParagraphs = 0;
|
|
487
|
+
turnWatched = false;
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async function releaseParagraphs() {
|
|
491
|
+
if (pendingParagraphs < MAX_PENDING_PARAGRAPHS) return;
|
|
492
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
493
|
+
}
|
|
494
|
+
var graphemeSegmenter = new Intl.Segmenter(void 0, {
|
|
495
|
+
granularity: "grapheme"
|
|
496
|
+
});
|
|
497
|
+
function splitEmoji(text, fontSize, letterSpacing) {
|
|
498
|
+
const items = [];
|
|
499
|
+
const emoji = [];
|
|
500
|
+
const marks = [];
|
|
501
|
+
let run = "";
|
|
502
|
+
let paragraphIndex = 0;
|
|
503
|
+
let textIndex = 0;
|
|
504
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
505
|
+
if (!isEmoji(segment)) {
|
|
506
|
+
run += segment;
|
|
507
|
+
paragraphIndex += segment.length;
|
|
508
|
+
textIndex += segment.length;
|
|
509
|
+
continue;
|
|
621
510
|
}
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
511
|
+
if (run) items.push(run);
|
|
512
|
+
run = "";
|
|
513
|
+
items.push({
|
|
514
|
+
// Letter spacing follows every character, an emoji too, but a
|
|
515
|
+
// paragraph doesn't add it to a placeholder.
|
|
516
|
+
width: Math.max(0, fontSize + letterSpacing),
|
|
517
|
+
height: fontSize,
|
|
518
|
+
verticalAlign: "baseline",
|
|
519
|
+
baselineOffset: fontSize * (1 - EMOJI_DROP),
|
|
520
|
+
// Lines break around it as around an emoji, not an inline-block: it
|
|
521
|
+
// stays with the punctuation next to it, so "Hi 🎉! ok" breaks as
|
|
522
|
+
// "Hi | 🎉! | ok" and "(🎉)" stays whole, as in browsers.
|
|
523
|
+
lineBreak: "emoji"
|
|
524
|
+
});
|
|
525
|
+
emoji.push(segment);
|
|
526
|
+
marks.push([paragraphIndex, textIndex]);
|
|
527
|
+
paragraphIndex += 1;
|
|
528
|
+
textIndex += segment.length;
|
|
529
|
+
marks.push([paragraphIndex, textIndex]);
|
|
530
|
+
}
|
|
531
|
+
if (run) items.push(run);
|
|
532
|
+
if (emoji.length === 0) return { items: [text], emoji };
|
|
533
|
+
return {
|
|
534
|
+
items,
|
|
535
|
+
emoji,
|
|
536
|
+
toTextIndex: (index) => {
|
|
537
|
+
let mark = [0, 0];
|
|
538
|
+
for (const m of marks) {
|
|
539
|
+
if (m[0] > index) break;
|
|
540
|
+
mark = m;
|
|
634
541
|
}
|
|
635
|
-
|
|
636
|
-
} else {
|
|
637
|
-
textBuffer += segment;
|
|
542
|
+
return mark[1] + (index - mark[0]);
|
|
638
543
|
}
|
|
639
|
-
}
|
|
640
|
-
if (textBuffer) {
|
|
641
|
-
totalWidth += measureWord(
|
|
642
|
-
textBuffer,
|
|
643
|
-
fontSize,
|
|
644
|
-
fontFamily,
|
|
645
|
-
fontWeight,
|
|
646
|
-
fontStyle,
|
|
647
|
-
ctx,
|
|
648
|
-
letterSpacing
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
return totalWidth;
|
|
544
|
+
};
|
|
652
545
|
}
|
|
653
|
-
function
|
|
654
|
-
const
|
|
546
|
+
function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
|
|
547
|
+
const styledFontSize = style.fontSize ?? 16;
|
|
548
|
+
const fontSize = Number.isFinite(styledFontSize) ? Math.max(styledFontSize, MIN_FONT_SIZE) : 16;
|
|
655
549
|
const fontFamily = style.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
656
550
|
const fontWeight = style.fontWeight ?? 400;
|
|
657
551
|
const fontStyle = style.fontStyle ?? "normal";
|
|
658
|
-
const
|
|
659
|
-
const textAlign = style.textAlign ?? "left";
|
|
660
|
-
const refMetrics = measureText(
|
|
661
|
-
"M",
|
|
662
|
-
fontSize,
|
|
663
|
-
fontFamily,
|
|
664
|
-
fontWeight,
|
|
665
|
-
fontStyle,
|
|
666
|
-
ctx
|
|
667
|
-
);
|
|
668
|
-
const fontMetrics = getFontMetrics(fontFamily, fontWeight, fontStyle);
|
|
669
|
-
const lineHeightPx = resolveLineHeight(
|
|
670
|
-
style.lineHeight,
|
|
671
|
-
fontSize,
|
|
672
|
-
refMetrics,
|
|
673
|
-
fontMetrics
|
|
674
|
-
);
|
|
675
|
-
const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
|
|
552
|
+
const letterSpacing = typeof style.letterSpacing === "number" && Number.isFinite(style.letterSpacing) ? style.letterSpacing : 0;
|
|
676
553
|
const whiteSpace = style.whiteSpace ?? "normal";
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
textStrokeWidth = rawStrokeWidth;
|
|
685
|
-
} else {
|
|
686
|
-
const resolved = resolveUnit(String(rawStrokeWidth), 0, 0, fontSize, 16);
|
|
687
|
-
textStrokeWidth = typeof resolved === "number" ? resolved : void 0;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
const textStrokeColor = style.WebkitTextStrokeColor;
|
|
691
|
-
const measure = emojiEnabled ? (word, ls) => emojiAwareMeasureWord(
|
|
692
|
-
word,
|
|
554
|
+
const noWrap = whiteSpace === "nowrap" || whiteSpace === "pre";
|
|
555
|
+
const lineClamp = style.lineClamp && style.lineClamp >= 1 ? Math.floor(style.lineClamp) : void 0;
|
|
556
|
+
const ellipsis = lineClamp !== void 0 || noWrap && style.textOverflow === "ellipsis" ? "\u2026" : void 0;
|
|
557
|
+
const width = maxWidth > 0 ? maxWidth : MIN_WIDTH;
|
|
558
|
+
const content = emojiEnabled ? splitEmoji(text, fontSize, letterSpacing) : { items: [text], emoji: [] };
|
|
559
|
+
const paragraph = new Paragraph(content.items, {
|
|
560
|
+
fontFamily: quoteFontFamilies(fontFamily),
|
|
693
561
|
fontSize,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
562
|
+
fontWeight: toWeight(fontWeight),
|
|
563
|
+
fontStyle: toFontStyle(fontStyle),
|
|
564
|
+
letterSpacing,
|
|
565
|
+
lineHeight,
|
|
566
|
+
textAlign: toTextAlign(style.textAlign),
|
|
567
|
+
noWrap,
|
|
568
|
+
maxLines: lineClamp,
|
|
569
|
+
ellipsis,
|
|
570
|
+
// `pre` and `pre-wrap` keep the spaces before a line break in the line.
|
|
571
|
+
keepTrailingWhitespace: whiteSpace === "pre" || whiteSpace === "pre-wrap",
|
|
572
|
+
wordBreak: toWordBreak(style),
|
|
573
|
+
overflowWrap: toOverflowWrap(style)
|
|
574
|
+
});
|
|
575
|
+
countParagraph();
|
|
576
|
+
const layout = paragraph.layout(width);
|
|
577
|
+
const lines = layout.lines.length > 0 ? layout.lines : [
|
|
578
|
+
{
|
|
579
|
+
left: 0,
|
|
580
|
+
width: 0,
|
|
581
|
+
// As Skia places a baseline: by half-leading in the line box.
|
|
582
|
+
baseline: (layout.lineHeight + layout.ascent - layout.descent) / 2,
|
|
583
|
+
startIndex: 0,
|
|
584
|
+
endIndex: 0,
|
|
585
|
+
hardBreak: true
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
const toTextIndex = content.toTextIndex ?? ((index) => index);
|
|
589
|
+
const segments = lines.map((line, i) => ({
|
|
590
|
+
// A line that ends the text at a newline reports the newline as its text.
|
|
591
|
+
text: text.slice(toTextIndex(line.startIndex), toTextIndex(line.endIndex)).replace(/\n/g, ""),
|
|
592
|
+
x: line.left,
|
|
593
|
+
y: line.baseline,
|
|
594
|
+
width: line.width,
|
|
595
|
+
height: layout.lineHeight,
|
|
701
596
|
fontSize,
|
|
702
597
|
fontFamily,
|
|
703
598
|
fontWeight,
|
|
704
599
|
fontStyle,
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (noWrap) {
|
|
721
|
-
lines.push(paragraph);
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
const wrapped = wrapText(
|
|
725
|
-
paragraph,
|
|
726
|
-
maxWidth,
|
|
727
|
-
fontSize,
|
|
728
|
-
fontFamily,
|
|
729
|
-
fontWeight,
|
|
730
|
-
fontStyle,
|
|
731
|
-
letterSpacing,
|
|
732
|
-
wordBreak,
|
|
733
|
-
ctx,
|
|
734
|
-
measure
|
|
735
|
-
);
|
|
736
|
-
lines.push(...wrapped);
|
|
737
|
-
}
|
|
738
|
-
if (textOverflow === "ellipsis" && noWrap && lines.length === 1) {
|
|
739
|
-
const line = lines[0];
|
|
740
|
-
const lineWidth = measure(line);
|
|
741
|
-
if (lineWidth > maxWidth) {
|
|
742
|
-
lines[0] = truncateWithEllipsis(
|
|
743
|
-
line,
|
|
744
|
-
maxWidth,
|
|
745
|
-
fontSize,
|
|
746
|
-
fontFamily,
|
|
747
|
-
fontWeight,
|
|
748
|
-
fontStyle,
|
|
749
|
-
ctx,
|
|
750
|
-
letterSpacing
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
const lineClamp = style.lineClamp;
|
|
755
|
-
if (lineClamp && lineClamp > 0 && lines.length > lineClamp) {
|
|
756
|
-
const lastLineText = lines.slice(lineClamp - 1).join(" ");
|
|
757
|
-
lines.length = lineClamp;
|
|
758
|
-
lines[lineClamp - 1] = truncateWithEllipsis(
|
|
759
|
-
lastLineText,
|
|
760
|
-
maxWidth,
|
|
761
|
-
fontSize,
|
|
762
|
-
fontFamily,
|
|
763
|
-
fontWeight,
|
|
764
|
-
fontStyle,
|
|
765
|
-
ctx,
|
|
766
|
-
letterSpacing
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
const segments = [];
|
|
770
|
-
let totalHeight = 0;
|
|
771
|
-
let maxLineWidth = 0;
|
|
772
|
-
const isAutoLineHeight = style.lineHeight === void 0 || style.lineHeight === "normal";
|
|
773
|
-
const fontPx = fontMetrics ? fontMetricsToPx(fontMetrics, fontSize) : null;
|
|
774
|
-
let lastPositioningDescent;
|
|
775
|
-
for (let i = 0; i < lines.length; i++) {
|
|
776
|
-
const line = lines[i];
|
|
777
|
-
const lineWidth = measure(line);
|
|
778
|
-
let x = 0;
|
|
779
|
-
if (textAlign === "center") {
|
|
780
|
-
x = (maxWidth - lineWidth) / 2;
|
|
781
|
-
} else if (textAlign === "right") {
|
|
782
|
-
x = maxWidth - lineWidth;
|
|
783
|
-
}
|
|
784
|
-
const metrics = measureText(
|
|
785
|
-
line || "M",
|
|
786
|
-
fontSize,
|
|
787
|
-
fontFamily,
|
|
788
|
-
fontWeight,
|
|
789
|
-
fontStyle,
|
|
790
|
-
ctx
|
|
791
|
-
);
|
|
792
|
-
const positioningAscent = fontPx?.ascent ?? metrics.ascent;
|
|
793
|
-
const positioningDescent = fontPx?.descent ?? metrics.descent;
|
|
794
|
-
lastPositioningDescent = positioningDescent;
|
|
795
|
-
const contentHeight = positioningAscent + positioningDescent;
|
|
796
|
-
let effectiveAscent = positioningAscent;
|
|
797
|
-
let effectiveDescent = positioningDescent;
|
|
798
|
-
if (isAutoLineHeight && contentHeight > lineHeightPx) {
|
|
799
|
-
const scale = lineHeightPx / contentHeight;
|
|
800
|
-
effectiveAscent = positioningAscent * scale;
|
|
801
|
-
effectiveDescent = positioningDescent * scale;
|
|
802
|
-
}
|
|
803
|
-
const baselineY = totalHeight + (lineHeightPx + effectiveAscent - effectiveDescent) / 2;
|
|
804
|
-
segments.push({
|
|
805
|
-
text: line,
|
|
806
|
-
x,
|
|
807
|
-
y: baselineY,
|
|
808
|
-
width: lineWidth,
|
|
809
|
-
height: lineHeightPx,
|
|
810
|
-
fontSize,
|
|
811
|
-
fontFamily,
|
|
812
|
-
fontWeight,
|
|
813
|
-
fontStyle,
|
|
814
|
-
color,
|
|
815
|
-
ascent: metrics.ascent,
|
|
816
|
-
textDecoration,
|
|
817
|
-
letterSpacing,
|
|
818
|
-
lineIndex: i,
|
|
819
|
-
textStrokeWidth,
|
|
820
|
-
textStrokeColor
|
|
600
|
+
color: style.color ?? "black",
|
|
601
|
+
ascent: layout.ascent,
|
|
602
|
+
textDecoration: style.textDecoration,
|
|
603
|
+
letterSpacing,
|
|
604
|
+
lineIndex: i
|
|
605
|
+
}));
|
|
606
|
+
const emoji = [];
|
|
607
|
+
layout.placeholders.forEach((box, i) => {
|
|
608
|
+
if (!box) return;
|
|
609
|
+
emoji.push({
|
|
610
|
+
grapheme: content.emoji[i],
|
|
611
|
+
x: box.x,
|
|
612
|
+
y: box.y,
|
|
613
|
+
size: fontSize,
|
|
614
|
+
baseline: lines[box.line].baseline
|
|
821
615
|
});
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const descentOverflow = lastSeg.y + lastPositioningDescent - totalHeight;
|
|
828
|
-
if (descentOverflow > 0) {
|
|
829
|
-
totalHeight += descentOverflow;
|
|
830
|
-
}
|
|
831
|
-
totalHeight = Math.ceil(totalHeight);
|
|
616
|
+
});
|
|
617
|
+
let height = lines.length * layout.lineHeight;
|
|
618
|
+
if (lineHeight === void 0) {
|
|
619
|
+
const noFontSize = Number.isFinite(styledFontSize) && styledFontSize <= 0;
|
|
620
|
+
height = noFontSize ? 0 : Math.ceil(height);
|
|
832
621
|
}
|
|
622
|
+
let paragraphOffsetY = 0;
|
|
833
623
|
const textBoxTrim = style.textBoxTrim;
|
|
834
|
-
if (textBoxTrim && textBoxTrim !== "none"
|
|
835
|
-
const
|
|
836
|
-
const trimMetrics = measureTrimMetrics(
|
|
624
|
+
if (textBoxTrim && textBoxTrim !== "none") {
|
|
625
|
+
const trim = measureTrimMetrics(
|
|
837
626
|
fontSize,
|
|
838
627
|
fontFamily,
|
|
839
628
|
fontWeight,
|
|
840
629
|
fontStyle,
|
|
841
|
-
|
|
842
|
-
textBoxEdge,
|
|
843
|
-
|
|
844
|
-
|
|
630
|
+
layout.lineHeight,
|
|
631
|
+
style.textBoxEdge ?? "text",
|
|
632
|
+
layout.ascent,
|
|
633
|
+
layout.descent
|
|
845
634
|
);
|
|
846
635
|
if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
|
|
847
|
-
for (const seg of segments)
|
|
848
|
-
|
|
636
|
+
for (const seg of segments) seg.y -= trim.overTrim;
|
|
637
|
+
for (const e of emoji) {
|
|
638
|
+
e.y -= trim.overTrim;
|
|
639
|
+
e.baseline -= trim.overTrim;
|
|
849
640
|
}
|
|
850
|
-
|
|
641
|
+
height -= trim.overTrim;
|
|
642
|
+
paragraphOffsetY = -trim.overTrim;
|
|
851
643
|
}
|
|
852
644
|
if (textBoxTrim === "trim-end" || textBoxTrim === "trim-both") {
|
|
853
|
-
|
|
645
|
+
height -= trim.underTrim;
|
|
854
646
|
}
|
|
855
647
|
}
|
|
856
648
|
return {
|
|
857
649
|
segments,
|
|
858
|
-
width:
|
|
859
|
-
height
|
|
650
|
+
width: segments.reduce((w, s) => Math.max(w, s.width), 0),
|
|
651
|
+
height,
|
|
652
|
+
paragraph,
|
|
653
|
+
paragraphOffsetY,
|
|
654
|
+
emoji
|
|
860
655
|
};
|
|
861
656
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
657
|
+
|
|
658
|
+
// src/jsx/text/white-space.ts
|
|
659
|
+
function isWhiteSpaceOnly(text) {
|
|
660
|
+
return /^[ \t\n\r\f\v]*$/.test(text);
|
|
661
|
+
}
|
|
662
|
+
function collapseWhiteSpace(text, whiteSpace) {
|
|
663
|
+
let result = text.replace(/[\v\f]/g, "").replace(/\r\n/g, "\n");
|
|
664
|
+
if (whiteSpace === "pre" || whiteSpace === "pre-wrap") {
|
|
665
|
+
result = result.replace(/\r/g, "");
|
|
666
|
+
} else if (/[\t\n\r]| {2}|^ | $/.test(result)) {
|
|
667
|
+
result = result.replace(/[\t\r]/g, " ").replace(/ *\n */g, "\n");
|
|
668
|
+
if (whiteSpace !== "pre-line") result = result.replace(/\n+/g, " ");
|
|
669
|
+
result = result.replace(/ {2,}/g, " ").replace(/^ | $/g, "");
|
|
670
|
+
}
|
|
671
|
+
return result.replace(FINAL_BREAK, "").replace(SEPARATORS, SEPARATOR_SPACE);
|
|
672
|
+
}
|
|
673
|
+
var FINAL_BREAK = /\n$/;
|
|
674
|
+
var SEPARATORS = /[\u2028\u2029]/g;
|
|
675
|
+
var SEPARATOR_SPACE = "\xA0\u200B";
|
|
676
|
+
|
|
677
|
+
// src/jsx/text/index.ts
|
|
678
|
+
function layoutText(text, style, maxWidth, emojiEnabled) {
|
|
679
|
+
const fontSize = style.fontSize ?? 16;
|
|
680
|
+
const result = layoutTextNative(
|
|
681
|
+
applyTextTransform(collapseWhiteSpace(text, style.whiteSpace), style),
|
|
682
|
+
style,
|
|
683
|
+
maxWidth,
|
|
684
|
+
resolveLineHeight(style.lineHeight, fontSize),
|
|
685
|
+
emojiEnabled
|
|
686
|
+
);
|
|
687
|
+
const textStrokeWidth = resolveTextStrokeWidth(style, fontSize);
|
|
688
|
+
for (const seg of result.segments) {
|
|
689
|
+
seg.textStrokeWidth = textStrokeWidth;
|
|
690
|
+
seg.textStrokeColor = style.WebkitTextStrokeColor;
|
|
869
691
|
}
|
|
870
|
-
|
|
871
|
-
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
function resolveTextStrokeWidth(style, fontSize) {
|
|
695
|
+
const raw = style.WebkitTextStrokeWidth;
|
|
696
|
+
if (raw === void 0) return void 0;
|
|
697
|
+
if (typeof raw === "number") return raw;
|
|
698
|
+
const resolved = resolveUnit(String(raw), 0, 0, fontSize, 16);
|
|
699
|
+
return typeof resolved === "number" ? resolved : void 0;
|
|
700
|
+
}
|
|
701
|
+
function applyTextTransform(text, style) {
|
|
702
|
+
if (style.textTransform === "uppercase") return text.toUpperCase();
|
|
703
|
+
if (style.textTransform === "lowercase") return text.toLowerCase();
|
|
704
|
+
if (style.textTransform === "capitalize") {
|
|
705
|
+
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
872
706
|
}
|
|
873
|
-
|
|
874
|
-
return isNaN(parsed) ? fontSize * 1.2 : parsed;
|
|
707
|
+
return text;
|
|
875
708
|
}
|
|
876
|
-
function
|
|
877
|
-
if (
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
fontSize
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
709
|
+
function resolveLineHeight(lineHeight, fontSize) {
|
|
710
|
+
if (lineHeight === void 0 || lineHeight === "normal") return void 0;
|
|
711
|
+
let px;
|
|
712
|
+
if (typeof lineHeight === "number") {
|
|
713
|
+
px = lineHeight > 5 ? lineHeight : lineHeight * fontSize;
|
|
714
|
+
} else {
|
|
715
|
+
px = parseFloat(lineHeight);
|
|
716
|
+
}
|
|
717
|
+
return Number.isFinite(px) && px >= 0 ? px : void 0;
|
|
718
|
+
}
|
|
719
|
+
var TextMeasure = class {
|
|
720
|
+
constructor(text, style, emojiEnabled) {
|
|
721
|
+
this.text = text;
|
|
722
|
+
this.style = style;
|
|
723
|
+
this.emojiEnabled = emojiEnabled;
|
|
724
|
+
this.measureStyle = { ...style, textOverflow: "clip" };
|
|
725
|
+
}
|
|
726
|
+
text;
|
|
727
|
+
style;
|
|
728
|
+
emojiEnabled;
|
|
729
|
+
/** Yoga's measurements by width (Infinity for unbounded). */
|
|
730
|
+
sizes = /* @__PURE__ */ new Map();
|
|
731
|
+
/** The heights reported to Yoga since the last `settle`. */
|
|
732
|
+
reported = /* @__PURE__ */ new Set();
|
|
733
|
+
pinnedHeight;
|
|
734
|
+
settled;
|
|
735
|
+
measureStyle;
|
|
736
|
+
/**
|
|
737
|
+
* Measure the text for Yoga.
|
|
738
|
+
*
|
|
739
|
+
* @param maxWidth - The width available, Infinity for unbounded
|
|
740
|
+
*/
|
|
741
|
+
measure(maxWidth) {
|
|
742
|
+
let size = this.sizes.get(maxWidth);
|
|
743
|
+
if (!size) {
|
|
744
|
+
const result = layoutText(
|
|
745
|
+
this.text,
|
|
746
|
+
this.measureStyle,
|
|
903
747
|
maxWidth,
|
|
904
|
-
|
|
905
|
-
fontFamily,
|
|
906
|
-
fontWeight,
|
|
907
|
-
fontStyle,
|
|
908
|
-
ctx,
|
|
909
|
-
letterSpacing,
|
|
910
|
-
measureFn
|
|
748
|
+
this.emojiEnabled
|
|
911
749
|
);
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
lineStart = opp.position;
|
|
919
|
-
}
|
|
920
|
-
lastBreak = opp.position;
|
|
921
|
-
}
|
|
922
|
-
if (lineStart < text.length) {
|
|
923
|
-
const remaining = text.slice(lineStart).replace(/\s+$/, "");
|
|
924
|
-
if (remaining) {
|
|
925
|
-
const remWidth = mw(remaining);
|
|
926
|
-
if (remWidth > maxWidth && wordBreak === "break-all") {
|
|
927
|
-
const broken = forceBreakWord(
|
|
928
|
-
text,
|
|
929
|
-
lineStart,
|
|
930
|
-
text.length,
|
|
931
|
-
maxWidth,
|
|
932
|
-
fontSize,
|
|
933
|
-
fontFamily,
|
|
934
|
-
fontWeight,
|
|
935
|
-
fontStyle,
|
|
936
|
-
ctx,
|
|
937
|
-
letterSpacing,
|
|
938
|
-
measureFn
|
|
939
|
-
);
|
|
940
|
-
lines.push(...broken.lines);
|
|
941
|
-
} else {
|
|
942
|
-
lines.push(remaining);
|
|
943
|
-
}
|
|
750
|
+
const wrapped = result.segments.length > 1 && maxWidth < Infinity;
|
|
751
|
+
size = {
|
|
752
|
+
width: wrapped ? maxWidth : Math.min(result.width, maxWidth),
|
|
753
|
+
height: result.height
|
|
754
|
+
};
|
|
755
|
+
this.sizes.set(maxWidth, size);
|
|
944
756
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (w > maxWidth) break;
|
|
966
|
-
breakPos++;
|
|
757
|
+
const height = this.pinnedHeight ?? size.height;
|
|
758
|
+
this.reported.add(height);
|
|
759
|
+
return { width: size.width, height };
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Lay the text out for drawing at the node's final content width, and
|
|
763
|
+
* check it against the heights Yoga was given since the last call.
|
|
764
|
+
*
|
|
765
|
+
* @param pin - Whether to pin later measurements to the drawn height when
|
|
766
|
+
* they disagree
|
|
767
|
+
* @returns Whether Yoga sized the node from another height. If `pin`, the
|
|
768
|
+
* node's measurements are now pinned to the drawn height, and the caller
|
|
769
|
+
* should mark the node dirty and compute the layout again.
|
|
770
|
+
*/
|
|
771
|
+
settle(width, pin) {
|
|
772
|
+
if (this.settled?.width !== width) {
|
|
773
|
+
this.settled = {
|
|
774
|
+
width,
|
|
775
|
+
result: layoutText(this.text, this.style, width, this.emojiEnabled)
|
|
776
|
+
};
|
|
967
777
|
}
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
778
|
+
const { height } = this.settled.result;
|
|
779
|
+
const agrees = this.reported.size === 0 || this.reported.size === 1 && this.reported.has(height);
|
|
780
|
+
this.reported.clear();
|
|
781
|
+
if (agrees) return false;
|
|
782
|
+
if (pin) this.pinnedHeight = height;
|
|
783
|
+
return true;
|
|
971
784
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
)
|
|
985
|
-
|
|
986
|
-
for (let i = text.length; i > 0; i--) {
|
|
987
|
-
const truncated = text.slice(0, i);
|
|
988
|
-
const w = measureWord(
|
|
989
|
-
truncated,
|
|
990
|
-
fontSize,
|
|
991
|
-
fontFamily,
|
|
992
|
-
fontWeight,
|
|
993
|
-
fontStyle,
|
|
994
|
-
ctx,
|
|
995
|
-
letterSpacing
|
|
996
|
-
);
|
|
997
|
-
if (w <= availWidth) {
|
|
998
|
-
return truncated + ellipsis;
|
|
999
|
-
}
|
|
785
|
+
/**
|
|
786
|
+
* Drop the pinned height, so that Yoga sees the measured heights again.
|
|
787
|
+
*
|
|
788
|
+
* @returns Whether there was a pin
|
|
789
|
+
*/
|
|
790
|
+
unpin() {
|
|
791
|
+
const pinned = this.pinnedHeight !== void 0;
|
|
792
|
+
this.pinnedHeight = void 0;
|
|
793
|
+
this.reported.clear();
|
|
794
|
+
return pinned;
|
|
795
|
+
}
|
|
796
|
+
/** The text as laid out by the last `settle`. */
|
|
797
|
+
get layout() {
|
|
798
|
+
return this.settled?.result;
|
|
1000
799
|
}
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1003
|
-
function createTextMeasureFunc(text, style, ctx, emojiEnabled) {
|
|
1004
|
-
const measureStyle = { ...style, textOverflow: "clip" };
|
|
1005
|
-
return (width, _widthMode, _height, _heightMode) => {
|
|
1006
|
-
const maxWidth = width > 0 ? width : Infinity;
|
|
1007
|
-
const result = layoutText(text, measureStyle, maxWidth, ctx, emojiEnabled);
|
|
1008
|
-
const wrapped = result.segments.length > 1;
|
|
1009
|
-
const reportedWidth = wrapped ? Math.min(maxWidth, width > 0 ? width : result.width) : result.width;
|
|
1010
|
-
return { width: Math.min(reportedWidth, maxWidth), height: result.height };
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
800
|
+
};
|
|
1013
801
|
|
|
1014
802
|
// src/jsx/draw/clip.ts
|
|
1015
803
|
function applyClip(ctx, x, y, width, height, borderRadius) {
|
|
@@ -1056,198 +844,8 @@ function hasRadius(r) {
|
|
|
1056
844
|
return r.topLeft > 0 || r.topRight > 0 || r.bottomRight > 0 || r.bottomLeft > 0;
|
|
1057
845
|
}
|
|
1058
846
|
|
|
1059
|
-
// src/jsx/draw/offscreen.ts
|
|
1060
|
-
import { createCanvas as createCanvas2 } from "@napi-rs/canvas";
|
|
1061
|
-
var canvasPool = /* @__PURE__ */ new Map();
|
|
1062
|
-
function acquireOffscreen(w, h) {
|
|
1063
|
-
const key = `${w}x${h}`;
|
|
1064
|
-
const stack = canvasPool.get(key);
|
|
1065
|
-
if (stack) {
|
|
1066
|
-
while (stack.length > 0) {
|
|
1067
|
-
const ref = stack.pop();
|
|
1068
|
-
const canvas2 = ref.deref();
|
|
1069
|
-
if (canvas2) {
|
|
1070
|
-
const ctx = canvas2.getContext("2d");
|
|
1071
|
-
ctx.reset();
|
|
1072
|
-
return [canvas2, ctx];
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
const canvas = createCanvas2(w, h);
|
|
1077
|
-
return [canvas, canvas.getContext("2d")];
|
|
1078
|
-
}
|
|
1079
|
-
function releaseOffscreen(canvas) {
|
|
1080
|
-
const key = `${canvas.width}x${canvas.height}`;
|
|
1081
|
-
let stack = canvasPool.get(key);
|
|
1082
|
-
if (!stack) {
|
|
1083
|
-
stack = [];
|
|
1084
|
-
canvasPool.set(key, stack);
|
|
1085
|
-
}
|
|
1086
|
-
stack.push(new WeakRef(canvas));
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// src/jsx/draw/backdrop-filter.ts
|
|
1090
|
-
function drawBackdropFilter(ctx, filter, x, y, width, height, borderRadius) {
|
|
1091
|
-
if (width <= 0 || height <= 0) return;
|
|
1092
|
-
const normalized = filter.trim();
|
|
1093
|
-
if (!normalized || normalized === "none") return;
|
|
1094
|
-
const m = ctx.getTransform();
|
|
1095
|
-
const det = m.a * m.d - m.b * m.c;
|
|
1096
|
-
if (det === 0) return;
|
|
1097
|
-
const pad = Math.ceil(filterBleed(normalized)) + 1;
|
|
1098
|
-
const bx = x - pad;
|
|
1099
|
-
const by = y - pad;
|
|
1100
|
-
const bw = width + 2 * pad;
|
|
1101
|
-
const bh = height + 2 * pad;
|
|
1102
|
-
const bounds = transformedBounds(m, bx, by, bw, bh);
|
|
1103
|
-
const rx = Math.floor(bounds.x0);
|
|
1104
|
-
const ry = Math.floor(bounds.y0);
|
|
1105
|
-
const rw = Math.ceil(bounds.x1) - rx;
|
|
1106
|
-
const rh = Math.ceil(bounds.y1) - ry;
|
|
1107
|
-
const canvasW = ctx.canvas.width;
|
|
1108
|
-
const canvasH = ctx.canvas.height;
|
|
1109
|
-
if (rx + rw <= 0 || ry + rh <= 0 || rx >= canvasW || ry >= canvasH) return;
|
|
1110
|
-
const sx = Math.hypot(m.a, m.b);
|
|
1111
|
-
const sy = Math.hypot(m.c, m.d);
|
|
1112
|
-
const eps = 1e-6 * Math.max(sx, sy);
|
|
1113
|
-
const similar = Math.abs(sx - sy) <= eps && Math.abs(m.a * m.c + m.b * m.d) <= eps * sx;
|
|
1114
|
-
let bufW;
|
|
1115
|
-
let bufH;
|
|
1116
|
-
let scale;
|
|
1117
|
-
if (similar) {
|
|
1118
|
-
bufW = rw;
|
|
1119
|
-
bufH = rh;
|
|
1120
|
-
scale = sx;
|
|
1121
|
-
} else {
|
|
1122
|
-
scale = Math.max(sx, sy);
|
|
1123
|
-
bufW = Math.ceil(bw * scale);
|
|
1124
|
-
bufH = Math.ceil(bh * scale);
|
|
1125
|
-
}
|
|
1126
|
-
if (bufW <= 0 || bufH <= 0) return;
|
|
1127
|
-
const [snapshot, snapCtx] = acquireOffscreen(bufW, bufH);
|
|
1128
|
-
if (similar) {
|
|
1129
|
-
snapCtx.translate(-rx, -ry);
|
|
1130
|
-
} else {
|
|
1131
|
-
const inv = invert(m, det);
|
|
1132
|
-
snapCtx.setTransform(
|
|
1133
|
-
scale * inv.a,
|
|
1134
|
-
scale * inv.b,
|
|
1135
|
-
scale * inv.c,
|
|
1136
|
-
scale * inv.d,
|
|
1137
|
-
scale * (inv.e - bx),
|
|
1138
|
-
scale * (inv.f - by)
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
drawExtendedBackdrop(snapCtx, ctx.canvas, rx, ry, rw, rh);
|
|
1142
|
-
const filtered = filterSnapshot(snapshot, normalized, scale);
|
|
1143
|
-
releaseOffscreen(snapshot);
|
|
1144
|
-
ctx.save();
|
|
1145
|
-
applyClip(ctx, x, y, width, height, borderRadius);
|
|
1146
|
-
ctx.filter = "none";
|
|
1147
|
-
if (similar) {
|
|
1148
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1149
|
-
ctx.drawImage(filtered, 0, 0, bufW, bufH, rx, ry, bufW, bufH);
|
|
1150
|
-
} else {
|
|
1151
|
-
ctx.drawImage(filtered, 0, 0, bufW, bufH, bx, by, bw, bh);
|
|
1152
|
-
}
|
|
1153
|
-
ctx.restore();
|
|
1154
|
-
releaseOffscreen(filtered);
|
|
1155
|
-
}
|
|
1156
|
-
function drawExtendedBackdrop(target, canvas, rx, ry, rw, rh) {
|
|
1157
|
-
const cw = canvas.width;
|
|
1158
|
-
const ch = canvas.height;
|
|
1159
|
-
const ix0 = Math.max(rx, 0);
|
|
1160
|
-
const iy0 = Math.max(ry, 0);
|
|
1161
|
-
const ix1 = Math.min(rx + rw, cw);
|
|
1162
|
-
const iy1 = Math.min(ry + rh, ch);
|
|
1163
|
-
if (ix1 <= ix0 || iy1 <= iy0) return;
|
|
1164
|
-
const iw = ix1 - ix0;
|
|
1165
|
-
const ih = iy1 - iy0;
|
|
1166
|
-
target.drawImage(canvas, ix0, iy0, iw, ih, ix0, iy0, iw, ih);
|
|
1167
|
-
const left = ix0 - rx;
|
|
1168
|
-
const right = rx + rw - ix1;
|
|
1169
|
-
const top = iy0 - ry;
|
|
1170
|
-
const bottom = ry + rh - iy1;
|
|
1171
|
-
if (left > 0) target.drawImage(canvas, 0, iy0, 1, ih, rx, iy0, left, ih);
|
|
1172
|
-
if (right > 0) {
|
|
1173
|
-
target.drawImage(canvas, cw - 1, iy0, 1, ih, ix1, iy0, right, ih);
|
|
1174
|
-
}
|
|
1175
|
-
if (top > 0) target.drawImage(canvas, ix0, 0, iw, 1, ix0, ry, iw, top);
|
|
1176
|
-
if (bottom > 0) {
|
|
1177
|
-
target.drawImage(canvas, ix0, ch - 1, iw, 1, ix0, iy1, iw, bottom);
|
|
1178
|
-
}
|
|
1179
|
-
if (left > 0 && top > 0) {
|
|
1180
|
-
target.drawImage(canvas, 0, 0, 1, 1, rx, ry, left, top);
|
|
1181
|
-
}
|
|
1182
|
-
if (right > 0 && top > 0) {
|
|
1183
|
-
target.drawImage(canvas, cw - 1, 0, 1, 1, ix1, ry, right, top);
|
|
1184
|
-
}
|
|
1185
|
-
if (left > 0 && bottom > 0) {
|
|
1186
|
-
target.drawImage(canvas, 0, ch - 1, 1, 1, rx, iy1, left, bottom);
|
|
1187
|
-
}
|
|
1188
|
-
if (right > 0 && bottom > 0) {
|
|
1189
|
-
target.drawImage(canvas, cw - 1, ch - 1, 1, 1, ix1, iy1, right, bottom);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
function filterSnapshot(snapshot, filter, scale) {
|
|
1193
|
-
const [out, outCtx] = acquireOffscreen(snapshot.width, snapshot.height);
|
|
1194
|
-
outCtx.filter = scale === 1 ? filter : scaleFilterLengths(filter, scale);
|
|
1195
|
-
outCtx.drawImage(snapshot, 0, 0);
|
|
1196
|
-
outCtx.filter = "none";
|
|
1197
|
-
return out;
|
|
1198
|
-
}
|
|
1199
|
-
function transformedBounds(m, x, y, w, h) {
|
|
1200
|
-
let x0 = Infinity;
|
|
1201
|
-
let y0 = Infinity;
|
|
1202
|
-
let x1 = -Infinity;
|
|
1203
|
-
let y1 = -Infinity;
|
|
1204
|
-
for (const [px, py] of [
|
|
1205
|
-
[x, y],
|
|
1206
|
-
[x + w, y],
|
|
1207
|
-
[x, y + h],
|
|
1208
|
-
[x + w, y + h]
|
|
1209
|
-
]) {
|
|
1210
|
-
const dx = m.a * px + m.c * py + m.e;
|
|
1211
|
-
const dy = m.b * px + m.d * py + m.f;
|
|
1212
|
-
if (dx < x0) x0 = dx;
|
|
1213
|
-
if (dx > x1) x1 = dx;
|
|
1214
|
-
if (dy < y0) y0 = dy;
|
|
1215
|
-
if (dy > y1) y1 = dy;
|
|
1216
|
-
}
|
|
1217
|
-
return { x0, y0, x1, y1 };
|
|
1218
|
-
}
|
|
1219
|
-
function invert(m, det) {
|
|
1220
|
-
return {
|
|
1221
|
-
a: m.d / det,
|
|
1222
|
-
b: -m.b / det,
|
|
1223
|
-
c: -m.c / det,
|
|
1224
|
-
d: m.a / det,
|
|
1225
|
-
e: (m.c * m.f - m.d * m.e) / det,
|
|
1226
|
-
f: (m.b * m.e - m.a * m.f) / det
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
function filterBleed(filter) {
|
|
1230
|
-
let bleed = 0;
|
|
1231
|
-
for (const m of filter.matchAll(/blur\(\s*(-?\d*\.?\d+)(?:px)?\s*\)/g)) {
|
|
1232
|
-
bleed += Math.abs(parseFloat(m[1])) * 3;
|
|
1233
|
-
}
|
|
1234
|
-
for (const m of filter.matchAll(/drop-shadow\(((?:[^()]|\([^()]*\))*)\)/g)) {
|
|
1235
|
-
const args = m[1].replace(/[\w-]+\([^()]*\)/g, " ");
|
|
1236
|
-
const nums = args.match(/-?\d*\.?\d+(?=px|\s|$)/g) ?? [];
|
|
1237
|
-
const [ox = "0", oy = "0", blur = "0"] = nums;
|
|
1238
|
-
bleed += Math.abs(parseFloat(ox)) + Math.abs(parseFloat(oy)) + Math.abs(parseFloat(blur)) * 3;
|
|
1239
|
-
}
|
|
1240
|
-
return bleed;
|
|
1241
|
-
}
|
|
1242
|
-
function scaleFilterLengths(filter, scale) {
|
|
1243
|
-
return filter.replace(
|
|
1244
|
-
/(?<![\w.])(-?\d*\.?\d+)px/g,
|
|
1245
|
-
(_, n) => `${formatCSSNumber(parseFloat(n) * scale)}px`
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
847
|
// src/jsx/draw/clip-path.ts
|
|
1250
|
-
import { Path2D } from "@
|
|
848
|
+
import { Path2D } from "@effing/skia";
|
|
1251
849
|
|
|
1252
850
|
// src/jsx/draw/rect.ts
|
|
1253
851
|
function drawRect(ctx, x, y, width, height, style) {
|
|
@@ -1453,14 +1051,6 @@ function drawBorders(ctx, x, y, width, height, style, borderRadius) {
|
|
|
1453
1051
|
}
|
|
1454
1052
|
}
|
|
1455
1053
|
var BOX_SHADOW_RE = /(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?(?:\s+(-?\d+(?:\.\d+)?)\s*(?:px)?)?\s+(.*)/;
|
|
1456
|
-
function boxShadowExtent(boxShadow) {
|
|
1457
|
-
const parts = boxShadow.match(BOX_SHADOW_RE);
|
|
1458
|
-
if (!parts) return 0;
|
|
1459
|
-
const offsetX = parseFloat(parts[1]);
|
|
1460
|
-
const offsetY = parseFloat(parts[2]);
|
|
1461
|
-
const blur = parseFloat(parts[3]);
|
|
1462
|
-
return blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
|
|
1463
|
-
}
|
|
1464
1054
|
function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
|
|
1465
1055
|
const parts = boxShadow.match(BOX_SHADOW_RE);
|
|
1466
1056
|
if (!parts) return;
|
|
@@ -2308,6 +1898,32 @@ function splitGradientArgs(content) {
|
|
|
2308
1898
|
return parts;
|
|
2309
1899
|
}
|
|
2310
1900
|
|
|
1901
|
+
// src/jsx/draw/group.ts
|
|
1902
|
+
import { beginGroup, endGroup } from "@effing/skia/extensions";
|
|
1903
|
+
function isNoFilter(filter) {
|
|
1904
|
+
if (!filter) return true;
|
|
1905
|
+
const normalized = filter.trim();
|
|
1906
|
+
return normalized === "" || normalized === "none";
|
|
1907
|
+
}
|
|
1908
|
+
function beginElementGroup(ctx, opacity, filter) {
|
|
1909
|
+
const hasFilter = !isNoFilter(filter);
|
|
1910
|
+
if (opacity >= 1 && !hasFilter) return false;
|
|
1911
|
+
beginGroup(ctx, { opacity, filter: hasFilter ? filter.trim() : void 0 });
|
|
1912
|
+
return true;
|
|
1913
|
+
}
|
|
1914
|
+
function endElementGroup(ctx) {
|
|
1915
|
+
endGroup(ctx);
|
|
1916
|
+
}
|
|
1917
|
+
function drawBackdropFilter(ctx, filter, x, y, width, height, borderRadius, opacity) {
|
|
1918
|
+
if (width <= 0 || height <= 0) return;
|
|
1919
|
+
if (isNoFilter(filter)) return;
|
|
1920
|
+
ctx.save();
|
|
1921
|
+
applyClip(ctx, x, y, width, height, borderRadius);
|
|
1922
|
+
beginGroup(ctx, { backdropFilter: filter.trim(), opacity });
|
|
1923
|
+
endGroup(ctx);
|
|
1924
|
+
ctx.restore();
|
|
1925
|
+
}
|
|
1926
|
+
|
|
2311
1927
|
// src/jsx/draw/object-fit.ts
|
|
2312
1928
|
function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
|
|
2313
1929
|
const imgRatio = imgW / imgH;
|
|
@@ -2529,7 +2145,7 @@ function applySvgTransform(ctx, transform) {
|
|
|
2529
2145
|
}
|
|
2530
2146
|
|
|
2531
2147
|
// src/jsx/draw/svg/path.ts
|
|
2532
|
-
import { Path2D as Path2D2 } from "@
|
|
2148
|
+
import { Path2D as Path2D2 } from "@effing/skia";
|
|
2533
2149
|
function pathBBox(d) {
|
|
2534
2150
|
let minX = Infinity;
|
|
2535
2151
|
let minY = Infinity;
|
|
@@ -2769,6 +2385,36 @@ function parsePoints(value) {
|
|
|
2769
2385
|
return result;
|
|
2770
2386
|
}
|
|
2771
2387
|
|
|
2388
|
+
// src/jsx/draw/offscreen.ts
|
|
2389
|
+
import { createCanvas as createCanvas2 } from "@effing/skia";
|
|
2390
|
+
var canvasPool = /* @__PURE__ */ new Map();
|
|
2391
|
+
function acquireOffscreen(w, h) {
|
|
2392
|
+
const key = `${w}x${h}`;
|
|
2393
|
+
const stack = canvasPool.get(key);
|
|
2394
|
+
if (stack) {
|
|
2395
|
+
while (stack.length > 0) {
|
|
2396
|
+
const ref = stack.pop();
|
|
2397
|
+
const canvas2 = ref.deref();
|
|
2398
|
+
if (canvas2) {
|
|
2399
|
+
const ctx = canvas2.getContext("2d");
|
|
2400
|
+
ctx.reset();
|
|
2401
|
+
return [canvas2, ctx];
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
const canvas = createCanvas2(w, h);
|
|
2406
|
+
return [canvas, canvas.getContext("2d")];
|
|
2407
|
+
}
|
|
2408
|
+
function releaseOffscreen(canvas) {
|
|
2409
|
+
const key = `${canvas.width}x${canvas.height}`;
|
|
2410
|
+
let stack = canvasPool.get(key);
|
|
2411
|
+
if (!stack) {
|
|
2412
|
+
stack = [];
|
|
2413
|
+
canvasPool.set(key, stack);
|
|
2414
|
+
}
|
|
2415
|
+
stack.push(new WeakRef(canvas));
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2772
2418
|
// src/jsx/draw/svg/filters.ts
|
|
2773
2419
|
function applyFeOffset(input, dx, dy, w, h) {
|
|
2774
2420
|
const [canvas, ctx] = acquireOffscreen(w, h);
|
|
@@ -2918,10 +2564,10 @@ function applyFilter(filterDef, sourceGraphic, w, h) {
|
|
|
2918
2564
|
}
|
|
2919
2565
|
|
|
2920
2566
|
// src/jsx/draw/svg/shapes.ts
|
|
2921
|
-
import { Path2D as Path2D4 } from "@
|
|
2567
|
+
import { Path2D as Path2D4 } from "@effing/skia";
|
|
2922
2568
|
|
|
2923
2569
|
// src/jsx/draw/svg/gradients.ts
|
|
2924
|
-
import { Path2D as Path2D3 } from "@
|
|
2570
|
+
import { Path2D as Path2D3 } from "@effing/skia";
|
|
2925
2571
|
import parseCssColor from "parse-css-color";
|
|
2926
2572
|
function parseFrac(value, fallback) {
|
|
2927
2573
|
if (value == null) return fallback;
|
|
@@ -3374,110 +3020,75 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
|
3374
3020
|
}
|
|
3375
3021
|
|
|
3376
3022
|
// src/jsx/draw/text.ts
|
|
3377
|
-
import { loadImage as loadImage2 } from "@
|
|
3023
|
+
import { loadImage as loadImage2 } from "@effing/skia";
|
|
3024
|
+
import { fillParagraph, strokeParagraph } from "@effing/skia/extensions";
|
|
3378
3025
|
import parseCssColor2 from "parse-css-color";
|
|
3379
3026
|
|
|
3380
3027
|
// src/jsx/emoji.ts
|
|
3381
|
-
var
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
var
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
c = unicodeSurrogates.charCodeAt(i++);
|
|
3399
|
-
if (p) {
|
|
3400
|
-
r.push((65536 + (p - 55296 << 10) + (c - 56320)).toString(16));
|
|
3401
|
-
p = 0;
|
|
3402
|
-
} else if (55296 <= c && c <= 56319) {
|
|
3403
|
-
p = c;
|
|
3404
|
-
} else {
|
|
3405
|
-
r.push(c.toString(16));
|
|
3028
|
+
var FE0F = 65039;
|
|
3029
|
+
var ZWJ = 8205;
|
|
3030
|
+
var KEYCAP = 8419;
|
|
3031
|
+
var EMOJI_CHAR2 = /[\p{Emoji}\p{Extended_Pictographic}]/u;
|
|
3032
|
+
var EMOJI_PRESENTATION = new RegExp(
|
|
3033
|
+
String.raw`\p{Emoji_Presentation}|${UNASSIGNED_PICTOGRAPHIC}`,
|
|
3034
|
+
"u"
|
|
3035
|
+
);
|
|
3036
|
+
var EMOJI_MODIFIER = /\p{Emoji_Modifier}/u;
|
|
3037
|
+
function qualifiedCodePoints(emoji) {
|
|
3038
|
+
const chars = [...emoji].filter((c) => c.codePointAt(0) !== FE0F);
|
|
3039
|
+
const codePoints = [];
|
|
3040
|
+
chars.forEach((char, i) => {
|
|
3041
|
+
codePoints.push(char.codePointAt(0));
|
|
3042
|
+
const startsElement = i === 0 || chars[i - 1] === "\u200D";
|
|
3043
|
+
if (startsElement && EMOJI_CHAR2.test(char) && !EMOJI_PRESENTATION.test(char) && !EMOJI_MODIFIER.test(chars[i + 1] ?? "")) {
|
|
3044
|
+
codePoints.push(FE0F);
|
|
3406
3045
|
}
|
|
3407
|
-
}
|
|
3408
|
-
return
|
|
3046
|
+
});
|
|
3047
|
+
return codePoints;
|
|
3409
3048
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
const key = type + ":" + code;
|
|
3413
|
-
if (key in emojiCache) return emojiCache[key];
|
|
3414
|
-
const api = emojiApis[type];
|
|
3415
|
-
const url = typeof api === "function" ? api(code) : `${api}${code.toUpperCase()}.svg`;
|
|
3416
|
-
return emojiCache[key] = fetch(url).then((r) => r.text());
|
|
3049
|
+
function hex(codePoints, pad = 0) {
|
|
3050
|
+
return codePoints.map((cp) => cp.toString(16).padStart(pad, "0")).join("-");
|
|
3417
3051
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
x: currentX,
|
|
3445
|
-
width: emojiSize
|
|
3446
|
-
});
|
|
3447
|
-
currentX += emojiSize + letterSpacing;
|
|
3448
|
-
textStartX = currentX;
|
|
3449
|
-
} else {
|
|
3450
|
-
if (!currentText) textStartX = currentX;
|
|
3451
|
-
currentText += segment;
|
|
3452
|
-
currentGraphemeCount++;
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
if (currentText) {
|
|
3456
|
-
const textWidth = measureText2(currentText);
|
|
3457
|
-
runs.push({
|
|
3458
|
-
kind: "text",
|
|
3459
|
-
text: currentText,
|
|
3460
|
-
x: textStartX,
|
|
3461
|
-
width: textWidth + letterSpacing * currentGraphemeCount
|
|
3462
|
-
});
|
|
3463
|
-
}
|
|
3464
|
-
return runs;
|
|
3052
|
+
function twemojiCode(codePoints) {
|
|
3053
|
+
const code = hex(
|
|
3054
|
+
codePoints.includes(ZWJ) ? codePoints : codePoints.filter((cp) => cp !== FE0F)
|
|
3055
|
+
);
|
|
3056
|
+
return code === "1f441-fe0f-200d-1f5e8-fe0f" ? "1f441-200d-1f5e8" : code;
|
|
3057
|
+
}
|
|
3058
|
+
function svgmojiCode(codePoints) {
|
|
3059
|
+
const keepFE0F = codePoints.includes(ZWJ) || codePoints.includes(KEYCAP);
|
|
3060
|
+
return hex(
|
|
3061
|
+
keepFE0F ? codePoints : codePoints.filter((cp) => cp !== FE0F),
|
|
3062
|
+
4
|
|
3063
|
+
).toUpperCase();
|
|
3064
|
+
}
|
|
3065
|
+
function fluentCode(codePoints) {
|
|
3066
|
+
return encodeURIComponent(String.fromCodePoint(...codePoints));
|
|
3067
|
+
}
|
|
3068
|
+
var emojiUrls = {
|
|
3069
|
+
twemoji: (cps) => `https://cdnjs.cloudflare.com/ajax/libs/twemoji/16.0.1/svg/${twemojiCode(cps)}.svg`,
|
|
3070
|
+
openmoji: (cps) => `https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/${svgmojiCode(cps)}.svg`,
|
|
3071
|
+
blobmoji: (cps) => `https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/${svgmojiCode(cps)}.svg`,
|
|
3072
|
+
noto: (cps) => `https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/${svgmojiCode(cps)}.svg`,
|
|
3073
|
+
fluent: (cps) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${fluentCode(cps)}_color.svg`,
|
|
3074
|
+
fluentFlat: (cps) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${fluentCode(cps)}_flat.svg`
|
|
3075
|
+
};
|
|
3076
|
+
function emojiUrl(style, emoji) {
|
|
3077
|
+
return emojiUrls[style](qualifiedCodePoints(emoji));
|
|
3465
3078
|
}
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
return false;
|
|
3079
|
+
var emojiCache = {};
|
|
3080
|
+
async function loadEmoji(style, emoji) {
|
|
3081
|
+
const url = emojiUrl(style, emoji);
|
|
3082
|
+
return emojiCache[url] ??= fetch(url).then((r) => r.text());
|
|
3471
3083
|
}
|
|
3472
3084
|
|
|
3473
3085
|
// src/jsx/draw/text.ts
|
|
3474
3086
|
var emojiImageCache = /* @__PURE__ */ new Map();
|
|
3475
|
-
function loadEmojiImage(style,
|
|
3476
|
-
const
|
|
3477
|
-
const key = style + ":" + code;
|
|
3087
|
+
function loadEmojiImage(style, emoji) {
|
|
3088
|
+
const key = emojiUrl(style, emoji);
|
|
3478
3089
|
let cached = emojiImageCache.get(key);
|
|
3479
3090
|
if (!cached) {
|
|
3480
|
-
cached = loadEmoji(style,
|
|
3091
|
+
cached = loadEmoji(style, emoji).then((svgText) => {
|
|
3481
3092
|
if (!svgText || !svgText.includes("<svg")) return null;
|
|
3482
3093
|
const dataUri = "data:image/svg+xml;base64," + Buffer.from(svgText).toString("base64");
|
|
3483
3094
|
return loadImage2(dataUri);
|
|
@@ -3486,166 +3097,62 @@ function loadEmojiImage(style, char) {
|
|
|
3486
3097
|
}
|
|
3487
3098
|
return cached;
|
|
3488
3099
|
}
|
|
3489
|
-
async function drawText(ctx,
|
|
3100
|
+
async function drawText(ctx, layout, offsetX, offsetY, textShadow, emojiStyle) {
|
|
3101
|
+
const { paragraph, segments, emoji } = layout;
|
|
3102
|
+
const first = segments[0];
|
|
3103
|
+
if (!first) return;
|
|
3104
|
+
const x = offsetX;
|
|
3105
|
+
const y = offsetY + layout.paragraphOffsetY;
|
|
3490
3106
|
const shadow = textShadow ? parseShadow(textShadow) : null;
|
|
3107
|
+
ctx.fillStyle = first.color;
|
|
3108
|
+
if (shadow) {
|
|
3109
|
+
drawShadowPass(
|
|
3110
|
+
ctx,
|
|
3111
|
+
shadow,
|
|
3112
|
+
getColorAlpha(first.color),
|
|
3113
|
+
() => fillParagraph(ctx, paragraph, x, y)
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
if (first.textStrokeWidth !== void 0 && first.textStrokeWidth > 0) {
|
|
3117
|
+
ctx.save();
|
|
3118
|
+
ctx.lineWidth = first.textStrokeWidth;
|
|
3119
|
+
ctx.strokeStyle = first.textStrokeColor ?? first.color;
|
|
3120
|
+
ctx.lineJoin = "round";
|
|
3121
|
+
strokeParagraph(ctx, paragraph, x, y);
|
|
3122
|
+
ctx.restore();
|
|
3123
|
+
}
|
|
3124
|
+
fillParagraph(ctx, paragraph, x, y);
|
|
3125
|
+
if (emojiStyle && emoji.length > 0) {
|
|
3126
|
+
await drawEmoji(ctx, emoji, first, offsetX, offsetY, emojiStyle);
|
|
3127
|
+
}
|
|
3491
3128
|
for (const seg of segments) {
|
|
3492
|
-
if (!seg.text) continue;
|
|
3493
|
-
setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
|
|
3494
|
-
ctx.fillStyle = seg.color;
|
|
3495
|
-
const x = offsetX + seg.x;
|
|
3496
|
-
const y = offsetY + seg.y;
|
|
3497
|
-
const textAlpha = shadow ? getColorAlpha(seg.color) : 1;
|
|
3498
|
-
const hasStroke = seg.textStrokeWidth !== void 0 && seg.textStrokeWidth > 0;
|
|
3499
|
-
if (emojiStyle) {
|
|
3500
|
-
await drawSegmentWithEmoji(
|
|
3501
|
-
ctx,
|
|
3502
|
-
seg,
|
|
3503
|
-
x,
|
|
3504
|
-
y,
|
|
3505
|
-
textShadow,
|
|
3506
|
-
emojiStyle,
|
|
3507
|
-
hasStroke
|
|
3508
|
-
);
|
|
3509
|
-
} else if (seg.letterSpacing && seg.letterSpacing !== 0) {
|
|
3510
|
-
if (shadow) {
|
|
3511
|
-
drawShadowPass(
|
|
3512
|
-
ctx,
|
|
3513
|
-
shadow,
|
|
3514
|
-
textAlpha,
|
|
3515
|
-
() => drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing)
|
|
3516
|
-
);
|
|
3517
|
-
}
|
|
3518
|
-
if (hasStroke) {
|
|
3519
|
-
drawStrokeWithLetterSpacing(
|
|
3520
|
-
ctx,
|
|
3521
|
-
seg.text,
|
|
3522
|
-
x,
|
|
3523
|
-
y,
|
|
3524
|
-
seg.letterSpacing,
|
|
3525
|
-
seg.textStrokeWidth,
|
|
3526
|
-
seg.textStrokeColor ?? seg.color
|
|
3527
|
-
);
|
|
3528
|
-
}
|
|
3529
|
-
drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
|
|
3530
|
-
} else {
|
|
3531
|
-
if (shadow) {
|
|
3532
|
-
drawShadowPass(
|
|
3533
|
-
ctx,
|
|
3534
|
-
shadow,
|
|
3535
|
-
textAlpha,
|
|
3536
|
-
() => ctx.fillText(seg.text, x, y)
|
|
3537
|
-
);
|
|
3538
|
-
}
|
|
3539
|
-
if (hasStroke) {
|
|
3540
|
-
ctx.save();
|
|
3541
|
-
ctx.lineWidth = seg.textStrokeWidth;
|
|
3542
|
-
ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
|
|
3543
|
-
ctx.lineJoin = "round";
|
|
3544
|
-
ctx.strokeText(seg.text, x, y);
|
|
3545
|
-
ctx.restore();
|
|
3546
|
-
}
|
|
3547
|
-
ctx.fillText(seg.text, x, y);
|
|
3548
|
-
}
|
|
3549
3129
|
if (seg.textDecoration) {
|
|
3550
3130
|
drawTextDecoration(ctx, seg, offsetX, offsetY);
|
|
3551
3131
|
}
|
|
3552
3132
|
}
|
|
3553
3133
|
}
|
|
3554
|
-
async function
|
|
3555
|
-
const
|
|
3556
|
-
|
|
3557
|
-
seg.text,
|
|
3558
|
-
(text) => {
|
|
3559
|
-
setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
|
|
3560
|
-
return ctx.measureText(text).width;
|
|
3561
|
-
},
|
|
3562
|
-
seg.fontSize,
|
|
3563
|
-
letterSpacing
|
|
3134
|
+
async function drawEmoji(ctx, emoji, font, offsetX, offsetY, emojiStyle) {
|
|
3135
|
+
const images = await Promise.all(
|
|
3136
|
+
emoji.map((e) => loadEmojiImage(emojiStyle, e.grapheme))
|
|
3564
3137
|
);
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
if (letterSpacing !== 0) {
|
|
3571
|
-
drawShadowPass(
|
|
3572
|
-
ctx,
|
|
3573
|
-
shadow,
|
|
3574
|
-
textAlpha,
|
|
3575
|
-
() => drawTextWithLetterSpacing(
|
|
3576
|
-
ctx,
|
|
3577
|
-
run.text,
|
|
3578
|
-
x + run.x,
|
|
3579
|
-
y,
|
|
3580
|
-
letterSpacing
|
|
3581
|
-
)
|
|
3582
|
-
);
|
|
3583
|
-
} else {
|
|
3584
|
-
drawShadowPass(
|
|
3585
|
-
ctx,
|
|
3586
|
-
shadow,
|
|
3587
|
-
textAlpha,
|
|
3588
|
-
() => ctx.fillText(run.text, x + run.x, y)
|
|
3589
|
-
);
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
if (hasStroke) {
|
|
3593
|
-
if (letterSpacing !== 0) {
|
|
3594
|
-
drawStrokeWithLetterSpacing(
|
|
3595
|
-
ctx,
|
|
3596
|
-
run.text,
|
|
3597
|
-
x + run.x,
|
|
3598
|
-
y,
|
|
3599
|
-
letterSpacing,
|
|
3600
|
-
seg.textStrokeWidth,
|
|
3601
|
-
seg.textStrokeColor ?? seg.color
|
|
3602
|
-
);
|
|
3603
|
-
} else {
|
|
3604
|
-
ctx.save();
|
|
3605
|
-
ctx.lineWidth = seg.textStrokeWidth;
|
|
3606
|
-
ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
|
|
3607
|
-
ctx.lineJoin = "round";
|
|
3608
|
-
ctx.strokeText(run.text, x + run.x, y);
|
|
3609
|
-
ctx.restore();
|
|
3610
|
-
}
|
|
3611
|
-
}
|
|
3612
|
-
if (letterSpacing !== 0) {
|
|
3613
|
-
drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
|
|
3614
|
-
} else {
|
|
3615
|
-
ctx.fillText(run.text, x + run.x, y);
|
|
3616
|
-
}
|
|
3617
|
-
} else {
|
|
3618
|
-
const img = await loadEmojiImage(emojiStyle, run.char);
|
|
3619
|
-
if (img) {
|
|
3620
|
-
const emojiSize = seg.fontSize;
|
|
3621
|
-
const emojiY = y - seg.ascent + (seg.height - seg.fontSize) / 2;
|
|
3622
|
-
ctx.drawImage(img, x + run.x, emojiY, emojiSize, emojiSize);
|
|
3623
|
-
} else {
|
|
3624
|
-
ctx.fillText(run.char, x + run.x, y);
|
|
3625
|
-
}
|
|
3138
|
+
emoji.forEach((e, i) => {
|
|
3139
|
+
const img = images[i];
|
|
3140
|
+
if (img) {
|
|
3141
|
+
ctx.drawImage(img, offsetX + e.x, offsetY + e.y, e.size, e.size);
|
|
3142
|
+
return;
|
|
3626
3143
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
ctx.lineWidth = strokeWidth;
|
|
3640
|
-
ctx.strokeStyle = strokeColor;
|
|
3641
|
-
ctx.lineJoin = "round";
|
|
3642
|
-
let currentX = x;
|
|
3643
|
-
for (const char of text) {
|
|
3644
|
-
ctx.strokeText(char, currentX, y);
|
|
3645
|
-
const metrics = ctx.measureText(char);
|
|
3646
|
-
currentX += metrics.width + letterSpacing;
|
|
3647
|
-
}
|
|
3648
|
-
ctx.restore();
|
|
3144
|
+
setFont(
|
|
3145
|
+
ctx,
|
|
3146
|
+
font.fontSize,
|
|
3147
|
+
font.fontFamily,
|
|
3148
|
+
font.fontWeight,
|
|
3149
|
+
font.fontStyle
|
|
3150
|
+
);
|
|
3151
|
+
const textRendering = ctx.textRendering;
|
|
3152
|
+
ctx.textRendering = "geometricPrecision";
|
|
3153
|
+
ctx.fillText(e.grapheme, offsetX + e.x, offsetY + e.baseline);
|
|
3154
|
+
ctx.textRendering = textRendering;
|
|
3155
|
+
});
|
|
3649
3156
|
}
|
|
3650
3157
|
function parseShadow(shadow) {
|
|
3651
3158
|
const parts = shadow.match(
|
|
@@ -3706,139 +3213,30 @@ function drawTextDecoration(ctx, seg, offsetX, offsetY) {
|
|
|
3706
3213
|
async function drawNode(ctx, node, parentX, parentY, context, emojiStyle) {
|
|
3707
3214
|
const x = parentX + node.x;
|
|
3708
3215
|
const y = parentY + node.y;
|
|
3709
|
-
const {
|
|
3216
|
+
const { style } = node;
|
|
3710
3217
|
const renderContext = context ?? {
|
|
3711
3218
|
imageCache: /* @__PURE__ */ new Map(),
|
|
3712
3219
|
debug: false
|
|
3713
3220
|
};
|
|
3714
3221
|
if (style.display === "none") return;
|
|
3715
|
-
const
|
|
3222
|
+
const rawOpacity = Number(style.opacity ?? 1);
|
|
3223
|
+
const opacity = Number.isFinite(rawOpacity) ? rawOpacity : 1;
|
|
3716
3224
|
if (opacity <= 0) return;
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
const bleed = Math.max(1, Math.ceil(subtree.overflowBleed));
|
|
3725
|
-
const qx = Math.max(1, Math.ceil(Math.abs(sx)));
|
|
3726
|
-
const qy = Math.max(1, Math.ceil(Math.abs(sy)));
|
|
3727
|
-
const bufW = Math.ceil((width + 2 * bleed) * qx);
|
|
3728
|
-
const bufH = Math.ceil((height + 2 * bleed) * qy);
|
|
3729
|
-
if (bufW > 0 && bufH > 0) {
|
|
3730
|
-
const [offscreen, offCtx] = acquireOffscreen(bufW, bufH);
|
|
3731
|
-
offCtx.save();
|
|
3732
|
-
offCtx.scale(qx, qy);
|
|
3733
|
-
await drawNodeCore(
|
|
3734
|
-
offCtx,
|
|
3735
|
-
node,
|
|
3736
|
-
parentX,
|
|
3737
|
-
parentY,
|
|
3738
|
-
bleed - x,
|
|
3739
|
-
bleed - y,
|
|
3740
|
-
emojiStyle,
|
|
3741
|
-
renderContext,
|
|
3742
|
-
transformWithoutScale
|
|
3743
|
-
);
|
|
3744
|
-
offCtx.restore();
|
|
3745
|
-
ctx.save();
|
|
3746
|
-
if (opacity < 1) {
|
|
3747
|
-
ctx.globalAlpha *= opacity;
|
|
3748
|
-
}
|
|
3749
|
-
let ox = x + width / 2;
|
|
3750
|
-
let oy = y + height / 2;
|
|
3751
|
-
if (style.transformOrigin) {
|
|
3752
|
-
const parts = style.transformOrigin.split(/\s+/);
|
|
3753
|
-
ox = resolveOrigin(parts[0], x, width);
|
|
3754
|
-
oy = resolveOrigin(parts[1], y, height);
|
|
3755
|
-
}
|
|
3756
|
-
ctx.translate(ox, oy);
|
|
3757
|
-
ctx.scale(sx, sy);
|
|
3758
|
-
ctx.translate(-ox, -oy);
|
|
3759
|
-
ctx.drawImage(
|
|
3760
|
-
offscreen,
|
|
3761
|
-
0,
|
|
3762
|
-
0,
|
|
3763
|
-
bufW,
|
|
3764
|
-
bufH,
|
|
3765
|
-
x - bleed,
|
|
3766
|
-
y - bleed,
|
|
3767
|
-
width + 2 * bleed,
|
|
3768
|
-
height + 2 * bleed
|
|
3769
|
-
);
|
|
3770
|
-
releaseOffscreen(offscreen);
|
|
3771
|
-
ctx.restore();
|
|
3772
|
-
return;
|
|
3773
|
-
}
|
|
3225
|
+
const group = { open: false };
|
|
3226
|
+
ctx.save();
|
|
3227
|
+
try {
|
|
3228
|
+
await paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, group);
|
|
3229
|
+
} finally {
|
|
3230
|
+
if (group.open) endElementGroup(ctx);
|
|
3231
|
+
ctx.restore();
|
|
3774
3232
|
}
|
|
3775
|
-
await drawNodeCore(
|
|
3776
|
-
ctx,
|
|
3777
|
-
node,
|
|
3778
|
-
parentX,
|
|
3779
|
-
parentY,
|
|
3780
|
-
0,
|
|
3781
|
-
0,
|
|
3782
|
-
emojiStyle,
|
|
3783
|
-
renderContext
|
|
3784
|
-
);
|
|
3785
|
-
}
|
|
3786
|
-
function extractScale(transform) {
|
|
3787
|
-
const scaleMatch = transform.match(/\b(scale|scaleX|scaleY)\(([^)]+)\)/);
|
|
3788
|
-
if (!scaleMatch) return null;
|
|
3789
|
-
const [fullMatch, name, args] = scaleMatch;
|
|
3790
|
-
const values = args.split(",").map((s) => s.trim());
|
|
3791
|
-
const sx = name === "scaleY" ? 1 : parseFloat(values[0]);
|
|
3792
|
-
const sy = name === "scaleX" ? 1 : parseFloat(values[name === "scale" ? 1 : 0] ?? String(sx));
|
|
3793
|
-
const remaining = transform.replace(fullMatch, "").trim();
|
|
3794
|
-
return { sx, sy, remaining };
|
|
3795
|
-
}
|
|
3796
|
-
function scanSubtree(node) {
|
|
3797
|
-
let overflowBleed = 0;
|
|
3798
|
-
let hasBackdropFilter = false;
|
|
3799
|
-
const visit = (n) => {
|
|
3800
|
-
if (n.style.display === "none") return;
|
|
3801
|
-
if ((n.style.opacity ?? 1) <= 0) return;
|
|
3802
|
-
const fontSize = typeof n.style.fontSize === "number" ? n.style.fontSize : 0;
|
|
3803
|
-
if (n.textContent !== void 0 && n.textContent !== "" && fontSize > 0) {
|
|
3804
|
-
const letterSpacing = typeof n.style.letterSpacing === "number" ? n.style.letterSpacing : 0;
|
|
3805
|
-
overflowBleed = Math.max(
|
|
3806
|
-
overflowBleed,
|
|
3807
|
-
fontSize + Math.max(0, -letterSpacing)
|
|
3808
|
-
);
|
|
3809
|
-
}
|
|
3810
|
-
if (n.style.boxShadow) {
|
|
3811
|
-
overflowBleed = Math.max(
|
|
3812
|
-
overflowBleed,
|
|
3813
|
-
boxShadowExtent(n.style.boxShadow)
|
|
3814
|
-
);
|
|
3815
|
-
}
|
|
3816
|
-
const filter = n.style.backdropFilter;
|
|
3817
|
-
if (filter && filter.trim() !== "none") hasBackdropFilter = true;
|
|
3818
|
-
for (const child of n.children) visit(child);
|
|
3819
|
-
};
|
|
3820
|
-
visit(node);
|
|
3821
|
-
return { overflowBleed, hasBackdropFilter };
|
|
3822
3233
|
}
|
|
3823
|
-
async function
|
|
3824
|
-
const x = parentX + node.x + offsetX;
|
|
3825
|
-
const y = parentY + node.y + offsetY;
|
|
3234
|
+
async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, group) {
|
|
3826
3235
|
const { width, height, style } = node;
|
|
3827
|
-
if (style.
|
|
3828
|
-
const opacity = style.opacity ?? 1;
|
|
3829
|
-
if (opacity <= 0) return;
|
|
3830
|
-
ctx.save();
|
|
3831
|
-
if (opacity < 1) {
|
|
3832
|
-
ctx.globalAlpha *= opacity;
|
|
3833
|
-
}
|
|
3834
|
-
if (style.filter) {
|
|
3835
|
-
ctx.filter = style.filter;
|
|
3836
|
-
}
|
|
3837
|
-
const transformToApply = overrideTransform !== void 0 ? overrideTransform : style.transform;
|
|
3838
|
-
if (transformToApply) {
|
|
3236
|
+
if (style.transform) {
|
|
3839
3237
|
applyTransform(
|
|
3840
3238
|
ctx,
|
|
3841
|
-
|
|
3239
|
+
style.transform,
|
|
3842
3240
|
x,
|
|
3843
3241
|
y,
|
|
3844
3242
|
width,
|
|
@@ -3858,9 +3256,11 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3858
3256
|
y,
|
|
3859
3257
|
width,
|
|
3860
3258
|
height,
|
|
3861
|
-
borderRadius
|
|
3259
|
+
borderRadius,
|
|
3260
|
+
opacity
|
|
3862
3261
|
);
|
|
3863
3262
|
}
|
|
3263
|
+
group.open = beginElementGroup(ctx, opacity, style.filter);
|
|
3864
3264
|
if (style.boxShadow) {
|
|
3865
3265
|
drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
|
|
3866
3266
|
}
|
|
@@ -3902,9 +3302,9 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3902
3302
|
applyClip(ctx, x, y, width, height, borderRadius);
|
|
3903
3303
|
}
|
|
3904
3304
|
const image = await cachedLoadImage(
|
|
3905
|
-
|
|
3305
|
+
renderContext.imageCache,
|
|
3906
3306
|
urlMatch[1],
|
|
3907
|
-
|
|
3307
|
+
renderContext.userAgent
|
|
3908
3308
|
);
|
|
3909
3309
|
const bgSize = style.backgroundSize;
|
|
3910
3310
|
if (bgSize === "cover") {
|
|
@@ -3960,7 +3360,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3960
3360
|
}
|
|
3961
3361
|
}
|
|
3962
3362
|
}
|
|
3963
|
-
if (
|
|
3363
|
+
if (renderContext.debug) {
|
|
3964
3364
|
ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
|
|
3965
3365
|
ctx.lineWidth = 1;
|
|
3966
3366
|
ctx.strokeRect(x, y, width, height);
|
|
@@ -3975,16 +3375,10 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3975
3375
|
const contentX = x + paddingLeft + borderLeftW;
|
|
3976
3376
|
const contentY = y + paddingTop + borderTopW;
|
|
3977
3377
|
const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
|
|
3978
|
-
const textLayout = layoutText(
|
|
3979
|
-
node.textContent,
|
|
3980
|
-
style,
|
|
3981
|
-
contentWidth,
|
|
3982
|
-
ctx,
|
|
3983
|
-
!!emojiStyle
|
|
3984
|
-
);
|
|
3378
|
+
const textLayout = node.textLayout && contentWidth === node.width ? node.textLayout : layoutText(node.textContent, style, contentWidth, !!emojiStyle);
|
|
3985
3379
|
await drawText(
|
|
3986
3380
|
ctx,
|
|
3987
|
-
textLayout
|
|
3381
|
+
textLayout,
|
|
3988
3382
|
contentX,
|
|
3989
3383
|
contentY,
|
|
3990
3384
|
style.textShadow,
|
|
@@ -4016,7 +3410,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
4016
3410
|
imgY,
|
|
4017
3411
|
imgW,
|
|
4018
3412
|
imgH,
|
|
4019
|
-
|
|
3413
|
+
renderContext,
|
|
4020
3414
|
style
|
|
4021
3415
|
);
|
|
4022
3416
|
}
|
|
@@ -4024,14 +3418,9 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
4024
3418
|
drawSvgContainer(ctx, node, x, y, width, height);
|
|
4025
3419
|
} else {
|
|
4026
3420
|
for (const child of node.children) {
|
|
4027
|
-
|
|
4028
|
-
await drawNode(ctx, child, x, y, context, emojiStyle);
|
|
4029
|
-
} else {
|
|
4030
|
-
await drawNodeCore(ctx, child, x, y, 0, 0, emojiStyle, context);
|
|
4031
|
-
}
|
|
3421
|
+
await drawNode(ctx, child, x, y, renderContext, emojiStyle);
|
|
4032
3422
|
}
|
|
4033
3423
|
}
|
|
4034
|
-
ctx.restore();
|
|
4035
3424
|
}
|
|
4036
3425
|
function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
|
|
4037
3426
|
let ox = x + width / 2;
|
|
@@ -4098,6 +3487,28 @@ function parseAngle(value) {
|
|
|
4098
3487
|
return parseFloat(value);
|
|
4099
3488
|
}
|
|
4100
3489
|
|
|
3490
|
+
// src/jsx/font.ts
|
|
3491
|
+
import { GlobalFonts } from "@effing/skia";
|
|
3492
|
+
var registeredFonts = /* @__PURE__ */ new Set();
|
|
3493
|
+
function registerFont(font) {
|
|
3494
|
+
const key = `${font.name}:${font.weight}:${font.style}`;
|
|
3495
|
+
if (registeredFonts.has(key)) return;
|
|
3496
|
+
const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
|
|
3497
|
+
GlobalFonts.register(buffer, font.name);
|
|
3498
|
+
registeredFonts.add(key);
|
|
3499
|
+
}
|
|
3500
|
+
function registerFontFromPath(path, nameAlias) {
|
|
3501
|
+
GlobalFonts.registerFromPath(path, nameAlias ?? "");
|
|
3502
|
+
}
|
|
3503
|
+
function registeredFamilies() {
|
|
3504
|
+
return GlobalFonts.families.map((f) => f.family);
|
|
3505
|
+
}
|
|
3506
|
+
function ensureFontsRegistered(fonts) {
|
|
3507
|
+
for (const font of fonts) {
|
|
3508
|
+
registerFont(font);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
4101
3512
|
// src/jsx/style/expand.ts
|
|
4102
3513
|
var SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
4103
3514
|
function parseSides(value) {
|
|
@@ -4291,6 +3702,10 @@ function expandStyle(raw, fontFamilies) {
|
|
|
4291
3702
|
style.backdropFilter = style.WebkitBackdropFilter;
|
|
4292
3703
|
delete style.WebkitBackdropFilter;
|
|
4293
3704
|
}
|
|
3705
|
+
if (style.wordWrap !== void 0) {
|
|
3706
|
+
if (style.overflowWrap === void 0) style.overflowWrap = style.wordWrap;
|
|
3707
|
+
delete style.wordWrap;
|
|
3708
|
+
}
|
|
4294
3709
|
if (style.WebkitClipPath !== void 0) {
|
|
4295
3710
|
if (style.clipPath === void 0) style.clipPath = style.WebkitClipPath;
|
|
4296
3711
|
delete style.WebkitClipPath;
|
|
@@ -4415,6 +3830,7 @@ import Yoga, {
|
|
|
4415
3830
|
FlexDirection,
|
|
4416
3831
|
Gutter,
|
|
4417
3832
|
Justify,
|
|
3833
|
+
MeasureMode,
|
|
4418
3834
|
Overflow,
|
|
4419
3835
|
PositionType,
|
|
4420
3836
|
Wrap
|
|
@@ -4422,6 +3838,14 @@ import Yoga, {
|
|
|
4422
3838
|
function createYogaNode() {
|
|
4423
3839
|
return Yoga.Node.create();
|
|
4424
3840
|
}
|
|
3841
|
+
var textConfig;
|
|
3842
|
+
function createTextYogaNode() {
|
|
3843
|
+
if (!textConfig) {
|
|
3844
|
+
textConfig = Yoga.Config.create();
|
|
3845
|
+
textConfig.setPointScaleFactor(0);
|
|
3846
|
+
}
|
|
3847
|
+
return Yoga.Node.create(textConfig);
|
|
3848
|
+
}
|
|
4425
3849
|
function freeYogaNode(node) {
|
|
4426
3850
|
node.freeRecursive();
|
|
4427
3851
|
}
|
|
@@ -4594,20 +4018,20 @@ function applyEdgeValue(node, setter, edge, value) {
|
|
|
4594
4018
|
}
|
|
4595
4019
|
|
|
4596
4020
|
// src/jsx/layout.ts
|
|
4597
|
-
async function buildLayoutTree(element, containerWidth, containerHeight,
|
|
4021
|
+
async function buildLayoutTree(element, containerWidth, containerHeight, emojiEnabled, fontFamilies, context) {
|
|
4598
4022
|
const renderContext = context ?? {
|
|
4599
4023
|
imageCache: /* @__PURE__ */ new Map(),
|
|
4600
4024
|
debug: false
|
|
4601
4025
|
};
|
|
4602
|
-
const
|
|
4026
|
+
const rootElement = renderComponents(element);
|
|
4027
|
+
const elementYogaNode = isText(rootElement) ? createTextYogaNode() : createYogaNode();
|
|
4603
4028
|
const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
|
|
4604
4029
|
const elementNode = await buildNode(
|
|
4605
|
-
|
|
4030
|
+
rootElement,
|
|
4606
4031
|
rootStyle,
|
|
4607
4032
|
elementYogaNode,
|
|
4608
4033
|
containerWidth,
|
|
4609
4034
|
containerHeight,
|
|
4610
|
-
ctx,
|
|
4611
4035
|
emojiEnabled,
|
|
4612
4036
|
fontFamilies,
|
|
4613
4037
|
renderContext
|
|
@@ -4618,6 +4042,11 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
4618
4042
|
rootYogaNode.setFlexDirection(FlexDirection.Row);
|
|
4619
4043
|
rootYogaNode.insertChild(elementYogaNode, 0);
|
|
4620
4044
|
rootYogaNode.calculateLayout(containerWidth, containerHeight);
|
|
4045
|
+
settleText(
|
|
4046
|
+
elementNode,
|
|
4047
|
+
() => rootYogaNode.calculateLayout(containerWidth, containerHeight),
|
|
4048
|
+
renderContext.debug
|
|
4049
|
+
);
|
|
4621
4050
|
const elementLayout = extractLayout(elementNode, elementYogaNode);
|
|
4622
4051
|
freeYogaNode(rootYogaNode);
|
|
4623
4052
|
const tree = {
|
|
@@ -4632,7 +4061,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
4632
4061
|
};
|
|
4633
4062
|
return { tree, imageCache: renderContext.imageCache };
|
|
4634
4063
|
}
|
|
4635
|
-
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight,
|
|
4064
|
+
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, emojiEnabled, fontFamilies, context) {
|
|
4636
4065
|
if (element === null || element === void 0 || typeof element === "boolean") {
|
|
4637
4066
|
return {
|
|
4638
4067
|
type: "empty",
|
|
@@ -4645,35 +4074,20 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4645
4074
|
if (typeof element === "string" || typeof element === "number") {
|
|
4646
4075
|
const text = String(element);
|
|
4647
4076
|
const style2 = resolveStyle(void 0, parentStyle);
|
|
4648
|
-
const
|
|
4649
|
-
yogaNode
|
|
4077
|
+
const textMeasure = new TextMeasure(text, style2, emojiEnabled);
|
|
4078
|
+
setTextMeasure(yogaNode, textMeasure);
|
|
4650
4079
|
return {
|
|
4651
4080
|
type: "text",
|
|
4652
4081
|
style: style2,
|
|
4653
4082
|
children: [],
|
|
4654
4083
|
textContent: text,
|
|
4084
|
+
textMeasure,
|
|
4655
4085
|
props: {},
|
|
4656
4086
|
yogaNode
|
|
4657
4087
|
};
|
|
4658
4088
|
}
|
|
4659
4089
|
const el = element;
|
|
4660
4090
|
const type = el.type;
|
|
4661
|
-
if (typeof type === "function") {
|
|
4662
|
-
const rendered = type(
|
|
4663
|
-
el.props ?? {}
|
|
4664
|
-
);
|
|
4665
|
-
return await buildNode(
|
|
4666
|
-
rendered,
|
|
4667
|
-
parentStyle,
|
|
4668
|
-
yogaNode,
|
|
4669
|
-
viewportWidth,
|
|
4670
|
-
viewportHeight,
|
|
4671
|
-
ctx,
|
|
4672
|
-
emojiEnabled,
|
|
4673
|
-
fontFamilies,
|
|
4674
|
-
context
|
|
4675
|
-
);
|
|
4676
|
-
}
|
|
4677
4091
|
const props = el.props ?? {};
|
|
4678
4092
|
const rawStyle = props.style ?? {};
|
|
4679
4093
|
const expanded = expandStyle(rawStyle, fontFamilies);
|
|
@@ -4767,17 +4181,14 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4767
4181
|
yogaNode
|
|
4768
4182
|
};
|
|
4769
4183
|
}
|
|
4770
|
-
const
|
|
4771
|
-
|
|
4184
|
+
const items = flattenChildren(props.children);
|
|
4185
|
+
const onlyText = items.length === 1 && isText(items[0]) ? items[0] : null;
|
|
4186
|
+
if (onlyText !== null && !isWhiteSpaceOnly(String(onlyText))) {
|
|
4187
|
+
const textContent = String(onlyText);
|
|
4772
4188
|
const childStyle = resolveStyle(void 0, style);
|
|
4773
|
-
const childYogaNode =
|
|
4774
|
-
const
|
|
4775
|
-
|
|
4776
|
-
childStyle,
|
|
4777
|
-
ctx,
|
|
4778
|
-
emojiEnabled
|
|
4779
|
-
);
|
|
4780
|
-
childYogaNode.setMeasureFunc(measureFunc);
|
|
4189
|
+
const childYogaNode = createTextYogaNode();
|
|
4190
|
+
const textMeasure = new TextMeasure(textContent, childStyle, emojiEnabled);
|
|
4191
|
+
setTextMeasure(childYogaNode, textMeasure);
|
|
4781
4192
|
const jc = style.justifyContent;
|
|
4782
4193
|
if (!jc || jc === "flex-start") {
|
|
4783
4194
|
childYogaNode.setFlexGrow(1);
|
|
@@ -4796,6 +4207,7 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4796
4207
|
style: childStyle,
|
|
4797
4208
|
children: [],
|
|
4798
4209
|
textContent,
|
|
4210
|
+
textMeasure,
|
|
4799
4211
|
props: {},
|
|
4800
4212
|
yogaNode: childYogaNode
|
|
4801
4213
|
}
|
|
@@ -4805,36 +4217,22 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4805
4217
|
};
|
|
4806
4218
|
}
|
|
4807
4219
|
const children = [];
|
|
4808
|
-
const
|
|
4809
|
-
|
|
4810
|
-
const
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
children.push(
|
|
4825
|
-
await buildNode(
|
|
4826
|
-
processedChild,
|
|
4827
|
-
style,
|
|
4828
|
-
childYogaNode,
|
|
4829
|
-
viewportWidth,
|
|
4830
|
-
viewportHeight,
|
|
4831
|
-
ctx,
|
|
4832
|
-
emojiEnabled,
|
|
4833
|
-
fontFamilies,
|
|
4834
|
-
context
|
|
4835
|
-
)
|
|
4836
|
-
);
|
|
4837
|
-
}
|
|
4220
|
+
for (const rendered of items) {
|
|
4221
|
+
if (typeof rendered === "string" && isWhiteSpaceOnly(rendered)) continue;
|
|
4222
|
+
const childYogaNode = isText(rendered) ? createTextYogaNode() : createYogaNode();
|
|
4223
|
+
yogaNode.insertChild(childYogaNode, children.length);
|
|
4224
|
+
children.push(
|
|
4225
|
+
await buildNode(
|
|
4226
|
+
rendered,
|
|
4227
|
+
style,
|
|
4228
|
+
childYogaNode,
|
|
4229
|
+
viewportWidth,
|
|
4230
|
+
viewportHeight,
|
|
4231
|
+
emojiEnabled,
|
|
4232
|
+
fontFamilies,
|
|
4233
|
+
context
|
|
4234
|
+
)
|
|
4235
|
+
);
|
|
4838
4236
|
}
|
|
4839
4237
|
return {
|
|
4840
4238
|
type: tagName,
|
|
@@ -4844,8 +4242,84 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4844
4242
|
yogaNode
|
|
4845
4243
|
};
|
|
4846
4244
|
}
|
|
4245
|
+
var MAX_TEXT_RELAYOUTS = 2;
|
|
4246
|
+
function settleText(root, relayout, debug) {
|
|
4247
|
+
const textNodes = [];
|
|
4248
|
+
const collect = (node) => {
|
|
4249
|
+
if (node.style.display === "none") return;
|
|
4250
|
+
if (node.textMeasure) textNodes.push(node);
|
|
4251
|
+
node.children.forEach(collect);
|
|
4252
|
+
};
|
|
4253
|
+
collect(root);
|
|
4254
|
+
const settle = (node, pin) => node.textMeasure.settle(node.yogaNode.getComputedWidth(), pin);
|
|
4255
|
+
for (let pass = 0; pass < MAX_TEXT_RELAYOUTS; pass++) {
|
|
4256
|
+
let unsettled2 = false;
|
|
4257
|
+
for (const node of textNodes) {
|
|
4258
|
+
if (settle(node, true)) {
|
|
4259
|
+
node.yogaNode.markDirty();
|
|
4260
|
+
unsettled2 = true;
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
if (!unsettled2) return;
|
|
4264
|
+
relayout();
|
|
4265
|
+
}
|
|
4266
|
+
if (!textNodes.some((node) => settle(node, false))) return;
|
|
4267
|
+
for (const node of textNodes) {
|
|
4268
|
+
if (node.textMeasure.unpin()) node.yogaNode.markDirty();
|
|
4269
|
+
}
|
|
4270
|
+
relayout();
|
|
4271
|
+
const unsettled = textNodes.filter((node) => settle(node, false)).length;
|
|
4272
|
+
if (debug && unsettled > 0) {
|
|
4273
|
+
console.warn(
|
|
4274
|
+
`[@effing/canvas] ${unsettled} text node(s) sized for other lines than drawn: the layout didn't settle in ${MAX_TEXT_RELAYOUTS} relayouts`
|
|
4275
|
+
);
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
function setTextMeasure(yogaNode, textMeasure) {
|
|
4279
|
+
yogaNode.setMeasureFunc(
|
|
4280
|
+
(width, widthMode) => textMeasure.measure(
|
|
4281
|
+
widthMode === MeasureMode.Undefined || Number.isNaN(width) ? Infinity : width
|
|
4282
|
+
)
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
var isText = (node) => typeof node === "string" || typeof node === "number";
|
|
4286
|
+
function renderComponents(node) {
|
|
4287
|
+
while (node !== null && typeof node === "object" && "type" in node && typeof node.type === "function") {
|
|
4288
|
+
const el = node;
|
|
4289
|
+
node = el.type(
|
|
4290
|
+
el.props ?? {}
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
return node;
|
|
4294
|
+
}
|
|
4295
|
+
function flattenChildren(node, items = []) {
|
|
4296
|
+
const rendered = renderComponents(node);
|
|
4297
|
+
if (rendered === null || rendered === void 0 || typeof rendered === "boolean") {
|
|
4298
|
+
return items;
|
|
4299
|
+
}
|
|
4300
|
+
if (Array.isArray(rendered)) {
|
|
4301
|
+
for (const child of rendered) flattenChildren(child, items);
|
|
4302
|
+
return items;
|
|
4303
|
+
}
|
|
4304
|
+
const el = rendered;
|
|
4305
|
+
if (typeof rendered === "object" && el.type === REACT_FRAGMENT) {
|
|
4306
|
+
return flattenChildren(el.props?.children, items);
|
|
4307
|
+
}
|
|
4308
|
+
const last = items[items.length - 1];
|
|
4309
|
+
if (isText(rendered) && items.length > 0 && isText(last)) {
|
|
4310
|
+
items[items.length - 1] = `${last}${rendered}`;
|
|
4311
|
+
} else {
|
|
4312
|
+
items.push(rendered);
|
|
4313
|
+
}
|
|
4314
|
+
return items;
|
|
4315
|
+
}
|
|
4316
|
+
function floorToPixel(value) {
|
|
4317
|
+
const rounded = Math.round(value);
|
|
4318
|
+
return Math.abs(value - rounded) < 1e-4 ? rounded : Math.floor(value);
|
|
4319
|
+
}
|
|
4847
4320
|
function extractLayout(node, yogaNode) {
|
|
4848
4321
|
const layout = yogaNode.getComputedLayout();
|
|
4322
|
+
const place = node.textMeasure ? floorToPixel : (v) => v;
|
|
4849
4323
|
return {
|
|
4850
4324
|
type: node.type,
|
|
4851
4325
|
style: node.style,
|
|
@@ -4854,37 +4328,14 @@ function extractLayout(node, yogaNode) {
|
|
|
4854
4328
|
return extractLayout(child, childYoga);
|
|
4855
4329
|
}),
|
|
4856
4330
|
textContent: node.textContent,
|
|
4331
|
+
textLayout: node.textMeasure?.layout,
|
|
4857
4332
|
props: node.props,
|
|
4858
|
-
x: layout.left,
|
|
4859
|
-
y: layout.top,
|
|
4333
|
+
x: place(layout.left),
|
|
4334
|
+
y: place(layout.top),
|
|
4860
4335
|
width: layout.width,
|
|
4861
4336
|
height: layout.height
|
|
4862
4337
|
};
|
|
4863
4338
|
}
|
|
4864
|
-
function isBrElement(child) {
|
|
4865
|
-
if (child === null || child === void 0 || typeof child !== "object")
|
|
4866
|
-
return false;
|
|
4867
|
-
return child.type === "br";
|
|
4868
|
-
}
|
|
4869
|
-
function extractTextContent(children) {
|
|
4870
|
-
if (children === void 0 || children === null) return void 0;
|
|
4871
|
-
if (typeof children === "string") return children;
|
|
4872
|
-
if (typeof children === "number") return String(children);
|
|
4873
|
-
if (Array.isArray(children)) {
|
|
4874
|
-
const parts = [];
|
|
4875
|
-
for (const child of children) {
|
|
4876
|
-
if (typeof child === "string") {
|
|
4877
|
-
parts.push(child);
|
|
4878
|
-
} else if (typeof child === "number") {
|
|
4879
|
-
parts.push(String(child));
|
|
4880
|
-
} else if (child !== null && child !== void 0 && typeof child !== "boolean") {
|
|
4881
|
-
return void 0;
|
|
4882
|
-
}
|
|
4883
|
-
}
|
|
4884
|
-
return parts.join("");
|
|
4885
|
-
}
|
|
4886
|
-
return void 0;
|
|
4887
|
-
}
|
|
4888
4339
|
var REACT_FRAGMENT = /* @__PURE__ */ Symbol.for("react.fragment");
|
|
4889
4340
|
function resolveSvgTree(node) {
|
|
4890
4341
|
if (node === null || node === void 0 || typeof node === "boolean")
|
|
@@ -4928,18 +4379,6 @@ function resolveSvgTree(node) {
|
|
|
4928
4379
|
}
|
|
4929
4380
|
return el;
|
|
4930
4381
|
}
|
|
4931
|
-
function hasElementChildren(children) {
|
|
4932
|
-
if (children === void 0 || children === null || typeof children === "boolean")
|
|
4933
|
-
return false;
|
|
4934
|
-
if (typeof children === "string" || typeof children === "number")
|
|
4935
|
-
return false;
|
|
4936
|
-
if (Array.isArray(children)) {
|
|
4937
|
-
return children.some(
|
|
4938
|
-
(child) => child !== null && child !== void 0 && typeof child !== "boolean" && typeof child !== "string" && typeof child !== "number"
|
|
4939
|
-
);
|
|
4940
|
-
}
|
|
4941
|
-
return typeof children === "object";
|
|
4942
|
-
}
|
|
4943
4382
|
|
|
4944
4383
|
// src/jsx/index.ts
|
|
4945
4384
|
async function renderReactElement(ctx, element, options = {}) {
|
|
@@ -4958,12 +4397,12 @@ async function renderReactElement(ctx, element, options = {}) {
|
|
|
4958
4397
|
element,
|
|
4959
4398
|
width,
|
|
4960
4399
|
height,
|
|
4961
|
-
ctx,
|
|
4962
4400
|
!!emojiStyle,
|
|
4963
4401
|
fontFamilies,
|
|
4964
4402
|
renderContext
|
|
4965
4403
|
);
|
|
4966
4404
|
await drawNode(ctx, layoutTree, 0, 0, renderContext, emojiStyle);
|
|
4405
|
+
await releaseParagraphs();
|
|
4967
4406
|
}
|
|
4968
4407
|
|
|
4969
4408
|
// src/fit-text.ts
|
|
@@ -5005,10 +4444,7 @@ function findLargestUsableFontSize(options) {
|
|
|
5005
4444
|
|
|
5006
4445
|
// src/index.ts
|
|
5007
4446
|
function createCanvas3(width, height) {
|
|
5008
|
-
|
|
5009
|
-
const origEncode = canvas.encode.bind(canvas);
|
|
5010
|
-
canvas.encode = (async (...args) => Buffer.from(await origEncode(...args)));
|
|
5011
|
-
return canvas;
|
|
4447
|
+
return _createCanvas(width, height);
|
|
5012
4448
|
}
|
|
5013
4449
|
export {
|
|
5014
4450
|
Canvas,
|