Painter.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. /**
  2. * SVG Painter
  3. */
  4. import {createElement, SVGNS, XLINKNS, XMLNS} from '../svg/core';
  5. import { normalizeColor } from '../svg/helper';
  6. import * as util from '../core/util';
  7. import Path from '../graphic/Path';
  8. import ZRImage from '../graphic/Image';
  9. import TSpan from '../graphic/TSpan';
  10. import arrayDiff from '../core/arrayDiff';
  11. import GradientManager from './helper/GradientManager';
  12. import PatternManager from './helper/PatternManager';
  13. import ClippathManager, {hasClipPath} from './helper/ClippathManager';
  14. import ShadowManager from './helper/ShadowManager';
  15. import {
  16. path as svgPath,
  17. image as svgImage,
  18. text as svgText,
  19. SVGProxy
  20. } from './graphic';
  21. import Displayable from '../graphic/Displayable';
  22. import Storage from '../Storage';
  23. import { PainterBase } from '../PainterBase';
  24. import { getSize } from '../canvas/helper';
  25. function getSvgProxy(el: Displayable) {
  26. if (el instanceof Path) {
  27. return svgPath;
  28. }
  29. else if (el instanceof ZRImage) {
  30. return svgImage;
  31. }
  32. else if (el instanceof TSpan) {
  33. return svgText;
  34. }
  35. else {
  36. return svgPath;
  37. }
  38. }
  39. function checkParentAvailable(parent: SVGElement, child: SVGElement) {
  40. return child && parent && child.parentNode !== parent;
  41. }
  42. function insertAfter(parent: SVGElement, child: SVGElement, prevSibling: SVGElement) {
  43. if (checkParentAvailable(parent, child) && prevSibling) {
  44. const nextSibling = prevSibling.nextSibling;
  45. nextSibling ? parent.insertBefore(child, nextSibling)
  46. : parent.appendChild(child);
  47. }
  48. }
  49. function prepend(parent: SVGElement, child: SVGElement) {
  50. if (checkParentAvailable(parent, child)) {
  51. const firstChild = parent.firstChild;
  52. firstChild ? parent.insertBefore(child, firstChild)
  53. : parent.appendChild(child);
  54. }
  55. }
  56. function remove(parent: SVGElement, child: SVGElement) {
  57. if (child && parent && child.parentNode === parent) {
  58. parent.removeChild(child);
  59. }
  60. }
  61. function removeFromMyParent(child: SVGElement) {
  62. if (child && child.parentNode) {
  63. child.parentNode.removeChild(child);
  64. }
  65. }
  66. function getSvgElement(displayable: Displayable) {
  67. return displayable.__svgEl;
  68. }
  69. interface SVGPainterOption {
  70. width?: number | string
  71. height?: number | string
  72. }
  73. class SVGPainter implements PainterBase {
  74. type = 'svg'
  75. root: HTMLElement
  76. storage: Storage
  77. private _opts: SVGPainterOption
  78. private _svgDom: SVGElement
  79. private _svgRoot: SVGGElement
  80. private _backgroundRoot: SVGGElement
  81. private _backgroundNode: SVGRectElement
  82. private _gradientManager: GradientManager
  83. private _patternManager: PatternManager
  84. private _clipPathManager: ClippathManager
  85. private _shadowManager: ShadowManager
  86. private _viewport: HTMLDivElement
  87. private _visibleList: Displayable[]
  88. private _width: number
  89. private _height: number
  90. constructor(root: HTMLElement, storage: Storage, opts: SVGPainterOption, zrId: number) {
  91. this.root = root;
  92. this.storage = storage;
  93. this._opts = opts = util.extend({}, opts || {});
  94. const svgDom = createElement('svg');
  95. svgDom.setAttributeNS(XMLNS, 'xmlns', SVGNS);
  96. svgDom.setAttributeNS(XMLNS, 'xmlns:xlink', XLINKNS);
  97. svgDom.setAttribute('version', '1.1');
  98. svgDom.setAttribute('baseProfile', 'full');
  99. svgDom.style.cssText = 'user-select:none;position:absolute;left:0;top:0;';
  100. const bgRoot = createElement('g') as SVGGElement;
  101. svgDom.appendChild(bgRoot);
  102. const svgRoot = createElement('g') as SVGGElement;
  103. svgDom.appendChild(svgRoot);
  104. this._gradientManager = new GradientManager(zrId, svgRoot);
  105. this._patternManager = new PatternManager(zrId, svgRoot);
  106. this._clipPathManager = new ClippathManager(zrId, svgRoot);
  107. this._shadowManager = new ShadowManager(zrId, svgRoot);
  108. const viewport = document.createElement('div');
  109. viewport.style.cssText = 'overflow:hidden;position:relative';
  110. this._svgDom = svgDom;
  111. this._svgRoot = svgRoot;
  112. this._backgroundRoot = bgRoot;
  113. this._viewport = viewport;
  114. root.appendChild(viewport);
  115. viewport.appendChild(svgDom);
  116. this.resize(opts.width, opts.height);
  117. this._visibleList = [];
  118. }
  119. getType() {
  120. return 'svg';
  121. }
  122. getViewportRoot() {
  123. return this._viewport;
  124. }
  125. getSvgDom() {
  126. return this._svgDom;
  127. }
  128. getSvgRoot() {
  129. return this._svgRoot;
  130. }
  131. getViewportRootOffset() {
  132. const viewportRoot = this.getViewportRoot();
  133. if (viewportRoot) {
  134. return {
  135. offsetLeft: viewportRoot.offsetLeft || 0,
  136. offsetTop: viewportRoot.offsetTop || 0
  137. };
  138. }
  139. }
  140. refresh() {
  141. const list = this.storage.getDisplayList(true);
  142. this._paintList(list);
  143. }
  144. setBackgroundColor(backgroundColor: string) {
  145. // TODO gradient
  146. // Insert a bg rect instead of setting background to viewport.
  147. // Otherwise, the exported SVG don't have background.
  148. if (this._backgroundRoot && this._backgroundNode) {
  149. this._backgroundRoot.removeChild(this._backgroundNode);
  150. }
  151. const bgNode = createElement('rect') as SVGRectElement;
  152. bgNode.setAttribute('width', this.getWidth() as any);
  153. bgNode.setAttribute('height', this.getHeight() as any);
  154. bgNode.setAttribute('x', 0 as any);
  155. bgNode.setAttribute('y', 0 as any);
  156. bgNode.setAttribute('id', 0 as any);
  157. const { color, opacity } = normalizeColor(backgroundColor);
  158. bgNode.setAttribute('fill', color);
  159. bgNode.setAttribute('fill-opacity', opacity as any);
  160. this._backgroundRoot.appendChild(bgNode);
  161. this._backgroundNode = bgNode;
  162. }
  163. createSVGElement(tag: string): SVGElement {
  164. return createElement(tag);
  165. }
  166. paintOne(el: Displayable): SVGElement {
  167. const svgProxy = getSvgProxy(el);
  168. svgProxy && (svgProxy as SVGProxy<Displayable>).brush(el);
  169. return getSvgElement(el);
  170. }
  171. _paintList(list: Displayable[]) {
  172. const gradientManager = this._gradientManager;
  173. const patternManager = this._patternManager;
  174. const clipPathManager = this._clipPathManager;
  175. const shadowManager = this._shadowManager;
  176. gradientManager.markAllUnused();
  177. patternManager.markAllUnused();
  178. clipPathManager.markAllUnused();
  179. shadowManager.markAllUnused();
  180. const svgRoot = this._svgRoot;
  181. const visibleList = this._visibleList;
  182. const listLen = list.length;
  183. const newVisibleList = [];
  184. for (let i = 0; i < listLen; i++) {
  185. const displayable = list[i];
  186. const svgProxy = getSvgProxy(displayable);
  187. let svgElement = getSvgElement(displayable);
  188. if (!displayable.invisible) {
  189. if (displayable.__dirty || !svgElement) {
  190. svgProxy && (svgProxy as SVGProxy<Displayable>).brush(displayable);
  191. svgElement = getSvgElement(displayable);
  192. // Update gradient and shadow
  193. if (svgElement && displayable.style) {
  194. gradientManager.update(displayable.style.fill);
  195. gradientManager.update(displayable.style.stroke);
  196. patternManager.update(displayable.style.fill);
  197. patternManager.update(displayable.style.stroke);
  198. shadowManager.update(svgElement, displayable);
  199. }
  200. displayable.__dirty = 0;
  201. }
  202. // May have optimizations and ignore brush(like empty string in TSpan)
  203. if (svgElement) {
  204. newVisibleList.push(displayable);
  205. }
  206. }
  207. }
  208. const diff = arrayDiff(visibleList, newVisibleList);
  209. let prevSvgElement;
  210. let topPrevSvgElement;
  211. // NOTE: First do remove, in case element moved to the head and do remove
  212. // after add
  213. for (let i = 0; i < diff.length; i++) {
  214. const item = diff[i];
  215. if (item.removed) {
  216. for (let k = 0; k < item.count; k++) {
  217. const displayable = visibleList[item.indices[k]];
  218. const svgElement = getSvgElement(displayable);
  219. hasClipPath(displayable) ? removeFromMyParent(svgElement)
  220. : remove(svgRoot, svgElement);
  221. }
  222. }
  223. }
  224. let prevDisplayable;
  225. let currentClipGroup;
  226. for (let i = 0; i < diff.length; i++) {
  227. const item = diff[i];
  228. // const isAdd = item.added;
  229. if (item.removed) {
  230. continue;
  231. }
  232. for (let k = 0; k < item.count; k++) {
  233. const displayable = newVisibleList[item.indices[k]];
  234. // Update clipPath
  235. const clipGroup = clipPathManager.update(displayable, prevDisplayable);
  236. if (clipGroup !== currentClipGroup) {
  237. // First pop to top level.
  238. prevSvgElement = topPrevSvgElement;
  239. if (clipGroup) {
  240. // Enter second level of clipping group.
  241. prevSvgElement ? insertAfter(svgRoot, clipGroup, prevSvgElement)
  242. : prepend(svgRoot, clipGroup);
  243. topPrevSvgElement = clipGroup;
  244. // Reset prevSvgElement in second level.
  245. prevSvgElement = null;
  246. }
  247. currentClipGroup = clipGroup;
  248. }
  249. const svgElement = getSvgElement(displayable);
  250. // if (isAdd) {
  251. prevSvgElement
  252. ? insertAfter(currentClipGroup || svgRoot, svgElement, prevSvgElement)
  253. : prepend(currentClipGroup || svgRoot, svgElement);
  254. // }
  255. prevSvgElement = svgElement || prevSvgElement;
  256. if (!currentClipGroup) {
  257. topPrevSvgElement = prevSvgElement;
  258. }
  259. gradientManager.markUsed(displayable);
  260. gradientManager.addWithoutUpdate(svgElement, displayable);
  261. patternManager.markUsed(displayable);
  262. patternManager.addWithoutUpdate(svgElement, displayable);
  263. clipPathManager.markUsed(displayable);
  264. prevDisplayable = displayable;
  265. }
  266. }
  267. gradientManager.removeUnused();
  268. patternManager.removeUnused();
  269. clipPathManager.removeUnused();
  270. shadowManager.removeUnused();
  271. this._visibleList = newVisibleList;
  272. }
  273. resize(width: number | string, height: number | string) {
  274. const viewport = this._viewport;
  275. // FIXME Why ?
  276. viewport.style.display = 'none';
  277. // Save input w/h
  278. const opts = this._opts;
  279. width != null && (opts.width = width);
  280. height != null && (opts.height = height);
  281. width = getSize(this.root, 0, opts);
  282. height = getSize(this.root, 1, opts);
  283. viewport.style.display = '';
  284. if (this._width !== width || this._height !== height) {
  285. this._width = width;
  286. this._height = height;
  287. const viewportStyle = viewport.style;
  288. viewportStyle.width = width + 'px';
  289. viewportStyle.height = height + 'px';
  290. const svgRoot = this._svgDom;
  291. // Set width by 'svgRoot.width = width' is invalid
  292. svgRoot.setAttribute('width', width + '');
  293. svgRoot.setAttribute('height', height + '');
  294. }
  295. if (this._backgroundNode) {
  296. this._backgroundNode.setAttribute('width', width as any);
  297. this._backgroundNode.setAttribute('height', height as any);
  298. }
  299. }
  300. /**
  301. * 获取绘图区域宽度
  302. */
  303. getWidth() {
  304. return this._width;
  305. }
  306. /**
  307. * 获取绘图区域高度
  308. */
  309. getHeight() {
  310. return this._height;
  311. }
  312. dispose() {
  313. this.root.innerHTML = '';
  314. this._svgRoot =
  315. this._backgroundRoot =
  316. this._svgDom =
  317. this._backgroundNode =
  318. this._viewport = this.storage = null;
  319. }
  320. clear() {
  321. const viewportNode = this._viewport;
  322. if (viewportNode && viewportNode.parentNode) {
  323. viewportNode.parentNode.removeChild(viewportNode);
  324. }
  325. }
  326. toDataURL() {
  327. this.refresh();
  328. const svgDom = this._svgDom;
  329. const outerHTML = svgDom.outerHTML
  330. // outerHTML of `svg` tag is not supported in IE, use `parentNode.innerHTML` instead
  331. // PENDING: Or use `new XMLSerializer().serializeToString(svg)`?
  332. || (svgDom.parentNode && (svgDom.parentNode as HTMLElement).innerHTML);
  333. const html = encodeURIComponent(outerHTML.replace(/></g, '>\n\r<'));
  334. return 'data:image/svg+xml;charset=UTF-8,' + html;
  335. }
  336. refreshHover = createMethodNotSupport('refreshHover') as PainterBase['refreshHover'];
  337. configLayer = createMethodNotSupport('configLayer') as PainterBase['configLayer'];
  338. }
  339. // Not supported methods
  340. function createMethodNotSupport(method: string): any {
  341. return function () {
  342. if (process.env.NODE_ENV !== 'production') {
  343. util.logError('In SVG mode painter not support method "' + method + '"');
  344. }
  345. };
  346. }
  347. export default SVGPainter;