Skip to content

Commit

Permalink
Implement linear-gradient rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasvh committed Aug 5, 2017
1 parent 56b3b6d commit 9bdb871
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 5 deletions.
24 changes: 24 additions & 0 deletions src/Angle.js
Original file line number Diff line number Diff line change
@@ -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;
};
19 changes: 19 additions & 0 deletions src/CanvasRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
calculatePaddingBoxPath
} from './Bounds';
import {FontMetrics} from './Font';
import {parseGradient} from './Gradient';
import TextContainer from './TextContainer';

import {
Expand Down Expand Up @@ -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);
}
}
});
}
Expand Down
165 changes: 165 additions & 0 deletions src/Gradient.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
4 changes: 2 additions & 2 deletions src/parsing/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
60 changes: 57 additions & 3 deletions tests/cases/background/linear-gradient.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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>
Expand All @@ -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>

0 comments on commit 9bdb871

Please sign in to comment.