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