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. scope.ssr = this._opts.ssr;
  118. const children: SVGVNode[] = [];
  119. const bgVNode = this._bgVNode = createBackgroundVNode(width, height, this._backgroundColor, scope);
  120. bgVNode && children.push(bgVNode);
  121. // Ignore the root g if wan't the output to be more tight.
  122. const mainVNode = !opts.compress
  123. ? (this._mainVNode = createVNode('g', 'main', {}, [])) : null;
  124. this._paintList(list, scope, mainVNode ? mainVNode.children : children);
  125. mainVNode && children.push(mainVNode);
  126. const defs = map(keys(scope.defs), (id) => scope.defs[id]);
  127. if (defs.length) {
  128. children.push(createVNode('defs', 'defs', {}, defs));
  129. }
  130. if (opts.animation) {
  131. const animationCssStr = getCssString(scope.cssNodes, scope.cssAnims, { newline: true });
  132. if (animationCssStr) {
  133. const styleNode = createVNode('style', 'stl', {}, [], animationCssStr);
  134. children.push(styleNode);
  135. }
  136. }
  137. return createSVGVNode(width, height, children, opts.useViewBox);
  138. }
  139. renderToString(opts?: {
  140. /**
  141. * If add css animation.
  142. * @default true
  143. */
  144. cssAnimation?: boolean,
  145. /**
  146. * If add css emphasis.
  147. * @default true
  148. */
  149. cssEmphasis?: boolean,
  150. /**
  151. * If use viewBox
  152. * @default true
  153. */
  154. useViewBox?: boolean
  155. }) {
  156. opts = opts || {};
  157. return vNodeToString(this.renderToVNode({
  158. animation: retrieve2(opts.cssAnimation, true),
  159. emphasis: retrieve2(opts.cssEmphasis, true),
  160. willUpdate: false,
  161. compress: true,
  162. useViewBox: retrieve2(opts.useViewBox, true)
  163. }), { newline: true });
  164. }
  165. setBackgroundColor(backgroundColor: SVGPainterBackgroundColor) {
  166. this._backgroundColor = backgroundColor;
  167. }
  168. getSvgRoot() {
  169. return this._mainVNode && this._mainVNode.elm as SVGElement;
  170. }
  171. _paintList(list: Displayable[], scope: BrushScope, out?: SVGVNode[]) {
  172. const listLen = list.length;
  173. const clipPathsGroupsStack: SVGVNode[] = [];
  174. let clipPathsGroupsStackDepth = 0;
  175. let currentClipPathGroup;
  176. let prevClipPaths: Path[];
  177. let clipGroupNodeIdx = 0;
  178. for (let i = 0; i < listLen; i++) {
  179. const displayable = list[i];
  180. if (!displayable.invisible) {
  181. const clipPaths = displayable.__clipPaths;
  182. const len = clipPaths && clipPaths.length || 0;
  183. const prevLen = prevClipPaths && prevClipPaths.length || 0;
  184. let lca;
  185. // Find the lowest common ancestor
  186. for (lca = Math.max(len - 1, prevLen - 1); lca >= 0; lca--) {
  187. if (clipPaths && prevClipPaths
  188. && clipPaths[lca] === prevClipPaths[lca]
  189. ) {
  190. break;
  191. }
  192. }
  193. // pop the stack
  194. for (let i = prevLen - 1; i > lca; i--) {
  195. clipPathsGroupsStackDepth--;
  196. // svgEls.push(closeGroup);
  197. currentClipPathGroup = clipPathsGroupsStack[clipPathsGroupsStackDepth - 1];
  198. }
  199. // Pop clip path group for clipPaths not match the previous.
  200. for (let i = lca + 1; i < len; i++) {
  201. const groupAttrs: SVGVNodeAttrs = {};
  202. setClipPath(
  203. clipPaths[i],
  204. groupAttrs,
  205. scope
  206. );
  207. const g = createVNode(
  208. 'g',
  209. 'clip-g-' + clipGroupNodeIdx++,
  210. groupAttrs,
  211. []
  212. );
  213. (currentClipPathGroup ? currentClipPathGroup.children : out).push(g);
  214. clipPathsGroupsStack[clipPathsGroupsStackDepth++] = g;
  215. currentClipPathGroup = g;
  216. }
  217. prevClipPaths = clipPaths;
  218. const ret = brush(displayable, scope);
  219. if (ret) {
  220. (currentClipPathGroup ? currentClipPathGroup.children : out).push(ret);
  221. }
  222. }
  223. }
  224. }
  225. resize(width: number, height: number) {
  226. // Save input w/h
  227. const opts = this._opts;
  228. const root = this.root;
  229. const viewport = this._viewport;
  230. width != null && (opts.width = width);
  231. height != null && (opts.height = height);
  232. if (root && viewport) {
  233. // FIXME Why ?
  234. viewport.style.display = 'none';
  235. width = getSize(root, 0, opts);
  236. height = getSize(root, 1, opts);
  237. viewport.style.display = '';
  238. }
  239. if (this._width !== width || this._height !== height) {
  240. this._width = width;
  241. this._height = height;
  242. if (viewport) {
  243. const viewportStyle = viewport.style;
  244. viewportStyle.width = width + 'px';
  245. viewportStyle.height = height + 'px';
  246. }
  247. if (!isPattern(this._backgroundColor)) {
  248. const svgDom = this._svgDom;
  249. if (svgDom) {
  250. // Set width by 'svgRoot.width = width' is invalid
  251. svgDom.setAttribute('width', width as any);
  252. svgDom.setAttribute('height', height as any);
  253. }
  254. const bgEl = this._bgVNode && this._bgVNode.elm as SVGElement;
  255. if (bgEl) {
  256. bgEl.setAttribute('width', width as any);
  257. bgEl.setAttribute('height', height as any);
  258. }
  259. }
  260. else {
  261. // pattern backgroundColor requires a full refresh
  262. this.refresh();
  263. }
  264. }
  265. }
  266. /**
  267. * 获取绘图区域宽度
  268. */
  269. getWidth() {
  270. return this._width;
  271. }
  272. /**
  273. * 获取绘图区域高度
  274. */
  275. getHeight() {
  276. return this._height;
  277. }
  278. dispose() {
  279. if (this.root) {
  280. this.root.innerHTML = '';
  281. }
  282. this._svgDom =
  283. this._viewport =
  284. this.storage =
  285. this._oldVNode =
  286. this._bgVNode =
  287. this._mainVNode = null;
  288. }
  289. clear() {
  290. if (this._svgDom) {
  291. this._svgDom.innerHTML = null;
  292. }
  293. this._oldVNode = null;
  294. }
  295. toDataURL(base64?: boolean) {
  296. let str = this.renderToString();
  297. const prefix = 'data:image/svg+xml;';
  298. if (base64) {
  299. str = encodeBase64(str);
  300. return str && prefix + 'base64,' + str;
  301. }
  302. return prefix + 'charset=UTF-8,' + encodeURIComponent(str);
  303. }
  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;