var _util = require("../../core/util"); var retrieve2 = _util.retrieve2; var retrieve3 = _util.retrieve3; var each = _util.each; var normalizeCssArray = _util.normalizeCssArray; var isString = _util.isString; var isObject = _util.isObject; var textContain = require("../../contain/text"); var roundRectHelper = require("./roundRect"); var imageHelper = require("./image"); var fixShadow = require("./fixShadow"); var _constant = require("../constant"); var ContextCachedBy = _constant.ContextCachedBy; var WILL_BE_RESTORED = _constant.WILL_BE_RESTORED; var DEFAULT_FONT = textContain.DEFAULT_FONT; // TODO: Have not support 'start', 'end' yet. var VALID_TEXT_ALIGN = { left: 1, right: 1, center: 1 }; var VALID_TEXT_VERTICAL_ALIGN = { top: 1, bottom: 1, middle: 1 }; // Different from `STYLE_COMMON_PROPS` of `graphic/Style`, // the default value of shadowColor is `'transparent'`. var SHADOW_STYLE_COMMON_PROPS = [['textShadowBlur', 'shadowBlur', 0], ['textShadowOffsetX', 'shadowOffsetX', 0], ['textShadowOffsetY', 'shadowOffsetY', 0], ['textShadowColor', 'shadowColor', 'transparent']]; var _tmpTextPositionResult = {}; var _tmpBoxPositionResult = {}; /** * @param {module:zrender/graphic/Style} style * @return {module:zrender/graphic/Style} The input style. */ function normalizeTextStyle(style) { normalizeStyle(style); each(style.rich, normalizeStyle); return style; } function normalizeStyle(style) { if (style) { style.font = textContain.makeFont(style); var textAlign = style.textAlign; textAlign === 'middle' && (textAlign = 'center'); style.textAlign = textAlign == null || VALID_TEXT_ALIGN[textAlign] ? textAlign : 'left'; // Compatible with textBaseline. var textVerticalAlign = style.textVerticalAlign || style.textBaseline; textVerticalAlign === 'center' && (textVerticalAlign = 'middle'); style.textVerticalAlign = textVerticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[textVerticalAlign] ? textVerticalAlign : 'top'; var textPadding = style.textPadding; if (textPadding) { style.textPadding = normalizeCssArray(style.textPadding); } } } /** * @param {CanvasRenderingContext2D} ctx * @param {string} text * @param {module:zrender/graphic/Style} style * @param {Object|boolean} [rect] {x, y, width, height} * If set false, rect text is not used. * @param {Element|module:zrender/graphic/helper/constant.WILL_BE_RESTORED} [prevEl] For ctx prop cache. */ function renderText(hostEl, ctx, text, style, rect, prevEl) { style.rich ? renderRichText(hostEl, ctx, text, style, rect, prevEl) : renderPlainText(hostEl, ctx, text, style, rect, prevEl); } // Avoid setting to ctx according to prevEl if possible for // performance in scenarios of large amount text. function renderPlainText(hostEl, ctx, text, style, rect, prevEl) { 'use strict'; var needDrawBg = needDrawBackground(style); var prevStyle; var checkCache = false; var cachedByMe = ctx.__attrCachedBy === ContextCachedBy.PLAIN_TEXT; // Only take and check cache for `Text` el, but not RectText. if (prevEl !== WILL_BE_RESTORED) { if (prevEl) { prevStyle = prevEl.style; checkCache = !needDrawBg && cachedByMe && prevStyle; } // Prevent from using cache in `Style::bind`, because of the case: // ctx property is modified by other properties than `Style::bind` // used, and Style::bind is called next. ctx.__attrCachedBy = needDrawBg ? ContextCachedBy.NONE : ContextCachedBy.PLAIN_TEXT; } // Since this will be restored, prevent from using these props to check cache in the next // entering of this method. But do not need to clear other cache like `Style::bind`. else if (cachedByMe) { ctx.__attrCachedBy = ContextCachedBy.NONE; } var styleFont = style.font || DEFAULT_FONT; // PENDING // Only `Text` el set `font` and keep it (`RectText` will restore). So theoretically // we can make font cache on ctx, which can cache for text el that are discontinuous. // But layer save/restore needed to be considered. // if (styleFont !== ctx.__fontCache) { // ctx.font = styleFont; // if (prevEl !== WILL_BE_RESTORED) { // ctx.__fontCache = styleFont; // } // } if (!checkCache || styleFont !== (prevStyle.font || DEFAULT_FONT)) { ctx.font = styleFont; } // Use the final font from context-2d, because the final // font might not be the style.font when it is illegal. // But get `ctx.font` might be time consuming. var computedFont = hostEl.__computedFont; if (hostEl.__styleFont !== styleFont) { hostEl.__styleFont = styleFont; computedFont = hostEl.__computedFont = ctx.font; } var textPadding = style.textPadding; var textLineHeight = style.textLineHeight; var contentBlock = hostEl.__textCotentBlock; if (!contentBlock || hostEl.__dirtyText) { contentBlock = hostEl.__textCotentBlock = textContain.parsePlainText(text, computedFont, textPadding, textLineHeight, style.truncate); } var outerHeight = contentBlock.outerHeight; var textLines = contentBlock.lines; var lineHeight = contentBlock.lineHeight; var boxPos = getBoxPosition(_tmpBoxPositionResult, hostEl, style, rect); var baseX = boxPos.baseX; var baseY = boxPos.baseY; var textAlign = boxPos.textAlign || 'left'; var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing. applyTextRotation(ctx, style, rect, baseX, baseY); var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign); var textX = baseX; var textY = boxY; if (needDrawBg || textPadding) { // Consider performance, do not call getTextWidth util necessary. var textWidth = textContain.getWidth(text, computedFont); var outerWidth = textWidth; textPadding && (outerWidth += textPadding[1] + textPadding[3]); var boxX = textContain.adjustTextX(baseX, outerWidth, textAlign); needDrawBg && drawBackground(hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight); if (textPadding) { textX = getTextXForPadding(baseX, textAlign, textPadding); textY += textPadding[0]; } } // Always set textAlign and textBase line, because it is difficute to calculate // textAlign from prevEl, and we dont sure whether textAlign will be reset if // font set happened. ctx.textAlign = textAlign; // Force baseline to be "middle". Otherwise, if using "top", the // text will offset downward a little bit in font "Microsoft YaHei". ctx.textBaseline = 'middle'; // Set text opacity ctx.globalAlpha = style.opacity || 1; // Always set shadowBlur and shadowOffset to avoid leak from displayable. for (var i = 0; i < SHADOW_STYLE_COMMON_PROPS.length; i++) { var propItem = SHADOW_STYLE_COMMON_PROPS[i]; var styleProp = propItem[0]; var ctxProp = propItem[1]; var val = style[styleProp]; if (!checkCache || val !== prevStyle[styleProp]) { ctx[ctxProp] = fixShadow(ctx, ctxProp, val || propItem[2]); } } // `textBaseline` is set as 'middle'. textY += lineHeight / 2; var textStrokeWidth = style.textStrokeWidth; var textStrokeWidthPrev = checkCache ? prevStyle.textStrokeWidth : null; var strokeWidthChanged = !checkCache || textStrokeWidth !== textStrokeWidthPrev; var strokeChanged = !checkCache || strokeWidthChanged || style.textStroke !== prevStyle.textStroke; var textStroke = getStroke(style.textStroke, textStrokeWidth); var textFill = getFill(style.textFill); if (textStroke) { if (strokeWidthChanged) { ctx.lineWidth = textStrokeWidth; } if (strokeChanged) { ctx.strokeStyle = textStroke; } } if (textFill) { if (!checkCache || style.textFill !== prevStyle.textFill) { ctx.fillStyle = textFill; } } // Optimize simply, in most cases only one line exists. if (textLines.length === 1) { // Fill after stroke so the outline will not cover the main part. textStroke && ctx.strokeText(textLines[0], textX, textY); textFill && ctx.fillText(textLines[0], textX, textY); } else { for (var i = 0; i < textLines.length; i++) { // Fill after stroke so the outline will not cover the main part. textStroke && ctx.strokeText(textLines[i], textX, textY); textFill && ctx.fillText(textLines[i], textX, textY); textY += lineHeight; } } } function renderRichText(hostEl, ctx, text, style, rect, prevEl) { // Do not do cache for rich text because of the complexity. // But `RectText` this will be restored, do not need to clear other cache like `Style::bind`. if (prevEl !== WILL_BE_RESTORED) { ctx.__attrCachedBy = ContextCachedBy.NONE; } var contentBlock = hostEl.__textCotentBlock; if (!contentBlock || hostEl.__dirtyText) { contentBlock = hostEl.__textCotentBlock = textContain.parseRichText(text, style); } drawRichText(hostEl, ctx, contentBlock, style, rect); } function drawRichText(hostEl, ctx, contentBlock, style, rect) { var contentWidth = contentBlock.width; var outerWidth = contentBlock.outerWidth; var outerHeight = contentBlock.outerHeight; var textPadding = style.textPadding; var boxPos = getBoxPosition(_tmpBoxPositionResult, hostEl, style, rect); var baseX = boxPos.baseX; var baseY = boxPos.baseY; var textAlign = boxPos.textAlign; var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing. applyTextRotation(ctx, style, rect, baseX, baseY); var boxX = textContain.adjustTextX(baseX, outerWidth, textAlign); var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign); var xLeft = boxX; var lineTop = boxY; if (textPadding) { xLeft += textPadding[3]; lineTop += textPadding[0]; } var xRight = xLeft + contentWidth; needDrawBackground(style) && drawBackground(hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight); for (var i = 0; i < contentBlock.lines.length; i++) { var line = contentBlock.lines[i]; var tokens = line.tokens; var tokenCount = tokens.length; var lineHeight = line.lineHeight; var usedWidth = line.width; var leftIndex = 0; var lineXLeft = xLeft; var lineXRight = xRight; var rightIndex = tokenCount - 1; var token; while (leftIndex < tokenCount && (token = tokens[leftIndex], !token.textAlign || token.textAlign === 'left')) { placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft, 'left'); usedWidth -= token.width; lineXLeft += token.width; leftIndex++; } while (rightIndex >= 0 && (token = tokens[rightIndex], token.textAlign === 'right')) { placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXRight, 'right'); usedWidth -= token.width; lineXRight -= token.width; rightIndex--; } // The other tokens are placed as textAlign 'center' if there is enough space. lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - usedWidth) / 2; while (leftIndex <= rightIndex) { token = tokens[leftIndex]; // Consider width specified by user, use 'center' rather than 'left'. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft + token.width / 2, 'center'); lineXLeft += token.width; leftIndex++; } lineTop += lineHeight; } } function applyTextRotation(ctx, style, rect, x, y) { // textRotation only apply in RectText. if (rect && style.textRotation) { var origin = style.textOrigin; if (origin === 'center') { x = rect.width / 2 + rect.x; y = rect.height / 2 + rect.y; } else if (origin) { x = origin[0] + rect.x; y = origin[1] + rect.y; } ctx.translate(x, y); // Positive: anticlockwise ctx.rotate(-style.textRotation); ctx.translate(-x, -y); } } function placeToken(hostEl, ctx, token, style, lineHeight, lineTop, x, textAlign) { var tokenStyle = style.rich[token.styleName] || {}; tokenStyle.text = token.text; // 'ctx.textBaseline' is always set as 'middle', for sake of // the bias of "Microsoft YaHei". var textVerticalAlign = token.textVerticalAlign; var y = lineTop + lineHeight / 2; if (textVerticalAlign === 'top') { y = lineTop + token.height / 2; } else if (textVerticalAlign === 'bottom') { y = lineTop + lineHeight - token.height / 2; } !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); var textPadding = token.textPadding; if (textPadding) { x = getTextXForPadding(x, textAlign, textPadding); y -= token.height / 2 - textPadding[2] - token.textHeight / 2; } setCtx(ctx, 'shadowBlur', retrieve3(tokenStyle.textShadowBlur, style.textShadowBlur, 0)); setCtx(ctx, 'shadowColor', tokenStyle.textShadowColor || style.textShadowColor || 'transparent'); setCtx(ctx, 'shadowOffsetX', retrieve3(tokenStyle.textShadowOffsetX, style.textShadowOffsetX, 0)); setCtx(ctx, 'shadowOffsetY', retrieve3(tokenStyle.textShadowOffsetY, style.textShadowOffsetY, 0)); setCtx(ctx, 'textAlign', textAlign); // Force baseline to be "middle". Otherwise, if using "top", the // text will offset downward a little bit in font "Microsoft YaHei". setCtx(ctx, 'textBaseline', 'middle'); setCtx(ctx, 'font', token.font || DEFAULT_FONT); var textStroke = getStroke(tokenStyle.textStroke || style.textStroke, textStrokeWidth); var textFill = getFill(tokenStyle.textFill || style.textFill); var textStrokeWidth = retrieve2(tokenStyle.textStrokeWidth, style.textStrokeWidth); // Fill after stroke so the outline will not cover the main part. if (textStroke) { setCtx(ctx, 'lineWidth', textStrokeWidth); setCtx(ctx, 'strokeStyle', textStroke); ctx.strokeText(token.text, x, y); } if (textFill) { setCtx(ctx, 'fillStyle', textFill); ctx.fillText(token.text, x, y); } } function needDrawBackground(style) { return !!(style.textBackgroundColor || style.textBorderWidth && style.textBorderColor); } // style: {textBackgroundColor, textBorderWidth, textBorderColor, textBorderRadius, text} // shape: {x, y, width, height} function drawBackground(hostEl, ctx, style, x, y, width, height) { var textBackgroundColor = style.textBackgroundColor; var textBorderWidth = style.textBorderWidth; var textBorderColor = style.textBorderColor; var isPlainBg = isString(textBackgroundColor); setCtx(ctx, 'shadowBlur', style.textBoxShadowBlur || 0); setCtx(ctx, 'shadowColor', style.textBoxShadowColor || 'transparent'); setCtx(ctx, 'shadowOffsetX', style.textBoxShadowOffsetX || 0); setCtx(ctx, 'shadowOffsetY', style.textBoxShadowOffsetY || 0); if (isPlainBg || textBorderWidth && textBorderColor) { ctx.beginPath(); var textBorderRadius = style.textBorderRadius; if (!textBorderRadius) { ctx.rect(x, y, width, height); } else { roundRectHelper.buildPath(ctx, { x: x, y: y, width: width, height: height, r: textBorderRadius }); } ctx.closePath(); } if (isPlainBg) { setCtx(ctx, 'fillStyle', textBackgroundColor); if (style.fillOpacity != null) { var originalGlobalAlpha = ctx.globalAlpha; ctx.globalAlpha = style.fillOpacity * style.opacity; ctx.fill(); ctx.globalAlpha = originalGlobalAlpha; } else { ctx.fill(); } } else if (isObject(textBackgroundColor)) { var image = textBackgroundColor.image; image = imageHelper.createOrUpdateImage(image, null, hostEl, onBgImageLoaded, textBackgroundColor); if (image && imageHelper.isImageReady(image)) { ctx.drawImage(image, x, y, width, height); } } if (textBorderWidth && textBorderColor) { setCtx(ctx, 'lineWidth', textBorderWidth); setCtx(ctx, 'strokeStyle', textBorderColor); if (style.strokeOpacity != null) { var originalGlobalAlpha = ctx.globalAlpha; ctx.globalAlpha = style.strokeOpacity * style.opacity; ctx.stroke(); ctx.globalAlpha = originalGlobalAlpha; } else { ctx.stroke(); } } } function onBgImageLoaded(image, textBackgroundColor) { // Replace image, so that `contain/text.js#parseRichText` // will get correct result in next tick. textBackgroundColor.image = image; } function getBoxPosition(out, hostEl, style, rect) { var baseX = style.x || 0; var baseY = style.y || 0; var textAlign = style.textAlign; var textVerticalAlign = style.textVerticalAlign; // Text position represented by coord if (rect) { var textPosition = style.textPosition; if (textPosition instanceof Array) { // Percent baseX = rect.x + parsePercent(textPosition[0], rect.width); baseY = rect.y + parsePercent(textPosition[1], rect.height); } else { var res = hostEl && hostEl.calculateTextPosition ? hostEl.calculateTextPosition(_tmpTextPositionResult, style, rect) : textContain.calculateTextPosition(_tmpTextPositionResult, style, rect); baseX = res.x; baseY = res.y; // Default align and baseline when has textPosition textAlign = textAlign || res.textAlign; textVerticalAlign = textVerticalAlign || res.textVerticalAlign; } // textOffset is only support in RectText, otherwise // we have to adjust boundingRect for textOffset. var textOffset = style.textOffset; if (textOffset) { baseX += textOffset[0]; baseY += textOffset[1]; } } out = out || {}; out.baseX = baseX; out.baseY = baseY; out.textAlign = textAlign; out.textVerticalAlign = textVerticalAlign; return out; } function setCtx(ctx, prop, value) { ctx[prop] = fixShadow(ctx, prop, value); return ctx[prop]; } /** * @param {string} [stroke] If specified, do not check style.textStroke. * @param {string} [lineWidth] If specified, do not check style.textStroke. * @param {number} style */ function getStroke(stroke, lineWidth) { return stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none' ? null // TODO pattern and gradient? : stroke.image || stroke.colorStops ? '#000' : stroke; } function getFill(fill) { return fill == null || fill === 'none' ? null // TODO pattern and gradient? : fill.image || fill.colorStops ? '#000' : fill; } function parsePercent(value, maxValue) { if (typeof value === 'string') { if (value.lastIndexOf('%') >= 0) { return parseFloat(value) / 100 * maxValue; } return parseFloat(value); } return value; } function getTextXForPadding(x, textAlign, textPadding) { return textAlign === 'right' ? x - textPadding[1] : textAlign === 'center' ? x + textPadding[3] / 2 - textPadding[1] / 2 : x + textPadding[3]; } /** * @param {string} text * @param {module:zrender/Style} style * @return {boolean} */ function needDrawText(text, style) { return text != null && (text || style.textBackgroundColor || style.textBorderWidth && style.textBorderColor || style.textPadding); } exports.normalizeTextStyle = normalizeTextStyle; exports.renderText = renderText; exports.getBoxPosition = getBoxPosition; exports.getStroke = getStroke; exports.getFill = getFill; exports.parsePercent = parsePercent; exports.needDrawText = needDrawText;