From 5d4747f78068f9600461b4e3bb2b79c4aaf18c74 Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 5 Aug 2026 15:25:52 +0200 Subject: [PATCH] Aux outputs: per-monitor overlay text (static or scrolling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each aux row gets a 🔤 toggle opening a sub-row: text, position (top/middle/bottom), static or marquee, size, speed, color, bold. The text renders as an overlay above whatever the window shows — effect, image or plain black — so a monitor can be a dedicated message board. Scroll duration scales with text length for constant apparent speed. Co-Authored-By: Claude Fable 5 --- src/aux.html | 18 ++++++++++++++++++ src/aux.js | 22 ++++++++++++++++++++++ src/control.js | 40 +++++++++++++++++++++++++++++++++++++++- src/style.css | 16 ++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/aux.html b/src/aux.html index f50f75b..553cba0 100644 --- a/src/aux.html +++ b/src/aux.html @@ -9,11 +9,29 @@ html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #000; overflow: hidden; cursor: none; } #gl { position: absolute; inset: 0; width: 100%; height: 100%; } #aux-image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: none; } + /* Overlay text: static (centered) or scrolling marquee, over any mode */ + #aux-text { + position: absolute; left: 0; width: 100%; + display: none; overflow: hidden; white-space: nowrap; + font-family: -apple-system, BlinkMacSystemFont, 'Arial Black', sans-serif; + text-shadow: 0 0 18px rgba(0,0,0,0.9), 0 2px 6px rgba(0,0,0,0.8); + pointer-events: none; + } + #aux-text.static { text-align: center; } + #aux-text.scroll #aux-text-inner { + display: inline-block; padding-left: 100vw; + animation: auxscroll var(--aux-scroll-dur, 14s) linear infinite; + } + @keyframes auxscroll { + from { transform: translateX(0); } + to { transform: translateX(-100%); } + } +
diff --git a/src/aux.js b/src/aux.js index 20a562c..6396242 100644 --- a/src/aux.js +++ b/src/aux.js @@ -36,6 +36,27 @@ function applyMode() { if (mode === 'follow' && followEffect) viz.setEffect(safeEffect(followEffect)); } +// Overlay text on top of whatever the mode shows (even plain black). +const txtEl = document.getElementById('aux-text'); +const txtInner = document.getElementById('aux-text-inner'); +function applyText(c) { + const on = !!(c && c.on && c.value && c.value.trim()); + txtEl.style.display = on ? 'block' : 'none'; + if (!on) return; + txtInner.textContent = c.value; + txtEl.className = c.scroll ? 'scroll' : 'static'; + txtEl.style.fontSize = (c.size || 8) + 'vh'; + txtEl.style.color = c.color || '#ffffff'; + txtEl.style.fontWeight = c.weight === false ? 'normal' : '900'; + const pos = c.pos || 'middle'; + txtEl.style.top = pos === 'top' ? '6%' : pos === 'bottom' ? 'auto' : '50%'; + txtEl.style.bottom = pos === 'bottom' ? '6%' : 'auto'; + txtEl.style.transform = pos === 'middle' ? 'translateY(-50%)' : 'none'; + // Longer texts scroll at the same apparent speed: duration scales with length. + const base = 8 + c.value.length * 0.35; + txtEl.style.setProperty('--aux-scroll-dur', (base / (c.speed || 1)).toFixed(1) + 's'); +} + // ---- audio + clock ------------------------------------------------------- // The relay carries the primary's clock (already speed-scaled). Between frames // we extrapolate with the local rAF delta so motion stays 60fps-smooth, and a @@ -71,6 +92,7 @@ window.djv.onControl((m) => { if (m.image) imgEl.src = toFileURL(m.image); else if (m.mode !== 'image') imgEl.removeAttribute('src'); applyMode(); + applyText(m.text); break; } } diff --git a/src/control.js b/src/control.js index c2e2baf..bff3e68 100644 --- a/src/control.js +++ b/src/control.js @@ -1628,6 +1628,7 @@ function auxSendCfg(displayId) { if (!c || !c.on) return; send({ type: 'auxCfg', displayId: parseInt(displayId, 10), mode: c.mode || 'follow', effectIndex: c.effectIndex, image: c.image || null, + text: c.text || null, effect: currentEffect }); // so a fresh window in 'follow' mode starts aligned } @@ -1677,10 +1678,32 @@ function renderAuxList() { extra = '' + (c.image ? c.image.split(/[\\/]/).pop() : 'nessuna') + ''; } + const tx = c.text = c.text || { on: false, value: '', pos: 'middle', size: 8, color: '#ffffff', weight: true, scroll: true, speed: 1 }; + let textRow = ''; + if (tx.on) { + textRow = + '
' + + '' + + '' + + '' + + 'Dim' + + 'Vel' + + '' + + '' + + '
'; + } row.innerHTML = '' + - '' + extra; + '' + extra + + '' + + textRow; row.querySelector('.aux-on').addEventListener('change', (e) => { c.on = e.target.checked; auxSave(); auxSyncAll(); @@ -1714,6 +1737,21 @@ function renderAuxList() { auxSave(); auxSendCfg(d.id); }); } + row.querySelector('.aux-txt').addEventListener('click', () => { + tx.on = !tx.on; auxSave(); auxSendCfg(d.id); renderAuxList(); + }); + const txVal = row.querySelector('.atx-val'); + if (txVal) { + txVal.value = tx.value || ''; + const live = () => { auxSave(); auxSendCfg(d.id); }; + txVal.addEventListener('input', (e) => { tx.value = e.target.value; live(); }); + row.querySelector('.atx-pos').addEventListener('change', (e) => { tx.pos = e.target.value; live(); }); + row.querySelector('.atx-scroll').addEventListener('change', (e) => { tx.scroll = e.target.value === '1'; live(); }); + row.querySelector('.atx-size').addEventListener('input', (e) => { tx.size = parseFloat(e.target.value); live(); }); + row.querySelector('.atx-speed').addEventListener('input', (e) => { tx.speed = parseFloat(e.target.value); live(); }); + row.querySelector('.atx-color').addEventListener('input', (e) => { tx.color = e.target.value; live(); }); + row.querySelector('.atx-bold').addEventListener('change', (e) => { tx.weight = e.target.checked; live(); }); + } const imgBtn = row.querySelector('.aux-img'); if (imgBtn) imgBtn.addEventListener('click', () => { const inp = document.createElement('input'); diff --git a/src/style.css b/src/style.css index f05c22f..f9bb355 100644 --- a/src/style.css +++ b/src/style.css @@ -963,3 +963,19 @@ kbd { .aux-row .aux-fx select { flex: 1; min-width: 0; padding: 4px 6px; font-size: 11px; } .aux-row .aux-img { flex: 0 0 auto; width: auto; padding: 4px 8px; font-size: 11px; } .aux-row .aux-img-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: 0.75; } +.aux-row { flex-wrap: wrap; } +.aux-row .aux-txt { + flex: 0 0 auto; width: auto; padding: 4px 8px; font-size: 12px; + background: transparent; border: 1px solid rgba(255,255,255,0.15); border-radius: 7px; +} +.aux-row .aux-txt.active { background: #2b3a6e; border-color: #5f7fe8; } +.aux-row .aux-text-row { + flex: 1 0 100%; display: flex; align-items: center; gap: 6px; + margin-top: 4px; padding-top: 6px; border-top: 1px dashed rgba(255,255,255,0.1); +} +.aux-row .atx-val { flex: 1 1 160px; min-width: 0; width: auto; padding: 4px 6px; font-size: 11px; } +.aux-row .atx-pos, .aux-row .atx-scroll { flex: 0 0 auto; width: 92px; padding: 4px 6px; font-size: 11px; } +.aux-row .atx-lbl { flex: 0 0 auto; font-size: 10px; opacity: 0.6; } +.aux-row .atx-size, .aux-row .atx-speed { flex: 0 0 70px; } +.aux-row .atx-color { flex: 0 0 30px; width: 30px; height: 22px; padding: 1px; } +.aux-row .atx-b { flex: 0 0 auto; display: flex; align-items: center; gap: 3px; font-size: 11px; }