Painter.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /**
  2. * SVG Painter
  3. */
  4. import {
  5. brush,
  6. setClipPath,
  7. setGradient,
  8. setPattern
  9. } from './graphic';
  10. import Displayable from '../graphic/Displayable';
  11. import Storage from '../Storage';
  12. import { PainterBase } from '../PainterBase';
  13. import {
  14. createElement,
  15. createVNode,
  16. vNodeToString,
  17. SVGVNodeAttrs,
  18. SVGVNode,
  19. getCssString,
  20. BrushScope,
  21. createBrushScope,
  22. createSVGVNode
  23. } from './core';
  24. import { normalizeColor, encodeBase64, isGradient, isPattern } from './helper';
  25. import { extend, keys, logError, map, noop, retrieve2 } from '../core/util';
  26. import Path from '../graphic/Path';
  27. import patch, { updateAttrs } from './patch';
  28. import { getSize } from '../canvas/helper';
  29. import { GradientObject } from '../graphic/Gradient';
  30. import { PatternObject } from '../graphic/Pattern';
  31. let svgId = 0;
  32. interface SVGPainterOption {
  33. width?: number
  34. height?: number
  35. ssr?: boolean
  36. }
  37. type SVGPainterBackgroundColor = string | GradientObject | PatternObject;
  38. class SVGPainter implements PainterBase {
  39. type = 'svg'
  40. storage: Storage
  41. root: HTMLElement
  42. private _svgDom: SVGElement
  43. private _viewport: HTMLElement
  44. private _opts: SVGPainterOption
  45. private _oldVNode: SVGVNode
  46. private _bgVNode: SVGVNode
  47. private _mainVNode: SVGVNode
  48. private _width: number
  49. private _height: number
  50. private _backgroundColor: SVGPainterBackgroundColor
  51. private _id: string
  52. constructor(root: HTMLElement, storage: Storage, opts: SVGPainterOption) {
  53. this.storage = storage;
  54. this._opts = opts = extend({}, opts);
  55. this.root = root;
  56. // A unique id for generating svg ids.
  57. this._id = 'zr' + svgId++;
  58. this._oldVNode = createSVGVNode(opts.width, opts.height);
  59. if (root && !opts.ssr) {
  60. const viewport = this._viewport = document.createElement('div');
  61. viewport.style.cssText = 'position:relative;overflow:hidden';
  62. const svgDom = this._svgDom = this._oldVNode.elm = createElement('svg');
  63. updateAttrs(null, this._oldVNode);
  64. viewport.appendChild(svgDom);
  65. root.appendChild(viewport);
  66. }
  67. this.resize(opts.width, opts.height);
  68. }
  69. getType() {
  70. return this.type;
  71. }
  72. getViewportRoot() {
  73. return this._viewport;
  74. }
  75. getViewportRootOffset() {
  76. const viewportRoot = this.getViewportRoot();
  77. if (viewportRoot) {
  78. return {
  79. offsetLeft: viewportRoot.offsetLeft || 0,
  80. offsetTop: viewportRoot.offsetTop || 0
  81. };
  82. }
  83. }
  84. getSvgDom() {
  85. return this._svgDom;
  86. }
  87. refresh() {
  88. if (this.root) {
  89. const vnode = this.renderToVNode({
  90. willUpdate: true
  91. });
  92. // Disable user selection.
  93. vnode.attrs.style = 'position:absolute;left:0;top:0;user-select:none';
  94. patch(this._oldVNode, vnode);
  95. this._oldVNode = vnode;
  96. }
  97. }
  98. renderOneToVNode(el: Displayable) {
  99. return brush(el, createBrushScope(this._id));
  100. }
  101. renderToVNode(opts?: {
  102. animation?: boolean,
  103. willUpdate?: boolean,
  104. compress?: boolean,
  105. useViewBox?: boolean,
  106. emphasis?: boolean
  107. }) {
  108. opts = opts || {};
  109. const list = this.storage.getDisplayList(true);
  110. const width = this._width;
  111. const height = this._height;
  112. const scope = createBrushScope(this._id);
  113. scope.animation = opts.animation;
  114. scope.willUpdate = opts.willUpdate;
  115. scope.compress = opts.compress;
  116. scope.emphasis = opts.emphasis;
  117. const children: SVGVNode[] = [];
  118. const bgVNode = this._bgVNode = createBackgroundVNode(width, height, this._backgroundColor, scope);
  119. bgVNode && children.push(bgVNode);
  120. // Ignore the root g if wan't the output to be more tight.
  121. const mainVNode = !opts.compress
  122. ? (this._mainVNode = createVNode('g', 'main', {}, [])) : null;
  123. this._paintList(list, scope, mainVNode ? mainVNode.children : children);
  124. mainVNode && children.push(mainVNode);
  125. const defs = map(keys(scope.defs), (id) => scope.defs[id]);
  126. if (defs.length) {
  127. children.push(createVNode('defs', 'defs', {}, defs));
  128. }
  129. if (opts.animation) {
  130. const animationCssStr = getCssString(scope.cssNodes, scope.cssAnims, { newline: true });
  131. if (animationCssStr) {
  132. const styleNode = createVNode('style', 'stl', {}, [], animationCssStr);
  133. children.push(styleNode);
  134. }
  135. }
  136. return createSVGVNode(width, height, children, opts.useViewBox);
  137. }
  138. renderToString(opts?: {
  139. /**
  140. * If add css animation.
  141. * @default true
  142. */
  143. cssAnimation?: boolean,
  144. /**
  145. * If add css emphasis.
  146. * @default true
  147. */
  148. cssEmphasis?: boolean,
  149. /**
  150. * If use viewBox
  151. * @default true
  152. */
  153. useViewBox?: boolean
  154. }) {
  155. opts = opts || {};
  156. return vNodeToString(this.renderToVNode({
  157. animation: retrieve2(opts.cssAnimation, true),
  158. emphasis: retrieve2(opts.cssEmphasis, true),
  159. willUpdate: false,
  160. compress: true,
  161. useViewBox: retrieve2(opts.useViewBox, true)
  162. }), { newline: true });
  163. }
  164. setBackgroundColor(backgroundColor: SVGPainterBackgroundColor) {
  165. this._backgroundColor = backgroundColor;
  166. }
  167. getSvgRoot() {
  168. return this._mainVNode && this._mainVNode.elm as SVGElement;
  169. }
  170. _paintList(list: Displayable[], scope: BrushScope, out?: SVGVNode[]) {
  171. const listLen = list.length;
  172. const clipPathsGroupsStack: SVGVNode[] = [];
  173. let clipPathsGroupsStackDepth = 0;
  174. let currentClipPathGroup;
  175. let prevClipPaths: Path[];
  176. let clipGroupNodeIdx = 0;
  177. for (let i = 0; i < listLen; i++) {
  178. const displayable = list[i];
  179. if (!displayable.invisible) {
  180. const clipPaths = displayable.__clipPaths;
  181. const len = clipPaths && clipPaths.length || 0;
  182. const prevLen = prevClipPaths && prevClipPaths.length || 0;
  183. let lca;
  184. // Find the lowest common ancestor
  185. for (lca = Math.max(len - 1, prevLen - 1); lca >= 0; lca--) {
  186. if (clipPaths && prevClipPaths
  187. && clipPaths[lca] === prevClipPaths[lca]
  188. ) {
  189. break;
  190. }
  191. }
  192. // pop the stack
  193. for (let i = prevLen - 1; i > lca; i--) {
  194. clipPathsGroupsStackDepth--;
  195. // svgEls.push(closeGroup);
  196. currentClipPathGroup = clipPathsGroupsStack[clipPathsGroupsStackDepth - 1];
  197. }
  198. // Pop clip path group for clipPaths not match the previous.
  199. for (let i = lca + 1; i < len; i++) {
  200. const groupAttrs: SVGVNodeAttrs = {};
  201. setClipPath(
  202. clipPaths[i],
  203. groupAttrs,
  204. scope
  205. );
  206. const g = createVNode(
  207. 'g',
  208. 'clip-g-' + clipGroupNodeIdx++,
  209. groupAttrs,
  210. []
  211. );
  212. (currentClipPathGroup ? currentClipPathGroup.children : out).push(g);
  213. clipPathsGroupsStack[clipPathsGroupsStackDepth++] = g;
  214. currentClipPathGroup = g;
  215. }
  216. prevClipPaths = clipPaths;
  217. const ret = brush(displayable, scope);
  218. if (ret) {
  219. (currentClipPathGroup ? currentClipPathGroup.children : out).push(ret);
  220. }
  221. }
  222. }
  223. }
  224. resize(width: number, height: number) {
  225. // Save input w/h
  226. const opts = this._opts;
  227. const root = this.root;
  228. const viewport = this._viewport;
  229. width != null && (opts.width = width);
  230. height != null && (opts.height = height);
  231. if (root && viewport) {
  232. // FIXME Why ?
  233. viewport.style.display = 'none';
  234. width = getSize(root, 0, opts);
  235. height = getSize(root, 1, opts);
  236. viewport.style.display = '';
  237. }
  238. if (this._width !== width || this._height !== height) {
  239. this._width = width;
  240. this._height = height;
  241. if (viewport) {
  242. const viewportStyle = viewport.style;
  243. viewportStyle.width = width + 'px';
  244. viewportStyle.height = height + 'px';
  245. }
  246. if (!isPattern(this._backgroundColor)) {
  247. const svgDom = this._svgDom;
  248. if (svgDom) {
  249. // Set width by 'svgRoot.width = width' is invalid
  250. svgDom.setAttribute('width', width as any);
  251. svgDom.setAttribute('height', height as any);
  252. }
  253. const bgEl = this._bgVNode && this._bgVNode.elm as SVGElement;
  254. if (bgEl) {
  255. bgEl.setAttribute('width', width as any);
  256. bgEl.setAttribute('height', height as any);
  257. }
  258. }
  259. else {
  260. // pattern backgroundColor requires a full refresh
  261. this.refresh();
  262. }
  263. }
  264. }
  265. /**
  266. * 获取绘图区域宽度
  267. */
  268. getWidth() {
  269. return this._width;
  270. }
  271. /**
  272. * 获取绘图区域高度
  273. */
  274. getHeight() {
  275. return this._height;
  276. }
  277. dispose() {
  278. if (this.root) {
  279. this.root.innerHTML = '';
  280. }
  281. this._svgDom =
  282. this._viewport =
  283. this.storage =
  284. this._oldVNode =
  285. this._bgVNode =
  286. this._mainVNode = null;
  287. }
  288. clear() {
  289. if (this._svgDom) {
  290. this._svgDom.innerHTML = null;
  291. }
  292. this._oldVNode = null;
  293. }
  294. toDataURL(base64?: boolean) {
  295. let str = this.renderToString();
  296. const prefix = 'data:image/svg+xml;';
  297. if (base64) {
  298. str = encodeBase64(str);
  299. return str && prefix + 'base64,' + str;
  300. }
  301. return prefix + 'charset=UTF-8,' + encodeURIComponent(str);
  302. }
  303. refreshHover = createMethodNotSupport('refreshHover') as PainterBase['refreshHover'];
  304. configLayer = createMethodNotSupport('configLayer') as PainterBase['configLayer'];
  305. }
  306. // Not supported methods
  307. function createMethodNotSupport(method: string): any {
  308. return function () {
  309. if (process.env.NODE_ENV !== 'production') {
  310. logError('In SVG mode painter not support method "' + method + '"');
  311. }
  312. };
  313. }
  314. function createBackgroundVNode(
  315. width: number,
  316. height: number,
  317. backgroundColor: SVGPainterBackgroundColor,
  318. scope: BrushScope
  319. ) {
  320. let bgVNode;
  321. if (backgroundColor && backgroundColor !== 'none') {
  322. bgVNode = createVNode(
  323. 'rect',
  324. 'bg',
  325. {
  326. width,
  327. height,
  328. x: '0',
  329. y: '0'
  330. }
  331. );
  332. if (isGradient(backgroundColor)) {
  333. setGradient({ fill: backgroundColor as any }, bgVNode.attrs, 'fill', scope);
  334. }
  335. else if (isPattern(backgroundColor)) {
  336. setPattern({
  337. style: {
  338. fill: backgroundColor
  339. },
  340. dirty: noop,
  341. getBoundingRect: () => ({ width, height })
  342. } as any, bgVNode.attrs, 'fill', scope);
  343. }
  344. else {
  345. const { color, opacity } = normalizeColor(backgroundColor);
  346. bgVNode.attrs.fill = color;
  347. opacity < 1 && (bgVNode.attrs['fill-opacity'] = opacity);
  348. }
  349. }
  350. return bgVNode;
  351. }
  352. export default SVGPainter;