From 9bdb871307f69d5c138cc0ce32662f2787466bba Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sat, 5 Aug 2017 21:13:53 +0800 Subject: [PATCH] Implement linear-gradient rendering --- src/Angle.js | 24 +++ src/CanvasRenderer.js | 19 +++ src/Gradient.js | 165 ++++++++++++++++++++ src/parsing/background.js | 4 +- tests/cases/background/linear-gradient.html | 60 ++++++- 5 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 src/Angle.js create mode 100644 src/Gradient.js diff --git a/src/Angle.js b/src/Angle.js new file mode 100644 index 0000000..2e62a89 --- /dev/null +++ b/src/Angle.js @@ -0,0 +1,24 @@ +/* @flow */ +'use strict'; + +const ANGLE = /([+-]?\d*\.?\d+)(deg|grad|rad|turn)/i; + +export const parseAngle = (angle: string): number | null => { + const match = angle.match(ANGLE); + + if (match) { + const value = parseFloat(match[1]); + switch (match[2].toLowerCase()) { + case 'deg': + return Math.PI * value / 180; + case 'grad': + return Math.PI / 200 * value; + case 'rad': + return value; + case 'turn': + return Math.PI * 2 * value; + } + } + + return null; +}; diff --git a/src/CanvasRenderer.js b/src/CanvasRenderer.js index e63bc8e..f0af78f 100644 --- a/src/CanvasRenderer.js +++ b/src/CanvasRenderer.js @@ -24,6 +24,7 @@ import { calculatePaddingBoxPath } from './Bounds'; import {FontMetrics} from './Font'; +import {parseGradient} from './Gradient'; import TextContainer from './TextContainer'; import { @@ -211,6 +212,24 @@ export default class CanvasRenderer { 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); + } } }); } diff --git a/src/Gradient.js b/src/Gradient.js new file mode 100644 index 0000000..3c01a75 --- /dev/null +++ b/src/Gradient.js @@ -0,0 +1,165 @@ +/* @flow */ +'use strict'; + +import type {BackgroundSource} from './parsing/background'; +import type {Bounds} from './Bounds'; +import {parseAngle} from './Angle'; +import Color from './Color'; +import Length, {LENGTH_TYPE} from './Length'; + +const SIDE_OR_CORNER = /^(to )?(left|top|right|bottom)( (left|top|right|bottom))?$/i; +const ENDS_WITH_LENGTH = /(px)|%|( 0)$/i; + +export type Direction = { + x0: number, + x1: number, + y0: number, + y1: number +}; + +export type ColorStop = { + color: Color, + stop: number +}; + +export type Gradient = { + direction: Direction, + colorStops: Array +}; + +export const parseGradient = ({args, method, prefix}: BackgroundSource, bounds: Bounds) => { + if (method === 'linear-gradient') { + return parseLinearGradient(args, bounds); + } + + // TODO: webkit-gradient syntax +}; + +const parseLinearGradient = (args: Array, bounds: Bounds): Gradient => { + const angle = parseAngle(args[0]); + const HAS_DIRECTION = SIDE_OR_CORNER.test(args[0]) || angle !== null; + const direction = HAS_DIRECTION + ? angle !== null + ? calculateGradientDirection(angle, bounds) + : parseSideOrCorner(args[0], bounds) + : calculateGradientDirection(Math.PI, bounds); + const colorStops = []; + const firstColorStopIndex = HAS_DIRECTION ? 1 : 0; + + for (let i = firstColorStopIndex; i < args.length; i++) { + const value = args[i]; + const HAS_LENGTH = ENDS_WITH_LENGTH.test(value); + const lastSpaceIndex = value.lastIndexOf(' '); + const color = new Color(HAS_LENGTH ? value.substring(0, lastSpaceIndex) : value); + const stop = HAS_LENGTH + ? new Length(value.substring(lastSpaceIndex + 1)) + : i === firstColorStopIndex + ? new Length('0%') + : i === args.length - 1 ? new Length('100%') : null; + colorStops.push({color, stop}); + } + + // TODO: Fix some inaccuracy with color stops with px values + const lineLength = Math.min( + Math.sqrt( + Math.pow(Math.abs(direction.x0) + Math.abs(direction.x1), 2) + + Math.pow(Math.abs(direction.y0) + Math.abs(direction.y1), 2) + ), + bounds.width * 2, + bounds.height * 2 + ); + + const absoluteValuedColorStops = colorStops.map(({color, stop}) => { + return { + color, + // $FlowFixMe + stop: stop ? stop.getAbsoluteValue(lineLength) / lineLength : null + }; + }); + + let previousColorStop = absoluteValuedColorStops[0].stop; + for (let i = 0; i < absoluteValuedColorStops.length; i++) { + if (previousColorStop !== null) { + const stop = absoluteValuedColorStops[i].stop; + if (stop === null) { + let n = i; + while (absoluteValuedColorStops[n].stop === null) { + n++; + } + const steps = n - i + 1; + const nextColorStep = absoluteValuedColorStops[n].stop; + const stepSize = (nextColorStep - previousColorStop) / steps; + for (; i < n; i++) { + previousColorStop = absoluteValuedColorStops[i].stop = + previousColorStop + stepSize; + } + } else { + previousColorStop = stop; + } + } + } + + return { + direction, + colorStops: absoluteValuedColorStops + }; +}; + +const calculateGradientDirection = (radian: number, bounds: Bounds): Direction => { + const width = bounds.width; + const height = bounds.height; + const HALF_WIDTH = width * 0.5; + const HALF_HEIGHT = height * 0.5; + const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian)); + const HALF_LINE_LENGTH = lineLength / 2; + + const x0 = HALF_WIDTH + Math.sin(radian) * HALF_LINE_LENGTH; + const y0 = HALF_HEIGHT - Math.cos(radian) * HALF_LINE_LENGTH; + const x1 = width - x0; + const y1 = height - y0; + + return {x0, x1, y0, y1}; +}; + +const parseTopRight = (bounds: Bounds) => + Math.acos( + bounds.width / 2 / (Math.sqrt(Math.pow(bounds.width, 2) + Math.pow(bounds.height, 2)) / 2) + ); + +const parseSideOrCorner = (side: string, bounds: Bounds): Direction => { + switch (side) { + case 'bottom': + case 'to top': + return calculateGradientDirection(0, bounds); + case 'left': + case 'to right': + return calculateGradientDirection(Math.PI / 2, bounds); + case 'right': + case 'to left': + return calculateGradientDirection(3 * Math.PI / 2, bounds); + case 'top right': + case 'right top': + case 'to bottom left': + case 'to left bottom': + return calculateGradientDirection(Math.PI + parseTopRight(bounds), bounds); + case 'top left': + case 'left top': + case 'to bottom right': + case 'to right bottom': + return calculateGradientDirection(Math.PI - parseTopRight(bounds), bounds); + case 'bottom left': + case 'left bottom': + case 'to top right': + case 'to right top': + return calculateGradientDirection(parseTopRight(bounds), bounds); + case 'bottom right': + case 'right bottom': + case 'to top left': + case 'to left top': + return calculateGradientDirection(2 * Math.PI - parseTopRight(bounds), bounds); + case 'top': + case 'to bottom': + default: + return calculateGradientDirection(Math.PI, bounds); + } +}; diff --git a/src/parsing/background.js b/src/parsing/background.js index 91d69c4..c99308e 100644 --- a/src/parsing/background.js +++ b/src/parsing/background.js @@ -335,7 +335,7 @@ const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array @@ -134,6 +175,19 @@
 
 
 
+
 
+
 
+
+
 
+
 
+
 
+
 
+
+
 
+
 
+
 
+
 
+