UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

328 lines (302 loc) 10.7 kB
import BoundingRect, { RectLike } from '../core/BoundingRect'; import { TextAlign, TextVerticalAlign, BuiltinTextPosition } from '../core/types'; import LRU from '../core/LRU'; import { DEFAULT_FONT, platformApi } from '../core/platform'; /** * @deprecated But keep for possible outside usage. * Use `ensureFontMeasureInfo` + `measureWidth` instead. */ export function getWidth(text: string, font: string): number { return measureWidth(ensureFontMeasureInfo(font), text); } export interface FontMeasureInfo { font: string; strWidthCache: LRU<number>; // Key: char code, index: 0~127 (include 127) asciiWidthMap: number[] | null | undefined; asciiWidthMapTried: boolean; // Default width char width used both in non-ascii and line height. stWideCharWidth: number; // Default asc char width asciiCharWidth: number; } export function ensureFontMeasureInfo(font: string): FontMeasureInfo { if (!_fontMeasureInfoCache) { _fontMeasureInfoCache = new LRU(100); } font = font || DEFAULT_FONT; let measureInfo = _fontMeasureInfoCache.get(font); if (!measureInfo) { measureInfo = { font: font, strWidthCache: new LRU(500), asciiWidthMap: null, // Init lazily for performance. asciiWidthMapTried: false, // FIXME // Other languages? // FIXME // Consider proportional font? stWideCharWidth: platformApi.measureText('国', font).width, asciiCharWidth: platformApi.measureText('a', font).width, }; _fontMeasureInfoCache.put(font, measureInfo); } return measureInfo; } let _fontMeasureInfoCache: LRU<FontMeasureInfo>; /** * For getting more precise result in truncate. * non-monospace font vary in char width. * But if it is time consuming in some platform, return null/undefined. * @return Key: char code, index: 0~127 (include 127) */ function tryCreateASCIIWidthMap(font: string): FontMeasureInfo['asciiWidthMap'] { // PENDING: is it necessary? Re-examine it if bad case reported. if (_getASCIIWidthMapLongCount >= GET_ASCII_WIDTH_LONG_COUNT_MAX) { return; } font = font || DEFAULT_FONT; const asciiWidthMap = []; const start = +(new Date()); // 0~31 and 127 may also have width, and may vary in some fonts. for (let code = 0; code <= 127; code++) { asciiWidthMap[code] = platformApi.measureText(String.fromCharCode(code), font).width; } const cost = +(new Date()) - start; if (cost > 16) { _getASCIIWidthMapLongCount = GET_ASCII_WIDTH_LONG_COUNT_MAX; } else if (cost > 2) { _getASCIIWidthMapLongCount++; } return asciiWidthMap; } let _getASCIIWidthMapLongCount: number = 0; const GET_ASCII_WIDTH_LONG_COUNT_MAX = 5; /** * Hot path, performance sensitive. */ export function measureCharWidth(fontMeasureInfo: FontMeasureInfo, charCode: number): number { if (!fontMeasureInfo.asciiWidthMapTried) { fontMeasureInfo.asciiWidthMap = tryCreateASCIIWidthMap(fontMeasureInfo.font); fontMeasureInfo.asciiWidthMapTried = true; } return (0 <= charCode && charCode <= 127) ? (fontMeasureInfo.asciiWidthMap != null ? fontMeasureInfo.asciiWidthMap[charCode] : fontMeasureInfo.asciiCharWidth ) : fontMeasureInfo.stWideCharWidth; } export function measureWidth(fontMeasureInfo: FontMeasureInfo, text: string): number { const strWidthCache = fontMeasureInfo.strWidthCache; let width = strWidthCache.get(text); if (width == null) { width = platformApi.measureText(text, fontMeasureInfo.font).width; strWidthCache.put(text, width); } return width; } /** * @deprecated See `getBoundingRect`. * Get bounding rect for inner usage(TSpan) * Which not include text newline. */ export function innerGetBoundingRect( text: string, font: string, textAlign?: TextAlign, textBaseline?: TextVerticalAlign ): BoundingRect { const width = measureWidth(ensureFontMeasureInfo(font), text); const height = getLineHeight(font); const x = adjustTextX(0, width, textAlign); const y = adjustTextY(0, height, textBaseline); const rect = new BoundingRect(x, y, width, height); return rect; } /** * @deprecated Use `(new Text(...)).getBoundingRect()` or `(new TSpan(...)).getBoundingRect()` instead. * This method behaves differently from `Text#getBoundingRect()` - e.g., it does not support the overflow * strategy, and only has single line height even if multiple lines. * * Get bounding rect for outer usage. Compatitable with old implementation * Which includes text newline. */ export function getBoundingRect( text: string, font: string, textAlign?: TextAlign, textBaseline?: TextVerticalAlign ) { const textLines = ((text || '') + '').split('\n'); const len = textLines.length; if (len === 1) { return innerGetBoundingRect(textLines[0], font, textAlign, textBaseline); } else { const uniondRect = new BoundingRect(0, 0, 0, 0); for (let i = 0; i < textLines.length; i++) { const rect = innerGetBoundingRect(textLines[i], font, textAlign, textBaseline); i === 0 ? uniondRect.copy(rect) : uniondRect.union(rect); } return uniondRect; } } export function adjustTextX(x: number, width: number, textAlign: TextAlign, inverse?: boolean): number { // TODO Right to left language if (textAlign === 'right') { !inverse ? (x -= width) : (x += width); } else if (textAlign === 'center') { !inverse ? (x -= width / 2) : (x += width / 2); } return x; } export function adjustTextY(y: number, height: number, verticalAlign: TextVerticalAlign, inverse?: boolean): number { if (verticalAlign === 'middle') { !inverse ? (y -= height / 2) : (y += height / 2); } else if (verticalAlign === 'bottom') { !inverse ? (y -= height) : (y += height); } return y; } export function getLineHeight(font?: string): number { // FIXME A rough approach. return ensureFontMeasureInfo(font).stWideCharWidth; } export function measureText(text: string, font?: string): { width: number } { return platformApi.measureText(text, font); } export function parsePercent(value: number | string, maxValue: number): number { if (typeof value === 'string') { if (value.lastIndexOf('%') >= 0) { return parseFloat(value) / 100 * maxValue; } return parseFloat(value); } return value; } export interface TextPositionCalculationResult { x: number y: number align: TextAlign verticalAlign: TextVerticalAlign } /** * Follow same interface to `Displayable.prototype.calculateTextPosition`. * @public * @param out Prepared out object. If not input, auto created in the method. * @param style where `textPosition` and `textDistance` are visited. * @param rect {x, y, width, height} Rect of the host elment, according to which the text positioned. * @return The input `out`. Set: {x, y, textAlign, textVerticalAlign} */ export function calculateTextPosition( out: TextPositionCalculationResult, opts: { position?: BuiltinTextPosition | (number | string)[] distance?: number // Default 5 global?: boolean }, rect: RectLike ): TextPositionCalculationResult { const textPosition = opts.position || 'inside'; const distance = opts.distance != null ? opts.distance : 5; const height = rect.height; const width = rect.width; const halfHeight = height / 2; let x = rect.x; let y = rect.y; let textAlign: TextAlign = 'left'; let textVerticalAlign: TextVerticalAlign = 'top'; if (textPosition instanceof Array) { x += parsePercent(textPosition[0], rect.width); y += parsePercent(textPosition[1], rect.height); // Not use textAlign / textVerticalAlign textAlign = null; textVerticalAlign = null; } else { 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 || {} as TextPositionCalculationResult; out.x = x; out.y = y; out.align = textAlign; out.verticalAlign = textVerticalAlign; return out; }