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