Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(svg): support gradients and patterns for background color in SVG renderer #959

Merged
merged 10 commits into from
Oct 13, 2022
10 changes: 9 additions & 1 deletion src/canvas/Layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,14 @@ export default class Layer extends Eventful {
let clearColorGradientOrPattern;
// Gradient
if (util.isGradientObject(clearColor)) {
// shouldn't cache when clearColor is not global and size changed
const shouldCache = clearColor.global || (
(clearColor as InnerGradientObject).__width === width
&& (clearColor as InnerGradientObject).__height === height
);
// Cache canvas gradient
clearColorGradientOrPattern = (clearColor as InnerGradientObject).__canvasGradient
clearColorGradientOrPattern = shouldCache
&& (clearColor as InnerGradientObject).__canvasGradient
|| getCanvasGradient(ctx, clearColor, {
x: 0,
y: 0,
Expand All @@ -432,6 +438,8 @@ export default class Layer extends Eventful {
});

(clearColor as InnerGradientObject).__canvasGradient = clearColorGradientOrPattern;
(clearColor as InnerGradientObject).__width = width;
(clearColor as InnerGradientObject).__height = height;
}
// Pattern
else if (util.isImagePatternObject(clearColor)) {
Expand Down
2 changes: 2 additions & 0 deletions src/graphic/Gradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface GradientObject {

export interface InnerGradientObject extends GradientObject {
__canvasGradient: CanvasGradient
__width: number
__height: number
}

export interface GradientColorStop {
Expand Down
113 changes: 72 additions & 41 deletions src/svg/Painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
*/

import {
brush, setClipPath
brush,
setClipPath,
setGradient,
setPattern
} from './graphic';
import Displayable from '../graphic/Displayable';
import Storage from '../Storage';
Expand All @@ -19,11 +22,13 @@ import {
createBrushScope,
createSVGVNode
} from './core';
import { normalizeColor, encodeBase64 } from './helper';
import { extend, keys, logError, map, retrieve2 } from '../core/util';
import { normalizeColor, encodeBase64, isGradient, isPattern } from './helper';
import { extend, keys, logError, map, noop, retrieve2 } from '../core/util';
import Path from '../graphic/Path';
import patch, { updateAttrs } from './patch';
import { getSize } from '../canvas/helper';
import { GradientObject } from '../graphic/Gradient';
import { PatternObject } from '../graphic/Pattern';

let svgId = 0;

Expand All @@ -33,6 +38,8 @@ interface SVGPainterOption {
ssr?: boolean
}

type SVGPainterBackgroundColor = string | GradientObject | PatternObject;

class SVGPainter implements PainterBase {

type = 'svg'
Expand All @@ -53,7 +60,7 @@ class SVGPainter implements PainterBase {
private _width: number
private _height: number

private _backgroundColor: string
private _backgroundColor: SVGPainterBackgroundColor

private _id: string

Expand Down Expand Up @@ -126,7 +133,6 @@ class SVGPainter implements PainterBase {
opts = opts || {};

const list = this.storage.getDisplayList(true);
const bgColor = this._backgroundColor;
const width = this._width;
const height = this._height;

Expand All @@ -137,26 +143,8 @@ class SVGPainter implements PainterBase {

const children: SVGVNode[] = [];

if (bgColor && bgColor !== 'none') {
const { color, opacity } = normalizeColor(bgColor);
this._bgVNode = createVNode(
'rect',
'bg',
{
width: width,
height: height,
x: '0',
y: '0',
id: '0',
fill: color,
'fill-opacity': opacity
}
);
children.push(this._bgVNode);
}
else {
this._bgVNode = null;
}
const bgVNode = this._bgVNode = createBackgroundVNode(width, height, this._backgroundColor, scope);
bgVNode && children.push(bgVNode);

// Ignore the root g if wan't the output to be more tight.
const mainVNode = !opts.compress
Expand Down Expand Up @@ -201,16 +189,8 @@ class SVGPainter implements PainterBase {
}), { newline: true });
}

setBackgroundColor(backgroundColor: string) {
setBackgroundColor(backgroundColor: SVGPainterBackgroundColor) {
this._backgroundColor = backgroundColor;
const bgVNode = this._bgVNode;
if (bgVNode && bgVNode.elm) {
const { color, opacity } = normalizeColor(backgroundColor);
(bgVNode.elm as SVGElement).setAttribute('fill', color);
if (opacity < 1) {
(bgVNode.elm as SVGElement).setAttribute('fill-opacity', opacity as any);
}
}
}

getSvgRoot() {
Expand Down Expand Up @@ -302,11 +282,23 @@ class SVGPainter implements PainterBase {
viewportStyle.height = height + 'px';
}

const svgDom = this._svgDom;
if (svgDom) {
// Set width by 'svgRoot.width = width' is invalid
svgDom.setAttribute('width', width as any);
svgDom.setAttribute('height', height as any);
if (!isPattern(this._backgroundColor)) {
const svgDom = this._svgDom;
if (svgDom) {
// Set width by 'svgRoot.width = width' is invalid
svgDom.setAttribute('width', width as any);
svgDom.setAttribute('height', height as any);
}

const bgEl = this._bgVNode && this._bgVNode.elm as SVGElement;
if (bgEl) {
bgEl.setAttribute('width', width as any);
bgEl.setAttribute('height', height as any);
}
}
else {
// pattern backgroundColor requires a full refresh
this.refresh();
}
}
}
Expand Down Expand Up @@ -344,13 +336,13 @@ class SVGPainter implements PainterBase {
this._oldVNode = null;
}
toDataURL(base64?: boolean) {
let str = encodeURIComponent(this.renderToString());
let str = this.renderToString();
const prefix = 'data:image/svg+xml;';
if (base64) {
str = encodeBase64(str);
return str && prefix + 'base64,' + str;
}
return prefix + 'charset=UTF-8,' + str;
return prefix + 'charset=UTF-8,' + encodeURIComponent(str);
}

refreshHover = createMethodNotSupport('refreshHover') as PainterBase['refreshHover'];
Expand All @@ -367,5 +359,44 @@ function createMethodNotSupport(method: string): any {
};
}

function createBackgroundVNode(
width: number,
height: number,
backgroundColor: SVGPainterBackgroundColor,
scope: BrushScope
) {
let bgVNode;
if (backgroundColor && backgroundColor !== 'none') {
bgVNode = createVNode(
'rect',
'bg',
{
width,
height,
x: '0',
y: '0',
id: '0'
}
);
if (isGradient(backgroundColor)) {
setGradient({ fill: backgroundColor as any }, bgVNode.attrs, 'fill', scope);
}
else if (isPattern(backgroundColor)) {
setPattern({
style: {
fill: backgroundColor
},
dirty: noop,
getBoundingRect: () => ({ width, height })
} as any, bgVNode.attrs, 'fill', scope);
}
else {
const { color, opacity } = normalizeColor(backgroundColor);
bgVNode.attrs.fill = color;
opacity < 1 && (bgVNode.attrs.fillOpacity = opacity);
}
}
return bgVNode;
}

export default SVGPainter;
66 changes: 53 additions & 13 deletions src/svg/graphic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function createAttrsConvert(desc: ShapeMapDesc): ConvertShapeToAttr {
};
}

const buitinShapesDef: Record<string, [ConvertShapeToAttr, ShapeValidator?]> = {
const builtinShapesDef: Record<string, [ConvertShapeToAttr, ShapeValidator?]> = {
circle: [createAttrsConvert(['cx', 'cy', 'r'])],
polyline: [convertPolyShape, validatePolyShape],
polygon: [convertPolyShape, validatePolyShape]
Expand All @@ -149,7 +149,7 @@ function hasShapeAnimation(el: Displayable) {
export function brushSVGPath(el: Path, scope: BrushScope) {
const style = el.style;
const shape = el.shape;
const builtinShpDef = buitinShapesDef[el.type];
const builtinShpDef = builtinShapesDef[el.type];
const attrs: SVGVNodeAttrs = {};
const needsAnimate = scope.animation;
let svgElType = 'path';
Expand Down Expand Up @@ -266,8 +266,8 @@ export function brushSVGTSpan(el: TSpan, scope: BrushScope) {
// style.font has been normalized by `normalizeTextStyle`.
const font = style.font || DEFAULT_FONT;

// Consider different font display differently in vertial align, we always
// set vertialAlign as 'middle', and use 'y' to locate text vertically.
// Consider different font display differently in vertical align, we always
// set verticalAlign as 'middle', and use 'y' to locate text vertically.
const x = style.x || 0;
const y = adjustTextY(style.y || 0, getLineHeight(font), style.textBaseline);
const textAlign = TEXT_ALIGN_TO_ANCHOR[style.textAlign as keyof typeof TEXT_ALIGN_TO_ANCHOR]
Expand Down Expand Up @@ -388,7 +388,7 @@ function setShadow(
}
}

function setGradient(
export function setGradient(
style: PathStyleProps,
attrs: SVGVNodeAttrs,
target: 'fill' | 'stroke',
Expand Down Expand Up @@ -467,16 +467,19 @@ function setGradient(
attrs[target] = getIdURL(gradientId);
}

function setPattern(
export function setPattern(
el: Displayable,
attrs: SVGVNodeAttrs,
target: 'fill' | 'stroke',
scope: BrushScope
) {
const val = el.style[target] as ImagePatternObject | SVGPatternObject;
const patternAttrs: SVGVNodeAttrs = {
'patternUnits': 'userSpaceOnUse'
};
const boundingRect = el.getBoundingRect();
const patternAttrs: SVGVNodeAttrs = {};
const repeat = (val as ImagePatternObject).repeat;
const noRepeat = repeat === 'no-repeat';
const repeatX = repeat === 'repeat-x';
const repeatY = repeat === 'repeat-y';
let child: SVGVNode;
if (isImagePattern(val)) {
let imageWidth = val.imageWidth;
Expand All @@ -503,8 +506,20 @@ function setPattern(
const setSizeToVNode = (vNode: SVGVNode, img: ImageLike) => {
if (vNode) {
const svgEl = vNode.elm as SVGElement;
const width = (vNode.attrs.width = imageWidth || img.width);
const height = (vNode.attrs.height = imageHeight || img.height);
let width = imageWidth || img.width;
let height = imageHeight || img.height;
if (vNode.tag === 'pattern') {
if (repeatX) {
height = 1;
width /= boundingRect.width;
}
else if (repeatY) {
width = 1;
height /= boundingRect.height;
}
}
vNode.attrs.width = width;
vNode.attrs.height = height;
if (svgEl) {
svgEl.setAttribute('width', width as any);
svgEl.setAttribute('height', height as any);
Expand All @@ -513,7 +528,7 @@ function setPattern(
};
const createdImage = createOrUpdateImage(
imageSrc, null, el, (img) => {
setSizeToVNode(patternVNode, img);
noRepeat || setSizeToVNode(patternVNode, img);
setSizeToVNode(child, img);
}
);
Expand Down Expand Up @@ -546,7 +561,32 @@ function setPattern(
return;
}

patternAttrs.patternTransform = getSRTTransformString(val);
let patternWidth;
let patternHeight;
if (noRepeat) {
patternWidth = patternHeight = 1;
}
else if (repeatX) {
patternHeight = 1;
patternWidth = (patternAttrs.width as number) / boundingRect.width;
}
else if (repeatY) {
patternWidth = 1;
patternHeight = (patternAttrs.height as number) / boundingRect.height;
}
else {
patternAttrs.patternUnits = 'userSpaceOnUse';
}

if (patternWidth != null && !isNaN(patternWidth)) {
patternAttrs.width = patternWidth;
}
if (patternHeight != null && !isNaN(patternHeight)) {
patternAttrs.height = patternHeight;
}

const patternTransform = getSRTTransformString(val);
patternTransform && (patternAttrs.patternTransform = patternTransform);

// Use the whole html as cache key.
let patternVNode = createVNode(
Expand Down
2 changes: 1 addition & 1 deletion src/svg/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export function getSRTTransformString(
export const encodeBase64 = (function () {
if (env.hasGlobalWindow && isFunction(window.btoa)) {
return function (str: string) {
return window.btoa(unescape(str));
return window.btoa(unescape(encodeURIComponent(str)));
};
}
if (typeof Buffer !== 'undefined') {
Expand Down
2 changes: 1 addition & 1 deletion test/-cases.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>echarts cases</title>
<title>zrender cases</title>
<script src="lib/testHelper.js"></script>
<script src="lib/caseFrame.js"></script>
<link rel="stylesheet" href="lib/caseFrame.css">
Expand Down
Loading