123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719 |
- var BoundingRect = require("../core/BoundingRect");
- var imageHelper = require("../graphic/helper/image");
- var _util = require("../core/util");
- var getContext = _util.getContext;
- var extend = _util.extend;
- var retrieve2 = _util.retrieve2;
- var retrieve3 = _util.retrieve3;
- var trim = _util.trim;
- var textWidthCache = {};
- var textWidthCacheCounter = 0;
- var TEXT_CACHE_MAX = 5000;
- var STYLE_REG = /\{([a-zA-Z0-9_]+)\|([^}]*)\}/g;
- var DEFAULT_FONT = '12px sans-serif'; // Avoid assign to an exported variable, for transforming to cjs.
- var methods = {};
- function $override(name, fn) {
- methods[name] = fn;
- }
- /**
- * @public
- * @param {string} text
- * @param {string} font
- * @return {number} width
- */
- function getWidth(text, font) {
- font = font || DEFAULT_FONT;
- var key = text + ':' + font;
- if (textWidthCache[key]) {
- return textWidthCache[key];
- }
- var textLines = (text + '').split('\n');
- var width = 0;
- for (var i = 0, l = textLines.length; i < l; i++) {
- // textContain.measureText may be overrided in SVG or VML
- width = Math.max(measureText(textLines[i], font).width, width);
- }
- if (textWidthCacheCounter > TEXT_CACHE_MAX) {
- textWidthCacheCounter = 0;
- textWidthCache = {};
- }
- textWidthCacheCounter++;
- textWidthCache[key] = width;
- return width;
- }
- /**
- * @public
- * @param {string} text
- * @param {string} font
- * @param {string} [textAlign='left']
- * @param {string} [textVerticalAlign='top']
- * @param {Array.<number>} [textPadding]
- * @param {Object} [rich]
- * @param {Object} [truncate]
- * @return {Object} {x, y, width, height, lineHeight}
- */
- function getBoundingRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, rich, truncate) {
- return rich ? getRichTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, rich, truncate) : getPlainTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, truncate);
- }
- function getPlainTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, truncate) {
- var contentBlock = parsePlainText(text, font, textPadding, textLineHeight, truncate);
- var outerWidth = getWidth(text, font);
- if (textPadding) {
- outerWidth += textPadding[1] + textPadding[3];
- }
- var outerHeight = contentBlock.outerHeight;
- var x = adjustTextX(0, outerWidth, textAlign);
- var y = adjustTextY(0, outerHeight, textVerticalAlign);
- var rect = new BoundingRect(x, y, outerWidth, outerHeight);
- rect.lineHeight = contentBlock.lineHeight;
- return rect;
- }
- function getRichTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, rich, truncate) {
- var contentBlock = parseRichText(text, {
- rich: rich,
- truncate: truncate,
- font: font,
- textAlign: textAlign,
- textPadding: textPadding,
- textLineHeight: textLineHeight
- });
- var outerWidth = contentBlock.outerWidth;
- var outerHeight = contentBlock.outerHeight;
- var x = adjustTextX(0, outerWidth, textAlign);
- var y = adjustTextY(0, outerHeight, textVerticalAlign);
- return new BoundingRect(x, y, outerWidth, outerHeight);
- }
- /**
- * @public
- * @param {number} x
- * @param {number} width
- * @param {string} [textAlign='left']
- * @return {number} Adjusted x.
- */
- function adjustTextX(x, width, textAlign) {
- // FIXME Right to left language
- if (textAlign === 'right') {
- x -= width;
- } else if (textAlign === 'center') {
- x -= width / 2;
- }
- return x;
- }
- /**
- * @public
- * @param {number} y
- * @param {number} height
- * @param {string} [textVerticalAlign='top']
- * @return {number} Adjusted y.
- */
- function adjustTextY(y, height, textVerticalAlign) {
- if (textVerticalAlign === 'middle') {
- y -= height / 2;
- } else if (textVerticalAlign === 'bottom') {
- y -= height;
- }
- return y;
- }
- /**
- * Follow same interface to `Displayable.prototype.calculateTextPosition`.
- * @public
- * @param {Obejct} [out] Prepared out object. If not input, auto created in the method.
- * @param {module:zrender/graphic/Style} style where `textPosition` and `textDistance` are visited.
- * @param {Object} rect {x, y, width, height} Rect of the host elment, according to which the text positioned.
- * @return {Object} The input `out`. Set: {x, y, textAlign, textVerticalAlign}
- */
- function calculateTextPosition(out, style, rect) {
- var textPosition = style.textPosition;
- var distance = style.textDistance;
- var x = rect.x;
- var y = rect.y;
- distance = distance || 0;
- var height = rect.height;
- var width = rect.width;
- var halfHeight = height / 2;
- var textAlign = 'left';
- var textVerticalAlign = 'top';
- switch (textPosition) {
- case 'left':
- x -= distance;
- y += halfHeight;
- textAlign = 'right';
- textVerticalAlign = 'middle';
- break;
- case 'right':
- x += distance + width;
- y += halfHeight;
- textVerticalAlign = 'middle';
- break;
- case 'top':
- x += width / 2;
- y -= distance;
- textAlign = 'center';
- textVerticalAlign = 'bottom';
- break;
- case 'bottom':
- x += width / 2;
- y += height + distance;
- textAlign = 'center';
- break;
- case 'inside':
- x += width / 2;
- y += halfHeight;
- textAlign = 'center';
- textVerticalAlign = 'middle';
- break;
- case 'insideLeft':
- x += distance;
- y += halfHeight;
- textVerticalAlign = 'middle';
- break;
- case 'insideRight':
- x += width - distance;
- y += halfHeight;
- textAlign = 'right';
- textVerticalAlign = 'middle';
- break;
- case 'insideTop':
- x += width / 2;
- y += distance;
- textAlign = 'center';
- break;
- case 'insideBottom':
- x += width / 2;
- y += height - distance;
- textAlign = 'center';
- textVerticalAlign = 'bottom';
- break;
- case 'insideTopLeft':
- x += distance;
- y += distance;
- break;
- case 'insideTopRight':
- x += width - distance;
- y += distance;
- textAlign = 'right';
- break;
- case 'insideBottomLeft':
- x += distance;
- y += height - distance;
- textVerticalAlign = 'bottom';
- break;
- case 'insideBottomRight':
- x += width - distance;
- y += height - distance;
- textAlign = 'right';
- textVerticalAlign = 'bottom';
- break;
- }
- out = out || {};
- out.x = x;
- out.y = y;
- out.textAlign = textAlign;
- out.textVerticalAlign = textVerticalAlign;
- return out;
- }
- /**
- * To be removed. But still do not remove in case that some one has imported it.
- * @deprecated
- * @public
- * @param {stirng} textPosition
- * @param {Object} rect {x, y, width, height}
- * @param {number} distance
- * @return {Object} {x, y, textAlign, textVerticalAlign}
- */
- function adjustTextPositionOnRect(textPosition, rect, distance) {
- var dummyStyle = {
- textPosition: textPosition,
- textDistance: distance
- };
- return calculateTextPosition({}, dummyStyle, rect);
- }
- /**
- * Show ellipsis if overflow.
- *
- * @public
- * @param {string} text
- * @param {string} containerWidth
- * @param {string} font
- * @param {number} [ellipsis='...']
- * @param {Object} [options]
- * @param {number} [options.maxIterations=3]
- * @param {number} [options.minChar=0] If truncate result are less
- * then minChar, ellipsis will not show, which is
- * better for user hint in some cases.
- * @param {number} [options.placeholder=''] When all truncated, use the placeholder.
- * @return {string}
- */
- function truncateText(text, containerWidth, font, ellipsis, options) {
- if (!containerWidth) {
- return '';
- }
- var textLines = (text + '').split('\n');
- options = prepareTruncateOptions(containerWidth, font, ellipsis, options); // FIXME
- // It is not appropriate that every line has '...' when truncate multiple lines.
- for (var i = 0, len = textLines.length; i < len; i++) {
- textLines[i] = truncateSingleLine(textLines[i], options);
- }
- return textLines.join('\n');
- }
- function prepareTruncateOptions(containerWidth, font, ellipsis, options) {
- options = extend({}, options);
- options.font = font;
- var ellipsis = retrieve2(ellipsis, '...');
- options.maxIterations = retrieve2(options.maxIterations, 2);
- var minChar = options.minChar = retrieve2(options.minChar, 0); // FIXME
- // Other languages?
- options.cnCharWidth = getWidth('国', font); // FIXME
- // Consider proportional font?
- var ascCharWidth = options.ascCharWidth = getWidth('a', font);
- options.placeholder = retrieve2(options.placeholder, ''); // Example 1: minChar: 3, text: 'asdfzxcv', truncate result: 'asdf', but not: 'a...'.
- // Example 2: minChar: 3, text: '维度', truncate result: '维', but not: '...'.
- var contentWidth = containerWidth = Math.max(0, containerWidth - 1); // Reserve some gap.
- for (var i = 0; i < minChar && contentWidth >= ascCharWidth; i++) {
- contentWidth -= ascCharWidth;
- }
- var ellipsisWidth = getWidth(ellipsis, font);
- if (ellipsisWidth > contentWidth) {
- ellipsis = '';
- ellipsisWidth = 0;
- }
- contentWidth = containerWidth - ellipsisWidth;
- options.ellipsis = ellipsis;
- options.ellipsisWidth = ellipsisWidth;
- options.contentWidth = contentWidth;
- options.containerWidth = containerWidth;
- return options;
- }
- function truncateSingleLine(textLine, options) {
- var containerWidth = options.containerWidth;
- var font = options.font;
- var contentWidth = options.contentWidth;
- if (!containerWidth) {
- return '';
- }
- var lineWidth = getWidth(textLine, font);
- if (lineWidth <= containerWidth) {
- return textLine;
- }
- for (var j = 0;; j++) {
- if (lineWidth <= contentWidth || j >= options.maxIterations) {
- textLine += options.ellipsis;
- break;
- }
- var subLength = j === 0 ? estimateLength(textLine, contentWidth, options.ascCharWidth, options.cnCharWidth) : lineWidth > 0 ? Math.floor(textLine.length * contentWidth / lineWidth) : 0;
- textLine = textLine.substr(0, subLength);
- lineWidth = getWidth(textLine, font);
- }
- if (textLine === '') {
- textLine = options.placeholder;
- }
- return textLine;
- }
- function estimateLength(text, contentWidth, ascCharWidth, cnCharWidth) {
- var width = 0;
- var i = 0;
- for (var len = text.length; i < len && width < contentWidth; i++) {
- var charCode = text.charCodeAt(i);
- width += 0 <= charCode && charCode <= 127 ? ascCharWidth : cnCharWidth;
- }
- return i;
- }
- /**
- * @public
- * @param {string} font
- * @return {number} line height
- */
- function getLineHeight(font) {
- // FIXME A rough approach.
- return getWidth('国', font);
- }
- /**
- * @public
- * @param {string} text
- * @param {string} font
- * @return {Object} width
- */
- function measureText(text, font) {
- return methods.measureText(text, font);
- } // Avoid assign to an exported variable, for transforming to cjs.
- methods.measureText = function (text, font) {
- var ctx = getContext();
- ctx.font = font || DEFAULT_FONT;
- return ctx.measureText(text);
- };
- /**
- * @public
- * @param {string} text
- * @param {string} font
- * @param {Object} [truncate]
- * @return {Object} block: {lineHeight, lines, height, outerHeight, canCacheByTextString}
- * Notice: for performance, do not calculate outerWidth util needed.
- * `canCacheByTextString` means the result `lines` is only determined by the input `text`.
- * Thus we can simply comparing the `input` text to determin whether the result changed,
- * without travel the result `lines`.
- */
- function parsePlainText(text, font, padding, textLineHeight, truncate) {
- text != null && (text += '');
- var lineHeight = retrieve2(textLineHeight, getLineHeight(font));
- var lines = text ? text.split('\n') : [];
- var height = lines.length * lineHeight;
- var outerHeight = height;
- var canCacheByTextString = true;
- if (padding) {
- outerHeight += padding[0] + padding[2];
- }
- if (text && truncate) {
- canCacheByTextString = false;
- var truncOuterHeight = truncate.outerHeight;
- var truncOuterWidth = truncate.outerWidth;
- if (truncOuterHeight != null && outerHeight > truncOuterHeight) {
- text = '';
- lines = [];
- } else if (truncOuterWidth != null) {
- var options = prepareTruncateOptions(truncOuterWidth - (padding ? padding[1] + padding[3] : 0), font, truncate.ellipsis, {
- minChar: truncate.minChar,
- placeholder: truncate.placeholder
- }); // FIXME
- // It is not appropriate that every line has '...' when truncate multiple lines.
- for (var i = 0, len = lines.length; i < len; i++) {
- lines[i] = truncateSingleLine(lines[i], options);
- }
- }
- }
- return {
- lines: lines,
- height: height,
- outerHeight: outerHeight,
- lineHeight: lineHeight,
- canCacheByTextString: canCacheByTextString
- };
- }
- /**
- * For example: 'some text {a|some text}other text{b|some text}xxx{c|}xxx'
- * Also consider 'bbbb{a|xxx\nzzz}xxxx\naaaa'.
- *
- * @public
- * @param {string} text
- * @param {Object} style
- * @return {Object} block
- * {
- * width,
- * height,
- * lines: [{
- * lineHeight,
- * width,
- * tokens: [[{
- * styleName,
- * text,
- * width, // include textPadding
- * height, // include textPadding
- * textWidth, // pure text width
- * textHeight, // pure text height
- * lineHeihgt,
- * font,
- * textAlign,
- * textVerticalAlign
- * }], [...], ...]
- * }, ...]
- * }
- * If styleName is undefined, it is plain text.
- */
- function parseRichText(text, style) {
- var contentBlock = {
- lines: [],
- width: 0,
- height: 0
- };
- text != null && (text += '');
- if (!text) {
- return contentBlock;
- }
- var lastIndex = STYLE_REG.lastIndex = 0;
- var result;
- while ((result = STYLE_REG.exec(text)) != null) {
- var matchedIndex = result.index;
- if (matchedIndex > lastIndex) {
- pushTokens(contentBlock, text.substring(lastIndex, matchedIndex));
- }
- pushTokens(contentBlock, result[2], result[1]);
- lastIndex = STYLE_REG.lastIndex;
- }
- if (lastIndex < text.length) {
- pushTokens(contentBlock, text.substring(lastIndex, text.length));
- }
- var lines = contentBlock.lines;
- var contentHeight = 0;
- var contentWidth = 0; // For `textWidth: 100%`
- var pendingList = [];
- var stlPadding = style.textPadding;
- var truncate = style.truncate;
- var truncateWidth = truncate && truncate.outerWidth;
- var truncateHeight = truncate && truncate.outerHeight;
- if (stlPadding) {
- truncateWidth != null && (truncateWidth -= stlPadding[1] + stlPadding[3]);
- truncateHeight != null && (truncateHeight -= stlPadding[0] + stlPadding[2]);
- } // Calculate layout info of tokens.
- for (var i = 0; i < lines.length; i++) {
- var line = lines[i];
- var lineHeight = 0;
- var lineWidth = 0;
- for (var j = 0; j < line.tokens.length; j++) {
- var token = line.tokens[j];
- var tokenStyle = token.styleName && style.rich[token.styleName] || {}; // textPadding should not inherit from style.
- var textPadding = token.textPadding = tokenStyle.textPadding; // textFont has been asigned to font by `normalizeStyle`.
- var font = token.font = tokenStyle.font || style.font; // textHeight can be used when textVerticalAlign is specified in token.
- var tokenHeight = token.textHeight = retrieve2( // textHeight should not be inherited, consider it can be specified
- // as box height of the block.
- tokenStyle.textHeight, getLineHeight(font));
- textPadding && (tokenHeight += textPadding[0] + textPadding[2]);
- token.height = tokenHeight;
- token.lineHeight = retrieve3(tokenStyle.textLineHeight, style.textLineHeight, tokenHeight);
- token.textAlign = tokenStyle && tokenStyle.textAlign || style.textAlign;
- token.textVerticalAlign = tokenStyle && tokenStyle.textVerticalAlign || 'middle';
- if (truncateHeight != null && contentHeight + token.lineHeight > truncateHeight) {
- return {
- lines: [],
- width: 0,
- height: 0
- };
- }
- token.textWidth = getWidth(token.text, font);
- var tokenWidth = tokenStyle.textWidth;
- var tokenWidthNotSpecified = tokenWidth == null || tokenWidth === 'auto'; // Percent width, can be `100%`, can be used in drawing separate
- // line when box width is needed to be auto.
- if (typeof tokenWidth === 'string' && tokenWidth.charAt(tokenWidth.length - 1) === '%') {
- token.percentWidth = tokenWidth;
- pendingList.push(token);
- tokenWidth = 0; // Do not truncate in this case, because there is no user case
- // and it is too complicated.
- } else {
- if (tokenWidthNotSpecified) {
- tokenWidth = token.textWidth; // FIXME: If image is not loaded and textWidth is not specified, calling
- // `getBoundingRect()` will not get correct result.
- var textBackgroundColor = tokenStyle.textBackgroundColor;
- var bgImg = textBackgroundColor && textBackgroundColor.image; // Use cases:
- // (1) If image is not loaded, it will be loaded at render phase and call
- // `dirty()` and `textBackgroundColor.image` will be replaced with the loaded
- // image, and then the right size will be calculated here at the next tick.
- // See `graphic/helper/text.js`.
- // (2) If image loaded, and `textBackgroundColor.image` is image src string,
- // use `imageHelper.findExistImage` to find cached image.
- // `imageHelper.findExistImage` will always be called here before
- // `imageHelper.createOrUpdateImage` in `graphic/helper/text.js#renderRichText`
- // which ensures that image will not be rendered before correct size calcualted.
- if (bgImg) {
- bgImg = imageHelper.findExistImage(bgImg);
- if (imageHelper.isImageReady(bgImg)) {
- tokenWidth = Math.max(tokenWidth, bgImg.width * tokenHeight / bgImg.height);
- }
- }
- }
- var paddingW = textPadding ? textPadding[1] + textPadding[3] : 0;
- tokenWidth += paddingW;
- var remianTruncWidth = truncateWidth != null ? truncateWidth - lineWidth : null;
- if (remianTruncWidth != null && remianTruncWidth < tokenWidth) {
- if (!tokenWidthNotSpecified || remianTruncWidth < paddingW) {
- token.text = '';
- token.textWidth = tokenWidth = 0;
- } else {
- token.text = truncateText(token.text, remianTruncWidth - paddingW, font, truncate.ellipsis, {
- minChar: truncate.minChar
- });
- token.textWidth = getWidth(token.text, font);
- tokenWidth = token.textWidth + paddingW;
- }
- }
- }
- lineWidth += token.width = tokenWidth;
- tokenStyle && (lineHeight = Math.max(lineHeight, token.lineHeight));
- }
- line.width = lineWidth;
- line.lineHeight = lineHeight;
- contentHeight += lineHeight;
- contentWidth = Math.max(contentWidth, lineWidth);
- }
- contentBlock.outerWidth = contentBlock.width = retrieve2(style.textWidth, contentWidth);
- contentBlock.outerHeight = contentBlock.height = retrieve2(style.textHeight, contentHeight);
- if (stlPadding) {
- contentBlock.outerWidth += stlPadding[1] + stlPadding[3];
- contentBlock.outerHeight += stlPadding[0] + stlPadding[2];
- }
- for (var i = 0; i < pendingList.length; i++) {
- var token = pendingList[i];
- var percentWidth = token.percentWidth; // Should not base on outerWidth, because token can not be placed out of padding.
- token.width = parseInt(percentWidth, 10) / 100 * contentWidth;
- }
- return contentBlock;
- }
- function pushTokens(block, str, styleName) {
- var isEmptyStr = str === '';
- var strs = str.split('\n');
- var lines = block.lines;
- for (var i = 0; i < strs.length; i++) {
- var text = strs[i];
- var token = {
- styleName: styleName,
- text: text,
- isLineHolder: !text && !isEmptyStr
- }; // The first token should be appended to the last line.
- if (!i) {
- var tokens = (lines[lines.length - 1] || (lines[0] = {
- tokens: []
- })).tokens; // Consider cases:
- // (1) ''.split('\n') => ['', '\n', ''], the '' at the first item
- // (which is a placeholder) should be replaced by new token.
- // (2) A image backage, where token likes {a|}.
- // (3) A redundant '' will affect textAlign in line.
- // (4) tokens with the same tplName should not be merged, because
- // they should be displayed in different box (with border and padding).
- var tokensLen = tokens.length;
- tokensLen === 1 && tokens[0].isLineHolder ? tokens[0] = token : // Consider text is '', only insert when it is the "lineHolder" or
- // "emptyStr". Otherwise a redundant '' will affect textAlign in line.
- (text || !tokensLen || isEmptyStr) && tokens.push(token);
- } // Other tokens always start a new line.
- else {
- // If there is '', insert it as a placeholder.
- lines.push({
- tokens: [token]
- });
- }
- }
- }
- function makeFont(style) {
- // FIXME in node-canvas fontWeight is before fontStyle
- // Use `fontSize` `fontFamily` to check whether font properties are defined.
- var font = (style.fontSize || style.fontFamily) && [style.fontStyle, style.fontWeight, (style.fontSize || 12) + 'px', // If font properties are defined, `fontFamily` should not be ignored.
- style.fontFamily || 'sans-serif'].join(' ');
- return font && trim(font) || style.textFont || style.font;
- }
- exports.DEFAULT_FONT = DEFAULT_FONT;
- exports.$override = $override;
- exports.getWidth = getWidth;
- exports.getBoundingRect = getBoundingRect;
- exports.adjustTextX = adjustTextX;
- exports.adjustTextY = adjustTextY;
- exports.calculateTextPosition = calculateTextPosition;
- exports.adjustTextPositionOnRect = adjustTextPositionOnRect;
- exports.truncateText = truncateText;
- exports.getLineHeight = getLineHeight;
- exports.measureText = measureText;
- exports.parsePlainText = parsePlainText;
- exports.parseRichText = parseRichText;
- exports.makeFont = makeFont;
|