Skip to content

fix(text): center 'middle' text on its cap height and measure capHeight without padding - #721

Merged
stormmuller merged 1 commit into
devfrom
claude/design-text-cap-height-centering
Oct 6, 2026
Merged

stormmuller merged 1 commit into
devfrom
claude/design-text-cap-height-centering

Conversation

@stormmuller

Copy link
Copy Markdown
Member

Summary

Implements design/text-cap-height-centering.md (Kind: Defect) and deletes the design doc.

verticalAlign: 'middle' centers the cap-height band. It used to center the string's own rendered glyphs (getInkBounds), so a label moved when its text changed: "LEVEL 1" and "QUIT" sat at different heights, and a lowercase label jumped when a "g" appeared. Now 'middle' centers the band from the first line's cap line to the last line's baseline: offset = -(capHeight * size + lastBaseline) / 2. This reads only font metrics and the line count. It's the same box as CSS text-box-trim with text-box-edge: cap alphabetic and Figma's vertical trim, and what TextMesh Pro calls Capline alignment. Forge's own 'capline' is a different thing: it's a top anchor, not centering. getInkBounds/InkBounds are deleted. createButton, createDropdown and createTooltip labels pick this up with no code change.

capHeight is measured without the distance-field padding. computeCapHeight in scripts/generate-font-atlas.mjs returned the reference capital's planeBounds.top. That value includes msdf-bmfont-xml's padding: distanceRange >> 1 px on each side. The generator now subtracts Math.floor(distanceRange / 2) / fontSize, which mirrors that integer padding. In the shipped default font (distanceRange 16, size 42), "H" spans 0.881 to -0.190 em, so capHeight drops from 0.881 to 0.690 em. As a result 'capline' now puts the top of a capital exactly on its anchor.

Changes from the design (the code moved on since it was written)

  • Only one shipped atlas. The doc lists documentation-site/static/fonts/default/ too. It no longer exists, because docs demos now import the package's fonts/default export (fix(text): load font atlases from explicit metrics and image URLs #713). Only assets/fonts/default/default.json changed.
  • The atlas JSON's capHeight was edited by hand, not regenerated. msdf-bmfont-xml and the source TTF aren't available in this environment. The value is a deterministic function of data already in the file (H's top, distanceRange, size), and H's bottom of exactly -8/42 confirms the padding. The PNG and glyph data would not change.
  • ascender/descender keep the padding, and the generator comment now says why. Outlines, glows and shadows draw into the padding, so padded bounds are the right outer bounds for 'top'/'bottom'. capHeight is a typographic metric. The doc only scoped 'top'/'bottom' out without saying why; this records the reason.
  • The Galactic Journey demo's textCenteredAt/titleBaseline/menu-button code lives outside this repo, so it isn't touched here. In this repo, the ui-main-menu docs demo's "CONTINUE - ACT II" tag label used 'capline' to fake centering. It now uses 'middle'.

Solution-reviewer verdict

REVISE, meaning the direction is right with small fixes. All of them are applied:

  1. Use the floored padding Math.floor(distanceRange / 2) to match msdf-bmfont-xml's distanceRange >> 1.
  2. Keep ascender/descender padded, and reword the generator comment so the difference is intentional.
  3. Editing the JSON by hand is fine; say so in the PR.
  4. Skipping a generator unit test is acceptable, since importing the .mjs runs its CLI.
  5. Check the other verticalAlign suites and rewrite the e2e scene's "centers this glyph's own ink" comment.
  6. Give the 'capline' shift its own wording in the changelog.

Decisions taken from the design's open questions

The design lists none. DL-1 (redefine 'middle' rather than add a new value) and DL-2 (fix the metric in the generator) are implemented as written.

Tests

  • shape-text.test.ts: tests for one line, three lines, an "A" vs. "x" pair sharing a baseline, and an empty string. The ink-centering tests are removed.
  • e2e text-effects-overlap: the synthetic atlas's capHeight is now the glyph's ink top, and glyphCenterWorldY is derived from it instead of hard-coded 0.

Related issue(s)

Design doc design/text-cap-height-centering.md (deleted by this PR).

Verification checklist

  • npm run check-types passes with 0 errors
  • npm test passes (1941 tests)
  • npm run lint passes with 0 errors
  • npm run cspell passes with 0 errors
  • npm run check-exports passes
  • Any new/changed public API is exported from the module's index.ts: no API surface changed
  • Documentation under /documentation-site/docs/docs is updated: text/rendering-text.md (what each vertical alignment anchors to) and text/loading-a-font-atlas.md (padded plane bounds vs. capHeight)
  • Demo updated and verified. Ran npm run build, then docs typecheck and build, then served the site and screenshotted text, ui-button, ui-main-menu, ui-toggle, ui-dropdown, ui-slider, ui-progress-bar, ui-anchors, ui-nested-resize, layout-groups, ui-world-space-canvas, ui-canvas-group and game-states. No page errors. Labels are centered, and 'capline' touches the top of the "A" in the text demo.
  • e2e text-effects-overlap.spec.ts: 7/7 pass locally (run against the container's Chromium build)

npm run prettier reports warnings only in files this PR doesn't touch; they're also there on dev.

Changelog

  • Two bullets under ## [Unreleased] → #### Changed: one for 'middle' semantics, one for the capHeight correction and the resulting 'capline' shift (~0.19 em × size).

🤖 Generated with Claude Code

https://claude.ai/code/session_01HszxXvkXYYmRTFwJkgyq7Y


Generated by Claude Code

…ht without padding

verticalAlign 'middle' now centers the band from the first line's cap
height to the last line's baseline, using only font metrics and the line
count, so a label no longer moves when its text changes. The font atlas
generator subtracts the distance field padding when it measures
capHeight, and the shipped default font's capHeight is corrected, so
'capline' puts the top of a capital on its anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HszxXvkXYYmRTFwJkgyq7Y
@stormmuller
stormmuller enabled auto-merge (squash) October 6, 2026 21:35
@stormmuller
stormmuller merged commit 0fecc1f into dev Oct 6, 2026
12 checks passed
@stormmuller
stormmuller deleted the claude/design-text-cap-height-centering branch October 6, 2026 21:37
@codecov

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants