Skip to content

Commit

Permalink
Merge pull request #959 from ecomfe/svg-background
Browse files Browse the repository at this point in the history
feat(svg): support gradients and patterns for background color in SVG renderer
  • Loading branch information
plainheart authored Oct 13, 2022
2 parents d66be23 + 2fe9601 commit 2ebc6fc
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 57 deletions.
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

0 comments on commit 2ebc6fc

Please sign in to comment.