Skip to content
glyphcss

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.
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();

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:

OptionDefault
text""the sentence; clock.setText replaces it
wpm250words per minute
leadIn550ms before the first word, while the ink gathers
loopfalsems of rest after the last word before reading again
autoplaytruestart 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
ripple0.63strength 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)
schedulertimersthe clock’s now / setTimeout / clearTimeout
grid52 columnsthe 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.

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.

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, wpmstate; 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.