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:
1 parent
09bc0cc8cf
commit
5d4747f780
4 files changed
+95
-1
No files matched your search
@@ -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%); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gl"></canvas>
|
||||
<img id="aux-image" alt="" />
|
||||
<div id="aux-text"><span id="aux-text-inner"></span></div>
|
||||
<script src="shaders.js"></script>
|
||||
<script src="effects.js"></script>
|
||||
<script src="fluid.js"></script>
|
||||
|
||||
+22
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+39
-1
@@ -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 = '<button class="aux-img">Carica…</button><span class="aux-img-name">' +
|
||||
(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 =
|
||||
'<label class="aux-chk"><input type="checkbox" class="aux-on"' + (c.on ? ' checked' : '') + ' /> ' +
|
||||
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) => {
|
||||
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');
|
||||
|
||||
@@ -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; }
|
||||
Reference in new issue
Block a user