diff --git a/static/index.html b/static/index.html index eb3f191..9e873f2 100644 --- a/static/index.html +++ b/static/index.html @@ -150,7 +150,8 @@ #editor::placeholder { color: #5a5249; } /* colored rhyme segments — background tints only, so the textarea text on top stays crisp and box metrics stay identical */ - .hseg { border-radius: 4px; } + .hseg { border-radius: 4px; transition: filter .2s ease, opacity .2s ease; } + .editor-shell.focusing .hseg[data-g].lit { color: var(--ink); filter: brightness(1.7) saturate(1.4); } .toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } @@ -389,8 +390,8 @@ function debounce(fn, ms){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout let analysis = null; // last server response let backendOk = true; -let focusGid = null; // spotlight: the family under the caret let focusAllit = null; // alliteration group under the caret +let activeFam = null; // the rhyme family currently emphasized (hover or caret) let hoverGid = null; // family under the mouse (hover preview) function gidAtPoint(x, y){ let off = null; @@ -445,8 +446,8 @@ function caretAllit(){ } function updateSpotlight(){ - const g = caretGid(), a = caretAllit(); - if(g !== focusGid || a !== focusAllit){ focusGid = g; focusAllit = a; render(); } + focusAllit = caretAllit(); + setEmphasis(caretGid()); } let analyzeSeq = 0; // guards against out-of-order responses @@ -607,7 +608,6 @@ function render(){ if(analysis.near) analysis.near.forEach(t=>{ (nearByLine[t.l] ||= []).push(t); }); } const colorOf = t => `var(--r${groupInfo[t.g].color % COLORS})`; - const litGid = hoverGid != null ? hoverGid : focusGid; let html = ''; lines.forEach((line, i)=>{ // apply spans where the line still matches what the server analyzed; @@ -653,19 +653,14 @@ function render(){ // whole word fills dimly; the rhyming part gets a bright underline. // gray = an ending still waiting for its answer let style = ''; - let tk = ''; + let tk = '', dg = ''; const shadows = []; if(w || p){ - const t = (w && (litGid === null || w.g === litGid)) ? w - : (p && (litGid === null || p.g === litGid)) ? p - : (w || p); + const t = w || p; let alpha = !t.ph ? (t.end ? 34 : 19) : (t.end ? 24 : 14); const str = t.str != null ? t.str : ((groupInfo[t.g] && groupInfo[t.g].strength) || 1); alpha = Math.round(alpha * (0.4 + 0.6 * str)); // brightness = this word's rhyme strength - if(litGid !== null && t.g === litGid){ - alpha = Math.min(85, Math.round(alpha * 2.8)); - style += 'color:#1a120c;font-weight:600;'; // dark ink on the lit chip - } + dg = ` data-g="${t.g}"`; style += `background:color-mix(in srgb, ${colorOf(t)} ${alpha}%, transparent);`; if(w){ tk = ` data-tk="${i}:${w.s}"`; if(w.rs == null || a >= w.rs) tk += ` data-tl="${i}:${w.s}"`; } // faint underline on the rhyming tail (rs..e) of a word — a @@ -680,13 +675,18 @@ function render(){ if(al) shadows.push(`inset 0 -2px 0 0 color-mix(in srgb, var(--r${al.g % COLORS}) 75%, transparent)`); if(nr) style += 'text-decoration:underline dotted color-mix(in srgb, var(--accent-2) 60%, transparent);text-underline-offset:3px;'; if(shadows.length) style += `box-shadow:${shadows.join(',')};`; - h += `${text}`; + h += `${text}`; } } const lcls = 'lmark' + (/^\s*#/.test(line) ? ' hdr' : /^\s*[([]/.test(line) ? ' anno' : ''); html += (line ? `${h}` : '') + '\n'; }); highlight.innerHTML = html; + if(activeFam != null) + highlight.querySelectorAll(`.hseg[data-g="${activeFam}"]`).forEach(s=>{ + s.classList.add('lit'); + const lm = s.closest('.lmark'); if(lm) lm.classList.add('litline'); + }); renderStress(lines); syncLayerHeights(); renderGutter(); @@ -725,11 +725,11 @@ function spineFor(memberLines, color, cls){ function renderGutter(){ gutter.innerHTML = ''; if(!analysis) return; - if(focusGid !== null && rhymeToggle.checked){ - const g = analysis.groups.find(x=>x.id === focusGid); + if(activeFam !== null && rhymeToggle.checked){ + const g = analysis.groups.find(x=>x.id === activeFam); if(g){ const ls = new Set(); - analysis.tokens.forEach(t=>{ if(t.g === focusGid) ls.add(t.l); }); + analysis.tokens.forEach(t=>{ if(t.g === activeFam) ls.add(t.l); }); spineFor(ls, `var(--r${g.color % COLORS})`, ''); } } @@ -837,9 +837,9 @@ function buildReadout(){ const bi = st.lines.indexOf(ln); if(bi >= 0) parts.unshift(`bar ${bi + 1}/${st.lines.length}`); } - if(focusGid !== null && analysis){ - const g = analysis.groups.find(x=>x.id === focusGid); - const fam = [...new Set(analysis.tokens.filter(t=>t.g===focusGid) + if(activeFam !== null && analysis){ + const g = analysis.groups.find(x=>x.id === activeFam); + const fam = [...new Set(analysis.tokens.filter(t=>t.g===activeFam) .map(t=>analysis.lines[t.l].slice(t.s,t.e).toLowerCase()))]; if(g && fam.length >= 2) parts.push(`rhymes ${esc(g.sound)} — ${esc(fam.slice(0,6).join(', '))}`); @@ -847,18 +847,33 @@ function buildReadout(){ schemeReadout.innerHTML = parts.join('    '); } -editor.addEventListener('input', ()=>{ focusGid = caretGid(); render(); analyzeSoon(); }); +editor.addEventListener('input', ()=>{ render(); analyzeSoon(); }); editor.addEventListener('scroll', ()=>{ highlight.scrollTop = stresslayer.scrollTop = editor.scrollTop; highlight.scrollLeft = stresslayer.scrollLeft = editor.scrollLeft; renderGutter(); }); +const editorShell = document.querySelector('.editor-shell'); +function setEmphasis(g){ + if(g === activeFam) return; + activeFam = g; + editorShell.classList.toggle('focusing', g !== null); + highlight.querySelectorAll('.hseg.lit').forEach(s=>s.classList.remove('lit')); + highlight.querySelectorAll('.lmark.litline').forEach(s=>s.classList.remove('litline')); + if(g !== null) + highlight.querySelectorAll(`.hseg[data-g="${g}"]`).forEach(s=>{ + s.classList.add('lit'); + const lm = s.closest('.lmark'); if(lm) lm.classList.add('litline'); + }); + renderGutter(); + buildReadout(); +} let hoverRAF = 0; editor.addEventListener('mousemove', e=>{ if(hoverRAF) return; hoverRAF = requestAnimationFrame(()=>{ hoverRAF = 0; const g = gidAtPoint(e.clientX, e.clientY); - if(g !== hoverGid){ hoverGid = g; render(); } + setEmphasis(g != null ? g : caretGid()); }); }); -editor.addEventListener('mouseleave', ()=>{ if(hoverGid !== null){ hoverGid = null; render(); } }); +editor.addEventListener('mouseleave', ()=> setEmphasis(caretGid())); editor.addEventListener('keyup', ()=>{ updateSpotlight(); buildReadout(); }); editor.addEventListener('click', ()=>{ updateSpotlight(); buildReadout(); });