cloud-server-8/node_modules/zrender/src/graphic/Text.ts

1039 lines
34 KiB
TypeScript

/**
* RichText is a container that manages complex text label.
* It will parse text string and create sub displayble elements respectively.
*/
import { TextAlign, TextVerticalAlign, ImageLike, Dictionary, MapToType, FontWeight, FontStyle } from '../core/types';
import { parseRichText, parsePlainText } from './helper/parseText';
import TSpan, { TSpanStyleProps } from './TSpan';
import { retrieve2, each, normalizeCssArray, trim, retrieve3, extend, keys, defaults } from '../core/util';
import { adjustTextX, adjustTextY } from '../contain/text';
import ZRImage from './Image';
import Rect from './shape/Rect';
import BoundingRect from '../core/BoundingRect';
import { MatrixArray } from '../core/matrix';
import Displayable, {
DisplayableStatePropNames,
DisplayableProps,
DEFAULT_COMMON_ANIMATION_PROPS
} from './Displayable';
import { ZRenderType } from '../zrender';
import Animator from '../animation/Animator';
import Transformable from '../core/Transformable';
import { ElementCommonState } from '../Element';
import { GroupLike } from './Group';
import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from '../core/platform';
type TextContentBlock = ReturnType<typeof parseRichText>
type TextLine = TextContentBlock['lines'][0]
type TextToken = TextLine['tokens'][0]
// TODO Default value?
export interface TextStylePropsPart {
// TODO Text is assigned inside zrender
text?: string
fill?: string
stroke?: string
strokeNoScale?: boolean
opacity?: number
fillOpacity?: number
strokeOpacity?: number
/**
* textStroke may be set as some color as a default
* value in upper applicaion, where the default value
* of lineWidth should be 0 to make sure that
* user can choose to do not use text stroke.
*/
lineWidth?: number
lineDash?: false | number[]
lineDashOffset?: number
borderDash?: false | number[]
borderDashOffset?: number
/**
* If `fontSize` or `fontFamily` exists, `font` will be reset by
* `fontSize`, `fontStyle`, `fontWeight`, `fontFamily`.
* So do not visit it directly in upper application (like echarts),
* but use `contain/text#makeFont` instead.
*/
font?: string
/**
* The same as font. Use font please.
* @deprecated
*/
textFont?: string
/**
* It helps merging respectively, rather than parsing an entire font string.
*/
fontStyle?: FontStyle
/**
* It helps merging respectively, rather than parsing an entire font string.
*/
fontWeight?: FontWeight
/**
* It helps merging respectively, rather than parsing an entire font string.
*/
fontFamily?: string
/**
* It helps merging respectively, rather than parsing an entire font string.
* Should be 12 but not '12px'.
*/
fontSize?: number | string
align?: TextAlign
verticalAlign?: TextVerticalAlign
/**
* Line height. Default to be text height of '国'
*/
lineHeight?: number
/**
* Width of text block. Not include padding
* Used for background, truncate, wrap
*/
width?: number | string
/**
* Height of text block. Not include padding
* Used for background, truncate
*/
height?: number
/**
* Reserved for special functinality, like 'hr'.
*/
tag?: string
textShadowColor?: string
textShadowBlur?: number
textShadowOffsetX?: number
textShadowOffsetY?: number
// Shadow, background, border of text box.
backgroundColor?: string | {
image: ImageLike | string
}
/**
* Can be `2` or `[2, 4]` or `[2, 3, 4, 5]`
*/
padding?: number | number[]
/**
* Margin of label. Used when layouting the label.
*/
margin?: number
borderColor?: string
borderWidth?: number
borderRadius?: number | number[]
/**
* Shadow color for background box.
*/
shadowColor?: string
/**
* Shadow blur for background box.
*/
shadowBlur?: number
/**
* Shadow offset x for background box.
*/
shadowOffsetX?: number
/**
* Shadow offset y for background box.
*/
shadowOffsetY?: number
}
export interface TextStyleProps extends TextStylePropsPart {
text?: string
x?: number
y?: number
/**
* Only support number in the top block.
*/
width?: number
/**
* Text styles for rich text.
*/
rich?: Dictionary<TextStylePropsPart>
/**
* Strategy when calculated text width exceeds textWidth.
* break: break by word
* break: will break inside the word
* truncate: truncate the text and show ellipsis
* Do nothing if not set
*/
overflow?: 'break' | 'breakAll' | 'truncate' | 'none'
/**
* Strategy when text lines exceeds textHeight.
* Do nothing if not set
*/
lineOverflow?: 'truncate'
/**
* Epllipsis used if text is truncated
*/
ellipsis?: string
/**
* Placeholder used if text is truncated to empty
*/
placeholder?: string
/**
* Min characters for truncating
*/
truncateMinChar?: number
}
export interface TextProps extends DisplayableProps {
style?: TextStyleProps
zlevel?: number
z?: number
z2?: number
culling?: boolean
cursor?: string
}
export type TextState = Pick<TextProps, DisplayableStatePropNames> & ElementCommonState
export type DefaultTextStyle = Pick<TextStyleProps, 'fill' | 'stroke' | 'align' | 'verticalAlign'> & {
autoStroke?: boolean
};
const DEFAULT_RICH_TEXT_COLOR = {
fill: '#000'
};
const DEFAULT_STROKE_LINE_WIDTH = 2;
// const DEFAULT_TEXT_STYLE: TextStyleProps = {
// x: 0,
// y: 0,
// fill: '#000',
// stroke: null,
// opacity: 0,
// fillOpacity:
// }
export const DEFAULT_TEXT_ANIMATION_PROPS: MapToType<TextProps, boolean> = {
style: defaults<MapToType<TextStyleProps, boolean>, MapToType<TextStyleProps, boolean>>({
fill: true,
stroke: true,
fillOpacity: true,
strokeOpacity: true,
lineWidth: true,
fontSize: true,
lineHeight: true,
width: true,
height: true,
textShadowColor: true,
textShadowBlur: true,
textShadowOffsetX: true,
textShadowOffsetY: true,
backgroundColor: true,
padding: true, // TODO needs normalize padding before animate
borderColor: true,
borderWidth: true,
borderRadius: true // TODO needs normalize radius before animate
}, DEFAULT_COMMON_ANIMATION_PROPS.style)
};
interface ZRText {
animate(key?: '', loop?: boolean): Animator<this>
animate(key: 'style', loop?: boolean): Animator<this['style']>
getState(stateName: string): TextState
ensureState(stateName: string): TextState
states: Dictionary<TextState>
stateProxy: (stateName: string) => TextState
}
class ZRText extends Displayable<TextProps> implements GroupLike {
type = 'text'
style: TextStyleProps
/**
* How to handling label overlap
*
* hidden:
*/
overlap: 'hidden' | 'show' | 'blur'
/**
* Will use this to calculate transform matrix
* instead of Element itseelf if it's give.
* Not exposed to developers
*/
innerTransformable: Transformable
private _children: (ZRImage | Rect | TSpan)[] = []
private _childCursor: 0
private _defaultStyle: DefaultTextStyle = DEFAULT_RICH_TEXT_COLOR
constructor(opts?: TextProps) {
super();
this.attr(opts);
}
childrenRef() {
return this._children;
}
update() {
super.update();
// Update children
if (this.styleChanged()) {
this._updateSubTexts();
}
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
// Set common properties.
child.zlevel = this.zlevel;
child.z = this.z;
child.z2 = this.z2;
child.culling = this.culling;
child.cursor = this.cursor;
child.invisible = this.invisible;
}
}
updateTransform() {
const innerTransformable = this.innerTransformable;
if (innerTransformable) {
innerTransformable.updateTransform();
if (innerTransformable.transform) {
this.transform = innerTransformable.transform;
}
}
else {
super.updateTransform();
}
}
getLocalTransform(m?: MatrixArray): MatrixArray {
const innerTransformable = this.innerTransformable;
return innerTransformable
? innerTransformable.getLocalTransform(m)
: super.getLocalTransform(m);
}
// TODO override setLocalTransform?
getComputedTransform() {
if (this.__hostTarget) {
// Update host target transform
this.__hostTarget.getComputedTransform();
// Update text position.
this.__hostTarget.updateInnerText(true);
}
return super.getComputedTransform();
}
private _updateSubTexts() {
// Reset child visit cursor
this._childCursor = 0;
normalizeTextStyle(this.style);
this.style.rich
? this._updateRichTexts()
: this._updatePlainTexts();
this._children.length = this._childCursor;
this.styleUpdated();
}
addSelfToZr(zr: ZRenderType) {
super.addSelfToZr(zr);
for (let i = 0; i < this._children.length; i++) {
// Also need mount __zr for case like hover detection.
// The case: hover on a label (position: 'top') causes host el
// scaled and label Y position lifts a bit so that out of the
// pointer, then mouse move should be able to trigger "mouseout".
this._children[i].__zr = zr;
}
}
removeSelfFromZr(zr: ZRenderType) {
super.removeSelfFromZr(zr);
for (let i = 0; i < this._children.length; i++) {
this._children[i].__zr = null;
}
}
getBoundingRect(): BoundingRect {
if (this.styleChanged()) {
this._updateSubTexts();
}
if (!this._rect) {
// TODO: Optimize when using width and overflow: wrap/truncate
const tmpRect = new BoundingRect(0, 0, 0, 0);
const children = this._children;
const tmpMat: MatrixArray = [];
let rect = null;
for (let i = 0; i < children.length; i++) {
const child = children[i];
const childRect = child.getBoundingRect();
const transform = child.getLocalTransform(tmpMat);
if (transform) {
tmpRect.copy(childRect);
tmpRect.applyTransform(transform);
rect = rect || tmpRect.clone();
rect.union(tmpRect);
}
else {
rect = rect || childRect.clone();
rect.union(childRect);
}
}
this._rect = rect || tmpRect;
}
return this._rect;
}
// Can be set in Element. To calculate text fill automatically when textContent is inside element
setDefaultTextStyle(defaultTextStyle: DefaultTextStyle) {
// Use builtin if defaultTextStyle is not given.
this._defaultStyle = defaultTextStyle || DEFAULT_RICH_TEXT_COLOR;
}
setTextContent(textContent: never) {
if (process.env.NODE_ENV !== 'production') {
throw new Error('Can\'t attach text on another text');
}
}
// getDefaultStyleValue<T extends keyof TextStyleProps>(key: T): TextStyleProps[T] {
// // Default value is on the prototype.
// return this.style.prototype[key];
// }
protected _mergeStyle(targetStyle: TextStyleProps, sourceStyle: TextStyleProps) {
if (!sourceStyle) {
return targetStyle;
}
// DO deep merge on rich configurations.
const sourceRich = sourceStyle.rich;
const targetRich = targetStyle.rich || (sourceRich && {}); // Create a new one if source have rich but target don't
extend(targetStyle, sourceStyle);
if (sourceRich && targetRich) {
// merge rich and assign rich again.
this._mergeRich(targetRich, sourceRich);
targetStyle.rich = targetRich;
}
else if (targetRich) {
// If source rich not exists. DON'T override the target rich
targetStyle.rich = targetRich;
}
return targetStyle;
}
private _mergeRich(targetRich: TextStyleProps['rich'], sourceRich: TextStyleProps['rich']) {
const richNames = keys(sourceRich);
// Merge by rich names.
for (let i = 0; i < richNames.length; i++) {
const richName = richNames[i];
targetRich[richName] = targetRich[richName] || {};
extend(targetRich[richName], sourceRich[richName]);
}
}
getAnimationStyleProps() {
return DEFAULT_TEXT_ANIMATION_PROPS;
}
private _getOrCreateChild(Ctor: {new(): TSpan}): TSpan
private _getOrCreateChild(Ctor: {new(): ZRImage}): ZRImage
private _getOrCreateChild(Ctor: {new(): Rect}): Rect
private _getOrCreateChild(Ctor: {new(): TSpan | Rect | ZRImage}): TSpan | Rect | ZRImage {
let child = this._children[this._childCursor];
if (!child || !(child instanceof Ctor)) {
child = new Ctor();
}
this._children[this._childCursor++] = child;
child.__zr = this.__zr;
// TODO to users parent can only be group.
child.parent = this as any;
return child;
}
private _updatePlainTexts() {
const style = this.style;
const textFont = style.font || DEFAULT_FONT;
const textPadding = style.padding as number[];
const text = getStyleText(style);
const contentBlock = parsePlainText(text, style);
const needDrawBg = needDrawBackground(style);
const bgColorDrawn = !!(style.backgroundColor);
const outerHeight = contentBlock.outerHeight;
const outerWidth = contentBlock.outerWidth;
const contentWidth = contentBlock.contentWidth;
const textLines = contentBlock.lines;
const lineHeight = contentBlock.lineHeight;
const defaultStyle = this._defaultStyle;
const baseX = style.x || 0;
const baseY = style.y || 0;
const textAlign = style.align || defaultStyle.align || 'left';
const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign || 'top';
let textX = baseX;
let textY = adjustTextY(baseY, contentBlock.contentHeight, verticalAlign);
if (needDrawBg || textPadding) {
// Consider performance, do not call getTextWidth util necessary.
const boxX = adjustTextX(baseX, outerWidth, textAlign);
const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
needDrawBg && this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
}
// `textBaseline` is set as 'middle'.
textY += lineHeight / 2;
if (textPadding) {
textX = getTextXForPadding(baseX, textAlign, textPadding);
if (verticalAlign === 'top') {
textY += textPadding[0];
}
else if (verticalAlign === 'bottom') {
textY -= textPadding[2];
}
}
let defaultLineWidth = 0;
let useDefaultFill = false;
const textFill = getFill(
'fill' in style
? style.fill
: (useDefaultFill = true, defaultStyle.fill)
);
const textStroke = getStroke(
'stroke' in style
? style.stroke
: (!bgColorDrawn
// If we use "auto lineWidth" widely, it probably bring about some bad case.
// So the current strategy is:
// If `style.fill` is specified (i.e., `useDefaultFill` is `false`)
// (A) And if `textConfig.insideStroke/outsideStroke` is not specified as a color
// (i.e., `defaultStyle.autoStroke` is `true`), we do not actually display
// the auto stroke because we can not make sure wether the stoke is approperiate to
// the given `fill`.
// (B) But if `textConfig.insideStroke/outsideStroke` is specified as a color,
// we give the auto lineWidth to display the given stoke color.
&& (!defaultStyle.autoStroke || useDefaultFill)
)
? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke)
: null
);
const hasShadow = style.textShadowBlur > 0;
const fixedBoundingRect = style.width != null
&& (style.overflow === 'truncate' || style.overflow === 'break' || style.overflow === 'breakAll');
const calculatedLineHeight = contentBlock.calculatedLineHeight;
for (let i = 0; i < textLines.length; i++) {
const el = this._getOrCreateChild(TSpan);
// Always create new style.
const subElStyle: TSpanStyleProps = el.createStyle();
el.useStyle(subElStyle);
subElStyle.text = textLines[i];
subElStyle.x = textX;
subElStyle.y = textY;
// 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.
if (textAlign) {
subElStyle.textAlign = textAlign;
}
// Force baseline to be "middle". Otherwise, if using "top", the
// text will offset downward a little bit in font "Microsoft YaHei".
subElStyle.textBaseline = 'middle';
subElStyle.opacity = style.opacity;
// Fill after stroke so the outline will not cover the main part.
subElStyle.strokeFirst = true;
if (hasShadow) {
subElStyle.shadowBlur = style.textShadowBlur || 0;
subElStyle.shadowColor = style.textShadowColor || 'transparent';
subElStyle.shadowOffsetX = style.textShadowOffsetX || 0;
subElStyle.shadowOffsetY = style.textShadowOffsetY || 0;
}
// Always override default fill and stroke value.
subElStyle.stroke = textStroke as string;
subElStyle.fill = textFill as string;
if (textStroke) {
subElStyle.lineWidth = style.lineWidth || defaultLineWidth;
subElStyle.lineDash = style.lineDash;
subElStyle.lineDashOffset = style.lineDashOffset || 0;
}
subElStyle.font = textFont;
setSeparateFont(subElStyle, style);
textY += lineHeight;
if (fixedBoundingRect) {
el.setBoundingRect(new BoundingRect(
adjustTextX(subElStyle.x, style.width, subElStyle.textAlign as TextAlign),
adjustTextY(subElStyle.y, calculatedLineHeight, subElStyle.textBaseline as TextVerticalAlign),
/**
* Text boundary should be the real text width.
* Otherwise, there will be extra space in the
* bounding rect calculated.
*/
contentWidth,
calculatedLineHeight
));
}
}
}
private _updateRichTexts() {
const style = this.style;
// TODO Only parse when text changed?
const text = getStyleText(style);
const contentBlock = parseRichText(text, style);
const contentWidth = contentBlock.width;
const outerWidth = contentBlock.outerWidth;
const outerHeight = contentBlock.outerHeight;
const textPadding = style.padding as number[];
const baseX = style.x || 0;
const baseY = style.y || 0;
const defaultStyle = this._defaultStyle;
const textAlign = style.align || defaultStyle.align;
const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign;
const boxX = adjustTextX(baseX, outerWidth, textAlign);
const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
let xLeft = boxX;
let lineTop = boxY;
if (textPadding) {
xLeft += textPadding[3];
lineTop += textPadding[0];
}
let xRight = xLeft + contentWidth;
if (needDrawBackground(style)) {
this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
}
const bgColorDrawn = !!(style.backgroundColor);
for (let i = 0; i < contentBlock.lines.length; i++) {
const line = contentBlock.lines[i];
const tokens = line.tokens;
const tokenCount = tokens.length;
const lineHeight = line.lineHeight;
let remainedWidth = line.width;
let leftIndex = 0;
let lineXLeft = xLeft;
let lineXRight = xRight;
let rightIndex = tokenCount - 1;
let token;
while (
leftIndex < tokenCount
&& (token = tokens[leftIndex], !token.align || token.align === 'left')
) {
this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn);
remainedWidth -= token.width;
lineXLeft += token.width;
leftIndex++;
}
while (
rightIndex >= 0
&& (token = tokens[rightIndex], token.align === 'right')
) {
this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn);
remainedWidth -= 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) - remainedWidth) / 2;
while (leftIndex <= rightIndex) {
token = tokens[leftIndex];
// Consider width specified by user, use 'center' rather than 'left'.
this._placeToken(
token, style, lineHeight, lineTop,
lineXLeft + token.width / 2, 'center', bgColorDrawn
);
lineXLeft += token.width;
leftIndex++;
}
lineTop += lineHeight;
}
}
private _placeToken(
token: TextToken,
style: TextStyleProps,
lineHeight: number,
lineTop: number,
x: number,
textAlign: string,
parentBgColorDrawn: boolean
) {
const tokenStyle = style.rich[token.styleName] || {};
tokenStyle.text = token.text;
// 'ctx.textBaseline' is always set as 'middle', for sake of
// the bias of "Microsoft YaHei".
const verticalAlign = token.verticalAlign;
let y = lineTop + lineHeight / 2;
if (verticalAlign === 'top') {
y = lineTop + token.height / 2;
}
else if (verticalAlign === 'bottom') {
y = lineTop + lineHeight - token.height / 2;
}
const needDrawBg = !token.isLineHolder && needDrawBackground(tokenStyle);
needDrawBg && this._renderBackground(
tokenStyle,
style,
textAlign === 'right'
? x - token.width
: textAlign === 'center'
? x - token.width / 2
: x,
y - token.height / 2,
token.width,
token.height
);
const bgColorDrawn = !!tokenStyle.backgroundColor;
const textPadding = token.textPadding;
if (textPadding) {
x = getTextXForPadding(x, textAlign, textPadding);
y -= token.height / 2 - textPadding[0] - token.innerHeight / 2;
}
const el = this._getOrCreateChild(TSpan);
const subElStyle: TSpanStyleProps = el.createStyle();
// Always create new style.
el.useStyle(subElStyle);
const defaultStyle = this._defaultStyle;
let useDefaultFill = false;
let defaultLineWidth = 0;
const textFill = getFill(
'fill' in tokenStyle ? tokenStyle.fill
: 'fill' in style ? style.fill
: (useDefaultFill = true, defaultStyle.fill)
);
const textStroke = getStroke(
'stroke' in tokenStyle ? tokenStyle.stroke
: 'stroke' in style ? style.stroke
: (
!bgColorDrawn
&& !parentBgColorDrawn
// See the strategy explained above.
&& (!defaultStyle.autoStroke || useDefaultFill)
) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke)
: null
);
const hasShadow = tokenStyle.textShadowBlur > 0
|| style.textShadowBlur > 0;
subElStyle.text = token.text;
subElStyle.x = x;
subElStyle.y = y;
if (hasShadow) {
subElStyle.shadowBlur = tokenStyle.textShadowBlur || style.textShadowBlur || 0;
subElStyle.shadowColor = tokenStyle.textShadowColor || style.textShadowColor || 'transparent';
subElStyle.shadowOffsetX = tokenStyle.textShadowOffsetX || style.textShadowOffsetX || 0;
subElStyle.shadowOffsetY = tokenStyle.textShadowOffsetY || style.textShadowOffsetY || 0;
}
subElStyle.textAlign = textAlign as CanvasTextAlign;
// Force baseline to be "middle". Otherwise, if using "top", the
// text will offset downward a little bit in font "Microsoft YaHei".
subElStyle.textBaseline = 'middle';
subElStyle.font = token.font || DEFAULT_FONT;
subElStyle.opacity = retrieve3(tokenStyle.opacity, style.opacity, 1);
// TODO inherit each item from top style in token style?
setSeparateFont(subElStyle, tokenStyle);
if (textStroke) {
subElStyle.lineWidth = retrieve3(tokenStyle.lineWidth, style.lineWidth, defaultLineWidth);
subElStyle.lineDash = retrieve2(tokenStyle.lineDash, style.lineDash);
subElStyle.lineDashOffset = style.lineDashOffset || 0;
subElStyle.stroke = textStroke;
}
if (textFill) {
subElStyle.fill = textFill;
}
const textWidth = token.contentWidth;
const textHeight = token.contentHeight;
// NOTE: Should not call dirtyStyle after setBoundingRect. Or it will be cleared.
el.setBoundingRect(new BoundingRect(
adjustTextX(subElStyle.x, textWidth, subElStyle.textAlign as TextAlign),
adjustTextY(subElStyle.y, textHeight, subElStyle.textBaseline as TextVerticalAlign),
textWidth,
textHeight
));
}
private _renderBackground(
style: TextStylePropsPart,
topStyle: TextStylePropsPart,
x: number,
y: number,
width: number,
height: number
) {
const textBackgroundColor = style.backgroundColor;
const textBorderWidth = style.borderWidth;
const textBorderColor = style.borderColor;
const isImageBg = textBackgroundColor && (textBackgroundColor as {image: ImageLike}).image;
const isPlainOrGradientBg = textBackgroundColor && !isImageBg;
const textBorderRadius = style.borderRadius;
const self = this;
let rectEl: Rect;
let imgEl: ZRImage;
if (isPlainOrGradientBg || style.lineHeight || (textBorderWidth && textBorderColor)) {
// Background is color
rectEl = this._getOrCreateChild(Rect);
rectEl.useStyle(rectEl.createStyle()); // Create an empty style.
rectEl.style.fill = null;
const rectShape = rectEl.shape;
rectShape.x = x;
rectShape.y = y;
rectShape.width = width;
rectShape.height = height;
rectShape.r = textBorderRadius;
rectEl.dirtyShape();
}
if (isPlainOrGradientBg) {
const rectStyle = rectEl.style;
rectStyle.fill = textBackgroundColor as string || null;
rectStyle.fillOpacity = retrieve2(style.fillOpacity, 1);
}
else if (isImageBg) {
imgEl = this._getOrCreateChild(ZRImage);
imgEl.onload = function () {
// Refresh and relayout after image loaded.
self.dirtyStyle();
};
const imgStyle = imgEl.style;
imgStyle.image = (textBackgroundColor as {image: ImageLike}).image;
imgStyle.x = x;
imgStyle.y = y;
imgStyle.width = width;
imgStyle.height = height;
}
if (textBorderWidth && textBorderColor) {
const rectStyle = rectEl.style;
rectStyle.lineWidth = textBorderWidth;
rectStyle.stroke = textBorderColor;
rectStyle.strokeOpacity = retrieve2(style.strokeOpacity, 1);
rectStyle.lineDash = style.borderDash;
rectStyle.lineDashOffset = style.borderDashOffset || 0;
rectEl.strokeContainThreshold = 0;
// Making shadow looks better.
if (rectEl.hasFill() && rectEl.hasStroke()) {
rectStyle.strokeFirst = true;
rectStyle.lineWidth *= 2;
}
}
const commonStyle = (rectEl || imgEl).style;
commonStyle.shadowBlur = style.shadowBlur || 0;
commonStyle.shadowColor = style.shadowColor || 'transparent';
commonStyle.shadowOffsetX = style.shadowOffsetX || 0;
commonStyle.shadowOffsetY = style.shadowOffsetY || 0;
commonStyle.opacity = retrieve3(style.opacity, topStyle.opacity, 1);
}
static makeFont(style: TextStylePropsPart): string {
// FIXME in node-canvas fontWeight is before fontStyle
// Use `fontSize` `fontFamily` to check whether font properties are defined.
let font = '';
if (hasSeparateFont(style)) {
font = [
style.fontStyle,
style.fontWeight,
parseFontSize(style.fontSize),
// If font properties are defined, `fontFamily` should not be ignored.
style.fontFamily || 'sans-serif'
].join(' ');
}
return font && trim(font) || style.textFont || style.font;
}
}
const VALID_TEXT_ALIGN = {left: true, right: 1, center: 1};
const VALID_TEXT_VERTICAL_ALIGN = {top: 1, bottom: 1, middle: 1};
const FONT_PARTS = ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily'] as const;
export function parseFontSize(fontSize: number | string) {
if (
typeof fontSize === 'string'
&& (
fontSize.indexOf('px') !== -1
|| fontSize.indexOf('rem') !== -1
|| fontSize.indexOf('em') !== -1
)
) {
return fontSize;
}
else if (!isNaN(+fontSize)) {
return fontSize + 'px';
}
else {
return DEFAULT_FONT_SIZE + 'px';
}
}
function setSeparateFont(
targetStyle: TSpanStyleProps,
sourceStyle: TextStylePropsPart
) {
for (let i = 0; i < FONT_PARTS.length; i++) {
const fontProp = FONT_PARTS[i];
const val = sourceStyle[fontProp];
if (val != null) {
(targetStyle as any)[fontProp] = val;
}
}
}
export function hasSeparateFont(style: Pick<TextStylePropsPart, 'fontSize' | 'fontFamily' | 'fontWeight'>) {
return style.fontSize != null || style.fontFamily || style.fontWeight;
}
export function normalizeTextStyle(style: TextStyleProps): TextStyleProps {
normalizeStyle(style);
// TODO inherit each item from top style in token style?
each(style.rich, normalizeStyle);
return style;
}
function normalizeStyle(style: TextStylePropsPart) {
if (style) {
style.font = ZRText.makeFont(style);
let textAlign = style.align;
// 'middle' is invalid, convert it to 'center'
(textAlign as string) === 'middle' && (textAlign = 'center');
style.align = (
textAlign == null || VALID_TEXT_ALIGN[textAlign]
) ? textAlign : 'left';
// Compatible with textBaseline.
let verticalAlign = style.verticalAlign;
(verticalAlign as string) === 'center' && (verticalAlign = 'middle');
style.verticalAlign = (
verticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[verticalAlign]
) ? verticalAlign : 'top';
// TODO Should not change the orignal value.
const textPadding = style.padding;
if (textPadding) {
style.padding = normalizeCssArray(style.padding);
}
}
}
/**
* @param stroke If specified, do not check style.textStroke.
* @param lineWidth If specified, do not check style.textStroke.
*/
function getStroke(
stroke?: TextStylePropsPart['stroke'],
lineWidth?: number
) {
return (stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none')
? null
: ((stroke as any).image || (stroke as any).colorStops)
? '#000'
: stroke;
}
function getFill(
fill?: TextStylePropsPart['fill']
) {
return (fill == null || fill === 'none')
? null
// TODO pattern and gradient?
: ((fill as any).image || (fill as any).colorStops)
? '#000'
: fill;
}
function getTextXForPadding(x: number, textAlign: string, textPadding: number[]): number {
return textAlign === 'right'
? (x - textPadding[1])
: textAlign === 'center'
? (x + textPadding[3] / 2 - textPadding[1] / 2)
: (x + textPadding[3]);
}
function getStyleText(style: TextStylePropsPart): string {
// Compat: set number to text is supported.
// set null/undefined to text is supported.
let text = style.text;
text != null && (text += '');
return text;
}
/**
* If needs draw background
* @param style Style of element
*/
function needDrawBackground(style: TextStylePropsPart): boolean {
return !!(
style.backgroundColor
|| style.lineHeight
|| (style.borderWidth && style.borderColor)
);
}
export default ZRText;