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:
parent
f7f445c71e
commit
a2895691ba
@ -1,510 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type Color from './Color';
|
||||
import type Size from './drawing/Size';
|
||||
|
||||
import type {BackgroundImage} from './parsing/background';
|
||||
import type {Border, BorderSide} from './parsing/border';
|
||||
import type {TextShadow} from './parsing/textShadow';
|
||||
|
||||
import type {Path, BoundCurves} from './Bounds';
|
||||
import type {ImageStore, ImageElement} from './ImageLoader';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type StackingContext from './StackingContext';
|
||||
import type {TextBounds} from './TextBounds';
|
||||
|
||||
import BezierCurve from './drawing/BezierCurve';
|
||||
import Circle from './drawing/Circle';
|
||||
import Vector from './drawing/Vector';
|
||||
|
||||
import {
|
||||
parsePathForBorder,
|
||||
calculateContentBox,
|
||||
calculatePaddingBox,
|
||||
calculatePaddingBoxPath
|
||||
} from './Bounds';
|
||||
import {FontMetrics} from './Font';
|
||||
import {parseGradient} from './Gradient';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {
|
||||
BACKGROUND_ORIGIN,
|
||||
calculateBackgroungPaintingArea,
|
||||
calculateBackgroundPosition,
|
||||
calculateBackgroundRepeatPath,
|
||||
calculateBackgroundSize
|
||||
} from './parsing/background';
|
||||
import {BORDER_STYLE} from './parsing/border';
|
||||
import {TEXT_DECORATION_LINE} from './parsing/textDecoration';
|
||||
|
||||
export type RenderOptions = {
|
||||
scale: number,
|
||||
backgroundColor: ?Color,
|
||||
imageStore: ImageStore,
|
||||
fontMetrics: FontMetrics
|
||||
};
|
||||
|
||||
export default class CanvasRenderer {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
options: RenderOptions;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, options: RenderOptions) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d');
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
renderNode(container: NodeContainer) {
|
||||
if (container.isVisible()) {
|
||||
this.renderNodeBackgroundAndBorders(container);
|
||||
this.renderNodeContent(container);
|
||||
}
|
||||
}
|
||||
|
||||
renderNodeContent(container: NodeContainer) {
|
||||
this.ctx.save();
|
||||
const clipPaths = container.getClipPaths();
|
||||
if (clipPaths.length) {
|
||||
clipPaths.forEach(path => {
|
||||
this.path(path);
|
||||
this.ctx.clip();
|
||||
});
|
||||
}
|
||||
|
||||
if (container.childNodes.length) {
|
||||
this.ctx.fillStyle = container.style.color.toString();
|
||||
this.ctx.font = [
|
||||
container.style.font.fontStyle,
|
||||
container.style.font.fontVariant,
|
||||
container.style.font.fontWeight,
|
||||
container.style.font.fontSize,
|
||||
container.style.font.fontFamily
|
||||
]
|
||||
.join(' ')
|
||||
.split(',')[0];
|
||||
container.childNodes.forEach(child => {
|
||||
if (child instanceof TextContainer) {
|
||||
this.renderTextNode(child);
|
||||
} else {
|
||||
this.path(child);
|
||||
this.ctx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (container.image) {
|
||||
const image = this.options.imageStore.get(container.image);
|
||||
if (image) {
|
||||
const contentBox = calculateContentBox(
|
||||
container.bounds,
|
||||
container.style.padding,
|
||||
container.style.border
|
||||
);
|
||||
const width = typeof image.width === 'number' ? image.width : contentBox.width;
|
||||
const height = typeof image.height === 'number' ? image.height : contentBox.height;
|
||||
this.ctx.save();
|
||||
this.path(calculatePaddingBoxPath(container.curvedBounds));
|
||||
this.ctx.clip();
|
||||
this.ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
contentBox.left,
|
||||
contentBox.top,
|
||||
contentBox.width,
|
||||
contentBox.height
|
||||
);
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
renderNodeBackgroundAndBorders(container: NodeContainer) {
|
||||
this.ctx.save();
|
||||
if (container.parent) {
|
||||
const clipPaths = container.parent.getClipPaths();
|
||||
if (clipPaths.length) {
|
||||
clipPaths.forEach(path => {
|
||||
this.path(path);
|
||||
this.ctx.clip();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const backgroungPaintingArea = calculateBackgroungPaintingArea(
|
||||
container.curvedBounds,
|
||||
container.style.background.backgroundClip
|
||||
);
|
||||
this.path(backgroungPaintingArea);
|
||||
if (!container.style.background.backgroundColor.isTransparent()) {
|
||||
this.ctx.fillStyle = container.style.background.backgroundColor.toString();
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
this.ctx.save();
|
||||
this.ctx.clip();
|
||||
this.renderBackgroundImage(container);
|
||||
this.ctx.restore();
|
||||
container.style.border.forEach((border, side) => {
|
||||
this.renderBorder(border, side, container.curvedBounds);
|
||||
});
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
renderTextNode(textContainer: TextContainer) {
|
||||
textContainer.bounds.forEach(text => this.renderText(text, textContainer));
|
||||
}
|
||||
|
||||
renderText(text: TextBounds, textContainer: TextContainer) {
|
||||
const container = textContainer.parent;
|
||||
this.ctx.fillStyle = container.style.color.toString();
|
||||
if (container.style.textShadow && text.text.trim().length) {
|
||||
container.style.textShadow.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);
|
||||
}
|
||||
|
||||
const textDecoration = container.style.textDecoration;
|
||||
if (textDecoration) {
|
||||
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(
|
||||
container.style.font
|
||||
);
|
||||
this.rectangle(
|
||||
text.bounds.left,
|
||||
Math.round(text.bounds.top + baseline),
|
||||
text.bounds.width,
|
||||
1,
|
||||
textDecoration.textDecorationColor || container.style.color
|
||||
);
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.OVERLINE:
|
||||
this.rectangle(
|
||||
text.bounds.left,
|
||||
Math.round(text.bounds.top),
|
||||
text.bounds.width,
|
||||
1,
|
||||
textDecoration.textDecorationColor || container.style.color
|
||||
);
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
||||
// TODO try and find exact position for line-through
|
||||
const {middle} = this.options.fontMetrics.getMetrics(container.style.font);
|
||||
this.rectangle(
|
||||
text.bounds.left,
|
||||
Math.ceil(text.bounds.top + middle),
|
||||
text.bounds.width,
|
||||
1,
|
||||
textDecoration.textDecorationColor || container.style.color
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderBackgroundImage(container: NodeContainer) {
|
||||
container.style.background.backgroundImage.reverse().forEach(backgroundImage => {
|
||||
if (backgroundImage.source.method === 'url' && backgroundImage.source.args.length) {
|
||||
this.renderBackgroundRepeat(container, backgroundImage);
|
||||
} else {
|
||||
const gradient = parseGradient(backgroundImage.source, container.bounds);
|
||||
if (gradient) {
|
||||
const bounds = container.bounds;
|
||||
const grad = 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 => {
|
||||
grad.addColorStop(colorStop.stop, colorStop.color.toString());
|
||||
});
|
||||
|
||||
this.ctx.fillStyle = grad;
|
||||
this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderBackgroundRepeat(container: NodeContainer, background: BackgroundImage) {
|
||||
const image = this.options.imageStore.get(background.source.args[0]);
|
||||
if (image) {
|
||||
const bounds = container.bounds;
|
||||
const paddingBox = calculatePaddingBox(bounds, container.style.border);
|
||||
const backgroundImageSize = calculateBackgroundSize(background, image, bounds);
|
||||
|
||||
// TODO support CONTENT_BOX
|
||||
const backgroundPositioningArea =
|
||||
container.style.background.backgroundOrigin === BACKGROUND_ORIGIN.BORDER_BOX
|
||||
? bounds
|
||||
: paddingBox;
|
||||
|
||||
const position = calculateBackgroundPosition(
|
||||
background.position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const path = calculateBackgroundRepeatPath(
|
||||
background,
|
||||
position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea,
|
||||
bounds
|
||||
);
|
||||
this.path(path);
|
||||
const offsetX = Math.round(paddingBox.left + position.x);
|
||||
const offsetY = Math.round(paddingBox.top + position.y);
|
||||
this.ctx.fillStyle = this.ctx.createPattern(
|
||||
this.resizeImage(image, backgroundImageSize),
|
||||
'repeat'
|
||||
);
|
||||
this.ctx.translate(offsetX, offsetY);
|
||||
this.ctx.fill();
|
||||
this.ctx.translate(-offsetX, -offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
resizeImage(image: ImageElement, size: Size) {
|
||||
if (image.width === size.width && image.height === size.height) {
|
||||
return image;
|
||||
}
|
||||
|
||||
const canvas = document.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;
|
||||
}
|
||||
|
||||
renderBorder(border: Border, side: BorderSide, curvePoints: BoundCurves) {
|
||||
if (border.borderStyle !== BORDER_STYLE.NONE && !border.borderColor.isTransparent()) {
|
||||
const path = parsePathForBorder(curvePoints, side);
|
||||
this.path(path);
|
||||
this.ctx.fillStyle = border.borderColor.toString();
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
renderStack(stack: StackingContext) {
|
||||
if (stack.container.isVisible()) {
|
||||
this.ctx.globalAlpha = stack.getOpacity();
|
||||
const transform = stack.container.style.transform;
|
||||
if (transform !== null) {
|
||||
this.ctx.save();
|
||||
this.ctx.translate(
|
||||
stack.container.bounds.left + transform.transformOrigin[0].value,
|
||||
stack.container.bounds.top + transform.transformOrigin[1].value
|
||||
);
|
||||
this.ctx.transform(
|
||||
transform.transform[0],
|
||||
transform.transform[1],
|
||||
transform.transform[2],
|
||||
transform.transform[3],
|
||||
transform.transform[4],
|
||||
transform.transform[5]
|
||||
);
|
||||
this.ctx.translate(
|
||||
-(stack.container.bounds.left + transform.transformOrigin[0].value),
|
||||
-(stack.container.bounds.top + transform.transformOrigin[1].value)
|
||||
);
|
||||
}
|
||||
const [
|
||||
negativeZIndex,
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity,
|
||||
positiveZIndex,
|
||||
nonPositionedFloats,
|
||||
nonPositionedInlineLevel
|
||||
] = splitStackingContexts(stack);
|
||||
const [inlineLevel, nonInlineLevel] = splitDescendants(stack);
|
||||
|
||||
// https://www.w3.org/TR/css-position-3/#painting-order
|
||||
// 1. the background and borders of the element forming the stacking context.
|
||||
this.renderNodeBackgroundAndBorders(stack.container);
|
||||
// 2. the child stacking contexts with negative stack levels (most negative first).
|
||||
negativeZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
|
||||
// 3. For all its in-flow, non-positioned, block-level descendants in tree order:
|
||||
this.renderNodeContent(stack.container);
|
||||
nonInlineLevel.forEach(this.renderNode, this);
|
||||
// 4. All non-positioned floating descendants, in tree order. For each one of these,
|
||||
// treat the element as if it created a new stacking context, but any positioned descendants and descendants
|
||||
// which actually create a new stacking context should be considered part of the parent stacking context,
|
||||
// not this new one.
|
||||
nonPositionedFloats.forEach(this.renderStack, this);
|
||||
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
|
||||
nonPositionedInlineLevel.forEach(this.renderStack, this);
|
||||
inlineLevel.forEach(this.renderNode, this);
|
||||
// 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
|
||||
// All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
|
||||
// For those with 'z-index: auto', treat the element as if it created a new stacking context,
|
||||
// but any positioned descendants and descendants which actually create a new stacking context should be
|
||||
// considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
|
||||
// treat the stacking context generated atomically.
|
||||
//
|
||||
// All opacity descendants with opacity less than 1
|
||||
//
|
||||
// All transform descendants with transform other than none
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity.forEach(this.renderStack, this);
|
||||
// 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
|
||||
// order (smallest first) then tree order.
|
||||
positiveZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
|
||||
|
||||
if (transform !== null) {
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(stack: StackingContext): Promise<HTMLCanvasElement> {
|
||||
this.ctx.scale(this.options.scale, this.options.scale);
|
||||
this.ctx.textBaseline = 'bottom';
|
||||
if (this.options.backgroundColor) {
|
||||
this.rectangle(
|
||||
0,
|
||||
0,
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
this.options.backgroundColor
|
||||
);
|
||||
}
|
||||
this.renderStack(stack);
|
||||
return Promise.resolve(this.canvas);
|
||||
}
|
||||
}
|
||||
|
||||
const splitDescendants = (stack: StackingContext): [Array<NodeContainer>, Array<NodeContainer>] => {
|
||||
const inlineLevel = [];
|
||||
const nonInlineLevel = [];
|
||||
|
||||
const length = stack.children.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let child = stack.children[i];
|
||||
if (child.isInlineLevel()) {
|
||||
inlineLevel.push(child);
|
||||
} else {
|
||||
nonInlineLevel.push(child);
|
||||
}
|
||||
}
|
||||
return [inlineLevel, nonInlineLevel];
|
||||
};
|
||||
|
||||
const splitStackingContexts = (
|
||||
stack: StackingContext
|
||||
): [
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>
|
||||
] => {
|
||||
const negativeZIndex = [];
|
||||
const zeroOrAutoZIndexOrTransformedOrOpacity = [];
|
||||
const positiveZIndex = [];
|
||||
const nonPositionedFloats = [];
|
||||
const nonPositionedInlineLevel = [];
|
||||
const length = stack.contexts.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let child = stack.contexts[i];
|
||||
if (
|
||||
child.container.isPositioned() ||
|
||||
child.container.style.opacity < 1 ||
|
||||
child.container.isTransformed()
|
||||
) {
|
||||
if (child.container.style.zIndex.order < 0) {
|
||||
negativeZIndex.push(child);
|
||||
} else if (child.container.style.zIndex.order > 0) {
|
||||
positiveZIndex.push(child);
|
||||
} else {
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity.push(child);
|
||||
}
|
||||
} else {
|
||||
if (child.container.isFloating()) {
|
||||
nonPositionedFloats.push(child);
|
||||
} else {
|
||||
nonPositionedInlineLevel.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
negativeZIndex,
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity,
|
||||
positiveZIndex,
|
||||
nonPositionedFloats,
|
||||
nonPositionedInlineLevel
|
||||
];
|
||||
};
|
||||
|
||||
const sortByZIndex = (a: StackingContext, b: StackingContext): number => {
|
||||
if (a.container.style.zIndex.order > b.container.style.zIndex.order) {
|
||||
return 1;
|
||||
} else if (a.container.style.zIndex.order < b.container.style.zIndex.order) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
@ -27,7 +27,10 @@ export type Gradient = {
|
||||
colorStops: Array<ColorStop>
|
||||
};
|
||||
|
||||
export const parseGradient = ({args, method, prefix}: BackgroundSource, bounds: Bounds) => {
|
||||
export const parseGradient = (
|
||||
{args, method, prefix}: BackgroundSource,
|
||||
bounds: Bounds
|
||||
): ?Gradient => {
|
||||
if (method === 'linear-gradient') {
|
||||
return parseLinearGradient(args, bounds);
|
||||
}
|
||||
|
364
src/Renderer.js
Normal file
364
src/Renderer.js
Normal file
@ -0,0 +1,364 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type Color from './Color';
|
||||
import type Size from './drawing/Size';
|
||||
import type Logger from './Logger';
|
||||
|
||||
import type {BackgroundImage} from './parsing/background';
|
||||
import type {Border, BorderSide} from './parsing/border';
|
||||
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, BoundCurves} from './Bounds';
|
||||
import type {Gradient} from './Gradient';
|
||||
import type {ImageStore, ImageElement} from './ImageLoader';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type StackingContext from './StackingContext';
|
||||
import type {TextBounds} from './TextBounds';
|
||||
|
||||
import {
|
||||
Bounds,
|
||||
parsePathForBorder,
|
||||
calculateContentBox,
|
||||
calculatePaddingBox,
|
||||
calculatePaddingBoxPath
|
||||
} from './Bounds';
|
||||
import {FontMetrics} from './Font';
|
||||
import {parseGradient} from './Gradient';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {
|
||||
BACKGROUND_ORIGIN,
|
||||
calculateBackgroungPaintingArea,
|
||||
calculateBackgroundPosition,
|
||||
calculateBackgroundRepeatPath,
|
||||
calculateBackgroundSize
|
||||
} from './parsing/background';
|
||||
import {BORDER_STYLE} from './parsing/border';
|
||||
|
||||
export type RenderOptions = {
|
||||
scale: number,
|
||||
backgroundColor: ?Color,
|
||||
imageStore: ImageStore,
|
||||
fontMetrics: FontMetrics,
|
||||
logger: Logger,
|
||||
width: number,
|
||||
height: number
|
||||
};
|
||||
|
||||
export interface RenderTarget {
|
||||
clip(clipPaths: Array<Path>, callback: () => void): void,
|
||||
|
||||
drawImage(image: ImageElement, source: Bounds, destination: Bounds): void,
|
||||
|
||||
drawShape(path: Path, color: Color): void,
|
||||
|
||||
fill(color: Color): void,
|
||||
|
||||
getTarget(): Promise<HTMLCanvasElement>,
|
||||
|
||||
rectangle(x: number, y: number, width: number, height: number, color: Color): void,
|
||||
|
||||
renderLinearGradient(bounds: Bounds, gradient: Gradient): void,
|
||||
|
||||
renderRepeat(
|
||||
path: Path,
|
||||
image: ImageElement,
|
||||
imageSize: Size,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
): void,
|
||||
|
||||
renderTextNode(
|
||||
textBounds: Array<TextBounds>,
|
||||
color: Color,
|
||||
font: Font,
|
||||
textDecoration: TextDecoration,
|
||||
textShadows: Array<TextShadow> | null
|
||||
): void,
|
||||
|
||||
setOpacity(opacity: number): void,
|
||||
|
||||
transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void): void
|
||||
}
|
||||
|
||||
export default class Renderer {
|
||||
target: RenderTarget;
|
||||
options: RenderOptions;
|
||||
|
||||
constructor(target: RenderTarget, options: RenderOptions) {
|
||||
this.target = target;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
renderNode(container: NodeContainer) {
|
||||
if (container.isVisible()) {
|
||||
this.renderNodeBackgroundAndBorders(container);
|
||||
this.renderNodeContent(container);
|
||||
}
|
||||
}
|
||||
|
||||
renderNodeContent(container: NodeContainer) {
|
||||
this.target.clip(container.getClipPaths(), () => {
|
||||
if (container.childNodes.length) {
|
||||
container.childNodes.forEach(child => {
|
||||
if (child instanceof TextContainer) {
|
||||
const style = child.parent.style;
|
||||
this.target.renderTextNode(
|
||||
child.bounds,
|
||||
style.color,
|
||||
style.font,
|
||||
style.textDecoration,
|
||||
style.textShadow
|
||||
);
|
||||
} else {
|
||||
this.target.drawShape(child, container.style.color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (container.image) {
|
||||
const image = this.options.imageStore.get(container.image);
|
||||
if (image) {
|
||||
const contentBox = calculateContentBox(
|
||||
container.bounds,
|
||||
container.style.padding,
|
||||
container.style.border
|
||||
);
|
||||
const width = typeof image.width === 'number' ? image.width : contentBox.width;
|
||||
const height =
|
||||
typeof image.height === 'number' ? image.height : contentBox.height;
|
||||
this.target.clip([calculatePaddingBoxPath(container.curvedBounds)], () => {
|
||||
this.target.drawImage(image, new Bounds(0, 0, width, height), contentBox);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderNodeBackgroundAndBorders(container: NodeContainer) {
|
||||
this.target.clip(container.parent ? container.parent.getClipPaths() : [], () => {
|
||||
const backgroundPaintingArea = calculateBackgroungPaintingArea(
|
||||
container.curvedBounds,
|
||||
container.style.background.backgroundClip
|
||||
);
|
||||
this.target.clip([backgroundPaintingArea], () => {
|
||||
if (!container.style.background.backgroundColor.isTransparent()) {
|
||||
this.target.fill(container.style.background.backgroundColor);
|
||||
}
|
||||
|
||||
this.renderBackgroundImage(container);
|
||||
});
|
||||
|
||||
container.style.border.forEach((border, side) => {
|
||||
this.renderBorder(border, side, container.curvedBounds);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderBackgroundImage(container: NodeContainer) {
|
||||
container.style.background.backgroundImage.reverse().forEach(backgroundImage => {
|
||||
if (backgroundImage.source.method === 'url' && backgroundImage.source.args.length) {
|
||||
this.renderBackgroundRepeat(container, backgroundImage);
|
||||
} else {
|
||||
const gradient = parseGradient(backgroundImage.source, container.bounds);
|
||||
if (gradient) {
|
||||
const bounds = container.bounds;
|
||||
this.target.renderLinearGradient(bounds, gradient);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderBackgroundRepeat(container: NodeContainer, background: BackgroundImage) {
|
||||
const image = this.options.imageStore.get(background.source.args[0]);
|
||||
if (image) {
|
||||
const bounds = container.bounds;
|
||||
const paddingBox = calculatePaddingBox(bounds, container.style.border);
|
||||
const backgroundImageSize = calculateBackgroundSize(background, image, bounds);
|
||||
|
||||
// TODO support CONTENT_BOX
|
||||
const backgroundPositioningArea =
|
||||
container.style.background.backgroundOrigin === BACKGROUND_ORIGIN.BORDER_BOX
|
||||
? bounds
|
||||
: paddingBox;
|
||||
|
||||
const position = calculateBackgroundPosition(
|
||||
background.position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const path = calculateBackgroundRepeatPath(
|
||||
background,
|
||||
position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea,
|
||||
bounds
|
||||
);
|
||||
|
||||
const offsetX = Math.round(paddingBox.left + position.x);
|
||||
const offsetY = Math.round(paddingBox.top + position.y);
|
||||
this.target.renderRepeat(path, image, backgroundImageSize, offsetX, offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
renderBorder(border: Border, side: BorderSide, curvePoints: BoundCurves) {
|
||||
if (border.borderStyle !== BORDER_STYLE.NONE && !border.borderColor.isTransparent()) {
|
||||
this.target.drawShape(parsePathForBorder(curvePoints, side), border.borderColor);
|
||||
}
|
||||
}
|
||||
|
||||
renderStack(stack: StackingContext) {
|
||||
if (stack.container.isVisible()) {
|
||||
this.target.setOpacity(stack.getOpacity());
|
||||
const transform = stack.container.style.transform;
|
||||
if (transform !== null) {
|
||||
this.target.transform(
|
||||
stack.container.bounds.left + transform.transformOrigin[0].value,
|
||||
stack.container.bounds.top + transform.transformOrigin[1].value,
|
||||
transform.transform,
|
||||
() => this.renderStackContent(stack)
|
||||
);
|
||||
} else {
|
||||
this.renderStackContent(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderStackContent(stack: StackingContext) {
|
||||
const [
|
||||
negativeZIndex,
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity,
|
||||
positiveZIndex,
|
||||
nonPositionedFloats,
|
||||
nonPositionedInlineLevel
|
||||
] = splitStackingContexts(stack);
|
||||
const [inlineLevel, nonInlineLevel] = splitDescendants(stack);
|
||||
|
||||
// https://www.w3.org/TR/css-position-3/#painting-order
|
||||
// 1. the background and borders of the element forming the stacking context.
|
||||
this.renderNodeBackgroundAndBorders(stack.container);
|
||||
// 2. the child stacking contexts with negative stack levels (most negative first).
|
||||
negativeZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
|
||||
// 3. For all its in-flow, non-positioned, block-level descendants in tree order:
|
||||
this.renderNodeContent(stack.container);
|
||||
nonInlineLevel.forEach(this.renderNode, this);
|
||||
// 4. All non-positioned floating descendants, in tree order. For each one of these,
|
||||
// treat the element as if it created a new stacking context, but any positioned descendants and descendants
|
||||
// which actually create a new stacking context should be considered part of the parent stacking context,
|
||||
// not this new one.
|
||||
nonPositionedFloats.forEach(this.renderStack, this);
|
||||
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
|
||||
nonPositionedInlineLevel.forEach(this.renderStack, this);
|
||||
inlineLevel.forEach(this.renderNode, this);
|
||||
// 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
|
||||
// All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
|
||||
// For those with 'z-index: auto', treat the element as if it created a new stacking context,
|
||||
// but any positioned descendants and descendants which actually create a new stacking context should be
|
||||
// considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
|
||||
// treat the stacking context generated atomically.
|
||||
//
|
||||
// All opacity descendants with opacity less than 1
|
||||
//
|
||||
// All transform descendants with transform other than none
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity.forEach(this.renderStack, this);
|
||||
// 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
|
||||
// order (smallest first) then tree order.
|
||||
positiveZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
|
||||
}
|
||||
|
||||
render(stack: StackingContext): Promise<HTMLCanvasElement> {
|
||||
if (this.options.backgroundColor) {
|
||||
this.target.rectangle(
|
||||
0,
|
||||
0,
|
||||
this.options.width,
|
||||
this.options.height,
|
||||
this.options.backgroundColor
|
||||
);
|
||||
}
|
||||
this.renderStack(stack);
|
||||
const target = this.target.getTarget();
|
||||
if (__DEV__) {
|
||||
return target.then(output => {
|
||||
this.options.logger.log(`Render completed`);
|
||||
return output;
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
const splitDescendants = (stack: StackingContext): [Array<NodeContainer>, Array<NodeContainer>] => {
|
||||
const inlineLevel = [];
|
||||
const nonInlineLevel = [];
|
||||
|
||||
const length = stack.children.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let child = stack.children[i];
|
||||
if (child.isInlineLevel()) {
|
||||
inlineLevel.push(child);
|
||||
} else {
|
||||
nonInlineLevel.push(child);
|
||||
}
|
||||
}
|
||||
return [inlineLevel, nonInlineLevel];
|
||||
};
|
||||
|
||||
const splitStackingContexts = (
|
||||
stack: StackingContext
|
||||
): [
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>
|
||||
] => {
|
||||
const negativeZIndex = [];
|
||||
const zeroOrAutoZIndexOrTransformedOrOpacity = [];
|
||||
const positiveZIndex = [];
|
||||
const nonPositionedFloats = [];
|
||||
const nonPositionedInlineLevel = [];
|
||||
const length = stack.contexts.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let child = stack.contexts[i];
|
||||
if (
|
||||
child.container.isPositioned() ||
|
||||
child.container.style.opacity < 1 ||
|
||||
child.container.isTransformed()
|
||||
) {
|
||||
if (child.container.style.zIndex.order < 0) {
|
||||
negativeZIndex.push(child);
|
||||
} else if (child.container.style.zIndex.order > 0) {
|
||||
positiveZIndex.push(child);
|
||||
} else {
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity.push(child);
|
||||
}
|
||||
} else {
|
||||
if (child.container.isFloating()) {
|
||||
nonPositionedFloats.push(child);
|
||||
} else {
|
||||
nonPositionedInlineLevel.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
negativeZIndex,
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity,
|
||||
positiveZIndex,
|
||||
nonPositionedFloats,
|
||||
nonPositionedInlineLevel
|
||||
];
|
||||
};
|
||||
|
||||
const sortByZIndex = (a: StackingContext, b: StackingContext): number => {
|
||||
if (a.container.style.zIndex.order > b.container.style.zIndex.order) {
|
||||
return 1;
|
||||
} else if (a.container.style.zIndex.order < b.container.style.zIndex.order) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
18
src/index.js
18
src/index.js
@ -2,7 +2,8 @@
|
||||
'use strict';
|
||||
|
||||
import {NodeParser} from './NodeParser';
|
||||
import CanvasRenderer from './CanvasRenderer';
|
||||
import Renderer from './Renderer';
|
||||
import CanvasRenderer from './renderer/CanvasRenderer';
|
||||
import Logger from './Logger';
|
||||
import ImageLoader from './ImageLoader';
|
||||
import {Bounds, parseDocumentSize} from './Bounds';
|
||||
@ -110,12 +111,19 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<HTMLCanvasE
|
||||
if (__DEV__) {
|
||||
logger.log(`Starting renderer`);
|
||||
}
|
||||
const renderer = new CanvasRenderer(canvas, {
|
||||
scale: options.scale,
|
||||
|
||||
const renderOptions = {
|
||||
backgroundColor,
|
||||
fontMetrics,
|
||||
imageStore,
|
||||
fontMetrics
|
||||
});
|
||||
logger,
|
||||
scale: options.scale,
|
||||
width,
|
||||
height
|
||||
};
|
||||
const canvasTarget = new CanvasRenderer(canvas, renderOptions);
|
||||
|
||||
const renderer = new Renderer(canvasTarget, renderOptions);
|
||||
return renderer.render(stack);
|
||||
});
|
||||
});
|
||||
|
@ -25,13 +25,11 @@ export const TEXT_DECORATION_LINE = {
|
||||
export type TextDecorationStyle = $Values<typeof TEXT_DECORATION_STYLE>;
|
||||
export type TextDecorationLine = $Values<typeof TEXT_DECORATION_LINE>;
|
||||
type TextDecorationLineType = Array<TextDecorationLine> | null;
|
||||
export type TextDecoration =
|
||||
| {
|
||||
textDecorationLine: Array<TextDecorationLine>,
|
||||
textDecorationStyle: TextDecorationStyle,
|
||||
textDecorationColor: Color | null
|
||||
}
|
||||
| $Values<typeof TEXT_DECORATION>;
|
||||
export type TextDecoration = {
|
||||
textDecorationLine: Array<TextDecorationLine>,
|
||||
textDecorationStyle: TextDecorationStyle,
|
||||
textDecorationColor: Color | null
|
||||
} | null;
|
||||
|
||||
const parseLine = (line: string): TextDecorationLine => {
|
||||
switch (line) {
|
||||
|
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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user