UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

410 lines (352 loc) 12.2 kB
/** * 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; 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;