mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Merge branch 'vnmc-feature/HTC-0009_RadialGradients'
This commit is contained in:
commit
261702a693
@ -1,6 +1,7 @@
|
||||
### Changelog ###
|
||||
|
||||
#### v1.0.0-alpha4 - TBD ####
|
||||
* Add support for radial-gradients
|
||||
* Fix logging option (#1302)
|
||||
* Add support for rendering webgl canvas content (#646)
|
||||
* Fix external SVG loading with proxies (#802)
|
||||
|
@ -11,6 +11,7 @@ Below is a list of all the supported CSS properties and values.
|
||||
- background-image
|
||||
- url()
|
||||
- linear-gradient()
|
||||
- radial-gradient()
|
||||
- background-origin
|
||||
- background-position
|
||||
- background-size
|
||||
@ -68,7 +69,6 @@ These CSS properties are **NOT** currently supported
|
||||
- [filter](https://github.com/niklasvh/html2canvas/issues/493)
|
||||
- [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085)
|
||||
- [list-style](https://github.com/niklasvh/html2canvas/issues/177)
|
||||
- radial-gradient()
|
||||
- [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162)
|
||||
- word-break
|
||||
- [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258)
|
||||
|
442
src/Gradient.js
442
src/Gradient.js
@ -3,14 +3,22 @@
|
||||
|
||||
import type {BackgroundSource} from './parsing/background';
|
||||
import type {Bounds} from './Bounds';
|
||||
import NodeContainer from './NodeContainer';
|
||||
import {parseAngle} from './Angle';
|
||||
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 PERCENTAGE_ANGLES = /^([+-]?\d*\.?\d+)% ([+-]?\d*\.?\d+)%$/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 = {
|
||||
x0: number,
|
||||
@ -24,12 +32,68 @@ export type ColorStop = {
|
||||
stop: number
|
||||
};
|
||||
|
||||
export type Gradient = {
|
||||
direction: Direction,
|
||||
export interface Gradient {
|
||||
type: GradientType,
|
||||
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 = (
|
||||
container: NodeContainer,
|
||||
{args, method, prefix}: BackgroundSource,
|
||||
bounds: Bounds
|
||||
): ?Gradient => {
|
||||
@ -38,37 +102,27 @@ export const parseGradient = (
|
||||
} else if (method === 'gradient' && args[0] === 'linear') {
|
||||
// TODO handle correct angle
|
||||
return parseLinearGradient(
|
||||
['to bottom'].concat(
|
||||
args
|
||||
.slice(3)
|
||||
.map(color => color.match(FROM_TO))
|
||||
.filter(v => v !== null)
|
||||
// $FlowFixMe
|
||||
.map(v => v[2])
|
||||
),
|
||||
['to bottom'].concat(transformObsoleteColorStops(args.slice(3))),
|
||||
bounds,
|
||||
!!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 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 parseColorStops = (args: Array<string>, firstColorStopIndex: number, lineLength: number) => {
|
||||
const colorStops = [];
|
||||
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
|
||||
|
||||
for (let i = firstColorStopIndex; i < args.length; i++) {
|
||||
const value = args[i];
|
||||
@ -83,16 +137,6 @@ const parseLinearGradient = (args: Array<string>, bounds: Bounds, hasPrefix: boo
|
||||
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,
|
||||
@ -123,10 +167,104 @@ const parseLinearGradient = (args: Array<string>, bounds: Bounds, hasPrefix: boo
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
direction,
|
||||
colorStops: absoluteValuedColorStops
|
||||
return 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 => {
|
||||
@ -146,9 +284,7 @@ const calculateGradientDirection = (radian: number, bounds: Bounds): Direction =
|
||||
};
|
||||
|
||||
const parseTopRight = (bounds: Bounds) =>
|
||||
Math.acos(
|
||||
bounds.width / 2 / (Math.sqrt(Math.pow(bounds.width, 2) + Math.pow(bounds.height, 2)) / 2)
|
||||
);
|
||||
Math.acos(bounds.width / 2 / (distance(bounds.width, bounds.height) / 2));
|
||||
|
||||
const parseSideOrCorner = (side: string, bounds: Bounds): Direction => {
|
||||
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);
|
||||
};
|
||||
|
||||
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 gradient’s 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 gradient’s 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}%`;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,10 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import NodeContainer from './NodeContainer';
|
||||
|
||||
const LENGTH_WITH_UNIT = /([\d.]+)(px|r?em|%)/i;
|
||||
|
||||
export const LENGTH_TYPE = {
|
||||
PX: 0,
|
||||
PERCENTAGE: 1
|
||||
@ -34,3 +38,31 @@ export default class Length {
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ import type {TextShadow} from './parsing/textShadow';
|
||||
import type {Matrix} from './parsing/transform';
|
||||
|
||||
import type {BoundCurves} from './Bounds';
|
||||
import type {Gradient} from './Gradient';
|
||||
import type {LinearGradient, RadialGradient} from './Gradient';
|
||||
import type {ResourceStore, ImageElement} from './ResourceLoader';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type StackingContext from './StackingContext';
|
||||
@ -22,7 +22,7 @@ import type {TextBounds} from './TextBounds';
|
||||
|
||||
import {Bounds, parsePathForBorder, calculateContentBox, calculatePaddingBoxPath} from './Bounds';
|
||||
import {FontMetrics} from './Font';
|
||||
import {parseGradient} from './Gradient';
|
||||
import {parseGradient, GRADIENT_TYPE} from './Gradient';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {
|
||||
@ -62,7 +62,9 @@ export interface RenderTarget<Output> {
|
||||
|
||||
render(options: RenderOptions): void,
|
||||
|
||||
renderLinearGradient(bounds: Bounds, gradient: Gradient): void,
|
||||
renderLinearGradient(bounds: Bounds, gradient: LinearGradient): void,
|
||||
|
||||
renderRadialGradient(bounds: Bounds, gradient: RadialGradient): void,
|
||||
|
||||
renderRepeat(
|
||||
path: Path,
|
||||
@ -265,9 +267,18 @@ export default class Renderer {
|
||||
backgroundImageSize.height
|
||||
);
|
||||
|
||||
const gradient = parseGradient(background.source, gradientBounds);
|
||||
const gradient = parseGradient(container, background.source, gradientBounds);
|
||||
if (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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
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 => {
|
||||
// Edge does not provide value for cssText
|
||||
for (let i = style.length - 1; i >= 0; i--) {
|
||||
|
@ -13,12 +13,23 @@ import type {Matrix} from '../parsing/transform';
|
||||
|
||||
import type {Bounds} from '../Bounds';
|
||||
import type {ImageElement} from '../ResourceLoader';
|
||||
import type {Gradient} from '../Gradient';
|
||||
import type {LinearGradient, RadialGradient} from '../Gradient';
|
||||
import type {TextBounds} from '../TextBounds';
|
||||
|
||||
import {PATH} from '../drawing/Path';
|
||||
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> {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
@ -131,7 +142,7 @@ export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
|
||||
this.ctx.fillRect(x, y, width, height);
|
||||
}
|
||||
|
||||
renderLinearGradient(bounds: Bounds, gradient: Gradient) {
|
||||
renderLinearGradient(bounds: Bounds, gradient: LinearGradient) {
|
||||
const linearGradient = this.ctx.createLinearGradient(
|
||||
bounds.left + gradient.direction.x1,
|
||||
bounds.top + gradient.direction.y1,
|
||||
@ -139,14 +150,43 @@ export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
|
||||
bounds.top + gradient.direction.y0
|
||||
);
|
||||
|
||||
gradient.colorStops.forEach(colorStop => {
|
||||
linearGradient.addColorStop(colorStop.stop, colorStop.color.toString());
|
||||
});
|
||||
|
||||
addColorStops(gradient, linearGradient);
|
||||
this.ctx.fillStyle = linearGradient;
|
||||
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(
|
||||
path: Path,
|
||||
image: ImageElement,
|
||||
|
@ -18,7 +18,7 @@ import type {Matrix} from '../parsing/transform';
|
||||
|
||||
import type {Bounds} from '../Bounds';
|
||||
import type {ImageElement} from '../ResourceLoader';
|
||||
import type {Gradient} from '../Gradient';
|
||||
import type {LinearGradient, RadialGradient} from '../Gradient';
|
||||
import type {TextBounds} from '../TextBounds';
|
||||
|
||||
import {TEXT_DECORATION_STYLE, TEXT_DECORATION_LINE} from '../parsing/textDecoration';
|
||||
@ -110,7 +110,7 @@ class RefTestRenderer implements RenderTarget<string> {
|
||||
return `Path (${string})`;
|
||||
}
|
||||
|
||||
renderLinearGradient(bounds: Bounds, gradient: Gradient) {
|
||||
renderLinearGradient(bounds: Bounds, gradient: LinearGradient) {
|
||||
const direction = [
|
||||
`x0: ${Math.round(gradient.direction.x0)}`,
|
||||
`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(
|
||||
path: Path,
|
||||
image: ImageElement,
|
||||
|
151
tests/node/gradient.js
Normal file
151
tests/node/gradient.js
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
51
tests/reftests/background/radial-gradient2.html
Normal file
51
tests/reftests/background/radial-gradient2.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user