feat: box-shadow rendering (#1848)

* feat: box-shadow rendering

* test: add box-shadow test
This commit is contained in:
Niklas von Hertzen 2019-05-25 21:53:50 -07:00 committed by GitHub
parent 522a443055
commit 5f31b74177
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 264 additions and 4 deletions

View File

@ -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>;
@ -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>;
@ -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);

View File

@ -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;
});
}
};

View File

@ -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);
}

View File

@ -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';
@ -55,6 +55,8 @@ export interface RenderOptions {
cache: Cache;
}
const MASK_OFFSET = 10000;
export class CanvasRenderer {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
@ -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) {
@ -493,8 +510,6 @@ export class CanvasRenderer {
);
}
});
this.ctx.closePath();
}
renderRepeat(path: Path[], pattern: CanvasPattern | CanvasGradient, offsetX: number, offsetY: number) {
@ -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();
@ -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;

View File

@ -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 => {
@ -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;

View File

@ -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;

View File

@ -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>