Skip to content

Commit

Permalink
fix: correctly render gradients with clip & repeat (fix #1773)
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasvh committed May 6, 2019
1 parent d7656b0 commit 3c50e53
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 168 deletions.
48 changes: 24 additions & 24 deletions src/Bounds.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,32 +115,32 @@ export const parsePathForBorder = (curves: BoundCurves, borderSide: BorderSide):
switch (borderSide) {
case TOP:
return createPathFromCurves(
curves.topLeftOuter,
curves.topLeftInner,
curves.topRightOuter,
curves.topRightInner
curves.topLeftBorderBox,
curves.topLeftPaddingBox,
curves.topRightBorderBox,
curves.topRightPaddingBox
);
case RIGHT:
return createPathFromCurves(
curves.topRightOuter,
curves.topRightInner,
curves.bottomRightOuter,
curves.bottomRightInner
curves.topRightBorderBox,
curves.topRightPaddingBox,
curves.bottomRightBorderBox,
curves.bottomRightPaddingBox
);
case BOTTOM:
return createPathFromCurves(
curves.bottomRightOuter,
curves.bottomRightInner,
curves.bottomLeftOuter,
curves.bottomLeftInner
curves.bottomRightBorderBox,
curves.bottomRightPaddingBox,
curves.bottomLeftBorderBox,
curves.bottomLeftPaddingBox
);
case LEFT:
default:
return createPathFromCurves(
curves.bottomLeftOuter,
curves.bottomLeftInner,
curves.topLeftOuter,
curves.topLeftInner
curves.bottomLeftBorderBox,
curves.bottomLeftPaddingBox,
curves.topLeftBorderBox,
curves.topLeftPaddingBox
);
}
};
Expand Down Expand Up @@ -181,19 +181,19 @@ const createPathFromCurves = (

export const calculateBorderBoxPath = (curves: BoundCurves): Path => {
return [
curves.topLeftOuter,
curves.topRightOuter,
curves.bottomRightOuter,
curves.bottomLeftOuter
curves.topLeftBorderBox,
curves.topRightBorderBox,
curves.bottomRightBorderBox,
curves.bottomLeftBorderBox
];
};

export const calculatePaddingBoxPath = (curves: BoundCurves): Path => {
return [
curves.topLeftInner,
curves.topRightInner,
curves.bottomRightInner,
curves.bottomLeftInner
curves.topLeftPaddingBox,
curves.topRightPaddingBox,
curves.bottomRightPaddingBox,
curves.bottomLeftPaddingBox
];
};

Expand Down
96 changes: 43 additions & 53 deletions src/core/canvas-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,18 @@ import {CSSParsedDeclaration} from '../css/index';
import {TextContainer} from '../dom/text-container';
import {Path} from '../render/path';
import {BACKGROUND_CLIP} from '../css/property-descriptors/background-clip';
import {BoundCurves, calculateBorderBoxPath, calculatePaddingBoxPath} from '../render/bound-curves';
import {
BoundCurves,
calculateBorderBoxPath,
calculateContentBoxPath,
calculatePaddingBoxPath
} from '../render/bound-curves';
import {isBezierCurve} from '../render/bezier-curve';
import {Vector} from '../render/vector';
import {CSSImageType, CSSURLImage} from '../css/types/image';
import {CSSImageType, CSSURLImage, isLinearGradient} from '../css/types/image';
import {parsePathForBorder} from '../render/border';
import {Cache} from './cache-storage';
import {
calculateBackgroundRepeatPath,
calculateBackgroundSize,
calculateBackgroungPositioningArea,
getBackgroundValueForIndex
} from '../render/background';
import {getAbsoluteValueForTuple} from '../css/types/length-percentage';
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../render/background';
import {isDimensionToken} from '../css/syntax/parser';
import {TextBounds} from '../css/layout/text';
import {fromCodePoint, toCodePoints} from 'css-line-break';
Expand All @@ -30,6 +29,7 @@ import {SVGElementContainer} from '../dom/replaced-elements/svg-element-containe
import {ReplacedElementContainer} from '../dom/replaced-elements/index';
import {EffectTarget, IElementEffect, isClipEffect, isTransformEffect} from '../render/effects';
import {contains} from './bitwise';
import {calculateGradientDirection, processColorStops} from '../css/types/functions/gradient';

export interface RenderOptions {
scale: number;
Expand Down Expand Up @@ -395,19 +395,9 @@ export class CanvasRenderer {
this.ctx.closePath();
}

renderRepeat(
path: Path[],
image: HTMLImageElement,
imageWidth: number,
imageHeight: number,
offsetX: number,
offsetY: number
) {
renderRepeat(path: Path[], pattern: CanvasPattern | CanvasGradient, offsetX: number, offsetY: number) {
this.path(path);
this.ctx.fillStyle = this.ctx.createPattern(
this.resizeImage(image, imageWidth, imageHeight),
'repeat'
) as CanvasPattern;
this.ctx.fillStyle = pattern;
this.ctx.translate(offsetX, offsetY);
this.ctx.fill();
this.ctx.translate(-offsetX, -offsetY);
Expand Down Expand Up @@ -438,38 +428,36 @@ export class CanvasRenderer {
Logger.error(`Error loading background-image ${url}`);
}

if (!image) {
return;
if (image) {
const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [
image.width,
image.height,
image.width / image.height
]);
const pattern = this.ctx.createPattern(
this.resizeImage(image, width, height),
'repeat'
) as CanvasPattern;
this.renderRepeat(path, pattern, x, y);
}

const backgroundPositioningArea = calculateBackgroungPositioningArea(
getBackgroundValueForIndex(container.styles.backgroundOrigin, index),
container
);
const backgroundImageSize = calculateBackgroundSize(
getBackgroundValueForIndex(container.styles.backgroundSize, index),
image,
backgroundPositioningArea
} else if (isLinearGradient(backgroundImage)) {
const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);

const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);

processColorStops(backgroundImage.stops, lineLength).forEach(colorStop =>
gradient.addColorStop(colorStop.stop, asString(colorStop.color))
);
const [sizeWidth, sizeHeight] = backgroundImageSize;

const position = getAbsoluteValueForTuple(
getBackgroundValueForIndex(container.styles.backgroundPosition, index),
backgroundPositioningArea.width - sizeWidth,
backgroundPositioningArea.height - sizeHeight
);

const path = calculateBackgroundRepeatPath(
getBackgroundValueForIndex(container.styles.backgroundRepeat, index),
position,
backgroundImageSize,
backgroundPositioningArea,
container.bounds
);

const offsetX = Math.round(backgroundPositioningArea.left + position[0]);
const offsetY = Math.round(backgroundPositioningArea.top + position[1]);
this.renderRepeat(path, image, sizeWidth, sizeHeight, offsetX, offsetY);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
const pattern = this.ctx.createPattern(canvas, 'repeat') as CanvasPattern;
this.renderRepeat(path, pattern, x, y);
}
index--;
}
Expand All @@ -493,14 +481,14 @@ export class CanvasRenderer {
{style: styles.borderLeftStyle, color: styles.borderLeftColor}
];

const backgroundPaintingArea = calculateBackgroungPaintingArea(
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(
getBackgroundValueForIndex(styles.backgroundClip, 0),
paint.curves
);

if (hasBackground) {
this.ctx.save();
this.path(backgroundPaintingArea);

this.ctx.clip();

if (!isTransparent(styles.backgroundColor)) {
Expand Down Expand Up @@ -535,10 +523,12 @@ export class CanvasRenderer {
}
}

const calculateBackgroungPaintingArea = (clip: BACKGROUND_CLIP, curves: BoundCurves): Path[] => {
const calculateBackgroundCurvedPaintingArea = (clip: BACKGROUND_CLIP, curves: BoundCurves): Path[] => {
switch (clip) {
case BACKGROUND_CLIP.BORDER_BOX:
return calculateBorderBoxPath(curves);
case BACKGROUND_CLIP.CONTENT_BOX:
return calculateContentBoxPath(curves);
case BACKGROUND_CLIP.PADDING_BOX:
default:
return calculatePaddingBoxPath(curves);
Expand Down
2 changes: 1 addition & 1 deletion src/css/types/functions/-prefix-linear-gradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const prefixLinearGradient = (tokens: CSSValue[]): CSSLinearGradientImage
angle = parseNamedSide(arg);
return;
} else if (isAngle(firstToken)) {
angle = angleType.parse(firstToken);
angle = angleType.parse(firstToken) + deg(90);
return;
}
}
Expand Down
71 changes: 69 additions & 2 deletions src/css/types/functions/gradient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,77 @@
import {CSSValue} from '../../syntax/parser';
import {UnprocessedGradientColorStop} from '../image';
import {GradientColorStop, UnprocessedGradientColorStop} from '../image';
import {color as colorType} from '../color';
import {isLengthPercentage} from '../length-percentage';
import {getAbsoluteValue, HUNDRED_PERCENT, isLengthPercentage, ZERO_LENGTH} from '../length-percentage';

export const parseColorStop = (args: CSSValue[]): UnprocessedGradientColorStop => {
const color = colorType.parse(args[0]);
const stop = args[1];
return stop && isLengthPercentage(stop) ? {color, stop} : {color, stop: null};
};

export const processColorStops = (stops: UnprocessedGradientColorStop[], lineLength: number): GradientColorStop[] => {
const first = stops[0];
const last = stops[stops.length - 1];
if (first.stop === null) {
first.stop = ZERO_LENGTH;
}

if (last.stop === null) {
last.stop = HUNDRED_PERCENT;
}

const processStops: (number | null)[] = [];
let previous = 0;
for (let i = 0; i < stops.length; i++) {
const stop = stops[i].stop;
if (stop !== null) {
const absoluteValue = getAbsoluteValue(stop, lineLength);
if (absoluteValue > previous) {
processStops.push(absoluteValue);
} else {
processStops.push(previous);
}
previous = absoluteValue;
} else {
processStops.push(null);
}
}

let gapBegin = null;
for (let i = 0; i < processStops.length; i++) {
const stop = processStops[i];
if (stop === null) {
if (gapBegin === null) {
gapBegin = i;
}
} else if (gapBegin !== null) {
const gapLength = i - gapBegin;
const beforeGap = processStops[gapBegin - 1] as number;
const gapValue = (stop - beforeGap) / (gapLength + 1);
for (let g = 1; g <= gapLength; g++) {
processStops[gapBegin + g - 1] = gapValue * g;
}
gapBegin = null;
}
}

return stops.map(({color}, i) => {
return {color, stop: Math.max(Math.min(1, (processStops[i] as number) / lineLength), 0)};
});
};

export const calculateGradientDirection = (
radian: number,
width: number,
height: number
): [number, number, number, number, number] => {
const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));

const halfWidth = width / 2;
const halfHeight = height / 2;

const yDiff = (Math.sin(radian - Math.PI / 2) * lineLength) / 2;
const xDiff = (Math.cos(radian - Math.PI / 2) * lineLength) / 2;

return [lineLength, halfWidth - xDiff, halfWidth + xDiff, halfHeight - yDiff, halfHeight + yDiff];
};
2 changes: 1 addition & 1 deletion src/css/types/functions/linear-gradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ export const linearGradient = (tokens: CSSValue[]): CSSLinearGradientImage => {
stops.push(colorStop);
});

return {angle, stops, type: CSSImageType.LINEAR_GRADIENT};
return {angle: angle, stops, type: CSSImageType.LINEAR_GRADIENT};
};
14 changes: 9 additions & 5 deletions src/css/types/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ export enum CSSImageType {
LINEAR_GRADIENT
}

export const isLinearGradient = (background: ICSSImage): background is CSSLinearGradientImage => {
return background.type === CSSImageType.LINEAR_GRADIENT;
};

export interface UnprocessedGradientColorStop {
color: Color;
stop: LengthPercentage | null;
}
/*
type GradientColorStop = {
color: Color
stop: LengthPercentage

export interface GradientColorStop {
color: Color;
stop: number;
}
*/

export interface ICSSImage {
type: CSSImageType;
}
Expand Down
19 changes: 18 additions & 1 deletion src/css/types/length-percentage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {DimensionToken, FLAG_INTEGER, NumberValueToken, TokenType} from '../syntax/tokenizer';
import {CSSValue} from '../syntax/parser';
import {CSSValue, isDimensionToken} from '../syntax/parser';
import {isLength} from './length';
export type LengthPercentage = DimensionToken | NumberValueToken;
export type LengthPercentageTuple = [LengthPercentage] | [LengthPercentage, LengthPercentage];
Expand All @@ -13,6 +13,13 @@ export const ZERO_LENGTH: NumberValueToken = {
number: 0,
flags: FLAG_INTEGER
};

export const HUNDRED_PERCENT: NumberValueToken = {
type: TokenType.PERCENTAGE_TOKEN,
number: 100,
flags: FLAG_INTEGER
};

export const getAbsoluteValueForTuple = (
tuple: LengthPercentageTuple,
width: number,
Expand All @@ -26,5 +33,15 @@ export const getAbsoluteValue = (token: LengthPercentage, parent: number) => {
return (token.number / 100) * parent;
}

if (isDimensionToken(token)) {
switch (token.unit) {
case 'em':
return 16 * token.number; // TODO use correct font-size
case 'px':
default:
return token.number;
}
}

return token.number;
};
Loading

0 comments on commit 3c50e53

Please sign in to comment.