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; }