graphic.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. var _core = require("./core");
  2. var createElement = _core.createElement;
  3. var PathProxy = require("../core/PathProxy");
  4. var BoundingRect = require("../core/BoundingRect");
  5. var matrix = require("../core/matrix");
  6. var textContain = require("../contain/text");
  7. var textHelper = require("../graphic/helper/text");
  8. var Text = require("../graphic/Text");
  9. // TODO
  10. // 1. shadow
  11. // 2. Image: sx, sy, sw, sh
  12. var CMD = PathProxy.CMD;
  13. var arrayJoin = Array.prototype.join;
  14. var NONE = 'none';
  15. var mathRound = Math.round;
  16. var mathSin = Math.sin;
  17. var mathCos = Math.cos;
  18. var PI = Math.PI;
  19. var PI2 = Math.PI * 2;
  20. var degree = 180 / PI;
  21. var EPSILON = 1e-4;
  22. function round4(val) {
  23. return mathRound(val * 1e4) / 1e4;
  24. }
  25. function isAroundZero(val) {
  26. return val < EPSILON && val > -EPSILON;
  27. }
  28. function pathHasFill(style, isText) {
  29. var fill = isText ? style.textFill : style.fill;
  30. return fill != null && fill !== NONE;
  31. }
  32. function pathHasStroke(style, isText) {
  33. var stroke = isText ? style.textStroke : style.stroke;
  34. return stroke != null && stroke !== NONE;
  35. }
  36. function setTransform(svgEl, m) {
  37. if (m) {
  38. attr(svgEl, 'transform', 'matrix(' + arrayJoin.call(m, ',') + ')');
  39. }
  40. }
  41. function attr(el, key, val) {
  42. if (!val || val.type !== 'linear' && val.type !== 'radial') {
  43. // Don't set attribute for gradient, since it need new dom nodes
  44. el.setAttribute(key, val);
  45. }
  46. }
  47. function attrXLink(el, key, val) {
  48. el.setAttributeNS('http://www.w3.org/1999/xlink', key, val);
  49. }
  50. function bindStyle(svgEl, style, isText, el) {
  51. if (pathHasFill(style, isText)) {
  52. var fill = isText ? style.textFill : style.fill;
  53. fill = fill === 'transparent' ? NONE : fill;
  54. attr(svgEl, 'fill', fill);
  55. attr(svgEl, 'fill-opacity', style.fillOpacity != null ? style.fillOpacity * style.opacity : style.opacity);
  56. } else {
  57. attr(svgEl, 'fill', NONE);
  58. }
  59. if (pathHasStroke(style, isText)) {
  60. var stroke = isText ? style.textStroke : style.stroke;
  61. stroke = stroke === 'transparent' ? NONE : stroke;
  62. attr(svgEl, 'stroke', stroke);
  63. var strokeWidth = isText ? style.textStrokeWidth : style.lineWidth;
  64. var strokeScale = !isText && style.strokeNoScale ? el.getLineScale() : 1;
  65. attr(svgEl, 'stroke-width', strokeWidth / strokeScale); // stroke then fill for text; fill then stroke for others
  66. attr(svgEl, 'paint-order', isText ? 'stroke' : 'fill');
  67. attr(svgEl, 'stroke-opacity', style.strokeOpacity != null ? style.strokeOpacity : style.opacity);
  68. var lineDash = style.lineDash;
  69. if (lineDash) {
  70. attr(svgEl, 'stroke-dasharray', style.lineDash.join(','));
  71. attr(svgEl, 'stroke-dashoffset', mathRound(style.lineDashOffset || 0));
  72. } else {
  73. attr(svgEl, 'stroke-dasharray', '');
  74. } // PENDING
  75. style.lineCap && attr(svgEl, 'stroke-linecap', style.lineCap);
  76. style.lineJoin && attr(svgEl, 'stroke-linejoin', style.lineJoin);
  77. style.miterLimit && attr(svgEl, 'stroke-miterlimit', style.miterLimit);
  78. } else {
  79. attr(svgEl, 'stroke', NONE);
  80. }
  81. }
  82. /***************************************************
  83. * PATH
  84. **************************************************/
  85. function pathDataToString(path) {
  86. var str = [];
  87. var data = path.data;
  88. var dataLength = path.len();
  89. for (var i = 0; i < dataLength;) {
  90. var cmd = data[i++];
  91. var cmdStr = '';
  92. var nData = 0;
  93. switch (cmd) {
  94. case CMD.M:
  95. cmdStr = 'M';
  96. nData = 2;
  97. break;
  98. case CMD.L:
  99. cmdStr = 'L';
  100. nData = 2;
  101. break;
  102. case CMD.Q:
  103. cmdStr = 'Q';
  104. nData = 4;
  105. break;
  106. case CMD.C:
  107. cmdStr = 'C';
  108. nData = 6;
  109. break;
  110. case CMD.A:
  111. var cx = data[i++];
  112. var cy = data[i++];
  113. var rx = data[i++];
  114. var ry = data[i++];
  115. var theta = data[i++];
  116. var dTheta = data[i++];
  117. var psi = data[i++];
  118. var clockwise = data[i++];
  119. var dThetaPositive = Math.abs(dTheta);
  120. var isCircle = isAroundZero(dThetaPositive - PI2) || (clockwise ? dTheta >= PI2 : -dTheta >= PI2); // Mapping to 0~2PI
  121. var unifiedTheta = dTheta > 0 ? dTheta % PI2 : dTheta % PI2 + PI2;
  122. var large = false;
  123. if (isCircle) {
  124. large = true;
  125. } else if (isAroundZero(dThetaPositive)) {
  126. large = false;
  127. } else {
  128. large = unifiedTheta >= PI === !!clockwise;
  129. }
  130. var x0 = round4(cx + rx * mathCos(theta));
  131. var y0 = round4(cy + ry * mathSin(theta)); // It will not draw if start point and end point are exactly the same
  132. // We need to shift the end point with a small value
  133. // FIXME A better way to draw circle ?
  134. if (isCircle) {
  135. if (clockwise) {
  136. dTheta = PI2 - 1e-4;
  137. } else {
  138. dTheta = -PI2 + 1e-4;
  139. }
  140. large = true;
  141. if (i === 9) {
  142. // Move to (x0, y0) only when CMD.A comes at the
  143. // first position of a shape.
  144. // For instance, when drawing a ring, CMD.A comes
  145. // after CMD.M, so it's unnecessary to move to
  146. // (x0, y0).
  147. str.push('M', x0, y0);
  148. }
  149. }
  150. var x = round4(cx + rx * mathCos(theta + dTheta));
  151. var y = round4(cy + ry * mathSin(theta + dTheta)); // FIXME Ellipse
  152. str.push('A', round4(rx), round4(ry), mathRound(psi * degree), +large, +clockwise, x, y);
  153. break;
  154. case CMD.Z:
  155. cmdStr = 'Z';
  156. break;
  157. case CMD.R:
  158. var x = round4(data[i++]);
  159. var y = round4(data[i++]);
  160. var w = round4(data[i++]);
  161. var h = round4(data[i++]);
  162. str.push('M', x, y, 'L', x + w, y, 'L', x + w, y + h, 'L', x, y + h, 'L', x, y);
  163. break;
  164. }
  165. cmdStr && str.push(cmdStr);
  166. for (var j = 0; j < nData; j++) {
  167. // PENDING With scale
  168. str.push(round4(data[i++]));
  169. }
  170. }
  171. return str.join(' ');
  172. }
  173. var svgPath = {};
  174. svgPath.brush = function (el) {
  175. var style = el.style;
  176. var svgEl = el.__svgEl;
  177. if (!svgEl) {
  178. svgEl = createElement('path');
  179. el.__svgEl = svgEl;
  180. }
  181. if (!el.path) {
  182. el.createPathProxy();
  183. }
  184. var path = el.path;
  185. if (el.__dirtyPath) {
  186. path.beginPath();
  187. path.subPixelOptimize = false;
  188. el.buildPath(path, el.shape);
  189. el.__dirtyPath = false;
  190. var pathStr = pathDataToString(path);
  191. if (pathStr.indexOf('NaN') < 0) {
  192. // Ignore illegal path, which may happen such in out-of-range
  193. // data in Calendar series.
  194. attr(svgEl, 'd', pathStr);
  195. }
  196. }
  197. bindStyle(svgEl, style, false, el);
  198. setTransform(svgEl, el.transform);
  199. if (style.text != null) {
  200. svgTextDrawRectText(el, el.getBoundingRect());
  201. } else {
  202. removeOldTextNode(el);
  203. }
  204. };
  205. /***************************************************
  206. * IMAGE
  207. **************************************************/
  208. var svgImage = {};
  209. svgImage.brush = function (el) {
  210. var style = el.style;
  211. var image = style.image;
  212. if (image instanceof HTMLImageElement) {
  213. var src = image.src;
  214. image = src;
  215. }
  216. if (!image) {
  217. return;
  218. }
  219. var x = style.x || 0;
  220. var y = style.y || 0;
  221. var dw = style.width;
  222. var dh = style.height;
  223. var svgEl = el.__svgEl;
  224. if (!svgEl) {
  225. svgEl = createElement('image');
  226. el.__svgEl = svgEl;
  227. }
  228. if (image !== el.__imageSrc) {
  229. attrXLink(svgEl, 'href', image); // Caching image src
  230. el.__imageSrc = image;
  231. }
  232. attr(svgEl, 'width', dw);
  233. attr(svgEl, 'height', dh);
  234. attr(svgEl, 'x', x);
  235. attr(svgEl, 'y', y);
  236. setTransform(svgEl, el.transform);
  237. if (style.text != null) {
  238. svgTextDrawRectText(el, el.getBoundingRect());
  239. } else {
  240. removeOldTextNode(el);
  241. }
  242. };
  243. /***************************************************
  244. * TEXT
  245. **************************************************/
  246. var svgText = {};
  247. var _tmpTextHostRect = new BoundingRect();
  248. var _tmpTextBoxPos = {};
  249. var _tmpTextTransform = [];
  250. var TEXT_ALIGN_TO_ANCHRO = {
  251. left: 'start',
  252. right: 'end',
  253. center: 'middle',
  254. middle: 'middle'
  255. };
  256. /**
  257. * @param {module:zrender/Element} el
  258. * @param {Object|boolean} [hostRect] {x, y, width, height}
  259. * If set false, rect text is not used.
  260. */
  261. var svgTextDrawRectText = function (el, hostRect) {
  262. var style = el.style;
  263. var elTransform = el.transform;
  264. var needTransformTextByHostEl = el instanceof Text || style.transformText;
  265. el.__dirty && textHelper.normalizeTextStyle(style, true);
  266. var text = style.text; // Convert to string
  267. text != null && (text += '');
  268. if (!textHelper.needDrawText(text, style)) {
  269. return;
  270. } // render empty text for svg if no text but need draw text.
  271. text == null && (text = ''); // Follow the setting in the canvas renderer, if not transform the
  272. // text, transform the hostRect, by which the text is located.
  273. if (!needTransformTextByHostEl && elTransform) {
  274. _tmpTextHostRect.copy(hostRect);
  275. _tmpTextHostRect.applyTransform(elTransform);
  276. hostRect = _tmpTextHostRect;
  277. }
  278. var textSvgEl = el.__textSvgEl;
  279. if (!textSvgEl) {
  280. textSvgEl = createElement('text');
  281. el.__textSvgEl = textSvgEl;
  282. } // style.font has been normalized by `normalizeTextStyle`.
  283. var textSvgElStyle = textSvgEl.style;
  284. var font = style.font || textContain.DEFAULT_FONT;
  285. var computedFont = textSvgEl.__computedFont;
  286. if (font !== textSvgEl.__styleFont) {
  287. textSvgElStyle.font = textSvgEl.__styleFont = font; // The computedFont might not be the orginal font if it is illegal font.
  288. computedFont = textSvgEl.__computedFont = textSvgElStyle.font;
  289. }
  290. var textPadding = style.textPadding;
  291. var textLineHeight = style.textLineHeight;
  292. var contentBlock = el.__textCotentBlock;
  293. if (!contentBlock || el.__dirtyText) {
  294. contentBlock = el.__textCotentBlock = textContain.parsePlainText(text, computedFont, textPadding, textLineHeight, style.truncate);
  295. }
  296. var outerHeight = contentBlock.outerHeight;
  297. var lineHeight = contentBlock.lineHeight;
  298. textHelper.getBoxPosition(_tmpTextBoxPos, el, style, hostRect);
  299. var baseX = _tmpTextBoxPos.baseX;
  300. var baseY = _tmpTextBoxPos.baseY;
  301. var textAlign = _tmpTextBoxPos.textAlign || 'left';
  302. var textVerticalAlign = _tmpTextBoxPos.textVerticalAlign;
  303. setTextTransform(textSvgEl, needTransformTextByHostEl, elTransform, style, hostRect, baseX, baseY);
  304. var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign);
  305. var textX = baseX;
  306. var textY = boxY; // TODO needDrawBg
  307. if (textPadding) {
  308. textX = getTextXForPadding(baseX, textAlign, textPadding);
  309. textY += textPadding[0];
  310. } // `textBaseline` is set as 'middle'.
  311. textY += lineHeight / 2;
  312. bindStyle(textSvgEl, style, true, el); // FIXME
  313. // Add a <style> to reset all of the text font as inherit?
  314. // otherwise the outer <style> may set the unexpected style.
  315. // Font may affect position of each tspan elements
  316. var canCacheByTextString = contentBlock.canCacheByTextString;
  317. var tspanList = el.__tspanList || (el.__tspanList = []);
  318. var tspanOriginLen = tspanList.length; // Optimize for most cases, just compare text string to determine change.
  319. if (canCacheByTextString && el.__canCacheByTextString && el.__text === text) {
  320. if (el.__dirtyText && tspanOriginLen) {
  321. for (var idx = 0; idx < tspanOriginLen; ++idx) {
  322. updateTextLocation(tspanList[idx], textAlign, textX, textY + idx * lineHeight);
  323. }
  324. }
  325. } else {
  326. el.__text = text;
  327. el.__canCacheByTextString = canCacheByTextString;
  328. var textLines = contentBlock.lines;
  329. var nTextLines = textLines.length;
  330. var idx = 0;
  331. for (; idx < nTextLines; idx++) {
  332. // Using cached tspan elements
  333. var tspan = tspanList[idx];
  334. var singleLineText = textLines[idx];
  335. if (!tspan) {
  336. tspan = tspanList[idx] = createElement('tspan');
  337. textSvgEl.appendChild(tspan);
  338. tspan.appendChild(document.createTextNode(singleLineText));
  339. } else if (tspan.__zrText !== singleLineText) {
  340. tspan.innerHTML = '';
  341. tspan.appendChild(document.createTextNode(singleLineText));
  342. }
  343. updateTextLocation(tspan, textAlign, textX, textY + idx * lineHeight);
  344. } // Remove unused tspan elements
  345. if (tspanOriginLen > nTextLines) {
  346. for (; idx < tspanOriginLen; idx++) {
  347. textSvgEl.removeChild(tspanList[idx]);
  348. }
  349. tspanList.length = nTextLines;
  350. }
  351. }
  352. };
  353. function setTextTransform(textSvgEl, needTransformTextByHostEl, elTransform, style, hostRect, baseX, baseY) {
  354. matrix.identity(_tmpTextTransform);
  355. if (needTransformTextByHostEl && elTransform) {
  356. matrix.copy(_tmpTextTransform, elTransform);
  357. } // textRotation only apply in RectText.
  358. var textRotation = style.textRotation;
  359. if (hostRect && textRotation) {
  360. var origin = style.textOrigin;
  361. if (origin === 'center') {
  362. baseX = hostRect.width / 2 + hostRect.x;
  363. baseY = hostRect.height / 2 + hostRect.y;
  364. } else if (origin) {
  365. baseX = origin[0] + hostRect.x;
  366. baseY = origin[1] + hostRect.y;
  367. }
  368. _tmpTextTransform[4] -= baseX;
  369. _tmpTextTransform[5] -= baseY; // Positive: anticlockwise
  370. matrix.rotate(_tmpTextTransform, _tmpTextTransform, textRotation);
  371. _tmpTextTransform[4] += baseX;
  372. _tmpTextTransform[5] += baseY;
  373. } // See the definition in `Style.js#textOrigin`, the default
  374. // origin is from the result of `getBoxPosition`.
  375. setTransform(textSvgEl, _tmpTextTransform);
  376. } // FIXME merge the same code with `helper/text.js#getTextXForPadding`;
  377. function getTextXForPadding(x, textAlign, textPadding) {
  378. return textAlign === 'right' ? x - textPadding[1] : textAlign === 'center' ? x + textPadding[3] / 2 - textPadding[1] / 2 : x + textPadding[3];
  379. }
  380. function updateTextLocation(tspan, textAlign, x, y) {
  381. // Consider different font display differently in vertial align, we always
  382. // set vertialAlign as 'middle', and use 'y' to locate text vertically.
  383. attr(tspan, 'dominant-baseline', 'middle');
  384. attr(tspan, 'text-anchor', TEXT_ALIGN_TO_ANCHRO[textAlign]);
  385. attr(tspan, 'x', x);
  386. attr(tspan, 'y', y);
  387. }
  388. function removeOldTextNode(el) {
  389. if (el && el.__textSvgEl) {
  390. // textSvgEl may has no parentNode if el has been removed temporary.
  391. if (el.__textSvgEl.parentNode) {
  392. el.__textSvgEl.parentNode.removeChild(el.__textSvgEl);
  393. }
  394. el.__textSvgEl = null;
  395. el.__tspanList = [];
  396. el.__text = null;
  397. }
  398. }
  399. svgText.drawRectText = svgTextDrawRectText;
  400. svgText.brush = function (el) {
  401. var style = el.style;
  402. if (style.text != null) {
  403. svgTextDrawRectText(el, false);
  404. } else {
  405. removeOldTextNode(el);
  406. }
  407. };
  408. exports.path = svgPath;
  409. exports.image = svgImage;
  410. exports.text = svgText;