diff --git a/src/css/index.ts b/src/css/index.ts index 27393d0d0..46db9a775 100644 --- a/src/css/index.ts +++ b/src/css/index.ts @@ -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; @@ -97,6 +98,7 @@ export class CSSParsedDeclaration { borderRightWidth: ReturnType; borderBottomWidth: ReturnType; borderLeftWidth: ReturnType; + boxShadow: ReturnType; color: Color; display: ReturnType; float: ReturnType; @@ -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); diff --git a/src/css/property-descriptors/box-shadow.ts b/src/css/property-descriptors/box-shadow.ts new file mode 100644 index 000000000..8a4253685 --- /dev/null +++ b/src/css/property-descriptors/box-shadow.ts @@ -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 = { + 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; + }); + } +}; diff --git a/src/render/bezier-curve.ts b/src/render/bezier-curve.ts index c6bebd030..13dbc6b50 100644 --- a/src/render/bezier-curve.ts +++ b/src/render/bezier-curve.ts @@ -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); } diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index 05c8c5ab1..c9067bf15 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -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; diff --git a/src/render/path.ts b/src/render/path.ts index 61c8672bb..9174078d6 100644 --- a/src/render/path.ts +++ b/src/render/path.ts @@ -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; diff --git a/src/render/vector.ts b/src/render/vector.ts index 0a08ee044..a55ec3476 100644 --- a/src/render/vector.ts +++ b/src/render/vector.ts @@ -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; diff --git a/tests/reftests/background/box-shadow.html b/tests/reftests/background/box-shadow.html new file mode 100644 index 000000000..98aec844d --- /dev/null +++ b/tests/reftests/background/box-shadow.html @@ -0,0 +1,118 @@ + + + + + box-shadow tests + + + + +
0 0 0 0.5em;
+
0 0 1em;
+
1em 0.5em;
+
1em 0.5em 1em;
+
0 2em 1em -0.7em;
+
0.3em 0.3em lightgreen;
+
0.3em 0.3em 0 0.6em lightgreen;
+
0 2em 0 -0.9em lightgreen;
+
2em 1.5em 0 -0.7em lightgreen;
+
9em 1.2em 0 -0.6em lightgreen;
+
-27.3em 0 lightgreen;
+
0 2em 0 -1em lightgreen;
+
0 0 1em maroon;
+
0 0 1em 0.5em maroon;
+
0 0 1em 1em maroon;
+
-0.4em -0.4em 1em olive;
+
0.4em 0.4em 1em olive;
+
0.4em 0.4em 1em -0.2em olive;
+
0.4em 0.4em 1em 0.4em olive;
+
0 1.5em 0.5em -1em olive;
+
inset 0.2em 0.4em red, inset + -1em -0.7em red; +
+
inset 11em 0 red;
+
inset -1em 0 red;
+
inset 13em 0 3em + -3em orange,inset -3em 0 3em -3em blue; +
+
inset 11em 0 2em orange;
+
inset 0 0.3em red;
+
inset 0 -1.1em red;
+
inset 1em 0 1em -1em blue;
+
inset 0 0 0.5em blue;
+
inset 0 0 2em blue;
+
inset 0 2em 3em -1em green;
+
inset 0 2em 3em -2em green;
+
inset 0 2em 3em -3em green;
+
inset 0 0 1em khaki;
+
inset 0 0 1em khaki, inset 0 0 1em khaki, inset 0 0 1em khaki, inset 0 0 1em khaki; +
+
inset 0 0 0.5em 0.5em khaki;
+
/* seamless if <blur-radius> ≤ <spread-radius> */
+
inset 0 0 0.5em + 0.5em khaki; +
+
inset 0 0 2em 2em + khaki; +
+
0 0 0.5em 0.5em teal;
+
inset 0 0 0.5em 0.5em indigo, + 0 0 0.5em 0.5em indigo; +
+
inset 0 0 1em black, inset 0 0 1em black, + inset 0 0 1em black, inset 0 0 1em black; +
+
inset 0 0 0.7em 0.5em black; + /* should be very similar to above */ +
+
inset 0 2em 3em + -1.5em green, + inset 0 -2em 3em -2em blue; +
+
inset 1em 1em 2em -1em blue;
+
inset 1em 1em 2em + -1em blue, + inset -1em -1em 2em -1em red; +
+
+ inset 0 2em 3em -2em white, + inset 0 -2em 3em -2.5em black; +
+
+ inset 1em 1em 1em -1em white, + inset -1em -1em 1em -1em black; +
+
+ inset -1em -1em 1em -1em black, + inset 1em 1em 1em -1em white; +
+
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); +
+
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); +
+
0.2em 0.2em 0.7em black, inset + 0 0 0.7em red; +
+ + +