html2canvas-pro 2.4.3 → 2.4.5
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/dist/html2canvas-pro.cjs +661 -86
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +661 -86
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +661 -86
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +5 -5
- package/dist/lib/core/render-element.js +14 -15
- package/dist/lib/css/property-descriptors/filter.js +14 -4
- package/dist/lib/dom/node-parser.js +1 -0
- package/dist/lib/render/canvas/box-shadow-geometry.js +52 -0
- package/dist/lib/render/canvas/box-shadow-painter.js +194 -0
- package/dist/lib/render/canvas/box-shadow-transform.js +42 -0
- package/dist/lib/render/canvas/canvas-renderer.js +185 -37
- package/dist/lib/render/canvas/filter-surface.js +270 -0
- package/dist/lib/render/canvas/surface-bounds.js +64 -0
- package/dist/lib/render/canvas/text-renderer.js +13 -19
- package/dist/lib/render/stacking-context.js +6 -5
- package/dist/types/render/canvas/box-shadow-geometry.d.ts +3 -0
- package/dist/types/render/canvas/box-shadow-painter.d.ts +13 -0
- package/dist/types/render/canvas/box-shadow-transform.d.ts +16 -0
- package/dist/types/render/canvas/canvas-renderer.d.ts +8 -1
- package/dist/types/render/canvas/filter-surface.d.ts +17 -0
- package/dist/types/render/canvas/surface-bounds.d.ts +10 -0
- package/dist/types/render/canvas/text-renderer.d.ts +10 -8
- package/dist/types/render/stacking-context.d.ts +1 -1
- package/package.json +4 -2
package/dist/html2canvas-pro.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.4.
|
|
2
|
+
* html2canvas-pro 2.4.5 <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
|
*/
|
|
@@ -5717,7 +5717,7 @@ const filter = {
|
|
|
5717
5717
|
const renderedArgs = renderFilterArgs(fn.values);
|
|
5718
5718
|
switch (fn.name) {
|
|
5719
5719
|
case "blur":
|
|
5720
|
-
parts.push(`blur(${renderedArgs}
|
|
5720
|
+
parts.push(`blur(${renderedArgs})`);
|
|
5721
5721
|
break;
|
|
5722
5722
|
case "brightness":
|
|
5723
5723
|
case "contrast":
|
|
@@ -5731,7 +5731,7 @@ const filter = {
|
|
|
5731
5731
|
parts.push(`grayscale(${renderedArgs})`);
|
|
5732
5732
|
break;
|
|
5733
5733
|
case "hue-rotate":
|
|
5734
|
-
parts.push(`hue-rotate(${renderedArgs}
|
|
5734
|
+
parts.push(`hue-rotate(${renderedArgs})`);
|
|
5735
5735
|
break;
|
|
5736
5736
|
case "drop-shadow":
|
|
5737
5737
|
parts.push(`drop-shadow(${renderedArgs})`);
|
|
@@ -5749,14 +5749,20 @@ const filter = {
|
|
|
5749
5749
|
const renderFilterArgs = (values) => {
|
|
5750
5750
|
const parts = [];
|
|
5751
5751
|
for (const v of values) {
|
|
5752
|
-
if (v.type === 31)
|
|
5753
|
-
|
|
5752
|
+
if (v.type === 31) {
|
|
5753
|
+
parts.push(" ");
|
|
5754
|
+
continue;
|
|
5755
|
+
}
|
|
5756
|
+
if (v.type === 18) parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
|
|
5757
|
+
else if (v.type === 4) parts.push(",");
|
|
5758
|
+
else if (v.type === 6) parts.push(v.value);
|
|
5759
|
+
else if (v.type === 15) parts.push(`${v.number}${v.unit}`);
|
|
5754
5760
|
else if (v.type === 17) parts.push(`${v.number}`);
|
|
5755
5761
|
else if (v.type === 16) parts.push(`${v.number}%`);
|
|
5756
5762
|
else if (v.type === 20) parts.push(v.value);
|
|
5757
5763
|
else if (v.type === 5) parts.push(`#${v.value}`);
|
|
5758
5764
|
}
|
|
5759
|
-
return parts.join("
|
|
5765
|
+
return parts.join("").trim();
|
|
5760
5766
|
};
|
|
5761
5767
|
//#endregion
|
|
5762
5768
|
//#region src/css/property-descriptors/font-variant-ligatures.ts
|
|
@@ -8306,7 +8312,7 @@ const parseTree = (context, element) => {
|
|
|
8306
8312
|
return container;
|
|
8307
8313
|
};
|
|
8308
8314
|
const createsRealStackingContext = (node, container, root) => {
|
|
8309
|
-
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8315
|
+
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || Boolean(container.styles.filter) || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8310
8316
|
};
|
|
8311
8317
|
const createsStackingContext = (styles) => {
|
|
8312
8318
|
if (styles.isPositioned() || styles.isFloating()) return true;
|
|
@@ -8743,12 +8749,13 @@ var ElementPaint = class {
|
|
|
8743
8749
|
this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
|
|
8744
8750
|
}
|
|
8745
8751
|
}
|
|
8746
|
-
getEffects(target) {
|
|
8752
|
+
getEffects(target, surfaceRoot) {
|
|
8747
8753
|
let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
|
|
8748
|
-
let parent = this.parent;
|
|
8749
|
-
const
|
|
8754
|
+
let parent = this === surfaceRoot ? null : this.parent;
|
|
8755
|
+
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
|
|
8756
|
+
const effects = sourceEffects(this);
|
|
8750
8757
|
while (parent) {
|
|
8751
|
-
const croplessEffects = parent.
|
|
8758
|
+
const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
|
|
8752
8759
|
if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
|
|
8753
8760
|
inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
|
|
8754
8761
|
if (hasOverflowClip(parent.container.styles)) {
|
|
@@ -8758,7 +8765,7 @@ var ElementPaint = class {
|
|
|
8758
8765
|
}
|
|
8759
8766
|
effects.unshift(...croplessEffects);
|
|
8760
8767
|
} else effects.unshift(...croplessEffects);
|
|
8761
|
-
parent = parent.parent;
|
|
8768
|
+
parent = parent === surfaceRoot ? null : parent.parent;
|
|
8762
8769
|
}
|
|
8763
8770
|
return effects.filter((effect) => contains(effect.target, target));
|
|
8764
8771
|
}
|
|
@@ -9039,6 +9046,268 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
|
|
|
9039
9046
|
}
|
|
9040
9047
|
};
|
|
9041
9048
|
//#endregion
|
|
9049
|
+
//#region src/core/abort-helper.ts
|
|
9050
|
+
/**
|
|
9051
|
+
* AbortSignal helper utilities.
|
|
9052
|
+
*/
|
|
9053
|
+
/**
|
|
9054
|
+
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
9055
|
+
* Used at checkpoints throughout the rendering pipeline to support
|
|
9056
|
+
* cancellable renders.
|
|
9057
|
+
*
|
|
9058
|
+
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
9059
|
+
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
9060
|
+
*/
|
|
9061
|
+
const throwIfAborted = (signal) => {
|
|
9062
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
9063
|
+
};
|
|
9064
|
+
//#endregion
|
|
9065
|
+
//#region src/render/canvas/filter-surface.ts
|
|
9066
|
+
var FilterSurfaceError = class extends Error {};
|
|
9067
|
+
const releaseSurface = (canvas) => {
|
|
9068
|
+
canvas.width = canvas.height = 0;
|
|
9069
|
+
};
|
|
9070
|
+
function parseSimpleFilter(value) {
|
|
9071
|
+
if (!value || value === "none") return { blur: 0 };
|
|
9072
|
+
const effect = new FilterEffect(value);
|
|
9073
|
+
const shadows = value.match(/drop-shadow\(/g) || [];
|
|
9074
|
+
if (shadows.length > 1 || shadows.length === 1 && !effect.shadow) return null;
|
|
9075
|
+
const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
|
|
9076
|
+
if (effect.safeFilterString && !blur) return null;
|
|
9077
|
+
if (blur && shadows.length && value.indexOf("blur(") > value.indexOf("drop-shadow(")) return null;
|
|
9078
|
+
return {
|
|
9079
|
+
blur: blur ? Number(blur[1]) : 0,
|
|
9080
|
+
shadow: effect.shadow ? {
|
|
9081
|
+
x: effect.shadow.offsetX,
|
|
9082
|
+
y: effect.shadow.offsetY,
|
|
9083
|
+
blur: effect.shadow.blur,
|
|
9084
|
+
color: effect.shadow.color
|
|
9085
|
+
} : void 0
|
|
9086
|
+
};
|
|
9087
|
+
}
|
|
9088
|
+
function filterOutset(filter) {
|
|
9089
|
+
return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) + Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
|
|
9090
|
+
}
|
|
9091
|
+
function canvasLike(source) {
|
|
9092
|
+
const canvas = document.createElement("canvas");
|
|
9093
|
+
canvas.width = source.width;
|
|
9094
|
+
canvas.height = source.height;
|
|
9095
|
+
return canvas;
|
|
9096
|
+
}
|
|
9097
|
+
const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
|
|
9098
|
+
const finish = (error) => {
|
|
9099
|
+
clearTimeout(timer);
|
|
9100
|
+
signal?.removeEventListener("abort", abort);
|
|
9101
|
+
if (error) reject(error);
|
|
9102
|
+
else resolve();
|
|
9103
|
+
};
|
|
9104
|
+
const abort = () => finish(new DOMException("The operation was aborted.", "AbortError"));
|
|
9105
|
+
const timer = setTimeout(() => finish(new FilterSurfaceError("Filter surface image decode timed out")), 1e4);
|
|
9106
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9107
|
+
if (signal?.aborted) abort();
|
|
9108
|
+
else image.decode().then(() => finish(), () => finish(new FilterSurfaceError("Filter surface image decode failed")));
|
|
9109
|
+
});
|
|
9110
|
+
const canvasFilterSupport = /* @__PURE__ */ new WeakMap();
|
|
9111
|
+
function supportsNativeFilters(owner = document) {
|
|
9112
|
+
const cached = canvasFilterSupport.get(owner);
|
|
9113
|
+
if (cached !== void 0) return cached;
|
|
9114
|
+
const source = owner.createElement("canvas");
|
|
9115
|
+
const target = owner.createElement("canvas");
|
|
9116
|
+
let supported = false;
|
|
9117
|
+
try {
|
|
9118
|
+
source.width = source.height = target.width = target.height = 32;
|
|
9119
|
+
const input = source.getContext("2d");
|
|
9120
|
+
const context = target.getContext("2d");
|
|
9121
|
+
if (input && context && "filter" in context) {
|
|
9122
|
+
input.fillStyle = "#fff";
|
|
9123
|
+
input.fillRect(8, 8, 8, 8);
|
|
9124
|
+
context.filter = "blur(2px)";
|
|
9125
|
+
context.drawImage(source, 0, 0);
|
|
9126
|
+
const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
|
|
9127
|
+
context.clearRect(0, 0, 32, 32);
|
|
9128
|
+
context.filter = "drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))";
|
|
9129
|
+
context.drawImage(source, 0, 0);
|
|
9130
|
+
const shadow = context.getImageData(24, 12, 1, 1).data;
|
|
9131
|
+
supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
|
|
9132
|
+
}
|
|
9133
|
+
} catch {} finally {
|
|
9134
|
+
releaseSurface(source);
|
|
9135
|
+
releaseSurface(target);
|
|
9136
|
+
}
|
|
9137
|
+
canvasFilterSupport.set(owner, supported);
|
|
9138
|
+
return supported;
|
|
9139
|
+
}
|
|
9140
|
+
function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9141
|
+
let filtered;
|
|
9142
|
+
let output;
|
|
9143
|
+
let complete = false;
|
|
9144
|
+
try {
|
|
9145
|
+
throwIfAborted(signal);
|
|
9146
|
+
filtered = canvasLike(source);
|
|
9147
|
+
const context = filtered.getContext("2d");
|
|
9148
|
+
if (!context || !("filter" in context)) return null;
|
|
9149
|
+
const functions = [];
|
|
9150
|
+
if (blur) functions.push(`blur(${blur * scale}px)`);
|
|
9151
|
+
if (shadow) functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
|
|
9152
|
+
context.filter = functions.join(" ");
|
|
9153
|
+
if (context.filter === "none") return null;
|
|
9154
|
+
context.drawImage(source, 0, 0);
|
|
9155
|
+
throwIfAborted(signal);
|
|
9156
|
+
output = canvasLike(source);
|
|
9157
|
+
const composite = output.getContext("2d");
|
|
9158
|
+
if (!composite) return null;
|
|
9159
|
+
composite.globalAlpha = opacity;
|
|
9160
|
+
composite.drawImage(filtered, 0, 0);
|
|
9161
|
+
throwIfAborted(signal);
|
|
9162
|
+
complete = true;
|
|
9163
|
+
return output;
|
|
9164
|
+
} catch {
|
|
9165
|
+
throwIfAborted(signal);
|
|
9166
|
+
return null;
|
|
9167
|
+
} finally {
|
|
9168
|
+
if (filtered) releaseSurface(filtered);
|
|
9169
|
+
if (output && !complete) releaseSurface(output);
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
9173
|
+
throwIfAborted(signal);
|
|
9174
|
+
if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
|
|
9175
|
+
const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
|
|
9176
|
+
if (output) return output;
|
|
9177
|
+
}
|
|
9178
|
+
return renderSvgFilterSurface(source, filter, opacity, scale, signal);
|
|
9179
|
+
}
|
|
9180
|
+
async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9181
|
+
throwIfAborted(signal);
|
|
9182
|
+
if (!blur && !shadow) {
|
|
9183
|
+
const output = canvasLike(source);
|
|
9184
|
+
const context = output.getContext("2d");
|
|
9185
|
+
if (!context) {
|
|
9186
|
+
releaseSurface(output);
|
|
9187
|
+
throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9188
|
+
}
|
|
9189
|
+
context.globalAlpha = opacity;
|
|
9190
|
+
try {
|
|
9191
|
+
context.drawImage(source, 0, 0);
|
|
9192
|
+
return output;
|
|
9193
|
+
} catch {
|
|
9194
|
+
releaseSurface(output);
|
|
9195
|
+
throw new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9196
|
+
}
|
|
9197
|
+
}
|
|
9198
|
+
const namespace = "http://www.w3.org/2000/svg";
|
|
9199
|
+
const element = (tag, attributes = {}) => {
|
|
9200
|
+
const node = document.createElementNS(namespace, tag);
|
|
9201
|
+
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
9202
|
+
return node;
|
|
9203
|
+
};
|
|
9204
|
+
const svg = element("svg", {
|
|
9205
|
+
width: source.width,
|
|
9206
|
+
height: source.height,
|
|
9207
|
+
viewBox: `0 0 ${source.width} ${source.height}`
|
|
9208
|
+
});
|
|
9209
|
+
const defs = element("defs");
|
|
9210
|
+
const filter = element("filter", {
|
|
9211
|
+
id: "composited-filter",
|
|
9212
|
+
filterUnits: "userSpaceOnUse",
|
|
9213
|
+
x: 0,
|
|
9214
|
+
y: 0,
|
|
9215
|
+
width: source.width,
|
|
9216
|
+
height: source.height,
|
|
9217
|
+
"color-interpolation-filters": "sRGB"
|
|
9218
|
+
});
|
|
9219
|
+
if (blur) filter.appendChild(element("feGaussianBlur", {
|
|
9220
|
+
stdDeviation: blur * scale,
|
|
9221
|
+
result: "blurred-source"
|
|
9222
|
+
}));
|
|
9223
|
+
if (shadow) {
|
|
9224
|
+
filter.appendChild(element("feGaussianBlur", {
|
|
9225
|
+
in: blur ? "blurred-source" : "SourceAlpha",
|
|
9226
|
+
stdDeviation: shadow.blur * scale
|
|
9227
|
+
}));
|
|
9228
|
+
filter.appendChild(element("feOffset", {
|
|
9229
|
+
dx: shadow.x * scale,
|
|
9230
|
+
dy: shadow.y * scale,
|
|
9231
|
+
result: "offset-shadow"
|
|
9232
|
+
}));
|
|
9233
|
+
filter.appendChild(element("feFlood", { "flood-color": shadow.color }));
|
|
9234
|
+
filter.appendChild(element("feComposite", {
|
|
9235
|
+
in2: "offset-shadow",
|
|
9236
|
+
operator: "in"
|
|
9237
|
+
}));
|
|
9238
|
+
const merge = element("feMerge");
|
|
9239
|
+
merge.appendChild(element("feMergeNode"));
|
|
9240
|
+
merge.appendChild(element("feMergeNode", { in: blur ? "blurred-source" : "SourceGraphic" }));
|
|
9241
|
+
filter.appendChild(merge);
|
|
9242
|
+
}
|
|
9243
|
+
defs.appendChild(filter);
|
|
9244
|
+
svg.appendChild(defs);
|
|
9245
|
+
let sourceUrl;
|
|
9246
|
+
try {
|
|
9247
|
+
sourceUrl = source.toDataURL("image/png");
|
|
9248
|
+
if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
|
|
9249
|
+
} catch {
|
|
9250
|
+
throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
|
|
9251
|
+
}
|
|
9252
|
+
svg.appendChild(element("image", {
|
|
9253
|
+
width: source.width,
|
|
9254
|
+
height: source.height,
|
|
9255
|
+
href: sourceUrl,
|
|
9256
|
+
filter: "url(#composited-filter)"
|
|
9257
|
+
}));
|
|
9258
|
+
const image = new Image();
|
|
9259
|
+
let output;
|
|
9260
|
+
try {
|
|
9261
|
+
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
9262
|
+
await decodeSurface(image, signal);
|
|
9263
|
+
throwIfAborted(signal);
|
|
9264
|
+
output = canvasLike(source);
|
|
9265
|
+
const context = output.getContext("2d");
|
|
9266
|
+
if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9267
|
+
context.globalAlpha = opacity;
|
|
9268
|
+
context.drawImage(image, 0, 0);
|
|
9269
|
+
return output;
|
|
9270
|
+
} catch (error) {
|
|
9271
|
+
if (output) releaseSurface(output);
|
|
9272
|
+
if (signal?.aborted) throwIfAborted(signal);
|
|
9273
|
+
throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9274
|
+
} finally {
|
|
9275
|
+
image.removeAttribute("src");
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
function reserveSurface(budget, width, height) {
|
|
9279
|
+
const pixels = width * height * 4;
|
|
9280
|
+
if (!Number.isFinite(pixels) || width <= 0 || height <= 0 || width > 8192 || height > 8192 || budget.pixels + pixels > 16777216) return 0;
|
|
9281
|
+
budget.pixels += pixels;
|
|
9282
|
+
return pixels;
|
|
9283
|
+
}
|
|
9284
|
+
const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
|
|
9285
|
+
const union = (a, b) => new Bounds(Math.min(a.left, b.left), Math.min(a.top, b.top), Math.max(a.left + a.width, b.left + b.width) - Math.min(a.left, b.left), Math.max(a.top + a.height, b.top + b.height) - Math.min(a.top, b.top));
|
|
9286
|
+
function surfaceBounds(container) {
|
|
9287
|
+
const styles = container.styles;
|
|
9288
|
+
let bounds = container.bounds;
|
|
9289
|
+
for (const shadow of styles.boxShadow) if (!shadow.inset) {
|
|
9290
|
+
const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
|
|
9291
|
+
bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
|
|
9292
|
+
}
|
|
9293
|
+
for (const text of container.textNodes) for (const part of text.textBounds) {
|
|
9294
|
+
const ink = expand(part.bounds, 2 * getAbsoluteValue(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
|
|
9295
|
+
bounds = union(bounds, ink);
|
|
9296
|
+
for (const shadow of styles.textShadow) bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
|
|
9297
|
+
}
|
|
9298
|
+
for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
|
|
9299
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
9300
|
+
return expand(bounds, filter ? filterOutset(filter) : 0);
|
|
9301
|
+
}
|
|
9302
|
+
function cropSurface(bounds, viewport, margin, scale) {
|
|
9303
|
+
const area = expand(viewport, margin);
|
|
9304
|
+
const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
|
|
9305
|
+
const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
|
|
9306
|
+
const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
|
|
9307
|
+
const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
|
|
9308
|
+
return new Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
|
|
9309
|
+
}
|
|
9310
|
+
//#endregion
|
|
9042
9311
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9043
9312
|
var TextDecorationRenderer = class {
|
|
9044
9313
|
constructor(ctx) {
|
|
@@ -9168,8 +9437,8 @@ const iOSBrokenFonts = ["-apple-system", "system-ui"];
|
|
|
9168
9437
|
const TEXT_OVERFLOW_EPSILON = 1;
|
|
9169
9438
|
/**
|
|
9170
9439
|
* Detect CJK (Chinese, Japanese, Korean) characters in a string.
|
|
9171
|
-
*
|
|
9172
|
-
*
|
|
9440
|
+
* Vertical writing modes use it to keep CJK glyphs upright and on the
|
|
9441
|
+
* ideographic baseline while other scripts are rotated.
|
|
9173
9442
|
*
|
|
9174
9443
|
* Covers:
|
|
9175
9444
|
* U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
|
|
@@ -9239,15 +9508,17 @@ var TextRenderer = class {
|
|
|
9239
9508
|
return w;
|
|
9240
9509
|
}
|
|
9241
9510
|
/**
|
|
9242
|
-
* Iterate grapheme clusters one-by-one, applying correct letter-spacing
|
|
9243
|
-
* per-script baseline for each character.
|
|
9511
|
+
* Iterate grapheme clusters one-by-one, applying correct letter-spacing.
|
|
9244
9512
|
*
|
|
9245
9513
|
* Issue #73: When letter-spacing is non-zero, text must be rendered character by
|
|
9246
|
-
* character
|
|
9247
|
-
*
|
|
9248
|
-
*
|
|
9249
|
-
*
|
|
9250
|
-
*
|
|
9514
|
+
* character, adding `letterSpacing` to each character's advance width.
|
|
9515
|
+
*
|
|
9516
|
+
* In horizontal text every glyph keeps the alphabetic baseline, CJK included:
|
|
9517
|
+
* inline layout aligns a whole line on its alphabetic baseline regardless of
|
|
9518
|
+
* script, and `baseline` was measured for that. Drawing CJK graphemes at the
|
|
9519
|
+
* same y with `textBaseline = 'ideographic'` lifts them above the Latin text
|
|
9520
|
+
* next to them, and away from where the `letterSpacing === 0` whole-string
|
|
9521
|
+
* path draws them.
|
|
9251
9522
|
*
|
|
9252
9523
|
* The `renderFn` callback receives (letter, x, y) and performs the actual draw
|
|
9253
9524
|
* call (fillText or strokeText), allowing fill and stroke paths to share one
|
|
@@ -9263,12 +9534,7 @@ var TextRenderer = class {
|
|
|
9263
9534
|
const fontString = this.ctx.font;
|
|
9264
9535
|
let left = text.bounds.left;
|
|
9265
9536
|
for (const letter of letters) {
|
|
9266
|
-
|
|
9267
|
-
const savedBaseline = this.ctx.textBaseline;
|
|
9268
|
-
this.ctx.textBaseline = "ideographic";
|
|
9269
|
-
renderFn(letter, left, y);
|
|
9270
|
-
this.ctx.textBaseline = savedBaseline;
|
|
9271
|
-
} else renderFn(letter, left, y);
|
|
9537
|
+
renderFn(letter, left, y);
|
|
9272
9538
|
left += this.cachedMeasureText(letter, fontString) + letterSpacing;
|
|
9273
9539
|
}
|
|
9274
9540
|
}
|
|
@@ -9310,7 +9576,7 @@ var TextRenderer = class {
|
|
|
9310
9576
|
/**
|
|
9311
9577
|
* Render text with letter-spacing applied (fill pass).
|
|
9312
9578
|
* When letterSpacing is 0 the whole string is drawn in one call; otherwise each
|
|
9313
|
-
* grapheme is drawn individually so spacing
|
|
9579
|
+
* grapheme is drawn individually so spacing is applied correctly.
|
|
9314
9580
|
*/
|
|
9315
9581
|
renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
|
|
9316
9582
|
this.renderFillText(text, letterSpacing, baseline, writingMode);
|
|
@@ -10543,14 +10809,235 @@ const canvasTextAlign = (textAlign) => {
|
|
|
10543
10809
|
}
|
|
10544
10810
|
};
|
|
10545
10811
|
//#endregion
|
|
10812
|
+
//#region src/render/canvas/box-shadow-geometry.ts
|
|
10813
|
+
const shadowRadius = (radius, spread) => {
|
|
10814
|
+
const delta = spread > radius ? spread * (1 + Math.pow(radius / spread - 1, 3)) : spread;
|
|
10815
|
+
return Math.max(0, radius + delta);
|
|
10816
|
+
};
|
|
10817
|
+
const spreadShadowPath = (paths, spread) => {
|
|
10818
|
+
if (spread === 0) return paths;
|
|
10819
|
+
const corners = paths.map((point, index) => {
|
|
10820
|
+
if (!isBezierCurve(point)) return {
|
|
10821
|
+
x: point.x,
|
|
10822
|
+
y: point.y,
|
|
10823
|
+
rx: 0,
|
|
10824
|
+
ry: 0
|
|
10825
|
+
};
|
|
10826
|
+
return {
|
|
10827
|
+
x: index % 2 === 0 ? point.start.x : point.end.x,
|
|
10828
|
+
y: index % 2 === 0 ? point.end.y : point.start.y,
|
|
10829
|
+
rx: Math.abs(point.end.x - point.start.x),
|
|
10830
|
+
ry: Math.abs(point.end.y - point.start.y)
|
|
10831
|
+
};
|
|
10832
|
+
});
|
|
10833
|
+
const width = corners[1].x - corners[0].x + 2 * spread;
|
|
10834
|
+
const height = corners[3].y - corners[0].y + 2 * spread;
|
|
10835
|
+
if (width <= 0 || height <= 0) return [];
|
|
10836
|
+
const radii = corners.map((c) => ({
|
|
10837
|
+
x: shadowRadius(c.rx, spread),
|
|
10838
|
+
y: shadowRadius(c.ry, spread)
|
|
10839
|
+
}));
|
|
10840
|
+
const ratio = (size, sum) => sum > 0 ? size / sum : 1;
|
|
10841
|
+
const factor = Math.min(1, ratio(width, radii[0].x + radii[1].x), ratio(width, radii[3].x + radii[2].x), ratio(height, radii[0].y + radii[3].y), ratio(height, radii[1].y + radii[2].y));
|
|
10842
|
+
return paths.map((point, index) => {
|
|
10843
|
+
const corner = corners[index];
|
|
10844
|
+
const x = corner.x + (index === 1 || index === 2 ? spread : -spread);
|
|
10845
|
+
const y = corner.y + (index >= 2 ? spread : -spread);
|
|
10846
|
+
if (!isBezierCurve(point)) return new Vector(x, y);
|
|
10847
|
+
const rx = radii[index].x * factor;
|
|
10848
|
+
const ry = radii[index].y * factor;
|
|
10849
|
+
if (!rx || !ry) return new Vector(x, y);
|
|
10850
|
+
const sx = rx / corner.rx;
|
|
10851
|
+
const sy = ry / corner.ry;
|
|
10852
|
+
const resize = (v) => new Vector(x + (v.x - corner.x) * sx, y + (v.y - corner.y) * sy);
|
|
10853
|
+
return new BezierCurve(resize(point.start), resize(point.startControl), resize(point.endControl), resize(point.end));
|
|
10854
|
+
});
|
|
10855
|
+
};
|
|
10856
|
+
//#endregion
|
|
10857
|
+
//#region src/render/canvas/box-shadow-transform.ts
|
|
10858
|
+
const shadowSpace = (matrix, width, height) => {
|
|
10859
|
+
const { a, b, c, d, e, f } = matrix;
|
|
10860
|
+
if (![
|
|
10861
|
+
a,
|
|
10862
|
+
b,
|
|
10863
|
+
c,
|
|
10864
|
+
d,
|
|
10865
|
+
e,
|
|
10866
|
+
f,
|
|
10867
|
+
width,
|
|
10868
|
+
height
|
|
10869
|
+
].every(Number.isFinite)) return null;
|
|
10870
|
+
const determinant = a * d - b * c;
|
|
10871
|
+
if (!Number.isFinite(determinant) || determinant === 0) return null;
|
|
10872
|
+
const inversePoint = (x, y) => ({
|
|
10873
|
+
x: (d * (x - e) - c * (y - f)) / determinant,
|
|
10874
|
+
y: (-b * (x - e) + a * (y - f)) / determinant
|
|
10875
|
+
});
|
|
10876
|
+
const corners = [
|
|
10877
|
+
inversePoint(0, 0),
|
|
10878
|
+
inversePoint(width, 0),
|
|
10879
|
+
inversePoint(width, height),
|
|
10880
|
+
inversePoint(0, height)
|
|
10881
|
+
];
|
|
10882
|
+
if (!corners.every((point) => Number.isFinite(point.x) && Number.isFinite(point.y))) return null;
|
|
10883
|
+
const left = Math.min(...corners.map((point) => point.x));
|
|
10884
|
+
const top = Math.min(...corners.map((point) => point.y));
|
|
10885
|
+
const right = Math.max(...corners.map((point) => point.x));
|
|
10886
|
+
const bottom = Math.max(...corners.map((point) => point.y));
|
|
10887
|
+
const xx = a * a + b * b;
|
|
10888
|
+
const yy = c * c + d * d;
|
|
10889
|
+
const xy = a * c + b * d;
|
|
10890
|
+
const scale = Math.sqrt((xx + yy + Math.hypot(xx - yy, 2 * xy)) / 2);
|
|
10891
|
+
if (!Number.isFinite(scale) || scale <= 0) return null;
|
|
10892
|
+
const tolerance = 1e-10 * Math.max(xx, yy);
|
|
10893
|
+
return {
|
|
10894
|
+
matrix,
|
|
10895
|
+
viewport: new Bounds(left, top, right - left, bottom - top),
|
|
10896
|
+
scale,
|
|
10897
|
+
uniform: Math.abs(xx - yy) <= tolerance && Math.abs(xy) <= tolerance
|
|
10898
|
+
};
|
|
10899
|
+
};
|
|
10900
|
+
//#endregion
|
|
10901
|
+
//#region src/render/canvas/box-shadow-painter.ts
|
|
10902
|
+
const complement = (ctx, rect, hole) => {
|
|
10903
|
+
ctx.beginPath();
|
|
10904
|
+
ctx.rect(rect.left, rect.top, rect.width, rect.height);
|
|
10905
|
+
if (hole.length) {
|
|
10906
|
+
formatCanvasPath(ctx, hole);
|
|
10907
|
+
ctx.closePath();
|
|
10908
|
+
}
|
|
10909
|
+
};
|
|
10910
|
+
const insetHole = (paint, shadow) => {
|
|
10911
|
+
const spread = shadow.spread.number;
|
|
10912
|
+
const box = paddingBox(paint.container);
|
|
10913
|
+
if (box.width - 2 * spread <= 0 || box.height - 2 * spread <= 0) return [];
|
|
10914
|
+
return spreadShadowPath(calculatePaddingBoxPath(paint.curves), -spread);
|
|
10915
|
+
};
|
|
10916
|
+
const paintShadowSurface = async (ctx, paint, shadow, options, budget, space) => {
|
|
10917
|
+
const padding = paddingBox(paint.container);
|
|
10918
|
+
const blur = shadow.blur.number / 2;
|
|
10919
|
+
const margin = Math.ceil(3 * blur) + 2;
|
|
10920
|
+
const spread = shadow.spread.number;
|
|
10921
|
+
const bounds = cropSurface((shadow.inset ? padding : paint.container.bounds.add(shadow.offsetX.number - spread, shadow.offsetY.number - spread, 2 * spread, 2 * spread)).add(-margin, -margin, 2 * margin, 2 * margin), space.viewport, margin, space.scale);
|
|
10922
|
+
if (!bounds.width || !bounds.height) return true;
|
|
10923
|
+
const width = Math.ceil(bounds.width * space.scale);
|
|
10924
|
+
const height = Math.ceil(bounds.height * space.scale);
|
|
10925
|
+
const reserved = reserveSurface(budget, width, height);
|
|
10926
|
+
if (!reserved) return false;
|
|
10927
|
+
let source;
|
|
10928
|
+
let filtered;
|
|
10929
|
+
try {
|
|
10930
|
+
source = ctx.canvas.ownerDocument.createElement("canvas");
|
|
10931
|
+
source.width = width;
|
|
10932
|
+
source.height = height;
|
|
10933
|
+
const sourceCtx = source.getContext("2d");
|
|
10934
|
+
if (!sourceCtx) return false;
|
|
10935
|
+
sourceCtx.scale(space.scale, space.scale);
|
|
10936
|
+
sourceCtx.translate(-bounds.left, -bounds.top);
|
|
10937
|
+
const path = transformPath(shadow.inset ? insetHole(paint, shadow) : spreadShadowPath(calculateBorderBoxPath(paint.curves), spread), shadow.offsetX.number, shadow.offsetY.number, 0, 0);
|
|
10938
|
+
if (shadow.inset) complement(sourceCtx, bounds, path);
|
|
10939
|
+
else createCanvasPath(sourceCtx, path);
|
|
10940
|
+
sourceCtx.fillStyle = asString(shadow.color);
|
|
10941
|
+
sourceCtx.fill(shadow.inset ? "evenodd" : "nonzero");
|
|
10942
|
+
filtered = await renderFilterSurface(source, { blur }, 1, space.scale, options.signal);
|
|
10943
|
+
ctx.save();
|
|
10944
|
+
try {
|
|
10945
|
+
if (shadow.inset) {
|
|
10946
|
+
createCanvasPath(ctx, calculatePaddingBoxPath(paint.curves));
|
|
10947
|
+
ctx.clip();
|
|
10948
|
+
} else {
|
|
10949
|
+
complement(ctx, space.viewport, calculateBorderBoxPath(paint.curves));
|
|
10950
|
+
ctx.clip("evenodd");
|
|
10951
|
+
}
|
|
10952
|
+
ctx.drawImage(filtered, bounds.left, bounds.top, filtered.width / space.scale, filtered.height / space.scale);
|
|
10953
|
+
} finally {
|
|
10954
|
+
ctx.restore();
|
|
10955
|
+
}
|
|
10956
|
+
return true;
|
|
10957
|
+
} catch (error) {
|
|
10958
|
+
if (!(error instanceof FilterSurfaceError)) throw error;
|
|
10959
|
+
return false;
|
|
10960
|
+
} finally {
|
|
10961
|
+
if (filtered && filtered !== source) releaseSurface(filtered);
|
|
10962
|
+
if (source) releaseSurface(source);
|
|
10963
|
+
budget.pixels -= reserved;
|
|
10964
|
+
}
|
|
10965
|
+
};
|
|
10966
|
+
const paintBoxShadow = async (ctx, paint, shadow, options, budget) => {
|
|
10967
|
+
if (options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
10968
|
+
if (isTransparent(shadow.color)) return;
|
|
10969
|
+
const spread = shadow.spread.number;
|
|
10970
|
+
if (!shadow.inset && (paint.container.bounds.width + 2 * spread <= 0 || paint.container.bounds.height + 2 * spread <= 0)) return;
|
|
10971
|
+
const knownTransform = typeof ctx.getTransform === "function";
|
|
10972
|
+
const matrix = knownTransform ? ctx.getTransform() : {
|
|
10973
|
+
a: options.scale,
|
|
10974
|
+
b: 0,
|
|
10975
|
+
c: 0,
|
|
10976
|
+
d: options.scale,
|
|
10977
|
+
e: -options.x * options.scale,
|
|
10978
|
+
f: -options.y * options.scale
|
|
10979
|
+
};
|
|
10980
|
+
const space = shadowSpace(matrix, ctx.canvas.width, ctx.canvas.height);
|
|
10981
|
+
if (!space) return;
|
|
10982
|
+
const { viewport } = space;
|
|
10983
|
+
if (shadow.blur.number > 0 && (shadow.inset || !space.uniform) && await paintShadowSurface(ctx, paint, shadow, options, budget, space)) return;
|
|
10984
|
+
const padding = paddingBox(paint.container);
|
|
10985
|
+
ctx.save();
|
|
10986
|
+
try {
|
|
10987
|
+
if (shadow.inset && shadow.blur.number === 0) {
|
|
10988
|
+
createCanvasPath(ctx, calculatePaddingBoxPath(paint.curves));
|
|
10989
|
+
ctx.clip();
|
|
10990
|
+
complement(ctx, padding, transformPath(insetHole(paint, shadow), shadow.offsetX.number, shadow.offsetY.number, 0, 0));
|
|
10991
|
+
ctx.fillStyle = asString(shadow.color);
|
|
10992
|
+
ctx.fill("evenodd");
|
|
10993
|
+
return;
|
|
10994
|
+
}
|
|
10995
|
+
const margin = Math.ceil(shadow.blur.number * 2 + Math.abs(spread) + Math.abs(shadow.offsetX.number) + Math.abs(shadow.offsetY.number)) + 2;
|
|
10996
|
+
const sourceBounds = shadow.inset ? padding.add(-margin, -margin, 2 * margin, 2 * margin) : paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
10997
|
+
const { a, b, c, d, e, f } = matrix;
|
|
10998
|
+
const right = sourceBounds.left + sourceBounds.width;
|
|
10999
|
+
const bottom = sourceBounds.top + sourceBounds.height;
|
|
11000
|
+
const maxX = Math.max(a * sourceBounds.left + c * sourceBounds.top + e, a * right + c * sourceBounds.top + e, a * right + c * bottom + e, a * sourceBounds.left + c * bottom + e);
|
|
11001
|
+
const displacement = Math.max(SHADOW_MASK_OFFSET, maxX + margin * space.scale + 2);
|
|
11002
|
+
if (shadow.inset) {
|
|
11003
|
+
createCanvasPath(ctx, calculatePaddingBoxPath(paint.curves));
|
|
11004
|
+
ctx.clip();
|
|
11005
|
+
} else {
|
|
11006
|
+
complement(ctx, viewport, calculateBorderBoxPath(paint.curves));
|
|
11007
|
+
ctx.clip("evenodd");
|
|
11008
|
+
}
|
|
11009
|
+
if (knownTransform) ctx.setTransform(a, b, c, d, e - displacement, f);
|
|
11010
|
+
else ctx.translate(-displacement / a, 0);
|
|
11011
|
+
if (shadow.inset) complement(ctx, sourceBounds, insetHole(paint, shadow));
|
|
11012
|
+
else createCanvasPath(ctx, spreadShadowPath(calculateBorderBoxPath(paint.curves), spread));
|
|
11013
|
+
if (knownTransform) ctx.setTransform(a, b, c, d, e, f);
|
|
11014
|
+
else ctx.translate(displacement / a, 0);
|
|
11015
|
+
ctx.shadowOffsetX = displacement + a * shadow.offsetX.number + c * shadow.offsetY.number;
|
|
11016
|
+
ctx.shadowOffsetY = b * shadow.offsetX.number + d * shadow.offsetY.number;
|
|
11017
|
+
ctx.shadowBlur = shadow.blur.number * space.scale;
|
|
11018
|
+
ctx.shadowColor = asString(shadow.color);
|
|
11019
|
+
ctx.fillStyle = "rgb(0, 0, 0)";
|
|
11020
|
+
ctx.fill(shadow.inset ? "evenodd" : "nonzero");
|
|
11021
|
+
} finally {
|
|
11022
|
+
ctx.restore();
|
|
11023
|
+
}
|
|
11024
|
+
};
|
|
11025
|
+
//#endregion
|
|
10546
11026
|
//#region src/render/canvas/canvas-renderer.ts
|
|
10547
11027
|
var CanvasRenderer = class CanvasRenderer {
|
|
10548
|
-
constructor(context, options) {
|
|
11028
|
+
constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
|
|
11029
|
+
this.surfaceRoot = surfaceRoot;
|
|
11030
|
+
this.surfaceBudget = surfaceBudget;
|
|
11031
|
+
this.legacySubtree = false;
|
|
10549
11032
|
this.context = context;
|
|
10550
11033
|
this.options = options;
|
|
10551
11034
|
this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
|
|
10552
11035
|
const ctx = this.canvas.getContext("2d");
|
|
10553
|
-
if (!ctx)
|
|
11036
|
+
if (!ctx) {
|
|
11037
|
+
if (!options.canvas) releaseSurface(this.canvas);
|
|
11038
|
+
if (surfaceRoot) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
11039
|
+
throw new Error("Failed to get 2D rendering context from canvas");
|
|
11040
|
+
}
|
|
10554
11041
|
this.ctx = ctx;
|
|
10555
11042
|
if (!options.canvas) {
|
|
10556
11043
|
this.canvas.width = Math.floor(options.width * options.scale);
|
|
@@ -10586,7 +11073,77 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10586
11073
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
10587
11074
|
}
|
|
10588
11075
|
async renderStack(stack) {
|
|
10589
|
-
|
|
11076
|
+
const styles = stack.element.container.styles;
|
|
11077
|
+
if (styles.isVisible()) {
|
|
11078
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
11079
|
+
if (stack.element !== this.surfaceRoot && !this.legacySubtree && filter && (styles.filter || styles.opacity < 1) && this.canComposite(stack.element)) {
|
|
11080
|
+
if (!await this.renderCompositedStack(stack, filter)) {
|
|
11081
|
+
this.legacySubtree = true;
|
|
11082
|
+
try {
|
|
11083
|
+
await this.renderStackContent(stack);
|
|
11084
|
+
} finally {
|
|
11085
|
+
this.legacySubtree = false;
|
|
11086
|
+
}
|
|
11087
|
+
}
|
|
11088
|
+
return;
|
|
11089
|
+
}
|
|
11090
|
+
await this.renderStackContent(stack);
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
canComposite(paint) {
|
|
11094
|
+
const supported = (container) => {
|
|
11095
|
+
const styles = container.styles;
|
|
11096
|
+
return !styles.isTransformed() && styles.zoom === 1 && styles.mixBlendMode === MIX_BLEND_MODE$1.NORMAL && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
|
|
11097
|
+
};
|
|
11098
|
+
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
11099
|
+
let ancestor = paint.parent;
|
|
11100
|
+
while (ancestor && ancestor !== this.surfaceRoot) {
|
|
11101
|
+
if (!supported(ancestor.container)) return false;
|
|
11102
|
+
ancestor = ancestor.parent;
|
|
11103
|
+
}
|
|
11104
|
+
return subtree(paint.container);
|
|
11105
|
+
}
|
|
11106
|
+
async renderCompositedStack(stack, filter) {
|
|
11107
|
+
const signal = this.options.signal;
|
|
11108
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
11109
|
+
const bounds = cropSurface(surfaceBounds(stack.element.container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filterOutset(filter), this.options.scale);
|
|
11110
|
+
if (!bounds.width || !bounds.height) return true;
|
|
11111
|
+
const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
11112
|
+
if (!reserved) {
|
|
11113
|
+
this.context.logger.info("Filter surface budget exceeded; using the existing renderer");
|
|
11114
|
+
return false;
|
|
11115
|
+
}
|
|
11116
|
+
const options = {
|
|
11117
|
+
...this.options,
|
|
11118
|
+
canvas: void 0,
|
|
11119
|
+
backgroundColor: null,
|
|
11120
|
+
x: bounds.left,
|
|
11121
|
+
y: bounds.top,
|
|
11122
|
+
width: bounds.width,
|
|
11123
|
+
height: bounds.height
|
|
11124
|
+
};
|
|
11125
|
+
let source;
|
|
11126
|
+
let filtered;
|
|
11127
|
+
try {
|
|
11128
|
+
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
11129
|
+
await source.renderStackContent(stack);
|
|
11130
|
+
source.effectsRenderer.applyEffects([]);
|
|
11131
|
+
filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
|
|
11132
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
11133
|
+
const own = stack.element.effects;
|
|
11134
|
+
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
|
|
11135
|
+
this.effectsRenderer.applyEffects(effects);
|
|
11136
|
+
this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
|
|
11137
|
+
return true;
|
|
11138
|
+
} catch (error) {
|
|
11139
|
+
if (!(error instanceof FilterSurfaceError)) throw error;
|
|
11140
|
+
this.context.logger.info(`${error.message}; using the existing renderer`);
|
|
11141
|
+
return false;
|
|
11142
|
+
} finally {
|
|
11143
|
+
if (source) releaseSurface(source.canvas);
|
|
11144
|
+
if (filtered) releaseSurface(filtered);
|
|
11145
|
+
this.surfaceBudget.pixels -= reserved;
|
|
11146
|
+
}
|
|
10590
11147
|
}
|
|
10591
11148
|
async renderNode(paint) {
|
|
10592
11149
|
if (paint.container.debugRender) debugger;
|
|
@@ -10614,7 +11171,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10614
11171
|
}
|
|
10615
11172
|
}
|
|
10616
11173
|
async renderNodeContent(paint) {
|
|
10617
|
-
this.effectsRenderer.applyEffects(paint.getEffects(4));
|
|
11174
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
|
|
10618
11175
|
const container = paint.container;
|
|
10619
11176
|
const curves = paint.curves;
|
|
10620
11177
|
const styles = container.styles;
|
|
@@ -10678,8 +11235,51 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10678
11235
|
formatPath(paths) {
|
|
10679
11236
|
formatCanvasPath(this.ctx, paths);
|
|
10680
11237
|
}
|
|
11238
|
+
async renderSurfaceBoxShadow(paint, shadow) {
|
|
11239
|
+
const spread = shadow.spread.number;
|
|
11240
|
+
const blur = shadow.blur.number / 2;
|
|
11241
|
+
const margin = Math.ceil(3 * blur) + 1;
|
|
11242
|
+
const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
11243
|
+
if (box.width <= 0 || box.height <= 0) return;
|
|
11244
|
+
const scale = this.options.scale;
|
|
11245
|
+
const bounds = cropSurface(box.add(-margin, -margin, 2 * margin, 2 * margin), new Bounds(this.options.x - shadow.offsetX.number, this.options.y - shadow.offsetY.number, this.options.width, this.options.height), margin, scale);
|
|
11246
|
+
if (!bounds.width || !bounds.height) return;
|
|
11247
|
+
const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
|
|
11248
|
+
const reserved = reserveSurface(this.surfaceBudget, width, height);
|
|
11249
|
+
if (!reserved) throw new FilterSurfaceError("Box shadow surface budget exceeded");
|
|
11250
|
+
let source;
|
|
11251
|
+
let filtered;
|
|
11252
|
+
try {
|
|
11253
|
+
source = document.createElement("canvas");
|
|
11254
|
+
source.width = width;
|
|
11255
|
+
source.height = height;
|
|
11256
|
+
const ctx = source.getContext("2d");
|
|
11257
|
+
if (!ctx) throw new FilterSurfaceError("Box shadow surface canvas is unavailable");
|
|
11258
|
+
ctx.scale(scale, scale);
|
|
11259
|
+
ctx.translate(-bounds.left, -bounds.top);
|
|
11260
|
+
createCanvasPath(ctx, transformPath(calculateBorderBoxPath(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
|
|
11261
|
+
ctx.fillStyle = asString(shadow.color);
|
|
11262
|
+
ctx.fill();
|
|
11263
|
+
filtered = await renderFilterSurface(source, { blur }, 1, scale, this.options.signal);
|
|
11264
|
+
this.ctx.save();
|
|
11265
|
+
try {
|
|
11266
|
+
this.ctx.beginPath();
|
|
11267
|
+
this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11268
|
+
formatCanvasPath(this.ctx, calculateBorderBoxPath(paint.curves));
|
|
11269
|
+
this.ctx.closePath();
|
|
11270
|
+
this.ctx.clip("evenodd");
|
|
11271
|
+
this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
|
|
11272
|
+
} finally {
|
|
11273
|
+
this.ctx.restore();
|
|
11274
|
+
}
|
|
11275
|
+
} finally {
|
|
11276
|
+
if (source) releaseSurface(source);
|
|
11277
|
+
if (filtered) releaseSurface(filtered);
|
|
11278
|
+
this.surfaceBudget.pixels -= reserved;
|
|
11279
|
+
}
|
|
11280
|
+
}
|
|
10681
11281
|
async renderNodeBackgroundAndBorders(paint) {
|
|
10682
|
-
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
11282
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
|
|
10683
11283
|
const styles = paint.container.styles;
|
|
10684
11284
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10685
11285
|
const hasTextClip = hasTextBackgroundClip(styles);
|
|
@@ -10720,28 +11320,13 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10720
11320
|
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10721
11321
|
this.ctx.restore();
|
|
10722
11322
|
}
|
|
10723
|
-
styles.boxShadow.slice(0).reverse()
|
|
10724
|
-
this.
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
const shadowPaintingArea = transformPath(borderBoxArea, -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, (shadow.inset ? 1 : -1) * shadow.spread.number, shadow.spread.number * (shadow.inset ? -2 : 2), shadow.spread.number * (shadow.inset ? -2 : 2));
|
|
10728
|
-
if (shadow.inset) {
|
|
10729
|
-
this.path(borderBoxArea);
|
|
10730
|
-
this.ctx.clip();
|
|
10731
|
-
this.mask(shadowPaintingArea);
|
|
10732
|
-
} else {
|
|
10733
|
-
this.mask(borderBoxArea);
|
|
10734
|
-
this.ctx.clip();
|
|
10735
|
-
this.path(shadowPaintingArea);
|
|
11323
|
+
for (const shadow of styles.boxShadow.slice(0).reverse()) {
|
|
11324
|
+
if (this.surfaceRoot && !shadow.inset) {
|
|
11325
|
+
await this.renderSurfaceBoxShadow(paint, shadow);
|
|
11326
|
+
continue;
|
|
10736
11327
|
}
|
|
10737
|
-
this.ctx
|
|
10738
|
-
|
|
10739
|
-
this.ctx.shadowColor = asString(shadow.color);
|
|
10740
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
10741
|
-
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
|
|
10742
|
-
this.ctx.fill();
|
|
10743
|
-
this.ctx.restore();
|
|
10744
|
-
});
|
|
11328
|
+
await paintBoxShadow(this.ctx, paint, shadow, this.options, this.surfaceBudget);
|
|
11329
|
+
}
|
|
10745
11330
|
}
|
|
10746
11331
|
if (styles.borderImageSource) {
|
|
10747
11332
|
const source = styles.borderImageSource;
|
|
@@ -10797,14 +11382,19 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10797
11382
|
}
|
|
10798
11383
|
}
|
|
10799
11384
|
async render(element) {
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
11385
|
+
try {
|
|
11386
|
+
if (this.options.backgroundColor) {
|
|
11387
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
11388
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11389
|
+
}
|
|
11390
|
+
const stack = parseStackingContexts(element);
|
|
11391
|
+
await this.renderStack(stack);
|
|
11392
|
+
this.effectsRenderer.applyEffects([]);
|
|
11393
|
+
return this.canvas;
|
|
11394
|
+
} catch (error) {
|
|
11395
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
11396
|
+
throw error;
|
|
10803
11397
|
}
|
|
10804
|
-
const stack = parseStackingContexts(element);
|
|
10805
|
-
await this.renderStack(stack);
|
|
10806
|
-
this.effectsRenderer.applyEffects([]);
|
|
10807
|
-
return this.canvas;
|
|
10808
11398
|
}
|
|
10809
11399
|
/**
|
|
10810
11400
|
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
@@ -11220,22 +11810,6 @@ var Context = class Context {
|
|
|
11220
11810
|
}
|
|
11221
11811
|
};
|
|
11222
11812
|
//#endregion
|
|
11223
|
-
//#region src/core/abort-helper.ts
|
|
11224
|
-
/**
|
|
11225
|
-
* AbortSignal helper utilities.
|
|
11226
|
-
*/
|
|
11227
|
-
/**
|
|
11228
|
-
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
11229
|
-
* Used at checkpoints throughout the rendering pipeline to support
|
|
11230
|
-
* cancellable renders.
|
|
11231
|
-
*
|
|
11232
|
-
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
11233
|
-
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
11234
|
-
*/
|
|
11235
|
-
const throwIfAborted = (signal) => {
|
|
11236
|
-
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
11237
|
-
};
|
|
11238
|
-
//#endregion
|
|
11239
11813
|
//#region src/core/config-assembler.ts
|
|
11240
11814
|
/**
|
|
11241
11815
|
* Coerce known numeric options from string (or other) values to actual numbers.
|
|
@@ -11404,18 +11978,19 @@ const renderElement = async (element, opts, config) => {
|
|
|
11404
11978
|
canvas = await new CanvasRenderer(context, renderOptions).render(root);
|
|
11405
11979
|
perfMonitor.end("render");
|
|
11406
11980
|
}
|
|
11981
|
+
} finally {
|
|
11407
11982
|
perfMonitor.start("cleanup");
|
|
11408
|
-
|
|
11409
|
-
if (
|
|
11983
|
+
try {
|
|
11984
|
+
if (root) root.restoreTree();
|
|
11985
|
+
} finally {
|
|
11986
|
+
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
11987
|
+
perfMonitor.end("cleanup");
|
|
11410
11988
|
}
|
|
11411
|
-
perfMonitor.end("cleanup");
|
|
11412
|
-
perfMonitor.end("total");
|
|
11413
|
-
context.logger.debug(`Finished rendering`);
|
|
11414
|
-
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11415
|
-
return canvas;
|
|
11416
|
-
} finally {
|
|
11417
|
-
if (root) root.restoreTree();
|
|
11418
11989
|
}
|
|
11990
|
+
perfMonitor.end("total");
|
|
11991
|
+
context.logger.debug(`Finished rendering`);
|
|
11992
|
+
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11993
|
+
return canvas;
|
|
11419
11994
|
};
|
|
11420
11995
|
//#endregion
|
|
11421
11996
|
//#region src/index.ts
|