mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
## What
Exposes the `pretext` text-measurement API on `window.__hyperframes`, so the API our agent-facing docs already describe actually exists.
Adds `pretext.prepare`, `.layout`, `.prepareWithSegments`, `.measureLineStats`, `.measureNaturalWidth`.
## Why
`skills/hyperframes-core/references/determinism-rules.md` is required reading for any agent authoring a composition. Line 59 tells them to call `window.__hyperframes.pretext.prepare(text, font)` then `pretext.layout(prepared, maxWidth, lineHeight)` for text measurement without a DOM reflow.
That object did not exist. The runtime exposed exactly `fitTextFontSize` and `getVariables`. Any composition following the documented recipe threw at runtime.
Deleting the doc line was the smaller change, but reflow-free measurement is genuinely the right tool for sizing text per frame, and `fitTextFontSize` is already built on it. Making the docs true is the better fix.
## How
- New `packages/core/src/text/pretext.ts` assembles the exposed surface in one place, with the include/exclude rationale next to it.
- `entry.ts` attaches it alongside the existing helpers. Sub-compositions inherit it for free: the scoping shim builds its scoped variant with `Object.assign({}, base, { getVariables })`, so anything added to the base object is carried through.
Two deliberate decisions:
**Wider than the doc named.** `layout()` returns only `{ lineCount, height }`. The doc's own "shrinkwrap containers" use case needs a width, which is impossible with just `prepare` + `layout`. `measureNaturalWidth` and `measureLineStats` make that claim achievable; `prepareWithSegments` is their required input.
**`clearCache` and `setLocale` withheld.** Both mutate state shared across compositions. Exposing them would let one composition change how a later one measures, making a render depend on what ran before it.
**Doc correction.** The reference called this "pure arithmetic, ~0.0002 ms per call". Not quite: `prepare` measures fonts through a canvas and throws outside a browser. Only the steps after a prepared string are arithmetic. Reworded, and documented the width helpers and the omissions.
## Trade-off
The runtime bundle grows **4,903 bytes (+1.30%)**, from 377,865 to 382,768. That ships inline in every composition. Measured by building the artifact with and without the change.
## Test plan
- [x] Unit tests added/updated
- [x] Manual testing performed
- [x] Documentation updated (if applicable)
`packages/core/src/text/pretext.test.ts` guards the shape of the published surface: the two documented names exist, the width helpers exist, and the two stateful functions are absent. Behaviour is deliberately not asserted there. `prepare` needs a canvas, and mocking it (as `fitTextFontSize.test.ts` must) would assert nothing real.
Real behaviour was verified by rendering a composition that calls the documented API:
```
lines=2 height=216 naturalWidth=1785
```
Self-consistent: natural width 1785 exceeds the 1600 container so it wraps to 2 lines, and 2 x 108 line-height is exactly the reported 216. The frame was inspected visually.
Also run:
- `packages/core` full suite from the package root: **903 passed, 46 files**
- `tsc --noEmit` and `tsc --noEmit -p tsconfig.runtime.json`: clean
- `oxlint` / `oxfmt`: clean
## Follow-ups (not in this PR)
An audit of the wider attribute surface found several more doc/runtime mismatches, including `data-gpu-mode` documented as an HTML attribute when it is a config field, and `data-no-timeline` being real, load-bearing, and absent from the table agents read. Those are separate changes.
52 lines
2.3 KiB
TypeScript
52 lines
2.3 KiB
TypeScript
import {
|
|
layout,
|
|
measureLineStats,
|
|
measureNaturalWidth,
|
|
prepare,
|
|
prepareWithSegments,
|
|
} from "@chenglou/pretext";
|
|
|
|
/**
|
|
* The text measurement surface exposed to compositions as
|
|
* `window.__hyperframes.pretext`.
|
|
*
|
|
* Measuring text by writing it into the DOM and reading it back forces a
|
|
* reflow, which is both slow per frame and a determinism risk (the value
|
|
* depends on when you read it). These measure off a canvas instead, so a
|
|
* composition can size a container, pick a font size, or decide a line break
|
|
* at any frame without disturbing layout. `fitTextFontSize` is itself built on
|
|
* `prepare` + `layout`.
|
|
*
|
|
* Note the split, because it decides where you call these: `prepare` (and
|
|
* `prepareWithSegments`) does the real font measurement and needs a canvas, so
|
|
* it only works in a browser, not in Node. Everything downstream of a prepared
|
|
* string is arithmetic and is cheap enough to run per frame.
|
|
*
|
|
* Deliberately omitted from this surface:
|
|
* - `clearCache` and `setLocale` mutate process-global state. A composition
|
|
* calling either would change how *other* compositions measure, which breaks
|
|
* the guarantee that the same file renders the same video every time.
|
|
* Withholding `clearCache` does not strand memory: the cache is keyed by
|
|
* (segment, font) where segments come from `Intl.Segmenter` at word
|
|
* granularity, so it grows with the number of distinct *words* a composition
|
|
* renders, not with frames. A counter or typewriter re-measuring every frame
|
|
* reuses the same entries. Only genuinely new words allocate, which bounds it
|
|
* at a composition's vocabulary.
|
|
* - The incremental cursor API (`layoutNextLine`, `walkLineRanges`, and
|
|
* friends) has no caller yet. Add it when something needs it.
|
|
*
|
|
* @see packages/core/src/runtime/entry.ts for where this is attached.
|
|
*/
|
|
export const pretext = {
|
|
/** Measure a string for a CSS font (e.g. `"700 48px Inter"`). */
|
|
prepare,
|
|
/** Line count and total height for a prepared string at a given width. */
|
|
layout,
|
|
/** Like `prepare`, but retains segments so widths can be measured. */
|
|
prepareWithSegments,
|
|
/** Line count plus `maxLineWidth` — the widest rendered line. */
|
|
measureLineStats,
|
|
/** Width the string would occupy on a single unwrapped line. */
|
|
measureNaturalWidth,
|
|
} as const;
|