Per processori LED con mapping 1:1: confina tutto l'output in un rettangolo
+ Larghezza×Altezza alla posizione X,Y sullo schermo (nero attorno). Es. wall 896×512 → 896 × 512 @ 0 , 0.
+
diff --git a/src/control.js b/src/control.js
index 1250836..ad8d824 100644
--- a/src/control.js
+++ b/src/control.js
@@ -1094,6 +1094,25 @@ $('#btn-display').addEventListener('click', () => {
});
$('#btn-fullscreen').addEventListener('click', () => djv.toggleOutputFullscreen());
+// ---- LED wall active area (pixel-mapping processors) ----
+// Persisted; re-sent whenever the output window (re)starts (see 'devices').
+let ledCfg = { on: false, w: 1024, h: 576, x: 0, y: 0 };
+try { ledCfg = Object.assign(ledCfg, JSON.parse(localStorage.getItem('ledarea') || '{}')); } catch (e) {}
+function ledSend() { send({ type: 'ledArea', on: ledCfg.on, w: ledCfg.w, h: ledCfg.h, x: ledCfg.x, y: ledCfg.y }); }
+function ledSave() { localStorage.setItem('ledarea', JSON.stringify(ledCfg)); ledSend(); }
+(function ledInit() {
+ $('#led-on').checked = ledCfg.on;
+ const fields = [['#led-w', 'w'], ['#led-h', 'h'], ['#led-x', 'x'], ['#led-y', 'y']];
+ fields.forEach(([sel, k]) => {
+ $(sel).value = ledCfg[k];
+ $(sel).addEventListener('change', (e) => {
+ ledCfg[k] = Math.max(0, parseInt(e.target.value || '0', 10) || 0);
+ ledSave();
+ });
+ });
+ $('#led-on').addEventListener('change', (e) => { ledCfg.on = e.target.checked; ledSave(); });
+})();
+
// ---------------------------------------------------------------- recording
let recOn = false, recTimer = null, recStartedAt = 0;
function fmtTime(s) {
@@ -1371,6 +1390,9 @@ djv.onReport((m) => {
}
break;
case 'devices': {
+ // The output reports its devices once at startup: good moment to push
+ // config the (possibly recreated) output window doesn't have yet.
+ if (ledCfg.on) ledSend();
const sel = $('#device-select');
const cur = sel.value;
sel.innerHTML = '';
diff --git a/src/output.html b/src/output.html
index 7afa451..be53104 100644
--- a/src/output.html
+++ b/src/output.html
@@ -8,6 +8,8 @@
+
+
@@ -39,6 +41,7 @@
🎛 In attesa dell'audio…
Usa il pannello di controllo sull'altro schermo
+
diff --git a/src/output.js b/src/output.js
index 43aabd3..5968cd1 100644
--- a/src/output.js
+++ b/src/output.js
@@ -83,7 +83,9 @@ function composeFrame() {
recCtx.globalAlpha = 1;
recCtx.globalCompositeOperation = 'source-over';
recCtx.clearRect(0, 0, W, H);
- recCtx.drawImage(canvas, 0, 0, W, H);
+ // Draw the canvas where it actually sits (the LED area may confine it).
+ const cr = canvas.getBoundingClientRect();
+ recCtx.drawImage(canvas, cr.x * sx, cr.y * sy, cr.width * sx, cr.height * sy);
if (bgVideo.classList.contains('show') && bgVideo.readyState >= 2 && bgVideo.videoWidth) {
recCtx.globalAlpha = parseFloat(getComputedStyle(bgVideo).opacity) || 1;
@@ -318,6 +320,19 @@ djv.onControl(async (m) => {
djv.report({ type: 'autoVj', on: autoVj });
break;
case 'svg': loadCustomTexture(m.dataUrl); break;
+ case 'ledArea': {
+ // Confine the whole show to a W×H rectangle at X,Y (LED pixel mapping).
+ const st = $('#stage').style;
+ if (m.on && m.w > 0 && m.h > 0) {
+ st.left = (m.x || 0) + 'px'; st.top = (m.y || 0) + 'px';
+ st.width = m.w + 'px'; st.height = m.h + 'px';
+ } else {
+ st.left = st.top = '0px';
+ st.width = st.height = '';
+ }
+ viz.resize();
+ break;
+ }
case 'recStart': startRecording(m); break;
case 'recStop': stopRecording(m); break;
case 'gain': audio.gain = m.value; break;
diff --git a/src/style.css b/src/style.css
index e3c8b55..0060fe9 100644
--- a/src/style.css
+++ b/src/style.css
@@ -14,11 +14,22 @@ html, body {
body.hide-cursor { cursor: none; }
+/* LED-wall active area: the whole show renders inside this box, black
+ outside. The transform makes it the containing block for the
+ position:fixed layers, so resizing it confines everything at once. */
+#stage {
+ position: fixed;
+ inset: 0;
+ overflow: hidden;
+ background: #000;
+ transform: translateZ(0);
+}
+
#gl {
position: fixed;
inset: 0;
- width: 100vw;
- height: 100vh;
+ width: 100%;
+ height: 100%;
display: block;
}
@@ -437,6 +448,11 @@ textarea.textarea-lg {
.playlist .tname.is-interlude { color: #c9b6ff; font-style: italic; }
/* ============ OUTPUT WINDOW ============ */
+/* LED area controls (Schermo tab) */
+.led-row { align-items: center; gap: 4px; }
+.led-num { width: 64px; }
+.led-chk { display: flex; align-items: center; gap: 4px; margin-right: 6px; white-space: nowrap; }
+
body.output { cursor: none; background: #000; }
/* ---- Image overlay layer ---- */
@@ -467,8 +483,8 @@ body.output { cursor: none; background: #000; }
.bg-video {
position: fixed;
inset: 0;
- width: 100vw;
- height: 100vh;
+ width: 100%;
+ height: 100%;
object-fit: cover;
z-index: 1; /* above the WebGL canvas, below overlays */
pointer-events: none;
@@ -479,8 +495,8 @@ body.output { cursor: none; background: #000; }
.track-video {
position: fixed;
inset: 0;
- width: 100vw;
- height: 100vh;
+ width: 100%;
+ height: 100%;
object-fit: contain;
background: #000;
z-index: 2; /* main visual when a video track plays */
diff --git a/src/visualizer.js b/src/visualizer.js
index 11426bb..f50ada8 100644
--- a/src/visualizer.js
+++ b/src/visualizer.js
@@ -72,7 +72,11 @@ class Visualizer {
resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
- const w = Math.floor(window.innerWidth * dpr), h = Math.floor(window.innerHeight * dpr);
+ // The canvas fills the #stage box (the LED active area), which is
+ // normally the whole window but can be a smaller mapping rectangle.
+ const cw = this.canvas.clientWidth || window.innerWidth;
+ const ch = this.canvas.clientHeight || window.innerHeight;
+ const w = Math.floor(cw * dpr), h = Math.floor(ch * dpr);
if (this.canvas.width !== w || this.canvas.height !== h) { this.canvas.width = w; this.canvas.height = h; }
this.gl.viewport(0, 0, w, h);
}