Skip to content

The Reading Anchor

samaBR edited this page Aug 3, 2026 · 1 revision

The Reading Anchor

The problem

Ordinary teleprompters store the scroll position in pixels. That works right up until the text changes.

Insert a paragraph three pages above the reading point and every line below it moves down. Pixel 4,200 used to be "nobody has to stop the recording"; now it is a sentence from two paragraphs earlier. The presenter sees the text jump.

The same happens when you raise the type size, change the margin, or switch to a wider typeface. Nothing about the content changed — only the layout — but the position was expressed in the layout's units, so the position broke.

That is why those apps make you leave the presentation screen to edit. It is not a missing feature; it is the storage model.

The fix

Valendo stores the position as a semantic anchor:

{ blockId: 'b7', wordOffset: 34 }

Block b7, thirty-fourth word. Nothing about pixels, nothing about lines.

After any reflow the pixel is recomputed from the anchor. Insert paragraphs above it and b7 is still b7. Raise the type size and word 34 is still word 34 — it just sits lower on a taller line. The presenter sees nothing move.

What follows from it

Three design consequences, none of them optional once the anchor exists:

A clock, not messages

The main process keeps only { ppm, wordsAtStart, startedAt } — the pace, where the reading started, and when. Each window derives its own position inside a requestAnimationFrame.

Nothing is sent per frame. There is no stream of scroll positions from the operator to the presenter, so there is nothing to fall behind, and the two screens cannot drift apart: they are solving the same equation from the same three numbers.

One component draws both screens

PrompterCanvas draws the broadcast and the operator's preview. The preview runs at the output's real viewport — 1920×1080, or whatever the monitor is — and only receives a scale() to fit the panel.

The replica is exact by construction rather than by calibration. If the preview and the output ever disagreed, it would be a bug in one component, not a mismatch between two.

Lines are composed by words, not by the browser

Lines are built by a word rule: a minimum and a maximum number of words per line, and never break in a place that leaves a preposition dangling at the end.

Letting the browser wrap the text would put the layout back in charge of where words land — and then changing the typeface would change which word is word 34 of the visible line. Composing by word count means changing the type size changes the height of a line and nothing else.

What this buys the operator

  • Rewrite a sentence with the show on the air.
  • Raise the type size because the presenter squinted, mid-sentence.
  • Insert a whole block that the producer just handed you, above where the reading is.
  • Change the margin, the alignment, the number of words per line.

None of these move the word being read.

Where it lives in the code

File What it does
src/shared/anchor.ts The anchor itself, and composing lines from blocks
src/shared/pacing.ts Words to seconds, the clock
src/renderer/src/prompter/PrompterCanvas.tsx The single component that draws both screens
src/main/state.ts The authoritative state, including the three clock numbers

The logic in src/shared is pure and covered by tests — it is where the anchor's promises are actually enforced.

Clone this wiki locally