@visactor/vrender-core
Version:
## Description
344 lines (331 loc) • 20.2 kB
JavaScript
import { max, isArray, getContextFont, transformBoundsWithMatrix, rotatePoint } from "@visactor/vutils";
import { textDrawOffsetX, textLayoutOffsetY } from "../common/text";
import { CanvasTextLayout } from "../core/contributions/textMeasure/layout";
import { application } from "../application";
import { Graphic, GRAPHIC_UPDATE_TAG_KEY, NOWORK_ANIMATE_ATTR } from "./graphic";
import { getTheme } from "./theme";
import { calculateLineHeight } from "../common/utils";
import { TEXT_NUMBER_TYPE } from "./constants";
import { boundStroke, TextDirection, verticalLayout } from "./tools";
const TEXT_UPDATE_TAG_KEY = [ "text", "maxLineWidth", "maxWidth", "textAlign", "textBaseline", "heightLimit", "lineClamp", "fontSize", "fontFamily", "fontWeight", "ellipsis", "lineHeight", "direction", "wordBreak", "heightLimit", "lineClamp", ...GRAPHIC_UPDATE_TAG_KEY ];
export class Text extends Graphic {
get font() {
const textTheme = this.getGraphicTheme();
return this._font || (this._font = getContextFont(this.attribute, textTheme)), this._font;
}
get clipedText() {
var _a;
const attribute = this.attribute, textTheme = this.getGraphicTheme(), maxWidth = this.getMaxWidth(textTheme);
return Number.isFinite(maxWidth) ? (this.tryUpdateAABBBounds(), this.cache.clipedText) : (null !== (_a = attribute.text) && void 0 !== _a ? _a : textTheme.text).toString();
}
get clipedWidth() {
return this.tryUpdateAABBBounds(), this.cache.clipedWidth;
}
get cliped() {
var _a, _b;
const textTheme = this.getGraphicTheme(), attribute = this.attribute, maxWidth = this.getMaxWidth(textTheme);
if (!Number.isFinite(maxWidth)) return !1;
const {text: text} = this.attribute;
if (this.tryUpdateAABBBounds(), null === (_b = null === (_a = this.cache) || void 0 === _a ? void 0 : _a.layoutData) || void 0 === _b ? void 0 : _b.lines) {
let mergedText = "";
this.cache.layoutData.lines.forEach((item => {
mergedText += item.str;
}));
return (Array.isArray(text) ? text.join("") : text) !== mergedText;
}
return "vertical" === attribute.direction && this.cache.verticalList && this.cache.verticalList[0] ? this.cache.verticalList[0].map((item => item.text)).join("") !== attribute.text.toString() : null != this.clipedText && this.clipedText !== attribute.text.toString();
}
get multilineLayout() {
return this.tryUpdateAABBBounds(), this.cache.layoutData;
}
get isMultiLine() {
return Array.isArray(this.attribute.text) || "normal" === this.attribute.whiteSpace;
}
constructor(params = {
text: "",
fontSize: 16
}) {
super(params), this.type = "text", this.numberType = TEXT_NUMBER_TYPE, this.cache = {};
}
isValid() {
return super.isValid() && this._isValid();
}
_isValid() {
const {text: text} = this.attribute;
return isArray(text) ? !text.every((t => null == t || "" === t)) : null != text && "" !== text;
}
getGraphicTheme() {
return getTheme(this).text;
}
doUpdateOBBBounds() {
const graphicTheme = this.getGraphicTheme();
this._OBBBounds.clear();
const attribute = this.attribute, {angle: angle = graphicTheme.angle} = attribute;
if (!angle) {
const b = this.AABBBounds;
return this._OBBBounds.setValue(b.x1, b.y1, b.x2, b.y2), this._OBBBounds;
}
this.obbText || (this.obbText = new Text({})), this.obbText.setAttributes(Object.assign(Object.assign({}, attribute), {
angle: 0
}));
const bounds1 = this.obbText.AABBBounds, {x: x, y: y} = attribute, boundsCenter = {
x: (bounds1.x1 + bounds1.x2) / 2,
y: (bounds1.y1 + bounds1.y2) / 2
}, center = rotatePoint(boundsCenter, angle, {
x: x,
y: y
});
return this._OBBBounds.copy(bounds1), this._OBBBounds.translate(center.x - boundsCenter.x, center.y - boundsCenter.y),
this._OBBBounds.angle = angle, this._OBBBounds;
}
updateAABBBounds(attribute, textTheme, aabbBounds) {
const {text: text = textTheme.text} = this.attribute;
Array.isArray(text) ? this.updateMultilineAABBBounds(text) : this.updateSingallineAABBBounds(text);
const {tb1: tb1} = application.graphicService.updateTempAABBBounds(aabbBounds), {scaleX: scaleX = textTheme.scaleX, scaleY: scaleY = textTheme.scaleY, shadowBlur: shadowBlur = textTheme.shadowBlur, strokeBoundsBuffer: strokeBoundsBuffer = textTheme.strokeBoundsBuffer} = attribute;
if (shadowBlur) {
const shadowBlurHalfWidth = shadowBlur / Math.abs(scaleX + scaleY);
boundStroke(tb1, shadowBlurHalfWidth, !0, strokeBoundsBuffer), aabbBounds.union(tb1);
}
return application.graphicService.combindShadowAABBBounds(aabbBounds, this), null == attribute.forceBoundsHeight && null == attribute.forceBoundsWidth || application.graphicService.updateHTMLTextAABBBounds(attribute, textTheme, aabbBounds),
transformBoundsWithMatrix(aabbBounds, aabbBounds, this.transMatrix), aabbBounds;
}
updateSingallineAABBBounds(text) {
this.updateMultilineAABBBounds([ text ]);
const layoutData = this.cache.layoutData;
if (layoutData && layoutData.lines && layoutData.lines.length) {
const line = layoutData.lines[0];
this.cache.clipedText = line.str, this.cache.clipedWidth = line.width;
}
return this._AABBBounds;
}
updateMultilineAABBBounds(text) {
const textTheme = this.getGraphicTheme(), {direction: direction = textTheme.direction, underlineOffset: underlineOffset = textTheme.underlineOffset} = this.attribute, b = "horizontal" === direction ? this.updateHorizontalMultilineAABBBounds(text) : this.updateVerticalMultilineAABBBounds(text);
return "horizontal" === direction && underlineOffset && this._AABBBounds.add(this._AABBBounds.x1, this._AABBBounds.y2 + underlineOffset),
b;
}
guessLineHeightBuf(fontSize) {
return fontSize ? .1 * fontSize : 0;
}
updateHorizontalMultilineAABBBounds(text) {
var _a;
const textTheme = this.getGraphicTheme(), attribute = this.attribute, {fontFamily: fontFamily = textTheme.fontFamily, textAlign: textAlign = textTheme.textAlign, textBaseline: textBaseline = textTheme.textBaseline, fontSize: fontSize = textTheme.fontSize, fontWeight: fontWeight = textTheme.fontWeight, ellipsis: ellipsis = textTheme.ellipsis, maxLineWidth: maxLineWidth, stroke: stroke = textTheme.stroke, wrap: wrap = textTheme.wrap, measureMode: measureMode = textTheme.measureMode, lineWidth: lineWidth = textTheme.lineWidth, whiteSpace: whiteSpace = textTheme.whiteSpace, suffixPosition: suffixPosition = textTheme.suffixPosition, ignoreBuf: ignoreBuf = textTheme.ignoreBuf, keepCenterInLine: keepCenterInLine = textTheme.keepCenterInLine} = attribute, buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize), lineHeight = this.getLineHeight(attribute, textTheme, buf);
if ("normal" === whiteSpace || wrap) return this.updateWrapAABBBounds(text);
if (!this.shouldUpdateShape() && (null === (_a = this.cache) || void 0 === _a ? void 0 : _a.layoutData)) {
const bbox = this.cache.layoutData.bbox;
return this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height),
stroke && this._AABBBounds.expand(lineWidth / 2), this._AABBBounds;
}
const textMeasure = application.graphicUtil.textMeasure, layoutData = new CanvasTextLayout(fontFamily, {
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: fontFamily,
lineHeight: lineHeight
}, textMeasure).GetLayoutByLines(text, textAlign, textBaseline, lineHeight, !0 === ellipsis ? textTheme.ellipsis : ellipsis || void 0, !1, {
lineWidth: maxLineWidth,
suffixPosition: suffixPosition,
measureMode: measureMode,
keepCenterInLine: keepCenterInLine
}), {bbox: bbox} = layoutData;
return this.cache.layoutData = layoutData, this.clearUpdateShapeTag(), this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height),
stroke && this._AABBBounds.expand(lineWidth / 2), this._AABBBounds;
}
updateWrapAABBBounds(text) {
var _a, _b, _c;
const textTheme = this.getGraphicTheme(), {fontFamily: fontFamily = textTheme.fontFamily, textAlign: textAlign = textTheme.textAlign, textBaseline: textBaseline = textTheme.textBaseline, fontSize: fontSize = textTheme.fontSize, ellipsis: ellipsis = textTheme.ellipsis, maxLineWidth: maxLineWidth, stroke: stroke = textTheme.stroke, lineWidth: lineWidth = textTheme.lineWidth, wordBreak: wordBreak = textTheme.wordBreak, fontWeight: fontWeight = textTheme.fontWeight, ignoreBuf: ignoreBuf = textTheme.ignoreBuf, measureMode: measureMode = textTheme.measureMode, suffixPosition: suffixPosition = textTheme.suffixPosition, heightLimit: heightLimit = 0, lineClamp: lineClamp, keepCenterInLine: keepCenterInLine = textTheme.keepCenterInLine} = this.attribute, buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize), lineHeight = this.getLineHeight(this.attribute, textTheme, buf);
if (!this.shouldUpdateShape() && (null === (_a = this.cache) || void 0 === _a ? void 0 : _a.layoutData)) {
const bbox = this.cache.layoutData.bbox;
return this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height),
stroke && this._AABBBounds.expand(lineWidth / 2), this._AABBBounds;
}
const textMeasure = application.graphicUtil.textMeasure, textOptions = {
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: fontFamily,
lineHeight: lineHeight
}, layoutObj = new CanvasTextLayout(fontFamily, textOptions, textMeasure), lines = isArray(text) ? text.map((l => l.toString())) : [ text.toString() ], linesLayout = [], bboxWH = [ 0, 0 ];
let lineCountLimit = 1 / 0;
if (heightLimit > 0 && (lineCountLimit = Math.max(Math.floor(heightLimit / lineHeight), 1)),
lineClamp && (lineCountLimit = Math.min(lineCountLimit, lineClamp)), "number" == typeof maxLineWidth && maxLineWidth !== 1 / 0) {
if (maxLineWidth > 0) for (let i = 0; i < lines.length; i++) {
const str = lines[i];
let needCut = !0;
if (i === lineCountLimit - 1) {
const clip = textMeasure.clipTextWithSuffix(str, textOptions, maxLineWidth, ellipsis, !1, suffixPosition, i !== lines.length - 1), matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions, measureMode);
linesLayout.push({
str: clip.str,
width: clip.width,
ascent: matrics.ascent,
descent: matrics.descent,
keepCenterInLine: keepCenterInLine
});
break;
}
const clip = textMeasure.clipText(str, textOptions, maxLineWidth, "break-all" !== wordBreak, "keep-all" === wordBreak);
if ("" !== str && "" === clip.str || clip.wordBreaked) {
if (ellipsis) {
const clipEllipsis = textMeasure.clipTextWithSuffix(str, textOptions, maxLineWidth, ellipsis, !1, suffixPosition);
clip.str = null !== (_b = clipEllipsis.str) && void 0 !== _b ? _b : "", clip.width = null !== (_c = clipEllipsis.width) && void 0 !== _c ? _c : 0;
} else clip.str = "", clip.width = 0;
needCut = !1;
}
const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions, measureMode);
linesLayout.push({
str: clip.str,
width: clip.width,
ascent: matrics.ascent,
descent: matrics.descent,
keepCenterInLine: keepCenterInLine
});
let cutLength = clip.str.length;
if (!clip.wordBreaked || "" !== str && "" === clip.str || (needCut = !0, cutLength = clip.wordBreaked),
clip.str.length === str.length) ; else if (needCut) {
let newStr = str.substring(cutLength);
"keep-all" === wordBreak && (newStr = newStr.replace(/^\s+/g, "")), lines.splice(i + 1, 0, newStr);
}
}
let maxWidth = 0;
linesLayout.forEach((layout => {
maxWidth = Math.max(maxWidth, layout.width);
})), bboxWH[0] = maxWidth;
} else {
let width, text, lineWidth = 0;
for (let i = 0, len = lines.length; i < len; i++) {
if (i === lineCountLimit - 1) {
const clip = textMeasure.clipTextWithSuffix(lines[i], textOptions, maxLineWidth, ellipsis, !1, suffixPosition), matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions, measureMode);
linesLayout.push({
str: clip.str,
width: clip.width,
ascent: matrics.ascent,
descent: matrics.descent,
keepCenterInLine: keepCenterInLine
}), lineWidth = Math.max(lineWidth, clip.width);
break;
}
text = lines[i], width = textMeasure.measureTextWidth(text, textOptions), lineWidth = Math.max(lineWidth, width);
const matrics = textMeasure.measureTextPixelADscentAndWidth(text, textOptions, measureMode);
linesLayout.push({
str: text,
width: width,
ascent: matrics.ascent,
descent: matrics.descent,
keepCenterInLine: keepCenterInLine
});
}
bboxWH[0] = lineWidth;
}
bboxWH[1] = linesLayout.length * lineHeight;
const bbox = {
xOffset: 0,
yOffset: 0,
width: bboxWH[0],
height: bboxWH[1]
};
layoutObj.LayoutBBox(bbox, textAlign, textBaseline, linesLayout);
const layoutData = layoutObj.layoutWithBBox(bbox, linesLayout, textAlign, textBaseline, lineHeight);
return this.cache.layoutData = layoutData, this.clearUpdateShapeTag(), this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height),
stroke && this._AABBBounds.expand(lineWidth / 2), this._AABBBounds;
}
updateVerticalMultilineAABBBounds(text) {
var _a, _b;
const textTheme = this.getGraphicTheme(), textMeasure = application.graphicUtil.textMeasure;
let width;
const attribute = this.attribute, {maxLineWidth: maxLineWidth = textTheme.maxLineWidth, ellipsis: ellipsis = textTheme.ellipsis, fontFamily: fontFamily = textTheme.fontFamily, fontSize: fontSize = textTheme.fontSize, fontWeight: fontWeight = textTheme.fontWeight, stroke: stroke = textTheme.stroke, lineWidth: lineWidth = textTheme.lineWidth, verticalMode: verticalMode = textTheme.verticalMode, suffixPosition: suffixPosition = textTheme.suffixPosition} = attribute, lineHeight = this.getLineHeight(attribute, textTheme, 0);
let {textAlign: textAlign = textTheme.textAlign, textBaseline: textBaseline = textTheme.textBaseline} = attribute;
if (!verticalMode) {
const t = textAlign;
textAlign = null !== (_a = Text.baselineMapAlign[textBaseline]) && void 0 !== _a ? _a : "left",
textBaseline = null !== (_b = Text.alignMapBaseline[t]) && void 0 !== _b ? _b : "top";
}
if (width = 0, !this.shouldUpdateShape() && this.cache) {
this.cache.verticalList.forEach((item => {
const w = item.reduce(((a, b) => a + b.width), 0);
width = max(w, width);
}));
const dx = textDrawOffsetX(textAlign, width), height = this.cache.verticalList.length * lineHeight, dy = textLayoutOffsetY(textBaseline, height, fontSize);
return this._AABBBounds.set(dy, dx, dy + height, dx + width), stroke && this._AABBBounds.expand(lineWidth / 2),
this._AABBBounds;
}
const verticalLists = text.map((str => verticalLayout(str.toString())));
verticalLists.forEach(((verticalData, i) => {
if (Number.isFinite(maxLineWidth)) if (ellipsis) {
const strEllipsis = !0 === ellipsis ? textTheme.ellipsis : ellipsis, data = textMeasure.clipTextWithSuffixVertical(verticalData, {
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: fontFamily
}, maxLineWidth, strEllipsis, !1, suffixPosition);
verticalLists[i] = data.verticalList, width = data.width;
} else {
const data = textMeasure.clipTextVertical(verticalData, {
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: fontFamily
}, maxLineWidth, !1);
verticalLists[i] = data.verticalList, width = data.width;
} else width = 0, verticalData.forEach((t => {
const w = t.direction === TextDirection.HORIZONTAL ? fontSize : textMeasure.measureTextWidth(t.text, {
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: fontFamily
});
width += w, t.width = w;
}));
})), this.cache.verticalList = verticalLists, this.clearUpdateShapeTag(), this.cache.verticalList.forEach((item => {
const w = item.reduce(((a, b) => a + b.width), 0);
width = max(w, width);
}));
const dx = textDrawOffsetX(textAlign, width), height = this.cache.verticalList.length * lineHeight, dy = textLayoutOffsetY(textBaseline, height, fontSize);
return this._AABBBounds.set(dy, dx, dy + height, dx + width), stroke && this._AABBBounds.expand(lineWidth / 2),
this._AABBBounds;
}
getMaxWidth(theme) {
var _a, _b;
const attribute = this.attribute;
return null !== (_b = null !== (_a = attribute.maxLineWidth) && void 0 !== _a ? _a : attribute.maxWidth) && void 0 !== _b ? _b : theme.maxWidth;
}
getLineHeight(attribute, textTheme, buf) {
var _a;
return null !== (_a = calculateLineHeight(attribute.lineHeight, attribute.fontSize || textTheme.fontSize)) && void 0 !== _a ? _a : (attribute.fontSize || textTheme.fontSize) + buf;
}
needUpdateTags(keys, k = TEXT_UPDATE_TAG_KEY) {
return super.needUpdateTags(keys, k);
}
needUpdateTag(key, k = TEXT_UPDATE_TAG_KEY) {
return super.needUpdateTag(key, k);
}
_interpolate(key, ratio, lastStepVal, nextStepVal, nextAttributes) {
"text" === key && (nextAttributes.text = nextStepVal);
}
clone() {
return new Text(Object.assign({}, this.attribute));
}
getNoWorkAnimateAttr() {
return Text.NOWORK_ANIMATE_ATTR;
}
getBaselineMapAlign() {
return Text.baselineMapAlign;
}
getAlignMapBaseline() {
return Text.alignMapBaseline;
}
}
Text.NOWORK_ANIMATE_ATTR = Object.assign({
ellipsis: 1,
wordBreak: 1,
direction: 1,
textAlign: 1,
textBaseline: 1,
fontFamily: 1,
fontWeight: 1
}, NOWORK_ANIMATE_ATTR), Text.baselineMapAlign = {
top: "left",
bottom: "right",
middle: "center"
}, Text.alignMapBaseline = {
left: "top",
right: "bottom",
center: "middle"
};
export function createText(attributes) {
return new Text(attributes);
}
//# sourceMappingURL=text.js.map