mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Extract render target logic out of renderer to be target agnostic
This commit is contained in:
266
src/renderer/CanvasRenderer.js
Normal file
266
src/renderer/CanvasRenderer.js
Normal file
@ -0,0 +1,266 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {RenderTarget, RenderOptions} from '../Renderer';
|
||||
|
||||
import type Color from '../Color';
|
||||
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 {Path, Bounds} from '../Bounds';
|
||||
import type {ImageElement} from '../ImageLoader';
|
||||
import type {Gradient} from '../Gradient';
|
||||
import type {TextBounds} from '../TextBounds';
|
||||
|
||||
import BezierCurve from '../drawing/BezierCurve';
|
||||
import Circle from '../drawing/Circle';
|
||||
import Vector from '../drawing/Vector';
|
||||
|
||||
import {TEXT_DECORATION_LINE} from '../parsing/textDecoration';
|
||||
|
||||
export default class CanvasRenderer implements RenderTarget {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
options: RenderOptions;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, options: RenderOptions) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d');
|
||||
this.options = options;
|
||||
|
||||
this.ctx.scale(this.options.scale, this.options.scale);
|
||||
this.ctx.textBaseline = 'bottom';
|
||||
options.logger.log(`Canvas renderer initialized 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 (path instanceof Circle) {
|
||||
this.ctx.arc(
|
||||
path.x + path.radius,
|
||||
path.y + path.radius,
|
||||
path.radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
path.forEach((point, index) => {
|
||||
const start = point instanceof Vector ? point : point.start;
|
||||
if (index === 0) {
|
||||
this.ctx.moveTo(start.x, start.y);
|
||||
} else {
|
||||
this.ctx.lineTo(start.x, start.y);
|
||||
}
|
||||
|
||||
if (point instanceof BezierCurve) {
|
||||
this.ctx.bezierCurveTo(
|
||||
point.startControl.x,
|
||||
point.startControl.y,
|
||||
point.endControl.x,
|
||||
point.endControl.y,
|
||||
point.end.x,
|
||||
point.end.y
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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: Gradient) {
|
||||
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
|
||||
);
|
||||
|
||||
gradient.colorStops.forEach(colorStop => {
|
||||
linearGradient.addColorStop(colorStop.stop, colorStop.color.toString());
|
||||
});
|
||||
|
||||
this.ctx.fillStyle = linearGradient;
|
||||
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,
|
||||
textShadows: Array<TextShadow> | null
|
||||
) {
|
||||
this.ctx.font = [
|
||||
font.fontStyle,
|
||||
font.fontVariant,
|
||||
font.fontWeight,
|
||||
font.fontSize,
|
||||
font.fontFamily
|
||||
]
|
||||
.join(' ')
|
||||
.split(',')[0];
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user