diff --git a/src/Bounds.js b/src/Bounds.js index c492fb2..80f49de 100644 --- a/src/Bounds.js +++ b/src/Bounds.js @@ -4,11 +4,12 @@ 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 Vector from './Vector'; -import BezierCurve from './BezierCurve'; +import Vector from './drawing/Vector'; +import BezierCurve from './drawing/BezierCurve'; -export type Path = Array; +export type Path = Array | Circle; const TOP = 0; const RIGHT = 1; diff --git a/src/CanvasRenderer.js b/src/CanvasRenderer.js index 6d6284a..e63bc8e 100644 --- a/src/CanvasRenderer.js +++ b/src/CanvasRenderer.js @@ -1,19 +1,30 @@ /* @flow */ 'use strict'; + import type Color from './Color'; -import type Size from './Size'; -import type {Path, BoundCurves} from './Bounds'; -import type {TextBounds} from './TextBounds'; +import type Size from './drawing/Size'; + import type {BackgroundImage} from './parsing/background'; import type {Border, BorderSide} from './parsing/border'; -import Vector from './Vector'; -import BezierCurve from './BezierCurve'; - -import type NodeContainer from './NodeContainer'; -import type TextContainer from './TextContainer'; +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 TextContainer from './TextContainer'; import { BACKGROUND_ORIGIN, @@ -24,14 +35,6 @@ import { } from './parsing/background'; import {BORDER_STYLE} from './parsing/border'; import {TEXT_DECORATION_LINE} from './parsing/textDecoration'; -import { - parsePathForBorder, - calculateContentBox, - calculatePaddingBox, - calculatePaddingBoxPath -} from './Bounds'; - -import {FontMetrics} from './Font'; export type RenderOptions = { scale: number, @@ -68,7 +71,7 @@ export default class CanvasRenderer { }); } - if (container.textNodes.length) { + if (container.childNodes.length) { this.ctx.fillStyle = container.style.color.toString(); this.ctx.font = [ container.style.font.fontStyle, @@ -79,7 +82,14 @@ export default class CanvasRenderer { ] .join(' ') .split(',')[0]; - container.textNodes.forEach(this.renderTextNode, this); + container.childNodes.forEach(child => { + if (child instanceof TextContainer) { + this.renderTextNode(child); + } else { + this.path(child); + this.ctx.fill(); + } + }); } if (container.image) { @@ -267,25 +277,37 @@ export default class CanvasRenderer { path(path: Path) { this.ctx.beginPath(); - 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 (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 + ); + } + }); + } - 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(); } diff --git a/src/Input.js b/src/Input.js new file mode 100644 index 0000000..caa42ae --- /dev/null +++ b/src/Input.js @@ -0,0 +1,128 @@ +/* @flow */ +'use strict'; +import type NodeContainer from './NodeContainer'; +import TextContainer from './TextContainer'; + +import {BACKGROUND_CLIP, BACKGROUND_ORIGIN} from './parsing/background'; +import {BORDER_STYLE} from './parsing/border'; + +import Circle from './drawing/Circle'; +import Color from './Color'; +import Length from './Length'; +import {Bounds} from './Bounds'; +import {TextBounds} from './TextBounds'; + +export const INPUT_COLOR = new Color([42, 42, 42]); +const INPUT_BORDER_COLOR = new Color([165, 165, 165]); +const INPUT_BACKGROUND_COLOR = new Color([222, 222, 222]); +const INPUT_BORDER = { + borderWidth: 1, + borderColor: INPUT_BORDER_COLOR, + borderStyle: BORDER_STYLE.SOLID +}; +export const INPUT_BORDERS = [INPUT_BORDER, INPUT_BORDER, INPUT_BORDER, INPUT_BORDER]; +export const INPUT_BACKGROUND = { + backgroundColor: INPUT_BACKGROUND_COLOR, + backgroundImage: [], + backgroundClip: BACKGROUND_CLIP.PADDING_BOX, + backgroundOrigin: BACKGROUND_ORIGIN.PADDING_BOX +}; + +const RADIO_BORDER_RADIUS = new Length('50%'); +const RADIO_BORDER_RADIUS_TUPLE = [RADIO_BORDER_RADIUS, RADIO_BORDER_RADIUS]; +const INPUT_RADIO_BORDER_RADIUS = [ + RADIO_BORDER_RADIUS_TUPLE, + RADIO_BORDER_RADIUS_TUPLE, + RADIO_BORDER_RADIUS_TUPLE, + RADIO_BORDER_RADIUS_TUPLE +]; + +const CHECKBOX_BORDER_RADIUS = new Length('3px'); +const CHECKBOX_BORDER_RADIUS_TUPLE = [CHECKBOX_BORDER_RADIUS, CHECKBOX_BORDER_RADIUS]; +const INPUT_CHECKBOX_BORDER_RADIUS = [ + CHECKBOX_BORDER_RADIUS_TUPLE, + CHECKBOX_BORDER_RADIUS_TUPLE, + CHECKBOX_BORDER_RADIUS_TUPLE, + CHECKBOX_BORDER_RADIUS_TUPLE +]; + +export const getInputBorderRadius = (node: HTMLInputElement) => { + return node.type === 'radio' ? INPUT_RADIO_BORDER_RADIUS : INPUT_CHECKBOX_BORDER_RADIUS; +}; + +export const inlineInputElement = (node: HTMLInputElement, container: NodeContainer): void => { + if (node.type === 'radio' || node.type === 'checkbox') { + if (node.checked) { + const size = Math.min(container.bounds.width, container.bounds.height); + // TODO draw checkmark with Path Array + container.style.font.fontSize = `${size - 3}px`; + container.childNodes.push( + node.type === 'checkbox' + ? new TextContainer('\u2714', container, [ + new TextBounds( + '\u2714', + new Bounds( + container.bounds.left + size / 6, + container.bounds.top + size - 1, + 0, + 0 + ) + ) + ]) + : new Circle( + container.bounds.left + size / 4, + container.bounds.top + size / 4, + size / 4 + ) + ); + } + } else { + inlineFormElement(getInputValue(node), node, container); + } +}; + +export const inlineTextAreaElement = ( + node: HTMLTextAreaElement, + container: NodeContainer +): void => { + inlineFormElement(node.value, node, container); +}; + +export const inlineSelectElement = (node: HTMLSelectElement, container: NodeContainer): void => { + const option = node.options[node.selectedIndex || 0]; + inlineFormElement(option ? option.text || '' : '', node, container); +}; + +export const reformatInputBounds = (bounds: Bounds): Bounds => { + if (bounds.width > bounds.height) { + bounds.left += (bounds.width - bounds.height) / 2; + bounds.width = bounds.height; + } else if (bounds.width < bounds.height) { + bounds.top += (bounds.height - bounds.width) / 2; + bounds.height = bounds.width; + } + return bounds; +}; + +const inlineFormElement = (value: string, node: HTMLElement, container: NodeContainer): void => { + const body = node.ownerDocument.body; + if (value.length > 0 && body) { + const wrapper = node.ownerDocument.createElement('html2canvaswrapper'); + wrapper.style = node.ownerDocument.defaultView.getComputedStyle(node, null).cssText; + wrapper.style.position = 'fixed'; + wrapper.style.left = `${container.bounds.left}px`; + wrapper.style.top = `${container.bounds.top}px`; + const text = node.ownerDocument.createTextNode(value); + wrapper.appendChild(text); + body.appendChild(wrapper); + container.childNodes.push(TextContainer.fromTextNode(text, container)); + body.removeChild(wrapper); + } +}; + +const getInputValue = (node: HTMLInputElement): string => { + const value = + node.type === 'password' ? new Array(node.value.length + 1).join('\u2022') : node.value; + + return value.length === 0 ? node.placeholder || '' : value; +}; diff --git a/src/NodeContainer.js b/src/NodeContainer.js index a38edfd..218b052 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -41,6 +41,13 @@ import {parseVisibility, VISIBILITY} from './parsing/visibility'; import {parseZIndex} from './parsing/zIndex'; import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds'; +import { + INPUT_BACKGROUND, + INPUT_BORDERS, + INPUT_COLOR, + getInputBorderRadius, + reformatInputBounds +} from './Input'; type StyleDeclaration = { background: Background, @@ -66,22 +73,30 @@ export default class NodeContainer { name: ?string; parent: ?NodeContainer; style: StyleDeclaration; - textNodes: Array; + childNodes: Array; bounds: Bounds; curvedBounds: BoundCurves; image: ?string; constructor(node: HTMLElement, parent: ?NodeContainer, imageLoader: ImageLoader) { this.parent = parent; - this.textNodes = []; - const style = node.ownerDocument.defaultView.getComputedStyle(node, null); + this.childNodes = []; + const defaultView = node.ownerDocument.defaultView; + const style = defaultView.getComputedStyle(node, null); const display = parseDisplay(style.display); + const IS_INPUT = node.type === 'radio' || node.type === 'checkbox'; + this.style = { - background: parseBackground(style, imageLoader), - border: parseBorder(style), - borderRadius: parseBorderRadius(style), - color: new Color(style.color), + background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, imageLoader), + border: IS_INPUT ? INPUT_BORDERS : parseBorder(style), + borderRadius: + (node instanceof defaultView.HTMLInputElement || + node instanceof HTMLInputElement) && + IS_INPUT + ? getInputBorderRadius(node) + : parseBorderRadius(style), + color: IS_INPUT ? INPUT_COLOR : new Color(style.color), display: display, float: parseCSSFloat(style.float), font: parseFont(style), @@ -103,7 +118,7 @@ export default class NodeContainer { } this.image = getImage(node, imageLoader); - this.bounds = parseBounds(node); + this.bounds = IS_INPUT ? reformatInputBounds(parseBounds(node)) : parseBounds(node); this.curvedBounds = parseBoundCurves( this.bounds, this.style.border, diff --git a/src/NodeParser.js b/src/NodeParser.js index 45efb70..bbc834a 100644 --- a/src/NodeParser.js +++ b/src/NodeParser.js @@ -5,6 +5,7 @@ import type Logger from './Logger'; import StackingContext from './StackingContext'; import NodeContainer from './NodeContainer'; import TextContainer from './TextContainer'; +import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input'; export const NodeParser = ( node: HTMLElement, @@ -45,17 +46,29 @@ const parseNodeTree = ( for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) { nextNode = childNode.nextSibling; const defaultView = childNode.ownerDocument.defaultView; - if (childNode instanceof defaultView.Text) { + if (childNode instanceof defaultView.Text || childNode instanceof Text) { if (childNode.data.trim().length > 0) { - parent.textNodes.push(new TextContainer(childNode, parent)); + parent.childNodes.push(TextContainer.fromTextNode(childNode, parent)); } - } else if (childNode instanceof defaultView.HTMLElement) { + } else if ( + childNode instanceof defaultView.HTMLElement || + childNode instanceof HTMLElement + ) { if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) { inlinePseudoElement(childNode, PSEUDO_BEFORE); inlinePseudoElement(childNode, PSEUDO_AFTER); const container = new NodeContainer(childNode, parent, imageLoader); - if (container.isVisible()) { + if (childNode.tagName === 'INPUT') { + // $FlowFixMe + inlineInputElement(childNode, container); + } else if (childNode.tagName === 'TEXTAREA') { + // $FlowFixMe + inlineTextAreaElement(childNode, container); + } else if (childNode.tagName === 'SELECT') { + // $FlowFixMe + inlineSelectElement(childNode, container); + } const treatAsRealStackingContext = createsRealStackingContext( container, childNode @@ -105,7 +118,7 @@ const inlinePseudoElement = (node: HTMLElement, pseudoElt: ':before' | ':after') // $FlowFixMe anonymousReplacedElement.src = stripQuotes(image[1]); } else { - anonymousReplacedElement.appendChild(node.ownerDocument.createTextNode(content)); + anonymousReplacedElement.textContent = content; } anonymousReplacedElement.style = style.cssText; diff --git a/src/TextBounds.js b/src/TextBounds.js index 4073fb0..d4cf384 100644 --- a/src/TextBounds.js +++ b/src/TextBounds.js @@ -2,7 +2,7 @@ 'use strict'; import {ucs2} from 'punycode'; -import type TextContainer from './TextContainer'; +import type NodeContainer from './NodeContainer'; import {Bounds, parseBounds} from './Bounds'; import {TEXT_DECORATION} from './parsing/textDecoration'; @@ -24,20 +24,20 @@ export class TextBounds { } } -export const parseTextBounds = (textContainer: TextContainer, node: Text): Array => { - const codePoints = ucs2.decode(textContainer.text); - const letterRendering = - textContainer.parent.style.letterSpacing !== 0 || hasUnicodeCharacters(textContainer.text); +export const parseTextBounds = ( + value: string, + parent: NodeContainer, + node: Text +): Array => { + const codePoints = ucs2.decode(value); + const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value); const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints); const length = textList.length; const textBounds = []; let offset = 0; for (let i = 0; i < length; i++) { let text = textList[i]; - if ( - textContainer.parent.style.textDecoration !== TEXT_DECORATION.NONE || - text.trim().length > 0 - ) { + if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { textBounds.push(new TextBounds(text, getRangeBounds(node, offset, text.length))); } else { diff --git a/src/TextContainer.js b/src/TextContainer.js index 2e16d2a..3f7dea4 100644 --- a/src/TextContainer.js +++ b/src/TextContainer.js @@ -12,10 +12,15 @@ export default class TextContainer { parent: NodeContainer; bounds: Array; - constructor(node: Text, parent: NodeContainer) { - this.text = transform(node.data, parent.style.textTransform); + constructor(text: string, parent: NodeContainer, bounds: Array) { + this.text = text; this.parent = parent; - this.bounds = parseTextBounds(this, node); + this.bounds = bounds; + } + + static fromTextNode(node: Text, parent: NodeContainer) { + const text = transform(node.data, parent.style.textTransform); + return new TextContainer(text, parent, parseTextBounds(text, parent, node)); } } diff --git a/src/BezierCurve.js b/src/drawing/BezierCurve.js similarity index 100% rename from src/BezierCurve.js rename to src/drawing/BezierCurve.js diff --git a/src/drawing/Circle.js b/src/drawing/Circle.js new file mode 100644 index 0000000..969c49d --- /dev/null +++ b/src/drawing/Circle.js @@ -0,0 +1,25 @@ +/* @flow */ +'use strict'; + +export default class Circle { + x: number; + y: number; + radius: number; + + constructor(x: number, y: number, radius: number) { + this.x = x; + this.y = y; + this.radius = radius; + if (__DEV__) { + if (isNaN(x)) { + console.error(`Invalid x value given for Circle`); + } + if (isNaN(y)) { + console.error(`Invalid y value given for Circle`); + } + if (isNaN(radius)) { + console.error(`Invalid radius value given for Circle`); + } + } + } +} diff --git a/src/Size.js b/src/drawing/Size.js similarity index 100% rename from src/Size.js rename to src/drawing/Size.js diff --git a/src/Vector.js b/src/drawing/Vector.js similarity index 100% rename from src/Vector.js rename to src/drawing/Vector.js diff --git a/src/parsing/background.js b/src/parsing/background.js index 4a70a03..91d69c4 100644 --- a/src/parsing/background.js +++ b/src/parsing/background.js @@ -6,8 +6,8 @@ import type ImageLoader, {ImageElement} from '../ImageLoader'; import Color from '../Color'; import Length from '../Length'; -import Size from '../Size'; -import Vector from '../Vector'; +import Size from '../drawing/Size'; +import Vector from '../drawing/Vector'; import {calculateBorderBoxPath, calculatePaddingBoxPath} from '../Bounds'; export type Background = {