2 Commits

Author SHA1 Message Date
ranger ab8d007b72 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.
2026-05-27 02:50:01 +01:00
ranger a5819f0c72 fix: high-WPM chunk modes were clamped by 1500ms minimum-display floor (v1.1.2)
After v1.1.1 raised the WPM cap to 3000 for Sentence and Paragraph modes,
David noticed pushing the slider from 1500 to 3000 didn't actually speed
up reading. The chunks still sat on screen for the same amount of time.

Root cause: the v1.1.0 minimum-display floor was 1500ms per chunk. At
3000 WPM, any sentence under ~60 words mathematically wants to display
for under 1500ms — but the floor clamped them all to 1500ms, so the
calculated speed-up never reached the user.

Lowered floors:
- Sentence mode: 1500ms → 400ms
- Paragraph mode: 1500ms → 800ms

Max cap stays at 12s for very long paragraphs at very low WPM.

At 3000 WPM with the new floors, a 20-word sentence now displays for
500ms (was 1500ms, 3x faster) and the slider behaves the way users
expect — higher WPM = faster reading, all the way to 3000.

The reader does NOT iterate word-by-word inside a chunk; the whole
sentence/paragraph is rendered in one DOM update. Word count is used
only to estimate the auto-advance interval, not as a per-word loop.
2026-05-27 02:43:36 +01:00
2 changed files with 94 additions and 7 deletions
+52
View File
@@ -9,6 +9,58 @@ 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
After v1.1.1 raised the WPM cap to 3000 in Sentence and Paragraph modes, David noticed that pushing the slider from 1500 to 3000 didn't actually speed up reading — the chunks were still sitting on screen for the same amount of time.
Root cause: the v1.1.0 minimum-display floor was **1500 ms per chunk** (set conservatively to prevent flash-frame illegibility). At 3000 WPM, any sentence under ~60 words mathematically wants to display for under 1500 ms — but the floor clamped them all to 1500 ms, so the calculated speed-up never reached the user.
Lowered floors:
- **Sentence mode**: 1500 ms → **400 ms** (eye can absorb a short sentence in well under half a second once it's on screen)
- **Paragraph mode**: 1500 ms → **800 ms** (paragraphs are denser; need slightly more time even at high WPM)
Maximum cap stays at 12 s for very long paragraphs at very low WPM.
At 3000 WPM with the new floors, a 20-word sentence displays for **500 ms** (was 1500 ms — 3× faster) and a 60-word paragraph displays for **1750 ms** (was 1500 ms — uncapped formula now visible).
This makes the WPM slider actually behave the way users expect: higher WPM = faster reading, all the way up to 3000.
### Clarification
The reader does NOT iterate word-by-word inside a chunk in Sentence or Paragraph modes. The whole sentence or paragraph is rendered in one DOM update; only the auto-advance interval is calculated from word count. The word count is used as a proxy for "how long does a reader need to absorb this chunk," not as a per-word display loop.
---
## [1.1.1] — 2026-05-27 ## [1.1.1] — 2026-05-27
### Changed — Mode-aware WPM ceiling ### Changed — Mode-aware WPM ceiling
+42 -7
View File
@@ -250,7 +250,7 @@ textarea:focus {
</div> </div>
<span class="meta" id="meta">Paste text below, drag a .txt file in, then press Play</span> <span class="meta" id="meta">Paste text below, drag a .txt file in, then press Play</span>
<span class="version">v1.1.1</span> <span class="version">v1.1.3</span>
</div> </div>
<div class="stage mode-word" id="stage"> <div class="stage mode-word" id="stage">
@@ -419,14 +419,39 @@ textarea:focus {
function renderChunk(chunk) { function renderChunk(chunk) {
if (!chunk) { display.innerHTML = ''; return; } if (!chunk) { display.innerHTML = ''; return; }
if (mode === 'word') { 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 i = Math.min(orpIndex(chunk), chunk.length - 1);
const before = chunk.slice(0, i); const before = chunk.slice(0, i);
const orp = chunk[i] || ''; const orp = chunk[i] || '';
const after = chunk.slice(i + 1); const after = chunk.slice(i + 1);
display.innerHTML = `${esc(before)}<span class="orp">${esc(orp)}</span>${esc(after)}`; 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 { } 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; display.textContent = chunk;
} }
} }
@@ -448,12 +473,22 @@ textarea:focus {
const base = 60000 / wpm; const base = 60000 / wpm;
return base * pauseMult(chunk); return base * pauseMult(chunk);
} }
// Sentence / paragraph: time = words / WPM, with a comprehension multiplier // Sentence / paragraph: in these modes the whole chunk is displayed
// in one DOM update — no per-word iteration. We only need to decide
// how long to leave the chunk visible before advancing.
//
// Time = (words in chunk) / WPM × 60 s × comprehension multiplier.
// At low WPM the formula dominates. At high WPM the floor kicks in.
// v1.1.2: lowered floors significantly — the previous 1.5 s sentence
// floor was clamping any value above ~1500 WPM, making 3000 WPM
// feel identical to 1500 WPM. Lower floors let high-WPM skim mode
// actually skim. The maximum cap stays at 12 s for very long
// paragraphs at very low WPM.
const wordCount = splitWords(chunk).length; const wordCount = splitWords(chunk).length;
const baseMs = (wordCount / wpm) * 60000; const baseMs = (wordCount / wpm) * 60000;
const multiplier = mode === 'sentence' ? 1.25 : 1.40; // brain needs more time on chunks const multiplier = mode === 'sentence' ? 1.25 : 1.40;
// Sensible bounds: 1.5s min, 12s max const floor = mode === 'sentence' ? 400 : 800;
return Math.max(1500, Math.min(12000, baseMs * multiplier)); return Math.max(floor, Math.min(12000, baseMs * multiplier));
} }
function tick() { function tick() {