Skip to content

renderText: GSUB shaping, emoji sequences, lazy fallback fonts, glyph cache - #76

Merged
matmen merged 1 commit into
masterfrom
emoji-sequences
Oct 16, 2026
Merged

matmen merged 1 commit into
masterfrom
emoji-sequences

Conversation

@matmen

@matmen matmen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

This is step 2 of color emoji: GSUB shaping. Emoji sequences now render as a single glyph, and text fonts get their default ligatures.

Shaping (src/render/gsub.ts)

  • Features: ccmp, rlig, liga, clig and calt, the ones browsers enable by default.
    • Records from all scripts and language systems apply, the same way the GPOS kerning already works.
  • Lookup types:
    • 1 single, 2 multiple (an empty sequence deletes the glyph), 4 ligature
    • 5 contextual and 6 chained contextual, each in formats 1–3, including nested lookups
    • 7 extension
  • Lookup flags: "ignore bases, ligatures or marks" and the mark attachment class, using the glyph classes from GDEF.
  • Speed: each lookup remembers, per glyph, whether any of its subtables can start there. This keeps text with many lookups (Source Sans) as fast as before.

Font choice per grapheme (src/render/text.ts)

  • Graphemes: a grapheme is a base character plus what extends it: combining marks, variation selectors, skin tones, the keycap, tags, ZWJ-joined characters, and pairs of regional indicators.
  • Choosing the font: each grapheme comes from the first font that has all of its visible characters. If none has all of them, the font is chosen per character, as in renderText: fallback fonts and color emoji (COLR v0, CBDT) #75.
    • With FE0F, a font with a color glyph wins, so ❤️ is the red emoji heart and not Source Sans' black ❤.
    • With FE0E, a font without a color glyph wins.
  • Runs: consecutive graphemes with the same font form a run, and the run goes through that font's GSUB. Invisible characters that no substitution consumed still take no room.

Loading and caching

  • Fallback fonts are only loaded once a character needs them.
  • Glyphs are kept per font, size and hinting across calls, with up to 16 sizes per font. This includes decoded and scaled emoji PNGs. A repeated call with Noto emoji took 6 instead of 49 ms; a line of Open Sans takes 0.3 instead of 1.1 ms.

Visible changes

  • Emoji sequences are now single glyphs: 👍🏽, 🇩🇪, 1️⃣, #️⃣, 👨‍👩‍👧, 👨‍🦰, 🏳️‍🌈 and 🏴󠁧󠁢󠁳󠁣󠁴󠁿.
  • Text fonts use ligatures: e.g. "office" now uses the ffi ligature, as in browsers. None of the existing targets contain ligatures, so they all still pass.
  • Combining accents are still not positioned (that needs GPOS mark positioning). This was already the case before.

Tests

  • These sequences must render as single glyphs: 👍🏽, 🇩🇪, 1️⃣, 👨‍👩‍👧.
  • ❤️ with [sans, emoji] must come out as the red emoji.
  • The emoji-cbdt and emoji-colr targets now include sequences.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ

- src/render/gsub.ts applies the default features (ccmp, rlig, liga,
  clig, calt): single, multiple, ligature, contextual and chained
  contextual substitutions (formats 1-3) and extension lookups, with
  lookup flags from GDEF and nested lookups. A memo per lookup skips
  glyphs no subtable starts at.
- Paragraphs are shaped: graphemes (extending characters, joined
  sequences, regional indicator pairs) go to the first font that has all
  of their visible characters, preferring color glyphs before FE0F and
  others before FE0E; each font's run is substituted. Invisible characters
  that weren't substituted show nothing.
- Fallback fonts load when the text first needs them; glyphs, including
  decoded and scaled emoji bitmaps, are kept per font, size and hinting
  across calls (a few sizes per font).
- Tests: skin tones, flags, keycaps and families are single glyphs, ❤️
  picks the emoji font's red heart; emoji targets regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
@matmen
matmen merged commit 2aafe8e into master Oct 16, 2026
13 checks passed
@matmen
matmen deleted the emoji-sequences branch October 16, 2026 20:13
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.

1 participant