diff --git a/src/common/CommonTypes.ts b/src/common/CommonTypes.ts index b77f998..71a9867 100644 --- a/src/common/CommonTypes.ts +++ b/src/common/CommonTypes.ts @@ -23,6 +23,11 @@ export interface Dimensions { export type NodeTextLoadedPayload = { type: 'text'; dimensions: Dimensions; + /** + * Visible glyph extent — from the first line's cap-top to the last + * line's descender bottom. See `CoreTextNode.trimmedHeight`. + */ + trimmedHeight: number; }; /** diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index 0fe5b2d..eb347d4 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -65,6 +65,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { private _renderInfo: TextRenderInfo = { width: 0, height: 0, + trimmedHeight: 0, }; private _type: 'sdf' | 'canvas' = 'sdf'; // Default to SDF renderer @@ -289,6 +290,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { w: this._renderInfo.width, h: this._renderInfo.height, }, + trimmedHeight: this._renderInfo.trimmedHeight, } satisfies NodeTextLoadedPayload); }; @@ -593,4 +595,24 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { get renderInfo(): TextRenderInfo { return this._renderInfo; } + + /** + * Visible glyph extent — from the first line's cap-top to the last + * line's descender bottom. Excludes half-leading and the slack above + * the cap-top inside the font's ascender. + * + * @remarks + * Useful with flex `alignItems: 'center'` (or any layout that aligns + * by node `h`) to optically center the glyphs rather than the + * lineHeightPx line-box. Listen for `loaded` and apply with: + * + * ```ts + * text.once('loaded', (n, p) => { text.h = p.trimmedHeight; }); + * ``` + * + * Zero before the text loads or for empty strings. + */ + get trimmedHeight(): number { + return this._renderInfo.trimmedHeight; + } } diff --git a/src/core/text-rendering/CanvasTextRenderer.ts b/src/core/text-rendering/CanvasTextRenderer.ts index ec1c4ca..40bbf70 100644 --- a/src/core/text-rendering/CanvasTextRenderer.ts +++ b/src/core/text-rendering/CanvasTextRenderer.ts @@ -87,6 +87,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { return { width: 0, height: 0, + trimmedHeight: 0, }; } @@ -191,10 +192,18 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { if (canvas.width > 0 && canvas.height > 0) { imageData = context.getImageData(0, 0, canvasW, canvasH); } + // Cap-top of first line to descender bottom of last line. + // descender is negative in NormalizedFontMetrics. + const trimmedHeight = + lineAmount > 0 + ? metrics.capHeight - metrics.descender + (lineAmount - 1) * lineHeightPx + : 0; + return { imageData, width: effectiveWidth, height: effectiveHeight, + trimmedHeight, remainingLines, hasRemainingText, }; diff --git a/src/core/text-rendering/SdfTextRenderer.ts b/src/core/text-rendering/SdfTextRenderer.ts index bcfd4cc..2638666 100644 --- a/src/core/text-rendering/SdfTextRenderer.ts +++ b/src/core/text-rendering/SdfTextRenderer.ts @@ -52,6 +52,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { return { width: 0, height: 0, + trimmedHeight: 0, }; } @@ -63,6 +64,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { hasRemainingText: false, width: layout.width, height: layout.height, + trimmedHeight: layout.trimmedHeight, layout, }; } @@ -74,6 +76,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { return { width: 0, height: 0, + trimmedHeight: 0, }; } @@ -87,6 +90,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { hasRemainingText: false, width: layout.width, height: layout.height, + trimmedHeight: layout.trimmedHeight, layout, }; }; @@ -345,12 +349,21 @@ const generateTextLayout = ( } } + // Cap-top of first line to descender bottom of last line. + // descender is negative in NormalizedFontMetrics, so subtracting it + // adds the descender depth. Zero when there are no rendered lines. + const trimmedHeight = + lineAmount > 0 + ? metrics.capHeight - metrics.descender + (lineAmount - 1) * lineHeightPx + : 0; + // Convert final dimensions to pixel space for the layout return { glyphs, distanceRange: fontScale * fontData.distanceField.distanceRange, width: effectiveWidth * fontScale, height: effectiveHeight, + trimmedHeight, fontScale: fontScale, lineHeight: lineHeightPx, fontFamily, diff --git a/src/core/text-rendering/TextRenderer.ts b/src/core/text-rendering/TextRenderer.ts index 677ad94..bb591d2 100644 --- a/src/core/text-rendering/TextRenderer.ts +++ b/src/core/text-rendering/TextRenderer.ts @@ -317,6 +317,11 @@ export interface TextLayout { * Total text height */ height: number; + /** + * Trimmed text height — cap-top of the first line to descender bottom + * of the last line. See `TextRenderInfo.trimmedHeight`. + */ + trimmedHeight: number; /** * Font scale factor */ @@ -413,6 +418,21 @@ export interface TextRenderProps { export interface TextRenderInfo { width: number; height: number; + /** + * Height of the visible glyph extent — from the first line's cap-top to + * the last line's descender bottom. Excludes half-leading and the slack + * between the font's ascender and cap-top. + * + * @remarks + * Formula: `capHeight − descender + (lines − 1) × lineHeightPx` + * (descender is negative in font metrics, so subtracting it adds the + * descender depth). For empty text, this is 0. + * + * Use this when you want flex `alignItems: 'center'` (or any layout + * that aligns by node `h`) to optically center the visible glyphs. + * Set `node.h = node.trimmedHeight` after the `loaded` event. + */ + trimmedHeight: number; hasRemainingText?: boolean; remainingLines?: number; imageData?: ImageData | null; // Image data for Canvas Text Renderer