graphic.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. var _core = require("./core");
  2. var createElement = _core.createElement;
  3. var PathProxy = require("../core/PathProxy");
  4. var BoundingRect = require("../core/BoundingRect");
  5. var textContain = require("../contain/text");
  6. var textHelper = require("../graphic/helper/text");
  7. var Text = require("../graphic/Text");
  8. // TODO
  9. // 1. shadow
  10. // 2. Image: sx, sy, sw, sh
  11. var CMD = PathProxy.CMD;
  12. var arrayJoin = Array.prototype.join;
  13. var NONE = 'none';
  14. var mathRound = Math.round;
  15. var mathSin = Math.sin;
  16. var mathCos = Math.cos;
  17. var PI = Math.PI;
  18. var PI2 = Math.PI * 2;
  19. var degree = 180 / PI;
  20. var EPSILON = 1e-4;
  21. function round4(val) {
  22. return mathRound(val * 1e4) / 1e4;
  23. }
  24. function isAroundZero(val) {
  25. return val < EPSILON && val > -EPSILON;
  26. }
  27. function pathHasFill(style, isText) {
  28. var fill = isText ? style.textFill : style.fill;
  29. return fill != null && fill !== NONE;
  30. }
  31. function pathHasStroke(style, isText) {
  32. var stroke = isText ? style.textStroke : style.stroke;
  33. return stroke != null && stroke !== NONE;
  34. }
  35. function setTransform(svgEl, m) {
  36. if (m) {
  37. attr(svgEl, 'transform', 'matrix(' + arrayJoin.call(m, ',') + ')');
  38. }
  39. }
  40. function attr(el, key, val) {
  41. if (!val || val.type !== 'linear' && val.type !== 'radial') {
  42. // Don't set attribute for gradient, since it need new dom nodes
  43. el.setAttribute(key, val);
  44. }
  45. }
  46. function attrXLink(el, key, val) {
  47. el.setAttributeNS('http://www.w3.org/1999/xlink', key, val);
  48. }
  49. function bindStyle(svgEl, style, isText) {
  50. if (pathHasFill(style, isText)) {
  51. var fill = isText ? style.textFill : style.fill;
  52. fill = fill === 'transparent' ? NONE : fill;
  53. /**
  54. * FIXME:
  55. * This is a temporary fix for Chrome's clipping bug
  56. * that happens when a clip-path is referring another one.
  57. * This fix should be used before Chrome's bug is fixed.
  58. * For an element that has clip-path, and fill is none,
  59. * set it to be "rgba(0, 0, 0, 0.002)" will hide the element.
  60. * Otherwise, it will show black fill color.
  61. * 0.002 is used because this won't work for alpha values smaller
  62. * than 0.002.
  63. *
  64. * See
  65. * https://bugs.chromium.org/p/chromium/issues/detail?id=659790
  66. * for more information.
  67. */
  68. if (svgEl.getAttribute('clip-path') !== 'none' && fill === NONE) {
  69. fill = 'rgba(0, 0, 0, 0.002)';
  70. }
  71. attr(svgEl, 'fill', fill);
  72. attr(svgEl, 'fill-opacity', style.opacity);
  73. } else {
  74. attr(svgEl, 'fill', NONE);
  75. }
  76. if (pathHasStroke(style, isText)) {
  77. var stroke = isText ? style.textStroke : style.stroke;
  78. stroke = stroke === 'transparent' ? NONE : stroke;
  79. attr(svgEl, 'stroke', stroke);
  80. var strokeWidth = isText ? style.textStrokeWidth : style.lineWidth;
  81. var strokeScale = style.strokeNoScale ? style.host.getLineScale() : 1;
  82. attr(svgEl, 'stroke-width', strokeWidth / strokeScale);
  83. attr(svgEl, 'paint-order', 'stroke');
  84. attr(svgEl, 'stroke-opacity', style.opacity);
  85. var lineDash = style.lineDash;
  86. if (lineDash) {
  87. attr(svgEl, 'stroke-dasharray', style.lineDash.join(','));
  88. attr(svgEl, 'stroke-dashoffset', mathRound(style.lineDashOffset || 0));
  89. } else {
  90. attr(svgEl, 'stroke-dasharray', '');
  91. } // PENDING
  92. style.lineCap && attr(svgEl, 'stroke-linecap', style.lineCap);
  93. style.lineJoin && attr(svgEl, 'stroke-linejoin', style.lineJoin);
  94. style.miterLimit && attr(svgEl, 'stroke-miterlimit', style.miterLimit);
  95. } else {
  96. attr(svgEl, 'stroke', NONE);
  97. }
  98. }
  99. /***************************************************
  100. * PATH
  101. **************************************************/
  102. function pathDataToString(path) {
  103. var str = [];
  104. var data = path.data;
  105. var dataLength = path.len();
  106. for (var i = 0; i < dataLength;) {
  107. var cmd = data[i++];
  108. var cmdStr = '';
  109. var nData = 0;
  110. switch (cmd) {
  111. case CMD.M:
  112. cmdStr = 'M';
  113. nData = 2;
  114. break;
  115. case CMD.L:
  116. cmdStr = 'L';
  117. nData = 2;
  118. break;
  119. case CMD.Q:
  120. cmdStr = 'Q';
  121. nData = 4;
  122. break;
  123. case CMD.C:
  124. cmdStr = 'C';
  125. nData = 6;
  126. break;
  127. case CMD.A:
  128. var cx = data[i++];
  129. var cy = data[i++];
  130. var rx = data[i++];
  131. var ry = data[i++];
  132. var theta = data[i++];
  133. var dTheta = data[i++];
  134. var psi = data[i++];
  135. var clockwise = data[i++];
  136. var dThetaPositive = Math.abs(dTheta);
  137. var isCircle = isAroundZero(dThetaPositive % PI2) && !isAroundZero(dThetaPositive);
  138. var large = false;
  139. if (dThetaPositive >= PI2) {
  140. large = true;
  141. } else if (isAroundZero(dThetaPositive)) {
  142. large = false;
  143. } else {
  144. large = (dTheta > -PI && dTheta < 0 || dTheta > PI) === !!clockwise;
  145. }
  146. var x0 = round4(cx + rx * mathCos(theta));
  147. var y0 = round4(cy + ry * mathSin(theta)); // It will not draw if start point and end point are exactly the same
  148. // We need to shift the end point with a small value
  149. // FIXME A better way to draw circle ?
  150. if (isCircle) {
  151. if (clockwise) {
  152. dTheta = PI2 - 1e-4;
  153. } else {
  154. dTheta = -PI2 + 1e-4;
  155. }
  156. large = true;
  157. if (i === 9) {
  158. // Move to (x0, y0) only when CMD.A comes at the
  159. // first position of a shape.
  160. // For instance, when drawing a ring, CMD.A comes
  161. // after CMD.M, so it's unnecessary to move to
  162. // (x0, y0).
  163. str.push('M', x0, y0);
  164. }
  165. }
  166. var x = round4(cx + rx * mathCos(theta + dTheta));
  167. var y = round4(cy + ry * mathSin(theta + dTheta)); // FIXME Ellipse
  168. str.push('A', round4(rx), round4(ry), mathRound(psi * degree), +large, +clockwise, x, y);
  169. break;
  170. case CMD.Z:
  171. cmdStr = 'Z';
  172. break;
  173. case CMD.R:
  174. var x = round4(data[i++]);
  175. var y = round4(data[i++]);
  176. var w = round4(data[i++]);
  177. var h = round4(data[i++]);
  178. str.push('M', x, y, 'L', x + w, y, 'L', x + w, y + h, 'L', x, y + h, 'L', x, y);
  179. break;
  180. }
  181. cmdStr && str.push(cmdStr);
  182. for (var j = 0; j < nData; j++) {
  183. // PENDING With scale
  184. str.push(round4(data[i++]));
  185. }
  186. }
  187. return str.join(' ');
  188. }
  189. var svgPath = {};
  190. svgPath.brush = function (el) {
  191. var style = el.style;
  192. var svgEl = el.__svgEl;
  193. if (!svgEl) {
  194. svgEl = createElement('path');
  195. el.__svgEl = svgEl;
  196. }
  197. if (!el.path) {
  198. el.createPathProxy();
  199. }
  200. var path = el.path;
  201. if (el.__dirtyPath) {
  202. path.beginPath();
  203. el.buildPath(path, el.shape);
  204. el.__dirtyPath = false;
  205. var pathStr = pathDataToString(path);
  206. if (pathStr.indexOf('NaN') < 0) {
  207. // Ignore illegal path, which may happen such in out-of-range
  208. // data in Calendar series.
  209. attr(svgEl, 'd', pathStr);
  210. }
  211. }
  212. bindStyle(svgEl, style);
  213. setTransform(svgEl, el.transform);
  214. if (style.text != null) {
  215. svgTextDrawRectText(el, el.getBoundingRect());
  216. }
  217. };
  218. /***************************************************
  219. * IMAGE
  220. **************************************************/
  221. var svgImage = {};
  222. svgImage.brush = function (el) {
  223. var style = el.style;
  224. var image = style.image;
  225. if (image instanceof HTMLImageElement) {
  226. var src = image.src;
  227. image = src;
  228. }
  229. if (!image) {
  230. return;
  231. }
  232. var x = style.x || 0;
  233. var y = style.y || 0;
  234. var dw = style.width;
  235. var dh = style.height;
  236. var svgEl = el.__svgEl;
  237. if (!svgEl) {
  238. svgEl = createElement('image');
  239. el.__svgEl = svgEl;
  240. }
  241. if (image !== el.__imageSrc) {
  242. attrXLink(svgEl, 'href', image); // Caching image src
  243. el.__imageSrc = image;
  244. }
  245. attr(svgEl, 'width', dw);
  246. attr(svgEl, 'height', dh);
  247. attr(svgEl, 'x', x);
  248. attr(svgEl, 'y', y);
  249. setTransform(svgEl, el.transform);
  250. if (style.text != null) {
  251. svgTextDrawRectText(el, el.getBoundingRect());
  252. }
  253. };
  254. /***************************************************
  255. * TEXT
  256. **************************************************/
  257. var svgText = {};
  258. var tmpRect = new BoundingRect();
  259. var svgTextDrawRectText = function (el, rect, textRect) {
  260. var style = el.style;
  261. el.__dirty && textHelper.normalizeTextStyle(style, true);
  262. var text = style.text; // Convert to string
  263. if (text == null) {
  264. // Draw no text only when text is set to null, but not ''
  265. return;
  266. } else {
  267. text += '';
  268. }
  269. var textSvgEl = el.__textSvgEl;
  270. if (!textSvgEl) {
  271. textSvgEl = createElement('text');
  272. el.__textSvgEl = textSvgEl;
  273. }
  274. bindStyle(textSvgEl, style, true);
  275. if (el instanceof Text || el.style.transformText) {
  276. // Transform text with element
  277. setTransform(textSvgEl, el.transform);
  278. } else {
  279. if (el.transform) {
  280. tmpRect.copy(rect);
  281. tmpRect.applyTransform(el.transform);
  282. rect = tmpRect;
  283. } else {
  284. var pos = el.transformCoordToGlobal(rect.x, rect.y);
  285. rect.x = pos[0];
  286. rect.y = pos[1];
  287. }
  288. }
  289. var x;
  290. var y;
  291. var textPosition = style.textPosition;
  292. var distance = style.textDistance;
  293. var align = style.textAlign || 'left';
  294. if (typeof style.fontSize === 'number') {
  295. style.fontSize += 'px';
  296. }
  297. var font = style.font || [style.fontStyle || '', style.fontWeight || '', style.fontSize || '', style.fontFamily || ''].join(' ') || textContain.DEFAULT_FONT;
  298. var verticalAlign = getVerticalAlignForSvg(style.textVerticalAlign);
  299. textRect = textContain.getBoundingRect(text, font, align, verticalAlign);
  300. var lineHeight = textRect.lineHeight; // Text position represented by coord
  301. if (textPosition instanceof Array) {
  302. x = rect.x + textPosition[0];
  303. y = rect.y + textPosition[1];
  304. } else {
  305. var newPos = textContain.adjustTextPositionOnRect(textPosition, rect, distance);
  306. x = newPos.x;
  307. y = newPos.y;
  308. verticalAlign = getVerticalAlignForSvg(newPos.textVerticalAlign);
  309. align = newPos.textAlign;
  310. }
  311. attr(textSvgEl, 'alignment-baseline', verticalAlign);
  312. if (font) {
  313. textSvgEl.style.font = font;
  314. }
  315. var textPadding = style.textPadding; // Make baseline top
  316. attr(textSvgEl, 'x', x);
  317. attr(textSvgEl, 'y', y);
  318. var textLines = text.split('\n');
  319. var nTextLines = textLines.length;
  320. var textAnchor = align; // PENDING
  321. if (textAnchor === 'left') {
  322. textAnchor = 'start';
  323. textPadding && (x += textPadding[3]);
  324. } else if (textAnchor === 'right') {
  325. textAnchor = 'end';
  326. textPadding && (x -= textPadding[1]);
  327. } else if (textAnchor === 'center') {
  328. textAnchor = 'middle';
  329. textPadding && (x += (textPadding[3] - textPadding[1]) / 2);
  330. }
  331. var dy = 0;
  332. if (verticalAlign === 'baseline') {
  333. dy = -textRect.height + lineHeight;
  334. textPadding && (dy -= textPadding[2]);
  335. } else if (verticalAlign === 'middle') {
  336. dy = (-textRect.height + lineHeight) / 2;
  337. textPadding && (y += (textPadding[0] - textPadding[2]) / 2);
  338. } else {
  339. textPadding && (dy += textPadding[0]);
  340. } // Font may affect position of each tspan elements
  341. if (el.__text !== text || el.__textFont !== font) {
  342. var tspanList = el.__tspanList || [];
  343. el.__tspanList = tspanList;
  344. for (var i = 0; i < nTextLines; i++) {
  345. // Using cached tspan elements
  346. var tspan = tspanList[i];
  347. if (!tspan) {
  348. tspan = tspanList[i] = createElement('tspan');
  349. textSvgEl.appendChild(tspan);
  350. attr(tspan, 'alignment-baseline', verticalAlign);
  351. attr(tspan, 'text-anchor', textAnchor);
  352. } else {
  353. tspan.innerHTML = '';
  354. }
  355. attr(tspan, 'x', x);
  356. attr(tspan, 'y', y + i * lineHeight + dy);
  357. tspan.appendChild(document.createTextNode(textLines[i]));
  358. } // Remove unsed tspan elements
  359. for (; i < tspanList.length; i++) {
  360. textSvgEl.removeChild(tspanList[i]);
  361. }
  362. tspanList.length = nTextLines;
  363. el.__text = text;
  364. el.__textFont = font;
  365. } else if (el.__tspanList.length) {
  366. // Update span x and y
  367. var len = el.__tspanList.length;
  368. for (var i = 0; i < len; ++i) {
  369. var tspan = el.__tspanList[i];
  370. if (tspan) {
  371. attr(tspan, 'x', x);
  372. attr(tspan, 'y', y + i * lineHeight + dy);
  373. }
  374. }
  375. }
  376. };
  377. function getVerticalAlignForSvg(verticalAlign) {
  378. if (verticalAlign === 'middle') {
  379. return 'middle';
  380. } else if (verticalAlign === 'bottom') {
  381. return 'baseline';
  382. } else {
  383. return 'hanging';
  384. }
  385. }
  386. svgText.drawRectText = svgTextDrawRectText;
  387. svgText.brush = function (el) {
  388. var style = el.style;
  389. if (style.text != null) {
  390. // 强制设置 textPosition
  391. style.textPosition = [0, 0];
  392. svgTextDrawRectText(el, {
  393. x: style.x || 0,
  394. y: style.y || 0,
  395. width: 0,
  396. height: 0
  397. }, el.getBoundingRect());
  398. }
  399. };
  400. exports.path = svgPath;
  401. exports.image = svgImage;
  402. exports.text = svgText;