mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
feat: box-shadow rendering (#1848)
* feat: box-shadow rendering * test: add box-shadow test
This commit is contained in:

committed by
GitHub

parent
522a443055
commit
5f31b74177
@ -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);
|
||||
|
59
src/css/property-descriptors/box-shadow.ts
Normal file
59
src/css/property-descriptors/box-shadow.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
};
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user