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