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
package/dist/html2canvas-pro.cjs
CHANGED
|
@@ -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
|
*/
|
|
@@ -4747,6 +4747,17 @@ const conicGradient = (context, tokens) => {
|
|
|
4747
4747
|
stops
|
|
4748
4748
|
};
|
|
4749
4749
|
};
|
|
4750
|
+
/**
|
|
4751
|
+
* repeating-conic-gradient shares the conic-gradient grammar exactly; only
|
|
4752
|
+
* the image type differs, which switches the renderer to periodic stops
|
|
4753
|
+
* stacked across the full sweep.
|
|
4754
|
+
*/
|
|
4755
|
+
const repeatingConicGradient = (context, tokens) => {
|
|
4756
|
+
return {
|
|
4757
|
+
...conicGradient(context, tokens),
|
|
4758
|
+
type: 6
|
|
4759
|
+
};
|
|
4760
|
+
};
|
|
4750
4761
|
//#endregion
|
|
4751
4762
|
//#region src/css/types/functions/image-set.ts
|
|
4752
4763
|
const parseResolution = (token) => {
|
|
@@ -4799,6 +4810,9 @@ const isRepeatingRadialGradient = (background) => {
|
|
|
4799
4810
|
const isConicGradient = (background) => {
|
|
4800
4811
|
return background.type === 5;
|
|
4801
4812
|
};
|
|
4813
|
+
const isRepeatingConicGradient = (background) => {
|
|
4814
|
+
return background.type === 6;
|
|
4815
|
+
};
|
|
4802
4816
|
const image = {
|
|
4803
4817
|
name: "image",
|
|
4804
4818
|
parse: (context, value) => {
|
|
@@ -4844,6 +4858,9 @@ const SUPPORTED_IMAGE_FUNCTIONS = {
|
|
|
4844
4858
|
"conic-gradient": conicGradient,
|
|
4845
4859
|
"-webkit-conic-gradient": conicGradient,
|
|
4846
4860
|
"-moz-conic-gradient": conicGradient,
|
|
4861
|
+
"repeating-conic-gradient": repeatingConicGradient,
|
|
4862
|
+
"-webkit-repeating-conic-gradient": repeatingConicGradient,
|
|
4863
|
+
"-moz-repeating-conic-gradient": repeatingConicGradient,
|
|
4847
4864
|
"image-set": imageSet,
|
|
4848
4865
|
"-webkit-image-set": imageSet
|
|
4849
4866
|
};
|
|
@@ -5332,6 +5349,7 @@ const listStyleImage = {
|
|
|
5332
5349
|
skipCache: true,
|
|
5333
5350
|
parse: (context, token) => {
|
|
5334
5351
|
if (token.type === 20 && token.value === "none") return null;
|
|
5352
|
+
if (!isSupportedImage(token)) return null;
|
|
5335
5353
|
return image.parse(context, token);
|
|
5336
5354
|
}
|
|
5337
5355
|
};
|
|
@@ -7776,9 +7794,12 @@ var PseudoContentResolver = class {
|
|
|
7776
7794
|
case "open-quote":
|
|
7777
7795
|
anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth++, true)));
|
|
7778
7796
|
break;
|
|
7779
|
-
case "close-quote":
|
|
7780
|
-
|
|
7797
|
+
case "close-quote": {
|
|
7798
|
+
const hasOpenQuote = this.quoteDepth > 0;
|
|
7799
|
+
this.quoteDepth = Math.max(0, this.quoteDepth - 1);
|
|
7800
|
+
if (hasOpenQuote) anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth, false)));
|
|
7781
7801
|
break;
|
|
7802
|
+
}
|
|
7782
7803
|
default: anonymousReplacedElement.appendChild(document.createTextNode(token.value));
|
|
7783
7804
|
}
|
|
7784
7805
|
});
|
|
@@ -8120,12 +8141,14 @@ var DocumentCloner = class {
|
|
|
8120
8141
|
const styleBefore = window.getComputedStyle(node, ":before");
|
|
8121
8142
|
const before = this.pseudoContents.resolvePseudoContent(node, clone, styleBefore, 0);
|
|
8122
8143
|
if (before) clone.insertBefore(before, clone.firstChild);
|
|
8144
|
+
}
|
|
8145
|
+
if (isCustomElement(node)) copyStyles = true;
|
|
8146
|
+
if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
|
|
8147
|
+
if (checkPseudoElements) {
|
|
8123
8148
|
const styleAfter = window.getComputedStyle(node, ":after");
|
|
8124
8149
|
const after = this.pseudoContents.resolvePseudoContent(node, clone, styleAfter, 1);
|
|
8125
8150
|
if (after) clone.appendChild(after);
|
|
8126
8151
|
}
|
|
8127
|
-
if (isCustomElement(node)) copyStyles = true;
|
|
8128
|
-
if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
|
|
8129
8152
|
this.counters.pop(counters);
|
|
8130
8153
|
if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node) || copyStyles) copyCSSStyles(style, clone);
|
|
8131
8154
|
if (node.scrollTop !== 0 || node.scrollLeft !== 0) this.scrolledElements.push([
|
|
@@ -9021,10 +9044,10 @@ const LIST_OWNERS = [
|
|
|
9021
9044
|
"UL",
|
|
9022
9045
|
"MENU"
|
|
9023
9046
|
];
|
|
9024
|
-
const parseNodeTree = (context, node, parent, root) => {
|
|
9047
|
+
const parseNodeTree = (context, node, parent, root, zoomScale = 1) => {
|
|
9025
9048
|
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
|
|
9026
9049
|
nextNode = childNode.nextSibling;
|
|
9027
|
-
parseChildNode(context, childNode, parent, root);
|
|
9050
|
+
parseChildNode(context, childNode, parent, root, zoomScale);
|
|
9028
9051
|
}
|
|
9029
9052
|
};
|
|
9030
9053
|
/**
|
|
@@ -9035,19 +9058,29 @@ const parseNodeTree = (context, node, parent, root) => {
|
|
|
9035
9058
|
* this, slotted shadow-DOM content such as web component labels was silently
|
|
9036
9059
|
* dropped because `parseNodeTree` only iterates `node.firstChild` (issue #226).
|
|
9037
9060
|
*/
|
|
9038
|
-
const parseChildNode = (context, childNode, parent, root) => {
|
|
9039
|
-
if (isTextNode(childNode) && childNode.data.length > 0)
|
|
9040
|
-
|
|
9041
|
-
if (
|
|
9061
|
+
const parseChildNode = (context, childNode, parent, root, zoomScale = 1) => {
|
|
9062
|
+
if (isTextNode(childNode) && childNode.data.length > 0) {
|
|
9063
|
+
const textContainer = new TextContainer(context, childNode, parent.styles);
|
|
9064
|
+
if (zoomScale !== 1) {
|
|
9065
|
+
const originLeft = parent.bounds.left;
|
|
9066
|
+
const originTop = parent.bounds.top;
|
|
9067
|
+
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)));
|
|
9068
|
+
}
|
|
9069
|
+
parent.textNodes.push(textContainer);
|
|
9070
|
+
} else if (isElementNode(childNode)) {
|
|
9071
|
+
if (isSlotElement(childNode) && childNode.assignedNodes) childNode.assignedNodes().forEach((assignedNode) => parseChildNode(context, assignedNode, parent, root, zoomScale));
|
|
9042
9072
|
else {
|
|
9043
9073
|
const container = createContainer(context, childNode);
|
|
9074
|
+
const ownZoom = container.styles.zoom || 1;
|
|
9075
|
+
if (ownZoom !== 1) container.bounds = new Bounds(container.bounds.left, container.bounds.top, container.bounds.width / ownZoom, container.bounds.height / ownZoom);
|
|
9044
9076
|
if (container.styles.isVisible()) {
|
|
9045
9077
|
if (createsRealStackingContext(childNode, container, root)) container.createsRealStackingContext = true;
|
|
9046
9078
|
else if (createsStackingContext(container.styles)) container.createsStackingContext = true;
|
|
9047
9079
|
if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) container.isListOwner = true;
|
|
9048
9080
|
parent.elements.push(container);
|
|
9049
|
-
|
|
9050
|
-
|
|
9081
|
+
const childZoom = zoomScale * (container.styles.zoom || 1);
|
|
9082
|
+
if (childNode.shadowRoot) parseNodeTree(context, childNode.shadowRoot, container, root, childZoom);
|
|
9083
|
+
else if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) parseNodeTree(context, childNode, container, root, childZoom);
|
|
9051
9084
|
}
|
|
9052
9085
|
}
|
|
9053
9086
|
}
|
|
@@ -9075,6 +9108,7 @@ const createsRealStackingContext = (node, container, root) => {
|
|
|
9075
9108
|
};
|
|
9076
9109
|
const createsStackingContext = (styles) => {
|
|
9077
9110
|
if (styles.isPositioned() || styles.isFloating()) return true;
|
|
9111
|
+
if (styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) return true;
|
|
9078
9112
|
return contains(styles.display, 268435456) || contains(styles.display, 33554432) || contains(styles.display, 536870912) || contains(styles.display, 134217728);
|
|
9079
9113
|
};
|
|
9080
9114
|
//#endregion
|
|
@@ -9494,9 +9528,8 @@ var ElementPaint = class {
|
|
|
9494
9528
|
if (this.container.styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) this.effects.push(new BlendEffect(this.container.styles.mixBlendMode));
|
|
9495
9529
|
if (this.container.styles.filter !== null) this.effects.push(new FilterEffect(this.container.styles.filter));
|
|
9496
9530
|
if (this.container.styles.zoom !== 1) {
|
|
9497
|
-
const
|
|
9498
|
-
const
|
|
9499
|
-
const offsetY = this.container.bounds.top + getAbsoluteValue(origin[1], this.container.bounds.height);
|
|
9531
|
+
const offsetX = this.container.bounds.left;
|
|
9532
|
+
const offsetY = this.container.bounds.top;
|
|
9500
9533
|
const z = this.container.styles.zoom;
|
|
9501
9534
|
const zoomMatrix = [
|
|
9502
9535
|
z,
|
|
@@ -9528,7 +9561,7 @@ var ElementPaint = class {
|
|
|
9528
9561
|
computeEffects(target, surfaceRoot) {
|
|
9529
9562
|
let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
|
|
9530
9563
|
let parent = this === surfaceRoot ? null : this.parent;
|
|
9531
|
-
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
|
|
9564
|
+
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect));
|
|
9532
9565
|
const effects = sourceEffects(this);
|
|
9533
9566
|
while (parent) {
|
|
9534
9567
|
const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
|
|
@@ -10421,7 +10454,7 @@ var TextRenderer = class {
|
|
|
10421
10454
|
this.ctx.shadowColor = asString(textShadow.color);
|
|
10422
10455
|
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
|
10423
10456
|
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
|
10424
|
-
this.ctx.shadowBlur = textShadow.blur.number;
|
|
10457
|
+
this.ctx.shadowBlur = textShadow.blur.number * this.options.scale;
|
|
10425
10458
|
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
|
|
10426
10459
|
});
|
|
10427
10460
|
this.ctx.shadowColor = "";
|
|
@@ -10571,7 +10604,7 @@ var TextRenderer = class {
|
|
|
10571
10604
|
this.ctx.textBaseline = "alphabetic";
|
|
10572
10605
|
const paintOrder = styles.paintOrder;
|
|
10573
10606
|
const baseline = this.measureBaselineCached(fontString ?? "", styles.fontSize.number);
|
|
10574
|
-
if (styles.webkitLineClamp > 0 && (styles.display &
|
|
10607
|
+
if (styles.webkitLineClamp > 0 && (styles.display & 34) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
|
|
10575
10608
|
if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
|
|
10576
10609
|
}
|
|
10577
10610
|
if (styles.textOverflow === 1 && containerBounds && styles.overflowX === 1 && text.textBounds.length > 0 && this.renderEllipsisText(text, styles, paintOrder, baseline, containerBounds)) return;
|
|
@@ -10770,6 +10803,7 @@ var BackgroundRenderer = class {
|
|
|
10770
10803
|
else if (isRadialGradient(backgroundImage)) this.renderRadialGradient(container, backgroundImage, index);
|
|
10771
10804
|
else if (isRepeatingRadialGradient(backgroundImage)) this.renderRepeatingRadialGradient(container, backgroundImage, index);
|
|
10772
10805
|
else if (isConicGradient(backgroundImage)) this.renderConicGradient(container, backgroundImage, index);
|
|
10806
|
+
else if (isRepeatingConicGradient(backgroundImage)) this.renderRepeatingConicGradient(container, backgroundImage, index);
|
|
10773
10807
|
if (layerCount > 0) {
|
|
10774
10808
|
if ((blendModes[layerCount] ?? blendModes[0] ?? "normal") !== "normal") this.ctx.restore();
|
|
10775
10809
|
}
|
|
@@ -10820,45 +10854,43 @@ var BackgroundRenderer = class {
|
|
|
10820
10854
|
null,
|
|
10821
10855
|
null
|
|
10822
10856
|
]);
|
|
10823
|
-
const [lineLength,
|
|
10857
|
+
const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(backgroundImage.angle, width, height);
|
|
10858
|
+
const gx0 = x + lx0;
|
|
10859
|
+
const gx1 = x + lx1;
|
|
10860
|
+
const gy0 = y + ly0;
|
|
10861
|
+
const gy1 = y + ly1;
|
|
10824
10862
|
const processedStops = processColorStops(backgroundImage.stops, lineLength || 1);
|
|
10825
10863
|
const lastStop = at(processedStops, processedStops.length - 1);
|
|
10826
10864
|
const firstStop = at(processedStops, 0);
|
|
10827
|
-
const
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10865
|
+
const period = Math.max(lastStop.stop - firstStop.stop, .01);
|
|
10866
|
+
if (lastStop.stop - firstStop.stop < 1e-6) {
|
|
10867
|
+
const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
|
|
10868
|
+
gradient.addColorStop(0, asString(lastStop.color));
|
|
10869
|
+
gradient.addColorStop(1, asString(lastStop.color));
|
|
10870
|
+
this.path(path);
|
|
10871
|
+
this.ctx.fillStyle = gradient;
|
|
10872
|
+
this.ctx.fill();
|
|
10831
10873
|
return;
|
|
10832
10874
|
}
|
|
10833
|
-
const
|
|
10834
|
-
const
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
const cacheKey = `rlg|${backgroundImage.angle}|${Math.round(patternLength)}|${JSON.stringify(backgroundImage.stops)}`;
|
|
10842
|
-
let pattern = this.patternCache.get(cacheKey);
|
|
10843
|
-
if (!pattern) {
|
|
10844
|
-
const canvas = ownerDocument.createElement("canvas");
|
|
10845
|
-
const canvasSize = Math.max(1, Math.ceil(patternLength));
|
|
10846
|
-
canvas.width = canvasSize;
|
|
10847
|
-
canvas.height = canvasSize;
|
|
10848
|
-
const ctx = canvas.getContext("2d");
|
|
10849
|
-
if (!ctx) return;
|
|
10850
|
-
const gradient = ctx.createLinearGradient(pX0 - x, pY0 - y, pX1 - x, pY1 - y);
|
|
10851
|
-
processedStops.forEach((colorStop) => {
|
|
10852
|
-
gradient.addColorStop((colorStop.stop - firstStop.stop) / stopSpan, asString(colorStop.color));
|
|
10875
|
+
const stops = [];
|
|
10876
|
+
const cycles = Math.ceil(1 / period);
|
|
10877
|
+
for (let k = 0; k < cycles; k++) for (const s of processedStops) {
|
|
10878
|
+
const stop = k * period + (s.stop - firstStop.stop);
|
|
10879
|
+
if (stop > 1) break;
|
|
10880
|
+
stops.push({
|
|
10881
|
+
stop,
|
|
10882
|
+
color: asString(s.color)
|
|
10853
10883
|
});
|
|
10854
|
-
ctx.fillStyle = gradient;
|
|
10855
|
-
ctx.fillRect(0, 0, canvasSize, canvasSize);
|
|
10856
|
-
if (canvasSize > 0) {
|
|
10857
|
-
pattern = this.ctx.createPattern(canvas, "repeat");
|
|
10858
|
-
this.patternCache.set(cacheKey, pattern);
|
|
10859
|
-
}
|
|
10860
10884
|
}
|
|
10861
|
-
|
|
10885
|
+
stops.push({
|
|
10886
|
+
stop: 1,
|
|
10887
|
+
color: asString(lastStop.color)
|
|
10888
|
+
});
|
|
10889
|
+
const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
|
|
10890
|
+
stops.forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color));
|
|
10891
|
+
this.path(path);
|
|
10892
|
+
this.ctx.fillStyle = gradient;
|
|
10893
|
+
this.ctx.fill();
|
|
10862
10894
|
}
|
|
10863
10895
|
/**
|
|
10864
10896
|
* Render a linear gradient background
|
|
@@ -10908,7 +10940,7 @@ var BackgroundRenderer = class {
|
|
|
10908
10940
|
}
|
|
10909
10941
|
if (rx > 0 && ry > 0) {
|
|
10910
10942
|
const cacheKey = `rg|${Math.round(x)}x${Math.round(y)}|${Math.round(rx)}x${Math.round(ry)}|${JSON.stringify(backgroundImage.stops)}`;
|
|
10911
|
-
const stops = processColorStops(backgroundImage.stops, rx
|
|
10943
|
+
const stops = processColorStops(backgroundImage.stops, rx);
|
|
10912
10944
|
const lastStop = stops[stops.length - 1];
|
|
10913
10945
|
let pattern = this.patternCache.get(cacheKey);
|
|
10914
10946
|
if (!pattern) {
|
|
@@ -10965,14 +10997,14 @@ var BackgroundRenderer = class {
|
|
|
10965
10997
|
ry = Math.max(ry, .01);
|
|
10966
10998
|
}
|
|
10967
10999
|
if (!(rx > 0 && ry > 0)) return;
|
|
10968
|
-
const processed = processColorStops(backgroundImage.stops, rx
|
|
11000
|
+
const processed = processColorStops(backgroundImage.stops, rx);
|
|
10969
11001
|
const firstStop = at(processed, 0);
|
|
10970
11002
|
const lastStop = at(processed, processed.length - 1);
|
|
10971
11003
|
const period = Math.max(lastStop.stop - firstStop.stop, .01);
|
|
10972
11004
|
const stops = [];
|
|
10973
|
-
const rings = Math.ceil(
|
|
11005
|
+
const rings = Math.ceil(1 / period);
|
|
10974
11006
|
for (let k = 0; k < rings; k++) for (const s of processed) {
|
|
10975
|
-
const stop =
|
|
11007
|
+
const stop = k * period + (s.stop - firstStop.stop);
|
|
10976
11008
|
if (stop > 1) break;
|
|
10977
11009
|
stops.push({
|
|
10978
11010
|
stop,
|
|
@@ -11039,6 +11071,65 @@ var BackgroundRenderer = class {
|
|
|
11039
11071
|
this.ctx.restore();
|
|
11040
11072
|
}
|
|
11041
11073
|
/**
|
|
11074
|
+
* Render a repeating conic gradient background.
|
|
11075
|
+
*
|
|
11076
|
+
* Canvas conic gradient stops span the full 2π sweep as [0, 1], so the
|
|
11077
|
+
* one-cycle stop list is stacked k·period + s for k = 0..cycles-1 over the
|
|
11078
|
+
* whole sweep — the same periodic-stop stacking the repeating linear and
|
|
11079
|
+
* radial renderers use along their gradient lines.
|
|
11080
|
+
*/
|
|
11081
|
+
renderRepeatingConicGradient(container, backgroundImage, index) {
|
|
11082
|
+
const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [
|
|
11083
|
+
null,
|
|
11084
|
+
null,
|
|
11085
|
+
null
|
|
11086
|
+
]);
|
|
11087
|
+
const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
|
|
11088
|
+
const cx = left + getAbsoluteValue(at(position, 0), width);
|
|
11089
|
+
const cy = top + getAbsoluteValue(at(position, position.length - 1), height);
|
|
11090
|
+
const processedStops = processColorStops(backgroundImage.stops, 1);
|
|
11091
|
+
const firstStop = at(processedStops, 0);
|
|
11092
|
+
const lastStop = at(processedStops, processedStops.length - 1);
|
|
11093
|
+
const period = Math.max(lastStop.stop - firstStop.stop, .01);
|
|
11094
|
+
if (lastStop.stop - firstStop.stop < 1e-6) {
|
|
11095
|
+
const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
|
|
11096
|
+
gradient.addColorStop(0, asString(lastStop.color));
|
|
11097
|
+
gradient.addColorStop(1, asString(lastStop.color));
|
|
11098
|
+
this.path(path);
|
|
11099
|
+
this.ctx.save();
|
|
11100
|
+
this.ctx.clip();
|
|
11101
|
+
this.ctx.fillStyle = gradient;
|
|
11102
|
+
this.ctx.fill();
|
|
11103
|
+
this.ctx.restore();
|
|
11104
|
+
return;
|
|
11105
|
+
}
|
|
11106
|
+
const stops = [];
|
|
11107
|
+
const cycles = Math.ceil(1 / period);
|
|
11108
|
+
for (let k = 0; k < cycles; k++) for (const s of processedStops) {
|
|
11109
|
+
const stop = k * period + (s.stop - firstStop.stop);
|
|
11110
|
+
if (stop > 1) break;
|
|
11111
|
+
stops.push({
|
|
11112
|
+
stop,
|
|
11113
|
+
color: asString(s.color)
|
|
11114
|
+
});
|
|
11115
|
+
}
|
|
11116
|
+
const lastIncluded = at(stops, stops.length - 1);
|
|
11117
|
+
if (lastIncluded && lastIncluded.stop < 1) stops.push({
|
|
11118
|
+
stop: 1,
|
|
11119
|
+
color: lastIncluded.color
|
|
11120
|
+
});
|
|
11121
|
+
const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
|
|
11122
|
+
stops.forEach((s) => {
|
|
11123
|
+
gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
|
|
11124
|
+
});
|
|
11125
|
+
this.path(path);
|
|
11126
|
+
this.ctx.save();
|
|
11127
|
+
this.ctx.clip();
|
|
11128
|
+
this.ctx.fillStyle = gradient;
|
|
11129
|
+
this.ctx.fill();
|
|
11130
|
+
this.ctx.restore();
|
|
11131
|
+
}
|
|
11132
|
+
/**
|
|
11042
11133
|
* Render a repeating pattern with offset
|
|
11043
11134
|
*
|
|
11044
11135
|
* @param path - Path to fill
|
|
@@ -11463,6 +11554,13 @@ var BorderImageRenderer = class {
|
|
|
11463
11554
|
* - Filter effects (CSS filter functions)
|
|
11464
11555
|
*/
|
|
11465
11556
|
/**
|
|
11557
|
+
* Scale `blur(<n>px)` radii in a ctx.filter string by the device-pixel
|
|
11558
|
+
* ratio. Other filter functions (brightness, saturate, …) take
|
|
11559
|
+
* unitless values and are unaffected.
|
|
11560
|
+
*/
|
|
11561
|
+
const BLUR_PX_RE = /blur\((\d+(?:\.\d+)?)px\)/gi;
|
|
11562
|
+
const scaleFilterBlur = (filter, scale) => scale === 1 ? filter : filter.replace(BLUR_PX_RE, (_match, value) => `blur(${parseFloat(value) * scale}px)`);
|
|
11563
|
+
/**
|
|
11466
11564
|
* Effects Renderer
|
|
11467
11565
|
*
|
|
11468
11566
|
* Manages rendering effects stack including opacity, transforms, and clipping.
|
|
@@ -11500,6 +11598,7 @@ var EffectsRenderer = class {
|
|
|
11500
11598
|
this.saveAtDepth = -1;
|
|
11501
11599
|
this.ctx = deps.ctx;
|
|
11502
11600
|
this.pathCallback = pathCallback;
|
|
11601
|
+
this.scale = deps.scale ?? 1;
|
|
11503
11602
|
}
|
|
11504
11603
|
/**
|
|
11505
11604
|
* Apply multiple effects.
|
|
@@ -11538,11 +11637,11 @@ var EffectsRenderer = class {
|
|
|
11538
11637
|
else if (isOpacityEffect(effect)) this.ctx.globalAlpha *= effect.opacity;
|
|
11539
11638
|
else if (isBlendEffect(effect)) this.ctx.globalCompositeOperation = effect.compositeOperation;
|
|
11540
11639
|
else if (isFilterEffect(effect)) {
|
|
11541
|
-
this.ctx.filter = effect.safeFilterString || "none";
|
|
11640
|
+
this.ctx.filter = scaleFilterBlur(effect.safeFilterString, this.scale) || "none";
|
|
11542
11641
|
if (effect.shadow) {
|
|
11543
|
-
this.ctx.shadowOffsetX = effect.shadow.offsetX;
|
|
11544
|
-
this.ctx.shadowOffsetY = effect.shadow.offsetY;
|
|
11545
|
-
this.ctx.shadowBlur = effect.shadow.blur;
|
|
11642
|
+
this.ctx.shadowOffsetX = effect.shadow.offsetX * this.scale;
|
|
11643
|
+
this.ctx.shadowOffsetY = effect.shadow.offsetY * this.scale;
|
|
11644
|
+
this.ctx.shadowBlur = effect.shadow.blur * this.scale;
|
|
11546
11645
|
this.ctx.shadowColor = effect.shadow.color;
|
|
11547
11646
|
}
|
|
11548
11647
|
}
|
|
@@ -11687,8 +11786,8 @@ const paintGradientLayer = (ctx, layer, area) => {
|
|
|
11687
11786
|
let gradient = null;
|
|
11688
11787
|
if (layer.type === 1 || layer.type === 3) {
|
|
11689
11788
|
const linear = layer;
|
|
11690
|
-
const [lineLength,
|
|
11691
|
-
gradient = ctx.createLinearGradient(
|
|
11789
|
+
const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(linear.angle || 0, width, height);
|
|
11790
|
+
gradient = ctx.createLinearGradient(area.left + lx0, area.top + ly0, area.left + lx1, area.top + ly1);
|
|
11692
11791
|
processColorStops(linear.stops, lineLength || 1).forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), asString(s.color)));
|
|
11693
11792
|
} else if (isConicGradient(layer)) {
|
|
11694
11793
|
const conic = layer;
|
|
@@ -11849,14 +11948,14 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
|
11849
11948
|
new Vector(container.bounds.left + size * .84, container.bounds.top + size * .34085),
|
|
11850
11949
|
new Vector(container.bounds.left + size * .39363, container.bounds.top + size * .79)
|
|
11851
11950
|
]);
|
|
11852
|
-
ctx.fillStyle = asString(container.styles.accentColor ??
|
|
11951
|
+
ctx.fillStyle = asString(container.styles.accentColor ?? 7733247);
|
|
11853
11952
|
ctx.fill();
|
|
11854
11953
|
ctx.restore();
|
|
11855
11954
|
} else if (container.type === "radio" && container.checked) {
|
|
11856
11955
|
ctx.save();
|
|
11857
11956
|
ctx.beginPath();
|
|
11858
11957
|
ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
|
|
11859
|
-
ctx.fillStyle = asString(container.styles.accentColor ??
|
|
11958
|
+
ctx.fillStyle = asString(container.styles.accentColor ?? 7733247);
|
|
11860
11959
|
ctx.fill();
|
|
11861
11960
|
ctx.restore();
|
|
11862
11961
|
}
|
|
@@ -11955,22 +12054,51 @@ async function renderListMarker(ctx, context, textRenderer, paint, container, st
|
|
|
11955
12054
|
const url = img.url;
|
|
11956
12055
|
try {
|
|
11957
12056
|
const image = await context.cache.match(url);
|
|
11958
|
-
|
|
12057
|
+
const fontSize = getAbsoluteValue(styles.fontSize, 0);
|
|
12058
|
+
const naturalWidth = image?.naturalWidth || image.width;
|
|
12059
|
+
const naturalHeight = image?.naturalHeight || image.height;
|
|
12060
|
+
if (naturalWidth <= 0 || naturalHeight <= 0) return;
|
|
12061
|
+
const markerScale = Math.min(fontSize / naturalWidth, fontSize / naturalHeight);
|
|
12062
|
+
const markerWidth = naturalWidth * markerScale;
|
|
12063
|
+
const markerHeight = naturalHeight * markerScale;
|
|
12064
|
+
const markerCenterY = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
|
|
12065
|
+
ctx.drawImage(image, container.bounds.left - (markerWidth + 10), markerCenterY - markerHeight / 2, markerWidth, markerHeight);
|
|
11959
12066
|
} catch (e) {
|
|
11960
12067
|
context.logger.error(`Error loading list-style-image ${url}`);
|
|
11961
12068
|
context.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
11962
12069
|
}
|
|
11963
12070
|
}
|
|
11964
12071
|
} else if (paint.listValue && container.styles.listStyleType !== -1) {
|
|
11965
|
-
const
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
12072
|
+
const type = container.styles.listStyleType;
|
|
12073
|
+
if (type === 0 || type === 1 || type === 2) {
|
|
12074
|
+
const size = styles.fontSize.number * .45;
|
|
12075
|
+
const cx = container.bounds.left - size * .9;
|
|
12076
|
+
const cy = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
|
|
12077
|
+
ctx.save();
|
|
12078
|
+
ctx.fillStyle = asString(styles.color);
|
|
12079
|
+
ctx.beginPath();
|
|
12080
|
+
if (type === 2) ctx.rect(cx - size / 2, cy - size / 2, size, size);
|
|
12081
|
+
else {
|
|
12082
|
+
ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
|
|
12083
|
+
if (type === 1) {
|
|
12084
|
+
ctx.strokeStyle = asString(styles.color);
|
|
12085
|
+
ctx.lineWidth = size * .18;
|
|
12086
|
+
}
|
|
12087
|
+
}
|
|
12088
|
+
if (type === 1) ctx.stroke();
|
|
12089
|
+
else ctx.fill();
|
|
12090
|
+
ctx.restore();
|
|
12091
|
+
} else {
|
|
12092
|
+
const [font] = textRenderer.createFontStyle(styles);
|
|
12093
|
+
ctx.font = font ?? "";
|
|
12094
|
+
ctx.fillStyle = asString(styles.color);
|
|
12095
|
+
ctx.textBaseline = "middle";
|
|
12096
|
+
ctx.textAlign = "right";
|
|
12097
|
+
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);
|
|
12098
|
+
textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
|
|
12099
|
+
ctx.textBaseline = "bottom";
|
|
12100
|
+
ctx.textAlign = "left";
|
|
12101
|
+
}
|
|
11974
12102
|
}
|
|
11975
12103
|
}
|
|
11976
12104
|
/**
|
|
@@ -12213,31 +12341,33 @@ var TextClipRenderer = class {
|
|
|
12213
12341
|
constructor(deps) {
|
|
12214
12342
|
this.ctx = deps.ctx;
|
|
12215
12343
|
this.context = deps.context;
|
|
12344
|
+
this.scale = deps.scale;
|
|
12216
12345
|
this.createFontStyle = deps.createFontStyle;
|
|
12217
12346
|
}
|
|
12218
12347
|
async render(paint) {
|
|
12219
12348
|
const container = paint.container;
|
|
12220
12349
|
const styles = container.styles;
|
|
12221
12350
|
const bounds = container.bounds;
|
|
12351
|
+
const scale = this.scale;
|
|
12222
12352
|
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
12223
12353
|
const ownerDocument = this.ctx.canvas.ownerDocument ?? document;
|
|
12224
|
-
const offscreen = ownerDocument.createElement("canvas");
|
|
12225
12354
|
const width = Math.ceil(bounds.width);
|
|
12226
12355
|
const height = Math.ceil(bounds.height);
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
const
|
|
12230
|
-
if (!
|
|
12356
|
+
const deviceWidth = Math.ceil(bounds.width * scale);
|
|
12357
|
+
const deviceHeight = Math.ceil(bounds.height * scale);
|
|
12358
|
+
const offscreen = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
|
|
12359
|
+
if (!offscreen) return;
|
|
12360
|
+
offscreen.ctx.scale(scale, scale);
|
|
12231
12361
|
const [fontString] = this.createFontStyle(styles);
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
const baseline = measureBaseline(
|
|
12362
|
+
offscreen.ctx.font = fontString ?? "";
|
|
12363
|
+
offscreen.ctx.textBaseline = "alphabetic";
|
|
12364
|
+
offscreen.ctx.textAlign = "left";
|
|
12365
|
+
offscreen.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
12366
|
+
const baseline = measureBaseline(offscreen.ctx, styles.fontSize.number);
|
|
12237
12367
|
const bgRenderer = new BackgroundRenderer({
|
|
12238
|
-
ctx:
|
|
12368
|
+
ctx: offscreen.ctx,
|
|
12239
12369
|
context: this.context,
|
|
12240
|
-
canvas: offscreen,
|
|
12370
|
+
canvas: offscreen.canvas,
|
|
12241
12371
|
options: {
|
|
12242
12372
|
width,
|
|
12243
12373
|
height,
|
|
@@ -12245,51 +12375,115 @@ var TextClipRenderer = class {
|
|
|
12245
12375
|
}
|
|
12246
12376
|
});
|
|
12247
12377
|
if (!isTransparent(styles.backgroundColor)) {
|
|
12248
|
-
|
|
12249
|
-
|
|
12378
|
+
offscreen.ctx.fillStyle = asString(styles.backgroundColor);
|
|
12379
|
+
offscreen.ctx.fillRect(0, 0, width, height);
|
|
12250
12380
|
}
|
|
12251
|
-
|
|
12252
|
-
|
|
12381
|
+
offscreen.ctx.save();
|
|
12382
|
+
offscreen.ctx.translate(-bounds.left, -bounds.top);
|
|
12253
12383
|
await bgRenderer.renderBackgroundImage(container);
|
|
12254
|
-
|
|
12255
|
-
const
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
}
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
this.
|
|
12384
|
+
offscreen.ctx.restore();
|
|
12385
|
+
const drawOptions = {
|
|
12386
|
+
bounds,
|
|
12387
|
+
baseline,
|
|
12388
|
+
letterSpacing: styles.letterSpacing,
|
|
12389
|
+
writingMode: styles.writingMode
|
|
12390
|
+
};
|
|
12391
|
+
const mask = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, offscreen.ctx);
|
|
12392
|
+
if (!mask) return;
|
|
12393
|
+
const hasStroke = !!styles.webkitTextStrokeWidth;
|
|
12394
|
+
if (hasStroke) {
|
|
12395
|
+
mask.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
|
|
12396
|
+
mask.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
12397
|
+
mask.ctx.lineJoin = this.getTextStrokeLineJoin();
|
|
12398
|
+
}
|
|
12399
|
+
for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(mask, textBound, drawOptions, (ctx, text, x, y) => {
|
|
12400
|
+
ctx.fillText(text, x, y);
|
|
12401
|
+
if (hasStroke) ctx.strokeText(text, x, y);
|
|
12402
|
+
});
|
|
12403
|
+
offscreen.ctx.globalCompositeOperation = "destination-in";
|
|
12404
|
+
offscreen.ctx.drawImage(mask.canvas, 0, 0, width, height);
|
|
12405
|
+
const shadowCanvases = this.renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, offscreen.ctx, styles, drawOptions);
|
|
12406
|
+
if (shadowCanvases) {
|
|
12407
|
+
const composite = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
|
|
12408
|
+
if (!composite) return;
|
|
12409
|
+
for (const shadowCanvas of shadowCanvases) composite.ctx.drawImage(shadowCanvas, 0, 0);
|
|
12410
|
+
composite.ctx.drawImage(offscreen.canvas, 0, 0);
|
|
12411
|
+
this.ctx.drawImage(composite.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
|
|
12412
|
+
} else this.ctx.drawImage(offscreen.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
|
|
12413
|
+
}
|
|
12414
|
+
createLayer(ownerDocument, width, height) {
|
|
12415
|
+
const canvas = ownerDocument.createElement("canvas");
|
|
12416
|
+
canvas.width = width;
|
|
12417
|
+
canvas.height = height;
|
|
12418
|
+
const ctx = canvas.getContext("2d");
|
|
12419
|
+
if (!ctx) return null;
|
|
12420
|
+
return {
|
|
12421
|
+
canvas,
|
|
12422
|
+
ctx
|
|
12423
|
+
};
|
|
12424
|
+
}
|
|
12425
|
+
createGlyphLayer(ownerDocument, width, height, fontSource) {
|
|
12426
|
+
const layer = this.createLayer(ownerDocument, width, height);
|
|
12427
|
+
if (!layer) return null;
|
|
12428
|
+
layer.ctx.scale(this.scale, this.scale);
|
|
12429
|
+
layer.ctx.font = fontSource.font;
|
|
12430
|
+
layer.ctx.textBaseline = fontSource.textBaseline;
|
|
12431
|
+
layer.ctx.textAlign = fontSource.textAlign;
|
|
12432
|
+
layer.ctx.direction = fontSource.direction;
|
|
12433
|
+
layer.ctx.fillStyle = "#000";
|
|
12434
|
+
return layer;
|
|
12276
12435
|
}
|
|
12277
12436
|
/**
|
|
12278
|
-
*
|
|
12279
|
-
*
|
|
12437
|
+
* Draw one text bound onto a glyph layer, splitting into per-glyph draws
|
|
12438
|
+
* when letter-spacing is in effect so the layout matches the main text
|
|
12439
|
+
* renderer. Vertical writing modes fall back to a single draw call.
|
|
12280
12440
|
*/
|
|
12281
|
-
|
|
12282
|
-
const
|
|
12441
|
+
drawGlyphs(layer, textBound, drawOptions, draw) {
|
|
12442
|
+
const x = textBound.bounds.left - drawOptions.bounds.left;
|
|
12443
|
+
const y = textBound.bounds.top - drawOptions.bounds.top + drawOptions.baseline;
|
|
12444
|
+
const { letterSpacing, writingMode } = drawOptions;
|
|
12445
|
+
if (letterSpacing <= 0) {
|
|
12446
|
+
draw(layer.ctx, textBound.text, x, y);
|
|
12447
|
+
return;
|
|
12448
|
+
}
|
|
12449
|
+
if (isVerticalWritingMode(writingMode)) {
|
|
12450
|
+
draw(layer.ctx, textBound.text, x, y);
|
|
12451
|
+
return;
|
|
12452
|
+
}
|
|
12283
12453
|
let offset = x;
|
|
12284
|
-
for (const letter of
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
return;
|
|
12288
|
-
}
|
|
12289
|
-
ctx.fillText(letter, offset, y);
|
|
12290
|
-
offset += ctx.measureText(letter).width + letterSpacing;
|
|
12454
|
+
for (const letter of segmentGraphemes(textBound.text)) {
|
|
12455
|
+
draw(layer.ctx, letter, offset, y);
|
|
12456
|
+
offset += layer.ctx.measureText(letter).width + letterSpacing;
|
|
12291
12457
|
}
|
|
12292
12458
|
}
|
|
12459
|
+
/**
|
|
12460
|
+
* Paint the text-shadow silhouettes for every glyph, one canvas per
|
|
12461
|
+
* shadow, returned front-to-back (the last shadow sits deepest). Returns
|
|
12462
|
+
* null when the element has no text shadows.
|
|
12463
|
+
*/
|
|
12464
|
+
renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, fontSource, styles, drawOptions) {
|
|
12465
|
+
const textShadows = styles.textShadow;
|
|
12466
|
+
if (!textShadows.length) return null;
|
|
12467
|
+
const canvases = [];
|
|
12468
|
+
for (let i = textShadows.length - 1; i >= 0; i--) {
|
|
12469
|
+
const textShadow = at(textShadows, i);
|
|
12470
|
+
const layer = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, fontSource);
|
|
12471
|
+
if (!layer) continue;
|
|
12472
|
+
layer.ctx.shadowColor = asString(textShadow.color);
|
|
12473
|
+
layer.ctx.shadowOffsetX = textShadow.offsetX.number * this.scale;
|
|
12474
|
+
layer.ctx.shadowOffsetY = textShadow.offsetY.number * this.scale;
|
|
12475
|
+
layer.ctx.shadowBlur = textShadow.blur.number * this.scale;
|
|
12476
|
+
layer.ctx.fillStyle = asString(textShadow.color);
|
|
12477
|
+
for (const textNode of paint.container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(layer, textBound, drawOptions, (ctx, text, x, y) => {
|
|
12478
|
+
ctx.fillText(text, x, y);
|
|
12479
|
+
});
|
|
12480
|
+
canvases.push(layer.canvas);
|
|
12481
|
+
}
|
|
12482
|
+
return canvases.length ? canvases : null;
|
|
12483
|
+
}
|
|
12484
|
+
getTextStrokeLineJoin() {
|
|
12485
|
+
return (typeof window !== "undefined" ? window : void 0)?.chrome ? "miter" : "round";
|
|
12486
|
+
}
|
|
12293
12487
|
};
|
|
12294
12488
|
//#endregion
|
|
12295
12489
|
//#region src/render/canvas/canvas-renderer.ts
|
|
@@ -12334,7 +12528,10 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12334
12528
|
formatPath: (paths) => this.formatPath(paths)
|
|
12335
12529
|
});
|
|
12336
12530
|
this.borderImageRenderer = new BorderImageRenderer(this.ctx);
|
|
12337
|
-
this.effectsRenderer = new EffectsRenderer({
|
|
12531
|
+
this.effectsRenderer = new EffectsRenderer({
|
|
12532
|
+
ctx: this.ctx,
|
|
12533
|
+
scale: options.scale
|
|
12534
|
+
}, { path: (paths) => this.path(paths) });
|
|
12338
12535
|
this.textRenderer = new TextRenderer({
|
|
12339
12536
|
ctx: this.ctx,
|
|
12340
12537
|
options: { scale: options.scale }
|
|
@@ -12342,6 +12539,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12342
12539
|
this.textClipRenderer = new TextClipRenderer({
|
|
12343
12540
|
ctx: this.ctx,
|
|
12344
12541
|
context: this.context,
|
|
12542
|
+
scale: options.scale,
|
|
12345
12543
|
createFontStyle: (styles) => this.textRenderer.createFontStyle(styles)
|
|
12346
12544
|
});
|
|
12347
12545
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
@@ -12352,8 +12550,9 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12352
12550
|
const filter = parseSimpleFilter(styles.filter);
|
|
12353
12551
|
const hasMask = styles.maskImage.length > 0;
|
|
12354
12552
|
const reflect = styles.webkitBoxReflect;
|
|
12355
|
-
|
|
12356
|
-
|
|
12553
|
+
const hasBlend = styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL;
|
|
12554
|
+
if (stack.element !== this.surfaceRoot && !this.legacySubtree && (hasMask || reflect || hasBlend || filter && (styles.filter || styles.opacity < 1)) && this.canComposite(stack.element)) {
|
|
12555
|
+
if (!await this.renderCompositedStack(stack, filter, hasMask, reflect, hasBlend)) {
|
|
12357
12556
|
this.legacySubtree = true;
|
|
12358
12557
|
try {
|
|
12359
12558
|
await this.renderStackContent(stack);
|
|
@@ -12369,7 +12568,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12369
12568
|
canComposite(paint) {
|
|
12370
12569
|
const supported = (container) => {
|
|
12371
12570
|
const styles = container.styles;
|
|
12372
|
-
return !styles.isTransformed() && styles.zoom === 1 && styles.
|
|
12571
|
+
return !styles.isTransformed() && styles.zoom === 1 && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
|
|
12373
12572
|
};
|
|
12374
12573
|
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
12375
12574
|
let ancestor = paint.parent;
|
|
@@ -12379,7 +12578,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12379
12578
|
}
|
|
12380
12579
|
return subtree(paint.container);
|
|
12381
12580
|
}
|
|
12382
|
-
async renderCompositedStack(stack, filter, hasMask, reflect) {
|
|
12581
|
+
async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
|
|
12383
12582
|
const signal = this.options.signal;
|
|
12384
12583
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
12385
12584
|
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);
|
|
@@ -12409,9 +12608,14 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12409
12608
|
if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
|
|
12410
12609
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
12411
12610
|
const own = stack.element.effects;
|
|
12412
|
-
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
|
|
12611
|
+
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect) && !isClipEffect(effect));
|
|
12413
12612
|
this.effectsRenderer.applyEffects(effects);
|
|
12414
|
-
|
|
12613
|
+
if (hasBlend) {
|
|
12614
|
+
this.ctx.save();
|
|
12615
|
+
this.ctx.globalCompositeOperation = stack.element.container.styles.mixBlendMode;
|
|
12616
|
+
this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
|
|
12617
|
+
this.ctx.restore();
|
|
12618
|
+
} else this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
|
|
12415
12619
|
if (reflect) await this.drawReflection(composited, stack.element.container.bounds, bounds, reflect);
|
|
12416
12620
|
return true;
|
|
12417
12621
|
} catch (error) {
|
|
@@ -12501,6 +12705,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12501
12705
|
maskCanvas.height = h;
|
|
12502
12706
|
const mctx = maskCanvas.getContext("2d");
|
|
12503
12707
|
if (mctx) {
|
|
12708
|
+
mctx.scale(scale, scale);
|
|
12504
12709
|
await paintMaskLayers(mctx, [reflect.mask], {
|
|
12505
12710
|
maskPosition: [],
|
|
12506
12711
|
maskRepeat: [],
|
|
@@ -12521,7 +12726,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12521
12726
|
ctx.scale(1, -1);
|
|
12522
12727
|
}
|
|
12523
12728
|
ctx.globalCompositeOperation = "destination-in";
|
|
12524
|
-
ctx.drawImage(maskCanvas, 0, 0);
|
|
12729
|
+
ctx.drawImage(maskCanvas, 0, 0, w, h);
|
|
12525
12730
|
} finally {
|
|
12526
12731
|
ctx.restore();
|
|
12527
12732
|
}
|
|
@@ -12733,26 +12938,27 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12733
12938
|
}
|
|
12734
12939
|
if (styles.borderImageSource) {
|
|
12735
12940
|
const source = styles.borderImageSource;
|
|
12941
|
+
const bounds = paint.container.bounds;
|
|
12942
|
+
const borderWidths = [
|
|
12943
|
+
Math.max(0, styles.borderTopWidth),
|
|
12944
|
+
Math.max(0, styles.borderRightWidth),
|
|
12945
|
+
Math.max(0, styles.borderBottomWidth),
|
|
12946
|
+
Math.max(0, styles.borderLeftWidth)
|
|
12947
|
+
];
|
|
12948
|
+
let image = null;
|
|
12736
12949
|
if (source.type === 0) {
|
|
12737
12950
|
const url = source.url;
|
|
12738
12951
|
try {
|
|
12739
|
-
|
|
12740
|
-
if (image) {
|
|
12741
|
-
const bounds = paint.container.bounds;
|
|
12742
|
-
const borderWidths = [
|
|
12743
|
-
Math.max(0, styles.borderTopWidth),
|
|
12744
|
-
Math.max(0, styles.borderRightWidth),
|
|
12745
|
-
Math.max(0, styles.borderBottomWidth),
|
|
12746
|
-
Math.max(0, styles.borderLeftWidth)
|
|
12747
|
-
];
|
|
12748
|
-
const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
|
|
12749
|
-
const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
|
|
12750
|
-
const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
|
|
12751
|
-
this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
|
|
12752
|
-
}
|
|
12952
|
+
image = await this.context.cache.match(url) ?? null;
|
|
12753
12953
|
} catch (e) {
|
|
12754
12954
|
this.context.logger.error(`Error loading border-image ${url}`);
|
|
12755
12955
|
}
|
|
12956
|
+
} else image = this.rasterizeGradientSource(source, bounds);
|
|
12957
|
+
if (image) {
|
|
12958
|
+
const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
|
|
12959
|
+
const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
|
|
12960
|
+
const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
|
|
12961
|
+
this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
|
|
12756
12962
|
}
|
|
12757
12963
|
return;
|
|
12758
12964
|
}
|
|
@@ -12820,6 +13026,91 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12820
13026
|
}
|
|
12821
13027
|
}
|
|
12822
13028
|
/**
|
|
13029
|
+
* Rasterise a gradient border-image-source across the border image area.
|
|
13030
|
+
* CSS allows any <image> as the border-image source; gradients are painted
|
|
13031
|
+
* over the whole border-image box and then sliced by the regular 9-slice
|
|
13032
|
+
* pipeline, matching browser behaviour for `border-image: linear-gradient()`.
|
|
13033
|
+
* Returns null when the gradient could not be painted.
|
|
13034
|
+
*/
|
|
13035
|
+
rasterizeGradientSource(source, bounds) {
|
|
13036
|
+
const width = Math.max(1, Math.ceil(bounds.width));
|
|
13037
|
+
const height = Math.max(1, Math.ceil(bounds.height));
|
|
13038
|
+
const canvas = (this.ctx.canvas.ownerDocument ?? document).createElement("canvas");
|
|
13039
|
+
canvas.width = width;
|
|
13040
|
+
canvas.height = height;
|
|
13041
|
+
const ctx = canvas.getContext("2d");
|
|
13042
|
+
if (!ctx) return null;
|
|
13043
|
+
if (source.type === 1 || source.type === 3) {
|
|
13044
|
+
const gradientImage = source;
|
|
13045
|
+
const [lineLength, x0, y0, x1, y1] = calculateGradientDirection(gradientImage.angle, width, height);
|
|
13046
|
+
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
13047
|
+
processColorStops(gradientImage.stops, lineLength || width).forEach((colorStop) => {
|
|
13048
|
+
gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
|
|
13049
|
+
});
|
|
13050
|
+
ctx.fillStyle = gradient;
|
|
13051
|
+
ctx.fillRect(0, 0, width, height);
|
|
13052
|
+
return canvas;
|
|
13053
|
+
}
|
|
13054
|
+
if (source.type === 2 || source.type === 4) {
|
|
13055
|
+
const gradientImage = source;
|
|
13056
|
+
const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
|
|
13057
|
+
const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
|
|
13058
|
+
const [rx, ry] = calculateRadius(gradientImage, cx, cy, width, height);
|
|
13059
|
+
if (rx <= 0 || ry <= 0) return null;
|
|
13060
|
+
ctx.save();
|
|
13061
|
+
ctx.translate(cx, cy);
|
|
13062
|
+
ctx.scale(1, ry / rx);
|
|
13063
|
+
ctx.translate(-cx, -cy);
|
|
13064
|
+
const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, rx);
|
|
13065
|
+
processColorStops(gradientImage.stops, rx).forEach((colorStop) => {
|
|
13066
|
+
gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
|
|
13067
|
+
});
|
|
13068
|
+
ctx.fillStyle = gradient;
|
|
13069
|
+
ctx.fillRect(0, 0, width, height);
|
|
13070
|
+
ctx.restore();
|
|
13071
|
+
return canvas;
|
|
13072
|
+
}
|
|
13073
|
+
if (source.type === 5 || source.type === 6) {
|
|
13074
|
+
const gradientImage = source;
|
|
13075
|
+
const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
|
|
13076
|
+
const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
|
|
13077
|
+
const gradient = ctx.createConicGradient(gradientImage.angle - Math.PI / 2, cx, cy);
|
|
13078
|
+
let stops;
|
|
13079
|
+
if (source.type === 6) {
|
|
13080
|
+
const processed = processColorStops(gradientImage.stops, 1);
|
|
13081
|
+
const first = at(processed, 0);
|
|
13082
|
+
const last = at(processed, processed.length - 1);
|
|
13083
|
+
const period = Math.max(last.stop - first.stop, .01);
|
|
13084
|
+
stops = [];
|
|
13085
|
+
const cycles = Math.ceil(1 / period);
|
|
13086
|
+
for (let k = 0; k < cycles; k++) for (const s of processed) {
|
|
13087
|
+
const stop = k * period + (s.stop - first.stop);
|
|
13088
|
+
if (stop > 1) break;
|
|
13089
|
+
stops.push({
|
|
13090
|
+
stop,
|
|
13091
|
+
color: asString(s.color)
|
|
13092
|
+
});
|
|
13093
|
+
}
|
|
13094
|
+
const lastIncluded = at(stops, stops.length - 1);
|
|
13095
|
+
if (lastIncluded && lastIncluded.stop < 1) stops.push({
|
|
13096
|
+
stop: 1,
|
|
13097
|
+
color: lastIncluded.color
|
|
13098
|
+
});
|
|
13099
|
+
} else stops = processColorStops(gradientImage.stops, Math.max(width, height)).map((colorStop) => ({
|
|
13100
|
+
stop: colorStop.stop,
|
|
13101
|
+
color: asString(colorStop.color)
|
|
13102
|
+
}));
|
|
13103
|
+
stops.forEach((s) => {
|
|
13104
|
+
gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
|
|
13105
|
+
});
|
|
13106
|
+
ctx.fillStyle = gradient;
|
|
13107
|
+
ctx.fillRect(0, 0, width, height);
|
|
13108
|
+
return canvas;
|
|
13109
|
+
}
|
|
13110
|
+
this.context.logger.warn("Unsupported border-image source; only URL and gradient images are supported");
|
|
13111
|
+
return null;
|
|
13112
|
+
}
|
|
13113
|
+
/**
|
|
12823
13114
|
* Filter the already-painted content beneath the element's border box and
|
|
12824
13115
|
* paint the result back, approximating CSS backdrop-filter for the
|
|
12825
13116
|
* blur/drop-shadow subset supported by parseSimpleFilter.
|
|
@@ -12923,9 +13214,6 @@ var ForeignObjectRenderer = class {
|
|
|
12923
13214
|
//#endregion
|
|
12924
13215
|
//#region src/core/logger.ts
|
|
12925
13216
|
var Logger = class {
|
|
12926
|
-
static {
|
|
12927
|
-
this.instances = {};
|
|
12928
|
-
}
|
|
12929
13217
|
constructor({ id, enabled }) {
|
|
12930
13218
|
this.id = id;
|
|
12931
13219
|
this.enabled = enabled;
|
|
@@ -12961,6 +13249,9 @@ var Logger = class {
|
|
|
12961
13249
|
//#endregion
|
|
12962
13250
|
//#region src/core/cache-storage.ts
|
|
12963
13251
|
var Cache = class {
|
|
13252
|
+
get _evictionEnabled() {
|
|
13253
|
+
return this._evictionSuspendCount === 0;
|
|
13254
|
+
}
|
|
12964
13255
|
constructor(context, _options) {
|
|
12965
13256
|
this.context = context;
|
|
12966
13257
|
this._options = _options;
|
|
@@ -12968,6 +13259,7 @@ var Cache = class {
|
|
|
12968
13259
|
this._pendingOperations = /* @__PURE__ */ new Map();
|
|
12969
13260
|
this._deferMode = false;
|
|
12970
13261
|
this._collectedUrls = /* @__PURE__ */ new Set();
|
|
13262
|
+
this._evictionSuspendCount = 0;
|
|
12971
13263
|
this.maxSize = _options.maxCacheSize ?? 100;
|
|
12972
13264
|
if (this.maxSize < 1) throw new Error("Cache maxSize must be at least 1");
|
|
12973
13265
|
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).`);
|
|
@@ -12978,6 +13270,7 @@ var Cache = class {
|
|
|
12978
13270
|
*/
|
|
12979
13271
|
startDefer() {
|
|
12980
13272
|
this._deferMode = true;
|
|
13273
|
+
this._evictionSuspendCount++;
|
|
12981
13274
|
}
|
|
12982
13275
|
/**
|
|
12983
13276
|
* Exit defer mode and load all collected URLs in parallel, respecting
|
|
@@ -12995,17 +13288,21 @@ var Cache = class {
|
|
|
12995
13288
|
this._deferMode = false;
|
|
12996
13289
|
const urls = Array.from(this._collectedUrls);
|
|
12997
13290
|
this._collectedUrls.clear();
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13291
|
+
try {
|
|
13292
|
+
if (urls.length === 0) return;
|
|
13293
|
+
const limit = Math.max(1, Math.min(concurrency, 100));
|
|
13294
|
+
this.context.logger.debug(`Preloading ${urls.length} image(s) with concurrency ${limit}`);
|
|
13295
|
+
let settled = 0;
|
|
13296
|
+
onProgress?.(0, urls.length);
|
|
13297
|
+
for (let i = 0; i < urls.length; i += limit) {
|
|
13298
|
+
const batch = urls.slice(i, i + limit);
|
|
13299
|
+
const operations = batch.map((url) => this._addImageWithPending(url));
|
|
13300
|
+
await Promise.allSettled(operations);
|
|
13301
|
+
settled += batch.length;
|
|
13302
|
+
onProgress?.(settled, urls.length);
|
|
13303
|
+
}
|
|
13304
|
+
} finally {
|
|
13305
|
+
this._evictionSuspendCount = Math.max(0, this._evictionSuspendCount - 1);
|
|
13009
13306
|
}
|
|
13010
13307
|
}
|
|
13011
13308
|
addImage(src) {
|
|
@@ -13072,7 +13369,7 @@ var Cache = class {
|
|
|
13072
13369
|
*/
|
|
13073
13370
|
set(key, value) {
|
|
13074
13371
|
if (this._cache.has(key)) this._cache.delete(key);
|
|
13075
|
-
else if (this._cache.size >= this.maxSize) {
|
|
13372
|
+
else if (this._evictionEnabled && this._cache.size >= this.maxSize) {
|
|
13076
13373
|
const lruKey = this._cache.keys().next().value;
|
|
13077
13374
|
if (lruKey !== void 0) {
|
|
13078
13375
|
this._cache.delete(lruKey);
|