/**
|
* SVG Painter
|
*/
|
|
import {
|
brush, setClipPath
|
} from './graphic';
|
import Displayable from '../graphic/Displayable';
|
import Storage from '../Storage';
|
import { PainterBase } from '../PainterBase';
|
import {
|
createElement,
|
createVNode,
|
vNodeToString,
|
SVGVNodeAttrs,
|
SVGVNode,
|
getCssString,
|
BrushScope,
|
createBrushScope,
|
createSVGVNode
|
} from './core';
|
import { normalizeColor, encodeBase64 } from './helper';
|
import { extend, keys, logError, map, retrieve2 } from '../core/util';
|
import Path from '../graphic/Path';
|
import patch, { updateAttrs } from './patch';
|
import { getSize } from '../canvas/helper';
|
|
let svgId = 0;
|
|
interface SVGPainterOption {
|
width?: number
|
height?: number
|
ssr?: boolean
|
}
|
|
class SVGPainter implements PainterBase {
|
|
type = 'svg'
|
|
storage: Storage
|
|
root: HTMLElement
|
|
private _svgDom: SVGElement
|
private _viewport: HTMLElement
|
|
private _opts: SVGPainterOption
|
|
private _oldVNode: SVGVNode
|
private _bgVNode: SVGVNode
|
private _mainVNode: SVGVNode
|
|
private _width: number
|
private _height: number
|
|
private _backgroundColor: string
|
|
private _id: string
|
|
constructor(root: HTMLElement, storage: Storage, opts: SVGPainterOption) {
|
this.storage = storage;
|
this._opts = opts = extend({}, opts);
|
|
this.root = root;
|
// A unique id for generating svg ids.
|
this._id = 'zr' + svgId++;
|
|
this._oldVNode = createSVGVNode(opts.width, opts.height);
|
|
if (root && !opts.ssr) {
|
const viewport = this._viewport = document.createElement('div');
|
viewport.style.cssText = 'position:relative;overflow:hidden';
|
const svgDom = this._svgDom = this._oldVNode.elm = createElement('svg');
|
updateAttrs(null, this._oldVNode);
|
viewport.appendChild(svgDom);
|
root.appendChild(viewport);
|
}
|
|
this.resize(opts.width, opts.height);
|
}
|
|
getType() {
|
return this.type;
|
}
|
|
getViewportRoot() {
|
return this._viewport;
|
}
|
getViewportRootOffset() {
|
const viewportRoot = this.getViewportRoot();
|
if (viewportRoot) {
|
return {
|
offsetLeft: viewportRoot.offsetLeft || 0,
|
offsetTop: viewportRoot.offsetTop || 0
|
};
|
}
|
}
|
|
getSvgDom() {
|
return this._svgDom;
|
}
|
|
refresh() {
|
if (this.root) {
|
const vnode = this.renderToVNode({
|
willUpdate: true
|
});
|
// Disable user selection.
|
vnode.attrs.style = 'position:absolute;left:0;top:0;user-select:none';
|
patch(this._oldVNode, vnode);
|
this._oldVNode = vnode;
|
}
|
}
|
|
renderOneToVNode(el: Displayable) {
|
return brush(el, createBrushScope(this._id));
|
}
|
|
renderToVNode(opts?: {
|
animation?: boolean
|
willUpdate?: boolean
|
compress?: boolean,
|
useViewBox?: boolean
|
}) {
|
|
opts = opts || {};
|
|
const list = this.storage.getDisplayList(true);
|
const bgColor = this._backgroundColor;
|
const width = this._width;
|
const height = this._height;
|
|
const scope = createBrushScope(this._id);
|
scope.animation = opts.animation;
|
scope.willUpdate = opts.willUpdate;
|
scope.compress = opts.compress;
|
|
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;
|
}
|
|
// Ignore the root g if wan't the output to be more tight.
|
const mainVNode = !opts.compress
|
? (this._mainVNode = createVNode('g', 'main', {}, [])) : null;
|
this._paintList(list, scope, mainVNode ? mainVNode.children : children);
|
mainVNode && children.push(mainVNode);
|
|
const defs = map(keys(scope.defs), (id) => scope.defs[id]);
|
if (defs.length) {
|
children.push(createVNode('defs', 'defs', {}, defs));
|
}
|
|
if (opts.animation) {
|
const animationCssStr = getCssString(scope.cssNodes, scope.cssAnims, { newline: true });
|
if (animationCssStr) {
|
const styleNode = createVNode('style', 'stl', {}, [], animationCssStr);
|
children.push(styleNode);
|
}
|
}
|
|
return createSVGVNode(width, height, children, opts.useViewBox);
|
}
|
|
renderToString(opts?: {
|
/**
|
* If add css animation.
|
* @default true
|
*/
|
cssAnimation?: boolean
|
/**
|
* If use viewBox
|
* @default true
|
*/
|
useViewBox?: boolean
|
}) {
|
opts = opts || {};
|
return vNodeToString(this.renderToVNode({
|
animation: retrieve2(opts.cssAnimation, true),
|
willUpdate: false,
|
compress: true,
|
useViewBox: retrieve2(opts.useViewBox, true)
|
}), { newline: true });
|
}
|
|
setBackgroundColor(backgroundColor: string) {
|
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() {
|
return this._mainVNode && this._mainVNode.elm as SVGElement;
|
}
|
|
_paintList(list: Displayable[], scope: BrushScope, out?: SVGVNode[]) {
|
const listLen = list.length;
|
|
const clipPathsGroupsStack: SVGVNode[] = [];
|
let clipPathsGroupsStackDepth = 0;
|
let currentClipPathGroup;
|
let prevClipPaths: Path[];
|
let clipGroupNodeIdx = 0;
|
for (let i = 0; i < listLen; i++) {
|
const displayable = list[i];
|
if (!displayable.invisible) {
|
const clipPaths = displayable.__clipPaths;
|
const len = clipPaths && clipPaths.length || 0;
|
const prevLen = prevClipPaths && prevClipPaths.length || 0;
|
let lca;
|
// Find the lowest common ancestor
|
for (lca = Math.max(len - 1, prevLen - 1); lca >= 0; lca--) {
|
if (clipPaths && prevClipPaths
|
&& clipPaths[lca] === prevClipPaths[lca]
|
) {
|
break;
|
}
|
}
|
// pop the stack
|
for (let i = prevLen - 1; i > lca; i--) {
|
clipPathsGroupsStackDepth--;
|
// svgEls.push(closeGroup);
|
currentClipPathGroup = clipPathsGroupsStack[clipPathsGroupsStackDepth - 1];
|
}
|
// Pop clip path group for clipPaths not match the previous.
|
for (let i = lca + 1; i < len; i++) {
|
const groupAttrs: SVGVNodeAttrs = {};
|
setClipPath(
|
clipPaths[i],
|
groupAttrs,
|
scope
|
);
|
const g = createVNode(
|
'g',
|
'clip-g-' + clipGroupNodeIdx++,
|
groupAttrs,
|
[]
|
);
|
(currentClipPathGroup ? currentClipPathGroup.children : out).push(g);
|
clipPathsGroupsStack[clipPathsGroupsStackDepth++] = g;
|
currentClipPathGroup = g;
|
}
|
prevClipPaths = clipPaths;
|
|
const ret = brush(displayable, scope);
|
if (ret) {
|
(currentClipPathGroup ? currentClipPathGroup.children : out).push(ret);
|
}
|
}
|
}
|
}
|
|
resize(width: number, height: number) {
|
// Save input w/h
|
const opts = this._opts;
|
const root = this.root;
|
const viewport = this._viewport;
|
width != null && (opts.width = width);
|
height != null && (opts.height = height);
|
|
if (root && viewport) {
|
// FIXME Why ?
|
viewport.style.display = 'none';
|
|
width = getSize(root, 0, opts);
|
height = getSize(root, 1, opts);
|
|
viewport.style.display = '';
|
}
|
|
if (this._width !== width || this._height !== height) {
|
this._width = width;
|
this._height = height;
|
|
if (viewport) {
|
const viewportStyle = viewport.style;
|
viewportStyle.width = width + 'px';
|
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);
|
}
|
}
|
}
|
|
/**
|
* 获取绘图区域宽度
|
*/
|
getWidth() {
|
return this._width;
|
}
|
|
/**
|
* 获取绘图区域高度
|
*/
|
getHeight() {
|
return this._height;
|
}
|
|
dispose() {
|
if (this.root) {
|
this.root.innerHTML = '';
|
}
|
|
this._svgDom =
|
this._viewport =
|
this.storage =
|
this._oldVNode =
|
this._bgVNode =
|
this._mainVNode = null;
|
}
|
clear() {
|
if (this._svgDom) {
|
this._svgDom.innerHTML = null;
|
}
|
this._oldVNode = null;
|
}
|
toDataURL(base64?: boolean) {
|
let str = encodeURIComponent(this.renderToString());
|
const prefix = 'data:image/svg+xml;';
|
if (base64) {
|
str = encodeBase64(str);
|
return str && prefix + 'base64,' + str;
|
}
|
return prefix + 'charset=UTF-8,' + str;
|
}
|
|
refreshHover = createMethodNotSupport('refreshHover') as PainterBase['refreshHover'];
|
configLayer = createMethodNotSupport('configLayer') as PainterBase['configLayer'];
|
}
|
|
|
// Not supported methods
|
function createMethodNotSupport(method: string): any {
|
return function () {
|
if (process.env.NODE_ENV !== 'production') {
|
logError('In SVG mode painter not support method "' + method + '"');
|
}
|
};
|
}
|
|
|
export default SVGPainter;
|