UNPKG

modern-text

Version:

Measure and render text in a way that describes the DOM.

360 lines (352 loc) 13.9 kB
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 };