@effing/canvas 0.41.1 → 0.42.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 +19 -13
- package/dist/index.d.ts +7 -7
- package/dist/index.js +346 -375
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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, {
|
|
@@ -321,7 +321,7 @@ function resolveLengthUnits(transform, viewportWidth, viewportHeight, fontSize,
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
// src/jsx/font.ts
|
|
324
|
-
import { GlobalFonts } from "@
|
|
324
|
+
import { GlobalFonts } from "@effing/skia";
|
|
325
325
|
|
|
326
326
|
// src/jsx/font-metrics.ts
|
|
327
327
|
import { inflateSync } from "zlib";
|
|
@@ -493,7 +493,7 @@ function findBreakOpportunities(text) {
|
|
|
493
493
|
}
|
|
494
494
|
|
|
495
495
|
// src/jsx/text/measure.ts
|
|
496
|
-
import { createCanvas } from "@
|
|
496
|
+
import { createCanvas } from "@effing/skia";
|
|
497
497
|
var scratchCtx = null;
|
|
498
498
|
function getScratchCtx() {
|
|
499
499
|
if (!scratchCtx) {
|
|
@@ -501,6 +501,16 @@ function getScratchCtx() {
|
|
|
501
501
|
}
|
|
502
502
|
return scratchCtx;
|
|
503
503
|
}
|
|
504
|
+
function withUnsnappedText(ctx, fn) {
|
|
505
|
+
const previous = ctx.textRendering;
|
|
506
|
+
if (previous === "geometricPrecision") return fn();
|
|
507
|
+
ctx.textRendering = "geometricPrecision";
|
|
508
|
+
try {
|
|
509
|
+
return fn();
|
|
510
|
+
} finally {
|
|
511
|
+
ctx.textRendering = previous;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
504
514
|
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
505
515
|
"serif",
|
|
506
516
|
"sans-serif",
|
|
@@ -520,14 +530,16 @@ function quoteFontFamily(family) {
|
|
|
520
530
|
if (!family || GENERIC_FAMILIES.has(family)) return family;
|
|
521
531
|
return `"${family}"`;
|
|
522
532
|
}
|
|
533
|
+
function quoteFontFamilies(fontFamily) {
|
|
534
|
+
return fontFamily.split(",").map((f) => quoteFontFamily(f.trim())).join(", ");
|
|
535
|
+
}
|
|
523
536
|
function setFont(ctx, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal") {
|
|
524
|
-
|
|
525
|
-
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${quoted}`;
|
|
537
|
+
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${quoteFontFamilies(fontFamily)}`;
|
|
526
538
|
}
|
|
527
539
|
function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx) {
|
|
528
540
|
const c = ctx ?? getScratchCtx();
|
|
529
541
|
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
530
|
-
const m = c.measureText(text);
|
|
542
|
+
const m = withUnsnappedText(c, () => c.measureText(text));
|
|
531
543
|
const ascent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent ?? fontSize * 0.8;
|
|
532
544
|
const descent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent ?? fontSize * 0.2;
|
|
533
545
|
return {
|
|
@@ -540,7 +552,8 @@ function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "
|
|
|
540
552
|
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx, fontMetrics) {
|
|
541
553
|
const c = ctx ?? getScratchCtx();
|
|
542
554
|
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
543
|
-
const
|
|
555
|
+
const measure = (text) => withUnsnappedText(c, () => c.measureText(text));
|
|
556
|
+
const refMetrics = measure("M");
|
|
544
557
|
let fontAscent;
|
|
545
558
|
let fontDescent;
|
|
546
559
|
if (fontMetrics) {
|
|
@@ -557,12 +570,12 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
|
|
|
557
570
|
let targetAscent;
|
|
558
571
|
switch (overEdge) {
|
|
559
572
|
case "cap": {
|
|
560
|
-
const capMetrics =
|
|
573
|
+
const capMetrics = measure("H");
|
|
561
574
|
targetAscent = capMetrics.actualBoundingBoxAscent ?? fontSize * 0.7;
|
|
562
575
|
break;
|
|
563
576
|
}
|
|
564
577
|
case "ex": {
|
|
565
|
-
const exMetrics =
|
|
578
|
+
const exMetrics = measure("x");
|
|
566
579
|
targetAscent = exMetrics.actualBoundingBoxAscent ?? fontSize * 0.5;
|
|
567
580
|
break;
|
|
568
581
|
}
|
|
@@ -606,6 +619,141 @@ function measureWord(word, fontSize, fontFamily, fontWeight = 400, fontStyle = "
|
|
|
606
619
|
return base + letterSpacing * word.length;
|
|
607
620
|
}
|
|
608
621
|
|
|
622
|
+
// src/jsx/text/native.ts
|
|
623
|
+
import { Paragraph } from "@effing/skia/extensions";
|
|
624
|
+
function canLayoutNatively(text, style, emojiEnabled) {
|
|
625
|
+
if (text === "") return false;
|
|
626
|
+
if (style.wordBreak === "break-all") return false;
|
|
627
|
+
if (style.whiteSpace === "pre" && /[ \t](?:\n|$)/.test(text)) return false;
|
|
628
|
+
if (emojiEnabled) {
|
|
629
|
+
for (const char of text) {
|
|
630
|
+
if (isEmoji(char)) return false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
function toWeight(weight) {
|
|
636
|
+
if (typeof weight === "number") return weight;
|
|
637
|
+
if (weight === "bold") return 700;
|
|
638
|
+
const parsed = parseInt(String(weight ?? "400"), 10);
|
|
639
|
+
return isNaN(parsed) ? 400 : parsed;
|
|
640
|
+
}
|
|
641
|
+
function toFontStyle(fontStyle) {
|
|
642
|
+
if (fontStyle?.startsWith("italic")) return "italic";
|
|
643
|
+
if (fontStyle?.startsWith("oblique")) return "oblique";
|
|
644
|
+
return "normal";
|
|
645
|
+
}
|
|
646
|
+
var TEXT_ALIGNS = /* @__PURE__ */ new Set([
|
|
647
|
+
"left",
|
|
648
|
+
"right",
|
|
649
|
+
"center",
|
|
650
|
+
"justify",
|
|
651
|
+
"start",
|
|
652
|
+
"end"
|
|
653
|
+
]);
|
|
654
|
+
function toTextAlign(textAlign) {
|
|
655
|
+
return textAlign !== void 0 && TEXT_ALIGNS.has(textAlign) ? textAlign : "left";
|
|
656
|
+
}
|
|
657
|
+
var MIN_WIDTH = 0.01;
|
|
658
|
+
var MAX_PENDING_PARAGRAPHS = 2e3;
|
|
659
|
+
var pendingParagraphs = 0;
|
|
660
|
+
var turnWatched = false;
|
|
661
|
+
function countParagraph() {
|
|
662
|
+
pendingParagraphs++;
|
|
663
|
+
if (turnWatched) return;
|
|
664
|
+
turnWatched = true;
|
|
665
|
+
setImmediate(() => {
|
|
666
|
+
pendingParagraphs = 0;
|
|
667
|
+
turnWatched = false;
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
async function releaseParagraphs() {
|
|
671
|
+
if (pendingParagraphs < MAX_PENDING_PARAGRAPHS) return;
|
|
672
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
673
|
+
}
|
|
674
|
+
function layoutTextNative(text, style, maxWidth, lineHeight) {
|
|
675
|
+
const fontSize = style.fontSize ?? 16;
|
|
676
|
+
const fontFamily = style.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
677
|
+
const fontWeight = style.fontWeight ?? 400;
|
|
678
|
+
const fontStyle = style.fontStyle ?? "normal";
|
|
679
|
+
const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
|
|
680
|
+
const whiteSpace = style.whiteSpace ?? "normal";
|
|
681
|
+
const noWrap = whiteSpace === "nowrap" || whiteSpace === "pre";
|
|
682
|
+
const lineClamp = style.lineClamp && style.lineClamp > 0 ? style.lineClamp : void 0;
|
|
683
|
+
const ellipsis = lineClamp !== void 0 || noWrap && style.textOverflow === "ellipsis" ? "\u2026" : void 0;
|
|
684
|
+
const width = maxWidth > 0 ? maxWidth : MIN_WIDTH;
|
|
685
|
+
const paragraph = new Paragraph(text, {
|
|
686
|
+
fontFamily: quoteFontFamilies(fontFamily),
|
|
687
|
+
fontSize,
|
|
688
|
+
fontWeight: toWeight(fontWeight),
|
|
689
|
+
fontStyle: toFontStyle(fontStyle),
|
|
690
|
+
letterSpacing,
|
|
691
|
+
lineHeight: lineHeight ?? 0,
|
|
692
|
+
textAlign: toTextAlign(style.textAlign),
|
|
693
|
+
noWrap,
|
|
694
|
+
maxLines: lineClamp,
|
|
695
|
+
ellipsis
|
|
696
|
+
});
|
|
697
|
+
countParagraph();
|
|
698
|
+
const layout = paragraph.layout(width);
|
|
699
|
+
if (!noWrap && layout.minIntrinsicWidth > width) return null;
|
|
700
|
+
const segments = layout.lines.map((line, i) => ({
|
|
701
|
+
// A line that ends the text at a newline reports the newline as its text.
|
|
702
|
+
text: text.slice(line.startIndex, line.endIndex).replace(/\n/g, ""),
|
|
703
|
+
x: line.left,
|
|
704
|
+
y: line.baseline,
|
|
705
|
+
width: line.width,
|
|
706
|
+
height: layout.lineHeight,
|
|
707
|
+
fontSize,
|
|
708
|
+
fontFamily,
|
|
709
|
+
fontWeight,
|
|
710
|
+
fontStyle,
|
|
711
|
+
color: style.color ?? "black",
|
|
712
|
+
ascent: layout.ascent,
|
|
713
|
+
textDecoration: style.textDecoration,
|
|
714
|
+
letterSpacing,
|
|
715
|
+
lineIndex: i
|
|
716
|
+
}));
|
|
717
|
+
let height = layout.height;
|
|
718
|
+
if (lineHeight === void 0 && segments.length > 0) {
|
|
719
|
+
height = Math.ceil(height);
|
|
720
|
+
}
|
|
721
|
+
let paragraphOffsetY = 0;
|
|
722
|
+
const textBoxTrim = style.textBoxTrim;
|
|
723
|
+
if (textBoxTrim && textBoxTrim !== "none" && segments.length > 0) {
|
|
724
|
+
const fontMetrics = {
|
|
725
|
+
unitsPerEm: fontSize,
|
|
726
|
+
ascender: layout.ascent,
|
|
727
|
+
descender: -layout.descent
|
|
728
|
+
};
|
|
729
|
+
const trim = measureTrimMetrics(
|
|
730
|
+
fontSize,
|
|
731
|
+
fontFamily,
|
|
732
|
+
fontWeight,
|
|
733
|
+
fontStyle,
|
|
734
|
+
layout.lineHeight,
|
|
735
|
+
style.textBoxEdge ?? "text",
|
|
736
|
+
void 0,
|
|
737
|
+
fontMetrics
|
|
738
|
+
);
|
|
739
|
+
if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
|
|
740
|
+
for (const seg of segments) seg.y -= trim.overTrim;
|
|
741
|
+
height -= trim.overTrim;
|
|
742
|
+
paragraphOffsetY = -trim.overTrim;
|
|
743
|
+
}
|
|
744
|
+
if (textBoxTrim === "trim-end" || textBoxTrim === "trim-both") {
|
|
745
|
+
height -= trim.underTrim;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
segments,
|
|
750
|
+
width: segments.reduce((w, s) => Math.max(w, s.width), 0),
|
|
751
|
+
height,
|
|
752
|
+
paragraph,
|
|
753
|
+
paragraphOffsetY
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
609
757
|
// src/jsx/text/index.ts
|
|
610
758
|
function emojiAwareMeasureWord(word, fontSize, fontFamily, fontWeight, fontStyle, ctx, letterSpacing = 0) {
|
|
611
759
|
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
@@ -651,6 +799,29 @@ function emojiAwareMeasureWord(word, fontSize, fontFamily, fontWeight, fontStyle
|
|
|
651
799
|
return totalWidth;
|
|
652
800
|
}
|
|
653
801
|
function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
802
|
+
const fontSize = style.fontSize ?? 16;
|
|
803
|
+
const isAutoLineHeight = style.lineHeight === void 0 || style.lineHeight === "normal";
|
|
804
|
+
const lineHeight = isAutoLineHeight ? void 0 : resolveLineHeight(style.lineHeight, fontSize);
|
|
805
|
+
const hasLineBox = lineHeight === void 0 || lineHeight > 0;
|
|
806
|
+
if (hasLineBox && canLayoutNatively(text, style, emojiEnabled)) {
|
|
807
|
+
const result = layoutTextNative(
|
|
808
|
+
applyTextTransform(text, style),
|
|
809
|
+
style,
|
|
810
|
+
maxWidth,
|
|
811
|
+
lineHeight
|
|
812
|
+
);
|
|
813
|
+
if (result) {
|
|
814
|
+
const textStrokeWidth = resolveTextStrokeWidth(style, fontSize);
|
|
815
|
+
for (const seg of result.segments) {
|
|
816
|
+
seg.textStrokeWidth = textStrokeWidth;
|
|
817
|
+
seg.textStrokeColor = style.WebkitTextStrokeColor;
|
|
818
|
+
}
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return layoutTextFallback(text, style, maxWidth, ctx, emojiEnabled);
|
|
823
|
+
}
|
|
824
|
+
function layoutTextFallback(text, style, maxWidth, ctx, emojiEnabled) {
|
|
654
825
|
const fontSize = style.fontSize ?? 16;
|
|
655
826
|
const fontFamily = style.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
656
827
|
const fontWeight = style.fontWeight ?? 400;
|
|
@@ -677,16 +848,7 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
677
848
|
const wordBreak = style.wordBreak ?? "normal";
|
|
678
849
|
const textOverflow = style.textOverflow ?? "clip";
|
|
679
850
|
const textDecoration = style.textDecoration;
|
|
680
|
-
|
|
681
|
-
const rawStrokeWidth = style.WebkitTextStrokeWidth;
|
|
682
|
-
if (rawStrokeWidth !== void 0) {
|
|
683
|
-
if (typeof rawStrokeWidth === "number") {
|
|
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
|
-
}
|
|
851
|
+
const textStrokeWidth = resolveTextStrokeWidth(style, fontSize);
|
|
690
852
|
const textStrokeColor = style.WebkitTextStrokeColor;
|
|
691
853
|
const measure = emojiEnabled ? (word, ls) => emojiAwareMeasureWord(
|
|
692
854
|
word,
|
|
@@ -705,14 +867,7 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
705
867
|
ctx,
|
|
706
868
|
ls ?? letterSpacing
|
|
707
869
|
);
|
|
708
|
-
|
|
709
|
-
if (style.textTransform === "uppercase") {
|
|
710
|
-
processedText = text.toUpperCase();
|
|
711
|
-
} else if (style.textTransform === "lowercase") {
|
|
712
|
-
processedText = text.toLowerCase();
|
|
713
|
-
} else if (style.textTransform === "capitalize") {
|
|
714
|
-
processedText = text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
715
|
-
}
|
|
870
|
+
const processedText = applyTextTransform(text, style);
|
|
716
871
|
const noWrap = whiteSpace === "nowrap" || whiteSpace === "pre";
|
|
717
872
|
const paragraphs = processedText.split("\n");
|
|
718
873
|
const lines = [];
|
|
@@ -775,11 +930,12 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
775
930
|
for (let i = 0; i < lines.length; i++) {
|
|
776
931
|
const line = lines[i];
|
|
777
932
|
const lineWidth = measure(line);
|
|
933
|
+
const slack = Math.max(0, maxWidth - lineWidth);
|
|
778
934
|
let x = 0;
|
|
779
935
|
if (textAlign === "center") {
|
|
780
|
-
x =
|
|
936
|
+
x = slack / 2;
|
|
781
937
|
} else if (textAlign === "right") {
|
|
782
|
-
x =
|
|
938
|
+
x = slack;
|
|
783
939
|
}
|
|
784
940
|
const metrics = measureText(
|
|
785
941
|
line || "M",
|
|
@@ -859,6 +1015,21 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
859
1015
|
height: totalHeight
|
|
860
1016
|
};
|
|
861
1017
|
}
|
|
1018
|
+
function resolveTextStrokeWidth(style, fontSize) {
|
|
1019
|
+
const raw = style.WebkitTextStrokeWidth;
|
|
1020
|
+
if (raw === void 0) return void 0;
|
|
1021
|
+
if (typeof raw === "number") return raw;
|
|
1022
|
+
const resolved = resolveUnit(String(raw), 0, 0, fontSize, 16);
|
|
1023
|
+
return typeof resolved === "number" ? resolved : void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function applyTextTransform(text, style) {
|
|
1026
|
+
if (style.textTransform === "uppercase") return text.toUpperCase();
|
|
1027
|
+
if (style.textTransform === "lowercase") return text.toLowerCase();
|
|
1028
|
+
if (style.textTransform === "capitalize") {
|
|
1029
|
+
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1030
|
+
}
|
|
1031
|
+
return text;
|
|
1032
|
+
}
|
|
862
1033
|
function resolveLineHeight(lineHeight, fontSize, canvasMetrics, fontMetrics) {
|
|
863
1034
|
if (lineHeight === void 0 || lineHeight === "normal") {
|
|
864
1035
|
if (fontMetrics) {
|
|
@@ -889,7 +1060,7 @@ function wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, fontStyle, l
|
|
|
889
1060
|
let lineStart = 0;
|
|
890
1061
|
let lastBreak = 0;
|
|
891
1062
|
for (const opp of breakOpps) {
|
|
892
|
-
const segment = text.slice(lineStart, opp.position);
|
|
1063
|
+
const segment = text.slice(lineStart, opp.position).replace(/\s+$/, "");
|
|
893
1064
|
const segWidth = mw(segment);
|
|
894
1065
|
if (segWidth > maxWidth && lastBreak > lineStart) {
|
|
895
1066
|
const line = text.slice(lineStart, lastBreak).replace(/\s+$/, "");
|
|
@@ -1056,198 +1227,8 @@ function hasRadius(r) {
|
|
|
1056
1227
|
return r.topLeft > 0 || r.topRight > 0 || r.bottomRight > 0 || r.bottomLeft > 0;
|
|
1057
1228
|
}
|
|
1058
1229
|
|
|
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
1230
|
// src/jsx/draw/clip-path.ts
|
|
1250
|
-
import { Path2D } from "@
|
|
1231
|
+
import { Path2D } from "@effing/skia";
|
|
1251
1232
|
|
|
1252
1233
|
// src/jsx/draw/rect.ts
|
|
1253
1234
|
function drawRect(ctx, x, y, width, height, style) {
|
|
@@ -1453,14 +1434,6 @@ function drawBorders(ctx, x, y, width, height, style, borderRadius) {
|
|
|
1453
1434
|
}
|
|
1454
1435
|
}
|
|
1455
1436
|
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
1437
|
function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
|
|
1465
1438
|
const parts = boxShadow.match(BOX_SHADOW_RE);
|
|
1466
1439
|
if (!parts) return;
|
|
@@ -2308,6 +2281,32 @@ function splitGradientArgs(content) {
|
|
|
2308
2281
|
return parts;
|
|
2309
2282
|
}
|
|
2310
2283
|
|
|
2284
|
+
// src/jsx/draw/group.ts
|
|
2285
|
+
import { beginGroup, endGroup } from "@effing/skia/extensions";
|
|
2286
|
+
function isNoFilter(filter) {
|
|
2287
|
+
if (!filter) return true;
|
|
2288
|
+
const normalized = filter.trim();
|
|
2289
|
+
return normalized === "" || normalized === "none";
|
|
2290
|
+
}
|
|
2291
|
+
function beginElementGroup(ctx, opacity, filter) {
|
|
2292
|
+
const hasFilter = !isNoFilter(filter);
|
|
2293
|
+
if (opacity >= 1 && !hasFilter) return false;
|
|
2294
|
+
beginGroup(ctx, { opacity, filter: hasFilter ? filter.trim() : void 0 });
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
function endElementGroup(ctx) {
|
|
2298
|
+
endGroup(ctx);
|
|
2299
|
+
}
|
|
2300
|
+
function drawBackdropFilter(ctx, filter, x, y, width, height, borderRadius, opacity) {
|
|
2301
|
+
if (width <= 0 || height <= 0) return;
|
|
2302
|
+
if (isNoFilter(filter)) return;
|
|
2303
|
+
ctx.save();
|
|
2304
|
+
applyClip(ctx, x, y, width, height, borderRadius);
|
|
2305
|
+
beginGroup(ctx, { backdropFilter: filter.trim(), opacity });
|
|
2306
|
+
endGroup(ctx);
|
|
2307
|
+
ctx.restore();
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2311
2310
|
// src/jsx/draw/object-fit.ts
|
|
2312
2311
|
function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
|
|
2313
2312
|
const imgRatio = imgW / imgH;
|
|
@@ -2529,7 +2528,7 @@ function applySvgTransform(ctx, transform) {
|
|
|
2529
2528
|
}
|
|
2530
2529
|
|
|
2531
2530
|
// src/jsx/draw/svg/path.ts
|
|
2532
|
-
import { Path2D as Path2D2 } from "@
|
|
2531
|
+
import { Path2D as Path2D2 } from "@effing/skia";
|
|
2533
2532
|
function pathBBox(d) {
|
|
2534
2533
|
let minX = Infinity;
|
|
2535
2534
|
let minY = Infinity;
|
|
@@ -2769,6 +2768,36 @@ function parsePoints(value) {
|
|
|
2769
2768
|
return result;
|
|
2770
2769
|
}
|
|
2771
2770
|
|
|
2771
|
+
// src/jsx/draw/offscreen.ts
|
|
2772
|
+
import { createCanvas as createCanvas2 } from "@effing/skia";
|
|
2773
|
+
var canvasPool = /* @__PURE__ */ new Map();
|
|
2774
|
+
function acquireOffscreen(w, h) {
|
|
2775
|
+
const key = `${w}x${h}`;
|
|
2776
|
+
const stack = canvasPool.get(key);
|
|
2777
|
+
if (stack) {
|
|
2778
|
+
while (stack.length > 0) {
|
|
2779
|
+
const ref = stack.pop();
|
|
2780
|
+
const canvas2 = ref.deref();
|
|
2781
|
+
if (canvas2) {
|
|
2782
|
+
const ctx = canvas2.getContext("2d");
|
|
2783
|
+
ctx.reset();
|
|
2784
|
+
return [canvas2, ctx];
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
const canvas = createCanvas2(w, h);
|
|
2789
|
+
return [canvas, canvas.getContext("2d")];
|
|
2790
|
+
}
|
|
2791
|
+
function releaseOffscreen(canvas) {
|
|
2792
|
+
const key = `${canvas.width}x${canvas.height}`;
|
|
2793
|
+
let stack = canvasPool.get(key);
|
|
2794
|
+
if (!stack) {
|
|
2795
|
+
stack = [];
|
|
2796
|
+
canvasPool.set(key, stack);
|
|
2797
|
+
}
|
|
2798
|
+
stack.push(new WeakRef(canvas));
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2772
2801
|
// src/jsx/draw/svg/filters.ts
|
|
2773
2802
|
function applyFeOffset(input, dx, dy, w, h) {
|
|
2774
2803
|
const [canvas, ctx] = acquireOffscreen(w, h);
|
|
@@ -2918,10 +2947,10 @@ function applyFilter(filterDef, sourceGraphic, w, h) {
|
|
|
2918
2947
|
}
|
|
2919
2948
|
|
|
2920
2949
|
// src/jsx/draw/svg/shapes.ts
|
|
2921
|
-
import { Path2D as Path2D4 } from "@
|
|
2950
|
+
import { Path2D as Path2D4 } from "@effing/skia";
|
|
2922
2951
|
|
|
2923
2952
|
// src/jsx/draw/svg/gradients.ts
|
|
2924
|
-
import { Path2D as Path2D3 } from "@
|
|
2953
|
+
import { Path2D as Path2D3 } from "@effing/skia";
|
|
2925
2954
|
import parseCssColor from "parse-css-color";
|
|
2926
2955
|
function parseFrac(value, fallback) {
|
|
2927
2956
|
if (value == null) return fallback;
|
|
@@ -3374,7 +3403,8 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
|
3374
3403
|
}
|
|
3375
3404
|
|
|
3376
3405
|
// src/jsx/draw/text.ts
|
|
3377
|
-
import { loadImage as loadImage2 } from "@
|
|
3406
|
+
import { loadImage as loadImage2 } from "@effing/skia";
|
|
3407
|
+
import { fillParagraph, strokeParagraph } from "@effing/skia/extensions";
|
|
3378
3408
|
import parseCssColor2 from "parse-css-color";
|
|
3379
3409
|
|
|
3380
3410
|
// src/jsx/emoji.ts
|
|
@@ -3486,7 +3516,58 @@ function loadEmojiImage(style, char) {
|
|
|
3486
3516
|
}
|
|
3487
3517
|
return cached;
|
|
3488
3518
|
}
|
|
3489
|
-
async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle) {
|
|
3519
|
+
async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle, native) {
|
|
3520
|
+
if (native) {
|
|
3521
|
+
drawParagraph(
|
|
3522
|
+
ctx,
|
|
3523
|
+
native.paragraph,
|
|
3524
|
+
segments,
|
|
3525
|
+
offsetX,
|
|
3526
|
+
offsetY,
|
|
3527
|
+
native.offsetY,
|
|
3528
|
+
textShadow
|
|
3529
|
+
);
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
const textRendering = ctx.textRendering;
|
|
3533
|
+
ctx.textRendering = "geometricPrecision";
|
|
3534
|
+
try {
|
|
3535
|
+
await drawSegments(ctx, segments, offsetX, offsetY, textShadow, emojiStyle);
|
|
3536
|
+
} finally {
|
|
3537
|
+
ctx.textRendering = textRendering;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
function drawParagraph(ctx, paragraph, segments, offsetX, offsetY, paragraphOffsetY, textShadow) {
|
|
3541
|
+
const first = segments[0];
|
|
3542
|
+
if (!first) return;
|
|
3543
|
+
const x = offsetX;
|
|
3544
|
+
const y = offsetY + paragraphOffsetY;
|
|
3545
|
+
const shadow = textShadow ? parseShadow(textShadow) : null;
|
|
3546
|
+
ctx.fillStyle = first.color;
|
|
3547
|
+
if (shadow) {
|
|
3548
|
+
drawShadowPass(
|
|
3549
|
+
ctx,
|
|
3550
|
+
shadow,
|
|
3551
|
+
getColorAlpha(first.color),
|
|
3552
|
+
() => fillParagraph(ctx, paragraph, x, y)
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
if (first.textStrokeWidth !== void 0 && first.textStrokeWidth > 0) {
|
|
3556
|
+
ctx.save();
|
|
3557
|
+
ctx.lineWidth = first.textStrokeWidth;
|
|
3558
|
+
ctx.strokeStyle = first.textStrokeColor ?? first.color;
|
|
3559
|
+
ctx.lineJoin = "round";
|
|
3560
|
+
strokeParagraph(ctx, paragraph, x, y);
|
|
3561
|
+
ctx.restore();
|
|
3562
|
+
}
|
|
3563
|
+
fillParagraph(ctx, paragraph, x, y);
|
|
3564
|
+
for (const seg of segments) {
|
|
3565
|
+
if (seg.textDecoration) {
|
|
3566
|
+
drawTextDecoration(ctx, seg, offsetX, offsetY);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
async function drawSegments(ctx, segments, offsetX, offsetY, textShadow, emojiStyle) {
|
|
3490
3571
|
const shadow = textShadow ? parseShadow(textShadow) : null;
|
|
3491
3572
|
for (const seg of segments) {
|
|
3492
3573
|
if (!seg.text) continue;
|
|
@@ -3706,139 +3787,30 @@ function drawTextDecoration(ctx, seg, offsetX, offsetY) {
|
|
|
3706
3787
|
async function drawNode(ctx, node, parentX, parentY, context, emojiStyle) {
|
|
3707
3788
|
const x = parentX + node.x;
|
|
3708
3789
|
const y = parentY + node.y;
|
|
3709
|
-
const {
|
|
3790
|
+
const { style } = node;
|
|
3710
3791
|
const renderContext = context ?? {
|
|
3711
3792
|
imageCache: /* @__PURE__ */ new Map(),
|
|
3712
3793
|
debug: false
|
|
3713
3794
|
};
|
|
3714
3795
|
if (style.display === "none") return;
|
|
3715
|
-
const
|
|
3796
|
+
const rawOpacity = Number(style.opacity ?? 1);
|
|
3797
|
+
const opacity = Number.isFinite(rawOpacity) ? rawOpacity : 1;
|
|
3716
3798
|
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
|
-
}
|
|
3799
|
+
const group = { open: false };
|
|
3800
|
+
ctx.save();
|
|
3801
|
+
try {
|
|
3802
|
+
await paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, group);
|
|
3803
|
+
} finally {
|
|
3804
|
+
if (group.open) endElementGroup(ctx);
|
|
3805
|
+
ctx.restore();
|
|
3774
3806
|
}
|
|
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
3807
|
}
|
|
3823
|
-
async function
|
|
3824
|
-
const x = parentX + node.x + offsetX;
|
|
3825
|
-
const y = parentY + node.y + offsetY;
|
|
3808
|
+
async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, group) {
|
|
3826
3809
|
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) {
|
|
3810
|
+
if (style.transform) {
|
|
3839
3811
|
applyTransform(
|
|
3840
3812
|
ctx,
|
|
3841
|
-
|
|
3813
|
+
style.transform,
|
|
3842
3814
|
x,
|
|
3843
3815
|
y,
|
|
3844
3816
|
width,
|
|
@@ -3858,9 +3830,11 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3858
3830
|
y,
|
|
3859
3831
|
width,
|
|
3860
3832
|
height,
|
|
3861
|
-
borderRadius
|
|
3833
|
+
borderRadius,
|
|
3834
|
+
opacity
|
|
3862
3835
|
);
|
|
3863
3836
|
}
|
|
3837
|
+
group.open = beginElementGroup(ctx, opacity, style.filter);
|
|
3864
3838
|
if (style.boxShadow) {
|
|
3865
3839
|
drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
|
|
3866
3840
|
}
|
|
@@ -3902,9 +3876,9 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3902
3876
|
applyClip(ctx, x, y, width, height, borderRadius);
|
|
3903
3877
|
}
|
|
3904
3878
|
const image = await cachedLoadImage(
|
|
3905
|
-
|
|
3879
|
+
renderContext.imageCache,
|
|
3906
3880
|
urlMatch[1],
|
|
3907
|
-
|
|
3881
|
+
renderContext.userAgent
|
|
3908
3882
|
);
|
|
3909
3883
|
const bgSize = style.backgroundSize;
|
|
3910
3884
|
if (bgSize === "cover") {
|
|
@@ -3960,7 +3934,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3960
3934
|
}
|
|
3961
3935
|
}
|
|
3962
3936
|
}
|
|
3963
|
-
if (
|
|
3937
|
+
if (renderContext.debug) {
|
|
3964
3938
|
ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
|
|
3965
3939
|
ctx.lineWidth = 1;
|
|
3966
3940
|
ctx.strokeRect(x, y, width, height);
|
|
@@ -3988,7 +3962,11 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
3988
3962
|
contentX,
|
|
3989
3963
|
contentY,
|
|
3990
3964
|
style.textShadow,
|
|
3991
|
-
emojiStyle
|
|
3965
|
+
emojiStyle,
|
|
3966
|
+
textLayout.paragraph && {
|
|
3967
|
+
paragraph: textLayout.paragraph,
|
|
3968
|
+
offsetY: textLayout.paragraphOffsetY ?? 0
|
|
3969
|
+
}
|
|
3992
3970
|
);
|
|
3993
3971
|
}
|
|
3994
3972
|
if (node.type === "img" && node.props.src) {
|
|
@@ -4016,7 +3994,7 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
4016
3994
|
imgY,
|
|
4017
3995
|
imgW,
|
|
4018
3996
|
imgH,
|
|
4019
|
-
|
|
3997
|
+
renderContext,
|
|
4020
3998
|
style
|
|
4021
3999
|
);
|
|
4022
4000
|
}
|
|
@@ -4024,14 +4002,9 @@ async function drawNodeCore(ctx, node, parentX, parentY, offsetX, offsetY, emoji
|
|
|
4024
4002
|
drawSvgContainer(ctx, node, x, y, width, height);
|
|
4025
4003
|
} else {
|
|
4026
4004
|
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
|
-
}
|
|
4005
|
+
await drawNode(ctx, child, x, y, renderContext, emojiStyle);
|
|
4032
4006
|
}
|
|
4033
4007
|
}
|
|
4034
|
-
ctx.restore();
|
|
4035
4008
|
}
|
|
4036
4009
|
function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
|
|
4037
4010
|
let ox = x + width / 2;
|
|
@@ -4964,6 +4937,7 @@ async function renderReactElement(ctx, element, options = {}) {
|
|
|
4964
4937
|
renderContext
|
|
4965
4938
|
);
|
|
4966
4939
|
await drawNode(ctx, layoutTree, 0, 0, renderContext, emojiStyle);
|
|
4940
|
+
await releaseParagraphs();
|
|
4967
4941
|
}
|
|
4968
4942
|
|
|
4969
4943
|
// src/fit-text.ts
|
|
@@ -5005,10 +4979,7 @@ function findLargestUsableFontSize(options) {
|
|
|
5005
4979
|
|
|
5006
4980
|
// src/index.ts
|
|
5007
4981
|
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;
|
|
4982
|
+
return _createCanvas(width, height);
|
|
5012
4983
|
}
|
|
5013
4984
|
export {
|
|
5014
4985
|
Canvas,
|