package.src.svg.Painter.ts Maven / Gradle / Ivy
/**
* SVG Painter
*/
import {
brush,
setClipPath,
setGradient,
setPattern
} 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, 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;
interface SVGPainterOption {
width?: number
height?: number
ssr?: boolean
}
type SVGPainterBackgroundColor = string | GradientObject | PatternObject;
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: SVGPainterBackgroundColor
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,
emphasis?: boolean
}) {
opts = opts || {};
const list = this.storage.getDisplayList(true);
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;
scope.emphasis = opts.emphasis;
scope.ssr = this._opts.ssr;
const children: SVGVNode[] = [];
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
? (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 add css emphasis.
* @default true
*/
cssEmphasis?: boolean,
/**
* If use viewBox
* @default true
*/
useViewBox?: boolean
}) {
opts = opts || {};
return vNodeToString(this.renderToVNode({
animation: retrieve2(opts.cssAnimation, true),
emphasis: retrieve2(opts.cssEmphasis, true),
willUpdate: false,
compress: true,
useViewBox: retrieve2(opts.useViewBox, true)
}), { newline: true });
}
setBackgroundColor(backgroundColor: SVGPainterBackgroundColor) {
this._backgroundColor = backgroundColor;
}
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';
}
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();
}
}
}
/**
* 获取绘图区域宽度
*/
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 = this.renderToString();
const prefix = 'data:image/svg+xml;';
if (base64) {
str = encodeBase64(str);
return str && prefix + 'base64,' + str;
}
return prefix + 'charset=UTF-8,' + encodeURIComponent(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 + '"');
}
};
}
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'
}
);
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['fill-opacity'] = opacity);
}
}
return bgVNode;
}
export default SVGPainter;
© 2015 - 2025 Weber Informatics LLC | Privacy Policy