123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- /**
- * 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<Displayable>).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<Displayable>).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(/></g, '>\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;
|