modern-text
Version:
Measure and render text in a way that describes the DOM.
1,880 lines • 60 kB
JavaScript
import { isGradient, parseGradient, isNone, getDefaultStyle, Reactivable, normalizeText, property } from 'modern-idoc';
import { setCanvasContext, Path2D, BoundingBox, svgToDom, svgToPath2DSet, Path2DSet, Vector2, Matrix3 } from 'modern-path2d';
import { fonts } from 'modern-font';
class Canvas2DRenderer {
constructor(text, context) {
this.text = text;
this.context = context;
}
pixelRatio = window?.devicePixelRatio || 1;
_setupView = () => {
const pixelRatio = this.pixelRatio;
const ctx = this.context;
const { left, top, width, height } = this.text.boundingBox;
const view = ctx.canvas;
view.dataset.viewBox = String(`${left} ${top} ${width} ${height}`);
view.dataset.pixelRatio = String(pixelRatio);
const canvasWidth = width;
const canvasHeight = height;
view.width = Math.max(1, Math.ceil(canvasWidth * pixelRatio));
view.height = Math.max(1, Math.ceil(canvasHeight * pixelRatio));
view.style.width = `${canvasWidth}px`;
view.style.height = `${canvasHeight}px`;
ctx.clearRect(0, 0, view.width, view.height);
ctx.scale(pixelRatio, pixelRatio);
ctx.translate(-left, -top);
};
_setupColors = () => {
const { paragraphs, computedStyle, glyphBox } = this.text;
this.uploadColor(computedStyle, glyphBox);
paragraphs.forEach((paragraph) => {
this.uploadColor(paragraph.computedStyle, paragraph.lineBox);
paragraph.fragments.forEach((fragment) => {
this.uploadColor(fragment.computedStyle, fragment.inlineBox);
});
});
};
setup = () => {
this._setupView();
this._setupColors();
return this;
};
_parseColor = (source, box) => {
if (typeof source === "string" && isGradient(source)) {
const gradient = parseGradient(source)[0];
if (gradient) {
switch (gradient.type) {
case "linear-gradient": {
let deg = 0;
if (gradient.orientation) {
switch (gradient.orientation.type) {
case "angular":
deg = Number(gradient.orientation.value);
break;
}
}
const { left, top, width, height } = box;
const rad = deg * Math.PI / 180;
const offsetX = width * Math.sin(rad);
const offsetY = height * Math.cos(rad);
const canvasGradient = this.context.createLinearGradient(
left + width / 2 - offsetX,
top + height / 2 + offsetY,
left + width / 2 + offsetX,
top + height / 2 - offsetY
);
gradient.colorStops.forEach((colorStop) => {
let offset = 0;
if (colorStop.length) {
switch (colorStop.length.type) {
case "%":
offset = Number(colorStop.length.value) / 100;
break;
}
}
switch (colorStop.type) {
case "rgb":
canvasGradient.addColorStop(offset, `rgb(${colorStop.value.join(", ")})`);
break;
case "rgba":
canvasGradient.addColorStop(offset, `rgba(${colorStop.value.join(", ")})`);
break;
case "hex":
canvasGradient.addColorStop(offset, `#${colorStop.value}`);
break;
}
});
return canvasGradient;
}
}
}
}
return source;
};
_uploadedStyles = [
"color",
"backgroundColor",
"textStrokeColor"
];
uploadColor = (style, box) => {
this._uploadedStyles.forEach((key) => {
style[key] = this._parseColor(style[key], box);
});
};
drawPath = (path, options = {}) => {
const { clipRect } = options;
const ctx = this.context;
ctx.save();
ctx.beginPath();
if (clipRect) {
ctx.rect(clipRect.left, clipRect.top, clipRect.width, clipRect.height);
ctx.clip();
ctx.beginPath();
}
path.drawTo(ctx, this._mergePathStyle(path, options));
ctx.restore();
};
_mergePathStyle(path, style) {
const {
fontSize = this.text.computedStyle.fontSize
} = style;
const pathStyle = path.style;
const stroke = style.stroke ?? pathStyle.stroke;
const strokeWidth = style.strokeWidth ? style.strokeWidth * fontSize : pathStyle.strokeWidth;
return {
...pathStyle,
...style,
fill: style.fill ?? pathStyle.fill,
stroke: strokeWidth === void 0 || strokeWidth > 0 ? stroke : void 0,
strokeLinecap: style.strokeLinecap ?? pathStyle.strokeLinecap ?? "round",
strokeLinejoin: style.strokeLinejoin ?? pathStyle.strokeLinejoin ?? "round",
strokeWidth,
shadowOffsetX: (style.shadowOffsetX ?? 0) * fontSize,
shadowOffsetY: (style.shadowOffsetY ?? 0) * fontSize,
shadowBlur: (style.shadowBlur ?? 0) * fontSize,
shadowColor: style.shadowColor
};
}
drawCharacter = (character, userStyle = {}) => {
const ctx = this.context;
const {
computedStyle,
path,
glyphBox,
isVertical,
content,
inlineBox,
baseline,
computedFill,
computedOutline
} = character;
const style = {
...computedStyle,
...userStyle
};
const pathStyle = {
strokeLinecap: computedOutline?.lineCap,
strokeLinejoin: computedOutline?.lineJoin,
...style,
fill: userStyle.color ?? computedFill?.color ?? computedStyle.color,
strokeWidth: userStyle.textStrokeWidth ?? computedOutline?.width ?? computedStyle.textStrokeWidth,
stroke: userStyle.textStrokeColor ?? computedOutline?.color ?? computedStyle.textStrokeColor
};
if (glyphBox) {
this.drawPath(path, pathStyle);
} else {
ctx.save();
ctx.beginPath();
setCanvasContext(ctx, this._mergePathStyle(path, pathStyle));
ctx.font = `${style.fontSize}px ${style.fontFamily}`;
if (isVertical) {
ctx.textBaseline = "middle";
ctx.fillText(content, inlineBox.left, inlineBox.top + inlineBox.height / 2);
} else {
ctx.textBaseline = "alphabetic";
ctx.fillText(content, inlineBox.left, inlineBox.top + baseline);
}
ctx.restore();
}
};
}
const set1 = /* @__PURE__ */ new Set(["\xA9", "\xAE", "\xF7"]);
const set2 = /* @__PURE__ */ new Set([
"\u2014",
"\u2026",
"\u201C",
"\u201D",
"\uFE4F",
"\uFE4B",
"\uFE4C",
"\u2018",
"\u2019",
"\u02DC"
]);
const fsSelectionMap = {
1: "italic",
32: "bold"
};
const macStyleMap = {
1: "italic",
2: "bold"
};
const fontWeightMap = {
100: -0.2,
200: -0.1,
300: 0,
400: 0,
normal: 0,
500: 0.1,
600: 0.2,
700: 0.3,
bold: 0.3,
800: 0.4,
900: 0.5
};
class Character {
constructor(content, index, parent) {
this.content = content;
this.index = index;
this.parent = parent;
}
path = new Path2D().setMeta(this);
lineBox = new BoundingBox();
inlineBox = new BoundingBox();
glyphBox;
advanceWidth = 0;
advanceHeight = 0;
underlinePosition = 0;
underlineThickness = 0;
strikeoutPosition = 0;
strikeoutSize = 0;
ascender = 0;
descender = 0;
typoAscender = 0;
typoDescender = 0;
typoLineGap = 0;
winAscent = 0;
winDescent = 0;
xHeight = 0;
capHeight = 0;
baseline = 0;
centerDiviation = 0;
fontStyle;
get compatibleGlyphBox() {
const size = this.computedStyle.fontSize * 0.8;
return this.glyphBox ?? (this.isVertical ? new BoundingBox(
this.lineBox.left + this.lineBox.width / 2 - size / 2,
this.lineBox.top,
size,
this.lineBox.height
) : new BoundingBox(
this.lineBox.left,
this.lineBox.top + this.lineBox.height / 2 - size / 2,
this.lineBox.width,
size
));
}
get center() {
return this.compatibleGlyphBox.center;
}
get computedFill() {
return this.parent.computedFill;
}
get computedOutline() {
return this.parent.computedOutline;
}
get computedStyle() {
return this.parent.computedStyle;
}
get isVertical() {
return this.computedStyle.writingMode.includes("vertical");
}
get fontSize() {
return this.computedStyle.fontSize;
}
get fontHeight() {
return this.fontSize * this.computedStyle.lineHeight;
}
_getFontSFNT(fonts$1) {
const fontFamily = this.computedStyle.fontFamily;
const _fonts = fonts$1 ?? fonts;
const font = fontFamily ? _fonts.get(fontFamily) : _fonts.fallbackFont;
let sfnt = font?.getSFNT();
if (sfnt?.textToGlyphIndexes(this.content).includes(0)) {
sfnt = _fonts.fallbackFont?.getSFNT();
}
return sfnt;
}
updateGlyph(sfnt = this._getFontSFNT()) {
if (!sfnt) {
return this;
}
const { hhea, os2, post, head } = sfnt;
const unitsPerEm = head.unitsPerEm;
const ascender = hhea.ascent;
const descender = hhea.descent;
const { content, computedStyle } = this;
const { fontSize } = computedStyle;
const rate = unitsPerEm / fontSize;
const advanceWidth = sfnt.getAdvanceWidth(content, fontSize);
const advanceHeight = (ascender + Math.abs(descender)) / rate;
const baseline = ascender / rate;
this.advanceWidth = advanceWidth;
this.advanceHeight = advanceHeight;
this.underlinePosition = (ascender - post.underlinePosition) / rate;
this.underlineThickness = post.underlineThickness / rate;
this.strikeoutPosition = (ascender - os2.yStrikeoutPosition) / rate;
this.strikeoutSize = os2.yStrikeoutSize / rate;
this.ascender = ascender / rate;
this.descender = descender / rate;
this.typoAscender = os2.sTypoAscender / rate;
this.typoDescender = os2.sTypoDescender / rate;
this.typoLineGap = os2.sTypoLineGap / rate;
this.winAscent = os2.usWinAscent / rate;
this.winDescent = os2.usWinDescent / rate;
this.xHeight = os2.sxHeight / rate;
this.capHeight = os2.sCapHeight / rate;
this.baseline = baseline;
this.centerDiviation = advanceHeight / 2 - baseline;
this.fontStyle = fsSelectionMap[os2.fsSelection] ?? macStyleMap[head.macStyle];
return this;
}
update(fonts) {
const sfnt = this._getFontSFNT(fonts);
if (!sfnt) {
return this;
}
this.updateGlyph(sfnt);
const {
isVertical,
content,
computedStyle: style,
baseline,
inlineBox,
ascender,
descender,
typoAscender,
fontStyle,
advanceWidth,
advanceHeight
} = this;
const { left, top } = inlineBox;
const needsItalic = style.fontStyle === "italic" && fontStyle !== "italic";
let x = left;
let y = top + baseline;
let glyphIndex;
const path = new Path2D().setMeta(this);
if (isVertical) {
x += (advanceHeight - advanceWidth) / 2;
if (Math.abs(advanceWidth - advanceHeight) > 0.1) {
y -= (ascender - typoAscender) / (ascender + Math.abs(descender)) * advanceHeight;
}
glyphIndex = void 0;
}
if (isVertical && !set1.has(content) && (content.codePointAt(0) <= 256 || set2.has(content))) {
path.addCommands(
sfnt.getPathCommands(
content,
x,
top + baseline - (advanceHeight - advanceWidth) / 2,
style.fontSize
)
);
const point = {
y: top - (advanceHeight - advanceWidth) / 2 + advanceHeight / 2,
x: x + advanceWidth / 2
};
if (needsItalic) {
this._italic(
path,
isVertical ? {
x: point.x,
y: top - (advanceHeight - advanceWidth) / 2 + baseline
} : void 0
);
}
path.rotate(90, point);
} else {
if (glyphIndex !== void 0) {
path.addCommands(
sfnt.glyphs.get(glyphIndex).getPathCommands(x, y, style.fontSize)
);
if (needsItalic) {
this._italic(
path,
isVertical ? {
x: x + advanceWidth / 2,
y: top + typoAscender / (ascender + Math.abs(descender)) * advanceHeight
} : void 0
);
}
} else {
path.addCommands(sfnt.getPathCommands(content, x, y, style.fontSize));
if (needsItalic) {
this._italic(
path,
isVertical ? { x: x + advanceHeight / 2, y } : void 0
);
}
}
}
const fontWeight = style.fontWeight ?? 400;
if (fontWeight in fontWeightMap && ((fontWeight === 700 || fontWeight === "bold") && fontStyle !== "bold")) {
path.bold(fontWeightMap[fontWeight] * style.fontSize * 0.05);
}
path.style = {
fill: this.computedFill ?? style.color,
fillRule: "nonzero",
stroke: this.computedOutline ?? (style.textStrokeWidth ? style.textStrokeColor : "none"),
strokeWidth: style.textStrokeWidth ? style.textStrokeWidth * style.fontSize * 0.03 : 0
};
this.path = path;
this.glyphBox = this.getGlyphBoundingBox();
return this;
}
_italic(path, startPoint) {
path.skew(-0.24, 0, startPoint || {
y: this.inlineBox.top + this.baseline,
x: this.inlineBox.left + this.inlineBox.width / 2
});
}
getGlyphMinMax(min, max, withStyle) {
if (this.path.curves[0]?.curves.length) {
return this.path.getMinMax(min, max, withStyle);
} else {
return void 0;
}
}
getGlyphBoundingBox(withStyle) {
const minMax = this.getGlyphMinMax(void 0, void 0, withStyle);
if (!minMax) {
return void 0;
}
const { min, max } = minMax;
return new BoundingBox(min.x, min.y, max.x - min.x, max.y - min.y);
}
}
function createSvgLoader() {
const loaded = /* @__PURE__ */ new Map();
async function load(svg) {
if (!loaded.has(svg)) {
loaded.set(svg, "");
try {
loaded.set(svg, await fetch(svg).then((rep) => rep.text()));
} catch (err) {
console.warn(err);
loaded.delete(svg);
}
}
}
function needsLoad(source) {
return source.startsWith("/") || source.startsWith("./") || source.startsWith("http://") || source.startsWith("https://") || source.startsWith("blob://");
}
return {
loaded,
needsLoad,
load
};
}
function createSvgParser(loader) {
const parsed = /* @__PURE__ */ new Map();
function parse(svg) {
let result = parsed.get(svg);
if (!result) {
const svgString = loader.needsLoad(svg) ? loader.loaded.get(svg) : svg;
if (svgString) {
const dom = svgToDom(svgString);
const pathSet = svgToPath2DSet(dom);
result = { dom, pathSet };
parsed.set(svg, result);
} else {
const dom = document.createElementNS("http://www.w3.org/2000/svg", "svg");
dom.setAttribute("width", "0");
dom.setAttribute("height", "0");
dom.setAttribute("viewBox", "0 0 0 0");
result = { dom, pathSet: new Path2DSet() };
}
}
return result;
}
return {
parsed,
parse
};
}
function parseValueNumber(value, ctx) {
if (typeof value === "number") {
return value;
} else {
if (value.endsWith("%")) {
value = value.substring(0, value.length - 1);
return Math.ceil(Number(value) / 100 * ctx.total);
} else if (value.endsWith("rem")) {
value = value.substring(0, value.length - 3);
return Number(value) * ctx.fontSize;
} else if (value.endsWith("em")) {
value = value.substring(0, value.length - 2);
return Number(value) * ctx.fontSize;
} else {
return Number(value);
}
}
}
function parseColormap(colormap) {
return isNone(colormap) ? {} : colormap;
}
function isEqualObject(obj1, obj2) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
const keys = Array.from(/* @__PURE__ */ new Set([...keys1, ...keys2]));
return keys.every((key) => isEqualValue(obj1[key], obj2[key]));
}
function isEqualValue(val1, val2) {
const typeof1 = typeof val1;
const typeof2 = typeof val2;
if (typeof1 === typeof2) {
if (typeof1 === "object") {
return isEqualObject(val1, val2);
}
return val1 === val2;
}
return false;
}
function hexToRgb(hex) {
const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex;
const isValidHex = /^(?:[0-9A-F]{3}|[0-9A-F]{6})$/i.test(cleanHex);
if (!isValidHex)
return null;
const fullHex = cleanHex.length === 3 ? cleanHex.split("").map((char) => char + char).join("") : cleanHex;
const r = Number.parseInt(fullHex.slice(0, 2), 16);
const g = Number.parseInt(fullHex.slice(2, 4), 16);
const b = Number.parseInt(fullHex.slice(4, 6), 16);
return `rgb(${r}, ${g}, ${b})`;
}
function filterEmpty(val) {
if (!val)
return val;
const res = {};
for (const key in val) {
if (val[key] !== "" && val[key] !== void 0) {
res[key] = val[key];
}
}
return res;
}
class Fragment {
constructor(content, style = {}, index, parent) {
this.content = content;
this.style = style;
this.index = index;
this.parent = parent;
this.updateComputedStyle().initCharacters();
}
inlineBox = new BoundingBox();
fill;
outline;
get computedFill() {
return this.fill ?? this.parent.computedFill;
}
get computedOutline() {
return this.outline ?? this.parent.computedOutline;
}
get computedContent() {
const style = this.computedStyle;
return style.textTransform === "uppercase" ? this.content.toUpperCase() : style.textTransform === "lowercase" ? this.content.toLowerCase() : this.content;
}
updateComputedStyle() {
this.computedStyle = {
...this.parent.computedStyle,
...filterEmpty(this.style)
};
return this;
}
initCharacters() {
const characters = [];
let index = 0;
for (const c of this.computedContent) {
characters.push(new Character(c, index++, this));
}
this.characters = characters;
return this;
}
}
class Paragraph {
constructor(style, index, parent) {
this.style = style;
this.index = index;
this.parent = parent;
this.updateComputedStyle();
}
lineBox = new BoundingBox();
fragments = [];
fill;
outline;
get computedFill() {
return this.fill ?? this.parent.fill;
}
get computedOutline() {
return this.outline ?? this.parent.outline;
}
updateComputedStyle() {
this.computedStyle = {
...filterEmpty(this.parent.computedStyle),
...filterEmpty(this.style)
};
return this;
}
}
function definePlugin(options) {
return options;
}
class Measurer {
static notZeroStyles = /* @__PURE__ */ new Set([
"width",
"height"
]);
static pxStyles = /* @__PURE__ */ new Set([
"width",
"height",
"fontSize",
"letterSpacing",
"textStrokeWidth",
"textIndent",
"shadowOffsetX",
"shadowOffsetY",
"shadowBlur",
"margin",
"marginLeft",
"marginTop",
"marginRight",
"marginBottom",
"padding",
"paddingLeft",
"paddingTop",
"paddingRight",
"paddingBottom"
]);
_toDomStyle(style) {
const _style = {};
for (const key in style) {
const value = style[key];
if (Measurer.notZeroStyles.has(key) && value === 0) ; else if (typeof value === "number" && Measurer.pxStyles.has(key)) {
_style[key] = `${value}px`;
} else {
_style[key] = value;
}
}
return _style;
}
/**
* <section style="...">
* <ul>
* <li style="...">
* <span style="...">...</span>
* <span>...</span>
* </li>
* </ul>
* </section>
*/
createDom(paragraphs, rootStyle) {
const dom = document.createElement("section");
const style = { ...rootStyle };
const isHorizontal = rootStyle.writingMode.includes("horizontal");
switch (rootStyle.textAlign) {
case "start":
case "left":
style.justifyContent = "flex-start";
break;
case "center":
style.justifyContent = "center";
break;
case "end":
case "right":
style.justifyContent = "flex-end";
break;
}
switch (rootStyle.verticalAlign) {
case "top":
style.alignItems = "flex-start";
break;
case "middle":
style.alignItems = "center";
break;
case "bottom":
style.alignItems = "flex-end";
break;
}
const isFlex = Boolean(style.justifyContent || style.alignItems);
Object.assign(dom.style, {
...this._toDomStyle({
...style,
boxSizing: style.boxSizing ?? "border-box",
display: style.display ?? (isFlex ? "inline-flex" : void 0),
width: style.width ?? "max-content",
height: style.height ?? "max-content"
}),
whiteSpace: "pre-wrap",
wordBreak: "break-all"
});
const ul = document.createElement("ul");
Object.assign(ul.style, {
verticalAlign: "inherit",
listStyleType: "inherit",
padding: "0",
margin: "0",
width: isFlex && isHorizontal ? "100%" : void 0,
height: isFlex && !isHorizontal ? "100%" : void 0
});
paragraphs.forEach((paragraph) => {
const li = document.createElement("li");
Object.assign(li.style, {
verticalAlign: "inherit",
...this._toDomStyle(paragraph.style)
});
paragraph.fragments.forEach((fragment) => {
const span = document.createElement("span");
Object.assign(span.style, {
verticalAlign: "inherit",
...this._toDomStyle(fragment.style)
});
span.appendChild(document.createTextNode(fragment.content));
li.appendChild(span);
});
ul.appendChild(li);
});
dom.appendChild(ul);
return dom;
}
measureDomText(text) {
const range = document.createRange();
range.selectNodeContents(text);
const data = text.data ?? "";
let offset = 0;
return Array.from(data).map((char) => {
const start = offset += data.substring(offset).indexOf(char);
const end = start + char.length;
offset += char.length;
range.setStart(text, Math.max(start, 0));
range.setEnd(text, end);
const rects = range.getClientRects?.() ?? [range.getBoundingClientRect()];
let rect = rects[rects.length - 1];
if (rects.length > 1 && rect.width < 2) {
rect = rects[rects.length - 2];
}
const content = range.toString();
if (content !== "" && rect && rect.width + rect.height !== 0) {
return {
content,
top: rect.top,
left: rect.left,
height: rect.height,
width: rect.width
};
}
return void 0;
}).filter(Boolean);
}
measureDom(dom) {
const paragraphs = [];
const fragments = [];
const characters = [];
dom.querySelectorAll("li").forEach((pDom, paragraphIndex) => {
const pBox = pDom.getBoundingClientRect();
paragraphs.push({
paragraphIndex,
left: pBox.x,
top: pBox.y,
width: pBox.width,
height: pBox.height
});
pDom.querySelectorAll(":scope > *").forEach((fDom, fragmentIndex) => {
const fBox = fDom.getBoundingClientRect();
fragments.push({
paragraphIndex,
fragmentIndex,
left: fBox.x,
top: fBox.y,
width: fBox.width,
height: fBox.height
});
let characterIndex = 0;
if (!fDom.children.length && fDom.firstChild instanceof window.Text) {
this.measureDomText(fDom.firstChild).forEach((char) => {
characters.push({
...char,
newParagraphIndex: -1,
paragraphIndex,
fragmentIndex,
characterIndex: characterIndex++,
textWidth: -1,
textHeight: -1
});
});
} else {
fDom.querySelectorAll(":scope > *").forEach((cDOM) => {
if (cDOM.firstChild instanceof window.Text) {
this.measureDomText(cDOM.firstChild).forEach((char) => {
characters.push({
...char,
newParagraphIndex: -1,
paragraphIndex,
fragmentIndex,
characterIndex: characterIndex++,
textWidth: -1,
textHeight: -1
});
});
}
});
}
});
});
return {
paragraphs,
fragments,
characters
};
}
measureParagraphDom(paragraphs, dom) {
const rect = dom.getBoundingClientRect();
const measured = this.measureDom(dom);
measured.paragraphs.forEach((p) => {
const box = paragraphs[p.paragraphIndex].lineBox;
box.left = p.left - rect.left;
box.top = p.top - rect.top;
box.width = p.width;
box.height = p.height;
});
measured.fragments.forEach((f) => {
const box = paragraphs[f.paragraphIndex].fragments[f.fragmentIndex].inlineBox;
box.left = f.left - rect.left;
box.top = f.top - rect.top;
box.width = f.width;
box.height = f.height;
});
const results = [];
let i = 0;
measured.characters.forEach((character) => {
const { paragraphIndex, fragmentIndex, characterIndex } = character;
results.push({
...character,
newParagraphIndex: paragraphIndex,
left: character.left - rect.left,
top: character.top - rect.top
});
const item = paragraphs[paragraphIndex].fragments[fragmentIndex].characters[characterIndex];
const { fontHeight, isVertical, inlineBox, lineBox } = item;
const result = results[i];
inlineBox.left = result.left;
inlineBox.top = result.top;
inlineBox.width = result.width;
inlineBox.height = result.height;
if (isVertical) {
lineBox.left = result.left + (result.width - fontHeight) / 2;
lineBox.top = result.top;
lineBox.width = fontHeight;
lineBox.height = result.height;
} else {
lineBox.left = result.left;
lineBox.top = result.top + (result.height - fontHeight) / 2;
lineBox.width = result.width;
lineBox.height = fontHeight;
}
i++;
});
return {
paragraphs,
boundingBox: new BoundingBox(0, 0, rect.width, rect.height)
};
}
measure(paragraphs, rootStyle, dom) {
let destory;
if (!dom) {
dom = this.createDom(paragraphs, rootStyle);
Object.assign(dom.style, {
position: "fixed",
visibility: "hidden"
});
document.body.appendChild(dom);
destory = () => dom?.parentNode?.removeChild(dom);
}
const result = this.measureParagraphDom(paragraphs, dom);
destory?.();
return result;
}
}
function backgroundPlugin() {
const pathSet = new Path2DSet();
const loader = createSvgLoader();
const parser = createSvgParser(loader);
return {
name: "background",
pathSet,
context: {
loader,
parser
},
load: async (text) => {
const { backgroundImage } = text.computedStyle;
if (backgroundImage && loader.needsLoad(backgroundImage)) {
await loader.load(backgroundImage);
}
},
update: (text) => {
pathSet.paths.length = 0;
const { computedStyle, lineBox, isVertical } = text;
const {
backgroundImage,
backgroundSize,
backgroundColormap
} = computedStyle;
if (isNone(backgroundImage))
return;
const { pathSet: imagePathSet } = parser.parse(backgroundImage);
const imageBox = imagePathSet.getBoundingBox(true) ?? new BoundingBox();
let x, y, width, height;
if (isVertical) {
({ x: y, y: x, width: height, height: width } = lineBox);
} else {
({ x, y, width, height } = lineBox);
}
const colormap = parseColormap(backgroundColormap ?? "none");
const paths = imagePathSet.paths.map((p) => {
const cloned = p.clone();
if (cloned.style.fill && cloned.style.fill in colormap) {
cloned.style.fill = colormap[cloned.style.fill];
}
if (cloned.style.stroke && cloned.style.stroke in colormap) {
cloned.style.stroke = colormap[cloned.style.stroke];
}
return cloned;
});
let scaleX;
let scaleY;
if (backgroundSize === "rigid") {
scaleX = Math.max(text.fontSize * 5 / imageBox.width);
scaleY = scaleX;
const dist = new Vector2();
dist.x = imageBox.width - width / scaleX;
dist.y = imageBox.height - height / scaleY;
paths.forEach((path) => {
path.applyTransform((p) => {
const hasX = p.x > imageBox.left + imageBox.width / 2;
const hasY = p.y > imageBox.top + imageBox.height / 2;
if (hasX) {
p.x -= dist.x;
}
if (hasY) {
p.y -= dist.y;
}
});
});
} else {
scaleX = width / imageBox.width;
scaleY = height / imageBox.height;
}
const transform = new Matrix3();
transform.translate(-imageBox.x, -imageBox.y);
transform.scale(scaleX, scaleY);
if (isVertical) {
transform.translate(-width / 2, -height / 2);
transform.rotate(-Math.PI / 2);
transform.translate(height / 2, width / 2);
}
transform.translate(x, y);
paths.forEach((path) => {
path.applyTransform((p) => {
p.applyMatrix3(transform);
});
});
pathSet.paths.push(...paths);
},
renderOrder: -2,
render: (renderer) => {
const { text, context } = renderer;
const { boundingBox, computedStyle: style } = text;
if (!isNone(style.backgroundColor)) {
context.fillStyle = style.backgroundColor;
context.fillRect(...boundingBox.array);
}
pathSet.paths.forEach((path) => {
renderer.drawPath(path);
if (text.debug) {
const box = new Path2DSet([path]).getBoundingBox();
if (box) {
context.strokeRect(box.x, box.y, box.width, box.height);
}
}
});
text.paragraphs.forEach((p) => {
const { lineBox, style: style2 } = p;
if (!isNone(style2.backgroundColor)) {
context.fillStyle = style2.backgroundColor;
context.fillRect(...lineBox.array);
}
p.fragments.forEach((f) => {
const { inlineBox, style: style3 } = f;
if (!isNone(style3.backgroundColor)) {
context.fillStyle = style3.backgroundColor;
context.fillRect(...inlineBox.array);
}
});
});
}
};
}
function getHighlightStyle(style) {
const {
highlight,
highlightImage,
highlightReferImage,
highlightColormap,
highlightLine,
highlightSize,
highlightThickness
} = style;
return {
image: highlight?.image ?? highlightImage ?? "none",
referImage: highlight?.referImage ?? highlightReferImage ?? "none",
colormap: highlight?.colormap ?? highlightColormap ?? "none",
line: highlight?.line ?? highlightLine ?? "none",
size: highlight?.size ?? highlightSize ?? "cover",
thickness: highlight?.thickness ?? highlightThickness ?? "100%"
};
}
function highlightPlugin() {
const pathSet = new Path2DSet();
const clipRects = [];
const loader = createSvgLoader();
const parser = createSvgParser(loader);
return definePlugin({
name: "highlight",
pathSet,
context: {
clipRects,
loader,
parser
},
load: async (text) => {
const set = /* @__PURE__ */ new Set();
text.forEachCharacter((character) => {
const { computedStyle: style } = character;
const { image, referImage } = getHighlightStyle(style);
if (image && loader.needsLoad(image)) {
set.add(image);
}
if (referImage && loader.needsLoad(referImage)) {
set.add(referImage);
}
});
await Promise.all(Array.from(set).map((src) => loader.load(src)));
},
update: (text) => {
clipRects.length = 0;
pathSet.paths.length = 0;
let groups = [];
let group;
let prevHighlight;
text.forEachCharacter((character) => {
const {
computedStyle: style
} = character;
const highlight = getHighlightStyle(style);
const {
image,
colormap,
line,
size,
thickness
} = highlight;
if (!isNone(image)) {
const { inlineBox, isVertical } = character;
const { fontSize } = style;
if ((!prevHighlight || isEqualValue(prevHighlight.image, image) && isEqualValue(prevHighlight.colormap, colormap) && isEqualValue(prevHighlight.line, line) && isEqualValue(prevHighlight.size, size) && isEqualValue(prevHighlight.thickness, thickness)) && group?.length && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top) && group[0].fontSize === fontSize) {
group.push(character);
} else {
group = [];
group.push(character);
groups.push(group);
}
} else {
if (group?.length) {
group = [];
groups.push(group);
}
}
prevHighlight = highlight;
});
groups = groups.filter((characters) => characters.length);
for (let i = 0; i < groups.length; i++) {
const characters = groups[i];
const char = characters[0];
const groupBox = BoundingBox.from(...characters.map((c) => c.compatibleGlyphBox));
if (!groupBox.height || !groupBox.width) {
continue;
}
const {
computedStyle: style,
isVertical,
inlineBox,
compatibleGlyphBox: glyphBox,
strikeoutPosition,
underlinePosition
} = char;
const { fontSize } = style;
const {
image,
referImage,
colormap,
line,
size,
thickness
} = getHighlightStyle(style);
const _thickness = parseValueNumber(thickness, { fontSize, total: groupBox.width }) / groupBox.width;
const _colormap = parseColormap(colormap);
const { pathSet: imagePathSet, dom: imageDom } = parser.parse(image);
const imageBox = imagePathSet.getBoundingBox(true) ?? new BoundingBox();
const styleScale = fontSize / imageBox.width * 2;
const targetBox = new BoundingBox().copy(groupBox);
if (isVertical) {
targetBox.width = groupBox.height;
targetBox.height = groupBox.width;
targetBox.left = groupBox.left + groupBox.width;
}
const rawWidth = Math.floor(targetBox.width);
let userWidth = rawWidth;
if (size !== "cover") {
userWidth = parseValueNumber(size, { fontSize, total: groupBox.width }) || rawWidth;
targetBox.width = userWidth;
}
const hasReferImage = !isNone(referImage) && isNone(line);
if (hasReferImage) {
imageBox.copy(
parser.parse(referImage).pathSet.getBoundingBox(true)
);
} else {
let _line;
if (isNone(line)) {
if (imageBox.width / imageBox.height > 4) {
_line = "underline";
const viewBox = imageDom.getAttribute("viewBox");
if (viewBox) {
const [_x, y, _w, h] = viewBox.split(" ").map((v) => Number(v));
const viewCenter = y + h / 2;
if (imageBox.y < viewCenter && imageBox.y + imageBox.height > viewCenter) {
_line = "line-through";
} else if (imageBox.y + imageBox.height < viewCenter) {
_line = "overline";
} else {
_line = "underline";
}
}
} else {
_line = "outline";
}
} else {
_line = line;
}
switch (_line) {
case "outline": {
const paddingX = targetBox.width * 0.2;
const paddingY = targetBox.height * 0.2;
if (isVertical) {
targetBox.x -= paddingY / 2;
targetBox.y -= paddingX / 2;
targetBox.x -= targetBox.height;
} else {
targetBox.x -= paddingX / 2;
targetBox.y -= paddingY / 2;
}
targetBox.width += paddingX;
targetBox.height += paddingY;
break;
}
case "overline":
targetBox.height = imageBox.height * styleScale;
if (isVertical) {
targetBox.x = inlineBox.left + inlineBox.width;
} else {
targetBox.y = inlineBox.top;
}
break;
case "line-through":
targetBox.height = imageBox.height * styleScale;
if (isVertical) {
targetBox.x = inlineBox.left + inlineBox.width - strikeoutPosition + targetBox.height / 2;
} else {
targetBox.y = inlineBox.top + strikeoutPosition - targetBox.height / 2;
}
break;
case "underline":
targetBox.height = imageBox.height * styleScale;
if (isVertical) {
targetBox.x = glyphBox.left + glyphBox.width - underlinePosition;
} else {
targetBox.y = inlineBox.top + underlinePosition;
}
break;
}
}
const transform = new Matrix3();
transform.translate(-imageBox.x, -imageBox.y);
transform.scale(targetBox.width / imageBox.width, targetBox.height / imageBox.height);
if (isVertical) {
const tx = targetBox.width / 2;
const ty = targetBox.height / 2;
if (!hasReferImage) {
transform.translate(-tx, -ty);
}
transform.rotate(-Math.PI / 2);
if (!hasReferImage) {
transform.translate(ty, tx);
}
}
transform.translate(targetBox.x, targetBox.y);
for (let i2 = 0; i2 < Math.ceil(rawWidth / userWidth); i2++) {
const _transform = transform.clone();
if (isVertical) {
_transform.translate(0, i2 * targetBox.width);
} else {
_transform.translate(i2 * targetBox.width, 0);
}
imagePathSet.paths.forEach((originalPath) => {
const path = originalPath.clone().applyTransform(_transform);
if (path.style.strokeWidth)
path.style.strokeWidth *= styleScale * _thickness;
if (path.style.strokeMiterlimit)
path.style.strokeMiterlimit *= styleScale;
if (path.style.strokeDashoffset)
path.style.strokeDashoffset *= styleScale;
if (path.style.strokeDasharray)
path.style.strokeDasharray = path.style.strokeDasharray.map((v) => v * styleScale);
if (path.style.fill && path.style.fill in _colormap) {
path.style.fill = _colormap[path.style.fill];
}
if (path.style.stroke && path.style.stroke in _colormap) {
path.style.stroke = _colormap[path.style.stroke];
}
pathSet.paths.push(path);
if (rawWidth !== userWidth) {
if (isVertical) {
clipRects[pathSet.paths.length - 1] = new BoundingBox(
groupBox.left - groupBox.width * 2,
groupBox.top,
groupBox.width * 4,
groupBox.height
);
} else {
clipRects[pathSet.paths.length - 1] = new BoundingBox(
groupBox.left,
groupBox.top - groupBox.height * 2,
groupBox.width,
groupBox.height * 4
);
}
}
});
}
}
},
renderOrder: -1,
getBoundingBox: () => {
const boundingBoxs = [];
pathSet.paths.forEach((path, index) => {
const clipRect = clipRects[index];
let box = path.getBoundingBox();
if (clipRect) {
const x = Math.max(box.x, clipRect.x);
const y = Math.max(box.y, clipRect.y);
const right = Math.min(box.right, clipRect.right);
const bottom = Math.min(box.bottom, clipRect.bottom);
box = new BoundingBox(x, y, right - x, bottom - y);
}
boundingBoxs.push(box);
});
return BoundingBox.from(...boundingBoxs);
},
render: (renderer) => {
const { text, context } = renderer;
pathSet.paths.forEach((path, index) => {
renderer.drawPath(path, { clipRect: clipRects[index] });
if (text.debug) {
const box = new Path2DSet([path]).getBoundingBox();
if (box) {
context.strokeRect(box.x, box.y, box.width, box.height);
}
}
});
}
});
}
function genDisc(r, color) {
return `<svg width="${r * 2}" height="${r * 2}" xmlns="http://www.w3.org/2000/svg">
<circle cx="${r}" cy="${r}" r="${r}" fill="${color}" />
</svg>`;
}
function listStylePlugin() {
const pathSet = new Path2DSet();
return definePlugin({
name: "listStyle",
pathSet,
update: (text) => {
pathSet.paths.length = 0;
const { paragraphs, isVertical, fontSize } = text;
const padding = fontSize * 0.45;
paragraphs.forEach((paragraph) => {
const {
computedStyle: style
} = paragraph;
const {
color,
listStyleImage,
listStyleColormap,
listStyleSize,
listStyleType
} = style;
const colormap = parseColormap(listStyleColormap);
let size = listStyleSize;
let image;
if (!isNone(listStyleImage)) {
image = listStyleImage;
} else if (!isNone(listStyleType)) {
const r = fontSize * 0.38 / 2;
size = size === "cover" ? r * 2 : size;
switch (listStyleType) {
case "disc":
image = genDisc(r, String(color));
break;
}
}
if (!image) {
return;
}
const imagePathSet = svgToPath2DSet(image);
const imageBox = imagePathSet.getBoundingBox();
const char = paragraph.fragments[0]?.characters[0];
if (!char) {
return;
}
const { inlineBox } = char;
const scale = size === "cover" ? 1 : parseValueNumber(size, { total: fontSize, fontSize }) / fontSize;
const m = new Matrix3();
if (isVertical) {
const _scale = fontSize / imageBox.height * scale;
m.translate(-imageBox.left, -imageBox.top).rotate(Math.PI / 2).scale(_scale, _scale).translate(
inlineBox.left + (inlineBox.width - imageBox.height * _scale) / 2,
inlineBox.top - padding
);
} else {
const _scale = fontSize / imageBox.height * scale;
m.translate(-imageBox.left, -imageBox.top).scale(_scale, _scale).translate(
inlineBox.left - imageBox.width * _scale - padding,
inlineBox.top + (inlineBox.height - imageBox.height * _scale) / 2
);
}
pathSet.paths.push(...imagePathSet.paths.map((p) => {
const path = p.clone();
path.applyTransform(m);
if (path.style.fill && path.style.fill in colormap) {
path.style.fill = colormap[path.style.fill];
}
if (path.style.stroke && path.style.stroke in colormap) {
path.style.stroke = colormap[path.style.stroke];
}
return path;
}));
});
}
});
}
function outlinePlugin() {
return {
name: "outline"
// TODO
};
}
const tempV1 = new Vector2();
const tempM1 = new Matrix3();
const tempM2 = new Matrix3();
function renderPlugin() {
const pathSet = new Path2DSet();
return definePlugin({
name: "render",
pathSet,
update: (text) => {
pathSet.paths.length = 0;
const { paragraphs } = text;
paragraphs.forEach((paragraph) => {
paragraph.fragments.forEach((fragment) => {
fragment.characters.forEach((character) => {
pathSet.paths.push(character.path);
});
});
});
},
getBoundingBox: (text) => {
const { characters, fontSize, computedEffects } = text;
const boxes = [];
characters.forEach((character) => {
computedEffects.forEach((style) => {
if (!character.glyphBox) {
return;
}
const aabb = character.glyphBox.clone();
const m = getTransform2D(text, style);
tempV1.set(aabb.left, aabb.top);
tempV1.applyMatrix3(m);
aabb.left = tempV1.x;
aabb.top = tempV1.y;
tempV1.set(aabb.right, aabb.bottom);
tempV1.applyMatrix3(m);
aabb.width = tempV1.x - aabb.left;
aabb.height = tempV1.y - aabb.top;
const shadowOffsetX = (style.shadowOffsetX ?? 0) * fontSize;
const shadowOffsetY = (style.shadowOffsetY ?? 0) * fontSize;
const textStrokeWidth = Math.max(0.1, style.textStrokeWidth ?? 0) * fontSize;
aabb.left += shadowOffsetX - textStrokeWidth;
aabb.top += shadowOffsetY - textStrokeWidth;
aabb.width += textStrokeWidth * 2;
aabb.height += textStrokeWidth * 2;
boxes.push(aabb);
});
});
return boxes.length ? BoundingBox.from(...boxes) : void 0;
},
render: (renderer) => {
const { text, context } = renderer;
const { paragraphs, glyphBox, computedEffects } = text;
if (paragraphs.length && computedEffects.length) {
computedEffects.forEach((style) => {
renderer.uploadColor(style, glyphBox);
context.save();
const [a, c, e, b, d, f] = getTransform2D(text, style).transpose().elements;
context.transform(a, b, c, d, e, f);
text.forEachCharacter((character) => {
renderer.drawCharacter(character, style);
});
context.restore();
});
} else {
paragraphs.forEach((paragraph) => {
paragraph.fragments.forEach((fragment) => {
fragment.characters.forEach((character) => {
renderer.drawCharacter(character);
});
});
});
}
if (text.debug) {
paragraphs.forEach((paragraph) => {
context.strokeRect(
paragraph.lineBox.x,
paragraph.lineBox.y,
paragraph.lineBox.width,
paragraph.lineBox.height
);
});
}
}
});
}
function getTransform2D(text, style) {
const { fontSize, glyphBox } = text;
const translateX = (style.translateX ?? 0) * fontSize;
const translateY = (style.translateY ?? 0) * fontSize;
const PI_2 = Math.PI * 2;
const skewX = (style.skewX ?? 0) / 360 * PI_2;
const skewY = (style.skewY ?? 0) / 360 * PI_2;
const { left, top, width, height } = glyphBox;
const centerX = left + width / 2;
const centerY = top + height / 2;
tempM1.identity();
tempM2.makeTranslation(translateX, translateY);
tempM1.multiply(tempM2);
tempM2.makeTranslation(centerX, centerY);
tempM1.multiply(tempM2);
tempM2.set(1, Math.tan(skewX), 0, Math.tan(skewY), 1, 0, 0, 0, 1);
tempM1.multiply(tempM2);
tempM2.makeTranslation(-centerX, -centerY);
tempM1.multiply(tempM2);
return tempM1.clone();
}
function textDecorationPlugin() {
const pathSet = new Path2DSet();
return definePlugin({
name: "textDecoration",
pathSet,
update: (text) => {
pathSet.paths.length = 0;
const groups = [];
let group;
let prevStyle;
text.forEachCharacter((character) => {
const {
computedStyle: style,
isVertical,
inlineBox,
underlinePosition,
underlineThickness,
strikeoutPosition,
strikeoutSize
} = character;
const { color, textDecoration, writingMode } = style;
if (!isNone(textDecoration)) {
let flag = false;
if (prevStyle?.textDecoration === textDecoration && prevStyle?.writingMode === writingMode && prevStyle?.color === color && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top)) {
switch (textDecoration) {
case "underline":
if (group[0].underlinePosition === underlinePosition && group[0].underlineThickness === underlineThickness) {
flag = true;
}
break;
case "line-through":
if (group[0].strikeoutPosition === strikeoutPosition && group[0].strikeoutSize === strikeoutSize) {
flag = true;
}
break;
}
}
if (flag) {
group.push(character);
} else {
group = [];
group.push(character);
groups.push(group);
}
prevStyle = style;
} else {
prevStyle = void 0;
}
});
groups.forEach((group2) => {
const {
computedStyle: style,
isVertical,
underlinePosition,
underlineThickness,
strikeoutPosition,
strikeoutSize
} = group2[0];
const {
color,
textDecoration
} = style;
const inlineBox = BoundingBox.from(...group2.map((c) => c.inlineBox));
const { left, top, width, height } = inlineBox;
let position = isVertical ? left + width : top;
const direction = isVertical ? -1 : 1;
let thickness = 0;
switch (textDecoration) {
case "overline":
thickness = underlineThickness * 2;
break;
case "underline":
position += direction * underlinePosition;
thickness = underlineThickness * 2;
break;
case "line-through":
position += direction * strikeoutPosition;
thickness = strikeoutSize * 2;
break;
}
position -= thickness;
let path;
if (isVertical) {
path = new Path2D([
{ type: "M", x: position, y: top },
{ type: "L", x: position, y: top + height },
{ type: "L", x: position + thickness, y: top + height },
{ type: "L", x: position + thickness, y: top },
{ type: "Z" }
], {
fill: color
});
} else {
path = new Path2D([
{ type: "M", x: left, y: position },
{ type: "L", x: left + width, y: position },
{ type: "L", x: left + width, y: position + thickness },
{ type: "L", x: left, y: position + thickness },
{ type: "Z" }
], {
fill: color
});
}
pathSet.paths.push(path);
});
},
render: (renderer) => {
const { text, context } = renderer;
const { computedEffects } = text;
if (computedEffects.length) {
computedEffects.forEach((effectStyle) => {
context.save();
const [a, c, e, b, d, f] = getTransform2D(text, effectStyle).transpose().elements;
context.transform(a, b, c, d, e, f);
pathSet.paths.forEach((path) => {
renderer.drawPath(path, effectStyle);
});
context.restore();
});
} else {
pathSet.paths.forEach((path) => {
renderer.drawPath(path);
});
}
}
});
}
var __defProp = Object.defineProperty;
var __decorateClass = (decorators, target, key, kind) => {
var result = void 0 ;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (decorator(target, key, result) ) || result;
if (result) __defProp(target, key, result);
return result;
};
const textDefaultStyle = getDefaultStyle();
class Text extends Reactivable {
needsUpdate = true;
computedStyle = { ...textDefaultStyle };
computedEffects = [];
paragraphs = [];
inlineBox = new BoundingBox();
lineBox = new BoundingBox();
rawGlyphBox = new BoundingBox();
glyphBox = new BoundingBox();
pathBox = new BoundingBox();
boundingBox = new BoundingBox();
measurer = new Measurer();
plugins = /* @__PURE__ */ new Map();
pathSets = [];
get fontSize() {
return this.computedStyle.fontSize;
}
get isVertical() {
return this.computedStyle.writingMode.includes("vertical");
}
get characters() {
return this.paragraphs.flatMap((p) => p.fragments.flatMap((f) => f.characters));
}
constructor(options = {}) {
super();
this.set(options);
}
set(options = {}) {
const {
content,
effects,
style,
measureDom,
fonts,
fill,
outline
} = normalizeText(options);
this.debug = options.debug ?? false;
this.content = content;
this.effects = effects;
this.style = style;
this.measureDom = measureDom;
this.fonts = fonts;
this.fill = fill;
this.outline = outline;
this.use(backgroundPlugin()).use(outlinePlugin()).use(listStylePlugin()).use(textDecorationPlugin()).use(highlightPlugin()).use(renderPlugin());
(options.plugins ?? []).forEach((plugin) => {
this.use(plugin);
});
this._update();
}
use(plugin) {
this.plugins.set(plugin.name, plugin);
return this;
}
forEachCharacter(handle) {
this.paragraphs.forEach((p, paragraphIndex) => {
p.fragments.forEach((f, fragmentIndex) => {
f.characters.forEach((c, characterIndex) => {
handle(c, { paragraphIndex, fragmentIndex, characterIndex });
});
});
});
return this;
}
async load() {
this._update();
await Promise.all(Array.from(this.plugins.values()).map((p) => p.load?.(this)));
}
_update() {
this.computedStyle = { ...textDefaultStyle, ...this.style };
this.computedEffects = this.effects?.map((v) => ({ ...v })) ?? [];
const paragraphs = [];
this.content.forEach((p, pIndex) => {
const { fragments, fill: pFill, outline: pOutline, ...pStyle } = p;
const paragraph = new Paragraph(pStyle, pIndex, this);
paragraph.fill = pFill;
paragraph.outline = pOutline;
fragments.forEach((f, fIndex) => {
const { content, fill: fFill, outline: fOutline, ...fStyle } = f;
if (content !== void 0) {
const fragment = new Fragment(content, fStyle, fIndex, paragraph);
paragraph.fragments.push(fragment);
fragment.fill = fFill;
fragment.outline = fOutline;
}
});
paragraphs.push(paragraph);
});
this.paragraphs = paragraphs;
return this;
}
createDom() {
this._update();
return this.measurer.createDom(this.paragraphs, this.computedStyle);
}
measure(dom = this.measureDom) {
const old = {
paragraphs: this.paragraphs,
inlineBox: this.inlineBox,
lineBox: this.lineBox,
rawGlyphBox: this.rawGlyphBox,
glyphBox: this.glyphBox,
pathBox: this.pathBox,
boundingBox: this.boundingBox
};
this._update();
const result = this.measurer.measure(this.paragraphs, this.computedStyle, dom);
this.paragraphs = result.paragraphs;
this.lineBox = result.boundingBox;
this.characters.forEach((c) => {
c.update(this.fonts);
});
this.rawGlyphBox = this.getGlyphBox();
Array.from(this.plugins.values()).sort((a, b) => (a.updateOrder ?? 0) - (b.updateOrder ?? 0)).forEach((plugin) => {
plugin.update?.(this);
});
this.pathSets.length = 0;
Array.from(this.plugins.values()).sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)).forEach((plugin) => {
if (plugin.pathSet?.paths.length) {
this.pathSets.push(plugin.pathSet);
}
});
this.glyphBox = this.getGlyphBox();
this._updateInlineBox()._updatePathBox()._updateBoundingBox();
for (const key in old) {
result[key] = this[key];
this[key] = old[key];
}
this.emit("measure", { text: this, result });
return result;
}
getGlyphBox() {
const min = Vector2.MAX;
const max = Vector2.MIN;
this.characters.forEach((c) => {
if (!c.getGlyphMinMax(min, max)) {
const { inlineBox: glyphBox } = c;
const { left, top, width, height } = glyphBox;
const a = new Vector2(left, top);
const b = new Vector2(left + width, top + height);
min.min(a, b);
max.max(a, b);
}
});
if (min.x === Number.MIN_SAFE_INTEGER || min.y === Number.MIN_SAFE_INTEGER || max.x === Number.MAX_SAFE_INTEGER || max.y === Number.MAX_SAFE_INTEGER) {
return new BoundingBox(0, 0, 0, 0);
}
return new BoundingBox(
min.x,
min.y,
max.x - min.x,
max.y - min.y
);
}
_updateInlineBox() {
this.inlineBox = BoundingBox.from(
...this.paragraphs.flatMap((p) => p.fragments.map((f) => f.inlineBox))
);
return this;
}
_updatePathBox() {
this.pathBox = BoundingBox.from(
this.glyphBox,
...Array.from(this.plugins.values()).map((plugin) => {
return plugin.getBoundingBox ? plugin.getBoundingBox(this) : plugin.pathSet?.getBoundingBox();
}).filter(Boolean)
);
return this;
}
_updateBoundingBox() {
this.boundingBox = BoundingBox.from(
this.rawGlyphBox,
this.lineBox,
this.pathBox
);
return this;
}
requestUpdate() {
this.needsUpdate = true;
return this;
}
update(dom = this.measureDom) {
this.needsUpdate = false;
const result = this.measure(dom);
for (const key in result) {
this[key] = result[key];
}
this.emit("update", { text: this });
return this;
}
render(options) {
const { view, pixelRatio = 2 } = options;
const ctx = view.getContext("2d");
if (!ctx) {
return;
}
if (this.needsUpdate) {
this.update();
}
const renderer = new Canvas2DRenderer(this, ctx);
renderer.pixelRatio = pixelRatio;
renderer.setup();
Array.from(this.plugins.values()).sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)).forEach((plugin) => {
if (plugin.render) {
plugin.render?.(renderer);
} else if (plugin.pathSet) {
plugin.pathSet.paths.forEach((path) => {
renderer.drawPath(path);
});
}
});
this.emit("render", { text: this, view, pixelRatio });
options.onContext?.(ctx);
}
toString() {
return this.content.flatMap((p) => p.fragments.map((f) => f.content)).join("");
}
}
__decorateClass([
property({ internal: true })
], Text.prototype, "debug");
__decorateClass([
property()
], Text.prototype, "content");
__decorateClass([
property()
], Text.prototype, "style");
__decorateClass([
property()
], Text.prototype, "effects");
__decorateClass([
property()
], Text.prototype, "fill");
__decorateClass([
property()
], Text.prototype, "outline");
__decorateClass([
property({ internal: true })
], Text.prototype, "measureDom");
__decorateClass([
property({ internal: true })
], Text.prototype, "fonts");
export { Canvas2DRenderer as C, Fragment as F, Measurer as M, Paragraph as P, Text as T, Character as a, backgroundPlugin as b, getTransform2D as c, definePlugin as d, textDefaultStyle as e, createSvgLoader as f, getHighlightStyle as g, highlightPlugin as h, createSvgParser as i, parseColormap as j, isEqualObject as k, listStylePlugin as l, isEqualValue as m, hexToRgb as n, outlinePlugin as o, parseValueNumber as p, filterEmpty as q, renderPlugin as r, textDecorationPlugin as t };