# Reader

`@glyphcss/fluid/reader` is a speed reader in two layers:

- **Rules**: tokenizer, optimal recognition point (ORP), word delay and a word clock. Pure functions with no fluid or DOM dependency.
- **Rendering**: `createGlyphFluidReader` draws the clock's words with a fluid of its own.

## Read a sentence

```ts
import { createGlyphFluidReader } from "@glyphcss/fluid/reader";

const reader = createGlyphFluidReader(host, {
  text: "Words appear one at a time, out of the fluid.",
  wpm: 250,
  shape: "circle",   // a circular surface
  loop: 2600,        // ms of rest after the last word, then read again
});

reader.clock.setWpm(400);            // takes effect from the next word
reader.clock.pause();
reader.clock.play();
reader.clock.seek(3);                // show word 3 now
reader.clock.setText("Something new to say.");
reader.destroy();
```

## Looks

Both looks pivot the ORP letter on the centre column in the accent orange. Every word change sends a soft ring out from the centre; after the last word the letters melt back into the field.

- **`plain`** (default): the fluid pools into a band under the word, and real letters condense out of it. A letter appears once enough ink has arrived under it, as a scramble of ramp glyphs (`.:-=+*#%@`), and decodes into the real letter as the ink fills in; it rides the wave height and brightens with the ink. The fluid stays blank under shown letters. Letters are bold monospace (`font`), sized once per sentence so they don't jump between words.
- **`glyph`**: the fluid itself takes the letter shapes, in a heavy face stretched ×1.9 in height (cells are taller than wide). Each word is sized to fill the screen on its own, so short words are large.

The plain letters are real DOM text: one positioned `<span>` per letter in an overlay above the fluid's `<pre>`, each updated with at most one inline-style assignment per frame (position, colour, opacity) and its text written only when the shown character changes. Whether a letter shows, and the ink and wave under it, come from the same simulation step as the fluid frame, on the worker or the main thread.

During the lead-in the first word is already the target, so its ink gathers (and its plain letters scramble in) before the clock shows it.

Reader options are the fluid's options (except `glue`) plus:

| Option | Default | |
|---|---|---|
| `text` | `""` | the sentence; `clock.setText` replaces it |
| `wpm` | `250` | words per minute |
| `leadIn` | `550` | ms before the first word, while the ink gathers |
| `loop` | `false` | ms of rest after the last word before reading again |
| `autoplay` | `true` | start once the fluid is ready |
| `look` | `"plain"` | `"plain"`: real letters condense out of a pooled band; `"glyph"`: the fluid takes the letter shapes |
| `font` | `"ui-monospace, Menlo, Consolas, monospace"` | plain letters' font-family, drawn bold |
| `ariaLive` | `"off"` | the hidden word element's `aria-live`; `"polite"` announces every word |
| `color` | `"#f2f5ff"` | letter colour; the ORP accent is the fluid's accent orange |
| `ripple` | `0.63` | strength of the ring each later word sends out; 0 for none (the first word condensing and the last melting still stir the fluid, like any target) |
| `scheduler` | timers | the clock's `now` / `setTimeout` / `clearTimeout` |
| `grid` | 52 columns | the default grid width |

The fluid's `<pre>` and the letter overlay are `aria-hidden`; the showing word is kept as text in a visually hidden element, with `aria-live="off"` by default (a word every quarter second would flood a screen reader), so it is readable on demand. For a sentence read aloud, keep the whole text in the page. Under reduced motion each word is drawn as one settled still, and plain letters show settled: no scramble, no wave.

## The rules

```ts
import { tokenizeGlyphReaderText, glyphReaderOrp, glyphReaderWordDelay } from "@glyphcss/fluid/reader";

const tokens = tokenizeGlyphReaderText("Stop.\n\nGo on, slowly.");
// [{ word: "Stop.", orp: 1, core: 4, para: true }, { word: "Go", ... }, ...]
glyphReaderWordDelay(tokens[0], 250);   // 600 ms
```

- **ORP**: strip leading and trailing non-alphanumerics to get the core; the focal letter is the core start plus a bucket of the core length: ≤ 1 → 0, 2–5 → 1, 6–9 → 2, 10–13 → 3, ≥ 14 → 4.
- **Delay**: `max(40, round(60000 / wpm × long × punctuation))` ms, where `long = min(2, 1 + 0.05 × (core − 6))` for cores longer than 6, and punctuation is 2.5 at a paragraph end, 2.0 after `.` `!` `?`, 1.5 after `,` `;` `:` (closing quotes and brackets are skipped).
- **Tokens**: words are runs of non-whitespace; a blank line marks the word before it as a paragraph end. Indices count UTF-16 code units, exactly as JavaScript string indices do.

The rules are held to a reference implementation on a corpus of punctuation, Unicode, emoji and paragraph cases, with planted-bug tests that prove the comparison catches a one-character slip.

## The word clock

```ts
import { createGlyphReaderClock } from "@glyphcss/fluid/reader";

const clock = createGlyphReaderClock({ text, wpm: 300, loop: 2000 });
const off = clock.subscribe((e) => {
  if (e.type === "text") prepare(e.tokens);          // a reading (re)starts: lead-in
  if (e.type === "word") show(e.token, e.index, e.ms);
  if (e.type === "end") clear();
});
```

| Member | |
|---|---|
| `tokens`, `index`, `playing`, `wpm` | state; `index` is −1 during the lead-in, `tokens.length` once ended |
| `play()`, `pause()` | pause keeps the remaining time of the current word |
| `seek(i)` | show word `i` now (clamped); while playing it stays for its full delay |
| `setWpm(wpm)` | from the next word |
| `setText(text)` | new tokens, back to the lead-in; playing state is kept |
| `subscribe(fn)` | `text`, `word` and `end` events; returns an unsubscribe |
| `destroy()` | cancels the pending step |

The clock knows nothing about the fluid: drive a terminal, a canvas or another renderer with it. If a second consumer appears, the rules layer moves to its own package unchanged.
