| import { retrieve2, retrieve3, each, normalizeCssArray, isString, isObject, isFunction } from '../../core/util'; |
| import * as textContain from '../../contain/text'; |
| import * as roundRectHelper from './roundRect'; |
| import * as imageHelper from './image'; |
| import fixShadow from './fixShadow'; // 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']]; |
| /** |
| * @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} [prevEl] For ctx prop cache. |
| */ |
| |
| |
| export function renderText(hostEl, ctx, text, style, rect, prevEl) { |
| style.rich ? renderRichText(hostEl, ctx, text, style, rect) : 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 prevStyle = prevEl && prevEl.style; // Some cache only available on textEl. |
| |
| var isPrevTextEl = prevStyle && prevEl.type === 'text'; |
| var styleFont = style.font || textContain.DEFAULT_FONT; |
| |
| if (!isPrevTextEl || styleFont !== (prevStyle.font || textContain.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 contentBlock = hostEl.__textCotentBlock; |
| |
| if (!contentBlock || hostEl.__dirtyText) { |
| contentBlock = hostEl.__textCotentBlock = textContain.parsePlainText(text, computedFont, textPadding, style.truncate); |
| } |
| |
| var outerHeight = contentBlock.outerHeight; |
| var textLines = contentBlock.lines; |
| var lineHeight = contentBlock.lineHeight; |
| var boxPos = getBoxPosition(outerHeight, 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; |
| var needDrawBg = needDrawBackground(style); |
| |
| 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'; // 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 (!isPrevTextEl || 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 = isPrevTextEl ? prevStyle.textStrokeWidth : null; |
| var strokeWidthChanged = !isPrevTextEl || textStrokeWidth !== textStrokeWidthPrev; |
| var strokeChanged = !isPrevTextEl || 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 (!isPrevTextEl || style.textFill !== prevStyle.textFill || prevStyle.textBackgroundColor) { |
| 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) { |
| 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(outerHeight, 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 || textContain.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 (isFunction(textBackgroundColor)) { |
| setCtx(ctx, 'fillStyle', textBackgroundColor(style)); |
| 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(blockHeiht, 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 = textContain.adjustTextPositionOnRect(textPosition, rect, style.textDistance); |
| 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]; |
| } |
| } |
| |
| return { |
| baseX: baseX, |
| baseY: baseY, |
| textAlign: textAlign, |
| textVerticalAlign: textVerticalAlign |
| }; |
| } |
| |
| 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); |
| } |