Aux outputs: per-monitor overlay text (static or scrolling)

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 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-08-05 15:25:52 +02:00
1 parent 09bc0cc8cf
commit 5d4747f780
4 files changed
+95 -1

No files matched your search

+18
View File
@@ -9,11 +9,29 @@
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #000; overflow: hidden; cursor: none; } 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%; } #gl { position: absolute; inset: 0; width: 100%; height: 100%; }
#aux-image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: none; } #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%); }
}
</style> </style>
</head> </head>
<body> <body>
<canvas id="gl"></canvas> <canvas id="gl"></canvas>
<img id="aux-image" alt="" /> <img id="aux-image" alt="" />
<div id="aux-text"><span id="aux-text-inner"></span></div>
<script src="shaders.js"></script> <script src="shaders.js"></script>
<script src="effects.js"></script> <script src="effects.js"></script>
<script src="fluid.js"></script> <script src="fluid.js"></script>
+22
View File
@@ -36,6 +36,27 @@ function applyMode() {
if (mode === 'follow' && followEffect) viz.setEffect(safeEffect(followEffect)); 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 ------------------------------------------------------- // ---- audio + clock -------------------------------------------------------
// The relay carries the primary's clock (already speed-scaled). Between frames // 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 // 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); if (m.image) imgEl.src = toFileURL(m.image);
else if (m.mode !== 'image') imgEl.removeAttribute('src'); else if (m.mode !== 'image') imgEl.removeAttribute('src');
applyMode(); applyMode();
applyText(m.text);
break; break;
} }
} }
+39 -1
View File
@@ -1628,6 +1628,7 @@ function auxSendCfg(displayId) {
if (!c || !c.on) return; if (!c || !c.on) return;
send({ type: 'auxCfg', displayId: parseInt(displayId, 10), send({ type: 'auxCfg', displayId: parseInt(displayId, 10),
mode: c.mode || 'follow', effectIndex: c.effectIndex, image: c.image || null, 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 effect: currentEffect }); // so a fresh window in 'follow' mode starts aligned
} }
@@ -1677,10 +1678,32 @@ function renderAuxList() {
extra = '<button class="aux-img">Carica…</button><span class="aux-img-name">' + extra = '<button class="aux-img">Carica…</button><span class="aux-img-name">' +
(c.image ? c.image.split(/[\\/]/).pop() : 'nessuna') + '</span>'; (c.image ? c.image.split(/[\\/]/).pop() : 'nessuna') + '</span>';
} }
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 =
'<div class="aux-text-row">' +
'<input class="atx-val" type="text" placeholder="Testo da mostrare su questo schermo…" />' +
'<select class="atx-pos">' +
[['top', 'Alto'], ['middle', 'Centro'], ['bottom', 'Basso']].map(([v, l]) =>
'<option value="' + v + '"' + (v === tx.pos ? ' selected' : '') + '>' + l + '</option>').join('') +
'</select>' +
'<select class="atx-scroll">' +
'<option value="1"' + (tx.scroll ? ' selected' : '') + '>Scorrevole</option>' +
'<option value="0"' + (!tx.scroll ? ' selected' : '') + '>Fisso</option>' +
'</select>' +
'<span class="atx-lbl">Dim</span><input class="atx-size" type="range" min="3" max="20" step="0.5" value="' + tx.size + '" />' +
'<span class="atx-lbl">Vel</span><input class="atx-speed" type="range" min="0.2" max="3" step="0.1" value="' + tx.speed + '" />' +
'<input class="atx-color" type="color" value="' + tx.color + '" />' +
'<label class="atx-b"><input type="checkbox" class="atx-bold"' + (tx.weight ? ' checked' : '') + ' /> B</label>' +
'</div>';
}
row.innerHTML = row.innerHTML =
'<label class="aux-chk"><input type="checkbox" class="aux-on"' + (c.on ? ' checked' : '') + ' /> ' + '<label class="aux-chk"><input type="checkbox" class="aux-on"' + (c.on ? ' checked' : '') + ' /> ' +
d.label + ' <small>' + d.bounds.width + '×' + d.bounds.height + '</small></label>' + d.label + ' <small>' + d.bounds.width + '×' + d.bounds.height + '</small></label>' +
'<select class="aux-mode">' + modeOpts + '</select>' + extra; '<select class="aux-mode">' + modeOpts + '</select>' + extra +
'<button class="aux-txt' + (tx.on ? ' active' : '') + '" title="Scritta su questo schermo (sopra qualsiasi contenuto)">🔤</button>' +
textRow;
row.querySelector('.aux-on').addEventListener('change', (e) => { row.querySelector('.aux-on').addEventListener('change', (e) => {
c.on = e.target.checked; auxSave(); auxSyncAll(); c.on = e.target.checked; auxSave(); auxSyncAll();
@@ -1714,6 +1737,21 @@ function renderAuxList() {
auxSave(); auxSendCfg(d.id); 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'); const imgBtn = row.querySelector('.aux-img');
if (imgBtn) imgBtn.addEventListener('click', () => { if (imgBtn) imgBtn.addEventListener('click', () => {
const inp = document.createElement('input'); const inp = document.createElement('input');
+16
View File
@@ -963,3 +963,19 @@ kbd {
.aux-row .aux-fx select { flex: 1; min-width: 0; padding: 4px 6px; font-size: 11px; } .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 { 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 .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; }