Skip to content

Commit

Permalink
feat: support advanced features in radial-gradient #1165
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed Sep 28, 2022
1 parent a33b350 commit eb45a78
Show file tree
Hide file tree
Showing 20 changed files with 1,285 additions and 336 deletions.
20 changes: 13 additions & 7 deletions packages/g-lite/src/css/cssom/CSSGradientValue.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { CSSKeywordValue } from './CSSKeywordValue';
import type { CSSUnitValue } from './CSSNumericValue';
import { CSSStyleValue, CSSStyleValueType } from './CSSStyleValue';
import type { Nested, ParenLess } from './types';

export interface LinearColorStop {
offset: CSSUnitValue;
color: string; // use user-defined value instead of parsed CSSRGB
}

export interface LinearGradient {
angle: number;
steps: [number, string][];
hash: string;
angle: CSSUnitValue;
steps: LinearColorStop[];
}

export interface RadialGradient {
cx: number;
cy: number;
steps: [number, string][];
hash: string;
cx: CSSUnitValue;
cy: CSSUnitValue;
size?: CSSUnitValue | CSSKeywordValue;
steps: LinearColorStop[];
}

export enum GradientType {
Expand Down
287 changes: 173 additions & 114 deletions packages/g-lite/src/css/parser/__tests__/color.spec.ts

Large diffs are not rendered by default.

149 changes: 88 additions & 61 deletions packages/g-lite/src/css/parser/gradient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { isNil, isString, memoize } from '@antv/util';
import type { AngularNode, ColorStop, DirectionalNode, PositionNode } from '../../utils';
import { colorStopToString, parseGradient as parse } from '../../utils';
import type { RadialGradient } from '../cssom';
import { CSSKeywordValue, CSSUnitValue, LinearColorStop, Odeg, RadialGradient } from '../cssom';
import { CSSGradientValue, GradientType } from '../cssom';
import type { Pattern } from './color';
import { getOrCreateKeyword, getOrCreateUnitValue } from '../CSSStyleValuePool';
import { Pattern } from './color';

const regexLG = /^l\s*\(\s*([\d.]+)\s*\)\s*(.*)/i;
const regexRG = /^r\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)\s*(.*)/i;
Expand All @@ -26,6 +27,7 @@ function spaceColorStops(colorStops: ColorStop[]) {
let previousIndex = 0;
let previousOffset = Number(colorStops[0].length.value);
for (let i = 1; i < length; i++) {
// support '%' & 'px'
const offset = colorStops[i].length?.value;
if (!isNil(offset) && !isNil(previousOffset)) {
for (let j = 1; j < i - previousIndex; j++)
Expand Down Expand Up @@ -58,74 +60,83 @@ const SideOrCornerToDegMap: Record<DirectionalNode['value'], number> = {
'bottom right': 135 - 90,
};

const angleToDeg = memoize((orientation: DirectionalNode | AngularNode) => {
let angle: number;
if (orientation.type === 'angular') {
angle = Number(orientation.value);
} else {
angle = SideOrCornerToDegMap[orientation.value] || 0;
}
return angle;
});
const angleToDeg: (orientation: DirectionalNode | AngularNode) => CSSUnitValue = memoize(
(orientation: DirectionalNode | AngularNode) => {
let angle: number;
if (orientation.type === 'angular') {
angle = Number(orientation.value);
} else {
angle = SideOrCornerToDegMap[orientation.value] || 0;
}
return getOrCreateUnitValue(angle, 'deg');
},
);

const positonToPercentage = memoize((position: PositionNode) => {
let cx = 0.5;
let cy = 0.5;
if (position?.type === 'position') {
const { x, y } = position.value;
if (x?.type === 'position-keyword') {
if (x.value === 'left') {
cx = 0;
} else if (x.value === 'center') {
cx = 0.5;
} else if (x.value === 'right') {
cx = 1;
} else if (x.value === 'top') {
cy = 0;
} else if (x.value === 'bottom') {
cy = 1;
const positonToCSSUnitValue: (position: PositionNode) => { cx: CSSUnitValue; cy: CSSUnitValue } =
memoize((position: PositionNode) => {
let cx = 50;
let cy = 50;
let unitX = '%';
let unitY = '%';
if (position?.type === 'position') {
const { x, y } = position.value;
if (x?.type === 'position-keyword') {
if (x.value === 'left') {
cx = 0;
} else if (x.value === 'center') {
cx = 50;
} else if (x.value === 'right') {
cx = 100;
} else if (x.value === 'top') {
cy = 0;
} else if (x.value === 'bottom') {
cy = 100;
}
}
}

if (y?.type === 'position-keyword') {
if (y.value === 'left') {
cx = 0;
} else if (y.value === 'center') {
cy = 0.5;
} else if (y.value === 'right') {
cx = 1;
} else if (y.value === 'top') {
cy = 0;
} else if (y.value === 'bottom') {
cy = 1;
if (y?.type === 'position-keyword') {
if (y.value === 'left') {
cx = 0;
} else if (y.value === 'center') {
cy = 50;
} else if (y.value === 'right') {
cx = 100;
} else if (y.value === 'top') {
cy = 0;
} else if (y.value === 'bottom') {
cy = 100;
}
}
}

if (x?.type === '%') {
cx = Number(x.value) / 100;
}
if (y?.type === '%') {
cy = Number(x.value) / 100;
if (x?.type === 'px' || x?.type === '%' || x?.type === 'em') {
unitX = x?.type;
cx = Number(x.value);
}
if (y?.type === 'px' || y?.type === '%' || y?.type === 'em') {
unitY = y?.type;
cy = Number(y.value);
}
}
}

return { cx, cy };
});
return { cx: getOrCreateUnitValue(cx, unitX), cy: getOrCreateUnitValue(cy, unitY) };
});

export const parseGradient = memoize((colorStr: string) => {
if (colorStr.indexOf('linear') > -1 || colorStr.indexOf('radial') > -1) {
const ast = parse(colorStr);
return ast.map(({ type, orientation, colorStops }) => {
spaceColorStops(colorStops);
const steps = colorStops.map<[number, string]>((colorStop) => {
const steps = colorStops.map<LinearColorStop>((colorStop) => {
// TODO: only support % for now, should calc percentage of axis length when using px/em
return [Number(colorStop.length.value) / 100, colorStopToString(colorStop)];
return {
offset: getOrCreateUnitValue(colorStop.length.value, '%'),
color: colorStopToString(colorStop),
};
});
if (type === 'linear-gradient') {
return new CSSGradientValue(GradientType.LinearGradient, {
angle: orientation ? angleToDeg(orientation as DirectionalNode | AngularNode) : 0,
angle: orientation ? angleToDeg(orientation as DirectionalNode | AngularNode) : Odeg,
steps,
hash: colorStr,
});
} else if (type === 'radial-gradient') {
if (!orientation) {
Expand All @@ -137,14 +148,26 @@ export const parseGradient = memoize((colorStr: string) => {
];
}
if (orientation[0].type === 'shape' && orientation[0].value === 'circle') {
const { cx, cy } = positonToPercentage(orientation[0].at);
const { cx, cy } = positonToCSSUnitValue(orientation[0].at);
let size: CSSUnitValue | CSSKeywordValue;
if (orientation[0].style) {
const { type, value } = orientation[0].style;

if (type === 'extent-keyword') {
size = getOrCreateKeyword(value);
} else {
size = getOrCreateUnitValue(value, type);
}
}
return new CSSGradientValue(GradientType.RadialGradient, {
cx,
cy,
size,
steps,
hash: colorStr,
});
}
// TODO: support ellipse shape
// TODO: repeating-linear-gradient & repeating-radial-gradient
// } else if (type === 'repeating-linear-gradient') {
// } else if (type === 'repeating-radial-gradient') {
}
Expand All @@ -160,9 +183,11 @@ export const parseGradient = memoize((colorStr: string) => {
const steps = arr[2].match(regexColorStop)?.map((stop) => stop.split(':')) || [];
return [
new CSSGradientValue(GradientType.LinearGradient, {
angle: parseFloat(arr[1]),
steps: steps.map(([offset, color]) => [Number(offset), color]),
hash: colorStr,
angle: getOrCreateUnitValue(parseFloat(arr[1]), 'deg'),
steps: steps.map(([offset, color]) => ({
offset: getOrCreateUnitValue(Number(offset) * 100, '%'),
color,
})),
}),
];
}
Expand All @@ -186,10 +211,12 @@ function parseRadialGradient(gradientStr: string): RadialGradient | string | nul
if (arr) {
const steps = arr[4].match(regexColorStop)?.map((stop) => stop.split(':')) || [];
return {
cx: 0.5,
cy: 0.5,
steps: steps.map(([offset, color]) => [Number(offset), color]),
hash: gradientStr,
cx: getOrCreateUnitValue(50, '%'),
cy: getOrCreateUnitValue(50, '%'),
steps: steps.map(([offset, color]) => ({
offset: getOrCreateUnitValue(Number(offset) * 100, '%'),
color,
})),
};
}
return null;
Expand Down
59 changes: 54 additions & 5 deletions packages/g-lite/src/utils/gradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
* @see https://github.com/rafaelcaricio/gradient-parser
*/

import { distanceSquareRoot } from '@antv/util';
import { CSSKeywordValue, CSSUnitValue, UnitType } from '../css';
import { deg2rad } from './math';

export interface LinearGradientNode {
type: 'linear-gradient';
orientation?: DirectionalNode | AngularNode | undefined;
Expand Down Expand Up @@ -479,8 +483,8 @@ export const parseGradient = (function () {
};
})();

export function computeLinearGradient(width: number, height: number, angle: number) {
const rad = (angle * Math.PI) / 180;
export function computeLinearGradient(width: number, height: number, angle: CSSUnitValue) {
const rad = deg2rad(angle.value);
const rx = 0;
const ry = 0;
const rcx = rx + width / 2;
Expand All @@ -496,8 +500,53 @@ export function computeLinearGradient(width: number, height: number, angle: numb
return { x1, y1, x2, y2 };
}

export function computeRadialGradient(width: number, height: number, cx: number, cy: number) {
const r = Math.sqrt(width * width + height * height) / 2;
export function computeRadialGradient(
width: number,
height: number,
cx: CSSUnitValue,
cy: CSSUnitValue,
size?: CSSUnitValue | CSSKeywordValue,
) {
// 'px'
let x = cx.value;
let y = cy.value;

// TODO: 'em'

// '%'
if (cx.unit === UnitType.kPercentage) {
x = (cx.value / 100) * width;
}
if (cy.unit === UnitType.kPercentage) {
y = (cy.value / 100) * height;
}

// default to farthest-side
let r = Math.max(
distanceSquareRoot([0, 0], [x, y]),
distanceSquareRoot([0, height], [x, y]),
distanceSquareRoot([width, height], [x, y]),
distanceSquareRoot([width, 0], [x, y]),
);
if (size) {
if (size instanceof CSSUnitValue) {
r = size.value;
} else if (size instanceof CSSKeywordValue) {
// @see https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Images/Using_CSS_gradients#example_closest-side_for_circles
if (size.value === 'closest-side') {
r = Math.min(x, width - x, y, height - y);
} else if (size.value === 'farthest-side') {
r = Math.max(x, width - x, y, height - y);
} else if (size.value === 'closest-corner') {
r = Math.min(
distanceSquareRoot([0, 0], [x, y]),
distanceSquareRoot([0, height], [x, y]),
distanceSquareRoot([width, height], [x, y]),
distanceSquareRoot([width, 0], [x, y]),
);
}
}
}

return { x: cx * width, y: cy * height, r };
return { x, y, r };
}
2 changes: 2 additions & 0 deletions packages/g-lottie-player/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

Lottie Docs: https://lottiefiles.github.io/lottie-docs/

Tips for rendering: https://lottiefiles.github.io/lottie-docs/rendering/

Inspired by https://github.com/pissang/lottie-parser
Loading

0 comments on commit eb45a78

Please sign in to comment.