feat: ORP anchor on the middle word in Sentence mode (v1.1.3)

David proposed: "for sentence we can add in the red letter to keep
the eye in the middle of the sentence block."

Sentence mode now picks the middle word of each sentence (by word
index) and applies the same ORP-character formula used in Word mode.
The single red letter gives the eye a fixed anchor; the rest of
the sentence is absorbed peripherally instead of saccading.

This matters most at high WPM (post v1.1.1/v1.1.2 — Sentence mode
now goes up to 3000 WPM with a 400ms minimum-display floor). Without
an anchor, the eye scans from the left as if reading normally and
runs out of time. With the anchor, the eye fixates immediately and
the whole sentence registers in one peripheral-vision read.

Paragraph mode still renders plain text — a single ORP on a
multi-line block doesn't give a useful fixation point. Per-sentence
ORP within paragraphs could come in a future release if needed.
This commit is contained in:
2026-05-27 02:50:01 +01:00
parent a5819f0c72
commit ab8d007b72
2 changed files with 56 additions and 3 deletions
+28
View File
@@ -9,6 +9,34 @@ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — versi
---
## [1.1.3] — 2026-05-27
### Added — ORP anchor in Sentence mode
Sentence mode now displays one red letter near the middle of each sentence — same ORP (Optimal Recognition Point) treatment used in Word mode, scaled up to sentence size. The eye fixates on the red character and processes the rest of the sentence peripherally instead of saccading across the line.
The anchor is calculated by finding the **middle word of the sentence** (by word index, not character index — keeps the anchor word-aligned), then applying the standard ORP-character formula to that word:
| Word length | ORP position (0-indexed) |
|---|---|
| 1 character | char 0 |
| 2-4 characters | char 1 |
| 5-9 characters | char 2 |
| 10-13 characters | char 3 |
| 14+ characters | char 4 |
For a 9-word sentence, the middle word is index 4. If that word is "evaluation" (10 chars), the ORP character is `l` (`eva*l*uation`) — that's the red letter on screen.
### Why this helps high-WPM Sentence mode
At 3000 WPM with the v1.1.2 lower floors, sentences flash by in 400-800 ms. Without an eye anchor, the eye starts scanning from the left as if reading normally, and runs out of time before the sentence advances. With the red ORP letter near the middle, the eye fixates on the anchor immediately and absorbs the sentence in a single peripheral-vision read. This is what makes high-WPM RSVP comprehensible rather than just fast.
### Paragraph mode
Paragraph mode still renders plain text without an ORP anchor. A single anchor character on a multi-line block doesn't give the eye a useful fixation point (it still needs to saccade between lines). Per-sentence ORP within paragraphs could be added in a future version if the use case emerges.
---
## [1.1.2] — 2026-05-27
### Fixed — High WPM in Sentence / Paragraph modes was clamped by a too-conservative minimum-display floor
+28 -3
View File
@@ -250,7 +250,7 @@ textarea:focus {
</div>
<span class="meta" id="meta">Paste text below, drag a .txt file in, then press Play</span>
<span class="version">v1.1.2</span>
<span class="version">v1.1.3</span>
</div>
<div class="stage mode-word" id="stage">
@@ -419,14 +419,39 @@ textarea:focus {
function renderChunk(chunk) {
if (!chunk) { display.innerHTML = ''; return; }
if (mode === 'word') {
// Single word with ORP coloring
// Single word with ORP coloring on the focal character
const i = Math.min(orpIndex(chunk), chunk.length - 1);
const before = chunk.slice(0, i);
const orp = chunk[i] || '';
const after = chunk.slice(i + 1);
display.innerHTML = `${esc(before)}<span class="orp">${esc(orp)}</span>${esc(after)}`;
} else if (mode === 'sentence') {
// v1.1.3: anchor the eye to ONE red letter near the middle of the
// sentence. Find the middle word (by index), then apply the same
// ORP-character formula to that word. Other tokens are preserved
// verbatim including original spacing.
const tokens = chunk.split(/(\s+)/); // keep whitespace tokens for layout
const wordTokenIndices = tokens
.map((t, i) => (/^\s+$/.test(t) || t.length === 0) ? -1 : i)
.filter(i => i !== -1);
const anchorTokenIdx = wordTokenIndices.length
? wordTokenIndices[Math.floor(wordTokenIndices.length / 2)]
: -1;
let html = '';
tokens.forEach((tok, i) => {
if (i === anchorTokenIdx && tok.length > 0) {
const j = Math.min(orpIndex(tok), tok.length - 1);
html += `${esc(tok.slice(0, j))}<span class="orp">${esc(tok[j])}</span>${esc(tok.slice(j + 1))}`;
} else {
// Sentence or paragraph — render as plain text (no ORP)
html += esc(tok);
}
});
display.innerHTML = html;
} else {
// Paragraph mode — plain text. (ORP on a multi-line block doesn't
// give the eye a single useful anchor; the eye still saccades
// across lines. If demand emerges, per-sentence ORP within the
// paragraph could be added in a future version.)
display.textContent = chunk;
}
}