Implement linear-gradient rendering

This commit is contained in:
Niklas von Hertzen 2017-08-05 21:13:53 +08:00
parent 56b3b6df27
commit 9bdb871307
5 changed files with 267 additions and 5 deletions

24
src/Angle.js Normal file
View File

@ -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;
};

View File

@ -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);
}
}
});
}

165
src/Gradient.js Normal file
View File

@ -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<ColorStop>
};
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<string>, 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);
}
};

View File

@ -335,7 +335,7 @@ const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array<Ba
}
if (definition) {
args.push(definition);
args.push(definition.trim());
}
const prefix_i = method.indexOf('-', 1) + 1;
@ -402,7 +402,7 @@ const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array<Ba
return;
} else if (mode === 1) {
if (numParen === 0 && !method.match(/^url$/i)) {
args.push(definition);
args.push(definition.trim());
definition = '';
return;
}

View File

@ -16,14 +16,24 @@
border:1px solid #000;
}
.horizontal {
width: auto !important;
height: auto !important;
}
.medium div{
width:200px;
width:210px;
height:200px;
float:left;
margin:10px;
border:1px solid #000;
}
.medium .horizontal div{
width:200px;
height:100px;
}
.small, .medium{
clear:both;
}
@ -110,15 +120,46 @@
}
.linearGradient8 {
background: linear-gradient(to top left, #fff 0%, #00263c 100%);
background: linear-gradient(to bottom left, #fff 0%, #00263c 100%);
}
.linearGradient9 {
background: linear-gradient(to top left, white 0%, black 100%);
background: linear-gradient(to bottom left, #0000Ff, rgb(255, 0,0) 50px, green 199px, rgba(0, 0, 0, 0.5) 100.0%);
}
.linearGradient10 {
background: linear-gradient(to left top, #0000Ff, rgb(255, 0,0) 50px, green 199px, rgba(0, 0, 0, 0.5) 100.0%);
}
.linearGradient11 {
background: linear-gradient(to bottom right, #0000Ff, rgb(255, 0,0) 50px, green 199px, rgba(0, 0, 0, 0.5) 100.0%);
}
.linearGradient12 {
background: linear-gradient(to top right, #0000Ff, rgb(255, 0,0) 50px, green 199px, rgba(0, 0, 0, 0.5) 100.0%);
}
.linearGradient13 {
background: linear-gradient(to top left, white 0%, black 100%);
}
.linearGradient14 {
background: linear-gradient(-375.5grad, yellow, blue, red 60%, blue);
}
.linearGradient15 {
background: linear-gradient(-375.5turn, yellow, red 60%, blue);
}
.linearGradient16 {
background: linear-gradient(-375.5rad, yellow, orange, red 60%, blue);
}
.linearGradient17 {
background: linear-gradient(-375.5deg, yellow, red 60%, blue);
width: 800px !important;
}
</style>
</head>
@ -134,6 +175,19 @@
<div class="linearGradient8">&nbsp;</div>
<div class="linearGradient9">&nbsp;</div>
<div class="linearGradient10">&nbsp;</div>
<div class="linearGradient11">&nbsp;</div>
<div class="linearGradient12">&nbsp;</div>
<div class="horizontal">
<div class="linearGradient9">&nbsp;</div>
<div class="linearGradient10">&nbsp;</div>
<div class="linearGradient11">&nbsp;</div>
<div class="linearGradient12">&nbsp;</div>
</div>
<div class="linearGradient13">&nbsp;</div>
<div class="linearGradient14">&nbsp;</div>
<div class="linearGradient15">&nbsp;</div>
<div class="linearGradient16">&nbsp;</div>
<div class="linearGradient17">&nbsp;</div>
</div>
</body>
</html>