Skip to content

Add pair-kerning scaffolding and sidebearing scale#99

Open
terryspitz wants to merge 8 commits into
mainfrom
claude/kerning-implementation-plan-MIBJb
Open

Add pair-kerning scaffolding and sidebearing scale#99
terryspitz wants to merge 8 commits into
mainfrom
claude/kerning-implementation-plan-MIBJb

Conversation

@terryspitz
Copy link
Copy Markdown
Owner

@terryspitz terryspitz commented Apr 18, 2026

Summary

Stacked on top of #98 (Proofs tab). Adds a minimal kerning pipeline so the
proof snapshots show the kerning diff as a reviewable visual change, both on
the SVG-rendered tabs and on the proofs tab (which uses a CSS web font).

Changes

  • src/generator/Spacing.fs (new) — curated override map for notorious
    pairs (AV, AT, To, Ty, Va, Vo, Wa, Ya, LT, fi, fl, rn, …) plus
    open-pairs for slab sequences (din, Num, NN, MM, UU) and targeted
    fixes for l/j (more room left of l, less right of l, tighter
    left of j against non-descenders). pairKernInt looks up the table;
    pairKern returns it as a float in glyph units.
  • src/generator/Axes.fs — one new field:
    • sidebearingScale — multiplier on the existing thickness + serif
      sidebearing padding; lets the whole font be tightened or opened
      uniformly without retuning tracking. Default 1.0 keeps output
      bit-identical.
  • src/generator/Font.fs
    • width multiplies the sidebearing term by sidebearingScale.
    • New pairKern / pairKerns for per-pair and per-string kerns.
    • stringWidth now includes kerns so measured width matches rendered
      advance.
    • stringToSvgLineInternal applies the kern to the second glyph of
      each pair. Every glyph except the first is kerned against its
      predecessor; the first glyph has no predecessor so it lands at
      offsetX unshifted.
  • src/explorer/Api.fsgenerateFontGlyphData now also returns
    kerningPairs: { left, right, value }[] so the web font can carry
    the kerning.
  • web/src/fontExport.js — feeds those pairs into opentype.js as
    font.kerningPairs, keyed by glyph index. opentype.js emits a kern
    table that modern browsers honour for CSS-rendered text, so the
    proofs tab (which uses @font-face) picks up the kerning.
  • src/generator/tests/FontTest.fs — conservation-law tests:
    stringWidth = Σ advances + Σ kerns; unknown pairs return 0;
    italic axis does not change pair kerns (overrides live in the
    pre-shear frame); sidebearingScale scales the per-char sidebearing
    linearly.

Test plan

  • dotnet test src/generator/tests/ passes (new conservation tests
    green).
  • proofs-uppercase and proofs-lowercase Playwright snapshots
    diff cleanly — diff is tightened / opened spacing on seeded pairs
    (AV, To, rn, fi, din, *l, *j, …), no glyph corruption or
    overlap.
  • Re-baseline the two proof snapshots on this branch after visual
    review.
  • italic=0 and italic=0.3 on the Proofs tab render balanced
    spacing on AVATAR, To, fi, rn.

https://claude.ai/code/session_01NSaWc8pxLiqZYTpVodbciX

@terryspitz terryspitz force-pushed the claude/kerning-implementation-plan-MIBJb branch 5 times, most recently from fb721f6 to c4e2806 Compare April 19, 2026 15:38
@terryspitz terryspitz changed the base branch from claude/proofs-tab to main April 19, 2026 15:39
@terryspitz terryspitz force-pushed the claude/kerning-implementation-plan-MIBJb branch 3 times, most recently from 99f4946 to d1ffa43 Compare April 26, 2026 08:12
@terryspitz terryspitz force-pushed the claude/kerning-implementation-plan-MIBJb branch from d1ffa43 to 060779f Compare May 2, 2026 15:24
claude and others added 8 commits May 2, 2026 23:56
Introduces a minimal kerning pipeline on top of the Proofs tab baselines:

- New `Spacing` module with a curated override map for notorious pairs
  (AV, AT, To, Ty, Va, Vo, Wa, Ya, LT, fi, fl, rn, cl, ...). `pairKernInt`
  looks up the table; `pairKern` returns it as a float in glyph units.
- Three new Axes fields: `opticalKerning` (wire for the follow-up bezier
  profile sampler; no-op today), `kerningTarget` (target minimum gap for
  that sampler), `sidebearingScale` (multiplier on the existing thickness
  + serif sidebearing padding so the whole font can be tightened or
  opened uniformly without retuning `tracking`).
- `Font.width` multiplies the sidebearing term by `sidebearingScale`.
  Default value 1.0 keeps the rendered output bit-identical at defaults.
- `Font.pairKern` and `Font.pairKerns` expose per-pair and per-string
  kerns. `Font.stringWidth` now includes kerns so string measurements
  match the rendered advance. `stringToSvgLineInternal` applies the kern
  to the *second* glyph of each pair, leaving the first and last glyph
  in a line unkerned.
- FontTest.fs adds conservation-law tests: stringWidth = Σ advances +
  Σ kerns; unknown pairs return 0; italic axis does not change pair
  kerns (overrides live in the pre-shear frame); sidebearingScale
  scales the per-char sidebearing term linearly.

Follow-up will add `GlyphProfile` with cubic-root bezier sampling and
wire `opticalKerning` through `pairKern` for automatic gap-balancing.

https://claude.ai/code/session_01NSaWc8pxLiqZYTpVodbciX
Visual review of the pre-kern proof baselines showed the original
seeded values were too timid to be visible. Two changes:

1. Tighten diagonal/open-counter pairs aggressively (~1.5×):
   - A/V/W/Y/T/F/L/P/R/K with round and straight followers
   - f/r with tall/vertical followers
   - v/w/y/c/f with round followers
   Values like AV -50 → -75, To -40 → -55, rn -10 → -18.

2. Add positive overrides to open flat-sided slab pairs where two
   vertical strokes end up too close: "din", "Num", "HINDU", "NN",
   "mm", etc. Covers I/N/M/U/D/H/E/B uppercase and i/n/m/u/d/h/l/b
   lowercase. Typical +15-20 (upper) / +6-10 (lower) units.

Plus fix the `Kerning_NoKernPairs_StringWidthUnchanged` test string
to avoid pairs now present in the override map (MN was unexpectedly
matched). Switched to `CGJOQSXZ` — none of these are present as the
left or right side of any override.

https://claude.ai/code/session_01NSaWc8pxLiqZYTpVodbciX
Per proof review: left of 'l' was crowded from almost every letter — add
positive overrides for (*, l) at 8–14 units. Right of 'l' was the opposite
— flip (l, *) to negative to tighten. Left of 'j' got too wide against
non-descenders — add (non-descender, j) at -12 to -15. Bump din, dh, di,
db to 12–14 to stop the slab-sequence feeling cramped.
Proofs tab renders via a CSS @font-face served by opentype.js, which
did not previously include a kern table — so pair-kerning was invisible
there. Expose Spacing.kerningOverrides from generateFontGlyphData as a
{left, right, value}[] array and feed it into opentype.js's
font.kerningPairs keyed by glyph index; opentype.js emits a `kern` table
the browser honours for CSS-rendered text.

Remove opticalKerning and kerningTarget axes — both were stubs with no
implementation (they were scaffolding for a planned bezier-profile
sampler that didn't land). Keeping them on the Axes surface was
misleading in the UI. sidebearingScale stays since it is wired up.
The Tweens tab renders a single glyph per tween cell. sidebearingScale
changes per-glyph advance width but nothing else on a single glyph — it
only becomes visible when glyphs are laid out in sequence (like tracking
and leading, already excluded). Drop the orphan baseline PNG the auto-
commit workflow seeded before this exclusion.
The current default (1.0) leaves glyphs cramped on the proofs tab. Open
each glyph's symmetric padding by 20% as a global tuning step before the
manual kern overrides go in.
@terryspitz terryspitz force-pushed the claude/kerning-implementation-plan-MIBJb branch from 060779f to e971f16 Compare May 2, 2026 23:57
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