html2canvas-pro
Version:
Screenshots with JavaScript. Next generation!
222 lines • 9.69 kB
JavaScript
"use strict";
/**
* Background Renderer
*
* Handles rendering of element backgrounds including:
* - Background colors
* - Background images (URL)
* - Linear gradients
* - Radial gradients
* - Background patterns and repeats
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BackgroundRenderer = void 0;
const image_1 = require("../../css/types/image");
const background_1 = require("../background");
const gradient_1 = require("../../css/types/functions/gradient");
const length_percentage_1 = require("../../css/types/length-percentage");
const color_utilities_1 = require("../../css/types/color-utilities");
const bezier_curve_1 = require("../bezier-curve");
const image_rendering_1 = require("../../css/property-descriptors/image-rendering");
/**
* Background Renderer
*
* Specialized renderer for element backgrounds.
* Extracted from CanvasRenderer to improve code organization and maintainability.
*/
class BackgroundRenderer {
constructor(deps) {
this.ctx = deps.ctx;
this.context = deps.context;
this.canvas = deps.canvas;
// Options stored in deps but not needed as instance property
}
/**
* Render background images for a container
* Supports URL images, linear gradients, and radial gradients
*
* @param container - Element container with background styles
*/
async renderBackgroundImage(container) {
let index = container.styles.backgroundImage.length - 1;
for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
if (backgroundImage.type === 0 /* CSSImageType.URL */) {
await this.renderBackgroundURLImage(container, backgroundImage, index);
}
else if ((0, image_1.isLinearGradient)(backgroundImage)) {
this.renderLinearGradient(container, backgroundImage, index);
}
else if ((0, image_1.isRadialGradient)(backgroundImage)) {
this.renderRadialGradient(container, backgroundImage, index);
}
index--;
}
}
/**
* Render a URL-based background image
*/
async renderBackgroundURLImage(container, backgroundImage, index) {
let image;
const url = backgroundImage.url;
try {
image = await this.context.cache.match(url);
}
catch (e) {
this.context.logger.error(`Error loading background-image ${url}`);
}
if (image) {
const imageWidth = isNaN(image.width) || image.width === 0 ? 1 : image.width;
const imageHeight = isNaN(image.height) || image.height === 0 ? 1 : image.height;
const [path, x, y, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [
imageWidth,
imageHeight,
imageWidth / imageHeight
]);
const pattern = this.ctx.createPattern(this.resizeImage(image, width, height, container.styles.imageRendering), 'repeat');
this.renderRepeat(path, pattern, x, y);
}
}
/**
* Render a linear gradient background
*/
renderLinearGradient(container, backgroundImage, index) {
const [path, x, y, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [null, null, null]);
const [lineLength, x0, x1, y0, y1] = (0, gradient_1.calculateGradientDirection)(backgroundImage.angle, width, height);
const ownerDocument = this.canvas.ownerDocument ?? document;
const canvas = ownerDocument.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
(0, gradient_1.processColorStops)(backgroundImage.stops, lineLength || 1).forEach((colorStop) => gradient.addColorStop(colorStop.stop, (0, color_utilities_1.asString)(colorStop.color)));
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
if (width > 0 && height > 0) {
const pattern = this.ctx.createPattern(canvas, 'repeat');
this.renderRepeat(path, pattern, x, y);
}
}
/**
* Render a radial gradient background
*/
renderRadialGradient(container, backgroundImage, index) {
const [path, left, top, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [null, null, null]);
const position = backgroundImage.position.length === 0 ? [length_percentage_1.FIFTY_PERCENT] : backgroundImage.position;
const x = (0, length_percentage_1.getAbsoluteValue)(position[0], width);
const y = (0, length_percentage_1.getAbsoluteValue)(position[position.length - 1], height);
let [rx, ry] = (0, gradient_1.calculateRadius)(backgroundImage, x, y, width, height);
// Handle edge case where radial gradient size is 0
// Use a minimum value of 0.01 to ensure gradient is still rendered
if (rx === 0 || ry === 0) {
rx = Math.max(rx, 0.01);
ry = Math.max(ry, 0.01);
}
if (rx > 0 && ry > 0) {
const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
(0, gradient_1.processColorStops)(backgroundImage.stops, rx * 2).forEach((colorStop) => radialGradient.addColorStop(colorStop.stop, (0, color_utilities_1.asString)(colorStop.color)));
this.path(path);
this.ctx.fillStyle = radialGradient;
if (rx !== ry) {
// transforms for elliptical radial gradient
const midX = container.bounds.left + 0.5 * container.bounds.width;
const midY = container.bounds.top + 0.5 * container.bounds.height;
const f = ry / rx;
const invF = 1 / f;
this.ctx.save();
this.ctx.translate(midX, midY);
this.ctx.transform(1, 0, 0, f, 0, 0);
this.ctx.translate(-midX, -midY);
this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
this.ctx.restore();
}
else {
this.ctx.fill();
}
}
}
/**
* Render a repeating pattern with offset
*
* @param path - Path to fill
* @param pattern - Canvas pattern or gradient
* @param offsetX - X offset for pattern
* @param offsetY - Y offset for pattern
*/
renderRepeat(path, pattern, offsetX, offsetY) {
this.path(path);
this.ctx.fillStyle = pattern;
this.ctx.translate(offsetX, offsetY);
this.ctx.fill();
this.ctx.translate(-offsetX, -offsetY);
}
/**
* Resize an image to target dimensions
*
* @param image - Source image
* @param width - Target width
* @param height - Target height
* @param imageRendering - CSS image-rendering property value
* @returns Resized canvas or original image
*/
resizeImage(image, width, height, imageRendering) {
// https://github.com/niklasvh/html2canvas/pull/2911
// if (image.width === width && image.height === height) {
// return image;
// }
const ownerDocument = this.canvas.ownerDocument ?? document;
const canvas = ownerDocument.createElement('canvas');
canvas.width = Math.max(1, width);
canvas.height = Math.max(1, height);
const ctx = canvas.getContext('2d');
// Apply image smoothing based on CSS image-rendering property
if (imageRendering === image_rendering_1.IMAGE_RENDERING.PIXELATED || imageRendering === image_rendering_1.IMAGE_RENDERING.CRISP_EDGES) {
this.context.logger.debug(`Disabling image smoothing for background image due to CSS image-rendering`);
ctx.imageSmoothingEnabled = false;
}
else if (imageRendering === image_rendering_1.IMAGE_RENDERING.SMOOTH) {
this.context.logger.debug(`Enabling image smoothing for background image due to CSS image-rendering: smooth`);
ctx.imageSmoothingEnabled = true;
}
else {
// AUTO: inherit from main renderer context
ctx.imageSmoothingEnabled = this.ctx.imageSmoothingEnabled;
}
// Inherit quality setting
if (this.ctx.imageSmoothingQuality) {
ctx.imageSmoothingQuality = this.ctx.imageSmoothingQuality;
}
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
return canvas;
}
/**
* Create a canvas path from path array
*
* @param paths - Array of path points
*/
path(paths) {
this.ctx.beginPath();
this.formatPath(paths);
this.ctx.closePath();
}
/**
* Format path points into canvas path
*
* @param paths - Array of path points
*/
formatPath(paths) {
paths.forEach((point, index) => {
const start = (0, bezier_curve_1.isBezierCurve)(point) ? point.start : point;
if (index === 0) {
this.ctx.moveTo(start.x, start.y);
}
else {
this.ctx.lineTo(start.x, start.y);
}
if ((0, bezier_curve_1.isBezierCurve)(point)) {
this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
}
});
}
}
exports.BackgroundRenderer = BackgroundRenderer;
//# sourceMappingURL=background-renderer.js.map