d40a7192ea0ef71fef7acf152c5643045c5c3bef8a3f1bc7f9b1fc8b0bbb679b425d2153c4b4f3a3065f56058c685d02ebca76bef996dc5ba97bf64074349e 11 KB

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