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
|
@@ -109,24 +109,23 @@ const renderElement = async (element, opts, config) => {
|
|
|
109
109
|
canvas = await renderer.render(root);
|
|
110
110
|
perfMonitor.end('render');
|
|
111
111
|
}
|
|
112
|
-
perfMonitor.start('cleanup');
|
|
113
|
-
if (opts.removeContainer ?? true) {
|
|
114
|
-
if (!document_cloner_1.DocumentCloner.destroy(container)) {
|
|
115
|
-
context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
perfMonitor.end('cleanup');
|
|
119
|
-
perfMonitor.end('total');
|
|
120
|
-
context.logger.debug(`Finished rendering`);
|
|
121
|
-
if (performanceMonitoring) {
|
|
122
|
-
perfMonitor.logSummary();
|
|
123
|
-
}
|
|
124
|
-
return canvas;
|
|
125
112
|
}
|
|
126
113
|
finally {
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
perfMonitor.start('cleanup');
|
|
115
|
+
try {
|
|
116
|
+
if (root)
|
|
117
|
+
root.restoreTree();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (opts.removeContainer ?? true)
|
|
121
|
+
document_cloner_1.DocumentCloner.destroy(container);
|
|
122
|
+
perfMonitor.end('cleanup');
|
|
129
123
|
}
|
|
130
124
|
}
|
|
125
|
+
perfMonitor.end('total');
|
|
126
|
+
context.logger.debug(`Finished rendering`);
|
|
127
|
+
if (performanceMonitoring)
|
|
128
|
+
perfMonitor.logSummary();
|
|
129
|
+
return canvas;
|
|
131
130
|
};
|
|
132
131
|
exports.renderElement = renderElement;
|
|
@@ -19,7 +19,7 @@ exports.filter = {
|
|
|
19
19
|
// Canvas API supports the same CSS filter string format
|
|
20
20
|
switch (fn.name) {
|
|
21
21
|
case 'blur':
|
|
22
|
-
parts.push(`blur(${renderedArgs}
|
|
22
|
+
parts.push(`blur(${renderedArgs})`);
|
|
23
23
|
break;
|
|
24
24
|
case 'brightness':
|
|
25
25
|
case 'contrast':
|
|
@@ -33,7 +33,7 @@ exports.filter = {
|
|
|
33
33
|
parts.push(`grayscale(${renderedArgs})`);
|
|
34
34
|
break;
|
|
35
35
|
case 'hue-rotate':
|
|
36
|
-
parts.push(`hue-rotate(${renderedArgs}
|
|
36
|
+
parts.push(`hue-rotate(${renderedArgs})`);
|
|
37
37
|
break;
|
|
38
38
|
case 'drop-shadow':
|
|
39
39
|
parts.push(`drop-shadow(${renderedArgs})`);
|
|
@@ -54,9 +54,19 @@ const renderFilterArgs = (values) => {
|
|
|
54
54
|
const parts = [];
|
|
55
55
|
for (const v of values) {
|
|
56
56
|
if (v.type === 31 /* TokenType.WHITESPACE_TOKEN */) {
|
|
57
|
+
parts.push(' ');
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
|
-
if (v.type ===
|
|
60
|
+
if (v.type === 18 /* TokenType.FUNCTION */) {
|
|
61
|
+
parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
|
|
62
|
+
}
|
|
63
|
+
else if (v.type === 4 /* TokenType.COMMA_TOKEN */) {
|
|
64
|
+
parts.push(',');
|
|
65
|
+
}
|
|
66
|
+
else if (v.type === 6 /* TokenType.DELIM_TOKEN */) {
|
|
67
|
+
parts.push(v.value);
|
|
68
|
+
}
|
|
69
|
+
else if (v.type === 15 /* TokenType.DIMENSION_TOKEN */) {
|
|
60
70
|
parts.push(`${v.number}${v.unit}`);
|
|
61
71
|
}
|
|
62
72
|
else if (v.type === 17 /* TokenType.NUMBER_TOKEN */) {
|
|
@@ -72,5 +82,5 @@ const renderFilterArgs = (values) => {
|
|
|
72
82
|
parts.push(`#${v.value}`);
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
|
-
return parts.join('
|
|
85
|
+
return parts.join('').trim();
|
|
76
86
|
};
|
|
@@ -106,6 +106,7 @@ exports.parseTree = parseTree;
|
|
|
106
106
|
const createsRealStackingContext = (node, container, root) => {
|
|
107
107
|
return (container.styles.isPositionedWithZIndex() ||
|
|
108
108
|
container.styles.opacity < 1 ||
|
|
109
|
+
Boolean(container.styles.filter) ||
|
|
109
110
|
container.styles.isTransformed() ||
|
|
110
111
|
((0, node_type_guards_1.isBodyElement)(node) && root.styles.isTransparent()));
|
|
111
112
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spreadShadowPath = exports.shadowRadius = void 0;
|
|
4
|
+
const bezier_curve_1 = require("../bezier-curve");
|
|
5
|
+
const vector_1 = require("../vector");
|
|
6
|
+
// CSS Backgrounds 3, 6.1.1: growing a small corner needs the cubic adjustment
|
|
7
|
+
// to approach a square continuously as the original radius approaches zero.
|
|
8
|
+
const shadowRadius = (radius, spread) => {
|
|
9
|
+
const delta = spread > radius ? spread * (1 + Math.pow(radius / spread - 1, 3)) : spread;
|
|
10
|
+
return Math.max(0, radius + delta);
|
|
11
|
+
};
|
|
12
|
+
exports.shadowRadius = shadowRadius;
|
|
13
|
+
// Input is the four clockwise border/padding corner segments from BoundCurves.
|
|
14
|
+
// Translation alone preserves the old radii and turns a spread circle into a
|
|
15
|
+
// rounded square. Resize each quarter ellipse around its box corner instead.
|
|
16
|
+
const spreadShadowPath = (paths, spread) => {
|
|
17
|
+
if (spread === 0)
|
|
18
|
+
return paths;
|
|
19
|
+
const corners = paths.map((point, index) => {
|
|
20
|
+
if (!(0, bezier_curve_1.isBezierCurve)(point))
|
|
21
|
+
return { x: point.x, y: point.y, rx: 0, ry: 0 };
|
|
22
|
+
return {
|
|
23
|
+
x: index % 2 === 0 ? point.start.x : point.end.x,
|
|
24
|
+
y: index % 2 === 0 ? point.end.y : point.start.y,
|
|
25
|
+
rx: Math.abs(point.end.x - point.start.x),
|
|
26
|
+
ry: Math.abs(point.end.y - point.start.y)
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const width = corners[1].x - corners[0].x + 2 * spread;
|
|
30
|
+
const height = corners[3].y - corners[0].y + 2 * spread;
|
|
31
|
+
if (width <= 0 || height <= 0)
|
|
32
|
+
return [];
|
|
33
|
+
const radii = corners.map((c) => ({ x: (0, exports.shadowRadius)(c.rx, spread), y: (0, exports.shadowRadius)(c.ry, spread) }));
|
|
34
|
+
const ratio = (size, sum) => (sum > 0 ? size / sum : 1);
|
|
35
|
+
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));
|
|
36
|
+
return paths.map((point, index) => {
|
|
37
|
+
const corner = corners[index];
|
|
38
|
+
const x = corner.x + (index === 1 || index === 2 ? spread : -spread);
|
|
39
|
+
const y = corner.y + (index >= 2 ? spread : -spread);
|
|
40
|
+
if (!(0, bezier_curve_1.isBezierCurve)(point))
|
|
41
|
+
return new vector_1.Vector(x, y);
|
|
42
|
+
const rx = radii[index].x * factor;
|
|
43
|
+
const ry = radii[index].y * factor;
|
|
44
|
+
if (!rx || !ry)
|
|
45
|
+
return new vector_1.Vector(x, y);
|
|
46
|
+
const sx = rx / corner.rx;
|
|
47
|
+
const sy = ry / corner.ry;
|
|
48
|
+
const resize = (v) => new vector_1.Vector(x + (v.x - corner.x) * sx, y + (v.y - corner.y) * sy);
|
|
49
|
+
return new bezier_curve_1.BezierCurve(resize(point.start), resize(point.startControl), resize(point.endControl), resize(point.end));
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
exports.spreadShadowPath = spreadShadowPath;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paintBoxShadow = void 0;
|
|
4
|
+
const color_utilities_1 = require("../../css/types/color-utilities");
|
|
5
|
+
const constants_1 = require("../../core/constants");
|
|
6
|
+
const bound_curves_1 = require("../bound-curves");
|
|
7
|
+
const box_sizing_1 = require("../box-sizing");
|
|
8
|
+
const path_1 = require("../path");
|
|
9
|
+
const canvas_path_1 = require("./canvas-path");
|
|
10
|
+
const filter_surface_1 = require("./filter-surface");
|
|
11
|
+
const surface_bounds_1 = require("./surface-bounds");
|
|
12
|
+
const box_shadow_geometry_1 = require("./box-shadow-geometry");
|
|
13
|
+
const box_shadow_transform_1 = require("./box-shadow-transform");
|
|
14
|
+
// Separate closed subpaths plus even-odd filling avoid reversing only the order
|
|
15
|
+
// of corner Beziers, which does not reverse the curves themselves.
|
|
16
|
+
const complement = (ctx, rect, hole) => {
|
|
17
|
+
ctx.beginPath();
|
|
18
|
+
ctx.rect(rect.left, rect.top, rect.width, rect.height);
|
|
19
|
+
if (hole.length) {
|
|
20
|
+
(0, canvas_path_1.formatCanvasPath)(ctx, hole);
|
|
21
|
+
ctx.closePath();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const insetHole = (paint, shadow) => {
|
|
25
|
+
const spread = shadow.spread.number;
|
|
26
|
+
const box = (0, box_sizing_1.paddingBox)(paint.container);
|
|
27
|
+
if (box.width - 2 * spread <= 0 || box.height - 2 * spread <= 0)
|
|
28
|
+
return [];
|
|
29
|
+
return (0, box_shadow_geometry_1.spreadShadowPath)((0, bound_curves_1.calculatePaddingBoxPath)(paint.curves), -spread);
|
|
30
|
+
};
|
|
31
|
+
// Rasterize in local coordinates, blur once, and then let the destination CTM
|
|
32
|
+
// transform the complete shadow. This also preserves anisotropic/sheared blur.
|
|
33
|
+
const paintShadowSurface = async (ctx, paint, shadow, options, budget, space) => {
|
|
34
|
+
const padding = (0, box_sizing_1.paddingBox)(paint.container);
|
|
35
|
+
const blur = shadow.blur.number / 2;
|
|
36
|
+
const margin = Math.ceil(3 * blur) + 2;
|
|
37
|
+
const spread = shadow.spread.number;
|
|
38
|
+
const area = shadow.inset
|
|
39
|
+
? padding
|
|
40
|
+
: paint.container.bounds.add(shadow.offsetX.number - spread, shadow.offsetY.number - spread, 2 * spread, 2 * spread);
|
|
41
|
+
const bounds = (0, surface_bounds_1.cropSurface)(area.add(-margin, -margin, 2 * margin, 2 * margin), space.viewport, margin, space.scale);
|
|
42
|
+
if (!bounds.width || !bounds.height)
|
|
43
|
+
return true;
|
|
44
|
+
const width = Math.ceil(bounds.width * space.scale);
|
|
45
|
+
const height = Math.ceil(bounds.height * space.scale);
|
|
46
|
+
const reserved = (0, surface_bounds_1.reserveSurface)(budget, width, height);
|
|
47
|
+
if (!reserved)
|
|
48
|
+
return false;
|
|
49
|
+
let source;
|
|
50
|
+
let filtered;
|
|
51
|
+
try {
|
|
52
|
+
source = ctx.canvas.ownerDocument.createElement('canvas');
|
|
53
|
+
source.width = width;
|
|
54
|
+
source.height = height;
|
|
55
|
+
const sourceCtx = source.getContext('2d');
|
|
56
|
+
if (!sourceCtx)
|
|
57
|
+
return false;
|
|
58
|
+
sourceCtx.scale(space.scale, space.scale);
|
|
59
|
+
sourceCtx.translate(-bounds.left, -bounds.top);
|
|
60
|
+
const silhouette = shadow.inset
|
|
61
|
+
? insetHole(paint, shadow)
|
|
62
|
+
: (0, box_shadow_geometry_1.spreadShadowPath)((0, bound_curves_1.calculateBorderBoxPath)(paint.curves), spread);
|
|
63
|
+
const path = (0, path_1.transformPath)(silhouette, shadow.offsetX.number, shadow.offsetY.number, 0, 0);
|
|
64
|
+
if (shadow.inset)
|
|
65
|
+
complement(sourceCtx, bounds, path);
|
|
66
|
+
else
|
|
67
|
+
(0, canvas_path_1.createCanvasPath)(sourceCtx, path);
|
|
68
|
+
sourceCtx.fillStyle = (0, color_utilities_1.asString)(shadow.color);
|
|
69
|
+
sourceCtx.fill(shadow.inset ? 'evenodd' : 'nonzero');
|
|
70
|
+
filtered = await (0, filter_surface_1.renderFilterSurface)(source, { blur }, 1, space.scale, options.signal);
|
|
71
|
+
ctx.save();
|
|
72
|
+
try {
|
|
73
|
+
if (shadow.inset) {
|
|
74
|
+
(0, canvas_path_1.createCanvasPath)(ctx, (0, bound_curves_1.calculatePaddingBoxPath)(paint.curves));
|
|
75
|
+
ctx.clip();
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
complement(ctx, space.viewport, (0, bound_curves_1.calculateBorderBoxPath)(paint.curves));
|
|
79
|
+
ctx.clip('evenodd');
|
|
80
|
+
}
|
|
81
|
+
ctx.drawImage(filtered, bounds.left, bounds.top, filtered.width / space.scale, filtered.height / space.scale);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
ctx.restore();
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!(error instanceof filter_surface_1.FilterSurfaceError))
|
|
90
|
+
throw error;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (filtered && filtered !== source)
|
|
95
|
+
(0, filter_surface_1.releaseSurface)(filtered);
|
|
96
|
+
if (source)
|
|
97
|
+
(0, filter_surface_1.releaseSurface)(source);
|
|
98
|
+
budget.pixels -= reserved;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const paintBoxShadow = async (ctx, paint, shadow, options, budget) => {
|
|
102
|
+
if (options.signal?.aborted)
|
|
103
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
104
|
+
if ((0, color_utilities_1.isTransparent)(shadow.color))
|
|
105
|
+
return;
|
|
106
|
+
const spread = shadow.spread.number;
|
|
107
|
+
if (!shadow.inset &&
|
|
108
|
+
(paint.container.bounds.width + 2 * spread <= 0 || paint.container.bounds.height + 2 * spread <= 0))
|
|
109
|
+
return;
|
|
110
|
+
// getTransform includes capture scale, CSS transforms and their origins.
|
|
111
|
+
// Without it the CTM is unknown: approximate it with the capture transform
|
|
112
|
+
// and only ever move the mask relative to the real CTM (see below).
|
|
113
|
+
const knownTransform = typeof ctx.getTransform === 'function';
|
|
114
|
+
const matrix = knownTransform
|
|
115
|
+
? ctx.getTransform()
|
|
116
|
+
: {
|
|
117
|
+
a: options.scale,
|
|
118
|
+
b: 0,
|
|
119
|
+
c: 0,
|
|
120
|
+
d: options.scale,
|
|
121
|
+
e: -options.x * options.scale,
|
|
122
|
+
f: -options.y * options.scale
|
|
123
|
+
};
|
|
124
|
+
const space = (0, box_shadow_transform_1.shadowSpace)(matrix, ctx.canvas.width, ctx.canvas.height);
|
|
125
|
+
// Singular transforms have zero painted area; never attempt to invert them.
|
|
126
|
+
if (!space)
|
|
127
|
+
return;
|
|
128
|
+
const { viewport } = space;
|
|
129
|
+
if (shadow.blur.number > 0 &&
|
|
130
|
+
(shadow.inset || !space.uniform) &&
|
|
131
|
+
(await paintShadowSurface(ctx, paint, shadow, options, budget, space)))
|
|
132
|
+
return;
|
|
133
|
+
const padding = (0, box_sizing_1.paddingBox)(paint.container);
|
|
134
|
+
ctx.save();
|
|
135
|
+
try {
|
|
136
|
+
if (shadow.inset && shadow.blur.number === 0) {
|
|
137
|
+
(0, canvas_path_1.createCanvasPath)(ctx, (0, bound_curves_1.calculatePaddingBoxPath)(paint.curves));
|
|
138
|
+
ctx.clip();
|
|
139
|
+
complement(ctx, padding, (0, path_1.transformPath)(insetHole(paint, shadow), shadow.offsetX.number, shadow.offsetY.number, 0, 0));
|
|
140
|
+
ctx.fillStyle = (0, color_utilities_1.asString)(shadow.color);
|
|
141
|
+
ctx.fill('evenodd');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const margin = Math.ceil(shadow.blur.number * 2 +
|
|
145
|
+
Math.abs(spread) +
|
|
146
|
+
Math.abs(shadow.offsetX.number) +
|
|
147
|
+
Math.abs(shadow.offsetY.number)) + 2;
|
|
148
|
+
// Displace in OUTPUT pixels, not along a CSS axis. A rotated or reflected
|
|
149
|
+
// local x axis need not point off the left edge of the output bitmap.
|
|
150
|
+
const sourceBounds = shadow.inset
|
|
151
|
+
? padding.add(-margin, -margin, 2 * margin, 2 * margin)
|
|
152
|
+
: paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
153
|
+
const { a, b, c, d, e, f } = matrix;
|
|
154
|
+
const right = sourceBounds.left + sourceBounds.width;
|
|
155
|
+
const bottom = sourceBounds.top + sourceBounds.height;
|
|
156
|
+
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);
|
|
157
|
+
const displacement = Math.max(constants_1.SHADOW_MASK_OFFSET, maxX + margin * space.scale + 2);
|
|
158
|
+
if (shadow.inset) {
|
|
159
|
+
(0, canvas_path_1.createCanvasPath)(ctx, (0, bound_curves_1.calculatePaddingBoxPath)(paint.curves));
|
|
160
|
+
ctx.clip();
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
complement(ctx, viewport, (0, bound_curves_1.calculateBorderBoxPath)(paint.curves));
|
|
164
|
+
ctx.clip('evenodd');
|
|
165
|
+
}
|
|
166
|
+
// Current paths retain their device coordinates across transform changes.
|
|
167
|
+
// setTransform with an approximate matrix would discard CSS transforms
|
|
168
|
+
// on the real CTM, so the legacy path translates relative to it instead.
|
|
169
|
+
if (knownTransform)
|
|
170
|
+
ctx.setTransform(a, b, c, d, e - displacement, f);
|
|
171
|
+
else
|
|
172
|
+
ctx.translate(-displacement / a, 0);
|
|
173
|
+
if (shadow.inset)
|
|
174
|
+
complement(ctx, sourceBounds, insetHole(paint, shadow));
|
|
175
|
+
else
|
|
176
|
+
(0, canvas_path_1.createCanvasPath)(ctx, (0, box_shadow_geometry_1.spreadShadowPath)((0, bound_curves_1.calculateBorderBoxPath)(paint.curves), spread));
|
|
177
|
+
if (knownTransform)
|
|
178
|
+
ctx.setTransform(a, b, c, d, e, f);
|
|
179
|
+
else
|
|
180
|
+
ctx.translate(displacement / a, 0);
|
|
181
|
+
ctx.shadowOffsetX = displacement + a * shadow.offsetX.number + c * shadow.offsetY.number;
|
|
182
|
+
ctx.shadowOffsetY = b * shadow.offsetX.number + d * shadow.offsetY.number;
|
|
183
|
+
// Allocation/filter-failure fallback remains bounded-memory. Its blur is
|
|
184
|
+
// isotropic under nonuniform transforms, but positioning stays correct.
|
|
185
|
+
ctx.shadowBlur = shadow.blur.number * space.scale;
|
|
186
|
+
ctx.shadowColor = (0, color_utilities_1.asString)(shadow.color);
|
|
187
|
+
ctx.fillStyle = 'rgb(0, 0, 0)';
|
|
188
|
+
ctx.fill(shadow.inset ? 'evenodd' : 'nonzero');
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
ctx.restore();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
exports.paintBoxShadow = paintBoxShadow;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shadowSpace = void 0;
|
|
4
|
+
const bounds_1 = require("../../css/layout/bounds");
|
|
5
|
+
// Map the output bitmap back into the current user space. Capture x/y are not
|
|
6
|
+
// in this space once CSS transforms (including ancestor transforms) are active.
|
|
7
|
+
// An AABB of all four inverse-transformed corners is conservative under rotation.
|
|
8
|
+
const shadowSpace = (matrix, width, height) => {
|
|
9
|
+
const { a, b, c, d, e, f } = matrix;
|
|
10
|
+
if (![a, b, c, d, e, f, width, height].every(Number.isFinite))
|
|
11
|
+
return null;
|
|
12
|
+
const determinant = a * d - b * c;
|
|
13
|
+
if (!Number.isFinite(determinant) || determinant === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const inversePoint = (x, y) => ({
|
|
16
|
+
x: (d * (x - e) - c * (y - f)) / determinant,
|
|
17
|
+
y: (-b * (x - e) + a * (y - f)) / determinant
|
|
18
|
+
});
|
|
19
|
+
const corners = [inversePoint(0, 0), inversePoint(width, 0), inversePoint(width, height), inversePoint(0, height)];
|
|
20
|
+
if (!corners.every((point) => Number.isFinite(point.x) && Number.isFinite(point.y)))
|
|
21
|
+
return null;
|
|
22
|
+
const left = Math.min(...corners.map((point) => point.x));
|
|
23
|
+
const top = Math.min(...corners.map((point) => point.y));
|
|
24
|
+
const right = Math.max(...corners.map((point) => point.x));
|
|
25
|
+
const bottom = Math.max(...corners.map((point) => point.y));
|
|
26
|
+
// Largest singular value: enough local raster resolution for either output
|
|
27
|
+
// axis. A scalar Canvas shadow blur is exact only for similarity transforms.
|
|
28
|
+
const xx = a * a + b * b;
|
|
29
|
+
const yy = c * c + d * d;
|
|
30
|
+
const xy = a * c + b * d;
|
|
31
|
+
const scale = Math.sqrt((xx + yy + Math.hypot(xx - yy, 2 * xy)) / 2);
|
|
32
|
+
if (!Number.isFinite(scale) || scale <= 0)
|
|
33
|
+
return null;
|
|
34
|
+
const tolerance = 1e-10 * Math.max(xx, yy);
|
|
35
|
+
return {
|
|
36
|
+
matrix,
|
|
37
|
+
viewport: new bounds_1.Bounds(left, top, right - left, bottom - top),
|
|
38
|
+
scale,
|
|
39
|
+
uniform: Math.abs(xx - yy) <= tolerance && Math.abs(xy) <= tolerance
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
exports.shadowSpace = shadowSpace;
|
|
@@ -7,6 +7,12 @@ const path_1 = require("../path");
|
|
|
7
7
|
const bound_curves_1 = require("../bound-curves");
|
|
8
8
|
const background_1 = require("../background");
|
|
9
9
|
const box_sizing_1 = require("../box-sizing");
|
|
10
|
+
const effects_1 = require("../effects");
|
|
11
|
+
const mix_blend_mode_1 = require("../../css/property-descriptors/mix-blend-mode");
|
|
12
|
+
const filter_surface_1 = require("./filter-surface");
|
|
13
|
+
const surface_bounds_1 = require("./surface-bounds");
|
|
14
|
+
const bounds_1 = require("../../css/layout/bounds");
|
|
15
|
+
const bitwise_1 = require("../../core/bitwise");
|
|
10
16
|
const text_renderer_1 = require("./text-renderer");
|
|
11
17
|
const background_renderer_1 = require("./background-renderer");
|
|
12
18
|
const border_renderer_1 = require("./border-renderer");
|
|
@@ -15,17 +21,24 @@ const effects_renderer_1 = require("./effects-renderer");
|
|
|
15
21
|
const canvas_path_1 = require("./canvas-path");
|
|
16
22
|
const object_fit_1 = require("../object-fit");
|
|
17
23
|
const content_renderer_1 = require("./content-renderer");
|
|
18
|
-
const
|
|
24
|
+
const box_shadow_painter_1 = require("./box-shadow-painter");
|
|
19
25
|
const writing_mode_1 = require("../../css/property-descriptors/writing-mode");
|
|
20
26
|
const font_utils_1 = require("./font-utils");
|
|
21
27
|
const text_1 = require("../../css/layout/text");
|
|
22
28
|
class CanvasRenderer {
|
|
23
|
-
constructor(context, options) {
|
|
29
|
+
constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
|
|
30
|
+
this.surfaceRoot = surfaceRoot;
|
|
31
|
+
this.surfaceBudget = surfaceBudget;
|
|
32
|
+
this.legacySubtree = false;
|
|
24
33
|
this.context = context;
|
|
25
34
|
this.options = options;
|
|
26
35
|
this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
|
|
27
36
|
const ctx = this.canvas.getContext('2d');
|
|
28
37
|
if (!ctx) {
|
|
38
|
+
if (!options.canvas)
|
|
39
|
+
(0, filter_surface_1.releaseSurface)(this.canvas);
|
|
40
|
+
if (surfaceRoot)
|
|
41
|
+
throw new filter_surface_1.FilterSurfaceError('Filter surface canvas is unavailable');
|
|
29
42
|
throw new Error('Failed to get 2D rendering context from canvas');
|
|
30
43
|
}
|
|
31
44
|
this.ctx = ctx;
|
|
@@ -71,9 +84,101 @@ class CanvasRenderer {
|
|
|
71
84
|
async renderStack(stack) {
|
|
72
85
|
const styles = stack.element.container.styles;
|
|
73
86
|
if (styles.isVisible()) {
|
|
87
|
+
const filter = (0, filter_surface_1.parseSimpleFilter)(styles.filter);
|
|
88
|
+
if (stack.element !== this.surfaceRoot &&
|
|
89
|
+
!this.legacySubtree &&
|
|
90
|
+
filter &&
|
|
91
|
+
(styles.filter || styles.opacity < 1) &&
|
|
92
|
+
this.canComposite(stack.element)) {
|
|
93
|
+
if (!(await this.renderCompositedStack(stack, filter))) {
|
|
94
|
+
// A rejected optional surface must preserve the previous subtree path.
|
|
95
|
+
this.legacySubtree = true;
|
|
96
|
+
try {
|
|
97
|
+
await this.renderStackContent(stack);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
this.legacySubtree = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
74
105
|
await this.renderStackContent(stack);
|
|
75
106
|
}
|
|
76
107
|
}
|
|
108
|
+
// Draft scope: transformed, blended and clip-path subtrees keep the old path.
|
|
109
|
+
// The surface root owns its filter/opacity; ancestors are applied at composition.
|
|
110
|
+
canComposite(paint) {
|
|
111
|
+
const supported = (container) => {
|
|
112
|
+
const styles = container.styles;
|
|
113
|
+
return (!styles.isTransformed() &&
|
|
114
|
+
styles.zoom === 1 &&
|
|
115
|
+
styles.mixBlendMode === mix_blend_mode_1.MIX_BLEND_MODE.NORMAL &&
|
|
116
|
+
styles.clipPath.type === 0 /* CLIP_PATH_TYPE.NONE */ &&
|
|
117
|
+
!(0, bitwise_1.contains)(styles.display, 2048 /* DISPLAY.LIST_ITEM */) &&
|
|
118
|
+
(0, filter_surface_1.parseSimpleFilter)(styles.filter) !== null);
|
|
119
|
+
};
|
|
120
|
+
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
121
|
+
let ancestor = paint.parent;
|
|
122
|
+
while (ancestor && ancestor !== this.surfaceRoot) {
|
|
123
|
+
if (!supported(ancestor.container))
|
|
124
|
+
return false;
|
|
125
|
+
ancestor = ancestor.parent;
|
|
126
|
+
}
|
|
127
|
+
return subtree(paint.container);
|
|
128
|
+
}
|
|
129
|
+
async renderCompositedStack(stack, filter) {
|
|
130
|
+
const signal = this.options.signal;
|
|
131
|
+
if (signal?.aborted)
|
|
132
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
133
|
+
const bounds = (0, surface_bounds_1.cropSurface)((0, surface_bounds_1.surfaceBounds)(stack.element.container), new bounds_1.Bounds(this.options.x, this.options.y, this.options.width, this.options.height), (0, filter_surface_1.filterOutset)(filter), this.options.scale);
|
|
134
|
+
if (!bounds.width || !bounds.height)
|
|
135
|
+
return true;
|
|
136
|
+
const reserved = (0, surface_bounds_1.reserveSurface)(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
137
|
+
if (!reserved) {
|
|
138
|
+
this.context.logger.info('Filter surface budget exceeded; using the existing renderer');
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const options = {
|
|
142
|
+
...this.options,
|
|
143
|
+
canvas: undefined,
|
|
144
|
+
backgroundColor: null,
|
|
145
|
+
x: bounds.left,
|
|
146
|
+
y: bounds.top,
|
|
147
|
+
width: bounds.width,
|
|
148
|
+
height: bounds.height
|
|
149
|
+
};
|
|
150
|
+
let source;
|
|
151
|
+
let filtered;
|
|
152
|
+
try {
|
|
153
|
+
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
154
|
+
await source.renderStackContent(stack);
|
|
155
|
+
source.effectsRenderer.applyEffects([]);
|
|
156
|
+
filtered = await (0, filter_surface_1.renderFilterSurface)(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
|
|
157
|
+
if (signal?.aborted)
|
|
158
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
159
|
+
const own = stack.element.effects;
|
|
160
|
+
const effects = stack.element
|
|
161
|
+
.getEffects(4 /* EffectTarget.CONTENT */, this.surfaceRoot)
|
|
162
|
+
.filter((effect) => !own.includes(effect) ||
|
|
163
|
+
(!(0, effects_1.isFilterEffect)(effect) && !(0, effects_1.isOpacityEffect)(effect) && !(0, effects_1.isClipEffect)(effect)));
|
|
164
|
+
this.effectsRenderer.applyEffects(effects);
|
|
165
|
+
this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (!(error instanceof filter_surface_1.FilterSurfaceError))
|
|
170
|
+
throw error;
|
|
171
|
+
this.context.logger.info(`${error.message}; using the existing renderer`);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (source)
|
|
176
|
+
(0, filter_surface_1.releaseSurface)(source.canvas);
|
|
177
|
+
if (filtered)
|
|
178
|
+
(0, filter_surface_1.releaseSurface)(filtered);
|
|
179
|
+
this.surfaceBudget.pixels -= reserved;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
77
182
|
async renderNode(paint) {
|
|
78
183
|
if (paint.container.debugRender) {
|
|
79
184
|
debugger;
|
|
@@ -103,7 +208,7 @@ class CanvasRenderer {
|
|
|
103
208
|
}
|
|
104
209
|
}
|
|
105
210
|
async renderNodeContent(paint) {
|
|
106
|
-
this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT
|
|
211
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */, this.surfaceRoot));
|
|
107
212
|
const container = paint.container;
|
|
108
213
|
const curves = paint.curves;
|
|
109
214
|
const styles = container.styles;
|
|
@@ -207,8 +312,61 @@ class CanvasRenderer {
|
|
|
207
312
|
formatPath(paths) {
|
|
208
313
|
(0, canvas_path_1.formatCanvasPath)(this.ctx, paths);
|
|
209
314
|
}
|
|
315
|
+
async renderSurfaceBoxShadow(paint, shadow) {
|
|
316
|
+
// Rasterize a visible silhouette, then blur it. Linux WebKit can drop the
|
|
317
|
+
// interior of Canvas shadows whose solid mask is outside a cropped canvas.
|
|
318
|
+
const spread = shadow.spread.number;
|
|
319
|
+
const blur = shadow.blur.number / 2;
|
|
320
|
+
const margin = Math.ceil(3 * blur) + 1;
|
|
321
|
+
const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
322
|
+
if (box.width <= 0 || box.height <= 0)
|
|
323
|
+
return;
|
|
324
|
+
const scale = this.options.scale;
|
|
325
|
+
const bounds = (0, surface_bounds_1.cropSurface)(box.add(-margin, -margin, 2 * margin, 2 * margin), new bounds_1.Bounds(this.options.x - shadow.offsetX.number, this.options.y - shadow.offsetY.number, this.options.width, this.options.height), margin, scale);
|
|
326
|
+
if (!bounds.width || !bounds.height)
|
|
327
|
+
return;
|
|
328
|
+
const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
|
|
329
|
+
const reserved = (0, surface_bounds_1.reserveSurface)(this.surfaceBudget, width, height);
|
|
330
|
+
if (!reserved)
|
|
331
|
+
throw new filter_surface_1.FilterSurfaceError('Box shadow surface budget exceeded');
|
|
332
|
+
let source;
|
|
333
|
+
let filtered;
|
|
334
|
+
try {
|
|
335
|
+
source = document.createElement('canvas');
|
|
336
|
+
source.width = width;
|
|
337
|
+
source.height = height;
|
|
338
|
+
const ctx = source.getContext('2d');
|
|
339
|
+
if (!ctx)
|
|
340
|
+
throw new filter_surface_1.FilterSurfaceError('Box shadow surface canvas is unavailable');
|
|
341
|
+
ctx.scale(scale, scale);
|
|
342
|
+
ctx.translate(-bounds.left, -bounds.top);
|
|
343
|
+
(0, canvas_path_1.createCanvasPath)(ctx, (0, path_1.transformPath)((0, bound_curves_1.calculateBorderBoxPath)(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
|
|
344
|
+
ctx.fillStyle = (0, color_utilities_1.asString)(shadow.color);
|
|
345
|
+
ctx.fill();
|
|
346
|
+
filtered = await (0, filter_surface_1.renderFilterSurface)(source, { blur }, 1, scale, this.options.signal);
|
|
347
|
+
this.ctx.save();
|
|
348
|
+
try {
|
|
349
|
+
this.ctx.beginPath();
|
|
350
|
+
this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
351
|
+
(0, canvas_path_1.formatCanvasPath)(this.ctx, (0, bound_curves_1.calculateBorderBoxPath)(paint.curves));
|
|
352
|
+
this.ctx.closePath();
|
|
353
|
+
this.ctx.clip('evenodd');
|
|
354
|
+
this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
this.ctx.restore();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
if (source)
|
|
362
|
+
(0, filter_surface_1.releaseSurface)(source);
|
|
363
|
+
if (filtered)
|
|
364
|
+
(0, filter_surface_1.releaseSurface)(filtered);
|
|
365
|
+
this.surfaceBudget.pixels -= reserved;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
210
368
|
async renderNodeBackgroundAndBorders(paint) {
|
|
211
|
-
this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS
|
|
369
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */, this.surfaceRoot));
|
|
212
370
|
const styles = paint.container.styles;
|
|
213
371
|
const hasBackground = !(0, color_utilities_1.isTransparent)(styles.backgroundColor) || styles.backgroundImage.length;
|
|
214
372
|
const hasTextClip = hasTextBackgroundClip(styles);
|
|
@@ -236,32 +394,13 @@ class CanvasRenderer {
|
|
|
236
394
|
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
237
395
|
this.ctx.restore();
|
|
238
396
|
}
|
|
239
|
-
styles.boxShadow
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
this.ctx.save();
|
|
244
|
-
const borderBoxArea = (0, bound_curves_1.calculateBorderBoxPath)(paint.curves);
|
|
245
|
-
const maskOffset = shadow.inset ? 0 : constants_1.SHADOW_MASK_OFFSET;
|
|
246
|
-
const shadowPaintingArea = (0, path_1.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));
|
|
247
|
-
if (shadow.inset) {
|
|
248
|
-
this.path(borderBoxArea);
|
|
249
|
-
this.ctx.clip();
|
|
250
|
-
this.mask(shadowPaintingArea);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
this.mask(borderBoxArea);
|
|
254
|
-
this.ctx.clip();
|
|
255
|
-
this.path(shadowPaintingArea);
|
|
397
|
+
for (const shadow of styles.boxShadow.slice(0).reverse()) {
|
|
398
|
+
if (this.surfaceRoot && !shadow.inset) {
|
|
399
|
+
await this.renderSurfaceBoxShadow(paint, shadow);
|
|
400
|
+
continue;
|
|
256
401
|
}
|
|
257
|
-
this.ctx
|
|
258
|
-
|
|
259
|
-
this.ctx.shadowColor = (0, color_utilities_1.asString)(shadow.color);
|
|
260
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
261
|
-
this.ctx.fillStyle = shadow.inset ? (0, color_utilities_1.asString)(shadow.color) : 'rgba(0,0,0,1)';
|
|
262
|
-
this.ctx.fill();
|
|
263
|
-
this.ctx.restore();
|
|
264
|
-
});
|
|
402
|
+
await (0, box_shadow_painter_1.paintBoxShadow)(this.ctx, paint, shadow, this.options, this.surfaceBudget);
|
|
403
|
+
}
|
|
265
404
|
}
|
|
266
405
|
// Render border-image if present (replaces traditional borders per CSS spec)
|
|
267
406
|
if (styles.borderImageSource) {
|
|
@@ -335,14 +474,23 @@ class CanvasRenderer {
|
|
|
335
474
|
}
|
|
336
475
|
}
|
|
337
476
|
async render(element) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
477
|
+
try {
|
|
478
|
+
if (this.options.backgroundColor) {
|
|
479
|
+
this.ctx.fillStyle = (0, color_utilities_1.asString)(this.options.backgroundColor);
|
|
480
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
481
|
+
}
|
|
482
|
+
const stack = (0, stacking_context_1.parseStackingContexts)(element);
|
|
483
|
+
await this.renderStack(stack);
|
|
484
|
+
this.effectsRenderer.applyEffects([]);
|
|
485
|
+
return this.canvas;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
// Failed captures have no returned canvas. Release only our backing
|
|
489
|
+
// store; a canvas supplied by the caller remains caller-owned.
|
|
490
|
+
if (!this.options.canvas)
|
|
491
|
+
(0, filter_surface_1.releaseSurface)(this.canvas);
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
346
494
|
}
|
|
347
495
|
/**
|
|
348
496
|
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|