mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
313 lines
11 KiB
JavaScript
313 lines
11 KiB
JavaScript
/* @flow */
|
|
'use strict';
|
|
|
|
import type {RenderTarget, RenderOptions} from '../Renderer';
|
|
import type Color from '../Color';
|
|
import type {Path} from '../drawing/Path';
|
|
import type Size from '../drawing/Size';
|
|
|
|
import type {Font} from '../parsing/font';
|
|
import type {TextDecoration} from '../parsing/textDecoration';
|
|
import type {TextShadow} from '../parsing/textShadow';
|
|
import type {Matrix} from '../parsing/transform';
|
|
|
|
import type {Bounds} from '../Bounds';
|
|
import type {ImageElement} from '../ResourceLoader';
|
|
import type {LinearGradient, RadialGradient} from '../Gradient';
|
|
import type {TextBounds} from '../TextBounds';
|
|
|
|
import {PATH} from '../drawing/Path';
|
|
import {TEXT_DECORATION_LINE} from '../parsing/textDecoration';
|
|
|
|
const addColorStops = (
|
|
gradient: LinearGradient | RadialGradient,
|
|
canvasGradient: CanvasGradient
|
|
): void => {
|
|
const maxStop = Math.max.apply(null, gradient.colorStops.map(colorStop => colorStop.stop));
|
|
const f = 1 / Math.max(1, maxStop);
|
|
gradient.colorStops.forEach(colorStop => {
|
|
canvasGradient.addColorStop(f * colorStop.stop, colorStop.color.toString());
|
|
});
|
|
};
|
|
|
|
export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
|
|
canvas: HTMLCanvasElement;
|
|
ctx: CanvasRenderingContext2D;
|
|
options: RenderOptions;
|
|
|
|
constructor(canvas: ?HTMLCanvasElement) {
|
|
this.canvas = canvas ? canvas : document.createElement('canvas');
|
|
}
|
|
|
|
render(options: RenderOptions) {
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.options = options;
|
|
this.canvas.width = Math.floor(options.width * options.scale);
|
|
this.canvas.height = Math.floor(options.height * options.scale);
|
|
this.canvas.style.width = `${options.width}px`;
|
|
this.canvas.style.height = `${options.height}px`;
|
|
|
|
this.ctx.scale(this.options.scale, this.options.scale);
|
|
this.ctx.translate(-options.x, -options.y);
|
|
this.ctx.textBaseline = 'bottom';
|
|
options.logger.log(
|
|
`Canvas renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${this
|
|
.options.scale}`
|
|
);
|
|
}
|
|
|
|
clip(clipPaths: Array<Path>, callback: () => void) {
|
|
if (clipPaths.length) {
|
|
this.ctx.save();
|
|
clipPaths.forEach(path => {
|
|
this.path(path);
|
|
this.ctx.clip();
|
|
});
|
|
}
|
|
|
|
callback();
|
|
|
|
if (clipPaths.length) {
|
|
this.ctx.restore();
|
|
}
|
|
}
|
|
|
|
drawImage(image: ImageElement, source: Bounds, destination: Bounds) {
|
|
this.ctx.drawImage(
|
|
image,
|
|
source.left,
|
|
source.top,
|
|
source.width,
|
|
source.height,
|
|
destination.left,
|
|
destination.top,
|
|
destination.width,
|
|
destination.height
|
|
);
|
|
}
|
|
|
|
drawShape(path: Path, color: Color) {
|
|
this.path(path);
|
|
this.ctx.fillStyle = color.toString();
|
|
this.ctx.fill();
|
|
}
|
|
|
|
fill(color: Color) {
|
|
this.ctx.fillStyle = color.toString();
|
|
this.ctx.fill();
|
|
}
|
|
|
|
getTarget(): Promise<HTMLCanvasElement> {
|
|
return Promise.resolve(this.canvas);
|
|
}
|
|
|
|
path(path: Path) {
|
|
this.ctx.beginPath();
|
|
if (Array.isArray(path)) {
|
|
path.forEach((point, index) => {
|
|
const start = point.type === PATH.VECTOR ? point : point.start;
|
|
if (index === 0) {
|
|
this.ctx.moveTo(start.x, start.y);
|
|
} else {
|
|
this.ctx.lineTo(start.x, start.y);
|
|
}
|
|
|
|
if (point.type === PATH.BEZIER_CURVE) {
|
|
this.ctx.bezierCurveTo(
|
|
point.startControl.x,
|
|
point.startControl.y,
|
|
point.endControl.x,
|
|
point.endControl.y,
|
|
point.end.x,
|
|
point.end.y
|
|
);
|
|
}
|
|
});
|
|
} else {
|
|
this.ctx.arc(
|
|
path.x + path.radius,
|
|
path.y + path.radius,
|
|
path.radius,
|
|
0,
|
|
Math.PI * 2,
|
|
true
|
|
);
|
|
}
|
|
|
|
this.ctx.closePath();
|
|
}
|
|
|
|
rectangle(x: number, y: number, width: number, height: number, color: Color) {
|
|
this.ctx.fillStyle = color.toString();
|
|
this.ctx.fillRect(x, y, width, height);
|
|
}
|
|
|
|
renderLinearGradient(bounds: Bounds, gradient: LinearGradient) {
|
|
const linearGradient = this.ctx.createLinearGradient(
|
|
bounds.left + gradient.direction.x1,
|
|
bounds.top + gradient.direction.y1,
|
|
bounds.left + gradient.direction.x0,
|
|
bounds.top + gradient.direction.y0
|
|
);
|
|
|
|
addColorStops(gradient, linearGradient);
|
|
this.ctx.fillStyle = linearGradient;
|
|
this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
}
|
|
|
|
renderRadialGradient(bounds: Bounds, gradient: RadialGradient) {
|
|
const x = bounds.left + gradient.center.x;
|
|
const y = bounds.top + gradient.center.y;
|
|
|
|
const radialGradient = this.ctx.createRadialGradient(x, y, 0, x, y, gradient.radius.x);
|
|
if (!radialGradient) {
|
|
return;
|
|
}
|
|
|
|
addColorStops(gradient, radialGradient);
|
|
this.ctx.fillStyle = radialGradient;
|
|
|
|
if (gradient.radius.x !== gradient.radius.y) {
|
|
// transforms for elliptical radial gradient
|
|
const midX = bounds.left + 0.5 * bounds.width;
|
|
const midY = bounds.top + 0.5 * bounds.height;
|
|
const f = gradient.radius.y / gradient.radius.x;
|
|
const invF = 1 / f;
|
|
|
|
this.transform(midX, midY, [1, 0, 0, f, 0, 0], () =>
|
|
this.ctx.fillRect(
|
|
bounds.left,
|
|
invF * (bounds.top - midY) + midY,
|
|
bounds.width,
|
|
bounds.height * invF
|
|
)
|
|
);
|
|
} else {
|
|
this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
}
|
|
}
|
|
|
|
renderRepeat(
|
|
path: Path,
|
|
image: ImageElement,
|
|
imageSize: Size,
|
|
offsetX: number,
|
|
offsetY: number
|
|
) {
|
|
this.path(path);
|
|
this.ctx.fillStyle = this.ctx.createPattern(this.resizeImage(image, imageSize), 'repeat');
|
|
this.ctx.translate(offsetX, offsetY);
|
|
this.ctx.fill();
|
|
this.ctx.translate(-offsetX, -offsetY);
|
|
}
|
|
|
|
renderTextNode(
|
|
textBounds: Array<TextBounds>,
|
|
color: Color,
|
|
font: Font,
|
|
textDecoration: TextDecoration | null,
|
|
textShadows: Array<TextShadow> | null
|
|
) {
|
|
this.ctx.font = [
|
|
font.fontStyle,
|
|
font.fontVariant,
|
|
font.fontWeight,
|
|
font.fontSize,
|
|
font.fontFamily
|
|
].join(' ');
|
|
|
|
textBounds.forEach(text => {
|
|
this.ctx.fillStyle = color.toString();
|
|
if (textShadows && text.text.trim().length) {
|
|
textShadows.slice(0).reverse().forEach(textShadow => {
|
|
this.ctx.shadowColor = textShadow.color.toString();
|
|
this.ctx.shadowOffsetX = textShadow.offsetX * this.options.scale;
|
|
this.ctx.shadowOffsetY = textShadow.offsetY * this.options.scale;
|
|
this.ctx.shadowBlur = textShadow.blur;
|
|
|
|
this.ctx.fillText(
|
|
text.text,
|
|
text.bounds.left,
|
|
text.bounds.top + text.bounds.height
|
|
);
|
|
});
|
|
} else {
|
|
this.ctx.fillText(
|
|
text.text,
|
|
text.bounds.left,
|
|
text.bounds.top + text.bounds.height
|
|
);
|
|
}
|
|
|
|
if (textDecoration !== null) {
|
|
const textDecorationColor = textDecoration.textDecorationColor || color;
|
|
textDecoration.textDecorationLine.forEach(textDecorationLine => {
|
|
switch (textDecorationLine) {
|
|
case TEXT_DECORATION_LINE.UNDERLINE:
|
|
// Draws a line at the baseline of the font
|
|
// TODO As some browsers display the line as more than 1px if the font-size is big,
|
|
// need to take that into account both in position and size
|
|
const {baseline} = this.options.fontMetrics.getMetrics(font);
|
|
this.rectangle(
|
|
text.bounds.left,
|
|
Math.round(text.bounds.top + baseline),
|
|
text.bounds.width,
|
|
1,
|
|
textDecorationColor
|
|
);
|
|
break;
|
|
case TEXT_DECORATION_LINE.OVERLINE:
|
|
this.rectangle(
|
|
text.bounds.left,
|
|
Math.round(text.bounds.top),
|
|
text.bounds.width,
|
|
1,
|
|
textDecorationColor
|
|
);
|
|
break;
|
|
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
|
// TODO try and find exact position for line-through
|
|
const {middle} = this.options.fontMetrics.getMetrics(font);
|
|
this.rectangle(
|
|
text.bounds.left,
|
|
Math.ceil(text.bounds.top + middle),
|
|
text.bounds.width,
|
|
1,
|
|
textDecorationColor
|
|
);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
resizeImage(image: ImageElement, size: Size): ImageElement {
|
|
if (image.width === size.width && image.height === size.height) {
|
|
return image;
|
|
}
|
|
|
|
const canvas = this.canvas.ownerDocument.createElement('canvas');
|
|
canvas.width = size.width;
|
|
canvas.height = size.height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, size.width, size.height);
|
|
return canvas;
|
|
}
|
|
|
|
setOpacity(opacity: number) {
|
|
this.ctx.globalAlpha = opacity;
|
|
}
|
|
|
|
transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void) {
|
|
this.ctx.save();
|
|
this.ctx.translate(offsetX, offsetY);
|
|
this.ctx.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
|
|
this.ctx.translate(-offsetX, -offsetY);
|
|
|
|
callback();
|
|
|
|
this.ctx.restore();
|
|
}
|
|
}
|