From de85639aafc7c34597191e0e44929b55d72913f0 Mon Sep 17 00:00:00 2001 From: luciano Date: Thu, 30 Jul 2026 10:41:23 +0200 Subject: [PATCH] LED wall active area: confine the output to a pixel-mapping rectangle New 'Area LED' controls in the Schermo tab: W x H @ X,Y. Everything (canvas, videos, images, logos, ticker) lives in a #stage box whose transform makes it the containing block for the fixed layers, so resizing it confines the whole show at once, black outside. The canvas resizes to the stage, so shaders render at the wall's native resolution. Persisted in localStorage and re-pushed when the output window (re)starts; the recorder follows the canvas position. Co-Authored-By: Claude Fable 5 --- src/control.html | 12 ++++++++++++ src/control.js | 22 ++++++++++++++++++++++ src/output.html | 3 +++ src/output.js | 17 ++++++++++++++++- src/style.css | 28 ++++++++++++++++++++++------ src/visualizer.js | 6 +++++- 6 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/control.html b/src/control.html index b47c17a..9e64793 100644 --- a/src/control.html +++ b/src/control.html @@ -344,6 +344,18 @@ +
+ +
+ + × + px @ + , + +
+
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); }