fix opacity

This commit is contained in:
hoho
2021-01-12 18:41:01 +08:00
parent 7222aba1b4
commit 1460aaa7d0
22 changed files with 1534 additions and 89 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"liveServer.settings.port": 0
}

BIN
src.zip Normal file

Binary file not shown.

View File

@@ -45,8 +45,11 @@ import {overflowWrap} from './property-descriptors/overflow-wrap';
import {paddingBottom, paddingLeft, paddingRight, paddingTop} from './property-descriptors/padding';
import {textAlign} from './property-descriptors/text-align';
import {position, POSITION} from './property-descriptors/position';
import {textFillColor} from './property-descriptors/text-fill-color';
import {textShadow} from './property-descriptors/text-shadow';
import {textTransform} from './property-descriptors/text-transform';
import {textStrokeColor} from './property-descriptors/text-stroke-color';
import {textStrokeWidth} from './property-descriptors/text-stroke-width';
import {transform} from './property-descriptors/transform';
import {transformOrigin} from './property-descriptors/transform-origin';
import {visibility, VISIBILITY} from './property-descriptors/visibility';
@@ -73,6 +76,7 @@ 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';
import {filter} from './property-descriptors/filter';
export class CSSParsedDeclaration {
backgroundClip: ReturnType<typeof backgroundClip.parse>;
@@ -101,6 +105,8 @@ export class CSSParsedDeclaration {
boxShadow: ReturnType<typeof boxShadow.parse>;
color: Color;
display: ReturnType<typeof display.parse>;
filter: ReturnType<typeof filter.parse>;
filterOriginal: string | null;
float: ReturnType<typeof float.parse>;
fontFamily: ReturnType<typeof fontFamily.parse>;
fontSize: LengthPercentage;
@@ -127,10 +133,13 @@ export class CSSParsedDeclaration {
paddingLeft: LengthPercentage;
position: ReturnType<typeof position.parse>;
textAlign: ReturnType<typeof textAlign.parse>;
textFillColor: Color;
textDecorationColor: Color;
textDecorationLine: ReturnType<typeof textDecorationLine.parse>;
textShadow: ReturnType<typeof textShadow.parse>;
textTransform: ReturnType<typeof textTransform.parse>;
textStrokeColor: Color;
textStrokeWidth: LengthPercentage;
transform: ReturnType<typeof transform.parse>;
transformOrigin: ReturnType<typeof transformOrigin.parse>;
visibility: ReturnType<typeof visibility.parse>;
@@ -164,6 +173,8 @@ export class CSSParsedDeclaration {
this.boxShadow = parse(boxShadow, declaration.boxShadow);
this.color = parse(color, declaration.color);
this.display = parse(display, declaration.display);
this.filter = parse(filter, declaration.filter);
this.filterOriginal = declaration.filter;
this.float = parse(float, declaration.cssFloat);
this.fontFamily = parse(fontFamily, declaration.fontFamily);
this.fontSize = parse(fontSize, declaration.fontSize);
@@ -191,10 +202,13 @@ export class CSSParsedDeclaration {
this.paddingLeft = parse(paddingLeft, declaration.paddingLeft);
this.position = parse(position, declaration.position);
this.textAlign = parse(textAlign, declaration.textAlign);
this.textFillColor = parse(textFillColor, declaration.textFillColor);
this.textDecorationColor = parse(textDecorationColor, declaration.textDecorationColor || declaration.color);
this.textDecorationLine = parse(textDecorationLine, declaration.textDecorationLine);
this.textShadow = parse(textShadow, declaration.textShadow);
this.textTransform = parse(textTransform, declaration.textTransform);
this.textStrokeColor = parse(textStrokeColor, declaration.webkitTextStrokeColor);
this.textStrokeWidth = parse(textStrokeWidth, declaration.webkitTextStrokeWidth);
this.transform = parse(transform, declaration.transform);
this.transformOrigin = parse(transformOrigin, declaration.transformOrigin);
this.visibility = parse(visibility, declaration.visibility);
@@ -261,7 +275,8 @@ export class CSSParsedCounterDeclaration {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parse = (descriptor: CSSPropertyDescriptor<any>, style?: string | null) => {
const tokenizer = new Tokenizer();
const value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;
const value =
style !== null && typeof style !== 'undefined' && style != '' ? style.toString() : descriptor.initialValue;
tokenizer.write(value);
const parser = new Parser(tokenizer.read());
switch (descriptor.type) {

View File

@@ -1,24 +0,0 @@
import {deepEqual} from 'assert';
import {Parser} from '../../syntax/parser';
import {fontFamily} from '../font-family';
const fontFamilyParse = (value: string) => fontFamily.parse(Parser.parseValues(value));
describe('property-descriptors', () => {
describe('font-family', () => {
it('sans-serif', () => deepEqual(fontFamilyParse('sans-serif'), ['sans-serif']));
it('great fonts 40 library', () =>
deepEqual(fontFamilyParse('great fonts 40 library'), ["'great fonts 40 library'"]));
it('preferred font, "quoted fallback font", font', () =>
deepEqual(fontFamilyParse('preferred font, "quoted fallback font", font'), [
"'preferred font'",
"'quoted fallback font'",
'font'
]));
it("'escaping test\\'s font'", () =>
deepEqual(fontFamilyParse("'escaping test\\'s font'"), ["'escaping test's font'"]));
});
});

View File

@@ -3,7 +3,8 @@ import {CSSValue, isIdentToken} from '../syntax/parser';
export enum BACKGROUND_CLIP {
BORDER_BOX = 0,
PADDING_BOX = 1,
CONTENT_BOX = 2
CONTENT_BOX = 2,
TEXT = 3
}
export type BackgroundClip = BACKGROUND_CLIP[];
@@ -21,6 +22,8 @@ export const backgroundClip: IPropertyListDescriptor<BackgroundClip> = {
return BACKGROUND_CLIP.PADDING_BOX;
case 'content-box':
return BACKGROUND_CLIP.CONTENT_BOX;
case 'text':
return BACKGROUND_CLIP.TEXT;
}
}
return BACKGROUND_CLIP.BORDER_BOX;

View File

@@ -0,0 +1,53 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentWithValue, CSSFunction, isCSSFunction} from '../syntax/parser';
import {isLength, Length} from '../types/length';
export type Filter = FilterItem[];
export interface FilterItem {
name: string;
value: Length;
}
export const filter: IPropertyListDescriptor<Filter | null> = {
name: 'filter',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
return null;
}
const filter: Filter = [];
let hasFilter: boolean = false;
tokens.filter(isCSSFunction).forEach((token: CSSFunction) => {
switch (token.name) {
case 'contrast':
case 'hue-rotate':
case 'grayscale':
case 'brightness':
case 'blur':
case 'invert':
case 'saturate':
case 'sepia':
for (let index = 0; index < token.values.length; index++) {
const value: CSSValue = token.values[index];
if (isLength(value)) {
hasFilter = true;
filter.push({name: token.name, value: value});
}
}
break;
default:
break;
}
});
if (hasFilter) {
return filter;
} else {
return null;
}
}
};

View File

@@ -1,6 +1,6 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
import {StringValueToken, TokenType} from '../syntax/tokenizer';
export type FONT_FAMILY = string;
@@ -12,26 +12,9 @@ export const fontFamily: IPropertyListDescriptor<FontFamily> = {
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
const accumulator: string[] = [];
const results: string[] = [];
tokens.forEach(token => {
switch (token.type) {
case TokenType.IDENT_TOKEN:
case TokenType.STRING_TOKEN:
accumulator.push(token.value);
break;
case TokenType.NUMBER_TOKEN:
accumulator.push(token.number.toString());
break;
case TokenType.COMMA_TOKEN:
results.push(accumulator.join(' '));
accumulator.length = 0;
break;
}
});
if (accumulator.length) {
results.push(accumulator.join(' '));
}
return results.map(result => (result.indexOf(' ') === -1 ? result : `'${result}'`));
return tokens.filter(isStringToken).map(token => token.value);
}
};
const isStringToken = (token: CSSValue): token is StringValueToken =>
token.type === TokenType.STRING_TOKEN || token.type === TokenType.IDENT_TOKEN;

View File

@@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const textFillColor: IPropertyTypeValueDescriptor = {
name: `text-fill-color`,
initialValue: 'transparent',
prefix: true,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

View File

@@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const textStrokeColor: IPropertyTypeValueDescriptor = {
name: `text-stroke-color`,
initialValue: 'transparent',
prefix: true,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

View File

@@ -0,0 +1,8 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const textStrokeWidth: IPropertyTypeValueDescriptor = {
name: 'text-stroke-width',
initialValue: '0',
type: PropertyDescriptorParsingType.TYPE_VALUE,
prefix: true,
format: 'length-percentage'
};

View File

@@ -141,6 +141,7 @@ export class Parser {
}
}
export const isCSSFunction = (token: CSSValue): token is CSSFunction => token.type === TokenType.FUNCTION;
export const isDimensionToken = (token: CSSValue): token is DimensionToken => token.type === TokenType.DIMENSION_TOKEN;
export const isNumberToken = (token: CSSValue): token is NumberValueToken => token.type === TokenType.NUMBER_TOKEN;
export const isIdentToken = (token: CSSValue): token is StringValueToken => token.type === TokenType.IDENT_TOKEN;

View File

@@ -302,3 +302,156 @@ export const COLORS: {[key: string]: Color} = {
YELLOW: 0xffff00ff,
YELLOWGREEN: 0x9acd32ff
};
export interface RGBColor {
r: number;
g: number;
b: number;
}
export const contrastRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
else if (value > 1) value = 1;
return {
r: Math.max(0, Math.min(255, value * (rgb.r - 128) + 128)),
g: Math.max(0, Math.min(255, value * (rgb.g - 128) + 128)),
b: Math.max(0, Math.min(255, value * (rgb.b - 128) + 128))
};
};
export const grayscaleRGB = (rgb: RGBColor, value: number, mode?: string | null) => {
var gray = 0;
//different grayscale algorithms
switch (mode) {
case 'average':
gray = (rgb.r + rgb.g + rgb.b) / 3;
break;
case 'luma:BT601':
gray = rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114;
break;
case 'desaturation':
gray = (Math.max(rgb.r, rgb.g, rgb.b) + Math.max(rgb.r, rgb.g, rgb.b)) / 2;
break;
case 'decompsition:max':
gray = Math.max(rgb.r, rgb.g, rgb.b);
break;
case 'decompsition:min':
gray = Math.min(rgb.r, rgb.g, rgb.b);
break;
case 'luma:BT709':
default:
gray = rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722;
break;
}
rgb.r = value * (gray - rgb.r) + rgb.r;
rgb.g = value * (gray - rgb.g) + rgb.g;
rgb.b = value * (gray - rgb.b) + rgb.b;
return rgb;
};
export const brightnessRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
return {
r: Math.max(0, Math.min(255, rgb.r * value)),
g: Math.max(0, Math.min(255, rgb.g * value)),
b: Math.max(0, Math.min(255, rgb.b * value))
};
};
export const invertRGB = (rgb: RGBColor, value: number): RGBColor => {
return {
r: value * (255 - 2 * rgb.r) + rgb.r,
g: value * (255 - 2 * rgb.g) + rgb.g,
b: value * (255 - 2 * rgb.b) + rgb.b
};
};
export const sepiaRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
else if (value > 1) value = 1;
return {
r: value * Math.min(255, rgb.r * 0.393 + rgb.g * 0.769 + rgb.b * 0.189 - rgb.r) + rgb.r,
g: value * Math.min(255, rgb.r * 0.349 + rgb.g * 0.686 + rgb.b * 0.168 - rgb.g) + rgb.g,
b: value * Math.min(255, rgb.r * 0.272 + rgb.g * 0.534 + rgb.b * 0.131 - rgb.b) + rgb.b
};
};
export const hueRotateRGB = (rgb: RGBColor, value: number): RGBColor => {
while (value < 0) value += 360;
while (value > 360) value -= 360;
rgb2hsl(rgb);
rgb.r += value;
if (rgb.r < 0) rgb.r += 360;
if (rgb.r > 359) rgb.r -= 360;
hsl2rgb(rgb);
return rgb;
};
export const saturateRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
rgb2hsl(rgb);
rgb.g *= value;
if (rgb.g > 100) rgb.g = 100;
hsl2rgb(rgb);
return rgb;
};
function rgb2hsl(rgb: RGBColor) {
rgb.r = Math.max(0, Math.min(255, rgb.r)) / 255;
rgb.g = Math.max(0, Math.min(255, rgb.g)) / 255;
rgb.b = Math.max(0, Math.min(255, rgb.b)) / 255;
let h, l;
let M = Math.max(rgb.r, rgb.g, rgb.b);
let m = Math.min(rgb.r, rgb.g, rgb.b);
let d = M - m;
if (d == 0) h = 0;
else if (M == rgb.r) h = ((rgb.g - rgb.b) / d) % 6;
else if (M == rgb.g) h = (rgb.b - rgb.r) / d + 2;
else h = (rgb.r - rgb.g) / d + 4;
h = Math.round(h * 60);
if (h < 0) h += 360;
rgb.r = h;
l = (M + m) / 2;
if (d == 0) rgb.g = 0;
else rgb.g = (d / (1 - Math.abs(2 * l - 1))) * 100;
rgb.b = l * 100;
}
function hsl2rgb(rgb: RGBColor) {
rgb.r = Math.max(0, Math.min(359, rgb.r));
rgb.g = Math.max(0, Math.min(100, rgb.g)) / 100;
rgb.b = Math.max(0, Math.min(100, rgb.b)) / 100;
let C = (1 - Math.abs(2 * rgb.b - 1)) * rgb.g;
let h = rgb.r / 60;
let X = C * (1 - Math.abs((h % 2) - 1));
let l = rgb.b;
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
if (h >= 0 && h < 1) {
rgb.r = C;
rgb.g = X;
} else if (h >= 1 && h < 2) {
rgb.r = X;
rgb.g = C;
} else if (h >= 2 && h < 3) {
rgb.g = C;
rgb.b = X;
} else if (h >= 3 && h < 4) {
rgb.g = X;
rgb.b = C;
} else if (h >= 4 && h < 5) {
rgb.r = X;
rgb.b = C;
} else {
rgb.r = C;
rgb.b = X;
}
let m = l - C / 2;
rgb.r += m;
rgb.g += m;
rgb.b += m;
rgb.r = Math.round(rgb.r * 255.0);
rgb.g = Math.round(rgb.g * 255.0);
rgb.b = Math.round(rgb.b * 255.0);
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import {
isElementNode,
isHTMLElementNode,
isIFrameElement,
isImageElement,
isScriptElement,
isSelectElement,
isStyleElement,
@@ -116,7 +115,7 @@ export class DocumentCloner {
return iframeLoad;
}
createElementClone<T extends HTMLElement | SVGElement>(node: T): HTMLElement | SVGElement {
createElementClone(node: HTMLElement): HTMLElement {
if (isCanvasElement(node)) {
return this.createCanvasClone(node);
}
@@ -129,14 +128,7 @@ export class DocumentCloner {
return this.createStyleClone(node);
}
const clone = node.cloneNode(false) as T;
// @ts-ignore
if (isImageElement(clone) && clone.loading === 'lazy') {
// @ts-ignore
clone.loading = 'eager';
}
return clone;
return node.cloneNode(false) as HTMLElement;
}
createStyleClone(node: HTMLStyleElement): HTMLStyleElement {
@@ -265,14 +257,14 @@ export class DocumentCloner {
const window = node.ownerDocument.defaultView;
if (window && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
if (isHTMLElementNode(node) && window) {
const clone = this.createElementClone(node);
const style = window.getComputedStyle(node);
const styleBefore = window.getComputedStyle(node, ':before');
const styleAfter = window.getComputedStyle(node, ':after');
if (this.referenceElement === node && isHTMLElementNode(clone)) {
if (this.referenceElement === node) {
this.clonedReferenceElement = clone;
}
if (isBodyElement(clone)) {
@@ -306,7 +298,7 @@ export class DocumentCloner {
this.counters.pop(counters);
if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) {
if (style && this.options.copyStyles && !isIFrameElement(node)) {
copyCSSStyles(style, clone);
}
@@ -486,7 +478,7 @@ const iframeLoader = (iframe: HTMLIFrameElement): Promise<HTMLIFrameElement> =>
});
};
export const copyCSSStyles = <T extends HTMLElement | SVGElement>(style: CSSStyleDeclaration, target: T): T => {
export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => {
// Edge does not provide value for cssText
for (let i = style.length - 1; i >= 0; i--) {
const property = style.item(i);

View File

@@ -102,7 +102,7 @@ const createsStackingContext = (styles: CSSParsedDeclaration): boolean => styles
export const isTextNode = (node: Node): node is Text => node.nodeType === Node.TEXT_NODE;
export const isElementNode = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE;
export const isHTMLElementNode = (node: Node): node is HTMLElement =>
isElementNode(node) && typeof (node as HTMLElement).style !== 'undefined' && !isSVGElementNode(node);
typeof (node as HTMLElement).style !== 'undefined';
export const isSVGElementNode = (element: Element): element is SVGElement =>
typeof (element as SVGElement).className === 'object';
export const isLIElement = (node: Element): node is HTMLLIElement => node.tagName === 'LI';

1
src/global.d.ts vendored
View File

@@ -1,6 +1,7 @@
interface CSSStyleDeclaration {
textDecorationColor: string | null;
textDecorationLine: string | null;
textFillColor: string | null;
overflowWrap: string | null;
}

View File

@@ -25,7 +25,7 @@ const html2canvas = (element: HTMLElement, options: Partial<Options> = {}): Prom
export default html2canvas;
if (typeof window !== 'undefined') {
if (typeof window !== "undefined") {
CacheStorage.setContext(window);
}

View File

@@ -10,7 +10,7 @@ import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
import {BoundCurves, calculateBorderBoxPath, calculateContentBoxPath, calculatePaddingBoxPath} from '../bound-curves';
import {isBezierCurve} from '../bezier-curve';
import {Vector} from '../vector';
import {CSSImageType, CSSURLImage, isLinearGradient, isRadialGradient} from '../../css/types/image';
import {CSSImageType, CSSLinearGradientImage, CSSRadialGradientImage, CSSURLImage, isLinearGradient, isRadialGradient} from '../../css/types/image';
import {parsePathForBorder} from '../border';
import {Cache} from '../../core/cache-storage';
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
@@ -22,7 +22,7 @@ import {contentBox} from '../box-sizing';
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container';
import {ReplacedElementContainer} from '../../dom/replaced-elements/index';
import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects';
import {EffectTarget, IElementEffect, isClipEffect, isTransformEffect, isOpacityEffect} from '../effects';
import {contains} from '../../core/bitwise';
import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient';
import {FIFTY_PERCENT, getAbsoluteValue} from '../../css/types/length-percentage';
@@ -38,6 +38,8 @@ import {TextareaElementContainer} from '../../dom/elements/textarea-element-cont
import {SelectElementContainer} from '../../dom/elements/select-element-container';
import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container';
import {TextShadow} from '../../css/property-descriptors/text-shadow';
import { processImage, isSupportedFilter } from '../image-filter';
import {isFontColorGradient } from '../font-color-gradient'
export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null;
@@ -99,10 +101,6 @@ export class CanvasRenderer {
applyEffect(effect: IElementEffect) {
this.ctx.save();
if (isOpacityEffect(effect)) {
this.ctx.globalAlpha = effect.opacity;
}
if (isTransformEffect(effect)) {
this.ctx.translate(effect.offsetX, effect.offsetY);
this.ctx.transform(
@@ -120,7 +118,9 @@ export class CanvasRenderer {
this.path(effect.path);
this.ctx.clip();
}
if (isOpacityEffect(effect)) {
this.ctx.globalAlpha = effect.val;
}
this._activeEffects.push(effect);
}
@@ -132,6 +132,7 @@ export class CanvasRenderer {
async renderStack(stack: StackingContext) {
const styles = stack.element.container.styles;
if (styles.isVisible()) {
this.ctx.globalAlpha = styles.opacity;
await this.renderStackContent(stack);
}
}
@@ -172,13 +173,56 @@ export class CanvasRenderer {
];
}
async renderTextNode(text: TextContainer, styles: CSSParsedDeclaration) {
async renderTextNode(text: TextContainer, styles: CSSParsedDeclaration, container: ElementContainer) {
const [font, fontFamily, fontSize] = this.createFontStyle(styles);
this.ctx.font = font;
let fillStyle: CanvasGradient|string = asString(styles.color);
if (isFontColorGradient(styles)) {
if (isLinearGradient(styles.backgroundImage[0])) {
const backgroundImage = styles.backgroundImage[0] as CSSLinearGradientImage
const [, x, y, width, height] = calculateBackgroundRendering(container, 0, [null, null, null]);
const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
if (Math.round(Math.abs(x0)) === Math.round(Math.abs(x1))) {
const gradient = this.ctx.createLinearGradient(x, y0+y, x, y1+y);
processColorStops(backgroundImage.stops, lineLength).forEach(colorStop =>
gradient.addColorStop(colorStop.stop, asString(colorStop.color))
);
fillStyle = gradient;
} else if (Math.round(Math.abs(y0)) === Math.round(Math.abs(y1))) {
const gradient = this.ctx.createLinearGradient(x+x0, y, x+x1, y);
processColorStops(backgroundImage.stops, lineLength).forEach(colorStop =>
gradient.addColorStop(colorStop.stop, asString(colorStop.color))
);
fillStyle = gradient;
}
} else if (isRadialGradient(styles.backgroundImage[0])) {
const backgroundImage = styles.backgroundImage[0] as CSSRadialGradientImage
const [, left, top, width, height] = calculateBackgroundRendering(container, 0, [
null,
null,
null
]);
const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
const x = getAbsoluteValue(position[0], width);
const y = getAbsoluteValue(position[position.length - 1], height);
const [rx,] = calculateRadius(backgroundImage, x, y, width, height);
if (rx > 0 && rx > 0) {
const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
processColorStops(backgroundImage.stops, rx * 2).forEach(colorStop =>
radialGradient.addColorStop(colorStop.stop, asString(colorStop.color))
);
fillStyle = radialGradient;
}
}
}
text.textBounds.forEach(text => {
this.ctx.fillStyle = asString(styles.color);
this.ctx.fillStyle = fillStyle;
this.renderTextWithLetterSpacing(text, styles.letterSpacing);
const textShadows: TextShadow = styles.textShadow;
@@ -234,6 +278,20 @@ export class CanvasRenderer {
}
});
}
if (
styles.textStrokeWidth &&
styles.textStrokeWidth.number &&
styles.textStrokeColor &&
text.text.trim().length
) {
this.ctx.strokeStyle = asString(styles.textStrokeColor);
this.ctx.lineWidth = getAbsoluteValue(styles.textStrokeWidth, text.bounds.width);
this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
this.ctx.strokeStyle = '';
this.ctx.lineWidth = 0;
}
});
}
@@ -248,6 +306,9 @@ export class CanvasRenderer {
this.path(path);
this.ctx.save();
this.ctx.clip();
if (isSupportedFilter(this.ctx) && container.styles.filterOriginal) {
this.ctx.filter = container.styles.filterOriginal;
}
this.ctx.drawImage(
image,
0,
@@ -269,12 +330,13 @@ export class CanvasRenderer {
const curves = paint.curves;
const styles = container.styles;
for (const child of container.textNodes) {
await this.renderTextNode(child, styles);
await this.renderTextNode(child, styles, container);
}
if (container instanceof ImageElementContainer) {
try {
const image = await this.options.cache.match(container.src);
if (styles.filter && !isSupportedFilter(this.ctx)) await processImage(image, styles.filter);
this.renderReplacedElement(container, curves, image);
} catch (e) {
Logger.getInstance(this.options.id).error(`Error loading image ${container.src}`);
@@ -545,6 +607,7 @@ export class CanvasRenderer {
async renderBackgroundImage(container: ElementContainer) {
let index = container.styles.backgroundImage.length - 1;
if (isFontColorGradient(container.styles)) return false;
for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
if (backgroundImage.type === CSSImageType.URL) {
let image;
@@ -570,7 +633,6 @@ export class CanvasRenderer {
} 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;

View File

@@ -18,37 +18,42 @@ export interface IElementEffect {
}
export class TransformEffect implements IElementEffect {
readonly type: EffectType = EffectType.TRANSFORM;
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
readonly type: EffectType;
readonly target: number;
readonly offsetX: number;
readonly offsetY: number;
readonly matrix: Matrix;
constructor(offsetX: number, offsetY: number, matrix: Matrix) {
this.type = EffectType.TRANSFORM;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.matrix = matrix;
this.target = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
}
}
export class ClipEffect implements IElementEffect {
readonly type: EffectType = EffectType.CLIP;
readonly type: EffectType;
readonly target: number;
readonly path: Path[];
constructor(path: Path[], target: EffectTarget) {
this.type = EffectType.CLIP;
this.target = target;
this.path = path;
}
}
export class OpacityEffect implements IElementEffect {
readonly type: EffectType = EffectType.OPACITY;
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
readonly opacity: number;
readonly type: EffectType;
readonly target: number;
readonly val: number;
constructor(opacity: number) {
this.opacity = opacity;
constructor(val: number, target: EffectTarget) {
this.type = EffectType.OPACITY;
this.target = target;
this.val = val;
}
}

View File

@@ -0,0 +1,13 @@
import {CSSParsedDeclaration} from '../css/index';
import {isLinearGradient, isRadialGradient} from '../css/types/image';
import {BACKGROUND_CLIP} from '../css/property-descriptors/background-clip';
export const isFontColorGradient = (styles: CSSParsedDeclaration) => {
return (
styles.backgroundImage.length === 1 &&
(isLinearGradient(styles.backgroundImage[0]) || isRadialGradient(styles.backgroundImage[0])) &&
(styles.textFillColor === 0 || styles.color === 0) &&
styles.backgroundClip.length === 1 &&
styles.backgroundClip[0] === BACKGROUND_CLIP.TEXT
);
};

View File

@@ -0,0 +1,99 @@
import {
RGBColor,
contrastRGB,
hueRotateRGB,
grayscaleRGB,
brightnessRGB,
invertRGB,
saturateRGB,
sepiaRGB
} from '../css/types/color';
import {Filter, FilterItem} from '../css/property-descriptors/filter';
import {stackBlurImage} from '../css/types/functions/stack-blur';
export const processImage = (img: HTMLImageElement, filter: Filter) => {
return new Promise((resolve, reject) => {
if (!img || !('naturalWidth' in img)) {
return resolve();
}
const w = img['naturalWidth'];
const h = img['naturalHeight'];
const canvas = document.createElement('canvas');
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
canvas.width = w * 2;
canvas.height = h * 2;
const context = canvas.getContext('2d') as CanvasRenderingContext2D;
context.clearRect(0, 0, w, h);
context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, w, h);
let imageData: ImageData = context.getImageData(0, 0, w, h);
handlePerPixel(imageData, filter);
let blurFilter = filter.find((item: FilterItem) => item.name === 'blur');
if (blurFilter) {
imageData = stackBlurImage(imageData, w, h, blurFilter.value.number * 2, 1);
}
context.putImageData(imageData, 0, 0);
img.crossOrigin = 'anonymous';
img.src = canvas.toDataURL();
img.onload = () => resolve(img);
img.onerror = reject;
if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => resolve(img), 500);
}
});
};
function handlePerPixel(imageData: ImageData, filter: Filter) {
for (let _j = 0; _j < imageData.height; _j++) {
for (let _i = 0; _i < imageData.width; _i++) {
let index = _j * 4 * imageData.width + _i * 4;
let rgb: RGBColor = {
r: imageData.data[index],
g: imageData.data[index + 1],
b: imageData.data[index + 2]
};
filter.forEach((item: FilterItem) => {
switch (item.name) {
case 'contrast':
rgb = contrastRGB(rgb, item.value.number);
break;
case 'hue-rotate':
rgb = hueRotateRGB(rgb, item.value.number);
break;
case 'grayscale':
rgb = grayscaleRGB(rgb, item.value.number);
break;
case 'brightness':
rgb = brightnessRGB(rgb, item.value.number);
break;
case 'invert':
rgb = invertRGB(rgb, item.value.number);
break;
case 'saturate':
rgb = saturateRGB(rgb, item.value.number);
break;
case 'sepia':
rgb = sepiaRGB(rgb, item.value.number);
break;
default:
break;
}
});
imageData.data[index] = rgb.r;
imageData.data[index + 1] = rgb.g;
imageData.data[index + 2] = rgb.b;
}
}
}
export const isSupportedFilter = (ctx: CanvasRenderingContext2D) => 'filter' in ctx;

View File

@@ -1,7 +1,7 @@
import {ElementContainer, FLAGS} from '../dom/element-container';
import {contains} from '../core/bitwise';
import {BoundCurves, calculateBorderBoxPath, calculatePaddingBoxPath} from './bound-curves';
import {ClipEffect, EffectTarget, IElementEffect, OpacityEffect, TransformEffect} from './effects';
import {ClipEffect, EffectTarget, IElementEffect, TransformEffect, OpacityEffect} from './effects';
import {OVERFLOW} from '../css/property-descriptors/overflow';
import {equalPath} from './path';
import {DISPLAY} from '../css/property-descriptors/display';
@@ -41,10 +41,6 @@ export class ElementPaint {
this.container = element;
this.effects = parentStack.slice(0);
this.curves = new BoundCurves(element);
if (element.styles.opacity < 1) {
this.effects.push(new OpacityEffect(element.styles.opacity));
}
if (element.styles.transform !== null) {
const offsetX = element.bounds.left + element.styles.transformOrigin[0].number;
const offsetY = element.bounds.top + element.styles.transformOrigin[1].number;
@@ -63,6 +59,10 @@ export class ElementPaint {
this.effects.push(new ClipEffect(paddingBox, EffectTarget.CONTENT));
}
}
if (element.styles.opacity < 1) {
this.effects.push(new OpacityEffect(element.styles.opacity, EffectTarget.CONTENT));
}
}
getParentEffects(): IElementEffect[] {
@@ -74,6 +74,9 @@ export class ElementPaint {
effects.push(new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT));
}
}
if (this.container.styles.opacity < 1) {
effects.push(new OpacityEffect(this.container.styles.opacity, EffectTarget.CONTENT));
}
return effects;
}
}
@@ -119,7 +122,7 @@ const parseStackTree = (
} else if (order > 0) {
let index = 0;
parentStack.positiveZIndex.some((current, i) => {
if (order >= current.element.container.styles.zIndex.order) {
if (order > current.element.container.styles.zIndex.order) {
index = i + 1;
return false;
} else if (index > 0) {