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 -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 {
html += esc(tok);
}
});
display.innerHTML = html;
} else {
// Sentence or paragraph — render as plain text (no ORP)
// 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;
}
}