html2canvas-pro 2.5.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/html2canvas-pro.cjs +456 -159
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +456 -159
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +456 -159
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +5 -5
- package/dist/types/core/cache-storage.d.ts +18 -0
- package/dist/types/core/logger.d.ts +0 -3
- package/dist/types/css/types/functions/conic-gradient.d.ts +6 -0
- package/dist/types/css/types/image.d.ts +4 -2
- package/dist/types/dom/replaced-elements/input-element-container.d.ts +1 -1
- package/dist/types/render/canvas/background-renderer.d.ts +9 -0
- package/dist/types/render/canvas/canvas-renderer.d.ts +8 -0
- package/dist/types/render/canvas/effects-renderer.d.ts +8 -0
- package/dist/types/render/canvas/text-clip-renderer.d.ts +25 -5
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.5.
|
|
2
|
+
* html2canvas-pro 2.5.1 <https://yorickshan.github.io/html2canvas-pro/>
|
|
3
3
|
* Copyright (c) 2024-present yorickshan and html2canvas-pro contributors
|
|
4
4
|
* Released under MIT License
|
|
5
5
|
*/
|
|
@@ -4743,6 +4743,17 @@ const conicGradient = (context, tokens) => {
|
|
|
4743
4743
|
stops
|
|
4744
4744
|
};
|
|
4745
4745
|
};
|
|
4746
|
+
/**
|
|
4747
|
+
* repeating-conic-gradient shares the conic-gradient grammar exactly; only
|
|
4748
|
+
* the image type differs, which switches the renderer to periodic stops
|
|
4749
|
+
* stacked across the full sweep.
|
|
4750
|
+
*/
|
|
4751
|
+
const repeatingConicGradient = (context, tokens) => {
|
|
4752
|
+
return {
|
|
4753
|
+
...conicGradient(context, tokens),
|
|
4754
|
+
type: 6
|
|
4755
|
+
};
|
|
4756
|
+
};
|
|
4746
4757
|
//#endregion
|
|
4747
4758
|
//#region src/css/types/functions/image-set.ts
|
|
4748
4759
|
const parseResolution = (token) => {
|
|
@@ -4795,6 +4806,9 @@ const isRepeatingRadialGradient = (background) => {
|
|
|
4795
4806
|
const isConicGradient = (background) => {
|
|
4796
4807
|
return background.type === 5;
|
|
4797
4808
|
};
|
|
4809
|
+
const isRepeatingConicGradient = (background) => {
|
|
4810
|
+
return background.type === 6;
|
|
4811
|
+
};
|
|
4798
4812
|
const image = {
|
|
4799
4813
|
name: "image",
|
|
4800
4814
|
parse: (context, value) => {
|
|
@@ -4840,6 +4854,9 @@ const SUPPORTED_IMAGE_FUNCTIONS = {
|
|
|
4840
4854
|
"conic-gradient": conicGradient,
|
|
4841
4855
|
"-webkit-conic-gradient": conicGradient,
|
|
4842
4856
|
"-moz-conic-gradient": conicGradient,
|
|
4857
|
+
"repeating-conic-gradient": repeatingConicGradient,
|
|
4858
|
+
"-webkit-repeating-conic-gradient": repeatingConicGradient,
|
|
4859
|
+
"-moz-repeating-conic-gradient": repeatingConicGradient,
|
|
4843
4860
|
"image-set": imageSet,
|
|
4844
4861
|
"-webkit-image-set": imageSet
|
|
4845
4862
|
};
|
|
@@ -5328,6 +5345,7 @@ const listStyleImage = {
|
|
|
5328
5345
|
skipCache: true,
|
|
5329
5346
|
parse: (context, token) => {
|
|
5330
5347
|
if (token.type === 20 && token.value === "none") return null;
|
|
5348
|
+
if (!isSupportedImage(token)) return null;
|
|
5331
5349
|
return image.parse(context, token);
|
|
5332
5350
|
}
|
|
5333
5351
|
};
|
|
@@ -7772,9 +7790,12 @@ var PseudoContentResolver = class {
|
|
|
7772
7790
|
case "open-quote":
|
|
7773
7791
|
anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth++, true)));
|
|
7774
7792
|
break;
|
|
7775
|
-
case "close-quote":
|
|
7776
|
-
|
|
7793
|
+
case "close-quote": {
|
|
7794
|
+
const hasOpenQuote = this.quoteDepth > 0;
|
|
7795
|
+
this.quoteDepth = Math.max(0, this.quoteDepth - 1);
|
|
7796
|
+
if (hasOpenQuote) anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth, false)));
|
|
7777
7797
|
break;
|
|
7798
|
+
}
|
|
7778
7799
|
default: anonymousReplacedElement.appendChild(document.createTextNode(token.value));
|
|
7779
7800
|
}
|
|
7780
7801
|
});
|
|
@@ -8116,12 +8137,14 @@ var DocumentCloner = class {
|
|
|
8116
8137
|
const styleBefore = window.getComputedStyle(node, ":before");
|
|
8117
8138
|
const before = this.pseudoContents.resolvePseudoContent(node, clone, styleBefore, 0);
|
|
8118
8139
|
if (before) clone.insertBefore(before, clone.firstChild);
|
|
8140
|
+
}
|
|
8141
|
+
if (isCustomElement(node)) copyStyles = true;
|
|
8142
|
+
if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
|
|
8143
|
+
if (checkPseudoElements) {
|
|
8119
8144
|
const styleAfter = window.getComputedStyle(node, ":after");
|
|
8120
8145
|
const after = this.pseudoContents.resolvePseudoContent(node, clone, styleAfter, 1);
|
|
8121
8146
|
if (after) clone.appendChild(after);
|
|
8122
8147
|
}
|
|
8123
|
-
if (isCustomElement(node)) copyStyles = true;
|
|
8124
|
-
if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
|
|
8125
8148
|
this.counters.pop(counters);
|
|
8126
8149
|
if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node) || copyStyles) copyCSSStyles(style, clone);
|
|
8127
8150
|
if (node.scrollTop !== 0 || node.scrollLeft !== 0) this.scrolledElements.push([
|
|
@@ -9017,10 +9040,10 @@ const LIST_OWNERS = [
|
|
|
9017
9040
|
"UL",
|
|
9018
9041
|
"MENU"
|
|
9019
9042
|
];
|
|
9020
|
-
const parseNodeTree = (context, node, parent, root) => {
|
|
9043
|
+
const parseNodeTree = (context, node, parent, root, zoomScale = 1) => {
|
|
9021
9044
|
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
|
|
9022
9045
|
nextNode = childNode.nextSibling;
|
|
9023
|
-
parseChildNode(context, childNode, parent, root);
|
|
9046
|
+
parseChildNode(context, childNode, parent, root, zoomScale);
|
|
9024
9047
|
}
|
|
9025
9048
|
};
|
|
9026
9049
|
/**
|
|
@@ -9031,19 +9054,29 @@ const parseNodeTree = (context, node, parent, root) => {
|
|
|
9031
9054
|
* this, slotted shadow-DOM content such as web component labels was silently
|
|
9032
9055
|
* dropped because `parseNodeTree` only iterates `node.firstChild` (issue #226).
|
|
9033
9056
|
*/
|
|
9034
|
-
const parseChildNode = (context, childNode, parent, root) => {
|
|
9035
|
-
if (isTextNode(childNode) && childNode.data.length > 0)
|
|
9036
|
-
|
|
9037
|
-
if (
|
|
9057
|
+
const parseChildNode = (context, childNode, parent, root, zoomScale = 1) => {
|
|
9058
|
+
if (isTextNode(childNode) && childNode.data.length > 0) {
|
|
9059
|
+
const textContainer = new TextContainer(context, childNode, parent.styles);
|
|
9060
|
+
if (zoomScale !== 1) {
|
|
9061
|
+
const originLeft = parent.bounds.left;
|
|
9062
|
+
const originTop = parent.bounds.top;
|
|
9063
|
+
textContainer.textBounds = textContainer.textBounds.map((tb) => new TextBounds(tb.text, new Bounds(originLeft + (tb.bounds.left - originLeft) / zoomScale, originTop + (tb.bounds.top - originTop) / zoomScale, tb.bounds.width / zoomScale, tb.bounds.height / zoomScale)));
|
|
9064
|
+
}
|
|
9065
|
+
parent.textNodes.push(textContainer);
|
|
9066
|
+
} else if (isElementNode(childNode)) {
|
|
9067
|
+
if (isSlotElement(childNode) && childNode.assignedNodes) childNode.assignedNodes().forEach((assignedNode) => parseChildNode(context, assignedNode, parent, root, zoomScale));
|
|
9038
9068
|
else {
|
|
9039
9069
|
const container = createContainer(context, childNode);
|
|
9070
|
+
const ownZoom = container.styles.zoom || 1;
|
|
9071
|
+
if (ownZoom !== 1) container.bounds = new Bounds(container.bounds.left, container.bounds.top, container.bounds.width / ownZoom, container.bounds.height / ownZoom);
|
|
9040
9072
|
if (container.styles.isVisible()) {
|
|
9041
9073
|
if (createsRealStackingContext(childNode, container, root)) container.createsRealStackingContext = true;
|
|
9042
9074
|
else if (createsStackingContext(container.styles)) container.createsStackingContext = true;
|
|
9043
9075
|
if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) container.isListOwner = true;
|
|
9044
9076
|
parent.elements.push(container);
|
|
9045
|
-
|
|
9046
|
-
|
|
9077
|
+
const childZoom = zoomScale * (container.styles.zoom || 1);
|
|
9078
|
+
if (childNode.shadowRoot) parseNodeTree(context, childNode.shadowRoot, container, root, childZoom);
|
|
9079
|
+
else if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) parseNodeTree(context, childNode, container, root, childZoom);
|
|
9047
9080
|
}
|
|
9048
9081
|
}
|
|
9049
9082
|
}
|
|
@@ -9071,6 +9104,7 @@ const createsRealStackingContext = (node, container, root) => {
|
|
|
9071
9104
|
};
|
|
9072
9105
|
const createsStackingContext = (styles) => {
|
|
9073
9106
|
if (styles.isPositioned() || styles.isFloating()) return true;
|
|
9107
|
+
if (styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) return true;
|
|
9074
9108
|
return contains(styles.display, 268435456) || contains(styles.display, 33554432) || contains(styles.display, 536870912) || contains(styles.display, 134217728);
|
|
9075
9109
|
};
|
|
9076
9110
|
//#endregion
|
|
@@ -9490,9 +9524,8 @@ var ElementPaint = class {
|
|
|
9490
9524
|
if (this.container.styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) this.effects.push(new BlendEffect(this.container.styles.mixBlendMode));
|
|
9491
9525
|
if (this.container.styles.filter !== null) this.effects.push(new FilterEffect(this.container.styles.filter));
|
|
9492
9526
|
if (this.container.styles.zoom !== 1) {
|
|
9493
|
-
const
|
|
9494
|
-
const
|
|
9495
|
-
const offsetY = this.container.bounds.top + getAbsoluteValue(origin[1], this.container.bounds.height);
|
|
9527
|
+
const offsetX = this.container.bounds.left;
|
|
9528
|
+
const offsetY = this.container.bounds.top;
|
|
9496
9529
|
const z = this.container.styles.zoom;
|
|
9497
9530
|
const zoomMatrix = [
|
|
9498
9531
|
z,
|
|
@@ -9524,7 +9557,7 @@ var ElementPaint = class {
|
|
|
9524
9557
|
computeEffects(target, surfaceRoot) {
|
|
9525
9558
|
let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
|
|
9526
9559
|
let parent = this === surfaceRoot ? null : this.parent;
|
|
9527
|
-
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
|
|
9560
|
+
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect));
|
|
9528
9561
|
const effects = sourceEffects(this);
|
|
9529
9562
|
while (parent) {
|
|
9530
9563
|
const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
|
|
@@ -10417,7 +10450,7 @@ var TextRenderer = class {
|
|
|
10417
10450
|
this.ctx.shadowColor = asString(textShadow.color);
|
|
10418
10451
|
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
|
10419
10452
|
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
|
10420
|
-
this.ctx.shadowBlur = textShadow.blur.number;
|
|
10453
|
+
this.ctx.shadowBlur = textShadow.blur.number * this.options.scale;
|
|
10421
10454
|
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
|
|
10422
10455
|
});
|
|
10423
10456
|
this.ctx.shadowColor = "";
|
|
@@ -10567,7 +10600,7 @@ var TextRenderer = class {
|
|
|
10567
10600
|
this.ctx.textBaseline = "alphabetic";
|
|
10568
10601
|
const paintOrder = styles.paintOrder;
|
|
10569
10602
|
const baseline = this.measureBaselineCached(fontString ?? "", styles.fontSize.number);
|
|
10570
|
-
if (styles.webkitLineClamp > 0 && (styles.display &
|
|
10603
|
+
if (styles.webkitLineClamp > 0 && (styles.display & 34) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
|
|
10571
10604
|
if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
|
|
10572
10605
|
}
|
|
10573
10606
|
if (styles.textOverflow === 1 && containerBounds && styles.overflowX === 1 && text.textBounds.length > 0 && this.renderEllipsisText(text, styles, paintOrder, baseline, containerBounds)) return;
|
|
@@ -10766,6 +10799,7 @@ var BackgroundRenderer = class {
|
|
|
10766
10799
|
else if (isRadialGradient(backgroundImage)) this.renderRadialGradient(container, backgroundImage, index);
|
|
10767
10800
|
else if (isRepeatingRadialGradient(backgroundImage)) this.renderRepeatingRadialGradient(container, backgroundImage, index);
|
|
10768
10801
|
else if (isConicGradient(backgroundImage)) this.renderConicGradient(container, backgroundImage, index);
|
|
10802
|
+
else if (isRepeatingConicGradient(backgroundImage)) this.renderRepeatingConicGradient(container, backgroundImage, index);
|
|
10769
10803
|
if (layerCount > 0) {
|
|
10770
10804
|
if ((blendModes[layerCount] ?? blendModes[0] ?? "normal") !== "normal") this.ctx.restore();
|
|
10771
10805
|
}
|
|
@@ -10816,45 +10850,43 @@ var BackgroundRenderer = class {
|
|
|
10816
10850
|
null,
|
|
10817
10851
|
null
|
|
10818
10852
|
]);
|
|
10819
|
-
const [lineLength,
|
|
10853
|
+
const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(backgroundImage.angle, width, height);
|
|
10854
|
+
const gx0 = x + lx0;
|
|
10855
|
+
const gx1 = x + lx1;
|
|
10856
|
+
const gy0 = y + ly0;
|
|
10857
|
+
const gy1 = y + ly1;
|
|
10820
10858
|
const processedStops = processColorStops(backgroundImage.stops, lineLength || 1);
|
|
10821
10859
|
const lastStop = at(processedStops, processedStops.length - 1);
|
|
10822
10860
|
const firstStop = at(processedStops, 0);
|
|
10823
|
-
const
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10861
|
+
const period = Math.max(lastStop.stop - firstStop.stop, .01);
|
|
10862
|
+
if (lastStop.stop - firstStop.stop < 1e-6) {
|
|
10863
|
+
const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
|
|
10864
|
+
gradient.addColorStop(0, asString(lastStop.color));
|
|
10865
|
+
gradient.addColorStop(1, asString(lastStop.color));
|
|
10866
|
+
this.path(path);
|
|
10867
|
+
this.ctx.fillStyle = gradient;
|
|
10868
|
+
this.ctx.fill();
|
|
10827
10869
|
return;
|
|
10828
10870
|
}
|
|
10829
|
-
const
|
|
10830
|
-
const
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
const cacheKey = `rlg|${backgroundImage.angle}|${Math.round(patternLength)}|${JSON.stringify(backgroundImage.stops)}`;
|
|
10838
|
-
let pattern = this.patternCache.get(cacheKey);
|
|
10839
|
-
if (!pattern) {
|
|
10840
|
-
const canvas = ownerDocument.createElement("canvas");
|
|
10841
|
-
const canvasSize = Math.max(1, Math.ceil(patternLength));
|
|
10842
|
-
canvas.width = canvasSize;
|
|
10843
|
-
canvas.height = canvasSize;
|
|
10844
|
-
const ctx = canvas.getContext("2d");
|
|
10845
|
-
if (!ctx) return;
|
|
10846
|
-
const gradient = ctx.createLinearGradient(pX0 - x, pY0 - y, pX1 - x, pY1 - y);
|
|
10847
|
-
processedStops.forEach((colorStop) => {
|
|
10848
|
-
gradient.addColorStop((colorStop.stop - firstStop.stop) / stopSpan, asString(colorStop.color));
|
|
10871
|
+
const stops = [];
|
|
10872
|
+
const cycles = Math.ceil(1 / period);
|
|
10873
|
+
for (let k = 0; k < cycles; k++) for (const s of processedStops) {
|
|
10874
|
+
const stop = k * period + (s.stop - firstStop.stop);
|
|
10875
|
+
if (stop > 1) break;
|
|
10876
|
+
stops.push({
|
|
10877
|
+
stop,
|
|
10878
|
+
color: asString(s.color)
|
|
10849
10879
|
});
|
|
10850
|
-
ctx.fillStyle = gradient;
|
|
10851
|
-
ctx.fillRect(0, 0, canvasSize, canvasSize);
|
|
10852
|
-
if (canvasSize > 0) {
|
|
10853
|
-
pattern = this.ctx.createPattern(canvas, "repeat");
|
|
10854
|
-
this.patternCache.set(cacheKey, pattern);
|
|
10855
|
-
}
|
|
10856
10880
|
}
|
|
10857
|
-
|
|
10881
|
+
stops.push({
|
|
10882
|
+
stop: 1,
|
|
10883
|
+
color: asString(lastStop.color)
|
|
10884
|
+
});
|
|
10885
|
+
const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
|
|
10886
|
+
stops.forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color));
|
|
10887
|
+
this.path(path);
|
|
10888
|
+
this.ctx.fillStyle = gradient;
|
|
10889
|
+
this.ctx.fill();
|
|
10858
10890
|
}
|
|
10859
10891
|
/**
|
|
10860
10892
|
* Render a linear gradient background
|
|
@@ -10904,7 +10936,7 @@ var BackgroundRenderer = class {
|
|
|
10904
10936
|
}
|
|
10905
10937
|
if (rx > 0 && ry > 0) {
|
|
10906
10938
|
const cacheKey = `rg|${Math.round(x)}x${Math.round(y)}|${Math.round(rx)}x${Math.round(ry)}|${JSON.stringify(backgroundImage.stops)}`;
|
|
10907
|
-
const stops = processColorStops(backgroundImage.stops, rx
|
|
10939
|
+
const stops = processColorStops(backgroundImage.stops, rx);
|
|
10908
10940
|
const lastStop = stops[stops.length - 1];
|
|
10909
10941
|
let pattern = this.patternCache.get(cacheKey);
|
|
10910
10942
|
if (!pattern) {
|
|
@@ -10961,14 +10993,14 @@ var BackgroundRenderer = class {
|
|
|
10961
10993
|
ry = Math.max(ry, .01);
|
|
10962
10994
|
}
|
|
10963
10995
|
if (!(rx > 0 && ry > 0)) return;
|
|
10964
|
-
const processed = processColorStops(backgroundImage.stops, rx
|
|
10996
|
+
const processed = processColorStops(backgroundImage.stops, rx);
|
|
10965
10997
|
const firstStop = at(processed, 0);
|
|
10966
10998
|
const lastStop = at(processed, processed.length - 1);
|
|
10967
10999
|
const period = Math.max(lastStop.stop - firstStop.stop, .01);
|
|
10968
11000
|
const stops = [];
|
|
10969
|
-
const rings = Math.ceil(
|
|
11001
|
+
const rings = Math.ceil(1 / period);
|
|
10970
11002
|
for (let k = 0; k < rings; k++) for (const s of processed) {
|
|
10971
|
-
const stop =
|
|
11003
|
+
const stop = k * period + (s.stop - firstStop.stop);
|
|
10972
11004
|
if (stop > 1) break;
|
|
10973
11005
|
stops.push({
|
|
10974
11006
|
stop,
|
|
@@ -11035,6 +11067,65 @@ var BackgroundRenderer = class {
|
|
|
11035
11067
|
this.ctx.restore();
|
|
11036
11068
|
}
|
|
11037
11069
|
/**
|
|
11070
|
+
* Render a repeating conic gradient background.
|
|
11071
|
+
*
|
|
11072
|
+
* Canvas conic gradient stops span the full 2π sweep as [0, 1], so the
|
|
11073
|
+
* one-cycle stop list is stacked k·period + s for k = 0..cycles-1 over the
|
|
11074
|
+
* whole sweep — the same periodic-stop stacking the repeating linear and
|
|
11075
|
+
* radial renderers use along their gradient lines.
|
|
11076
|
+
*/
|
|
11077
|
+
renderRepeatingConicGradient(container, backgroundImage, index) {
|
|
11078
|
+
const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [
|
|
11079
|
+
null,
|
|
11080
|
+
null,
|
|
11081
|
+
null
|
|
11082
|
+
]);
|
|
11083
|
+
const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
|
|
11084
|
+
const cx = left + getAbsoluteValue(at(position, 0), width);
|
|
11085
|
+
const cy = top + getAbsoluteValue(at(position, position.length - 1), height);
|
|
11086
|
+
const processedStops = processColorStops(backgroundImage.stops, 1);
|
|
11087
|
+
const firstStop = at(processedStops, 0);
|
|
11088
|
+
const lastStop = at(processedStops, processedStops.length - 1);
|
|
11089
|
+
const period = Math.max(lastStop.stop - firstStop.stop, .01);
|
|
11090
|
+
if (lastStop.stop - firstStop.stop < 1e-6) {
|
|
11091
|
+
const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
|
|
11092
|
+
gradient.addColorStop(0, asString(lastStop.color));
|
|
11093
|
+
gradient.addColorStop(1, asString(lastStop.color));
|
|
11094
|
+
this.path(path);
|
|
11095
|
+
this.ctx.save();
|
|
11096
|
+
this.ctx.clip();
|
|
11097
|
+
this.ctx.fillStyle = gradient;
|
|
11098
|
+
this.ctx.fill();
|
|
11099
|
+
this.ctx.restore();
|
|
11100
|
+
return;
|
|
11101
|
+
}
|
|
11102
|
+
const stops = [];
|
|
11103
|
+
const cycles = Math.ceil(1 / period);
|
|
11104
|
+
for (let k = 0; k < cycles; k++) for (const s of processedStops) {
|
|
11105
|
+
const stop = k * period + (s.stop - firstStop.stop);
|
|
11106
|
+
if (stop > 1) break;
|
|
11107
|
+
stops.push({
|
|
11108
|
+
stop,
|
|
11109
|
+
color: asString(s.color)
|
|
11110
|
+
});
|
|
11111
|
+
}
|
|
11112
|
+
const lastIncluded = at(stops, stops.length - 1);
|
|
11113
|
+
if (lastIncluded && lastIncluded.stop < 1) stops.push({
|
|
11114
|
+
stop: 1,
|
|
11115
|
+
color: lastIncluded.color
|
|
11116
|
+
});
|
|
11117
|
+
const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
|
|
11118
|
+
stops.forEach((s) => {
|
|
11119
|
+
gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
|
|
11120
|
+
});
|
|
11121
|
+
this.path(path);
|
|
11122
|
+
this.ctx.save();
|
|
11123
|
+
this.ctx.clip();
|
|
11124
|
+
this.ctx.fillStyle = gradient;
|
|
11125
|
+
this.ctx.fill();
|
|
11126
|
+
this.ctx.restore();
|
|
11127
|
+
}
|
|
11128
|
+
/**
|
|
11038
11129
|
* Render a repeating pattern with offset
|
|
11039
11130
|
*
|
|
11040
11131
|
* @param path - Path to fill
|
|
@@ -11459,6 +11550,13 @@ var BorderImageRenderer = class {
|
|
|
11459
11550
|
* - Filter effects (CSS filter functions)
|
|
11460
11551
|
*/
|
|
11461
11552
|
/**
|
|
11553
|
+
* Scale `blur(<n>px)` radii in a ctx.filter string by the device-pixel
|
|
11554
|
+
* ratio. Other filter functions (brightness, saturate, …) take
|
|
11555
|
+
* unitless values and are unaffected.
|
|
11556
|
+
*/
|
|
11557
|
+
const BLUR_PX_RE = /blur\((\d+(?:\.\d+)?)px\)/gi;
|
|
11558
|
+
const scaleFilterBlur = (filter, scale) => scale === 1 ? filter : filter.replace(BLUR_PX_RE, (_match, value) => `blur(${parseFloat(value) * scale}px)`);
|
|
11559
|
+
/**
|
|
11462
11560
|
* Effects Renderer
|
|
11463
11561
|
*
|
|
11464
11562
|
* Manages rendering effects stack including opacity, transforms, and clipping.
|
|
@@ -11496,6 +11594,7 @@ var EffectsRenderer = class {
|
|
|
11496
11594
|
this.saveAtDepth = -1;
|
|
11497
11595
|
this.ctx = deps.ctx;
|
|
11498
11596
|
this.pathCallback = pathCallback;
|
|
11597
|
+
this.scale = deps.scale ?? 1;
|
|
11499
11598
|
}
|
|
11500
11599
|
/**
|
|
11501
11600
|
* Apply multiple effects.
|
|
@@ -11534,11 +11633,11 @@ var EffectsRenderer = class {
|
|
|
11534
11633
|
else if (isOpacityEffect(effect)) this.ctx.globalAlpha *= effect.opacity;
|
|
11535
11634
|
else if (isBlendEffect(effect)) this.ctx.globalCompositeOperation = effect.compositeOperation;
|
|
11536
11635
|
else if (isFilterEffect(effect)) {
|
|
11537
|
-
this.ctx.filter = effect.safeFilterString || "none";
|
|
11636
|
+
this.ctx.filter = scaleFilterBlur(effect.safeFilterString, this.scale) || "none";
|
|
11538
11637
|
if (effect.shadow) {
|
|
11539
|
-
this.ctx.shadowOffsetX = effect.shadow.offsetX;
|
|
11540
|
-
this.ctx.shadowOffsetY = effect.shadow.offsetY;
|
|
11541
|
-
this.ctx.shadowBlur = effect.shadow.blur;
|
|
11638
|
+
this.ctx.shadowOffsetX = effect.shadow.offsetX * this.scale;
|
|
11639
|
+
this.ctx.shadowOffsetY = effect.shadow.offsetY * this.scale;
|
|
11640
|
+
this.ctx.shadowBlur = effect.shadow.blur * this.scale;
|
|
11542
11641
|
this.ctx.shadowColor = effect.shadow.color;
|
|
11543
11642
|
}
|
|
11544
11643
|
}
|
|
@@ -11683,8 +11782,8 @@ const paintGradientLayer = (ctx, layer, area) => {
|
|
|
11683
11782
|
let gradient = null;
|
|
11684
11783
|
if (layer.type === 1 || layer.type === 3) {
|
|
11685
11784
|
const linear = layer;
|
|
11686
|
-
const [lineLength,
|
|
11687
|
-
gradient = ctx.createLinearGradient(
|
|
11785
|
+
const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(linear.angle || 0, width, height);
|
|
11786
|
+
gradient = ctx.createLinearGradient(area.left + lx0, area.top + ly0, area.left + lx1, area.top + ly1);
|
|
11688
11787
|
processColorStops(linear.stops, lineLength || 1).forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), asString(s.color)));
|
|
11689
11788
|
} else if (isConicGradient(layer)) {
|
|
11690
11789
|
const conic = layer;
|
|
@@ -11845,14 +11944,14 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
|
11845
11944
|
new Vector(container.bounds.left + size * .84, container.bounds.top + size * .34085),
|
|
11846
11945
|
new Vector(container.bounds.left + size * .39363, container.bounds.top + size * .79)
|
|
11847
11946
|
]);
|
|
11848
|
-
ctx.fillStyle = asString(container.styles.accentColor ??
|
|
11947
|
+
ctx.fillStyle = asString(container.styles.accentColor ?? 7733247);
|
|
11849
11948
|
ctx.fill();
|
|
11850
11949
|
ctx.restore();
|
|
11851
11950
|
} else if (container.type === "radio" && container.checked) {
|
|
11852
11951
|
ctx.save();
|
|
11853
11952
|
ctx.beginPath();
|
|
11854
11953
|
ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
|
|
11855
|
-
ctx.fillStyle = asString(container.styles.accentColor ??
|
|
11954
|
+
ctx.fillStyle = asString(container.styles.accentColor ?? 7733247);
|
|
11856
11955
|
ctx.fill();
|
|
11857
11956
|
ctx.restore();
|
|
11858
11957
|
}
|
|
@@ -11951,22 +12050,51 @@ async function renderListMarker(ctx, context, textRenderer, paint, container, st
|
|
|
11951
12050
|
const url = img.url;
|
|
11952
12051
|
try {
|
|
11953
12052
|
const image = await context.cache.match(url);
|
|
11954
|
-
|
|
12053
|
+
const fontSize = getAbsoluteValue(styles.fontSize, 0);
|
|
12054
|
+
const naturalWidth = image?.naturalWidth || image.width;
|
|
12055
|
+
const naturalHeight = image?.naturalHeight || image.height;
|
|
12056
|
+
if (naturalWidth <= 0 || naturalHeight <= 0) return;
|
|
12057
|
+
const markerScale = Math.min(fontSize / naturalWidth, fontSize / naturalHeight);
|
|
12058
|
+
const markerWidth = naturalWidth * markerScale;
|
|
12059
|
+
const markerHeight = naturalHeight * markerScale;
|
|
12060
|
+
const markerCenterY = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
|
|
12061
|
+
ctx.drawImage(image, container.bounds.left - (markerWidth + 10), markerCenterY - markerHeight / 2, markerWidth, markerHeight);
|
|
11955
12062
|
} catch (e) {
|
|
11956
12063
|
context.logger.error(`Error loading list-style-image ${url}`);
|
|
11957
12064
|
context.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
11958
12065
|
}
|
|
11959
12066
|
}
|
|
11960
12067
|
} else if (paint.listValue && container.styles.listStyleType !== -1) {
|
|
11961
|
-
const
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
12068
|
+
const type = container.styles.listStyleType;
|
|
12069
|
+
if (type === 0 || type === 1 || type === 2) {
|
|
12070
|
+
const size = styles.fontSize.number * .45;
|
|
12071
|
+
const cx = container.bounds.left - size * .9;
|
|
12072
|
+
const cy = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
|
|
12073
|
+
ctx.save();
|
|
12074
|
+
ctx.fillStyle = asString(styles.color);
|
|
12075
|
+
ctx.beginPath();
|
|
12076
|
+
if (type === 2) ctx.rect(cx - size / 2, cy - size / 2, size, size);
|
|
12077
|
+
else {
|
|
12078
|
+
ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
|
|
12079
|
+
if (type === 1) {
|
|
12080
|
+
ctx.strokeStyle = asString(styles.color);
|
|
12081
|
+
ctx.lineWidth = size * .18;
|
|
12082
|
+
}
|
|
12083
|
+
}
|
|
12084
|
+
if (type === 1) ctx.stroke();
|
|
12085
|
+
else ctx.fill();
|
|
12086
|
+
ctx.restore();
|
|
12087
|
+
} else {
|
|
12088
|
+
const [font] = textRenderer.createFontStyle(styles);
|
|
12089
|
+
ctx.font = font ?? "";
|
|
12090
|
+
ctx.fillStyle = asString(styles.color);
|
|
12091
|
+
ctx.textBaseline = "middle";
|
|
12092
|
+
ctx.textAlign = "right";
|
|
12093
|
+
const bounds = new Bounds(container.bounds.left, container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width), container.bounds.width, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1);
|
|
12094
|
+
textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
|
|
12095
|
+
ctx.textBaseline = "bottom";
|
|
12096
|
+
ctx.textAlign = "left";
|
|
12097
|
+
}
|
|
11970
12098
|
}
|
|
11971
12099
|
}
|
|
11972
12100
|
/**
|
|
@@ -12209,31 +12337,33 @@ var TextClipRenderer = class {
|
|
|
12209
12337
|
constructor(deps) {
|
|
12210
12338
|
this.ctx = deps.ctx;
|
|
12211
12339
|
this.context = deps.context;
|
|
12340
|
+
this.scale = deps.scale;
|
|
12212
12341
|
this.createFontStyle = deps.createFontStyle;
|
|
12213
12342
|
}
|
|
12214
12343
|
async render(paint) {
|
|
12215
12344
|
const container = paint.container;
|
|
12216
12345
|
const styles = container.styles;
|
|
12217
12346
|
const bounds = container.bounds;
|
|
12347
|
+
const scale = this.scale;
|
|
12218
12348
|
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
12219
12349
|
const ownerDocument = this.ctx.canvas.ownerDocument ?? document;
|
|
12220
|
-
const offscreen = ownerDocument.createElement("canvas");
|
|
12221
12350
|
const width = Math.ceil(bounds.width);
|
|
12222
12351
|
const height = Math.ceil(bounds.height);
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
const
|
|
12226
|
-
if (!
|
|
12352
|
+
const deviceWidth = Math.ceil(bounds.width * scale);
|
|
12353
|
+
const deviceHeight = Math.ceil(bounds.height * scale);
|
|
12354
|
+
const offscreen = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
|
|
12355
|
+
if (!offscreen) return;
|
|
12356
|
+
offscreen.ctx.scale(scale, scale);
|
|
12227
12357
|
const [fontString] = this.createFontStyle(styles);
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
const baseline = measureBaseline(
|
|
12358
|
+
offscreen.ctx.font = fontString ?? "";
|
|
12359
|
+
offscreen.ctx.textBaseline = "alphabetic";
|
|
12360
|
+
offscreen.ctx.textAlign = "left";
|
|
12361
|
+
offscreen.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
12362
|
+
const baseline = measureBaseline(offscreen.ctx, styles.fontSize.number);
|
|
12233
12363
|
const bgRenderer = new BackgroundRenderer({
|
|
12234
|
-
ctx:
|
|
12364
|
+
ctx: offscreen.ctx,
|
|
12235
12365
|
context: this.context,
|
|
12236
|
-
canvas: offscreen,
|
|
12366
|
+
canvas: offscreen.canvas,
|
|
12237
12367
|
options: {
|
|
12238
12368
|
width,
|
|
12239
12369
|
height,
|
|
@@ -12241,51 +12371,115 @@ var TextClipRenderer = class {
|
|
|
12241
12371
|
}
|
|
12242
12372
|
});
|
|
12243
12373
|
if (!isTransparent(styles.backgroundColor)) {
|
|
12244
|
-
|
|
12245
|
-
|
|
12374
|
+
offscreen.ctx.fillStyle = asString(styles.backgroundColor);
|
|
12375
|
+
offscreen.ctx.fillRect(0, 0, width, height);
|
|
12246
12376
|
}
|
|
12247
|
-
|
|
12248
|
-
|
|
12377
|
+
offscreen.ctx.save();
|
|
12378
|
+
offscreen.ctx.translate(-bounds.left, -bounds.top);
|
|
12249
12379
|
await bgRenderer.renderBackgroundImage(container);
|
|
12250
|
-
|
|
12251
|
-
const
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
}
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
this.
|
|
12380
|
+
offscreen.ctx.restore();
|
|
12381
|
+
const drawOptions = {
|
|
12382
|
+
bounds,
|
|
12383
|
+
baseline,
|
|
12384
|
+
letterSpacing: styles.letterSpacing,
|
|
12385
|
+
writingMode: styles.writingMode
|
|
12386
|
+
};
|
|
12387
|
+
const mask = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, offscreen.ctx);
|
|
12388
|
+
if (!mask) return;
|
|
12389
|
+
const hasStroke = !!styles.webkitTextStrokeWidth;
|
|
12390
|
+
if (hasStroke) {
|
|
12391
|
+
mask.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
|
|
12392
|
+
mask.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
12393
|
+
mask.ctx.lineJoin = this.getTextStrokeLineJoin();
|
|
12394
|
+
}
|
|
12395
|
+
for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(mask, textBound, drawOptions, (ctx, text, x, y) => {
|
|
12396
|
+
ctx.fillText(text, x, y);
|
|
12397
|
+
if (hasStroke) ctx.strokeText(text, x, y);
|
|
12398
|
+
});
|
|
12399
|
+
offscreen.ctx.globalCompositeOperation = "destination-in";
|
|
12400
|
+
offscreen.ctx.drawImage(mask.canvas, 0, 0, width, height);
|
|
12401
|
+
const shadowCanvases = this.renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, offscreen.ctx, styles, drawOptions);
|
|
12402
|
+
if (shadowCanvases) {
|
|
12403
|
+
const composite = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
|
|
12404
|
+
if (!composite) return;
|
|
12405
|
+
for (const shadowCanvas of shadowCanvases) composite.ctx.drawImage(shadowCanvas, 0, 0);
|
|
12406
|
+
composite.ctx.drawImage(offscreen.canvas, 0, 0);
|
|
12407
|
+
this.ctx.drawImage(composite.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
|
|
12408
|
+
} else this.ctx.drawImage(offscreen.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
|
|
12409
|
+
}
|
|
12410
|
+
createLayer(ownerDocument, width, height) {
|
|
12411
|
+
const canvas = ownerDocument.createElement("canvas");
|
|
12412
|
+
canvas.width = width;
|
|
12413
|
+
canvas.height = height;
|
|
12414
|
+
const ctx = canvas.getContext("2d");
|
|
12415
|
+
if (!ctx) return null;
|
|
12416
|
+
return {
|
|
12417
|
+
canvas,
|
|
12418
|
+
ctx
|
|
12419
|
+
};
|
|
12420
|
+
}
|
|
12421
|
+
createGlyphLayer(ownerDocument, width, height, fontSource) {
|
|
12422
|
+
const layer = this.createLayer(ownerDocument, width, height);
|
|
12423
|
+
if (!layer) return null;
|
|
12424
|
+
layer.ctx.scale(this.scale, this.scale);
|
|
12425
|
+
layer.ctx.font = fontSource.font;
|
|
12426
|
+
layer.ctx.textBaseline = fontSource.textBaseline;
|
|
12427
|
+
layer.ctx.textAlign = fontSource.textAlign;
|
|
12428
|
+
layer.ctx.direction = fontSource.direction;
|
|
12429
|
+
layer.ctx.fillStyle = "#000";
|
|
12430
|
+
return layer;
|
|
12272
12431
|
}
|
|
12273
12432
|
/**
|
|
12274
|
-
*
|
|
12275
|
-
*
|
|
12433
|
+
* Draw one text bound onto a glyph layer, splitting into per-glyph draws
|
|
12434
|
+
* when letter-spacing is in effect so the layout matches the main text
|
|
12435
|
+
* renderer. Vertical writing modes fall back to a single draw call.
|
|
12276
12436
|
*/
|
|
12277
|
-
|
|
12278
|
-
const
|
|
12437
|
+
drawGlyphs(layer, textBound, drawOptions, draw) {
|
|
12438
|
+
const x = textBound.bounds.left - drawOptions.bounds.left;
|
|
12439
|
+
const y = textBound.bounds.top - drawOptions.bounds.top + drawOptions.baseline;
|
|
12440
|
+
const { letterSpacing, writingMode } = drawOptions;
|
|
12441
|
+
if (letterSpacing <= 0) {
|
|
12442
|
+
draw(layer.ctx, textBound.text, x, y);
|
|
12443
|
+
return;
|
|
12444
|
+
}
|
|
12445
|
+
if (isVerticalWritingMode(writingMode)) {
|
|
12446
|
+
draw(layer.ctx, textBound.text, x, y);
|
|
12447
|
+
return;
|
|
12448
|
+
}
|
|
12279
12449
|
let offset = x;
|
|
12280
|
-
for (const letter of
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
return;
|
|
12284
|
-
}
|
|
12285
|
-
ctx.fillText(letter, offset, y);
|
|
12286
|
-
offset += ctx.measureText(letter).width + letterSpacing;
|
|
12450
|
+
for (const letter of segmentGraphemes(textBound.text)) {
|
|
12451
|
+
draw(layer.ctx, letter, offset, y);
|
|
12452
|
+
offset += layer.ctx.measureText(letter).width + letterSpacing;
|
|
12287
12453
|
}
|
|
12288
12454
|
}
|
|
12455
|
+
/**
|
|
12456
|
+
* Paint the text-shadow silhouettes for every glyph, one canvas per
|
|
12457
|
+
* shadow, returned front-to-back (the last shadow sits deepest). Returns
|
|
12458
|
+
* null when the element has no text shadows.
|
|
12459
|
+
*/
|
|
12460
|
+
renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, fontSource, styles, drawOptions) {
|
|
12461
|
+
const textShadows = styles.textShadow;
|
|
12462
|
+
if (!textShadows.length) return null;
|
|
12463
|
+
const canvases = [];
|
|
12464
|
+
for (let i = textShadows.length - 1; i >= 0; i--) {
|
|
12465
|
+
const textShadow = at(textShadows, i);
|
|
12466
|
+
const layer = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, fontSource);
|
|
12467
|
+
if (!layer) continue;
|
|
12468
|
+
layer.ctx.shadowColor = asString(textShadow.color);
|
|
12469
|
+
layer.ctx.shadowOffsetX = textShadow.offsetX.number * this.scale;
|
|
12470
|
+
layer.ctx.shadowOffsetY = textShadow.offsetY.number * this.scale;
|
|
12471
|
+
layer.ctx.shadowBlur = textShadow.blur.number * this.scale;
|
|
12472
|
+
layer.ctx.fillStyle = asString(textShadow.color);
|
|
12473
|
+
for (const textNode of paint.container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(layer, textBound, drawOptions, (ctx, text, x, y) => {
|
|
12474
|
+
ctx.fillText(text, x, y);
|
|
12475
|
+
});
|
|
12476
|
+
canvases.push(layer.canvas);
|
|
12477
|
+
}
|
|
12478
|
+
return canvases.length ? canvases : null;
|
|
12479
|
+
}
|
|
12480
|
+
getTextStrokeLineJoin() {
|
|
12481
|
+
return (typeof window !== "undefined" ? window : void 0)?.chrome ? "miter" : "round";
|
|
12482
|
+
}
|
|
12289
12483
|
};
|
|
12290
12484
|
//#endregion
|
|
12291
12485
|
//#region src/render/canvas/canvas-renderer.ts
|
|
@@ -12330,7 +12524,10 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12330
12524
|
formatPath: (paths) => this.formatPath(paths)
|
|
12331
12525
|
});
|
|
12332
12526
|
this.borderImageRenderer = new BorderImageRenderer(this.ctx);
|
|
12333
|
-
this.effectsRenderer = new EffectsRenderer({
|
|
12527
|
+
this.effectsRenderer = new EffectsRenderer({
|
|
12528
|
+
ctx: this.ctx,
|
|
12529
|
+
scale: options.scale
|
|
12530
|
+
}, { path: (paths) => this.path(paths) });
|
|
12334
12531
|
this.textRenderer = new TextRenderer({
|
|
12335
12532
|
ctx: this.ctx,
|
|
12336
12533
|
options: { scale: options.scale }
|
|
@@ -12338,6 +12535,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12338
12535
|
this.textClipRenderer = new TextClipRenderer({
|
|
12339
12536
|
ctx: this.ctx,
|
|
12340
12537
|
context: this.context,
|
|
12538
|
+
scale: options.scale,
|
|
12341
12539
|
createFontStyle: (styles) => this.textRenderer.createFontStyle(styles)
|
|
12342
12540
|
});
|
|
12343
12541
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
@@ -12348,8 +12546,9 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12348
12546
|
const filter = parseSimpleFilter(styles.filter);
|
|
12349
12547
|
const hasMask = styles.maskImage.length > 0;
|
|
12350
12548
|
const reflect = styles.webkitBoxReflect;
|
|
12351
|
-
|
|
12352
|
-
|
|
12549
|
+
const hasBlend = styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL;
|
|
12550
|
+
if (stack.element !== this.surfaceRoot && !this.legacySubtree && (hasMask || reflect || hasBlend || filter && (styles.filter || styles.opacity < 1)) && this.canComposite(stack.element)) {
|
|
12551
|
+
if (!await this.renderCompositedStack(stack, filter, hasMask, reflect, hasBlend)) {
|
|
12353
12552
|
this.legacySubtree = true;
|
|
12354
12553
|
try {
|
|
12355
12554
|
await this.renderStackContent(stack);
|
|
@@ -12365,7 +12564,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12365
12564
|
canComposite(paint) {
|
|
12366
12565
|
const supported = (container) => {
|
|
12367
12566
|
const styles = container.styles;
|
|
12368
|
-
return !styles.isTransformed() && styles.zoom === 1 && styles.
|
|
12567
|
+
return !styles.isTransformed() && styles.zoom === 1 && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
|
|
12369
12568
|
};
|
|
12370
12569
|
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
12371
12570
|
let ancestor = paint.parent;
|
|
@@ -12375,7 +12574,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12375
12574
|
}
|
|
12376
12575
|
return subtree(paint.container);
|
|
12377
12576
|
}
|
|
12378
|
-
async renderCompositedStack(stack, filter, hasMask, reflect) {
|
|
12577
|
+
async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
|
|
12379
12578
|
const signal = this.options.signal;
|
|
12380
12579
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
12381
12580
|
const bounds = cropSurface(surfaceBounds(stack.element.container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filter ? filterOutset(filter) : 0, this.options.scale);
|
|
@@ -12405,9 +12604,14 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12405
12604
|
if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
|
|
12406
12605
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
12407
12606
|
const own = stack.element.effects;
|
|
12408
|
-
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
|
|
12607
|
+
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect) && !isClipEffect(effect));
|
|
12409
12608
|
this.effectsRenderer.applyEffects(effects);
|
|
12410
|
-
|
|
12609
|
+
if (hasBlend) {
|
|
12610
|
+
this.ctx.save();
|
|
12611
|
+
this.ctx.globalCompositeOperation = stack.element.container.styles.mixBlendMode;
|
|
12612
|
+
this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
|
|
12613
|
+
this.ctx.restore();
|
|
12614
|
+
} else this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
|
|
12411
12615
|
if (reflect) await this.drawReflection(composited, stack.element.container.bounds, bounds, reflect);
|
|
12412
12616
|
return true;
|
|
12413
12617
|
} catch (error) {
|
|
@@ -12497,6 +12701,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12497
12701
|
maskCanvas.height = h;
|
|
12498
12702
|
const mctx = maskCanvas.getContext("2d");
|
|
12499
12703
|
if (mctx) {
|
|
12704
|
+
mctx.scale(scale, scale);
|
|
12500
12705
|
await paintMaskLayers(mctx, [reflect.mask], {
|
|
12501
12706
|
maskPosition: [],
|
|
12502
12707
|
maskRepeat: [],
|
|
@@ -12517,7 +12722,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12517
12722
|
ctx.scale(1, -1);
|
|
12518
12723
|
}
|
|
12519
12724
|
ctx.globalCompositeOperation = "destination-in";
|
|
12520
|
-
ctx.drawImage(maskCanvas, 0, 0);
|
|
12725
|
+
ctx.drawImage(maskCanvas, 0, 0, w, h);
|
|
12521
12726
|
} finally {
|
|
12522
12727
|
ctx.restore();
|
|
12523
12728
|
}
|
|
@@ -12729,26 +12934,27 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12729
12934
|
}
|
|
12730
12935
|
if (styles.borderImageSource) {
|
|
12731
12936
|
const source = styles.borderImageSource;
|
|
12937
|
+
const bounds = paint.container.bounds;
|
|
12938
|
+
const borderWidths = [
|
|
12939
|
+
Math.max(0, styles.borderTopWidth),
|
|
12940
|
+
Math.max(0, styles.borderRightWidth),
|
|
12941
|
+
Math.max(0, styles.borderBottomWidth),
|
|
12942
|
+
Math.max(0, styles.borderLeftWidth)
|
|
12943
|
+
];
|
|
12944
|
+
let image = null;
|
|
12732
12945
|
if (source.type === 0) {
|
|
12733
12946
|
const url = source.url;
|
|
12734
12947
|
try {
|
|
12735
|
-
|
|
12736
|
-
if (image) {
|
|
12737
|
-
const bounds = paint.container.bounds;
|
|
12738
|
-
const borderWidths = [
|
|
12739
|
-
Math.max(0, styles.borderTopWidth),
|
|
12740
|
-
Math.max(0, styles.borderRightWidth),
|
|
12741
|
-
Math.max(0, styles.borderBottomWidth),
|
|
12742
|
-
Math.max(0, styles.borderLeftWidth)
|
|
12743
|
-
];
|
|
12744
|
-
const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
|
|
12745
|
-
const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
|
|
12746
|
-
const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
|
|
12747
|
-
this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
|
|
12748
|
-
}
|
|
12948
|
+
image = await this.context.cache.match(url) ?? null;
|
|
12749
12949
|
} catch (e) {
|
|
12750
12950
|
this.context.logger.error(`Error loading border-image ${url}`);
|
|
12751
12951
|
}
|
|
12952
|
+
} else image = this.rasterizeGradientSource(source, bounds);
|
|
12953
|
+
if (image) {
|
|
12954
|
+
const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
|
|
12955
|
+
const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
|
|
12956
|
+
const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
|
|
12957
|
+
this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
|
|
12752
12958
|
}
|
|
12753
12959
|
return;
|
|
12754
12960
|
}
|
|
@@ -12816,6 +13022,91 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12816
13022
|
}
|
|
12817
13023
|
}
|
|
12818
13024
|
/**
|
|
13025
|
+
* Rasterise a gradient border-image-source across the border image area.
|
|
13026
|
+
* CSS allows any <image> as the border-image source; gradients are painted
|
|
13027
|
+
* over the whole border-image box and then sliced by the regular 9-slice
|
|
13028
|
+
* pipeline, matching browser behaviour for `border-image: linear-gradient()`.
|
|
13029
|
+
* Returns null when the gradient could not be painted.
|
|
13030
|
+
*/
|
|
13031
|
+
rasterizeGradientSource(source, bounds) {
|
|
13032
|
+
const width = Math.max(1, Math.ceil(bounds.width));
|
|
13033
|
+
const height = Math.max(1, Math.ceil(bounds.height));
|
|
13034
|
+
const canvas = (this.ctx.canvas.ownerDocument ?? document).createElement("canvas");
|
|
13035
|
+
canvas.width = width;
|
|
13036
|
+
canvas.height = height;
|
|
13037
|
+
const ctx = canvas.getContext("2d");
|
|
13038
|
+
if (!ctx) return null;
|
|
13039
|
+
if (source.type === 1 || source.type === 3) {
|
|
13040
|
+
const gradientImage = source;
|
|
13041
|
+
const [lineLength, x0, y0, x1, y1] = calculateGradientDirection(gradientImage.angle, width, height);
|
|
13042
|
+
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
13043
|
+
processColorStops(gradientImage.stops, lineLength || width).forEach((colorStop) => {
|
|
13044
|
+
gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
|
|
13045
|
+
});
|
|
13046
|
+
ctx.fillStyle = gradient;
|
|
13047
|
+
ctx.fillRect(0, 0, width, height);
|
|
13048
|
+
return canvas;
|
|
13049
|
+
}
|
|
13050
|
+
if (source.type === 2 || source.type === 4) {
|
|
13051
|
+
const gradientImage = source;
|
|
13052
|
+
const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
|
|
13053
|
+
const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
|
|
13054
|
+
const [rx, ry] = calculateRadius(gradientImage, cx, cy, width, height);
|
|
13055
|
+
if (rx <= 0 || ry <= 0) return null;
|
|
13056
|
+
ctx.save();
|
|
13057
|
+
ctx.translate(cx, cy);
|
|
13058
|
+
ctx.scale(1, ry / rx);
|
|
13059
|
+
ctx.translate(-cx, -cy);
|
|
13060
|
+
const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, rx);
|
|
13061
|
+
processColorStops(gradientImage.stops, rx).forEach((colorStop) => {
|
|
13062
|
+
gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
|
|
13063
|
+
});
|
|
13064
|
+
ctx.fillStyle = gradient;
|
|
13065
|
+
ctx.fillRect(0, 0, width, height);
|
|
13066
|
+
ctx.restore();
|
|
13067
|
+
return canvas;
|
|
13068
|
+
}
|
|
13069
|
+
if (source.type === 5 || source.type === 6) {
|
|
13070
|
+
const gradientImage = source;
|
|
13071
|
+
const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
|
|
13072
|
+
const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
|
|
13073
|
+
const gradient = ctx.createConicGradient(gradientImage.angle - Math.PI / 2, cx, cy);
|
|
13074
|
+
let stops;
|
|
13075
|
+
if (source.type === 6) {
|
|
13076
|
+
const processed = processColorStops(gradientImage.stops, 1);
|
|
13077
|
+
const first = at(processed, 0);
|
|
13078
|
+
const last = at(processed, processed.length - 1);
|
|
13079
|
+
const period = Math.max(last.stop - first.stop, .01);
|
|
13080
|
+
stops = [];
|
|
13081
|
+
const cycles = Math.ceil(1 / period);
|
|
13082
|
+
for (let k = 0; k < cycles; k++) for (const s of processed) {
|
|
13083
|
+
const stop = k * period + (s.stop - first.stop);
|
|
13084
|
+
if (stop > 1) break;
|
|
13085
|
+
stops.push({
|
|
13086
|
+
stop,
|
|
13087
|
+
color: asString(s.color)
|
|
13088
|
+
});
|
|
13089
|
+
}
|
|
13090
|
+
const lastIncluded = at(stops, stops.length - 1);
|
|
13091
|
+
if (lastIncluded && lastIncluded.stop < 1) stops.push({
|
|
13092
|
+
stop: 1,
|
|
13093
|
+
color: lastIncluded.color
|
|
13094
|
+
});
|
|
13095
|
+
} else stops = processColorStops(gradientImage.stops, Math.max(width, height)).map((colorStop) => ({
|
|
13096
|
+
stop: colorStop.stop,
|
|
13097
|
+
color: asString(colorStop.color)
|
|
13098
|
+
}));
|
|
13099
|
+
stops.forEach((s) => {
|
|
13100
|
+
gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
|
|
13101
|
+
});
|
|
13102
|
+
ctx.fillStyle = gradient;
|
|
13103
|
+
ctx.fillRect(0, 0, width, height);
|
|
13104
|
+
return canvas;
|
|
13105
|
+
}
|
|
13106
|
+
this.context.logger.warn("Unsupported border-image source; only URL and gradient images are supported");
|
|
13107
|
+
return null;
|
|
13108
|
+
}
|
|
13109
|
+
/**
|
|
12819
13110
|
* Filter the already-painted content beneath the element's border box and
|
|
12820
13111
|
* paint the result back, approximating CSS backdrop-filter for the
|
|
12821
13112
|
* blur/drop-shadow subset supported by parseSimpleFilter.
|
|
@@ -12919,9 +13210,6 @@ var ForeignObjectRenderer = class {
|
|
|
12919
13210
|
//#endregion
|
|
12920
13211
|
//#region src/core/logger.ts
|
|
12921
13212
|
var Logger = class {
|
|
12922
|
-
static {
|
|
12923
|
-
this.instances = {};
|
|
12924
|
-
}
|
|
12925
13213
|
constructor({ id, enabled }) {
|
|
12926
13214
|
this.id = id;
|
|
12927
13215
|
this.enabled = enabled;
|
|
@@ -12957,6 +13245,9 @@ var Logger = class {
|
|
|
12957
13245
|
//#endregion
|
|
12958
13246
|
//#region src/core/cache-storage.ts
|
|
12959
13247
|
var Cache = class {
|
|
13248
|
+
get _evictionEnabled() {
|
|
13249
|
+
return this._evictionSuspendCount === 0;
|
|
13250
|
+
}
|
|
12960
13251
|
constructor(context, _options) {
|
|
12961
13252
|
this.context = context;
|
|
12962
13253
|
this._options = _options;
|
|
@@ -12964,6 +13255,7 @@ var Cache = class {
|
|
|
12964
13255
|
this._pendingOperations = /* @__PURE__ */ new Map();
|
|
12965
13256
|
this._deferMode = false;
|
|
12966
13257
|
this._collectedUrls = /* @__PURE__ */ new Set();
|
|
13258
|
+
this._evictionSuspendCount = 0;
|
|
12967
13259
|
this.maxSize = _options.maxCacheSize ?? 100;
|
|
12968
13260
|
if (this.maxSize < 1) throw new Error("Cache maxSize must be at least 1");
|
|
12969
13261
|
if (this.maxSize > 1e4) this.context.logger.warn(`Cache maxSize ${this.maxSize} is very large and may cause memory issues. Consider using a smaller value (recommended: 100-1000).`);
|
|
@@ -12974,6 +13266,7 @@ var Cache = class {
|
|
|
12974
13266
|
*/
|
|
12975
13267
|
startDefer() {
|
|
12976
13268
|
this._deferMode = true;
|
|
13269
|
+
this._evictionSuspendCount++;
|
|
12977
13270
|
}
|
|
12978
13271
|
/**
|
|
12979
13272
|
* Exit defer mode and load all collected URLs in parallel, respecting
|
|
@@ -12991,17 +13284,21 @@ var Cache = class {
|
|
|
12991
13284
|
this._deferMode = false;
|
|
12992
13285
|
const urls = Array.from(this._collectedUrls);
|
|
12993
13286
|
this._collectedUrls.clear();
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13287
|
+
try {
|
|
13288
|
+
if (urls.length === 0) return;
|
|
13289
|
+
const limit = Math.max(1, Math.min(concurrency, 100));
|
|
13290
|
+
this.context.logger.debug(`Preloading ${urls.length} image(s) with concurrency ${limit}`);
|
|
13291
|
+
let settled = 0;
|
|
13292
|
+
onProgress?.(0, urls.length);
|
|
13293
|
+
for (let i = 0; i < urls.length; i += limit) {
|
|
13294
|
+
const batch = urls.slice(i, i + limit);
|
|
13295
|
+
const operations = batch.map((url) => this._addImageWithPending(url));
|
|
13296
|
+
await Promise.allSettled(operations);
|
|
13297
|
+
settled += batch.length;
|
|
13298
|
+
onProgress?.(settled, urls.length);
|
|
13299
|
+
}
|
|
13300
|
+
} finally {
|
|
13301
|
+
this._evictionSuspendCount = Math.max(0, this._evictionSuspendCount - 1);
|
|
13005
13302
|
}
|
|
13006
13303
|
}
|
|
13007
13304
|
addImage(src) {
|
|
@@ -13068,7 +13365,7 @@ var Cache = class {
|
|
|
13068
13365
|
*/
|
|
13069
13366
|
set(key, value) {
|
|
13070
13367
|
if (this._cache.has(key)) this._cache.delete(key);
|
|
13071
|
-
else if (this._cache.size >= this.maxSize) {
|
|
13368
|
+
else if (this._evictionEnabled && this._cache.size >= this.maxSize) {
|
|
13072
13369
|
const lruKey = this._cache.keys().next().value;
|
|
13073
13370
|
if (lruKey !== void 0) {
|
|
13074
13371
|
this._cache.delete(lruKey);
|