From d7f681a60bbb62b31d5323bd9c7bddb966465be9 Mon Sep 17 00:00:00 2001 From: ysk2014 <1181102772@qq.com> Date: Tue, 13 Oct 2020 12:04:19 +0800 Subject: [PATCH] feat: support font color gradient --- src/css/index.ts | 5 +- .../property-descriptors/background-clip.ts | 5 +- .../property-descriptors/text-fill-color.ts | 9 +++ src/global.d.ts | 1 + src/render/canvas/canvas-renderer.ts | 56 ++++++++++++++++-- src/render/font-color-gradient.ts | 12 ++++ tests/reftests/text/font-color-gradient.html | 57 +++++++++++++++++++ 7 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 src/css/property-descriptors/text-fill-color.ts create mode 100644 src/render/font-color-gradient.ts create mode 100644 tests/reftests/text/font-color-gradient.html diff --git a/src/css/index.ts b/src/css/index.ts index 499c903..148a33a 100644 --- a/src/css/index.ts +++ b/src/css/index.ts @@ -44,7 +44,8 @@ import {overflow, OVERFLOW} from './property-descriptors/overflow'; 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 { 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'; @@ -132,6 +133,7 @@ export class CSSParsedDeclaration { paddingLeft: LengthPercentage; position: ReturnType; textAlign: ReturnType; + textFillColor: Color; textDecorationColor: Color; textDecorationLine: ReturnType; textShadow: ReturnType; @@ -200,6 +202,7 @@ 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); diff --git a/src/css/property-descriptors/background-clip.ts b/src/css/property-descriptors/background-clip.ts index db83504..21fa019 100644 --- a/src/css/property-descriptors/background-clip.ts +++ b/src/css/property-descriptors/background-clip.ts @@ -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 = { return BACKGROUND_CLIP.PADDING_BOX; case 'content-box': return BACKGROUND_CLIP.CONTENT_BOX; + case 'text': + return BACKGROUND_CLIP.TEXT; } } return BACKGROUND_CLIP.BORDER_BOX; diff --git a/src/css/property-descriptors/text-fill-color.ts b/src/css/property-descriptors/text-fill-color.ts new file mode 100644 index 0000000..9781f5b --- /dev/null +++ b/src/css/property-descriptors/text-fill-color.ts @@ -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' +}; diff --git a/src/global.d.ts b/src/global.d.ts index a94aa96..5cd3a36 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,6 +1,7 @@ interface CSSStyleDeclaration { textDecorationColor: string | null; textDecorationLine: string | null; + textFillColor: string | null; overflowWrap: string | null; } diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index 12cb2c9..34f597c 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -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'; @@ -38,7 +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 { processImage, isSupportedFilter } from '../image-filter'; +import {isFontColorGradient } from '../font-color-gradient' export type RenderConfigurations = RenderOptions & { backgroundColor: Color | null; @@ -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; @@ -286,7 +330,7 @@ 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) { @@ -563,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; @@ -588,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; diff --git a/src/render/font-color-gradient.ts b/src/render/font-color-gradient.ts new file mode 100644 index 0000000..8196e73 --- /dev/null +++ b/src/render/font-color-gradient.ts @@ -0,0 +1,12 @@ + +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) => { + if (styles.backgroundImage.length === 1 && (isLinearGradient(styles.backgroundImage[0]) || isRadialGradient(styles.backgroundImage[0]))) { + return (styles.textFillColor === 0 || styles.color === 0) && styles.backgroundClip.length === 1 && styles.backgroundClip[0] === BACKGROUND_CLIP.TEXT; + } else { + return false; + } +} diff --git a/tests/reftests/text/font-color-gradient.html b/tests/reftests/text/font-color-gradient.html new file mode 100644 index 0000000..4d7fb40 --- /dev/null +++ b/tests/reftests/text/font-color-gradient.html @@ -0,0 +1,57 @@ + + + + + Text tests + + + + + + + +

font-color-gradient

+
+ Hello world + no effects +
+
+ Hello world +
+
+ Hello world +
+
npm install --save html2canvas +
+ + + + \ No newline at end of file