Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: box-shadow rendering #1848

Merged
merged 2 commits into from
May 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {content} from './property-descriptors/content';
import {counterIncrement} from './property-descriptors/counter-increment';
import {counterReset} from './property-descriptors/counter-reset';
import {quotes} from './property-descriptors/quotes';
import {boxShadow} from './property-descriptors/box-shadow';

export class CSSParsedDeclaration {
backgroundClip: ReturnType<typeof backgroundClip.parse>;
Expand All @@ -97,6 +98,7 @@ export class CSSParsedDeclaration {
borderRightWidth: ReturnType<typeof borderRightWidth.parse>;
borderBottomWidth: ReturnType<typeof borderBottomWidth.parse>;
borderLeftWidth: ReturnType<typeof borderLeftWidth.parse>;
boxShadow: ReturnType<typeof boxShadow.parse>;
color: Color;
display: ReturnType<typeof display.parse>;
float: ReturnType<typeof float.parse>;
Expand Down Expand Up @@ -158,6 +160,7 @@ export class CSSParsedDeclaration {
this.borderRightWidth = parse(borderRightWidth, declaration.borderRightWidth);
this.borderBottomWidth = parse(borderBottomWidth, declaration.borderBottomWidth);
this.borderLeftWidth = parse(borderLeftWidth, declaration.borderLeftWidth);
this.boxShadow = parse(boxShadow, declaration.boxShadow);
this.color = parse(color, declaration.color);
this.display = parse(display, declaration.display);
this.float = parse(float, declaration.cssFloat);
Expand Down
59 changes: 59 additions & 0 deletions src/css/property-descriptors/box-shadow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
import {CSSValue, isIdentWithValue, parseFunctionArgs} from '../syntax/parser';
import {ZERO_LENGTH} from '../types/length-percentage';
import {color, Color} from '../types/color';
import {isLength, Length} from '../types/length';

export type BoxShadow = BoxShadowItem[];
interface BoxShadowItem {
inset: boolean;
color: Color;
offsetX: Length;
offsetY: Length;
blur: Length;
spread: Length;
}

export const boxShadow: IPropertyListDescriptor<BoxShadow> = {
name: 'box-shadow',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]): BoxShadow => {
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
return [];
}

return parseFunctionArgs(tokens).map((values: CSSValue[]) => {
const shadow: BoxShadowItem = {
color: 0x000000ff,
offsetX: ZERO_LENGTH,
offsetY: ZERO_LENGTH,
blur: ZERO_LENGTH,
spread: ZERO_LENGTH,
inset: false
};
let c = 0;
for (let i = 0; i < values.length; i++) {
const token = values[i];
if (isIdentWithValue(token, 'inset')) {
shadow.inset = true;
} else if (isLength(token)) {
if (c === 0) {
shadow.offsetX = token;
} else if (c === 1) {
shadow.offsetY = token;
} else if (c === 2) {
shadow.blur = token;
} else {
shadow.spread = token;
}
c++;
} else {
shadow.color = color.parse(token);
}
}
return shadow;
});
}
};
9 changes: 9 additions & 0 deletions src/render/bezier-curve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export class BezierCurve implements IPath {
return firstHalf ? new BezierCurve(this.start, ab, abbc, dest) : new BezierCurve(dest, bccd, cd, this.end);
}

add(deltaX: number, deltaY: number): BezierCurve {
return new BezierCurve(
this.start.add(deltaX, deltaY),
this.startControl.add(deltaX, deltaY),
this.endControl.add(deltaX, deltaY),
this.end.add(deltaX, deltaY)
);
}

reverse(): BezierCurve {
return new BezierCurve(this.end, this.endControl, this.startControl, this.start);
}
Expand Down
58 changes: 54 additions & 4 deletions src/render/canvas/canvas-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {ElementContainer} from '../../dom/element-container';
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
import {CSSParsedDeclaration} from '../../css/index';
import {TextContainer} from '../../dom/text-container';
import {Path} from '../path';
import {Path, transformPath} from '../path';
import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
import {BoundCurves, calculateBorderBoxPath, calculateContentBoxPath, calculatePaddingBoxPath} from '../bound-curves';
import {isBezierCurve} from '../bezier-curve';
Expand Down Expand Up @@ -55,6 +55,8 @@ export interface RenderOptions {
cache: Cache;
}

const MASK_OFFSET = 10000;

export class CanvasRenderer {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
Expand Down Expand Up @@ -471,9 +473,24 @@ export class CanvasRenderer {
}
}

mask(paths: Path[]) {
this.ctx.beginPath();
this.ctx.moveTo(0, 0);
this.ctx.lineTo(this.canvas.width, 0);
this.ctx.lineTo(this.canvas.width, this.canvas.height);
this.ctx.lineTo(0, this.canvas.height);
this.ctx.lineTo(0, 0);
this.formatPath(paths.slice(0).reverse());
this.ctx.closePath();
}

path(paths: Path[]) {
this.ctx.beginPath();
this.formatPath(paths);
this.ctx.closePath();
}

formatPath(paths: Path[]) {
paths.forEach((point, index) => {
const start: Vector = isBezierCurve(point) ? point.start : point;
if (index === 0) {
Expand All @@ -493,8 +510,6 @@ export class CanvasRenderer {
);
}
});

this.ctx.closePath();
}

renderRepeat(path: Path[], pattern: CanvasPattern | CanvasGradient, offsetX: number, offsetY: number) {
Expand Down Expand Up @@ -626,7 +641,7 @@ export class CanvasRenderer {
paint.curves
);

if (hasBackground) {
if (hasBackground || styles.boxShadow.length) {
this.ctx.save();
this.path(backgroundPaintingArea);
this.ctx.clip();
Expand All @@ -639,6 +654,41 @@ export class CanvasRenderer {
await this.renderBackgroundImage(paint.container);

this.ctx.restore();

styles.boxShadow
.slice(0)
.reverse()
.forEach(shadow => {
this.ctx.save();
const borderBoxArea = calculateBorderBoxPath(paint.curves);
const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
const shadowPaintingArea = transformPath(
borderBoxArea,
-maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number,
(shadow.inset ? 1 : -1) * shadow.spread.number,
shadow.spread.number * (shadow.inset ? -2 : 2),
shadow.spread.number * (shadow.inset ? -2 : 2)
);

if (shadow.inset) {
this.path(borderBoxArea);
this.ctx.clip();
this.mask(shadowPaintingArea);
} else {
this.mask(borderBoxArea);
this.ctx.clip();
this.path(shadowPaintingArea);
}

this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
this.ctx.shadowOffsetY = shadow.offsetY.number;
this.ctx.shadowColor = asString(shadow.color);
this.ctx.shadowBlur = shadow.blur.number;
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';

this.ctx.fill();
this.ctx.restore();
});
}

let side = 0;
Expand Down
17 changes: 17 additions & 0 deletions src/render/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum PathType {

export interface IPath {
type: PathType;
add(deltaX: number, deltaY: number): IPath;
}

export const equalPath = (a: Path[], b: Path[]): boolean => {
Expand All @@ -17,4 +18,20 @@ export const equalPath = (a: Path[], b: Path[]): boolean => {
return false;
};

export const transformPath = (path: Path[], deltaX: number, deltaY: number, deltaW: number, deltaH: number): Path[] => {
return path.map((point, index) => {
switch (index) {
case 0:
return point.add(deltaX, deltaY);
case 1:
return point.add(deltaX + deltaW, deltaY);
case 2:
return point.add(deltaX + deltaW, deltaY + deltaH);
case 3:
return point.add(deltaX, deltaY + deltaH);
}
return point;
});
};

export type Path = Vector | BezierCurve;
4 changes: 4 additions & 0 deletions src/render/vector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export class Vector implements IPath {
this.x = x;
this.y = y;
}

add(deltaX: number, deltaY: number): Vector {
return new Vector(this.x + deltaX, this.y + deltaY);
}
}

export const isVector = (path: Path): path is Vector => path.type === PathType.VECTOR;
118 changes: 118 additions & 0 deletions tests/reftests/background/box-shadow.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>box-shadow tests</title>
<script type="text/javascript" src="../../test.js"></script>
<style>
.boxshadow {
margin: 10px;
display: inline-block;
min-height: 50px;
// border-radius: 10px;
}

body {
max-width: 800px;
}
</style>
</head>
<body>
<div style="box-shadow: 0 0 0 0.5em;" class="boxshadow">0 0 0 0.5em;</div>
<div style="box-shadow: 0 0 1em;" class="boxshadow">0 0 1em;</div>
<div style="box-shadow: 1em 0.5em;" class="boxshadow">1em 0.5em;</div>
<div style="box-shadow: 1em 0.5em 1em;" class="boxshadow">1em 0.5em 1em;</div>
<div style="box-shadow: 0 2em 1em -0.7em;" class="boxshadow">0 2em 1em -0.7em;</div>
<div style="box-shadow: 0.3em 0.3em lightgreen;" class="boxshadow">0.3em 0.3em lightgreen;</div>
<div style="box-shadow: 0.3em 0.3em 0 0.6em lightgreen;" class="boxshadow">0.3em 0.3em 0 0.6em lightgreen;</div>
<div style="box-shadow: 0 2em 0 -0.9em lightgreen;" class="boxshadow">0 2em 0 -0.9em lightgreen;</div>
<div style="box-shadow: 2em 1.5em 0 -0.7em lightgreen;" class="boxshadow">2em 1.5em 0 -0.7em lightgreen;</div>
<div style="box-shadow: 9em 1.2em 0 -0.6em lightgreen;" class="boxshadow">9em 1.2em 0 -0.6em lightgreen;</div>
<div style="box-shadow: -27.3em 0 lightgreen;" class="boxshadow">-27.3em 0 lightgreen;</div>
<div style="box-shadow: 0 2em 0 -1em lightgreen;" class="boxshadow">0 2em 0 -1em lightgreen;</div>
<div style="box-shadow: 0 0 1em maroon;" class="boxshadow">0 0 1em maroon;</div>
<div style="box-shadow: 0 0 1em 0.5em maroon;" class="boxshadow">0 0 1em 0.5em maroon;</div>
<div style="margin-top:3em;box-shadow: 0 0 1em 1em maroon;" class="boxshadow">0 0 1em 1em maroon;</div>
<div style="box-shadow: -0.4em -0.4em 1em olive;" class="boxshadow">-0.4em -0.4em 1em olive;</div>
<div style="box-shadow: 0.4em 0.4em 1em olive;" class="boxshadow">0.4em 0.4em 1em olive;</div>
<div style="box-shadow: 0.4em 0.4em 1em -0.2em olive;" class="boxshadow">0.4em 0.4em 1em -0.2em olive;</div>
<div style="box-shadow: 0.4em 0.4em 1em 0.4em olive;" class="boxshadow">0.4em 0.4em 1em 0.4em olive;</div>
<div style="box-shadow: 0 1.5em 0.5em -1em olive;" class="boxshadow">0 1.5em 0.5em -1em olive;</div>
<div style="box-shadow: inset 0.2em 0.4em red, inset -1em -0.7em red;" class="boxshadow">inset 0.2em 0.4em red, inset
-1em -0.7em red;
</div>
<div style="box-shadow: inset 11em 0 red;" class="boxshadow">inset 11em 0 red;</div>
<div style="box-shadow: inset -1em 0 red;" class="boxshadow">inset -1em 0 red;</div>
<div style="box-shadow: inset 13em 0 3em -3em orange, inset -3em 0 3em -3em blue;" class="boxshadow">inset 13em 0 3em
-3em orange,inset -3em 0 3em -3em blue;
</div>
<div style="box-shadow: inset 11em 0 2em orange;" class="boxshadow">inset 11em 0 2em orange;</div>
<div style="box-shadow: inset 0 0.3em red;" class="boxshadow">inset 0 0.3em red;</div>
<div style="box-shadow: inset 0 -1.1em red;" class="boxshadow">inset 0 -1.1em red;</div>
<div style="box-shadow: inset 1em 0 1em -1em blue;" class="boxshadow">inset 1em 0 1em -1em blue;</div>
<div style="box-shadow: inset 0 0 0.5em blue;" class="boxshadow">inset 0 0 0.5em blue;</div>
<div style="box-shadow: inset 0 0 2em blue;" class="boxshadow">inset 0 0 2em blue;</div>
<div style="box-shadow: inset 0 2em 3em -1em green;" class="boxshadow">inset 0 2em 3em -1em green;</div>
<div style="box-shadow: inset 0 2em 3em -2em green;" class="boxshadow">inset 0 2em 3em -2em green;</div>
<div style="box-shadow: inset 0 2em 3em -3em green;" class="boxshadow">inset 0 2em 3em -3em green;</div>
<div style="box-shadow: inset 0 0 1em khaki;" class="boxshadow">inset 0 0 1em khaki;</div>
<div style="box-shadow: inset 0 0 1em khaki, inset 0 0 1em khaki, inset 0 0 1em khaki, inset 0 0 1em khaki;"
class="boxshadow">inset 0 0 1em khaki, inset 0 0 1em khaki, inset 0 0 1em khaki, inset 0 0 1em khaki;
</div>
<div style="box-shadow: inset 0 0 0.5em 0.5em khaki;" class="boxshadow">inset 0 0 0.5em 0.5em khaki;</div>
<div class="boxshadow">/* seamless if &lt;blur-radius> ≤ &lt;spread-radius> */</div>
<div style="box-shadow: inset 0 0 0.5em 0.5em khaki;background:black;color:gold;" class="boxshadow">inset 0 0 0.5em
0.5em khaki;
</div>
<div style="box-shadow: inset 0 0 2em 2em khaki;background:red;padding:2em;" class="boxshadow">inset 0 0 2em 2em
khaki;
</div>
<div style="box-shadow: 0 0 0.5em 0.5em teal;background:teal;color:gold;" class="boxshadow">0 0 0.5em 0.5em teal;</div>
<div style="box-shadow: inset 0 0 0.5em 0.5em indigo, 0 0 0.5em 0.5em indigo;padding:1em;margin-top:3em;"
class="boxshadow">inset 0 0 0.5em 0.5em indigo,
0 0 0.5em 0.5em indigo;
</div>
<div style="box-shadow: inset 0 0 1em black, inset 0 0 1em black, inset 0 0 1em black, inset 0 0 1em black;"
class="boxshadow">inset 0 0 1em black, inset 0 0 1em black,
inset 0 0 1em black, inset 0 0 1em black;
</div>
<div style="box-shadow: inset 0 0 0.7em 0.5em black;" class="boxshadow">inset 0 0 0.7em 0.5em black;
/* should be very similar to above */
</div>
<div style="box-shadow: inset 0 2em 3em -1.5em green, inset 0 -2em 3em -2em blue;" class="boxshadow">inset 0 2em 3em
-1.5em green,
inset 0 -2em 3em -2em blue;
</div>
<div style="box-shadow: inset 1em 1em 2em -1em blue;" class="boxshadow">inset 1em 1em 2em -1em blue;</div>
<div style="box-shadow: inset 1em 1em 2em -1em blue, inset -1em -1em 2em -1em red;" class="boxshadow">inset 1em 1em 2em
-1em blue,
inset -1em -1em 2em -1em red;
</div>
<div style="box-shadow: inset 0 2em 3em -2em white, inset 0 -2em 3em -2.5em black;background:red;" class="boxshadow">
inset 0 2em 3em -2em white,
inset 0 -2em 3em -2.5em black;
</div>
<div style="box-shadow: inset 1em 1em 1em -1em white, inset -1em -1em 1em -1em black;background:red;" class="boxshadow">
inset 1em 1em 1em -1em white,
inset -1em -1em 1em -1em black;
</div>
<div style="box-shadow: inset -1em -1em 1em -1em black, inset 1em 1em 1em -1em white;background:red;" class="boxshadow">
inset -1em -1em 1em -1em black,
inset 1em 1em 1em -1em white;
</div>
<div
style="box-shadow: inset -0.3em -0.3em 0.6em rgba(0,0,0,0.5), inset 0.3em 0.3em 0.6em rgba(256,256,256,0.7);background:red;"
class="boxshadow">inset -0.3em -0.3em 0.6em rgba(0,0,0,0.5),
inset 0.3em 0.3em 0.6em rgba(256,256,256,0.7);
</div>
<div
style="box-shadow: inset 0.3em 0.3em 0.6em rgba(256,256,256,0.5), inset -0.3em -0.3em 0.6em rgba(0,0,0,0.5);background:red;"
class="boxshadow">inset 0.3em 0.3em 0.6em rgba(256,256,256,0.5),
inset -0.3em -0.3em 0.6em rgba(0,0,0,0.5);
</div>
<div style="box-shadow: 0.2em 0.2em 0.7em black, inset 0 0 0.7em red;" class="boxshadow">0.2em 0.2em 0.7em black, inset
0 0 0.7em red;
</div>

</body>
</html>