text.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. var _util = require("../../core/util");
  2. var retrieve2 = _util.retrieve2;
  3. var retrieve3 = _util.retrieve3;
  4. var each = _util.each;
  5. var normalizeCssArray = _util.normalizeCssArray;
  6. var isString = _util.isString;
  7. var isObject = _util.isObject;
  8. var textContain = require("../../contain/text");
  9. var roundRectHelper = require("./roundRect");
  10. var imageHelper = require("./image");
  11. // TODO: Have not support 'start', 'end' yet.
  12. var VALID_TEXT_ALIGN = {
  13. left: 1,
  14. right: 1,
  15. center: 1
  16. };
  17. var VALID_TEXT_VERTICAL_ALIGN = {
  18. top: 1,
  19. bottom: 1,
  20. middle: 1
  21. };
  22. /**
  23. * @param {module:zrender/graphic/Style} style
  24. * @return {module:zrender/graphic/Style} The input style.
  25. */
  26. function normalizeTextStyle(style) {
  27. normalizeStyle(style);
  28. each(style.rich, normalizeStyle);
  29. return style;
  30. }
  31. function normalizeStyle(style) {
  32. if (style) {
  33. style.font = textContain.makeFont(style);
  34. var textAlign = style.textAlign;
  35. textAlign === 'middle' && (textAlign = 'center');
  36. style.textAlign = textAlign == null || VALID_TEXT_ALIGN[textAlign] ? textAlign : 'left'; // Compatible with textBaseline.
  37. var textVerticalAlign = style.textVerticalAlign || style.textBaseline;
  38. textVerticalAlign === 'center' && (textVerticalAlign = 'middle');
  39. style.textVerticalAlign = textVerticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[textVerticalAlign] ? textVerticalAlign : 'top';
  40. var textPadding = style.textPadding;
  41. if (textPadding) {
  42. style.textPadding = normalizeCssArray(style.textPadding);
  43. }
  44. }
  45. }
  46. /**
  47. * @param {CanvasRenderingContext2D} ctx
  48. * @param {string} text
  49. * @param {module:zrender/graphic/Style} style
  50. * @param {Object|boolean} [rect] {x, y, width, height}
  51. * If set false, rect text is not used.
  52. */
  53. function renderText(hostEl, ctx, text, style, rect) {
  54. style.rich ? renderRichText(hostEl, ctx, text, style, rect) : renderPlainText(hostEl, ctx, text, style, rect);
  55. }
  56. function renderPlainText(hostEl, ctx, text, style, rect) {
  57. var font = setCtx(ctx, 'font', style.font || textContain.DEFAULT_FONT);
  58. var textPadding = style.textPadding;
  59. var contentBlock = hostEl.__textCotentBlock;
  60. if (!contentBlock || hostEl.__dirty) {
  61. contentBlock = hostEl.__textCotentBlock = textContain.parsePlainText(text, font, textPadding, style.truncate);
  62. }
  63. var outerHeight = contentBlock.outerHeight;
  64. var textLines = contentBlock.lines;
  65. var lineHeight = contentBlock.lineHeight;
  66. var boxPos = getBoxPosition(outerHeight, style, rect);
  67. var baseX = boxPos.baseX;
  68. var baseY = boxPos.baseY;
  69. var textAlign = boxPos.textAlign;
  70. var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing.
  71. applyTextRotation(ctx, style, rect, baseX, baseY);
  72. var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign);
  73. var textX = baseX;
  74. var textY = boxY;
  75. var needDrawBg = needDrawBackground(style);
  76. if (needDrawBg || textPadding) {
  77. // Consider performance, do not call getTextWidth util necessary.
  78. var textWidth = textContain.getWidth(text, font);
  79. var outerWidth = textWidth;
  80. textPadding && (outerWidth += textPadding[1] + textPadding[3]);
  81. var boxX = textContain.adjustTextX(baseX, outerWidth, textAlign);
  82. needDrawBg && drawBackground(hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight);
  83. if (textPadding) {
  84. textX = getTextXForPadding(baseX, textAlign, textPadding);
  85. textY += textPadding[0];
  86. }
  87. }
  88. setCtx(ctx, 'textAlign', textAlign || 'left'); // Force baseline to be "middle". Otherwise, if using "top", the
  89. // text will offset downward a little bit in font "Microsoft YaHei".
  90. setCtx(ctx, 'textBaseline', 'middle'); // Always set shadowBlur and shadowOffset to avoid leak from displayable.
  91. setCtx(ctx, 'shadowBlur', style.textShadowBlur || 0);
  92. setCtx(ctx, 'shadowColor', style.textShadowColor || 'transparent');
  93. setCtx(ctx, 'shadowOffsetX', style.textShadowOffsetX || 0);
  94. setCtx(ctx, 'shadowOffsetY', style.textShadowOffsetY || 0); // `textBaseline` is set as 'middle'.
  95. textY += lineHeight / 2;
  96. var textStrokeWidth = style.textStrokeWidth;
  97. var textStroke = getStroke(style.textStroke, textStrokeWidth);
  98. var textFill = getFill(style.textFill);
  99. if (textStroke) {
  100. setCtx(ctx, 'lineWidth', textStrokeWidth);
  101. setCtx(ctx, 'strokeStyle', textStroke);
  102. }
  103. if (textFill) {
  104. setCtx(ctx, 'fillStyle', textFill);
  105. }
  106. for (var i = 0; i < textLines.length; i++) {
  107. // Fill after stroke so the outline will not cover the main part.
  108. textStroke && ctx.strokeText(textLines[i], textX, textY);
  109. textFill && ctx.fillText(textLines[i], textX, textY);
  110. textY += lineHeight;
  111. }
  112. }
  113. function renderRichText(hostEl, ctx, text, style, rect) {
  114. var contentBlock = hostEl.__textCotentBlock;
  115. if (!contentBlock || hostEl.__dirty) {
  116. contentBlock = hostEl.__textCotentBlock = textContain.parseRichText(text, style);
  117. }
  118. drawRichText(hostEl, ctx, contentBlock, style, rect);
  119. }
  120. function drawRichText(hostEl, ctx, contentBlock, style, rect) {
  121. var contentWidth = contentBlock.width;
  122. var outerWidth = contentBlock.outerWidth;
  123. var outerHeight = contentBlock.outerHeight;
  124. var textPadding = style.textPadding;
  125. var boxPos = getBoxPosition(outerHeight, style, rect);
  126. var baseX = boxPos.baseX;
  127. var baseY = boxPos.baseY;
  128. var textAlign = boxPos.textAlign;
  129. var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing.
  130. applyTextRotation(ctx, style, rect, baseX, baseY);
  131. var boxX = textContain.adjustTextX(baseX, outerWidth, textAlign);
  132. var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign);
  133. var xLeft = boxX;
  134. var lineTop = boxY;
  135. if (textPadding) {
  136. xLeft += textPadding[3];
  137. lineTop += textPadding[0];
  138. }
  139. var xRight = xLeft + contentWidth;
  140. needDrawBackground(style) && drawBackground(hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight);
  141. for (var i = 0; i < contentBlock.lines.length; i++) {
  142. var line = contentBlock.lines[i];
  143. var tokens = line.tokens;
  144. var tokenCount = tokens.length;
  145. var lineHeight = line.lineHeight;
  146. var usedWidth = line.width;
  147. var leftIndex = 0;
  148. var lineXLeft = xLeft;
  149. var lineXRight = xRight;
  150. var rightIndex = tokenCount - 1;
  151. var token;
  152. while (leftIndex < tokenCount && (token = tokens[leftIndex], !token.textAlign || token.textAlign === 'left')) {
  153. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft, 'left');
  154. usedWidth -= token.width;
  155. lineXLeft += token.width;
  156. leftIndex++;
  157. }
  158. while (rightIndex >= 0 && (token = tokens[rightIndex], token.textAlign === 'right')) {
  159. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXRight, 'right');
  160. usedWidth -= token.width;
  161. lineXRight -= token.width;
  162. rightIndex--;
  163. } // The other tokens are placed as textAlign 'center' if there is enough space.
  164. lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - usedWidth) / 2;
  165. while (leftIndex <= rightIndex) {
  166. token = tokens[leftIndex]; // Consider width specified by user, use 'center' rather than 'left'.
  167. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft + token.width / 2, 'center');
  168. lineXLeft += token.width;
  169. leftIndex++;
  170. }
  171. lineTop += lineHeight;
  172. }
  173. }
  174. function applyTextRotation(ctx, style, rect, x, y) {
  175. // textRotation only apply in RectText.
  176. if (rect && style.textRotation) {
  177. var origin = style.textOrigin;
  178. if (origin === 'center') {
  179. x = rect.width / 2 + rect.x;
  180. y = rect.height / 2 + rect.y;
  181. } else if (origin) {
  182. x = origin[0] + rect.x;
  183. y = origin[1] + rect.y;
  184. }
  185. ctx.translate(x, y); // Positive: anticlockwise
  186. ctx.rotate(-style.textRotation);
  187. ctx.translate(-x, -y);
  188. }
  189. }
  190. function placeToken(hostEl, ctx, token, style, lineHeight, lineTop, x, textAlign) {
  191. var tokenStyle = style.rich[token.styleName] || {}; // 'ctx.textBaseline' is always set as 'middle', for sake of
  192. // the bias of "Microsoft YaHei".
  193. var textVerticalAlign = token.textVerticalAlign;
  194. var y = lineTop + lineHeight / 2;
  195. if (textVerticalAlign === 'top') {
  196. y = lineTop + token.height / 2;
  197. } else if (textVerticalAlign === 'bottom') {
  198. y = lineTop + lineHeight - token.height / 2;
  199. }
  200. !token.isLineHolder && needDrawBackground(tokenStyle) && drawBackground(hostEl, ctx, tokenStyle, textAlign === 'right' ? x - token.width : textAlign === 'center' ? x - token.width / 2 : x, y - token.height / 2, token.width, token.height);
  201. var textPadding = token.textPadding;
  202. if (textPadding) {
  203. x = getTextXForPadding(x, textAlign, textPadding);
  204. y -= token.height / 2 - textPadding[2] - token.textHeight / 2;
  205. }
  206. setCtx(ctx, 'shadowBlur', retrieve3(tokenStyle.textShadowBlur, style.textShadowBlur, 0));
  207. setCtx(ctx, 'shadowColor', tokenStyle.textShadowColor || style.textShadowColor || 'transparent');
  208. setCtx(ctx, 'shadowOffsetX', retrieve3(tokenStyle.textShadowOffsetX, style.textShadowOffsetX, 0));
  209. setCtx(ctx, 'shadowOffsetY', retrieve3(tokenStyle.textShadowOffsetY, style.textShadowOffsetY, 0));
  210. setCtx(ctx, 'textAlign', textAlign); // Force baseline to be "middle". Otherwise, if using "top", the
  211. // text will offset downward a little bit in font "Microsoft YaHei".
  212. setCtx(ctx, 'textBaseline', 'middle');
  213. setCtx(ctx, 'font', token.font || textContain.DEFAULT_FONT);
  214. var textStroke = getStroke(tokenStyle.textStroke || style.textStroke, textStrokeWidth);
  215. var textFill = getFill(tokenStyle.textFill || style.textFill);
  216. var textStrokeWidth = retrieve2(tokenStyle.textStrokeWidth, style.textStrokeWidth); // Fill after stroke so the outline will not cover the main part.
  217. if (textStroke) {
  218. setCtx(ctx, 'lineWidth', textStrokeWidth);
  219. setCtx(ctx, 'strokeStyle', textStroke);
  220. ctx.strokeText(token.text, x, y);
  221. }
  222. if (textFill) {
  223. setCtx(ctx, 'fillStyle', textFill);
  224. ctx.fillText(token.text, x, y);
  225. }
  226. }
  227. function needDrawBackground(style) {
  228. return style.textBackgroundColor || style.textBorderWidth && style.textBorderColor;
  229. } // style: {textBackgroundColor, textBorderWidth, textBorderColor, textBorderRadius}
  230. // shape: {x, y, width, height}
  231. function drawBackground(hostEl, ctx, style, x, y, width, height) {
  232. var textBackgroundColor = style.textBackgroundColor;
  233. var textBorderWidth = style.textBorderWidth;
  234. var textBorderColor = style.textBorderColor;
  235. var isPlainBg = isString(textBackgroundColor);
  236. setCtx(ctx, 'shadowBlur', style.textBoxShadowBlur || 0);
  237. setCtx(ctx, 'shadowColor', style.textBoxShadowColor || 'transparent');
  238. setCtx(ctx, 'shadowOffsetX', style.textBoxShadowOffsetX || 0);
  239. setCtx(ctx, 'shadowOffsetY', style.textBoxShadowOffsetY || 0);
  240. if (isPlainBg || textBorderWidth && textBorderColor) {
  241. ctx.beginPath();
  242. var textBorderRadius = style.textBorderRadius;
  243. if (!textBorderRadius) {
  244. ctx.rect(x, y, width, height);
  245. } else {
  246. roundRectHelper.buildPath(ctx, {
  247. x: x,
  248. y: y,
  249. width: width,
  250. height: height,
  251. r: textBorderRadius
  252. });
  253. }
  254. ctx.closePath();
  255. }
  256. if (isPlainBg) {
  257. setCtx(ctx, 'fillStyle', textBackgroundColor);
  258. ctx.fill();
  259. } else if (isObject(textBackgroundColor)) {
  260. var image = textBackgroundColor.image;
  261. image = imageHelper.createOrUpdateImage(image, null, hostEl, onBgImageLoaded, textBackgroundColor);
  262. if (image && imageHelper.isImageReady(image)) {
  263. ctx.drawImage(image, x, y, width, height);
  264. }
  265. }
  266. if (textBorderWidth && textBorderColor) {
  267. setCtx(ctx, 'lineWidth', textBorderWidth);
  268. setCtx(ctx, 'strokeStyle', textBorderColor);
  269. ctx.stroke();
  270. }
  271. }
  272. function onBgImageLoaded(image, textBackgroundColor) {
  273. // Replace image, so that `contain/text.js#parseRichText`
  274. // will get correct result in next tick.
  275. textBackgroundColor.image = image;
  276. }
  277. function getBoxPosition(blockHeiht, style, rect) {
  278. var baseX = style.x || 0;
  279. var baseY = style.y || 0;
  280. var textAlign = style.textAlign;
  281. var textVerticalAlign = style.textVerticalAlign; // Text position represented by coord
  282. if (rect) {
  283. var textPosition = style.textPosition;
  284. if (textPosition instanceof Array) {
  285. // Percent
  286. baseX = rect.x + parsePercent(textPosition[0], rect.width);
  287. baseY = rect.y + parsePercent(textPosition[1], rect.height);
  288. } else {
  289. var res = textContain.adjustTextPositionOnRect(textPosition, rect, style.textDistance);
  290. baseX = res.x;
  291. baseY = res.y; // Default align and baseline when has textPosition
  292. textAlign = textAlign || res.textAlign;
  293. textVerticalAlign = textVerticalAlign || res.textVerticalAlign;
  294. } // textOffset is only support in RectText, otherwise
  295. // we have to adjust boundingRect for textOffset.
  296. var textOffset = style.textOffset;
  297. if (textOffset) {
  298. baseX += textOffset[0];
  299. baseY += textOffset[1];
  300. }
  301. }
  302. return {
  303. baseX: baseX,
  304. baseY: baseY,
  305. textAlign: textAlign,
  306. textVerticalAlign: textVerticalAlign
  307. };
  308. }
  309. function setCtx(ctx, prop, value) {
  310. // FIXME ??? performance try
  311. // if (ctx.__currentValues[prop] !== value) {
  312. // ctx[prop] = ctx.__currentValues[prop] = value;
  313. ctx[prop] = value; // }
  314. return ctx[prop];
  315. }
  316. /**
  317. * @param {string} [stroke] If specified, do not check style.textStroke.
  318. * @param {string} [lineWidth] If specified, do not check style.textStroke.
  319. * @param {number} style
  320. */
  321. function getStroke(stroke, lineWidth) {
  322. return stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none' ? null // TODO pattern and gradient?
  323. : stroke.image || stroke.colorStops ? '#000' : stroke;
  324. }
  325. function getFill(fill) {
  326. return fill == null || fill === 'none' ? null // TODO pattern and gradient?
  327. : fill.image || fill.colorStops ? '#000' : fill;
  328. }
  329. function parsePercent(value, maxValue) {
  330. if (typeof value === 'string') {
  331. if (value.lastIndexOf('%') >= 0) {
  332. return parseFloat(value) / 100 * maxValue;
  333. }
  334. return parseFloat(value);
  335. }
  336. return value;
  337. }
  338. function getTextXForPadding(x, textAlign, textPadding) {
  339. return textAlign === 'right' ? x - textPadding[1] : textAlign === 'center' ? x + textPadding[3] / 2 - textPadding[1] / 2 : x + textPadding[3];
  340. }
  341. /**
  342. * @param {string} text
  343. * @param {module:zrender/Style} style
  344. * @return {boolean}
  345. */
  346. function needDrawText(text, style) {
  347. return text != null && (text || style.textBackgroundColor || style.textBorderWidth && style.textBorderColor || style.textPadding);
  348. }
  349. exports.normalizeTextStyle = normalizeTextStyle;
  350. exports.renderText = renderText;
  351. exports.getStroke = getStroke;
  352. exports.getFill = getFill;
  353. exports.needDrawText = needDrawText;