From a824c72593b9606b892ed5dacf21d20e6d3c5887 Mon Sep 17 00:00:00 2001 From: Astronidsp Date: Fri, 16 Jan 2026 16:59:47 +0530 Subject: [PATCH 1/3] added more things --- app.js | 230 +++++++++++++++++++++++++++++++++++++++++------------ index.html | 90 ++++++++++++++------- styles.css | 136 +++++++++++++++++++++++-------- 3 files changed, 345 insertions(+), 111 deletions(-) diff --git a/app.js b/app.js index a42a1c8..19c3037 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,5 @@ + + const textInput = document.getElementById("textInput"); const wordEl = document.getElementById("word"); const focusBox = document.getElementById("focus-box"); @@ -7,23 +9,46 @@ const playPauseBtn = document.getElementById("playPause"); const stopBtn = document.getElementById("stop"); const pivotToggle = document.getElementById("pivotToggle"); +const chunkToggle = document.getElementById("chunkToggle"); +const biasLabel = document.getElementById("biasLabel"); +const biasButtons = document.querySelectorAll(".pivot-bias button"); + const wpmSlider = document.getElementById("wpm"); const wpmVal = document.getElementById("wpmVal"); -let words = []; -let positions = []; + +let units = []; let index = 0; let running = false; let timer = null; + +let pivotBias = 0; // -1, 0, +1 +let chunkEnabled = true; + + wpmVal.textContent = wpmSlider.value; -wpmSlider.oninput = () => wpmVal.textContent = wpmSlider.value; +wpmSlider.oninput = () => (wpmVal.textContent = wpmSlider.value); document.addEventListener("keydown", e => { - if (e.code !== "Space") return; - if (document.activeElement === textInput && !textInput.readOnly) return; - e.preventDefault(); - togglePlayPause(); + // Space = play / pause + if (e.code === "Space") { + if (document.activeElement === textInput && !textInput.readOnly) return; + e.preventDefault(); + togglePlayPause(); + } + + + if (e.code === "ArrowLeft") { + e.preventDefault(); + if (running) togglePlayPause(); + step(-1); + } + if (e.code === "ArrowRight") { + e.preventDefault(); + if (running) togglePlayPause(); + step(1); + } }); playPauseBtn.onclick = togglePlayPause; @@ -40,61 +65,143 @@ pivotToggle.onchange = () => { pivotLine.classList.toggle("hidden", !pivotToggle.checked); }; + +chunkToggle.onchange = () => { + chunkEnabled = chunkToggle.checked; + index = 0; + parseText(); + if (!running) draw(); +}; + + +biasButtons.forEach(btn => { + btn.onclick = () => { + pivotBias += Number(btn.dataset.bias); + pivotBias = Math.max(-1, Math.min(1, pivotBias)); + + biasLabel.textContent = + pivotBias === 0 ? "Default" : pivotBias < 0 ? "Earlier" : "Later"; + + if (!running) draw(); + }; +}); + + +const SEED_CHUNKS = new Set([ + "in the", "of the", "to the", "for the", "on the", + "as well", "there is", "there are", + "for example", "such as", "because of", "in order", + "according to", "at least", "going to", "at one", "one of", + "part of", "due to", "in fact", "in case", "by the", + "in which", "so that", "as soon", "be able", + "the end", "the same", "the way", "some of", + "a lot", "lots of", "kind of", "sort of", "a little", + "as if", "as to", "as for", "as to", "in time", + "in place", "on time", "on place", "by far", + "by then", "up to", "out of", "due to", "ahead of", "close to", "was a", "is a", + "it is", "it was", "that is", "that was", "there was", "he was", "she was" +]); + +function isChunkCandidate(a, b) { + if (!chunkEnabled) return false; + if (/[.!?,;:]$/.test(a)) return false; + if (a.length > 8 || b.length > 8) return false; + return SEED_CHUNKS.has((a + " " + b).toLowerCase()); +} + + function parseText() { - words = []; - positions = []; + units = []; const text = textInput.value; const regex = /\S+/g; + const tokens = []; let m; while ((m = regex.exec(text)) !== null) { - words.push(m[0]); - positions.push([m.index, m.index + m[0].length]); + tokens.push({ + word: m[0], + start: m.index, + end: m.index + m[0].length + }); + } + + let i = 0; + while (i < tokens.length) { + if ( + i + 1 < tokens.length && + isChunkCandidate(tokens[i].word, tokens[i + 1].word) + ) { + units.push(makeUnit(tokens.slice(i, i + 2))); + i += 2; + } else { + units.push(makeUnit([tokens[i]])); + i += 1; + } } } -function pivotIndex(word) { +function makeUnit(tokens) { + const text = tokens.map(t => t.word).join(" "); + const span = [tokens[0].start, tokens[tokens.length - 1].end]; + + let pivotWordIndex = 0; + let maxLen = 0; + + tokens.forEach((t, i) => { + const clean = t.word.replace(/\W/g, ""); + if (clean.length > maxLen) { + maxLen = clean.length; + pivotWordIndex = i; + } + }); + + return { text, span, pivotWordIndex }; +} + + +function basePivotIndex(word) { if (word.length <= 1) return 0; if (word.length <= 5) return 1; if (word.length <= 9) return 2; return 3; } -function highlight(i) { - const p = positions[i]; - if (!p) return; - textInput.focus(); - textInput.setSelectionRange(p[0], p[1]); - scrollHighlightIntoView(); -} -function scrollHighlightIntoView() { - const ta = textInput; - const before = ta.value.slice(0, ta.selectionStart); - const lines = before.split("\n"); - const line = lines.length - 1; - const lineHeight = 20; - const center = ta.clientHeight / 2; - ta.scrollTop = line * lineHeight - center; +function highlight(unit) { + textInput.focus(); + textInput.setSelectionRange(unit.span[0], unit.span[1]); } function draw() { - if (!words[index]) return; + const unit = units[index]; + if (!unit) return; - highlight(index); + highlight(unit); - const raw = words[index].replace(/^[\W_]+|[\W_]+$/g, ""); - const p = pivotIndex(raw); - const pre = raw.slice(0, p); - const mid = raw[p] || ""; - const post = raw.slice(p + 1); + const words = unit.text.split(" "); + const pivotWord = words[unit.pivotWordIndex]; + const clean = pivotWord.replace(/\W/g, ""); - wordEl.innerHTML = - `${pre}${mid}${post}`; + let p = basePivotIndex(clean) + pivotBias; + p = Math.max(0, Math.min(clean.length - 1, p)); - wordEl.style.fontSize = "96px"; + let before = words.slice(0, unit.pivotWordIndex).join(" "); + if (before) before += " "; + const pre = clean.slice(0, p); + const mid = clean[p] || ""; + const post = clean.slice(p + 1); + + let after = words.slice(unit.pivotWordIndex + 1).join(" "); + if (after) after = " " + after; + + wordEl.innerHTML = + `${before}${pre}` + + `${mid}` + + `${post}${after}`; + + wordEl.style.fontSize = "90px"; requestAnimationFrame(alignAndScale); } @@ -102,22 +209,36 @@ function alignAndScale() { const mid = wordEl.querySelector(".pivot"); if (!mid) return; - const boxWidth = focusBox.clientWidth; - const pivotX = boxWidth / 2; + const pivotX = focusBox.clientWidth / 2; const midCenter = mid.offsetLeft + mid.offsetWidth / 2; wordEl.style.left = (pivotX - midCenter) + "px"; - let size = 96; - while (wordEl.scrollWidth > boxWidth - 40 && size > 40) { + let size = 90; + while (wordEl.scrollWidth > focusBox.clientWidth - 40 && size > 40) { size -= 2; wordEl.style.fontSize = size + "px"; } } + +function pacingMultiplier(text) { + let m = 1; + + if (/[.!?]$/.test(text)) m *= 2.4; + else if (/[,:;]$/.test(text)) m *= 1.6; + + const letters = text.replace(/\W/g, "").length; + if (letters >= 10) m *= 1.35; + else if (letters >= 7) m *= 1.2; + + return m; +} + + function togglePlayPause() { if (!running) { - if (!words.length || index === 0) parseText(); + if (!units.length || index === 0) parseText(); running = true; textInput.readOnly = true; playPauseBtn.textContent = "⏸"; @@ -129,15 +250,10 @@ function togglePlayPause() { } } -function punctuationDelay(word) { - if (/[.!?]$/.test(word)) return 2.4; - if (/[,:;]$/.test(word)) return 1.6; - return 1; -} - function loop() { if (!running) return; - if (index >= words.length) { + + if (index >= units.length) { running = false; textInput.readOnly = false; playPauseBtn.textContent = "▶"; @@ -147,8 +263,22 @@ function loop() { draw(); let delay = 60000 / wpmSlider.value; - delay *= punctuationDelay(words[index]); + delay *= pacingMultiplier(units[index].text); index++; timer = setTimeout(loop, delay); } + + + +function step(dir) { + if (!units.length) return; + index = Math.max(0, Math.min(units.length - 1, index + dir)); + draw(); +} +// mobile +focusBox.addEventListener("click", () => { + if (document.activeElement === textInput && !textInput.readOnly) return; + togglePlayPause(); +}); + diff --git a/index.html b/index.html index c5ebf06..20c9488 100644 --- a/index.html +++ b/index.html @@ -1,19 +1,28 @@ - + + - -Neuro Reader - + + + Neuro Reader + + -
+
+ +
+ + +
+ -
+
-
+ +
- - -
500
+ + +
500
-
- - -
+
+ + +
+ + + + - + +
+ + Default + +
+
+ Use Play button or Space to start and pause. + use the ⇐ or ⇒ to move, press stop to edit/paste text. +
+ + +
-
-
-
-
-
+ +
+
+
+
+
-
-
+
- + - + + \ No newline at end of file diff --git a/styles.css b/styles.css index 6c4bb46..b552a32 100644 --- a/styles.css +++ b/styles.css @@ -5,6 +5,7 @@ body { font-family: system-ui, sans-serif; } + #app { height: 100vh; display: grid; @@ -14,11 +15,13 @@ body { #top { display: grid; grid-template-columns: 1.3fr 1fr; - gap: 10px; - padding: 10px; + gap: 12px; + padding: 12px; } -#text-panel, #controls-panel { + +#text-panel, +#controls-panel { background: #0e121c; border-radius: 12px; padding: 16px; @@ -26,19 +29,31 @@ body { flex-direction: column; } + textarea { flex: 1; - background: black; + background: #000; color: white; border: none; padding: 12px; resize: none; font-size: 16px; + line-height: 1.4; +} + +textarea:focus { + outline: none; +} + +textarea::selection { + background: rgba(0, 180, 255, 0.45); } + label { margin-top: 12px; color: #aaa; + font-size: 14px; } button { @@ -47,7 +62,7 @@ button { width: 65px; height: 48px; font-size: 18px; - border: 1px; + border: none; color: white; cursor: pointer; } @@ -59,25 +74,43 @@ button:hover { .controls-group { display: flex; gap: 16px; - margin-top: 20px; - margin-bottom: 20px; + margin: 20px 0; } .toggle { display: flex; align-items: center; - gap: 8px; - font-size: 20px; - color: #aaa; - height: 55px; + gap: 10px; + font-size: 15px; + margin-top: 10px; } .toggle input { accent-color: #ff5c5c; - width: 55px; - height: 18px; } + +.pivot-bias { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +} + +.pivot-bias button { + width: 44px; + height: 36px; + font-size: 16px; +} + +#biasLabel { + color: #ccc; + min-width: 70px; + text-align: center; + font-size: 14px; +} + + #reader-stage { border-top: 1px solid #1c2130; background: radial-gradient(circle, #0f1320, #05070b); @@ -94,6 +127,7 @@ button:hover { overflow: hidden; } + #word { position: absolute; top: 50%; @@ -106,16 +140,16 @@ button:hover { #word .pivot { color: #ff5c5c; - text-shadow: 0 0 12px rgba(255,90,90,0.6); + text-shadow: 0 0 12px rgba(255, 90, 90, 0.6); } + #pivot { position: absolute; width: 1px; height: 100%; - background: rgba(255,80,80,0.35); + background: rgba(255, 80, 80, 0.35); left: 50%; - top: 0; transform: translateX(-50%); } @@ -123,23 +157,63 @@ button:hover { display: none; } -textarea:focus { - outline: none; - box-shadow: none; +.controls-note { + margin-top: auto; + padding-top: 12px; + font-size: 13px; + color: #8a92a8; + line-height: 1.5; + text-align: center; + opacity: 0.9; + border-top: 1px solid rgba(255,255,255,0.06); } -textarea::selection { - background: rgba(0, 180, 255, 0.45); - color: white; -} +@media (max-width: 768px) { -textarea::-moz-selection { - background: rgba(0, 180, 255, 0.45); - color: white; -} + #app { + grid-template-rows: auto 1fr; + } + + #top { + grid-template-columns: 1fr; + padding: 8px; + } + + #text-panel { + height: 160px; + } + + textarea { + font-size: 14px; + } + + #controls-panel { + padding: 12px; + } -/* textarea[readonly] { - opacity: 0.7; - cursor: default; -} */ + .controls-group { + justify-content: space-around; + } + + #reader-stage { + border-top: none; + } + + #focus-box { + height: 120px; + } + + #word { + font-size: 56px; + } + + .pivot-bias { + justify-content: center; + } + + .controls-note { + font-size: 11px; + line-height: 1.4; + } +} From ac1e27cee55ca5a179b1856cb47cb3d9919fc0d1 Mon Sep 17 00:00:00 2001 From: Astro Date: Fri, 16 Jan 2026 19:38:53 +0530 Subject: [PATCH 2/3] Reduce font size of #word from 56px to 36px --- styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles.css b/styles.css index b552a32..9358323 100644 --- a/styles.css +++ b/styles.css @@ -204,7 +204,7 @@ button:hover { } #word { - font-size: 56px; + font-size: 36px; } .pivot-bias { From 606d9c144c536afb3ceaa543c57765a4e7c28059 Mon Sep 17 00:00:00 2001 From: Astronidsp Date: Fri, 16 Jan 2026 20:59:12 +0530 Subject: [PATCH 3/3] more fixes --- app.js | 43 +++++++++++++++++++++++++++++++++---------- styles.css | 6 ++++-- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app.js b/app.js index 19c3037..f582f01 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,4 @@ - const textInput = document.getElementById("textInput"); const wordEl = document.getElementById("word"); const focusBox = document.getElementById("focus-box"); @@ -161,10 +160,14 @@ function makeUnit(tokens) { function basePivotIndex(word) { - if (word.length <= 1) return 0; - if (word.length <= 5) return 1; - if (word.length <= 9) return 2; - return 3; + const len = word.length; + + if (len <= 4) return 1; + if (len <= 7) return 2; + if (len <= 10) return 3; + + + return Math.min(len - 2, Math.floor(len * 0.35)); } @@ -201,27 +204,47 @@ function draw() { `${mid}` + `${post}${after}`; - wordEl.style.fontSize = "90px"; + // wordEl.style.fontSize = "90px"; + wordEl.style.fontSize = window.innerWidth <= 768 ? "48px" : "90px"; requestAnimationFrame(alignAndScale); } + function alignAndScale() { const mid = wordEl.querySelector(".pivot"); if (!mid) return; - const pivotX = focusBox.clientWidth / 2; - const midCenter = mid.offsetLeft + mid.offsetWidth / 2; + const boxWidth = focusBox.clientWidth; + const pivotX = boxWidth / 2; + + + wordEl.style.left = "0px"; + + + wordEl.offsetWidth; + + const midCenter = + mid.offsetLeft + mid.offsetWidth / 2; + wordEl.style.left = (pivotX - midCenter) + "px"; - let size = 90; - while (wordEl.scrollWidth > focusBox.clientWidth - 40 && size > 40) { + let size = parseFloat(getComputedStyle(wordEl).fontSize); + const minSize = window.innerWidth <= 768 ? 28 : 40; + const padding = window.innerWidth <= 768 ? 14 : 40; + + while ( + wordEl.scrollWidth > boxWidth - padding && + size > minSize + ) { size -= 2; wordEl.style.fontSize = size + "px"; + wordEl.offsetWidth; // force reflow } } + function pacingMultiplier(text) { let m = 1; diff --git a/styles.css b/styles.css index 9358323..7eb5b49 100644 --- a/styles.css +++ b/styles.css @@ -200,11 +200,13 @@ button:hover { } #focus-box { - height: 120px; + position: relative; } #word { - font-size: 36px; + position: absolute; + top: 50%; + transform: translateY(-50%); } .pivot-bias {