Skip to content

Commit 1a8b2ae

Browse files
chengloumayrang
andcommitted
feat: add letterSpacing support
Adds numeric pixel letterSpacing support to prepare() and prepareWithSegments(), with rich-inline threading, browser-oracle coverage, refreshed accuracy snapshots, and refreshed benchmark snapshots. Closes #78 Refs #107 Co-authored-by: ImpulseB23 <impulse@frostcrypt.com> Co-authored-by: mayrang <pkss0626@naver.com>
1 parent f201433 commit 1a8b2ae

22 files changed

Lines changed: 2424 additions & 458 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
2626
# caches
2727
.eslintcache
2828
.cache
29+
.codex
2930
*.tsbuildinfo
3031

3132
# IntelliJ based IDEs

DEVELOPMENT.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ bun install
3434
- `bun run benchmark-check:safari`
3535
- `bun run pre-wrap-check` — compact browser oracle for `{ whiteSpace: 'pre-wrap' }`
3636
- `bun run keep-all-check` — compact browser oracle for `{ wordBreak: 'keep-all' }`, including mixed-script no-space canaries
37+
- `bun run letter-spacing-check` — compact batched browser oracle for `{ letterSpacing }`, using one posted-report probe per browser and covering narrow wraps, combining marks, bidi, CJK, emoji, digits, RTL punctuation, `pre-wrap`, and soft hyphens
38+
- `bun run letter-spacing-snapshot` — refresh `accuracy/letter-spacing.json` from the Chrome + Safari compact `{ letterSpacing }` oracle
3739
- `bun run probe-check` — smaller browser probe/diagnostic entrypoint
3840
- `bun run probe-check:safari`
3941
On a first-break mismatch, probe output now includes a small break trace.
@@ -78,6 +80,7 @@ Use these for the current checked-in picture:
7880
- [STATUS.md](STATUS.md) — short pointer doc for the main browser accuracy + benchmark snapshots
7981
- [status/dashboard.json](status/dashboard.json) — machine-readable main dashboard
8082
- [accuracy/chrome.json](accuracy/chrome.json), [accuracy/safari.json](accuracy/safari.json), [accuracy/firefox.json](accuracy/firefox.json) — raw browser accuracy rows
83+
- [accuracy/letter-spacing.json](accuracy/letter-spacing.json) — compact Chrome + Safari `{ letterSpacing }` oracle snapshot
8184
- [benchmarks/chrome.json](benchmarks/chrome.json), [benchmarks/safari.json](benchmarks/safari.json) — raw benchmark snapshots
8285
- [corpora/STATUS.md](corpora/STATUS.md) — short pointer doc for long-form corpora
8386
- [corpora/dashboard.json](corpora/dashboard.json) — machine-readable corpus dashboard

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' }
3737
const { height } = layout(prepared, textareaWidth, 20)
3838
```
3939

40-
If you want CSS-like `word-break: keep-all`, pass `{ wordBreak: 'keep-all' }` to `prepare()` too.
40+
Other `prepare()` options are `{ wordBreak: 'keep-all' }` for CSS-like `word-break: keep-all`, and `{ letterSpacing: n }` to match CSS `letter-spacing` (`n` is treated as a px value).
4141

4242
The returned height is the crucial last piece for unlocking web UIs:
4343
- proper virtualization/occlusion without guesstimates & caching
@@ -124,13 +124,13 @@ It is intentionally narrow:
124124

125125
Use-case 1 APIs:
126126
```ts
127-
prepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap', wordBreak?: 'normal' | 'keep-all' }): PreparedText // one-time text analysis + measurement pass, returns an opaque value to pass to `layout()`. Make sure `font` is synced with your css `font` declaration shorthand (e.g. size, weight, style, family) for the text you're measuring. `font` is the same format as what you'd use for `myCanvasContext.font = ...`, e.g. `16px Inter`.
127+
prepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap', wordBreak?: 'normal' | 'keep-all', letterSpacing?: number }): PreparedText // one-time text analysis + measurement pass, returns an opaque value to pass to `layout()`. Make sure `font` and `letterSpacing` are synced with your CSS for the text you're measuring. `font` is the same format as what you'd use for `myCanvasContext.font = ...`, e.g. `16px Inter`; `letterSpacing` is a CSS pixel value.
128128
layout(prepared: PreparedText, maxWidth: number, lineHeight: number): { height: number, lineCount: number } // calculates text height given a max width and lineHeight. Make sure `lineHeight` is synced with your css `line-height` declaration for the text you're measuring.
129129
```
130130

131131
Use-case 2 APIs:
132132
```ts
133-
prepareWithSegments(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap', wordBreak?: 'normal' | 'keep-all' }): PreparedTextWithSegments // same as `prepare()`, but returns a richer structure for manual line layout needs
133+
prepareWithSegments(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap', wordBreak?: 'normal' | 'keep-all', letterSpacing?: number }): PreparedTextWithSegments // same as `prepare()`, but returns a richer structure for manual line layout needs
134134
layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number): { height: number, lineCount: number, lines: LayoutLine[] } // high-level api for manual layout needs. Accepts a fixed max width for all lines. Similar to `layout()`'s return, but additionally returns the lines info
135135
walkLineRanges(prepared: PreparedTextWithSegments, maxWidth: number, onLine: (line: LayoutLineRange) => void): number // low-level api for manual layout needs. Accepts a fixed max width for all lines. Calls `onLine` once per line with its actual calculated line width and start/end cursors, without building line text strings. Very useful for certain cases where you wanna speculatively test a few width and height boundaries (e.g. binary search a nice width value by repeatedly calling walkLineRanges and checking the line count, and therefore height, is "nice" too). You can have text messages shrinkwrap and balanced text layout this way. After walkLineRanges calls, you'd call layoutWithLines once, with your satisfying max width, to get the actual lines info.
136136
measureLineStats(prepared: PreparedTextWithSegments, maxWidth: number): { lineCount: number, maxLineWidth: number } // returns only how many lines this width produces, and how wide the widest one is. Avoids line/string allocations.
@@ -169,6 +169,7 @@ measureRichInlineStats(prepared: PreparedRichInline, maxWidth: number): { lineCo
169169
type RichInlineItem = {
170170
text: string // raw author text, including leading/trailing collapsible spaces
171171
font: string // canvas font shorthand for this item
172+
letterSpacing?: number // extra horizontal spacing between graphemes, in CSS px
172173
break?: 'normal' | 'never' // `never` keeps the item atomic, like a chip
173174
extraWidth?: number // caller-owned horizontal chrome, e.g. padding + border width
174175
}
@@ -231,11 +232,12 @@ Pretext doesn't try to be a full font rendering engine (yet?). It currently targ
231232
- `word-break: normal` and `keep-all`
232233
- `overflow-wrap: break-word`. Very narrow widths can still break inside words, but only at grapheme boundaries.
233234
- `line-break: auto`
235+
- `letter-spacing` as a numeric pixel value passed to `prepare()` / `prepareWithSegments()`
234236
- Tabs follow the default browser-style `tab-size: 8`
235237
- `{ wordBreak: 'keep-all' }` is supported too. It behaves like you'd expect for CJK/Hangul text, while keeping the same `overflow-wrap: break-word` fallback for overlong runs.
236238
- `system-ui` is unsafe for `layout()` accuracy on macOS. Use a named font.
237239
- Runtime requires `Intl.Segmenter` and Canvas 2D text measurement. Browsers or runtimes without `Intl.Segmenter` are currently unsupported.
238-
- CSS text features outside the canvas `font` shorthand, such as `letter-spacing`, `font-optical-sizing`, `font-feature-settings`, and standalone `font-variation-settings`, are not modeled separately. Variable-font axes only help when the active axis is reflected in the canvas font string, for example via weight.
240+
- CSS text features outside the canvas `font` shorthand, such as `font-optical-sizing`, `font-feature-settings`, and standalone `font-variation-settings`, are not modeled separately. Variable-font axes only help when the active axis is reflected in the canvas font string, for example via weight.
239241
240242
## Develop
241243

STATUS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ Use [corpora/STATUS.md](corpora/STATUS.md) for the long-form corpus canaries.
1414
- [accuracy/chrome.json](accuracy/chrome.json)
1515
- [accuracy/safari.json](accuracy/safari.json)
1616
- [accuracy/firefox.json](accuracy/firefox.json)
17+
- [accuracy/letter-spacing.json](accuracy/letter-spacing.json)
1718

1819
Notes:
1920
- This is the checked-in `4 fonts x 8 sizes x 8 widths x 30 texts` browser sweep.
2021
- The public accuracy page is basically a regression gate now, not the main steering metric.
22+
- The letter-spacing snapshot is a compact Chrome + Safari oracle, not part of the full sweep matrix.
2123

2224
## Benchmark Snapshots
2325

accuracy/chrome.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
{
22
"status": "ready",
33
"environment": {
4-
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
4+
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
55
"devicePixelRatio": 2,
66
"viewport": {
7-
"innerWidth": 1512,
8-
"innerHeight": 764,
9-
"outerWidth": 1512,
10-
"outerHeight": 851,
7+
"innerWidth": 1440,
8+
"innerHeight": 2443,
9+
"outerWidth": 1440,
10+
"outerHeight": 2530,
1111
"visualViewportScale": 1
1212
},
1313
"screen": {
14-
"width": 1512,
15-
"height": 982,
16-
"availWidth": 1512,
17-
"availHeight": 851,
14+
"width": 1440,
15+
"height": 2560,
16+
"availWidth": 1440,
17+
"availHeight": 2530,
1818
"colorDepth": 30,
1919
"pixelDepth": 30
2020
}
@@ -76825,5 +76825,5 @@
7682576825
"diff": 0
7682676826
}
7682776827
],
76828-
"requestId": "1775742756023-a4eaqt"
76828+
"requestId": "1776835259537-jsfey2"
7682976829
}

accuracy/firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76825,5 +76825,5 @@
7682576825
"diff": 0
7682676826
}
7682776827
],
76828-
"requestId": "1775742800315-s23e3v"
76828+
"requestId": "1776835275074-973fmg"
7682976829
}

0 commit comments

Comments
 (0)