diff --git a/src/Bounds.js b/src/Bounds.js index 80f49de..255c6de 100644 --- a/src/Bounds.js +++ b/src/Bounds.js @@ -4,13 +4,11 @@ import type {Border, BorderSide} from './parsing/border'; import type {BorderRadius} from './parsing/borderRadius'; import type {Padding} from './parsing/padding'; -import type Circle from './drawing/Circle'; +import type {Path} from './drawing/Path'; import Vector from './drawing/Vector'; import BezierCurve from './drawing/BezierCurve'; -export type Path = Array | Circle; - const TOP = 0; const RIGHT = 1; const BOTTOM = 2; diff --git a/src/NodeContainer.js b/src/NodeContainer.js index cf62e59..c8cad46 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -17,9 +17,9 @@ import type {Transform} from './parsing/transform'; import type {Visibility} from './parsing/visibility'; import type {zIndex} from './parsing/zIndex'; -import type {Bounds, BoundCurves, Path} from './Bounds'; +import type {Bounds, BoundCurves} from './Bounds'; import type ImageLoader from './ImageLoader'; - +import type {Path} from './drawing/Path'; import type TextContainer from './TextContainer'; import Color from './Color'; @@ -64,7 +64,7 @@ type StyleDeclaration = { overflow: Overflow, padding: Padding, position: Position, - textDecoration: TextDecoration, + textDecoration: TextDecoration | null, textShadow: Array | null, textTransform: TextTransform, transform: Transform, diff --git a/src/Renderer.js b/src/Renderer.js index 0c3759d..0aecae4 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -2,6 +2,7 @@ 'use strict'; import type Color from './Color'; +import type {Path} from './drawing/Path'; import type Size from './drawing/Size'; import type Logger from './Logger'; @@ -12,7 +13,7 @@ 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 {BoundCurves} from './Bounds'; import type {Gradient} from './Gradient'; import type {ImageStore, ImageElement} from './ImageLoader'; import type NodeContainer from './NodeContainer'; @@ -49,7 +50,7 @@ export type RenderOptions = { height: number }; -export interface RenderTarget { +export interface RenderTarget { clip(clipPaths: Array, callback: () => void): void, drawImage(image: ImageElement, source: Bounds, destination: Bounds): void, @@ -58,10 +59,12 @@ export interface RenderTarget { fill(color: Color): void, - getTarget(): Promise, + getTarget(): Promise, rectangle(x: number, y: number, width: number, height: number, color: Color): void, + render(options: RenderOptions): void, + renderLinearGradient(bounds: Bounds, gradient: Gradient): void, renderRepeat( @@ -76,7 +79,7 @@ export interface RenderTarget { textBounds: Array, color: Color, font: Font, - textDecoration: TextDecoration, + textDecoration: TextDecoration | null, textShadows: Array | null ): void, @@ -86,12 +89,14 @@ export interface RenderTarget { } export default class Renderer { - target: RenderTarget; + target: RenderTarget<*>; options: RenderOptions; + _opacity: ?number; - constructor(target: RenderTarget, options: RenderOptions) { + constructor(target: RenderTarget<*>, options: RenderOptions) { this.target = target; this.options = options; + target.render(options); } renderNode(container: NodeContainer) { @@ -102,7 +107,7 @@ export default class Renderer { } renderNodeContent(container: NodeContainer) { - this.target.clip(container.getClipPaths(), () => { + const callback = () => { if (container.childNodes.length) { container.childNodes.forEach(child => { if (child instanceof TextContainer) { @@ -136,11 +141,17 @@ export default class Renderer { }); } } - }); + }; + const paths = container.getClipPaths(); + if (paths.length) { + this.target.clip(paths, callback); + } else { + callback(); + } } renderNodeBackgroundAndBorders(container: NodeContainer) { - this.target.clip(container.parent ? container.parent.getClipPaths() : [], () => { + const callback = () => { const backgroundPaintingArea = calculateBackgroungPaintingArea( container.curvedBounds, container.style.background.backgroundClip @@ -156,7 +167,14 @@ export default class Renderer { container.style.border.forEach((border, side) => { this.renderBorder(border, side, container.curvedBounds); }); - }); + }; + + const paths = container.parent ? container.parent.getClipPaths() : []; + if (paths.length) { + this.target.clip(paths, callback); + } else { + callback(); + } } renderBackgroundImage(container: NodeContainer) { @@ -213,7 +231,12 @@ export default class Renderer { renderStack(stack: StackingContext) { if (stack.container.isVisible()) { - this.target.setOpacity(stack.getOpacity()); + const opacity = stack.getOpacity(); + if (opacity !== this._opacity) { + this.target.setOpacity(stack.getOpacity()); + this._opacity = opacity; + } + const transform = stack.container.style.transform; if (transform !== null) { this.target.transform( @@ -270,7 +293,7 @@ export default class Renderer { positiveZIndex.sort(sortByZIndex).forEach(this.renderStack, this); } - render(stack: StackingContext): Promise { + render(stack: StackingContext): Promise<*> { if (this.options.backgroundColor) { this.target.rectangle( 0, diff --git a/src/drawing/BezierCurve.js b/src/drawing/BezierCurve.js index 122209a..2861e83 100644 --- a/src/drawing/BezierCurve.js +++ b/src/drawing/BezierCurve.js @@ -1,18 +1,22 @@ /* @flow */ 'use strict'; +import type {Drawable} from './Path'; +import {PATH} from './Path'; import Vector from './Vector'; const lerp = (a: Vector, b: Vector, t: number): Vector => { return new Vector(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); }; -export default class BezierCurve { +export default class BezierCurve implements Drawable<1> { + type: 1; start: Vector; startControl: Vector; endControl: Vector; end: Vector; constructor(start: Vector, startControl: Vector, endControl: Vector, end: Vector) { + this.type = PATH.BEZIER_CURVE; this.start = start; this.startControl = startControl; this.endControl = endControl; diff --git a/src/drawing/Circle.js b/src/drawing/Circle.js index 969c49d..06ad010 100644 --- a/src/drawing/Circle.js +++ b/src/drawing/Circle.js @@ -1,12 +1,16 @@ /* @flow */ 'use strict'; +import type {Drawable} from './Path'; +import {PATH} from './Path'; -export default class Circle { +export default class Circle implements Drawable<2> { + type: 2; x: number; y: number; radius: number; constructor(x: number, y: number, radius: number) { + this.type = PATH.CIRCLE; this.x = x; this.y = y; this.radius = radius; diff --git a/src/drawing/Path.js b/src/drawing/Path.js new file mode 100644 index 0000000..c87d8ce --- /dev/null +++ b/src/drawing/Path.js @@ -0,0 +1,20 @@ +/* @flow */ +'use strict'; + +import type Vector from './Vector'; +import type BezierCurve from './BezierCurve'; +import type Circle from './Circle'; + +export const PATH = { + VECTOR: 0, + BEZIER_CURVE: 1, + CIRCLE: 2 +}; + +export type PathType = $Values; + +export interface Drawable { + type: A +} + +export type Path = Array | Circle; diff --git a/src/drawing/Vector.js b/src/drawing/Vector.js index 5f97473..ecce15f 100644 --- a/src/drawing/Vector.js +++ b/src/drawing/Vector.js @@ -1,11 +1,15 @@ /* @flow */ 'use strict'; +import type {Drawable} from './Path'; +import {PATH} from './Path'; -export default class Vector { +export default class Vector implements Drawable<0> { + type: 0; x: number; y: number; constructor(x: number, y: number) { + this.type = PATH.VECTOR; this.x = x; this.y = y; if (__DEV__) { diff --git a/src/index.js b/src/index.js index 1c1172a..8e2bfce 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,8 @@ /* @flow */ 'use strict'; +import type {RenderTarget} from './Renderer'; + import {NodeParser} from './NodeParser'; import Renderer from './Renderer'; import CanvasRenderer from './renderer/CanvasRenderer'; @@ -19,12 +21,13 @@ export type Options = { proxy: ?string, removeContainer: ?boolean, scale: number, + target: RenderTarget<*> | Array>, type: ?string, windowWidth: number, windowHeight: number }; -const html2canvas = (element: HTMLElement, config: Options): Promise => { +const html2canvas = (element: HTMLElement, config: Options): Promise<*> => { if (typeof console === 'object' && typeof console.log === 'function') { console.log(`html2canvas ${__VERSION__}`); } @@ -37,11 +40,11 @@ const html2canvas = (element: HTMLElement, config: Options): Promise { + const renderer = new Renderer(target, renderOptions); + return renderer.render(stack); + }) + ); + } else { + const renderer = new Renderer(options.target, renderOptions); + return renderer.render(stack); + } }); }); @@ -137,4 +138,6 @@ const html2canvas = (element: HTMLElement, config: Options): Promise, textDecorationStyle: TextDecorationStyle, textDecorationColor: Color | null -} | null; +}; const parseLine = (line: string): TextDecorationLine => { switch (line) { @@ -65,7 +65,7 @@ const parseTextDecorationStyle = (style: string): TextDecorationStyle => { return TEXT_DECORATION_STYLE.SOLID; }; -export const parseTextDecoration = (style: CSSStyleDeclaration): TextDecoration => { +export const parseTextDecoration = (style: CSSStyleDeclaration): TextDecoration | null => { const textDecorationLine = parseTextDecorationLine( style.textDecorationLine ? style.textDecorationLine : style.textDecoration ); diff --git a/src/renderer/CanvasRenderer.js b/src/renderer/CanvasRenderer.js index 601f912..081f226 100644 --- a/src/renderer/CanvasRenderer.js +++ b/src/renderer/CanvasRenderer.js @@ -2,8 +2,8 @@ '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'; @@ -11,26 +11,30 @@ 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 {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 {PATH} from '../drawing/Path'; import {TEXT_DECORATION_LINE} from '../parsing/textDecoration'; -export default class CanvasRenderer implements RenderTarget { +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'); + 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.textBaseline = 'bottom'; @@ -84,25 +88,16 @@ export default class CanvasRenderer implements RenderTarget { 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 { + if (Array.isArray(path)) { path.forEach((point, index) => { - const start = point instanceof Vector ? point : point.start; + 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 instanceof BezierCurve) { + if (point.type === PATH.BEZIER_CURVE) { this.ctx.bezierCurveTo( point.startControl.x, point.startControl.y, @@ -113,6 +108,15 @@ export default class CanvasRenderer implements RenderTarget { ); } }); + } else { + this.ctx.arc( + path.x + path.radius, + path.y + path.radius, + path.radius, + 0, + Math.PI * 2, + true + ); } this.ctx.closePath(); @@ -157,7 +161,7 @@ export default class CanvasRenderer implements RenderTarget { textBounds: Array, color: Color, font: Font, - textDecoration: TextDecoration, + textDecoration: TextDecoration | null, textShadows: Array | null ) { this.ctx.font = [ diff --git a/src/renderer/RefTestRenderer.js b/src/renderer/RefTestRenderer.js new file mode 100644 index 0000000..c8152cb --- /dev/null +++ b/src/renderer/RefTestRenderer.js @@ -0,0 +1,249 @@ +/* @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, + TextDecorationStyle, + TextDecorationLine +} from '../parsing/textDecoration'; +import type {TextShadow} from '../parsing/textShadow'; +import type {Matrix} from '../parsing/transform'; + +import type {Bounds} from '../Bounds'; +import type {ImageElement} from '../ImageLoader'; +import type {Gradient} from '../Gradient'; +import type {TextBounds} from '../TextBounds'; + +import {TEXT_DECORATION_STYLE, TEXT_DECORATION_LINE} from '../parsing/textDecoration'; +import {PATH} from '../drawing/Path'; + +class RefTestRenderer implements RenderTarget { + options: RenderOptions; + indent: number; + lines: Array; + + render(options: RenderOptions) { + this.options = options; + this.indent = 0; + this.lines = []; + options.logger.log(`RefTest renderer initialized`); + } + + clip(clipPaths: Array, callback: () => void) { + this.writeLine(`Clip ${clipPaths.map(this.formatPath, this).join(', ')}`); + this.indent += 2; + callback(); + this.indent -= 2; + } + + drawImage(image: ImageElement, source: Bounds, destination: Bounds) { + this.writeLine( + `Draw image ${this.formatImage(image)} (source: ${this.formatBounds( + source + )} (destination: ${this.formatBounds(source)})` + ); + } + + drawShape(path: Path, color: Color) { + this.writeLine(`Shape ${color.toString()} ${this.formatPath(path)}`); + } + + fill(color: Color) { + this.writeLine(`Fill ${color.toString()}`); + } + + getTarget(): Promise { + return Promise.resolve(this.lines.join('\n')); + } + + rectangle(x: number, y: number, width: number, height: number, color: Color) { + const list = [x, y, width, height].map(v => Math.round(v)).join(', '); + this.writeLine(`Rectangle [${list}] ${color.toString()}`); + } + + formatBounds(bounds: Bounds): string { + const list = [bounds.left, bounds.top, bounds.width, bounds.height]; + return `[${list.map(v => Math.round(v)).join(', ')}]`; + } + + formatImage(image: ImageElement): string { + // $FlowFixMe + return image.tagName === 'CANVAS' ? 'Canvas' : `Image src="${image.src}"`; + } + + formatPath(path: Path): string { + if (!Array.isArray(path)) { + return `Circle(x: ${Math.round(path.x)}, y: ${Math.round(path.y)}, r: ${Math.round( + path.radius + )})`; + } + const string = path + .map(v => { + if (v.type === PATH.VECTOR) { + return `Vector(x: ${Math.round(v.x)}, y: ${Math.round(v.y)}))`; + } + if (v.type === PATH.BEZIER_CURVE) { + const values = [ + `x0: ${Math.round(v.start.x)}`, + `y0: ${Math.round(v.start.y)}`, + `x1: ${Math.round(v.end.x)}`, + `y1: ${Math.round(v.end.y)}`, + `cx0: ${Math.round(v.startControl.x)}`, + `cy0: ${Math.round(v.startControl.y)}`, + `cx1: ${Math.round(v.endControl.x)}`, + `cy1: ${Math.round(v.endControl.y)}` + ]; + return `BezierCurve(${values.join(', ')})`; + } + }) + .join(', '); + return `Path (${string})`; + } + + renderLinearGradient(bounds: Bounds, gradient: Gradient) { + const direction = [ + `x0: ${Math.round(gradient.direction.x0)}`, + `x1: ${Math.round(gradient.direction.x1)}`, + `y0: ${Math.round(gradient.direction.y0)}`, + `y1: ${Math.round(gradient.direction.y1)}` + ]; + + const stops = gradient.colorStops.map(stop => `${stop.color.toString()} ${stop.stop}px`); + + this.writeLine( + `${this.formatBounds(bounds)} linear-gradient(${direction.join(', ')} ${stops.join( + ', ' + )})` + ); + } + + renderRepeat( + path: Path, + image: ImageElement, + imageSize: Size, + offsetX: number, + offsetY: number + ) { + this.writeLine( + `Repeat ${this.formatImage( + image + )} [${offsetX}, ${offsetY}] Size (${imageSize.width}, ${imageSize.height}) ${this.formatPath( + path + )}` + ); + } + + renderTextNode( + textBounds: Array, + color: Color, + font: Font, + textDecoration: TextDecoration | null, + textShadows: Array | null + ) { + const fontString = [ + font.fontStyle, + font.fontVariant, + font.fontWeight, + font.fontSize, + font.fontFamily + ] + .join(' ') + .split(',')[0]; + + const textDecorationString = this.textDecoration(textDecoration, color); + const shadowString = textShadows + ? ` Shadows: (${textShadows + .map( + shadow => + `${shadow.color.toString()} ${shadow.offsetX}px ${shadow.offsetY}px ${shadow.blur}px` + ) + .join(', ')})` + : ''; + + this.writeLine( + `Text ${color.toString()} ${fontString}${shadowString}${textDecorationString}` + ); + + this.indent += 2; + textBounds.forEach(textBound => { + this.writeLine( + `[${Math.round(textBound.bounds.left)}, ${Math.round( + textBound.bounds.top + )}]: ${textBound.text}` + ); + }); + this.indent -= 2; + } + + textDecoration(textDecoration: TextDecoration | null, color: Color): string { + if (textDecoration) { + const textDecorationColor = (textDecoration.textDecorationColor + ? textDecoration.textDecorationColor + : color).toString(); + const textDecorationLines = textDecoration.textDecorationLine.map( + this.textDecorationLine, + this + ); + return textDecoration + ? ` ${this.textDecorationStyle( + textDecoration.textDecorationStyle + )} ${textDecorationColor} ${textDecorationLines.join(', ')}` + : ''; + } + + return ''; + } + + textDecorationLine(textDecorationLine: TextDecorationLine): string { + switch (textDecorationLine) { + case TEXT_DECORATION_LINE.LINE_THROUGH: + return 'line-through'; + case TEXT_DECORATION_LINE.OVERLINE: + return 'overline'; + case TEXT_DECORATION_LINE.UNDERLINE: + return 'underline'; + case TEXT_DECORATION_LINE.BLINK: + return 'blink'; + } + return 'UNKNOWN'; + } + + textDecorationStyle(textDecorationStyle: TextDecorationStyle): string { + switch (textDecorationStyle) { + case TEXT_DECORATION_STYLE.SOLID: + return 'solid'; + case TEXT_DECORATION_STYLE.DOTTED: + return 'dotted'; + case TEXT_DECORATION_STYLE.DOUBLE: + return 'double'; + case TEXT_DECORATION_STYLE.DASHED: + return 'dashed'; + case TEXT_DECORATION_STYLE.WAVY: + return 'WAVY'; + } + return 'UNKNOWN'; + } + + setOpacity(opacity: number) { + this.writeLine(`Opacity ${opacity}`); + } + + transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void) { + this.writeLine(`Transform (${offsetX}, ${offsetY}) [${matrix.join(', ')}]`); + this.indent += 2; + callback(); + this.indent -= 2; + } + + writeLine(text: string) { + this.lines.push(`${new Array(this.indent).join(' ')}${text}`); + } +} + +module.exports = RefTestRenderer; diff --git a/webpack.config.js b/webpack.config.js index 1264cf8..6012d98 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,25 +9,41 @@ const banner = Copyright (c) ${(new Date()).getFullYear()} ${pkg.author.name} <${pkg.author.url}> Released under ${pkg.license} License`; -module.exports = { - entry: './src/index.js', - output: { - filename: './dist/html2canvas.js', - library: 'html2canvas', - libraryTarget: 'umd' - }, - module: { - loaders: [{ - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader' - }] - }, - plugins: [ - new webpack.DefinePlugin({ - '__DEV__': true, - '__VERSION__': JSON.stringify(pkg.version) - }), - new webpack.BannerPlugin(banner) - ] +const plugins = [ + new webpack.DefinePlugin({ + '__DEV__': true, + '__VERSION__': JSON.stringify(pkg.version) + }), + new webpack.BannerPlugin(banner) +]; + +const modules = { + loaders: [{ + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] }; + +module.exports = [ + { + entry: './src/index.js', + output: { + filename: './dist/html2canvas.js', + library: 'html2canvas', + libraryTarget: 'umd' + }, + module: modules, + plugins + }, + { + entry: './src/renderer/RefTestRenderer.js', + output: { + filename: './dist/RefTestRenderer.js', + library: 'RefTestRenderer', + libraryTarget: 'umd' + }, + module: modules, + plugins + } +];