Merge branch 'vnmc-feature/HTC-0009_RadialGradients'

This commit is contained in:
Niklas von Hertzen 2017-12-12 20:58:07 +08:00
commit 261702a693
10 changed files with 713 additions and 58 deletions

View File

@ -1,6 +1,7 @@
### Changelog ### ### Changelog ###
#### v1.0.0-alpha4 - TBD #### #### v1.0.0-alpha4 - TBD ####
* Add support for radial-gradients
* Fix logging option (#1302) * Fix logging option (#1302)
* Add support for rendering webgl canvas content (#646) * Add support for rendering webgl canvas content (#646)
* Fix external SVG loading with proxies (#802) * Fix external SVG loading with proxies (#802)

View File

@ -11,6 +11,7 @@ Below is a list of all the supported CSS properties and values.
- background-image - background-image
- url() - url()
- linear-gradient() - linear-gradient()
- radial-gradient()
- background-origin - background-origin
- background-position - background-position
- background-size - background-size
@ -68,7 +69,6 @@ These CSS properties are **NOT** currently supported
- [filter](https://github.com/niklasvh/html2canvas/issues/493) - [filter](https://github.com/niklasvh/html2canvas/issues/493)
- [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085) - [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085)
- [list-style](https://github.com/niklasvh/html2canvas/issues/177) - [list-style](https://github.com/niklasvh/html2canvas/issues/177)
- radial-gradient()
- [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162) - [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162)
- word-break - word-break
- [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258) - [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258)

View File

@ -3,14 +3,22 @@
import type {BackgroundSource} from './parsing/background'; import type {BackgroundSource} from './parsing/background';
import type {Bounds} from './Bounds'; import type {Bounds} from './Bounds';
import NodeContainer from './NodeContainer';
import {parseAngle} from './Angle'; import {parseAngle} from './Angle';
import Color from './Color'; import Color from './Color';
import Length, {LENGTH_TYPE} from './Length'; import Length, {LENGTH_TYPE, calculateLengthFromValueWithUnit} from './Length';
import {distance} from './Util';
const SIDE_OR_CORNER = /^(to )?(left|top|right|bottom)( (left|top|right|bottom))?$/i; const SIDE_OR_CORNER = /^(to )?(left|top|right|bottom)( (left|top|right|bottom))?$/i;
const PERCENTAGE_ANGLES = /^([+-]?\d*\.?\d+)% ([+-]?\d*\.?\d+)%$/i; const PERCENTAGE_ANGLES = /^([+-]?\d*\.?\d+)% ([+-]?\d*\.?\d+)%$/i;
const ENDS_WITH_LENGTH = /(px)|%|( 0)$/i; const ENDS_WITH_LENGTH = /(px)|%|( 0)$/i;
const FROM_TO = /^(from|to)\((.+)\)$/i; const FROM_TO_COLORSTOP = /^(from|to|color-stop)\((?:([\d.]+)(%)?,\s*)?(.+?)\)$/i;
const RADIAL_SHAPE_DEFINITION = /^\s*(circle|ellipse)?\s*((?:([\d.]+)(px|r?em|%)\s*(?:([\d.]+)(px|r?em|%))?)|closest-side|closest-corner|farthest-side|farthest-corner)?\s*(?:at\s*(?:(left|center|right)|([\d.]+)(px|r?em|%))\s+(?:(top|center|bottom)|([\d.]+)(px|r?em|%)))?(?:\s|$)/i;
export type Point = {
x: number,
y: number
};
export type Direction = { export type Direction = {
x0: number, x0: number,
@ -24,12 +32,68 @@ export type ColorStop = {
stop: number stop: number
}; };
export type Gradient = { export interface Gradient {
direction: Direction, type: GradientType,
colorStops: Array<ColorStop> colorStops: Array<ColorStop>
}
export const GRADIENT_TYPE = {
LINEAR_GRADIENT: 0,
RADIAL_GRADIENT: 1
}; };
export type GradientType = $Values<typeof GRADIENT_TYPE>;
export const RADIAL_GRADIENT_SHAPE = {
CIRCLE: 0,
ELLIPSE: 1
};
export type RadialGradientShapeType = $Values<typeof RADIAL_GRADIENT_SHAPE>;
const LENGTH_FOR_POSITION = {
left: new Length('0%'),
top: new Length('0%'),
center: new Length('50%'),
right: new Length('100%'),
bottom: new Length('100%')
};
export class LinearGradient implements Gradient {
type: GradientType;
colorStops: Array<ColorStop>;
direction: Direction;
constructor(colorStops: Array<ColorStop>, direction: Direction) {
this.type = GRADIENT_TYPE.LINEAR_GRADIENT;
this.colorStops = colorStops;
this.direction = direction;
}
}
export class RadialGradient implements Gradient {
type: GradientType;
colorStops: Array<ColorStop>;
shape: RadialGradientShapeType;
center: Point;
radius: Point;
constructor(
colorStops: Array<ColorStop>,
shape: RadialGradientShapeType,
center: Point,
radius: Point
) {
this.type = GRADIENT_TYPE.RADIAL_GRADIENT;
this.colorStops = colorStops;
this.shape = shape;
this.center = center;
this.radius = radius;
}
}
export const parseGradient = ( export const parseGradient = (
container: NodeContainer,
{args, method, prefix}: BackgroundSource, {args, method, prefix}: BackgroundSource,
bounds: Bounds bounds: Bounds
): ?Gradient => { ): ?Gradient => {
@ -38,37 +102,27 @@ export const parseGradient = (
} else if (method === 'gradient' && args[0] === 'linear') { } else if (method === 'gradient' && args[0] === 'linear') {
// TODO handle correct angle // TODO handle correct angle
return parseLinearGradient( return parseLinearGradient(
['to bottom'].concat( ['to bottom'].concat(transformObsoleteColorStops(args.slice(3))),
args
.slice(3)
.map(color => color.match(FROM_TO))
.filter(v => v !== null)
// $FlowFixMe
.map(v => v[2])
),
bounds, bounds,
!!prefix !!prefix
); );
} else if (method === 'radial-gradient') {
return parseRadialGradient(
container,
prefix === '-webkit-' ? transformWebkitRadialGradientArgs(args) : args,
bounds
);
} else if (method === 'gradient' && args[0] === 'radial') {
return parseRadialGradient(
container,
transformObsoleteColorStops(transformWebkitRadialGradientArgs(args.slice(1))),
bounds
);
} }
}; };
const parseLinearGradient = (args: Array<string>, bounds: Bounds, hasPrefix: boolean): Gradient => { const parseColorStops = (args: Array<string>, firstColorStopIndex: number, lineLength: number) => {
const angle = parseAngle(args[0]);
const HAS_SIDE_OR_CORNER = SIDE_OR_CORNER.test(args[0]);
const HAS_DIRECTION = HAS_SIDE_OR_CORNER || angle !== null || PERCENTAGE_ANGLES.test(args[0]);
const direction = HAS_DIRECTION
? angle !== null
? calculateGradientDirection(
// if there is a prefix, the 0° angle points due East (instead of North per W3C)
hasPrefix ? angle - Math.PI * 0.5 : angle,
bounds
)
: HAS_SIDE_OR_CORNER
? parseSideOrCorner(args[0], bounds)
: parsePercentageAngle(args[0], bounds)
: calculateGradientDirection(Math.PI, bounds);
const colorStops = []; const colorStops = [];
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
for (let i = firstColorStopIndex; i < args.length; i++) { for (let i = firstColorStopIndex; i < args.length; i++) {
const value = args[i]; const value = args[i];
@ -83,16 +137,6 @@ const parseLinearGradient = (args: Array<string>, bounds: Bounds, hasPrefix: boo
colorStops.push({color, stop}); 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}) => { const absoluteValuedColorStops = colorStops.map(({color, stop}) => {
return { return {
color, color,
@ -123,10 +167,104 @@ const parseLinearGradient = (args: Array<string>, bounds: Bounds, hasPrefix: boo
} }
} }
return { return absoluteValuedColorStops;
direction, };
colorStops: absoluteValuedColorStops
const parseLinearGradient = (
args: Array<string>,
bounds: Bounds,
hasPrefix: boolean
): LinearGradient => {
const angle = parseAngle(args[0]);
const HAS_SIDE_OR_CORNER = SIDE_OR_CORNER.test(args[0]);
const HAS_DIRECTION = HAS_SIDE_OR_CORNER || angle !== null || PERCENTAGE_ANGLES.test(args[0]);
const direction = HAS_DIRECTION
? angle !== null
? calculateGradientDirection(
// if there is a prefix, the 0° angle points due East (instead of North per W3C)
hasPrefix ? angle - Math.PI * 0.5 : angle,
bounds
)
: HAS_SIDE_OR_CORNER
? parseSideOrCorner(args[0], bounds)
: parsePercentageAngle(args[0], bounds)
: calculateGradientDirection(Math.PI, bounds);
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
// TODO: Fix some inaccuracy with color stops with px values
const lineLength = Math.min(
distance(
Math.abs(direction.x0) + Math.abs(direction.x1),
Math.abs(direction.y0) + Math.abs(direction.y1)
),
bounds.width * 2,
bounds.height * 2
);
return new LinearGradient(parseColorStops(args, firstColorStopIndex, lineLength), direction);
};
const parseRadialGradient = (
container: NodeContainer,
args: Array<string>,
bounds: Bounds
): RadialGradient => {
const m = args[0].match(RADIAL_SHAPE_DEFINITION);
const shape =
m &&
(m[1] === 'circle' || // explicit shape specification
(m[3] !== undefined && m[5] === undefined)) // only one radius coordinate
? RADIAL_GRADIENT_SHAPE.CIRCLE
: RADIAL_GRADIENT_SHAPE.ELLIPSE;
const radius = {};
const center = {};
if (m) {
// Radius
if (m[3] !== undefined) {
radius.x = calculateLengthFromValueWithUnit(container, m[3], m[4]).getAbsoluteValue(
bounds.width
);
}
if (m[5] !== undefined) {
radius.y = calculateLengthFromValueWithUnit(container, m[5], m[6]).getAbsoluteValue(
bounds.height
);
}
// Position
if (m[7]) {
center.x = LENGTH_FOR_POSITION[m[7].toLowerCase()];
} else if (m[8] !== undefined) {
center.x = calculateLengthFromValueWithUnit(container, m[8], m[9]);
}
if (m[10]) {
center.y = LENGTH_FOR_POSITION[m[10].toLowerCase()];
} else if (m[11] !== undefined) {
center.y = calculateLengthFromValueWithUnit(container, m[11], m[12]);
}
}
const gradientCenter = {
x: center.x === undefined ? bounds.width / 2 : center.x.getAbsoluteValue(bounds.width),
y: center.y === undefined ? bounds.height / 2 : center.y.getAbsoluteValue(bounds.height)
}; };
const gradientRadius = calculateRadius(
(m && m[2]) || 'farthest-corner',
shape,
gradientCenter,
radius,
bounds
);
return new RadialGradient(
parseColorStops(args, m ? 1 : 0, Math.min(gradientRadius.x, gradientRadius.y)),
shape,
gradientCenter,
gradientRadius
);
}; };
const calculateGradientDirection = (radian: number, bounds: Bounds): Direction => { const calculateGradientDirection = (radian: number, bounds: Bounds): Direction => {
@ -146,9 +284,7 @@ const calculateGradientDirection = (radian: number, bounds: Bounds): Direction =
}; };
const parseTopRight = (bounds: Bounds) => const parseTopRight = (bounds: Bounds) =>
Math.acos( Math.acos(bounds.width / 2 / (distance(bounds.width, bounds.height) / 2));
bounds.width / 2 / (Math.sqrt(Math.pow(bounds.width, 2) + Math.pow(bounds.height, 2)) / 2)
);
const parseSideOrCorner = (side: string, bounds: Bounds): Direction => { const parseSideOrCorner = (side: string, bounds: Bounds): Direction => {
switch (side) { switch (side) {
@ -194,3 +330,221 @@ const parsePercentageAngle = (angle: string, bounds: Bounds): Direction => {
return calculateGradientDirection(Math.atan(isNaN(ratio) ? 1 : ratio) + Math.PI / 2, bounds); return calculateGradientDirection(Math.atan(isNaN(ratio) ? 1 : ratio) + Math.PI / 2, bounds);
}; };
const findCorner = (bounds: Bounds, x: number, y: number, closest: boolean): Point => {
var corners = [
{x: 0, y: 0},
{x: 0, y: bounds.height},
{x: bounds.width, y: 0},
{x: bounds.width, y: bounds.height}
];
let optimumDistance = closest ? Infinity : -Infinity;
let optimumCorner = null;
for (let corner of corners) {
const d = distance(x - corner.x, y - corner.y);
if (closest ? d < optimumDistance : d > optimumDistance) {
optimumDistance = d;
optimumCorner = corner;
}
}
// $FlowFixMe
return optimumCorner;
};
const calculateRadius = (
extent: string,
shape: RadialGradientShapeType,
center: Point,
radius: Point,
bounds: Bounds
): Point => {
const x = center.x;
const y = center.y;
let rx = 0;
let ry = 0;
switch (extent) {
case 'closest-side':
// The ending shape is sized so that that it exactly meets the side of the gradient box closest to the gradients center.
// If the shape is an ellipse, it exactly meets the closest side in each dimension.
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.min(
Math.abs(x),
Math.abs(x - bounds.width),
Math.abs(y),
Math.abs(y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
rx = Math.min(Math.abs(x), Math.abs(x - bounds.width));
ry = Math.min(Math.abs(y), Math.abs(y - bounds.height));
}
break;
case 'closest-corner':
// The ending shape is sized so that that it passes through the corner of the gradient box closest to the gradients center.
// If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified.
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.min(
distance(x, y),
distance(x, y - bounds.height),
distance(x - bounds.width, y),
distance(x - bounds.width, y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
// Compute the ratio ry/rx (which is to be the same as for "closest-side")
const c =
Math.min(Math.abs(y), Math.abs(y - bounds.height)) /
Math.min(Math.abs(x), Math.abs(x - bounds.width));
const corner = findCorner(bounds, x, y, true);
rx = distance(corner.x - x, (corner.y - y) / c);
ry = c * rx;
}
break;
case 'farthest-side':
// Same as closest-side, except the ending shape is sized based on the farthest side(s)
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.max(
Math.abs(x),
Math.abs(x - bounds.width),
Math.abs(y),
Math.abs(y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
rx = Math.max(Math.abs(x), Math.abs(x - bounds.width));
ry = Math.max(Math.abs(y), Math.abs(y - bounds.height));
}
break;
case 'farthest-corner':
// Same as closest-corner, except the ending shape is sized based on the farthest corner.
// If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified.
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.max(
distance(x, y),
distance(x, y - bounds.height),
distance(x - bounds.width, y),
distance(x - bounds.width, y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
// Compute the ratio ry/rx (which is to be the same as for "farthest-side")
const c =
Math.max(Math.abs(y), Math.abs(y - bounds.height)) /
Math.max(Math.abs(x), Math.abs(x - bounds.width));
const corner = findCorner(bounds, x, y, false);
rx = distance(corner.x - x, (corner.y - y) / c);
ry = c * rx;
}
break;
default:
// pixel or percentage values
rx = radius.x || 0;
ry = radius.y !== undefined ? radius.y : rx;
break;
}
return {
x: rx,
y: ry
};
};
export const transformWebkitRadialGradientArgs = (args: Array<string>): Array<string> => {
let shape = '';
let radius = '';
let extent = '';
let position = '';
let idx = 0;
const POSITION = /^(left|center|right|\d+(?:px|r?em|%)?)(?:\s+(top|center|bottom|\d+(?:px|r?em|%)?))?$/i;
const SHAPE_AND_EXTENT = /^(circle|ellipse)?\s*(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)?$/i;
const RADIUS = /^\d+(px|r?em|%)?(?:\s+\d+(px|r?em|%)?)?$/i;
const matchStartPosition = args[idx].match(POSITION);
if (matchStartPosition) {
idx++;
}
const matchShapeExtent = args[idx].match(SHAPE_AND_EXTENT);
if (matchShapeExtent) {
shape = matchShapeExtent[1] || '';
extent = matchShapeExtent[2] || '';
if (extent === 'contain') {
extent = 'closest-side';
} else if (extent === 'cover') {
extent = 'farthest-corner';
}
idx++;
}
const matchStartRadius = args[idx].match(RADIUS);
if (matchStartRadius) {
idx++;
}
const matchEndPosition = args[idx].match(POSITION);
if (matchEndPosition) {
idx++;
}
const matchEndRadius = args[idx].match(RADIUS);
if (matchEndRadius) {
idx++;
}
const matchPosition = matchEndPosition || matchStartPosition;
if (matchPosition && matchPosition[1]) {
position = matchPosition[1] + (/^\d+$/.test(matchPosition[1]) ? 'px' : '');
if (matchPosition[2]) {
position += ' ' + matchPosition[2] + (/^\d+$/.test(matchPosition[2]) ? 'px' : '');
}
}
const matchRadius = matchEndRadius || matchStartRadius;
if (matchRadius) {
radius = matchRadius[0];
if (!matchRadius[1]) {
radius += 'px';
}
}
if (position && !shape && !radius && !extent) {
radius = position;
position = '';
}
if (position) {
position = `at ${position}`;
}
return [[shape, extent, radius, position].filter(s => !!s).join(' ')].concat(args.slice(idx));
};
const transformObsoleteColorStops = (args: Array<string>): Array<string> => {
return (
args
.map(color => color.match(FROM_TO_COLORSTOP))
// $FlowFixMe
.map((v: Array<string>, index: number) => {
if (!v) {
return args[index];
}
switch (v[1]) {
case 'from':
return `${v[4]} 0%`;
case 'to':
return `${v[4]} 100%`;
case 'color-stop':
if (v[3] === '%') {
return `${v[4]} ${v[2]}`;
}
return `${v[4]} ${parseFloat(v[2]) * 100}%`;
}
})
);
};

View File

@ -1,6 +1,10 @@
/* @flow */ /* @flow */
'use strict'; 'use strict';
import NodeContainer from './NodeContainer';
const LENGTH_WITH_UNIT = /([\d.]+)(px|r?em|%)/i;
export const LENGTH_TYPE = { export const LENGTH_TYPE = {
PX: 0, PX: 0,
PERCENTAGE: 1 PERCENTAGE: 1
@ -34,3 +38,31 @@ export default class Length {
return new Length(v); return new Length(v);
} }
} }
const getRootFontSize = (container: NodeContainer): number => {
const parent = container.parent;
return parent ? getRootFontSize(parent) : parseFloat(container.style.font.fontSize);
};
export const calculateLengthFromValueWithUnit = (
container: NodeContainer,
value: string,
unit: string
): Length => {
switch (unit) {
case 'px':
case '%':
return new Length(value + unit);
case 'em':
case 'rem':
const length = new Length(value);
length.value *=
unit === 'em'
? parseFloat(container.style.font.fontSize)
: getRootFontSize(container);
return length;
default:
// TODO: handle correctly if unknown unit is used
return new Length('0');
}
};

View File

@ -14,7 +14,7 @@ import type {TextShadow} from './parsing/textShadow';
import type {Matrix} from './parsing/transform'; import type {Matrix} from './parsing/transform';
import type {BoundCurves} from './Bounds'; import type {BoundCurves} from './Bounds';
import type {Gradient} from './Gradient'; import type {LinearGradient, RadialGradient} from './Gradient';
import type {ResourceStore, ImageElement} from './ResourceLoader'; import type {ResourceStore, ImageElement} from './ResourceLoader';
import type NodeContainer from './NodeContainer'; import type NodeContainer from './NodeContainer';
import type StackingContext from './StackingContext'; import type StackingContext from './StackingContext';
@ -22,7 +22,7 @@ import type {TextBounds} from './TextBounds';
import {Bounds, parsePathForBorder, calculateContentBox, calculatePaddingBoxPath} from './Bounds'; import {Bounds, parsePathForBorder, calculateContentBox, calculatePaddingBoxPath} from './Bounds';
import {FontMetrics} from './Font'; import {FontMetrics} from './Font';
import {parseGradient} from './Gradient'; import {parseGradient, GRADIENT_TYPE} from './Gradient';
import TextContainer from './TextContainer'; import TextContainer from './TextContainer';
import { import {
@ -62,7 +62,9 @@ export interface RenderTarget<Output> {
render(options: RenderOptions): void, render(options: RenderOptions): void,
renderLinearGradient(bounds: Bounds, gradient: Gradient): void, renderLinearGradient(bounds: Bounds, gradient: LinearGradient): void,
renderRadialGradient(bounds: Bounds, gradient: RadialGradient): void,
renderRepeat( renderRepeat(
path: Path, path: Path,
@ -265,9 +267,18 @@ export default class Renderer {
backgroundImageSize.height backgroundImageSize.height
); );
const gradient = parseGradient(background.source, gradientBounds); const gradient = parseGradient(container, background.source, gradientBounds);
if (gradient) { if (gradient) {
this.target.renderLinearGradient(gradientBounds, gradient); switch (gradient.type) {
case GRADIENT_TYPE.LINEAR_GRADIENT:
// $FlowFixMe
this.target.renderLinearGradient(gradientBounds, gradient);
break;
case GRADIENT_TYPE.RADIAL_GRADIENT:
// $FlowFixMe
this.target.renderRadialGradient(gradientBounds, gradient);
break;
}
} }
} }

View File

@ -3,6 +3,8 @@
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0; export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
export const distance = (a: number, b: number): number => Math.sqrt(a * a + b * b);
export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => { export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => {
// Edge does not provide value for cssText // Edge does not provide value for cssText
for (let i = style.length - 1; i >= 0; i--) { for (let i = style.length - 1; i >= 0; i--) {

View File

@ -13,12 +13,23 @@ import type {Matrix} from '../parsing/transform';
import type {Bounds} from '../Bounds'; import type {Bounds} from '../Bounds';
import type {ImageElement} from '../ResourceLoader'; import type {ImageElement} from '../ResourceLoader';
import type {Gradient} from '../Gradient'; import type {LinearGradient, RadialGradient} from '../Gradient';
import type {TextBounds} from '../TextBounds'; import type {TextBounds} from '../TextBounds';
import {PATH} from '../drawing/Path'; import {PATH} from '../drawing/Path';
import {TEXT_DECORATION_LINE} from '../parsing/textDecoration'; import {TEXT_DECORATION_LINE} from '../parsing/textDecoration';
const addColorStops = (
gradient: LinearGradient | RadialGradient,
canvasGradient: CanvasGradient
): void => {
const maxStop = Math.max.apply(null, gradient.colorStops.map(colorStop => colorStop.stop));
const f = 1 / Math.max(1, maxStop);
gradient.colorStops.forEach(colorStop => {
canvasGradient.addColorStop(f * colorStop.stop, colorStop.color.toString());
});
};
export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> { export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
@ -131,7 +142,7 @@ export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
this.ctx.fillRect(x, y, width, height); this.ctx.fillRect(x, y, width, height);
} }
renderLinearGradient(bounds: Bounds, gradient: Gradient) { renderLinearGradient(bounds: Bounds, gradient: LinearGradient) {
const linearGradient = this.ctx.createLinearGradient( const linearGradient = this.ctx.createLinearGradient(
bounds.left + gradient.direction.x1, bounds.left + gradient.direction.x1,
bounds.top + gradient.direction.y1, bounds.top + gradient.direction.y1,
@ -139,14 +150,43 @@ export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
bounds.top + gradient.direction.y0 bounds.top + gradient.direction.y0
); );
gradient.colorStops.forEach(colorStop => { addColorStops(gradient, linearGradient);
linearGradient.addColorStop(colorStop.stop, colorStop.color.toString());
});
this.ctx.fillStyle = linearGradient; this.ctx.fillStyle = linearGradient;
this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height); this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
} }
renderRadialGradient(bounds: Bounds, gradient: RadialGradient) {
const x = bounds.left + gradient.center.x;
const y = bounds.top + gradient.center.y;
const radialGradient = this.ctx.createRadialGradient(x, y, 0, x, y, gradient.radius.x);
if (!radialGradient) {
return;
}
addColorStops(gradient, radialGradient);
this.ctx.fillStyle = radialGradient;
if (gradient.radius.x !== gradient.radius.y) {
// transforms for elliptical radial gradient
const midX = bounds.left + 0.5 * bounds.width;
const midY = bounds.top + 0.5 * bounds.height;
const f = gradient.radius.y / gradient.radius.x;
const invF = 1 / f;
this.transform(midX, midY, [1, 0, 0, f, 0, 0], () =>
this.ctx.fillRect(
bounds.left,
invF * (bounds.top - midY) + midY,
bounds.width,
bounds.height * invF
)
);
} else {
this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
}
}
renderRepeat( renderRepeat(
path: Path, path: Path,
image: ImageElement, image: ImageElement,

View File

@ -18,7 +18,7 @@ import type {Matrix} from '../parsing/transform';
import type {Bounds} from '../Bounds'; import type {Bounds} from '../Bounds';
import type {ImageElement} from '../ResourceLoader'; import type {ImageElement} from '../ResourceLoader';
import type {Gradient} from '../Gradient'; import type {LinearGradient, RadialGradient} from '../Gradient';
import type {TextBounds} from '../TextBounds'; import type {TextBounds} from '../TextBounds';
import {TEXT_DECORATION_STYLE, TEXT_DECORATION_LINE} from '../parsing/textDecoration'; import {TEXT_DECORATION_STYLE, TEXT_DECORATION_LINE} from '../parsing/textDecoration';
@ -110,7 +110,7 @@ class RefTestRenderer implements RenderTarget<string> {
return `Path (${string})`; return `Path (${string})`;
} }
renderLinearGradient(bounds: Bounds, gradient: Gradient) { renderLinearGradient(bounds: Bounds, gradient: LinearGradient) {
const direction = [ const direction = [
`x0: ${Math.round(gradient.direction.x0)}`, `x0: ${Math.round(gradient.direction.x0)}`,
`x1: ${Math.round(gradient.direction.x1)}`, `x1: ${Math.round(gradient.direction.x1)}`,
@ -129,6 +129,19 @@ class RefTestRenderer implements RenderTarget<string> {
); );
} }
renderRadialGradient(bounds: Bounds, gradient: RadialGradient) {
const stops = gradient.colorStops.map(
stop => `${stop.color.toString()} ${Math.ceil(stop.stop * 100) / 100}`
);
this.writeLine(
`RadialGradient: ${this.formatBounds(bounds)} radial-gradient(${gradient.radius
.x} ${gradient.radius.y} at ${gradient.center.x} ${gradient.center.y}, ${stops.join(
', '
)})`
);
}
renderRepeat( renderRepeat(
path: Path, path: Path,
image: ImageElement, image: ImageElement,

151
tests/node/gradient.js Normal file
View File

@ -0,0 +1,151 @@
const Gradient = require('../../dist/npm/Gradient');
const assert = require('assert');
describe('Gradient', () => {
describe('transformWebkitRadialGradientArgs', () => {
it('white, black', () => {
assert.equal(Gradient.transformWebkitRadialGradientArgs(['white', 'black'])[0], '');
});
it('circle, white, black', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs(['circle', 'white', 'black'])[0],
'circle'
);
});
it('10% 30%, white, black', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs(['10% 30%', 'white', 'black'])[0],
'10% 30%'
);
});
it('30% 30%, closest-corner, white, black', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'30% 30%',
'closest-corner',
'white',
'black'
])[0],
'closest-corner at 30% 30%'
);
});
it('30% 30%, circle closest-corner, white, black', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'30% 30%',
'circle closest-corner',
'white',
'black'
])[0],
'circle closest-corner at 30% 30%'
);
});
it('center, 5em 40px, white, black', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'center',
'5em 40px',
'white',
'black'
])[0],
'5em 40px at center'
);
});
it('45 45, 10, 52 50, 30, from(#A7D30C), to(red)', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'45 45',
'10',
'52 50',
'30',
'from(#A7D30C)'
])[0],
'30px at 52px 50px'
);
});
it('75% 19%, ellipse closest-side, #ababab, #0000ff 33%,#991f1f 100%', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'75% 19%',
'ellipse closest-side',
'#ababab',
'#0000ff 33%',
'#991f1f 100%'
])[0],
'ellipse closest-side at 75% 19%'
);
});
it('75% 19%, circle contain, #ababab, #0000ff 33%,#991f1f 100%', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'75% 19%',
'circle contain',
'#ababab',
'#0000ff 33%',
'#991f1f 100%'
])[0],
'circle closest-side at 75% 19%'
);
});
it('75% 19%, circle cover, #ababab, #0000ff 33%,#991f1f 100%', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'75% 19%',
'circle cover',
'#ababab',
'#0000ff 33%',
'#991f1f 100%'
])[0],
'circle farthest-corner at 75% 19%'
);
});
it('right 19%, ellipse cover, #ababab, #0000ff 33%,#991f1f 100%', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'right 19%',
'ellipse cover',
'#ababab',
'#0000ff 33%',
'#991f1f 100%'
])[0],
'ellipse farthest-corner at right 19%'
);
});
it('left 19%, ellipse cover, #ababab, #0000ff 33%,#991f1f 100%', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'left 19%',
'ellipse cover',
'#ababab',
'#0000ff 33%',
'#991f1f 100%'
])[0],
'ellipse farthest-corner at left 19%'
);
});
it('left top, circle cover, #ababab, #0000ff 33%,#991f1f 100%', () => {
assert.equal(
Gradient.transformWebkitRadialGradientArgs([
'left top',
'circle cover',
'#ababab',
'#0000ff 33%',
'#991f1f 100%'
])[0],
'circle farthest-corner at left top'
);
});
});
});

View File

@ -0,0 +1,51 @@
<!doctype html>
<html>
<head>
<title>Background attribute tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
div {
display: inline-block;
width: 100px;
height: 100px;
padding: 10px;
margin: 5px;
border: 15px solid black;
vertical-align: top;
}
</style>
</head>
<body>
<div style="background: radial-gradient(red, blue)"></div>
<div style="background: radial-gradient(circle, red, blue)"></div>
<div style="background: radial-gradient(ellipse, red, blue)"></div>
<div style="background: radial-gradient(circle, red, blue); width:200px"></div>
<div style="background: radial-gradient(ellipse, red, blue); width:200px"></div>
<div style="background: radial-gradient(closest-side, red, blue)"></div>
<div style="background: radial-gradient(closest-corner, red, blue)"></div>
<div style="background: radial-gradient(farthest-side, red, blue)"></div>
<div style="background: radial-gradient(farthest-corner, red, blue)"></div>
<div style="background: radial-gradient(circle 20px, red, blue)"></div>
<div style="background: radial-gradient(ellipse 20px 30px, red, blue)"></div>
<div style="font-size: 24px; border: none; padding: 0; width: auto;">
<div style="background: radial-gradient(circle 20px at 2em 80px, red, blue)"></div>
<div style="background: radial-gradient(circle 20px at 6rem 80px, red, blue)"></div>
</div>
<div style="background: radial-gradient(circle farthest-side, red, blue)"></div>
<div style="background: radial-gradient(at 20px 20px, red, blue)"></div>
<div style="background: radial-gradient(ellipse farthest-corner at 45px 45px , #00FFFF 0%, rgba(0, 0, 255, 0) 50%, #0000FF 95%);"></div>
<div style="background: radial-gradient(16px at 70px 50% , #000000 0%, #000000 14px, rgba(0, 0, 0, 0.3) 18px, rgba(0, 0, 0, 0) 19px);"></div>
<div style="background: radial-gradient(16px at 70px 50% , #000000 0%, #000000 87.5%, rgba(0, 0, 0, 0.3) 112.5%, rgba(0, 0, 0, 0) 118.75%);"></div>
<div style="background: radial-gradient(19px at 70px 50% , #000000 0%, #000000 73.68%, rgba(0, 0, 0, 0.3) 94.74%, rgba(0, 0, 0, 0) 100%);"></div>
<div style="background: radial-gradient(ellipse 60px 30px at 70px 50%, #000000 0%, blue 10%, yellow 15%, red 18%);"></div>
<div style="background: radial-gradient(circle farthest-corner at left top, aquamarine, deeppink);"></div>
<div style="width: 300px; height: 200px; transform: translate(20px, 30px) rotate(20deg)">
<div style="width: 200px; height: 150px; transform: translate(-10px, -20px) rotate(10deg)">
<div style="background: radial-gradient(circle, red, blue)"></div>
<div style="background: radial-gradient(ellipse, red, blue); width:150px;"></div>
</div>
</div>
</body>
</html>