| import { retrieve2, retrieve3, each, normalizeCssArray, isString, isObject } from '../../core/util'; |
| import * as textContain from '../../contain/text'; |
| import * as roundRectHelper from './roundRect'; |
| import * as imageHelper from './image'; |
| import fixShadow from './fixShadow'; |
| import { ContextCachedBy, WILL_BE_RESTORED } from '../constant'; |
| 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. |
| */ |
| |
| export 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. |
| */ |
| |
| |
| export 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 |
| */ |
| |
| |
| export function getStroke(stroke, lineWidth) { |
| return stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none' ? null // TODO pattern and gradient? |
| : stroke.image || stroke.colorStops ? '#000' : stroke; |
| } |
| export 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} |
| */ |
| |
| |
| export function needDrawText(text, style) { |
| return text != null && (text || style.textBackgroundColor || style.textBorderWidth && style.textBorderColor || style.textPadding); |
| } |