modern-text
Version:
Measure and render text in a way that describes the DOM.
1,828 lines • 58.2 kB
JavaScript
import { isNone, getDefaultStyle, EventEmitter, normalizeText, property } from 'modern-idoc';
import { Path2D, BoundingBox, setCanvasContext, svgToDom, svgToPath2DSet, Path2DSet, Vector2, Matrix3 } from 'modern-path2d';
import { fonts } from 'modern-font';
function parseColor(ctx, source, box) {
if (typeof source === "string" && source.startsWith("linear-gradient")) {
const { x0, y0, x1, y1, stops } = parseCssLinearGradient(source, box.left, box.top, box.width, box.height);
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
stops.forEach((stop) => gradient.addColorStop(stop.offset, stop.color));
return gradient;
}
return source;
}
function uploadColor(style, box, ctx) {
if (style?.color) {
style.color = parseColor(ctx, style.color, box);
}
if (style?.backgroundColor) {
style.backgroundColor = parseColor(ctx, style.backgroundColor, box);
}
if (style?.textStrokeColor) {
style.textStrokeColor = parseColor(ctx, style.textStrokeColor, box);
}
}
function parseCssLinearGradient(css, x, y, width, height) {
const str = css.match(/linear-gradient\((.+)\)$/)?.[1] ?? "";
const first = str.split(",")[0];
const cssDeg = first.includes("deg") ? first : "0deg";
const matched = str.replace(cssDeg, "").matchAll(/(#|rgba|rgb)(.+?) ([\d.]+%)/gi);
const deg = Number(cssDeg.replace("deg", "")) || 0;
const rad = deg * Math.PI / 180;
const offsetX = width * Math.sin(rad);
const offsetY = height * Math.cos(rad);
return {
x0: x + width / 2 - offsetX,
y0: y + height / 2 + offsetY,
x1: x + width / 2 + offsetX,
y1: y + height / 2 - offsetY,
stops: Array.from(matched).map((res) => {
let color = res[2];
if (color.startsWith("(")) {
color = color.split(",").length > 3 ? `rgba${color}` : `rgb${color}`;
} else {
color = `#${color}`;
}
return {
offset: Number(res[3].replace("%", "")) / 100,
color
};
})
};
}
function drawPath(options) {
const { ctx, path, fontSize, clipRect } = options;
ctx.save();
ctx.beginPath();
const pathStyle = path.style;
const style = {
...pathStyle,
fill: options.color ?? pathStyle.fill,
stroke: options.textStrokeColor ?? pathStyle.stroke,
strokeWidth: options.textStrokeWidth ? options.textStrokeWidth * fontSize : pathStyle.strokeWidth,
strokeLinecap: "round",
strokeLinejoin: "round",
shadowOffsetX: (options.shadowOffsetX ?? 0) * fontSize,
shadowOffsetY: (options.shadowOffsetY ?? 0) * fontSize,
shadowBlur: (options.shadowBlur ?? 0) * fontSize,
shadowColor: options.shadowColor
};
if (clipRect) {
ctx.rect(clipRect.left, clipRect.top, clipRect.width, clipRect.height);
ctx.clip();
ctx.beginPath();
}
path.drawTo(ctx, style);
ctx.restore();
}
function setupView(ctx, pixelRatio, boundingBox) {
const { left, top, width, height } = 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);
}
function uploadColors(ctx, text) {
const { paragraphs, computedStyle: style, glyphBox } = text;
uploadColor(style, glyphBox, ctx);
paragraphs.forEach((paragraph) => {
uploadColor(paragraph.computedStyle, paragraph.lineBox, ctx);
paragraph.fragments.forEach((fragment) => {
uploadColor(fragment.computedStyle, fragment.inlineBox, ctx);
});
});
}
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();
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, isVertical } = 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.inlineBox.width = isVertical ? advanceHeight : advanceWidth;
this.inlineBox.height = isVertical ? advanceWidth : 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();
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);
}
drawTo(ctx, config = {}) {
const style = this.computedStyle;
const options = {
ctx,
path: this.path,
fontSize: style.fontSize,
color: style.color,
...config
};
if (this.glyphBox) {
drawPath(options);
} else {
ctx.save();
ctx.beginPath();
const pathStyle = this.path.style;
const _style = {
...pathStyle,
fill: options.color ?? pathStyle.fill,
stroke: options.textStrokeColor ?? pathStyle.stroke,
strokeWidth: options.textStrokeWidth ? options.textStrokeWidth * options.fontSize : pathStyle.strokeWidth,
shadowOffsetX: (options.shadowOffsetX ?? 0) * options.fontSize,
shadowOffsetY: (options.shadowOffsetY ?? 0) * options.fontSize,
shadowBlur: (options.shadowBlur ?? 0) * options.fontSize,
shadowColor: options.shadowColor
};
setCanvasContext(ctx, _style);
ctx.font = `${options.fontSize}px ${options.fontFamily}`;
if (this.isVertical) {
ctx.textBaseline = "middle";
ctx.fillText(this.content, this.inlineBox.left, this.inlineBox.top + this.inlineBox.height / 2);
} else {
ctx.textBaseline = "alphabetic";
ctx.fillText(this.content, this.inlineBox.left, this.inlineBox.top + this.baseline);
}
ctx.restore();
}
}
}
function createSVGLoader() {
const loaded = /* @__PURE__ */ new Map();
async function load(svg) {
if (!loaded.has(svg)) {
loaded.set(svg, 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("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 dom = svgToDom(
loader.needsLoad(svg) ? loader.loaded.get(svg) ?? svg : svg
);
const pathSet = svgToPath2DSet(dom);
result = { dom, pathSet };
parsed.set(svg, result);
}
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 = {}, parent) {
this.content = content;
this.style = style;
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, parent) {
this.style = style;
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;
}
addFragment(content, style) {
const fragment = new Fragment(content, style, this);
this.fragments.push(fragment);
return fragment;
}
}
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.left,
top: pBox.top,
width: pBox.width,
height: pBox.height
});
pDOM.querySelectorAll(":scope > *").forEach((fDOM, fragmentIndex) => {
const fBox = fDOM.getBoundingClientRect();
fragments.push({
paragraphIndex,
fragmentIndex,
left: fBox.left,
top: fBox.top,
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 _p = paragraphs[p.paragraphIndex];
_p.lineBox.left = p.left - rect.left;
_p.lineBox.top = p.top - rect.top;
_p.lineBox.width = p.width;
_p.lineBox.height = p.height;
});
measured.fragments.forEach((f) => {
const _f = paragraphs[f.paragraphIndex].fragments[f.fragmentIndex];
_f.inlineBox.left = f.left - rect.left;
_f.inlineBox.top = f.top - rect.top;
_f.inlineBox.width = f.width;
_f.inlineBox.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 } = item;
const result = results[i];
item.inlineBox.left = result.left;
item.inlineBox.top = result.top;
item.inlineBox.width = result.width;
item.inlineBox.height = result.height;
if (isVertical) {
item.lineBox.left = result.left + (result.width - fontHeight) / 2;
item.lineBox.top = result.top;
item.lineBox.width = fontHeight;
item.lineBox.height = result.height;
} else {
item.lineBox.left = result.left;
item.lineBox.top = result.top + (result.height - fontHeight) / 2;
item.lineBox.width = result.width;
item.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,
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);
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: (ctx, text) => {
const { boundingBox, computedStyle: style } = text;
if (!isNone(style.backgroundColor)) {
ctx.fillStyle = style.backgroundColor;
ctx.fillRect(...boundingBox.array);
}
pathSet.paths.forEach((path) => {
drawPath({ ctx, path, fontSize: style.fontSize });
if (text.debug) {
const box = new Path2DSet([path]).getBoundingBox();
if (box) {
ctx.strokeRect(box.x, box.y, box.width, box.height);
}
}
});
text.paragraphs.forEach((p) => {
const { lineBox, style: style2 } = p;
if (!isNone(style2.backgroundColor)) {
ctx.fillStyle = style2.backgroundColor;
ctx.fillRect(...lineBox.array);
}
p.fragments.forEach((f) => {
const { inlineBox, style: style3 } = f;
if (!isNone(style3.backgroundColor)) {
ctx.fillStyle = style3.backgroundColor;
ctx.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,
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);
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: (ctx, text) => {
pathSet.paths.forEach((path, index) => {
drawPath({
ctx,
path,
fontSize: text.computedStyle.fontSize,
clipRect: clipRects[index]
});
if (text.debug) {
const box = new Path2DSet([path]).getBoundingBox();
if (box) {
ctx.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, effects } = text;
const boxes = [];
characters.forEach((character) => {
effects?.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: (ctx, text) => {
const { paragraphs, glyphBox, effects } = text;
if (effects) {
effects.forEach((style) => {
uploadColor(style, glyphBox, ctx);
ctx.save();
const [a, c, e, b, d, f] = getTransform2D(text, style).transpose().elements;
ctx.transform(a, b, c, d, e, f);
text.forEachCharacter((character) => {
character.drawTo(ctx, style);
});
ctx.restore();
});
} else {
paragraphs.forEach((paragraph) => {
paragraph.fragments.forEach((fragment) => {
fragment.characters.forEach((character) => {
character.drawTo(ctx);
});
});
});
}
if (text.debug) {
paragraphs.forEach((paragraph) => {
ctx.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: (ctx, text) => {
const { effects, computedStyle: style } = text;
if (effects) {
effects.forEach((effectStyle) => {
ctx.save();
const [a, c, e, b, d, f] = getTransform2D(text, effectStyle).transpose().elements;
ctx.transform(a, b, c, d, e, f);
pathSet.paths.forEach((path) => {
drawPath({
ctx,
path,
fontSize: style.fontSize,
...effectStyle
});
});
ctx.restore();
});
} else {
pathSet.paths.forEach((path) => {
drawPath({
ctx,
path,
fontSize: style.fontSize
});
});
}
}
});
}
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 EventEmitter {
needsUpdate = true;
computedStyle = { ...textDefaultStyle };
paragraphs = [];
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.updateParagraphs();
}
onUpdateProperty(key, newValue, oldValue, declaration) {
this.emit("updateProperty", key, newValue, oldValue, declaration);
}
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() {
await Promise.all(Array.from(this.plugins.values()).map((p) => p.load?.(this)));
}
updateParagraphs() {
this.computedStyle = { ...textDefaultStyle, ...this.style };
const { content } = this;
const paragraphs = [];
content.forEach((p) => {
const { fragments, fill: pFill, outline: pOutline, ...pStyle } = p;
const paragraph = new Paragraph(pStyle, this);
paragraph.fill = pFill;
paragraph.outline = pOutline;
fragments.forEach((f) => {
const { content: content2, fill: fFill, outline: fOutline, ...fStyle } = f;
if (content2 !== void 0) {
const fragment = paragraph.addFragment(content2, fStyle);
fragment.fill = fFill;
fragment.outline = fOutline;
}
});
paragraphs.push(paragraph);
});
this.paragraphs = paragraphs;
return this;
}
createDom() {
this.updateParagraphs();
return this.measurer.createDom(this.paragraphs, this.computedStyle);
}
measure(dom = this.measureDom) {
const old = {
paragraphs: this.paragraphs,
lineBox: this.lineBox,
rawGlyphBox: this.rawGlyphBox,
glyphBox: this.glyphBox,
pathBox: this.pathBox,
boundingBox: this.boundingBox
};
this.updateParagraphs();
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.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);
}
});
return new BoundingBox(
min.x,
min.y,
max.x - min.x,
max.y - min.y
);
}
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) {
const result = this.measure(dom);
for (const key in result) {
this[key] = result[key];
}
this.emit("update", { text: this });
this.needsUpdate = false;
return this;
}
render(options) {
const { view, pixelRatio = 2 } = options;
const ctx = view.getContext("2d");
if (!ctx) {
return;
}
if (this.needsUpdate) {
this.update();
}
setupView(ctx, pixelRatio, this.boundingBox);
uploadColors(ctx, this);
Array.from(this.plugins.values()).sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)).forEach((plugin) => {
if (plugin.render) {
plugin.render?.(ctx, this);
} else if (plugin.pathSet) {
const style = this.computedStyle;
plugin.pathSet.paths.forEach((path) => {
drawPath({
ctx,
path,
fontSize: style.fontSize
});
});
}
});
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()
], 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()
], Text.prototype, "measureDom");
__decorateClass([
property()
], Text.prototype, "fonts");
export { Character as C, Fragment as F, Measurer as M, Paragraph as P, Text as T, uploadColors as a, definePlugin as b, backgroundPlugin as c, drawPath as d, getTransform2D as e, textDefaultStyle as f, getHighlightStyle as g, highlightPlugin as h, createSVGLoader as i, createSVGParser as j, parseValueNumber as k, listStylePlugin as l, parseColormap as m, isEqualObject as n, outlinePlugin as o, parseColor as p, isEqualValue as q, renderPlugin as r, setupView as s, textDecorationPlugin as t, uploadColor as u, hexToRgb as v, filterEmpty as w };