Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/core/text-rendering/SdfFontHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,10 @@ const processFontData = (
// normalizeFontMetrics.
}

// Same derivation for x-height using glyph 'x' (id 120). Only consumed
// when `RendererMainSettings.textBaselineMode === 'x'`; otherwise the
// 0.5 × ascender fallback inside `normalizeFontMetrics` is sufficient.
// Same derivation for x-height using glyph 'x' (id 120). Consumed by
// both `textBaselineMode === 'x'` and `'optical'` (which uses the mean
// of cap-height and x-height); the 0.5 × ascender fallback inside
// `normalizeFontMetrics` covers fonts that ship without an 'x' glyph.
if (metrics.xHeight === undefined) {
const xGlyph = glyphMap.get(120); // 'x'
if (xGlyph !== undefined) {
Expand Down
55 changes: 31 additions & 24 deletions src/core/text-rendering/TextLayoutEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ export const normalizeFontMetrics = (
/**
* Engine-wide per-line baseline anchor. Configured once at renderer creation
* via {@link RendererMainSettings.textBaselineMode}, not exposed per node so
* a single app can't mix anchor models across its text. Defaults to `'cap'`
* — see {@link TextBaselineMode} for the rationale.
* a single app can't mix anchor models across its text. Defaults to
* `'optical'` — see {@link TextBaselineMode} for the rationale.
*/
let baselineMode: TextBaselineMode = 'cap';
let baselineMode: TextBaselineMode = 'optical';

/**
* Sets the engine-wide baseline anchor. Called by `Stage` during construction;
Expand Down Expand Up @@ -185,44 +185,51 @@ export const mapTextLayout = (

// line[4] stores the alphabetic baseline Y of each line in screen px.
//
// ── Per-line anchor: cap-height centering ─────────────────────────────
// ── Per-line anchor: optical centering (default) ─────────────────────
//
// baselineY(i) = lineHeight/2 + capHeight/2 + i × lineHeight
// anchor = (capHeight + xHeight) / 2
// baselineY = lineHeight/2 + anchor/2 + i × lineHeight
//
// The baseline sits below the line's geometric mid-line by exactly
// `capHeight / 2`, so the top of an uppercase letter lands the same
// distance *above* the mid-line — i.e. capital letters bracket the
// center symmetrically. Cap-height centering matches what designers
// expect for UI text (button labels, headings, badges): TXYZ and 1234
// sit centered; descenders like 'gjpq' hang slightly below, mirroring
// CSS button behavior in browsers.
// Pure cap-height centering reads slightly low for lowercase-heavy
// mixed-case strings like "Button" because the visual mass sits in
// the x-height band, below cap-center. Pure x-height centering goes
// the other way — caps appear high. Splitting the difference (mean of
// cap and x heights) lands at the optical center for the broadest mix
// of UI text. This is the same heuristic macOS/iOS controls use.
//
// Alternative anchors considered (kept here for the record):
//
// ── cap-height centering ────────────────────────────────────────────
// baselineY = lineHeight/2 + capHeight/2 + i × lineHeight
// Caps bracket the mid-line symmetrically. Right for all-caps or
// numeric content (timers, badges); reads low for "Button".
//
// ── x-height centering ──────────────────────────────────────────────
// baselineY(i) = lineHeight/2 + xHeight/2 + i × lineHeight
// Centers lowercase letters on the mid-line. Matches CSS inline
// `vertical-align: middle`. Reads well for running body text but
// capitals appear high in headings/labels — wrong default for TV UI.
// baselineY = lineHeight/2 + xHeight/2 + i × lineHeight
// Centers lowercase. Matches CSS inline `vertical-align: middle`.
// Reads well for body text; capitals appear high in headings.
//
// ── line-box centering (pre-cap-height behavior) ───────────────────
// const halfLeading = (lineHeightPx − bareLineHeight) / 2
// baselineY(i) = halfLeading + ascender + i × lineHeight
// Centers the abstract asc-to-desc-plus-leading rectangle. The visible
// ink lands noticeably high because asc/(asc−desc) is asymmetric for
// most Latin fonts (~4.2:1 for Ubuntu). Mathematically tidy, visually
// wrong.
// baselineY = halfLeading + ascender + i × lineHeight
// Centers the abstract asc-to-desc-plus-leading rectangle. Ink lands
// noticeably high because asc/(asc−desc) is asymmetric for most
// Latin fonts (~4.2:1 for Ubuntu). Mathematically tidy, visually wrong.
//
// The active anchor is configured at renderer creation via
// `RendererMainSettings.textBaselineMode`. Defaults to `'cap'`.
// `RendererMainSettings.textBaselineMode`. Defaults to `'optical'`.
let firstBaselineY: number;
if (baselineMode === 'x') {
if (baselineMode === 'cap') {
firstBaselineY = (lineHeightPx + metrics.capHeight) * 0.5;
} else if (baselineMode === 'x') {
firstBaselineY = (lineHeightPx + metrics.xHeight) * 0.5;
} else if (baselineMode === 'linebox') {
const halfLeading = (lineHeightPx - bareLineHeight) * 0.5;
firstBaselineY = halfLeading + metrics.ascender;
} else {
firstBaselineY = (lineHeightPx + metrics.capHeight) * 0.5;
// 'optical' — mean of cap-height and x-height
const opticalAnchor = (metrics.capHeight + metrics.xHeight) * 0.5;
firstBaselineY = (lineHeightPx + opticalAnchor) * 0.5;
}
for (let i = 0; i < effectiveLineAmount; i++) {
const line = lines[i] as TextLineStruct;
Expand Down
15 changes: 9 additions & 6 deletions src/core/text-rendering/TextRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,20 @@ export type TextRenderers = 'canvas' | 'sdf';
* cannot be overridden per node — see the engine-wide reasoning in
* `TextLayoutEngine.mapTextLayout`.
*
* - `'cap'` (default): capital letters centered. Best for UI text — button
* labels, headings, badges. Capitals and digits bracket the center
* symmetrically; descenders hang slightly below, matching CSS button
* behavior in browsers.
* - `'x'`: lowercase x-height centered. Better for running body text;
* - `'optical'` (default): the midpoint of cap-height and x-height is
* centered. Looks visually centered for mixed-case text — the
* sweet spot between `'cap'` (which can read low for lowercase-heavy
* strings) and `'x'` (which can read high for headings).
* - `'cap'`: capital letters centered. Use when content is mostly
* uppercase or numeric (badges, timers). Mixed-case strings like
* "Button" may read slightly low.
* - `'x'`: lowercase x-height centered. Best for running body text;
* capitals appear slightly high in headings.
* - `'linebox'`: legacy. Centers the abstract asc-to-desc-plus-leading
* rectangle. Mathematically tidy but visually unbalanced because most
* Latin fonts have asymmetric asc/desc ratios.
*/
export type TextBaselineMode = 'cap' | 'x' | 'linebox';
export type TextBaselineMode = 'optical' | 'cap' | 'x' | 'linebox';
/**
* Structure mapping font family names to a set of font faces.
*/
Expand Down
15 changes: 9 additions & 6 deletions src/main-api/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,17 +374,20 @@ export type RendererMainSettings = RendererRuntimeSettings & {
* mid-line. This is engine-wide and intentionally not exposed per node —
* mixing anchor models within one app produces visually inconsistent text.
*
* - `'cap'` (default): capital letters and digits sit centered on the line.
* Best fit for UI text (button labels, headings, badges); descenders on
* words like 'gjpq' hang slightly below center, matching CSS button
* behavior in browsers.
* - `'optical'` (default): the mean of cap-height and x-height is centered.
* Reads visually centered for mixed-case UI text like "Button"; the
* sweet spot between `'cap'` (low for lowercase-heavy) and `'x'` (high
* for headings). Matches macOS/iOS control behavior.
* - `'cap'`: capital letters and digits sit centered on the line. Use
* when content is mostly uppercase or numeric (timers, badges); mixed-
* case "Button" can read slightly low.
* - `'x'`: lowercase x-height is centered. Better for running body text;
* capitals appear slightly high in headings.
* - `'linebox'`: legacy mode. Centers the asc/lineGap/desc rectangle.
* Mathematically tidy but visually unbalanced because most Latin fonts
* have asymmetric asc/desc ratios.
*
* @defaultValue `'cap'`
* @defaultValue `'optical'`
*/
textBaselineMode: TextBaselineMode;

Expand Down Expand Up @@ -555,7 +558,7 @@ export class RendererMain extends EventEmitter {
renderEngine: settings.renderEngine,
quadBufferSize: settings.quadBufferSize ?? 4 * 1024 * 1024,
fontEngines: settings.fontEngines ?? [],
textBaselineMode: settings.textBaselineMode ?? 'cap',
textBaselineMode: settings.textBaselineMode ?? 'optical',
textureProcessingTimeLimit: settings.textureProcessingTimeLimit || 10,
canvas: settings.canvas,
createImageBitmapSupport: settings.createImageBitmapSupport || 'full',
Expand Down
Binary file modified visual-regression/certified-snapshots/chromium-ci/alignment-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified visual-regression/certified-snapshots/chromium-ci/clipping-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified visual-regression/certified-snapshots/chromium-ci/clipping-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified visual-regression/certified-snapshots/chromium-ci/clipping-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified visual-regression/certified-snapshots/chromium-ci/resize-mode-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified visual-regression/certified-snapshots/chromium-ci/resize-mode-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified visual-regression/certified-snapshots/chromium-ci/resize-mode-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified visual-regression/certified-snapshots/chromium-ci/scaling-1.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/scaling-2.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/scaling-3.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-align-1.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-align-2.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-align-3.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-align-5.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-align-6.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-alpha-1.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-alpha-2.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-jump-1.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-zwsp-1.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-zwsp-2.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/text-zwsp-3.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/texture-svg-1.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/textures-1.png
Binary file modified visual-regression/certified-snapshots/chromium-ci/zIndex-1.png
Loading