text.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  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. var fixShadow = require("./fixShadow");
  12. var _constant = require("../constant");
  13. var ContextCachedBy = _constant.ContextCachedBy;
  14. var WILL_BE_RESTORED = _constant.WILL_BE_RESTORED;
  15. var DEFAULT_FONT = textContain.DEFAULT_FONT; // TODO: Have not support 'start', 'end' yet.
  16. var VALID_TEXT_ALIGN = {
  17. left: 1,
  18. right: 1,
  19. center: 1
  20. };
  21. var VALID_TEXT_VERTICAL_ALIGN = {
  22. top: 1,
  23. bottom: 1,
  24. middle: 1
  25. }; // Different from `STYLE_COMMON_PROPS` of `graphic/Style`,
  26. // the default value of shadowColor is `'transparent'`.
  27. var SHADOW_STYLE_COMMON_PROPS = [['textShadowBlur', 'shadowBlur', 0], ['textShadowOffsetX', 'shadowOffsetX', 0], ['textShadowOffsetY', 'shadowOffsetY', 0], ['textShadowColor', 'shadowColor', 'transparent']];
  28. var _tmpTextPositionResult = {};
  29. var _tmpBoxPositionResult = {};
  30. /**
  31. * @param {module:zrender/graphic/Style} style
  32. * @return {module:zrender/graphic/Style} The input style.
  33. */
  34. function normalizeTextStyle(style) {
  35. normalizeStyle(style);
  36. each(style.rich, normalizeStyle);
  37. return style;
  38. }
  39. function normalizeStyle(style) {
  40. if (style) {
  41. style.font = textContain.makeFont(style);
  42. var textAlign = style.textAlign;
  43. textAlign === 'middle' && (textAlign = 'center');
  44. style.textAlign = textAlign == null || VALID_TEXT_ALIGN[textAlign] ? textAlign : 'left'; // Compatible with textBaseline.
  45. var textVerticalAlign = style.textVerticalAlign || style.textBaseline;
  46. textVerticalAlign === 'center' && (textVerticalAlign = 'middle');
  47. style.textVerticalAlign = textVerticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[textVerticalAlign] ? textVerticalAlign : 'top';
  48. var textPadding = style.textPadding;
  49. if (textPadding) {
  50. style.textPadding = normalizeCssArray(style.textPadding);
  51. }
  52. }
  53. }
  54. /**
  55. * @param {CanvasRenderingContext2D} ctx
  56. * @param {string} text
  57. * @param {module:zrender/graphic/Style} style
  58. * @param {Object|boolean} [rect] {x, y, width, height}
  59. * If set false, rect text is not used.
  60. * @param {Element|module:zrender/graphic/helper/constant.WILL_BE_RESTORED} [prevEl] For ctx prop cache.
  61. */
  62. function renderText(hostEl, ctx, text, style, rect, prevEl) {
  63. style.rich ? renderRichText(hostEl, ctx, text, style, rect, prevEl) : renderPlainText(hostEl, ctx, text, style, rect, prevEl);
  64. } // Avoid setting to ctx according to prevEl if possible for
  65. // performance in scenarios of large amount text.
  66. function renderPlainText(hostEl, ctx, text, style, rect, prevEl) {
  67. 'use strict';
  68. var needDrawBg = needDrawBackground(style);
  69. var prevStyle;
  70. var checkCache = false;
  71. var cachedByMe = ctx.__attrCachedBy === ContextCachedBy.PLAIN_TEXT; // Only take and check cache for `Text` el, but not RectText.
  72. if (prevEl !== WILL_BE_RESTORED) {
  73. if (prevEl) {
  74. prevStyle = prevEl.style;
  75. checkCache = !needDrawBg && cachedByMe && prevStyle;
  76. } // Prevent from using cache in `Style::bind`, because of the case:
  77. // ctx property is modified by other properties than `Style::bind`
  78. // used, and Style::bind is called next.
  79. ctx.__attrCachedBy = needDrawBg ? ContextCachedBy.NONE : ContextCachedBy.PLAIN_TEXT;
  80. } // Since this will be restored, prevent from using these props to check cache in the next
  81. // entering of this method. But do not need to clear other cache like `Style::bind`.
  82. else if (cachedByMe) {
  83. ctx.__attrCachedBy = ContextCachedBy.NONE;
  84. }
  85. var styleFont = style.font || DEFAULT_FONT; // PENDING
  86. // Only `Text` el set `font` and keep it (`RectText` will restore). So theoretically
  87. // we can make font cache on ctx, which can cache for text el that are discontinuous.
  88. // But layer save/restore needed to be considered.
  89. // if (styleFont !== ctx.__fontCache) {
  90. // ctx.font = styleFont;
  91. // if (prevEl !== WILL_BE_RESTORED) {
  92. // ctx.__fontCache = styleFont;
  93. // }
  94. // }
  95. if (!checkCache || styleFont !== (prevStyle.font || DEFAULT_FONT)) {
  96. ctx.font = styleFont;
  97. } // Use the final font from context-2d, because the final
  98. // font might not be the style.font when it is illegal.
  99. // But get `ctx.font` might be time consuming.
  100. var computedFont = hostEl.__computedFont;
  101. if (hostEl.__styleFont !== styleFont) {
  102. hostEl.__styleFont = styleFont;
  103. computedFont = hostEl.__computedFont = ctx.font;
  104. }
  105. var textPadding = style.textPadding;
  106. var textLineHeight = style.textLineHeight;
  107. var contentBlock = hostEl.__textCotentBlock;
  108. if (!contentBlock || hostEl.__dirtyText) {
  109. contentBlock = hostEl.__textCotentBlock = textContain.parsePlainText(text, computedFont, textPadding, textLineHeight, style.truncate);
  110. }
  111. var outerHeight = contentBlock.outerHeight;
  112. var textLines = contentBlock.lines;
  113. var lineHeight = contentBlock.lineHeight;
  114. var boxPos = getBoxPosition(_tmpBoxPositionResult, hostEl, style, rect);
  115. var baseX = boxPos.baseX;
  116. var baseY = boxPos.baseY;
  117. var textAlign = boxPos.textAlign || 'left';
  118. var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing.
  119. applyTextRotation(ctx, style, rect, baseX, baseY);
  120. var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign);
  121. var textX = baseX;
  122. var textY = boxY;
  123. if (needDrawBg || textPadding) {
  124. // Consider performance, do not call getTextWidth util necessary.
  125. var textWidth = textContain.getWidth(text, computedFont);
  126. var outerWidth = textWidth;
  127. textPadding && (outerWidth += textPadding[1] + textPadding[3]);
  128. var boxX = textContain.adjustTextX(baseX, outerWidth, textAlign);
  129. needDrawBg && drawBackground(hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight);
  130. if (textPadding) {
  131. textX = getTextXForPadding(baseX, textAlign, textPadding);
  132. textY += textPadding[0];
  133. }
  134. } // Always set textAlign and textBase line, because it is difficute to calculate
  135. // textAlign from prevEl, and we dont sure whether textAlign will be reset if
  136. // font set happened.
  137. ctx.textAlign = textAlign; // Force baseline to be "middle". Otherwise, if using "top", the
  138. // text will offset downward a little bit in font "Microsoft YaHei".
  139. ctx.textBaseline = 'middle'; // Set text opacity
  140. ctx.globalAlpha = style.opacity || 1; // Always set shadowBlur and shadowOffset to avoid leak from displayable.
  141. for (var i = 0; i < SHADOW_STYLE_COMMON_PROPS.length; i++) {
  142. var propItem = SHADOW_STYLE_COMMON_PROPS[i];
  143. var styleProp = propItem[0];
  144. var ctxProp = propItem[1];
  145. var val = style[styleProp];
  146. if (!checkCache || val !== prevStyle[styleProp]) {
  147. ctx[ctxProp] = fixShadow(ctx, ctxProp, val || propItem[2]);
  148. }
  149. } // `textBaseline` is set as 'middle'.
  150. textY += lineHeight / 2;
  151. var textStrokeWidth = style.textStrokeWidth;
  152. var textStrokeWidthPrev = checkCache ? prevStyle.textStrokeWidth : null;
  153. var strokeWidthChanged = !checkCache || textStrokeWidth !== textStrokeWidthPrev;
  154. var strokeChanged = !checkCache || strokeWidthChanged || style.textStroke !== prevStyle.textStroke;
  155. var textStroke = getStroke(style.textStroke, textStrokeWidth);
  156. var textFill = getFill(style.textFill);
  157. if (textStroke) {
  158. if (strokeWidthChanged) {
  159. ctx.lineWidth = textStrokeWidth;
  160. }
  161. if (strokeChanged) {
  162. ctx.strokeStyle = textStroke;
  163. }
  164. }
  165. if (textFill) {
  166. if (!checkCache || style.textFill !== prevStyle.textFill) {
  167. ctx.fillStyle = textFill;
  168. }
  169. } // Optimize simply, in most cases only one line exists.
  170. if (textLines.length === 1) {
  171. // Fill after stroke so the outline will not cover the main part.
  172. textStroke && ctx.strokeText(textLines[0], textX, textY);
  173. textFill && ctx.fillText(textLines[0], textX, textY);
  174. } else {
  175. for (var i = 0; i < textLines.length; i++) {
  176. // Fill after stroke so the outline will not cover the main part.
  177. textStroke && ctx.strokeText(textLines[i], textX, textY);
  178. textFill && ctx.fillText(textLines[i], textX, textY);
  179. textY += lineHeight;
  180. }
  181. }
  182. }
  183. function renderRichText(hostEl, ctx, text, style, rect, prevEl) {
  184. // Do not do cache for rich text because of the complexity.
  185. // But `RectText` this will be restored, do not need to clear other cache like `Style::bind`.
  186. if (prevEl !== WILL_BE_RESTORED) {
  187. ctx.__attrCachedBy = ContextCachedBy.NONE;
  188. }
  189. var contentBlock = hostEl.__textCotentBlock;
  190. if (!contentBlock || hostEl.__dirtyText) {
  191. contentBlock = hostEl.__textCotentBlock = textContain.parseRichText(text, style);
  192. }
  193. drawRichText(hostEl, ctx, contentBlock, style, rect);
  194. }
  195. function drawRichText(hostEl, ctx, contentBlock, style, rect) {
  196. var contentWidth = contentBlock.width;
  197. var outerWidth = contentBlock.outerWidth;
  198. var outerHeight = contentBlock.outerHeight;
  199. var textPadding = style.textPadding;
  200. var boxPos = getBoxPosition(_tmpBoxPositionResult, hostEl, style, rect);
  201. var baseX = boxPos.baseX;
  202. var baseY = boxPos.baseY;
  203. var textAlign = boxPos.textAlign;
  204. var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing.
  205. applyTextRotation(ctx, style, rect, baseX, baseY);
  206. var boxX = textContain.adjustTextX(baseX, outerWidth, textAlign);
  207. var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign);
  208. var xLeft = boxX;
  209. var lineTop = boxY;
  210. if (textPadding) {
  211. xLeft += textPadding[3];
  212. lineTop += textPadding[0];
  213. }
  214. var xRight = xLeft + contentWidth;
  215. needDrawBackground(style) && drawBackground(hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight);
  216. for (var i = 0; i < contentBlock.lines.length; i++) {
  217. var line = contentBlock.lines[i];
  218. var tokens = line.tokens;
  219. var tokenCount = tokens.length;
  220. var lineHeight = line.lineHeight;
  221. var usedWidth = line.width;
  222. var leftIndex = 0;
  223. var lineXLeft = xLeft;
  224. var lineXRight = xRight;
  225. var rightIndex = tokenCount - 1;
  226. var token;
  227. while (leftIndex < tokenCount && (token = tokens[leftIndex], !token.textAlign || token.textAlign === 'left')) {
  228. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft, 'left');
  229. usedWidth -= token.width;
  230. lineXLeft += token.width;
  231. leftIndex++;
  232. }
  233. while (rightIndex >= 0 && (token = tokens[rightIndex], token.textAlign === 'right')) {
  234. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXRight, 'right');
  235. usedWidth -= token.width;
  236. lineXRight -= token.width;
  237. rightIndex--;
  238. } // The other tokens are placed as textAlign 'center' if there is enough space.
  239. lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - usedWidth) / 2;
  240. while (leftIndex <= rightIndex) {
  241. token = tokens[leftIndex]; // Consider width specified by user, use 'center' rather than 'left'.
  242. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft + token.width / 2, 'center');
  243. lineXLeft += token.width;
  244. leftIndex++;
  245. }
  246. lineTop += lineHeight;
  247. }
  248. }
  249. function applyTextRotation(ctx, style, rect, x, y) {
  250. // textRotation only apply in RectText.
  251. if (rect && style.textRotation) {
  252. var origin = style.textOrigin;
  253. if (origin === 'center') {
  254. x = rect.width / 2 + rect.x;
  255. y = rect.height / 2 + rect.y;
  256. } else if (origin) {
  257. x = origin[0] + rect.x;
  258. y = origin[1] + rect.y;
  259. }
  260. ctx.translate(x, y); // Positive: anticlockwise
  261. ctx.rotate(-style.textRotation);
  262. ctx.translate(-x, -y);
  263. }
  264. }
  265. function placeToken(hostEl, ctx, token, style, lineHeight, lineTop, x, textAlign) {
  266. var tokenStyle = style.rich[token.styleName] || {};
  267. tokenStyle.text = token.text; // 'ctx.textBaseline' is always set as 'middle', for sake of
  268. // the bias of "Microsoft YaHei".
  269. var textVerticalAlign = token.textVerticalAlign;
  270. var y = lineTop + lineHeight / 2;
  271. if (textVerticalAlign === 'top') {
  272. y = lineTop + token.height / 2;
  273. } else if (textVerticalAlign === 'bottom') {
  274. y = lineTop + lineHeight - token.height / 2;
  275. }
  276. !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);
  277. var textPadding = token.textPadding;
  278. if (textPadding) {
  279. x = getTextXForPadding(x, textAlign, textPadding);
  280. y -= token.height / 2 - textPadding[2] - token.textHeight / 2;
  281. }
  282. setCtx(ctx, 'shadowBlur', retrieve3(tokenStyle.textShadowBlur, style.textShadowBlur, 0));
  283. setCtx(ctx, 'shadowColor', tokenStyle.textShadowColor || style.textShadowColor || 'transparent');
  284. setCtx(ctx, 'shadowOffsetX', retrieve3(tokenStyle.textShadowOffsetX, style.textShadowOffsetX, 0));
  285. setCtx(ctx, 'shadowOffsetY', retrieve3(tokenStyle.textShadowOffsetY, style.textShadowOffsetY, 0));
  286. setCtx(ctx, 'textAlign', textAlign); // Force baseline to be "middle". Otherwise, if using "top", the
  287. // text will offset downward a little bit in font "Microsoft YaHei".
  288. setCtx(ctx, 'textBaseline', 'middle');
  289. setCtx(ctx, 'font', token.font || DEFAULT_FONT);
  290. var textStroke = getStroke(tokenStyle.textStroke || style.textStroke, textStrokeWidth);
  291. var textFill = getFill(tokenStyle.textFill || style.textFill);
  292. var textStrokeWidth = retrieve2(tokenStyle.textStrokeWidth, style.textStrokeWidth); // Fill after stroke so the outline will not cover the main part.
  293. if (textStroke) {
  294. setCtx(ctx, 'lineWidth', textStrokeWidth);
  295. setCtx(ctx, 'strokeStyle', textStroke);
  296. ctx.strokeText(token.text, x, y);
  297. }
  298. if (textFill) {
  299. setCtx(ctx, 'fillStyle', textFill);
  300. ctx.fillText(token.text, x, y);
  301. }
  302. }
  303. function needDrawBackground(style) {
  304. return !!(style.textBackgroundColor || style.textBorderWidth && style.textBorderColor);
  305. } // style: {textBackgroundColor, textBorderWidth, textBorderColor, textBorderRadius, text}
  306. // shape: {x, y, width, height}
  307. function drawBackground(hostEl, ctx, style, x, y, width, height) {
  308. var textBackgroundColor = style.textBackgroundColor;
  309. var textBorderWidth = style.textBorderWidth;
  310. var textBorderColor = style.textBorderColor;
  311. var isPlainBg = isString(textBackgroundColor);
  312. setCtx(ctx, 'shadowBlur', style.textBoxShadowBlur || 0);
  313. setCtx(ctx, 'shadowColor', style.textBoxShadowColor || 'transparent');
  314. setCtx(ctx, 'shadowOffsetX', style.textBoxShadowOffsetX || 0);
  315. setCtx(ctx, 'shadowOffsetY', style.textBoxShadowOffsetY || 0);
  316. if (isPlainBg || textBorderWidth && textBorderColor) {
  317. ctx.beginPath();
  318. var textBorderRadius = style.textBorderRadius;
  319. if (!textBorderRadius) {
  320. ctx.rect(x, y, width, height);
  321. } else {
  322. roundRectHelper.buildPath(ctx, {
  323. x: x,
  324. y: y,
  325. width: width,
  326. height: height,
  327. r: textBorderRadius
  328. });
  329. }
  330. ctx.closePath();
  331. }
  332. if (isPlainBg) {
  333. setCtx(ctx, 'fillStyle', textBackgroundColor);
  334. if (style.fillOpacity != null) {
  335. var originalGlobalAlpha = ctx.globalAlpha;
  336. ctx.globalAlpha = style.fillOpacity * style.opacity;
  337. ctx.fill();
  338. ctx.globalAlpha = originalGlobalAlpha;
  339. } else {
  340. ctx.fill();
  341. }
  342. } else if (isObject(textBackgroundColor)) {
  343. var image = textBackgroundColor.image;
  344. image = imageHelper.createOrUpdateImage(image, null, hostEl, onBgImageLoaded, textBackgroundColor);
  345. if (image && imageHelper.isImageReady(image)) {
  346. ctx.drawImage(image, x, y, width, height);
  347. }
  348. }
  349. if (textBorderWidth && textBorderColor) {
  350. setCtx(ctx, 'lineWidth', textBorderWidth);
  351. setCtx(ctx, 'strokeStyle', textBorderColor);
  352. if (style.strokeOpacity != null) {
  353. var originalGlobalAlpha = ctx.globalAlpha;
  354. ctx.globalAlpha = style.strokeOpacity * style.opacity;
  355. ctx.stroke();
  356. ctx.globalAlpha = originalGlobalAlpha;
  357. } else {
  358. ctx.stroke();
  359. }
  360. }
  361. }
  362. function onBgImageLoaded(image, textBackgroundColor) {
  363. // Replace image, so that `contain/text.js#parseRichText`
  364. // will get correct result in next tick.
  365. textBackgroundColor.image = image;
  366. }
  367. function getBoxPosition(out, hostEl, style, rect) {
  368. var baseX = style.x || 0;
  369. var baseY = style.y || 0;
  370. var textAlign = style.textAlign;
  371. var textVerticalAlign = style.textVerticalAlign; // Text position represented by coord
  372. if (rect) {
  373. var textPosition = style.textPosition;
  374. if (textPosition instanceof Array) {
  375. // Percent
  376. baseX = rect.x + parsePercent(textPosition[0], rect.width);
  377. baseY = rect.y + parsePercent(textPosition[1], rect.height);
  378. } else {
  379. var res = hostEl && hostEl.calculateTextPosition ? hostEl.calculateTextPosition(_tmpTextPositionResult, style, rect) : textContain.calculateTextPosition(_tmpTextPositionResult, style, rect);
  380. baseX = res.x;
  381. baseY = res.y; // Default align and baseline when has textPosition
  382. textAlign = textAlign || res.textAlign;
  383. textVerticalAlign = textVerticalAlign || res.textVerticalAlign;
  384. } // textOffset is only support in RectText, otherwise
  385. // we have to adjust boundingRect for textOffset.
  386. var textOffset = style.textOffset;
  387. if (textOffset) {
  388. baseX += textOffset[0];
  389. baseY += textOffset[1];
  390. }
  391. }
  392. out = out || {};
  393. out.baseX = baseX;
  394. out.baseY = baseY;
  395. out.textAlign = textAlign;
  396. out.textVerticalAlign = textVerticalAlign;
  397. return out;
  398. }
  399. function setCtx(ctx, prop, value) {
  400. ctx[prop] = fixShadow(ctx, prop, value);
  401. return ctx[prop];
  402. }
  403. /**
  404. * @param {string} [stroke] If specified, do not check style.textStroke.
  405. * @param {string} [lineWidth] If specified, do not check style.textStroke.
  406. * @param {number} style
  407. */
  408. function getStroke(stroke, lineWidth) {
  409. return stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none' ? null // TODO pattern and gradient?
  410. : stroke.image || stroke.colorStops ? '#000' : stroke;
  411. }
  412. function getFill(fill) {
  413. return fill == null || fill === 'none' ? null // TODO pattern and gradient?
  414. : fill.image || fill.colorStops ? '#000' : fill;
  415. }
  416. function parsePercent(value, maxValue) {
  417. if (typeof value === 'string') {
  418. if (value.lastIndexOf('%') >= 0) {
  419. return parseFloat(value) / 100 * maxValue;
  420. }
  421. return parseFloat(value);
  422. }
  423. return value;
  424. }
  425. function getTextXForPadding(x, textAlign, textPadding) {
  426. return textAlign === 'right' ? x - textPadding[1] : textAlign === 'center' ? x + textPadding[3] / 2 - textPadding[1] / 2 : x + textPadding[3];
  427. }
  428. /**
  429. * @param {string} text
  430. * @param {module:zrender/Style} style
  431. * @return {boolean}
  432. */
  433. function needDrawText(text, style) {
  434. return text != null && (text || style.textBackgroundColor || style.textBorderWidth && style.textBorderColor || style.textPadding);
  435. }
  436. exports.normalizeTextStyle = normalizeTextStyle;
  437. exports.renderText = renderText;
  438. exports.getBoxPosition = getBoxPosition;
  439. exports.getStroke = getStroke;
  440. exports.getFill = getFill;
  441. exports.parsePercent = parsePercent;
  442. exports.needDrawText = needDrawText;