/** * SVG Painter */ import {createElement, SVGNS, XLINKNS, XMLNS} from '../svg/core'; import { normalizeColor } from '../svg/helper'; import * as util from '../core/util'; import Path from '../graphic/Path'; import ZRImage from '../graphic/Image'; import TSpan from '../graphic/TSpan'; import arrayDiff from '../core/arrayDiff'; import GradientManager from './helper/GradientManager'; import PatternManager from './helper/PatternManager'; import ClippathManager, {hasClipPath} from './helper/ClippathManager'; import ShadowManager from './helper/ShadowManager'; import { path as svgPath, image as svgImage, text as svgText, SVGProxy } from './graphic'; import Displayable from '../graphic/Displayable'; import Storage from '../Storage'; import { PainterBase } from '../PainterBase'; import { getSize } from '../canvas/helper'; function getSvgProxy(el: Displayable) { if (el instanceof Path) { return svgPath; } else if (el instanceof ZRImage) { return svgImage; } else if (el instanceof TSpan) { return svgText; } else { return svgPath; } } function checkParentAvailable(parent: SVGElement, child: SVGElement) { return child && parent && child.parentNode !== parent; } function insertAfter(parent: SVGElement, child: SVGElement, prevSibling: SVGElement) { if (checkParentAvailable(parent, child) && prevSibling) { const nextSibling = prevSibling.nextSibling; nextSibling ? parent.insertBefore(child, nextSibling) : parent.appendChild(child); } } function prepend(parent: SVGElement, child: SVGElement) { if (checkParentAvailable(parent, child)) { const firstChild = parent.firstChild; firstChild ? parent.insertBefore(child, firstChild) : parent.appendChild(child); } } function remove(parent: SVGElement, child: SVGElement) { if (child && parent && child.parentNode === parent) { parent.removeChild(child); } } function removeFromMyParent(child: SVGElement) { if (child && child.parentNode) { child.parentNode.removeChild(child); } } function getSvgElement(displayable: Displayable) { return displayable.__svgEl; } interface SVGPainterOption { width?: number | string height?: number | string } class SVGPainter implements PainterBase { type = 'svg' root: HTMLElement storage: Storage private _opts: SVGPainterOption private _svgDom: SVGElement private _svgRoot: SVGGElement private _backgroundRoot: SVGGElement private _backgroundNode: SVGRectElement private _gradientManager: GradientManager private _patternManager: PatternManager private _clipPathManager: ClippathManager private _shadowManager: ShadowManager private _viewport: HTMLDivElement private _visibleList: Displayable[] private _width: number private _height: number constructor(root: HTMLElement, storage: Storage, opts: SVGPainterOption, zrId: number) { this.root = root; this.storage = storage; this._opts = opts = util.extend({}, opts || {}); const svgDom = createElement('svg'); svgDom.setAttributeNS(XMLNS, 'xmlns', SVGNS); svgDom.setAttributeNS(XMLNS, 'xmlns:xlink', XLINKNS); svgDom.setAttribute('version', '1.1'); svgDom.setAttribute('baseProfile', 'full'); svgDom.style.cssText = 'user-select:none;position:absolute;left:0;top:0;'; const bgRoot = createElement('g') as SVGGElement; svgDom.appendChild(bgRoot); const svgRoot = createElement('g') as SVGGElement; svgDom.appendChild(svgRoot); this._gradientManager = new GradientManager(zrId, svgRoot); this._patternManager = new PatternManager(zrId, svgRoot); this._clipPathManager = new ClippathManager(zrId, svgRoot); this._shadowManager = new ShadowManager(zrId, svgRoot); const viewport = document.createElement('div'); viewport.style.cssText = 'overflow:hidden;position:relative'; this._svgDom = svgDom; this._svgRoot = svgRoot; this._backgroundRoot = bgRoot; this._viewport = viewport; root.appendChild(viewport); viewport.appendChild(svgDom); this.resize(opts.width, opts.height); this._visibleList = []; } getType() { return 'svg'; } getViewportRoot() { return this._viewport; } getSvgDom() { return this._svgDom; } getSvgRoot() { return this._svgRoot; } getViewportRootOffset() { const viewportRoot = this.getViewportRoot(); if (viewportRoot) { return { offsetLeft: viewportRoot.offsetLeft || 0, offsetTop: viewportRoot.offsetTop || 0 }; } } refresh() { const list = this.storage.getDisplayList(true); this._paintList(list); } setBackgroundColor(backgroundColor: string) { // TODO gradient // Insert a bg rect instead of setting background to viewport. // Otherwise, the exported SVG don't have background. if (this._backgroundRoot && this._backgroundNode) { this._backgroundRoot.removeChild(this._backgroundNode); } const bgNode = createElement('rect') as SVGRectElement; bgNode.setAttribute('width', this.getWidth() as any); bgNode.setAttribute('height', this.getHeight() as any); bgNode.setAttribute('x', 0 as any); bgNode.setAttribute('y', 0 as any); bgNode.setAttribute('id', 0 as any); const { color, opacity } = normalizeColor(backgroundColor); bgNode.setAttribute('fill', color); bgNode.setAttribute('fill-opacity', opacity as any); this._backgroundRoot.appendChild(bgNode); this._backgroundNode = bgNode; } createSVGElement(tag: string): SVGElement { return createElement(tag); } paintOne(el: Displayable): SVGElement { const svgProxy = getSvgProxy(el); svgProxy && (svgProxy as SVGProxy).brush(el); return getSvgElement(el); } _paintList(list: Displayable[]) { const gradientManager = this._gradientManager; const patternManager = this._patternManager; const clipPathManager = this._clipPathManager; const shadowManager = this._shadowManager; gradientManager.markAllUnused(); patternManager.markAllUnused(); clipPathManager.markAllUnused(); shadowManager.markAllUnused(); const svgRoot = this._svgRoot; const visibleList = this._visibleList; const listLen = list.length; const newVisibleList = []; for (let i = 0; i < listLen; i++) { const displayable = list[i]; const svgProxy = getSvgProxy(displayable); let svgElement = getSvgElement(displayable); if (!displayable.invisible) { if (displayable.__dirty || !svgElement) { svgProxy && (svgProxy as SVGProxy).brush(displayable); svgElement = getSvgElement(displayable); // Update gradient and shadow if (svgElement && displayable.style) { gradientManager.update(displayable.style.fill); gradientManager.update(displayable.style.stroke); patternManager.update(displayable.style.fill); patternManager.update(displayable.style.stroke); shadowManager.update(svgElement, displayable); } displayable.__dirty = 0; } // May have optimizations and ignore brush(like empty string in TSpan) if (svgElement) { newVisibleList.push(displayable); } } } const diff = arrayDiff(visibleList, newVisibleList); let prevSvgElement; let topPrevSvgElement; // NOTE: First do remove, in case element moved to the head and do remove // after add for (let i = 0; i < diff.length; i++) { const item = diff[i]; if (item.removed) { for (let k = 0; k < item.count; k++) { const displayable = visibleList[item.indices[k]]; const svgElement = getSvgElement(displayable); hasClipPath(displayable) ? removeFromMyParent(svgElement) : remove(svgRoot, svgElement); } } } let prevDisplayable; let currentClipGroup; for (let i = 0; i < diff.length; i++) { const item = diff[i]; // const isAdd = item.added; if (item.removed) { continue; } for (let k = 0; k < item.count; k++) { const displayable = newVisibleList[item.indices[k]]; // Update clipPath const clipGroup = clipPathManager.update(displayable, prevDisplayable); if (clipGroup !== currentClipGroup) { // First pop to top level. prevSvgElement = topPrevSvgElement; if (clipGroup) { // Enter second level of clipping group. prevSvgElement ? insertAfter(svgRoot, clipGroup, prevSvgElement) : prepend(svgRoot, clipGroup); topPrevSvgElement = clipGroup; // Reset prevSvgElement in second level. prevSvgElement = null; } currentClipGroup = clipGroup; } const svgElement = getSvgElement(displayable); // if (isAdd) { prevSvgElement ? insertAfter(currentClipGroup || svgRoot, svgElement, prevSvgElement) : prepend(currentClipGroup || svgRoot, svgElement); // } prevSvgElement = svgElement || prevSvgElement; if (!currentClipGroup) { topPrevSvgElement = prevSvgElement; } gradientManager.markUsed(displayable); gradientManager.addWithoutUpdate(svgElement, displayable); patternManager.markUsed(displayable); patternManager.addWithoutUpdate(svgElement, displayable); clipPathManager.markUsed(displayable); prevDisplayable = displayable; } } gradientManager.removeUnused(); patternManager.removeUnused(); clipPathManager.removeUnused(); shadowManager.removeUnused(); this._visibleList = newVisibleList; } resize(width: number | string, height: number | string) { const viewport = this._viewport; // FIXME Why ? viewport.style.display = 'none'; // Save input w/h const opts = this._opts; width != null && (opts.width = width); height != null && (opts.height = height); width = getSize(this.root, 0, opts); height = getSize(this.root, 1, opts); viewport.style.display = ''; if (this._width !== width || this._height !== height) { this._width = width; this._height = height; const viewportStyle = viewport.style; viewportStyle.width = width + 'px'; viewportStyle.height = height + 'px'; const svgRoot = this._svgDom; // Set width by 'svgRoot.width = width' is invalid svgRoot.setAttribute('width', width + ''); svgRoot.setAttribute('height', height + ''); } if (this._backgroundNode) { this._backgroundNode.setAttribute('width', width as any); this._backgroundNode.setAttribute('height', height as any); } } /** * 获取绘图区域宽度 */ getWidth() { return this._width; } /** * 获取绘图区域高度 */ getHeight() { return this._height; } dispose() { this.root.innerHTML = ''; this._svgRoot = this._backgroundRoot = this._svgDom = this._backgroundNode = this._viewport = this.storage = null; } clear() { const viewportNode = this._viewport; if (viewportNode && viewportNode.parentNode) { viewportNode.parentNode.removeChild(viewportNode); } } toDataURL() { this.refresh(); const svgDom = this._svgDom; const outerHTML = svgDom.outerHTML // outerHTML of `svg` tag is not supported in IE, use `parentNode.innerHTML` instead // PENDING: Or use `new XMLSerializer().serializeToString(svg)`? || (svgDom.parentNode && (svgDom.parentNode as HTMLElement).innerHTML); const html = encodeURIComponent(outerHTML.replace(/>\n\r<')); return 'data:image/svg+xml;charset=UTF-8,' + html; } 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') { util.logError('In SVG mode painter not support method "' + method + '"'); } }; } export default SVGPainter;