modern-text
Version:
Measure and render text in a way that describes the DOM.
360 lines (352 loc) • 13.9 kB
text/typescript
import { NormalizedStyle, NormalizedFill, NormalizedOutline, FullStyle, LinearGradientWithType, RadialGradientWithType, NormalizedEffect, NormalizedShadow, TextObject, ReactivableEvents, Reactivable, NormalizedText } from 'modern-idoc';
import { SFNT, Fonts } from 'modern-font';
import { BoundingBox, Path2D, Vector2, Vector2Like, Path2DStyle, Path2DSet } from 'modern-path2d';
declare class Fragment {
readonly content: string;
readonly style: NormalizedStyle;
readonly fill: NormalizedFill | undefined;
readonly outline: NormalizedOutline | undefined;
readonly index: number;
readonly parent: Paragraph;
inlineBox: BoundingBox;
characters: Character[];
computedStyle: FullStyle;
computedFill: NormalizedFill | undefined;
computedOutline: NormalizedOutline | undefined;
get computedContent(): string;
constructor(content: string, style: NormalizedStyle | undefined, fill: NormalizedFill | undefined, outline: NormalizedOutline | undefined, index: number, parent: Paragraph);
update(): this;
initCharacters(): this;
}
interface GlyphTemplate {
path: Path2D;
glyphBox?: BoundingBox;
}
interface FontMetrics {
sfnt: SFNT;
unitsPerEm: number;
advanceHeight: number;
baseline: number;
ascender: number;
descender: number;
underlinePosition: number;
underlineThickness: number;
strikeoutPosition: number;
strikeoutSize: number;
typoAscender: number;
typoDescender: number;
typoLineGap: number;
winAscent: number;
winDescent: number;
xHeight: number;
capHeight: number;
centerDiviation: number;
fontStyle?: 'bold' | 'italic';
}
declare class Character {
content: string;
index: number;
parent: Fragment;
protected _path?: Path2D<Character>;
protected _lazyPath?: {
tmpl: GlyphTemplate;
x: number;
y: number;
style: any;
};
get path(): Path2D<Character>;
set path(value: Path2D<Character>);
inlineBox: BoundingBox;
get lineBox(): BoundingBox;
glyphBox?: BoundingBox;
advanceWidth: number;
kerningBefore: number;
protected _glyphIndex?: number;
protected _metrics?: FontMetrics;
get advanceHeight(): number;
get baseline(): number;
get ascender(): number;
get descender(): number;
get underlinePosition(): number;
get underlineThickness(): number;
get strikeoutPosition(): number;
get strikeoutSize(): number;
get typoAscender(): number;
get typoDescender(): number;
get typoLineGap(): number;
get winAscent(): number;
get winDescent(): number;
get xHeight(): number;
get capHeight(): number;
get centerDiviation(): number;
get fontStyle(): 'bold' | 'italic' | undefined;
translateY(dy: number): void;
get compatibleGlyphBox(): BoundingBox;
get center(): Vector2;
get computedFill(): NormalizedFill | undefined;
get computedOutline(): NormalizedOutline | undefined;
get computedStyle(): FullStyle;
get isVertical(): boolean;
get fontSize(): number;
get fontHeight(): number;
constructor(content: string, index: number, parent: Fragment);
protected _getFontSFNT(fonts?: Fonts): SFNT | undefined;
updateGlyph(sfnt?: SFNT | undefined): this;
computeKerningBefore(prev?: Character): number;
/**
* Populate glyph metrics only (advance width/height, ascender/descender,
* baseline, …) without building the glyph `path` or touching boxes.
*
* The pure-JS `Measurer` must know advances *before* it can place characters,
* so it calls this ahead of layout. `update()` later recomputes the same metrics
* while building the path, so this is idempotent.
*/
measureGlyph(fonts?: Fonts): this;
update(fonts?: Fonts): this;
protected _updateFromCache(sfnt: SFNT, style: FullStyle): void;
protected _italic(path: Path2D, startPoint?: Vector2Like): void;
getGlyphMinMax(min?: Vector2, max?: Vector2, withStyle?: boolean): {
min: Vector2;
max: Vector2;
} | undefined;
getGlyphBoundingBox(withStyle?: boolean): BoundingBox | undefined;
}
interface DrawShapePathsOptions extends Partial<Path2DStyle> {
clipRect?: BoundingBox;
}
declare class Canvas2DRenderer {
text: Text;
context: CanvasRenderingContext2D;
pixelRatio: number;
region?: {
x: number;
y: number;
width: number;
height: number;
};
constructor(text: Text, context: CanvasRenderingContext2D);
protected _setupView: () => void;
protected _setupColors: () => void;
setup: () => this;
protected _parseColor: (source: string | CanvasGradient | CanvasPattern | LinearGradientWithType | RadialGradientWithType, box: BoundingBox) => string | CanvasGradient | CanvasPattern;
protected _uploadedStyles: string[];
uploadColor: (box: BoundingBox, ctx: {
style?: NormalizedStyle;
fill?: NormalizedFill;
outline?: NormalizedOutline;
effects?: NormalizedEffect[];
}) => void;
protected _mergePathStyle(path: Path2D, style: Partial<Path2DStyle>): Partial<Path2DStyle>;
drawPath: (path: Path2D, options?: DrawShapePathsOptions) => void;
effectToPathStyle(effect: NormalizedEffect): Partial<Path2DStyle>;
transformEffect(effect: NormalizedEffect): void;
protected _shadowCanvas?: HTMLCanvasElement;
protected _shadowCtx?: CanvasRenderingContext2D | null;
drawWithShadow: (shadow: NormalizedShadow, drawFn: () => void) => void;
drawCharacter: (character: Character, effect?: NormalizedEffect) => void;
}
/**
* A measurer's output: the paragraphs with their four-level boxes filled in place,
* plus the overall bounding box. (Formerly `MeasureDomResult`; layout is now
* DOM-free via {@link import('./Measurer').Measurer}.)
*/
interface MeasurerResult {
paragraphs: Paragraph[];
boundingBox: BoundingBox;
}
interface Plugin {
name: string;
pathSet?: Path2DSet;
getBoundingBox?: (text: Text) => BoundingBox | undefined;
update?: (text: Text) => void;
updateOrder?: number;
render?: (renderer: Canvas2DRenderer) => void;
renderOrder?: number;
load?: (text: Text) => Promise<void>;
context?: Record<string, any>;
}
/**
* Pluggable layout backend. Implementations fill the four-level boxes
* (`character.inlineBox`/`lineBox`, `fragment.inlineBox`, `paragraph.lineBox`)
* in place and return the overall bounding box.
*
* The built-in backend is the pure-JS, DOM-free {@link import('./Measurer').Measurer},
* which resolves glyph advances/kerning from `modern-font` — it runs in Node/SSR/Worker
* and measures the exact font that is rendered. `fonts` is passed positionally by
* `Text.measure()`.
*/
interface TextMeasurer {
measure: (paragraphs: Paragraph[], rootStyle: FullStyle, dom?: HTMLElement, fonts?: Fonts) => MeasurerResult;
dispose?: () => void;
}
interface Options extends TextObject {
debug?: boolean;
measureDom?: HTMLElement;
fonts?: Fonts;
plugins?: Plugin[];
}
interface RenderOptions {
view: HTMLCanvasElement;
pixelRatio?: number;
/** 只渲染 boundingBox 内的这一子块(相对偏移 + 尺寸),用于超大文字按 GPU 上限分块栅格。 */
region?: {
x: number;
y: number;
width: number;
height: number;
};
onContext?: (context: CanvasRenderingContext2D) => void;
}
interface MeasureResult {
paragraphs: Paragraph[];
lineBox: BoundingBox;
rawGlyphBox: BoundingBox;
glyphBox: BoundingBox;
pathBox: BoundingBox;
boundingBox: BoundingBox;
}
declare const textDefaultStyle: FullStyle;
interface TextEvents extends ReactivableEvents {
update: [ctx: {
text: Text;
}];
measure: [ctx: {
text: Text;
result: MeasureResult;
}];
render: [ctx: {
text: Text;
view: HTMLCanvasElement;
pixelRatio: number;
}];
}
interface Text {
on: <K extends keyof TextEvents & string>(event: K, listener: (...args: TextEvents[K]) => void) => this;
once: <K extends keyof TextEvents & string>(event: K, listener: (...args: TextEvents[K]) => void) => this;
off: <K extends keyof TextEvents & string>(event: K, listener: (...args: TextEvents[K]) => void) => this;
emit: <K extends keyof TextEvents & string>(event: K, ...args: TextEvents[K]) => this;
}
declare class Text extends Reactivable {
debug: boolean;
content: NormalizedText['content'];
style?: NormalizedText['style'];
effects?: NormalizedText['effects'];
fill?: NormalizedText['fill'];
outline?: NormalizedText['outline'];
deformation?: NormalizedText['deformation'];
measureDom?: HTMLElement;
fonts?: Fonts;
needsUpdate: boolean;
computedStyle: FullStyle;
computedFill: NormalizedFill | undefined;
computedOutline: NormalizedOutline | undefined;
computedEffects: NormalizedEffect[];
inlineBox: BoundingBox;
lineBox: BoundingBox;
rawGlyphBox: BoundingBox;
glyphBox: BoundingBox;
pathBox: BoundingBox;
boundingBox: BoundingBox;
measurer: TextMeasurer;
plugins: Map<string, Plugin>;
pathSets: Path2DSet[];
protected _paragraphs: Paragraph[];
protected _cachedCharacters?: Character[];
incrementalLayout: boolean;
protected _prevParagraphs: Paragraph[];
protected _prevContentKeys: string[];
protected _prevStyleKey: string;
protected _prevFonts: unknown;
protected _prevFontsSet: boolean;
protected _prevFontSig: string;
protected _pendingFontSig: string;
protected _pendingContentKeys: string[];
protected _pendingStyleKey: string;
protected _pluginsByUpdateOrder: Plugin[];
protected _pluginsByRenderOrder: Plugin[];
protected _renderer?: Canvas2DRenderer;
protected _rendererCtx?: CanvasRenderingContext2D;
get paragraphs(): Paragraph[];
set paragraphs(value: Paragraph[]);
get fontSize(): number;
get defaultFamily(): string;
get isVertical(): boolean;
get characters(): Character[];
constructor(options?: Options);
set(options?: Options): void;
use(plugin: Plugin): this;
protected _resortPlugins(): void;
forEachCharacter(handle: (character: Character, ctx: {
paragraphIndex: number;
fragmentIndex: number;
characterIndex: number;
}) => void): this;
load(): Promise<void>;
/**
* Eagerly decode the fonts this text uses, off the main thread — WOFF tables
* are decompressed via modern-font's async `createSFNTAsync` (fflate async).
* This warms the SFNT cache so the synchronous `measure()` / `render()` pass
* never stalls the main thread inflating WOFF tables on first glyph access.
*
* No-op for already-decoded fonts and for formats without async decoding.
*/
protected _decodeFonts(): Promise<number>;
/**
* Coerce numeric style fields to finite numbers.
*
* `normalizeText`/`normalizeStyle` only runs through the constructor, so a
* `style` provided via direct assignment or `setPropertyAccessor` reaches
* `computedStyle` un-normalized. An invalid numeric value (`''`, `NaN`,
* `'10px'`, `'50%'`) would then poison layout arithmetic — e.g.
* `textIndent ?? 0` keeps `''` (`??` does not catch empty strings), which
* corrupts glyph positions and the text fails to render. Fall every numeric
* field back to its default when it is not a finite number (parsing leading
* numerics like `parseFloat`, matching the normalized path).
*/
protected _normalizeComputedStyle(style: FullStyle): FullStyle;
protected _buildParagraph(p: NormalizedText['content'][number], pIndex: number): Paragraph;
/**
* 本文本「已用字体族 → 当前解析到的字体源」的签名。
* 遍历根/段落/片段声明的 fontFamily,各取 `fonts.get(family)?.src`(modern-font 里每个已加载字体/兜底
* 字体的唯一源标识)拼成签名。某族未加载时 `get` 返回兜底字体(其 src 如 modern-font:embedded-fallback),
* 加载完成后返回该字体(src=字体 URL)→ src 变化 → 签名变化,据此可靠感知「字体到位」并作废增量复用。
*/
protected _computeFontSig(): string;
protected _canReuseLayout(styleKey: string, fontSig: string): boolean;
protected _update(): this;
measure(dom?: HTMLElement | undefined): MeasureResult;
getGlyphBox(): BoundingBox;
protected _paragraphGlyphBox(paragraph: Paragraph): BoundingBox | undefined;
protected _computeGlyphBox(): BoundingBox;
protected _updateInlineBox(): this;
protected _updatePathBox(): this;
protected _updateBoundingBox(): this;
requestUpdate(): this;
update(dom?: HTMLElement | undefined): this;
protected _getRenderer(ctx: CanvasRenderingContext2D): Canvas2DRenderer;
render(options: RenderOptions): void;
dispose(): void;
toString(): string;
}
declare class Paragraph {
readonly style: NormalizedStyle;
readonly index: number;
readonly parent: Text;
lineBox: BoundingBox;
fragments: Fragment[];
fill?: NormalizedFill;
outline?: NormalizedOutline;
_layoutDirty: boolean;
_layoutTop: number;
_layoutHeight: number;
_layoutRight: number;
_glyphBox?: BoundingBox;
_layoutValid: boolean;
computedStyle: FullStyle;
computedFill: NormalizedFill | undefined;
computedOutline: NormalizedOutline | undefined;
constructor(style: NormalizedStyle, index: number, parent: Text);
update(): this;
}
export { Character as C, Fragment as F, Paragraph as a, Text as c, Canvas2DRenderer as d, textDefaultStyle as t };
export type { DrawShapePathsOptions as D, MeasurerResult as M, Options as O, Plugin as P, RenderOptions as R, TextMeasurer as T, MeasureResult as b, TextEvents as e };