feat: support font color gradient

This commit is contained in:
ysk2014 2020-10-13 12:04:19 +08:00
parent fc8c95ea74
commit d7f681a60b
7 changed files with 137 additions and 8 deletions

View File

@ -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<typeof position.parse>;
textAlign: ReturnType<typeof textAlign.parse>;
textFillColor: Color;
textDecorationColor: Color;
textDecorationLine: ReturnType<typeof textDecorationLine.parse>;
textShadow: ReturnType<typeof textShadow.parse>;
@ -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);

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,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const textFillColor: IPropertyTypeValueDescriptor = {
name: `text-fill-color`,
initialValue: 'transparent',
prefix: true,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

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

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

View File

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

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>Text tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
body {
font-family: Arial;
}
.gradient-text {
font-size: 40px;
font-weight: bolder;
position: relative;
}
.gradient-text-one {
background-image: -webkit-linear-gradient(bottom, red, #fd8403, yellow);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.gradient-text-two {
background-image: -webkit-linear-gradient(right, red, blue);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.gradient-text-three {
background-image: radial-gradient(red, yellow, green);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<h1>font-color-gradient</h1>
<div style="margin-top: 10px;">
<span class="gradient-text gradient-text-one">Hello world</span>
<span>no effects</span>
</div>
<div style="margin-top: 10px;">
<span class="gradient-text gradient-text-two">Hello world</span>
</div>
<div style="margin-top: 10px;">
<span class="gradient-text gradient-text-three">Hello world</span>
</div>
<div style="font-family: 'Inconsolata', Monaco, Consolas, 'Andale Mono', monospace">npm install --save html2canvas
</div>
</body>
</html>