diff --git a/src/BezierCurve.js b/src/BezierCurve.js index c7ec69a..122209a 100644 --- a/src/BezierCurve.js +++ b/src/BezierCurve.js @@ -2,6 +2,10 @@ 'use strict'; 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 { start: Vector; startControl: Vector; @@ -15,21 +19,16 @@ export default class BezierCurve { this.end = end; } - 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); - } - - subdivide(t: number): [BezierCurve, BezierCurve] { - const ab = this.lerp(this.start, this.startControl, t); - const bc = this.lerp(this.startControl, this.endControl, t); - const cd = this.lerp(this.endControl, this.end, t); - const abbc = this.lerp(ab, bc, t); - const bccd = this.lerp(bc, cd, t); - const dest = this.lerp(abbc, bccd, t); - return [ - new BezierCurve(this.start, ab, abbc, dest), - new BezierCurve(dest, bccd, cd, this.end) - ]; + subdivide(t: number, firstHalf: boolean): BezierCurve { + const ab = lerp(this.start, this.startControl, t); + const bc = lerp(this.startControl, this.endControl, t); + const cd = lerp(this.endControl, this.end, t); + const abbc = lerp(ab, bc, t); + const bccd = lerp(bc, cd, t); + const dest = lerp(abbc, bccd, t); + return firstHalf + ? new BezierCurve(this.start, ab, abbc, dest) + : new BezierCurve(dest, bccd, cd, this.end); } reverse(): BezierCurve { diff --git a/src/Bounds.js b/src/Bounds.js index 1388fdc..83a9571 100644 --- a/src/Bounds.js +++ b/src/Bounds.js @@ -15,15 +15,18 @@ const RIGHT = 1; const BOTTOM = 2; const LEFT = 3; +const H = 0; +const V = 0; + export type BoundCurves = { - topLeftOuter: [BezierCurve, BezierCurve], - topLeftInner: [BezierCurve, BezierCurve], - topRightOuter: [BezierCurve, BezierCurve], - topRightInner: [BezierCurve, BezierCurve], - bottomRightOuter: [BezierCurve, BezierCurve], - bottomRightInner: [BezierCurve, BezierCurve], - bottomLeftOuter: [BezierCurve, BezierCurve], - bottomLeftInner: [BezierCurve, BezierCurve] + topLeftOuter: BezierCurve | Vector, + topLeftInner: BezierCurve | Vector, + topRightOuter: BezierCurve | Vector, + topRightInner: BezierCurve | Vector, + bottomRightOuter: BezierCurve | Vector, + bottomRightInner: BezierCurve | Vector, + bottomLeftOuter: BezierCurve | Vector, + bottomLeftInner: BezierCurve | Vector }; export class Bounds { @@ -123,6 +126,7 @@ export const parsePathForBorder = (curves: BoundCurves, borderSide: BorderSide): curves.bottomLeftOuter, curves.bottomLeftInner ); + case LEFT: default: return createPathFromCurves( curves.bottomLeftOuter, @@ -134,16 +138,35 @@ export const parsePathForBorder = (curves: BoundCurves, borderSide: BorderSide): }; const createPathFromCurves = ( - outer1: [BezierCurve, BezierCurve], - inner1: [BezierCurve, BezierCurve], - outer2: [BezierCurve, BezierCurve], - inner2: [BezierCurve, BezierCurve] + outer1: BezierCurve | Vector, + inner1: BezierCurve | Vector, + outer2: BezierCurve | Vector, + inner2: BezierCurve | Vector ): Path => { const path = []; - path.push(outer1[1]); - path.push(outer2[0]); - path.push(inner2[0].reverse()); - path.push(inner1[1].reverse()); + if (outer1 instanceof BezierCurve) { + path.push(outer1.subdivide(0.5, false)); + } else { + path.push(outer1); + } + + if (outer2 instanceof BezierCurve) { + path.push(outer2.subdivide(0.5, true)); + } else { + path.push(outer2); + } + + if (inner2 instanceof BezierCurve) { + path.push(inner2.subdivide(0.5, true).reverse()); + } else { + path.push(inner2); + } + + if (inner1 instanceof BezierCurve) { + path.push(inner1.subdivide(0.5, false).reverse()); + } else { + path.push(inner1); + } return path; }; @@ -154,22 +177,40 @@ export const parseBoundCurves = ( borderRadius: Array ): BoundCurves => { // TODO support percentage borderRadius + const HALF_WIDTH = bounds.width / 2; + const HALF_HEIGHT = bounds.height / 2; const tlh = - borderRadius[0][0].value < bounds.width / 2 ? borderRadius[0][0].value : bounds.width / 2; + borderRadius[CORNER.TOP_LEFT][H].value < HALF_WIDTH + ? borderRadius[CORNER.TOP_LEFT][H].value + : HALF_WIDTH; const tlv = - borderRadius[0][1].value < bounds.height / 2 ? borderRadius[0][1].value : bounds.height / 2; + borderRadius[CORNER.TOP_LEFT][V].value < HALF_HEIGHT + ? borderRadius[CORNER.TOP_LEFT][V].value + : HALF_HEIGHT; const trh = - borderRadius[1][0].value < bounds.width / 2 ? borderRadius[1][0].value : bounds.width / 2; + borderRadius[CORNER.TOP_RIGHT][H].value < HALF_WIDTH + ? borderRadius[CORNER.TOP_RIGHT][H].value + : HALF_WIDTH; const trv = - borderRadius[1][1].value < bounds.height / 2 ? borderRadius[1][1].value : bounds.height / 2; + borderRadius[CORNER.TOP_RIGHT][V].value < HALF_HEIGHT + ? borderRadius[CORNER.TOP_RIGHT][V].value + : HALF_HEIGHT; const brh = - borderRadius[2][0].value < bounds.width / 2 ? borderRadius[2][0].value : bounds.width / 2; + borderRadius[CORNER.BOTTOM_RIGHT][H].value < HALF_WIDTH + ? borderRadius[CORNER.BOTTOM_RIGHT][H].value + : HALF_WIDTH; const brv = - borderRadius[2][1].value < bounds.height / 2 ? borderRadius[2][1].value : bounds.height / 2; + borderRadius[CORNER.BOTTOM_RIGHT][V].value < HALF_HEIGHT + ? borderRadius[CORNER.BOTTOM_RIGHT][V].value + : HALF_HEIGHT; const blh = - borderRadius[3][0].value < bounds.width / 2 ? borderRadius[3][0].value : bounds.width / 2; + borderRadius[CORNER.BOTTOM_LEFT][H].value < HALF_WIDTH + ? borderRadius[CORNER.BOTTOM_LEFT][H].value + : HALF_WIDTH; const blv = - borderRadius[3][1].value < bounds.height / 2 ? borderRadius[3][1].value : bounds.height / 2; + borderRadius[CORNER.BOTTOM_LEFT][V].value < HALF_HEIGHT + ? borderRadius[CORNER.BOTTOM_LEFT][V].value + : HALF_HEIGHT; const topWidth = bounds.width - trh; const rightHeight = bounds.height - brv; @@ -177,58 +218,82 @@ export const parseBoundCurves = ( const leftHeight = bounds.height - blv; return { - topLeftOuter: getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT).subdivide( - 0.5 - ), - topLeftInner: getCurvePoints( - bounds.left + borders[3].borderWidth, - bounds.top + borders[0].borderWidth, - Math.max(0, tlh - borders[3].borderWidth), - Math.max(0, tlv - borders[0].borderWidth), - CORNER.TOP_LEFT - ).subdivide(0.5), - topRightOuter: getCurvePoints( - bounds.left + topWidth, - bounds.top, - trh, - trv, - CORNER.TOP_RIGHT - ).subdivide(0.5), - topRightInner: getCurvePoints( - bounds.left + Math.min(topWidth, bounds.width + borders[3].borderWidth), - bounds.top + borders[0].borderWidth, - topWidth > bounds.width + borders[3].borderWidth ? 0 : trh - borders[3].borderWidth, - trv - borders[0].borderWidth, - CORNER.TOP_RIGHT - ).subdivide(0.5), - bottomRightOuter: getCurvePoints( - bounds.left + bottomWidth, - bounds.top + rightHeight, - brh, - brv, - CORNER.BOTTOM_RIGHT - ).subdivide(0.5), - bottomRightInner: getCurvePoints( - bounds.left + Math.min(bottomWidth, bounds.width - borders[3].borderWidth), - bounds.top + Math.min(rightHeight, bounds.height + borders[0].borderWidth), - Math.max(0, brh - borders[1].borderWidth), - brv - borders[2].borderWidth, - CORNER.BOTTOM_RIGHT - ).subdivide(0.5), - bottomLeftOuter: getCurvePoints( - bounds.left, - bounds.top + leftHeight, - blh, - blv, - CORNER.BOTTOM_LEFT - ).subdivide(0.5), - bottomLeftInner: getCurvePoints( - bounds.left + borders[3].borderWidth, - bounds.top + leftHeight, - Math.max(0, blh - borders[3].borderWidth), - blv - borders[2].borderWidth, - CORNER.BOTTOM_LEFT - ).subdivide(0.5) + topLeftOuter: + tlh > 0 || tlv > 0 + ? getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT) + : new Vector(bounds.left, bounds.top), + topLeftInner: + tlh > 0 || tlv > 0 + ? getCurvePoints( + bounds.left + borders[LEFT].borderWidth, + bounds.top + borders[TOP].borderWidth, + Math.max(0, tlh - borders[LEFT].borderWidth), + Math.max(0, tlv - borders[TOP].borderWidth), + CORNER.TOP_LEFT + ) + : new Vector( + bounds.left + borders[LEFT].borderWidth, + bounds.top + borders[TOP].borderWidth + ), + topRightOuter: + trh > 0 || trv > 0 + ? getCurvePoints(bounds.left + topWidth, bounds.top, trh, trv, CORNER.TOP_RIGHT) + : new Vector(bounds.left + bounds.width, bounds.top), + topRightInner: + trh > 0 || trv > 0 + ? getCurvePoints( + bounds.left + Math.min(topWidth, bounds.width + borders[LEFT].borderWidth), + bounds.top + borders[TOP].borderWidth, + topWidth > bounds.width + borders[LEFT].borderWidth + ? 0 + : trh - borders[LEFT].borderWidth, + trv - borders[TOP].borderWidth, + CORNER.TOP_RIGHT + ) + : new Vector( + bounds.left + bounds.width - borders[RIGHT].borderWidth, + bounds.top + borders[TOP].borderWidth + ), + bottomRightOuter: + brh > 0 || brv > 0 + ? getCurvePoints( + bounds.left + bottomWidth, + bounds.top + rightHeight, + brh, + brv, + CORNER.BOTTOM_RIGHT + ) + : new Vector(bounds.left + bounds.width, bounds.top + bounds.height), + bottomRightInner: + brh > 0 || brv > 0 + ? getCurvePoints( + bounds.left + Math.min(bottomWidth, bounds.width - borders[LEFT].borderWidth), + bounds.top + Math.min(rightHeight, bounds.height + borders[TOP].borderWidth), + Math.max(0, brh - borders[RIGHT].borderWidth), + brv - borders[BOTTOM].borderWidth, + CORNER.BOTTOM_RIGHT + ) + : new Vector( + bounds.left + bounds.width - borders[RIGHT].borderWidth, + bounds.top + bounds.height - borders[BOTTOM].borderWidth + ), + bottomLeftOuter: + blh > 0 || blv > 0 + ? getCurvePoints(bounds.left, bounds.top + leftHeight, blh, blv, CORNER.BOTTOM_LEFT) + : new Vector(bounds.left, bounds.top + bounds.height), + bottomLeftInner: + blh > 0 || blv > 0 + ? getCurvePoints( + bounds.left + borders[LEFT].borderWidth, + bounds.top + leftHeight, + Math.max(0, blh - borders[LEFT].borderWidth), + blv - borders[BOTTOM].borderWidth, + CORNER.BOTTOM_LEFT + ) + : new Vector( + bounds.left + borders[LEFT].borderWidth, + bounds.top + bounds.height - borders[BOTTOM].borderWidth + ) }; }; @@ -276,11 +341,13 @@ const getCurvePoints = ( new Vector(x + ox, ym), new Vector(x, ym) ); + case CORNER.BOTTOM_LEFT: + default: + return new BezierCurve( + new Vector(xm, ym), + new Vector(xm - ox, ym), + new Vector(x, y + oy), + new Vector(x, y) + ); } - return new BezierCurve( - new Vector(xm, ym), - new Vector(xm - ox, ym), - new Vector(x, y + oy), - new Vector(x, y) - ); }; diff --git a/src/CanvasRenderer.js b/src/CanvasRenderer.js index 71a2fd0..d1416d3 100644 --- a/src/CanvasRenderer.js +++ b/src/CanvasRenderer.js @@ -16,9 +16,12 @@ import type {ImageStore} from './ImageLoader'; import type StackingContext from './StackingContext'; import { - calculateBackgroundSize, + BACKGROUND_CLIP, + BACKGROUND_ORIGIN, + calculateBackgroungPaintingArea, calculateBackgroundPosition, - calculateBackgroundRepeatPath + calculateBackgroundRepeatPath, + calculateBackgroundSize } from './parsing/background'; import {BORDER_STYLE} from './parsing/border'; import { @@ -98,7 +101,20 @@ export default class CanvasRenderer { container.style.borderRadius ); - this.renderBackground(container); + const backgroungPaintingArea = calculateBackgroungPaintingArea( + curvePoints, + 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, curvePoints); }); @@ -112,25 +128,6 @@ export default class CanvasRenderer { this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height); } - renderBackground(container: NodeContainer) { - if (container.bounds.height > 0 && container.bounds.width > 0) { - this.renderBackgroundColor(container); - this.renderBackgroundImage(container); - } - } - - renderBackgroundColor(container: NodeContainer) { - if (!container.style.background.backgroundColor.isTransparent()) { - this.rectangle( - container.bounds.left, - container.bounds.top, - container.bounds.width, - container.bounds.height, - container.style.background.backgroundColor - ); - } - } - renderBackgroundImage(container: NodeContainer) { container.style.background.backgroundImage.reverse().forEach(backgroundImage => { if (backgroundImage.source.method === 'url' && backgroundImage.source.args.length) { @@ -144,13 +141,33 @@ export default class CanvasRenderer { if (image) { const bounds = container.bounds; const paddingBox = calculatePaddingBox(bounds, container.style.border); - const size = calculateBackgroundSize(background, image, bounds); - const position = calculateBackgroundPosition(background.position, size, bounds); - const path = calculateBackgroundRepeatPath(background, position, size, paddingBox); + 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, size), 'repeat'); + this.ctx.fillStyle = this.ctx.createPattern( + this.resizeImage(image, backgroundImageSize), + 'repeat' + ); this.ctx.translate(offsetX, offsetY); this.ctx.fill(); this.ctx.translate(-offsetX, -offsetY); diff --git a/src/parsing/background.js b/src/parsing/background.js index b399c79..170b42d 100644 --- a/src/parsing/background.js +++ b/src/parsing/background.js @@ -1,7 +1,7 @@ /* @flow */ 'use strict'; -import type {Bounds} from '../Bounds'; +import type {Bounds, BoundCurves, Path} from '../Bounds'; import type ImageLoader from '../ImageLoader'; import Color from '../Color'; @@ -11,9 +11,13 @@ import Vector from '../Vector'; export type Background = { backgroundImage: Array, - backgroundColor: Color + backgroundClip: BackgroundClip, + backgroundColor: Color, + backgroundOrigin: BackgroundOrigin }; +export type BackgroundClip = $Values; +export type BackgroundOrigin = $Values; export type BackgroundRepeat = $Values; export type BackgroundSizeTypes = $Values; @@ -44,6 +48,14 @@ export const BACKGROUND_SIZE = { LENGTH: 3 }; +export const BACKGROUND_CLIP = { + BORDER_BOX: 0, + PADDING_BOX: 1, + CONTENT_BOX: 2 +}; + +export const BACKGROUND_ORIGIN = BACKGROUND_CLIP; + const AUTO = 'auto'; class BackgroundSize { @@ -79,8 +91,8 @@ export const calculateBackgroundSize = ( const targetRatio = bounds.width / bounds.height; const currentRatio = image.width / image.height; return targetRatio < currentRatio !== (size[0].size === BACKGROUND_SIZE.COVER) - ? new Size(bounds.height * currentRatio, bounds.height) - : new Size(bounds.width, bounds.width / currentRatio); + ? new Size(bounds.width, bounds.width / currentRatio) + : new Size(bounds.height * currentRatio, bounds.height); } if (size[0].value) { @@ -104,6 +116,30 @@ export const calculateBackgroundSize = ( const AUTO_SIZE = new BackgroundSize(AUTO); +export const calculateBackgroungPaintingArea = ( + curves: BoundCurves, + clip: BackgroundClip +): Path => { + // TODO support CONTENT_BOX + switch (clip) { + case BACKGROUND_CLIP.BORDER_BOX: + return [ + curves.topLeftOuter, + curves.topRightOuter, + curves.bottomRightOuter, + curves.bottomLeftOuter + ]; + case BACKGROUND_CLIP.PADDING_BOX: + default: + return [ + curves.topLeftInner, + curves.topRightInner, + curves.bottomRightInner, + curves.bottomLeftInner + ]; + } +}; + export const calculateBackgroundPosition = ( position: [Length, Length], size: Size, @@ -119,59 +155,66 @@ export const calculateBackgroundRepeatPath = ( background: BackgroundImage, position: Vector, size: Size, + backgroundPositioningArea: Bounds, bounds: Bounds ) => { const repeat = background.repeat; switch (repeat) { case BACKGROUND_REPEAT.REPEAT_X: return [ - new Vector(Math.round(bounds.left), Math.round(bounds.top + position.y)), new Vector( - Math.round(bounds.left + bounds.width), - Math.round(bounds.top + position.y) + Math.round(bounds.left), + Math.round(backgroundPositioningArea.top + position.y) ), new Vector( Math.round(bounds.left + bounds.width), - Math.round(size.height + bounds.top + position.y) + Math.round(backgroundPositioningArea.top + position.y) + ), + new Vector( + Math.round(bounds.left + bounds.width), + Math.round(size.height + backgroundPositioningArea.top + position.y) ), new Vector( Math.round(bounds.left), - Math.round(size.height + bounds.top + position.y) + Math.round(size.height + backgroundPositioningArea.top + position.y) ) ]; case BACKGROUND_REPEAT.REPEAT_Y: return [ - new Vector(Math.round(bounds.left + position.x), Math.round(bounds.top)), new Vector( - Math.round(bounds.left + position.x + size.width), + Math.round(backgroundPositioningArea.left + position.x), Math.round(bounds.top) ), new Vector( - Math.round(bounds.left + position.x + size.width), + Math.round(backgroundPositioningArea.left + position.x + size.width), + Math.round(bounds.top) + ), + new Vector( + Math.round(backgroundPositioningArea.left + position.x + size.width), Math.round(bounds.height + bounds.top) ), new Vector( - Math.round(bounds.left + position.x), + Math.round(backgroundPositioningArea.left + position.x), Math.round(bounds.height + bounds.top) ) ]; case BACKGROUND_REPEAT.NO_REPEAT: return [ new Vector( - Math.round(bounds.left + position.x), - Math.round(bounds.top + position.y) + Math.round(backgroundPositioningArea.left + position.x), + Math.round(backgroundPositioningArea.top + position.y) ), new Vector( - Math.round(bounds.left + position.x + size.width), - Math.round(bounds.top + position.y) + Math.round(backgroundPositioningArea.left + position.x + size.width), + Math.round(backgroundPositioningArea.top + position.y) ), new Vector( - Math.round(bounds.left + position.x + size.width), - Math.round(bounds.top + position.y + size.height) + Math.round(backgroundPositioningArea.left + position.x + size.width), + Math.round(backgroundPositioningArea.top + position.y + size.height) ), new Vector( - Math.round(bounds.left + position.x), - Math.round(bounds.top + position.y + size.height) + Math.round(backgroundPositioningArea.left + position.x), + Math.round(backgroundPositioningArea.top + position.y + size.height) ) ]; default: @@ -192,11 +235,33 @@ export const parseBackground = ( imageLoader: ImageLoader ): Background => { return { + backgroundColor: new Color(style.backgroundColor), backgroundImage: parseBackgroundImages(style, imageLoader), - backgroundColor: new Color(style.backgroundColor) + backgroundClip: parseBackgroundClip(style.backgroundClip), + backgroundOrigin: parseBackgroundOrigin(style.backgroundOrigin) }; }; +const parseBackgroundClip = (backgroundClip: string): BackgroundClip => { + switch (backgroundClip) { + case 'padding-box': + return BACKGROUND_CLIP.PADDING_BOX; + case 'content-box': + return BACKGROUND_CLIP.CONTENT_BOX; + } + return BACKGROUND_CLIP.BORDER_BOX; +}; + +const parseBackgroundOrigin = (backgroundOrigin: string): BackgroundOrigin => { + switch (backgroundOrigin) { + case 'padding-box': + return BACKGROUND_ORIGIN.PADDING_BOX; + case 'content-box': + return BACKGROUND_ORIGIN.CONTENT_BOX; + } + return BACKGROUND_ORIGIN.BORDER_BOX; +}; + const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => { switch (backgroundRepeat.trim()) { case 'no-repeat':