| /** | 
|  * 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; |