import BoundingRect, { RectLike } from '../core/BoundingRect';
|
import { Dictionary, TextAlign, TextVerticalAlign, BuiltinTextPosition } from '../core/types';
|
import LRU from '../core/LRU';
|
import { DEFAULT_FONT, platformApi } from '../core/platform';
|
|
let textWidthCache: Dictionary<LRU<number>> = {};
|
|
export function getWidth(text: string, font: string): number {
|
font = font || DEFAULT_FONT;
|
let cacheOfFont = textWidthCache[font];
|
if (!cacheOfFont) {
|
cacheOfFont = textWidthCache[font] = new LRU(500);
|
}
|
let width = cacheOfFont.get(text);
|
if (width == null) {
|
width = platformApi.measureText(text, font).width;
|
cacheOfFont.put(text, width);
|
}
|
|
return width;
|
}
|
|
/**
|
*
|
* 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 = getWidth(text, font);
|
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;
|
}
|
|
/**
|
*
|
* 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): number {
|
// TODO Right to left language
|
if (textAlign === 'right') {
|
x -= width;
|
}
|
else if (textAlign === 'center') {
|
x -= width / 2;
|
}
|
return x;
|
}
|
|
export function adjustTextY(y: number, height: number, verticalAlign: TextVerticalAlign): number {
|
if (verticalAlign === 'middle') {
|
y -= height / 2;
|
}
|
else if (verticalAlign === 'bottom') {
|
y -= height;
|
}
|
return y;
|
}
|
|
|
export function getLineHeight(font?: string): number {
|
// FIXME A rough approach.
|
return getWidth('国', font);
|
}
|
|
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;
|
}
|