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 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-30 10:41:23 +02:00
1 parent c1b3f9c311
commit de85639aaf
6 files changed
+80 -8

No files matched your search

+12
View File
@@ -344,6 +344,18 @@
<button id="btn-fullscreen">⛶ Fullscreen</button>
</div>
</section>
<section class="span2">
<label>Area LED (pixel mapping)</label>
<div class="row led-row">
<label class="led-chk"><input type="checkbox" id="led-on" /> Attiva</label>
<input type="number" class="led-num" id="led-w" min="16" max="8192" step="1" title="Larghezza in px" /> ×
<input type="number" class="led-num" id="led-h" min="16" max="8192" step="1" title="Altezza in px" /> px @
<input type="number" class="led-num" id="led-x" min="0" max="8192" step="1" title="Posizione X in px" /> ,
<input type="number" class="led-num" id="led-y" min="0" max="8192" step="1" title="Posizione Y in px" />
</div>
<div class="hint-mini">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.</div>
</section>
</div>
</main>
+22
View File
@@ -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 = '<option value="">— Input live (mic/line/BlackHole) —</option>';
+3
View File
@@ -8,6 +8,8 @@
<link rel="stylesheet" href="style.css" />
</head>
<body class="output">
<!-- LED-wall active area: every layer lives inside; black outside -->
<div id="stage">
<canvas id="gl"></canvas>
<!-- Video layer (VJ loop) that blends with the generated visuals -->
@@ -39,6 +41,7 @@
<div class="big">🎛 In attesa dell'audio…</div>
<div class="small">Usa il pannello di controllo sull'altro schermo</div>
</div>
</div><!-- /#stage -->
<script src="shaders.js"></script>
<script src="effects.js"></script>
+16 -1
View File
@@ -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;
+22 -6
View File
@@ -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 */
+5 -1
View File
@@ -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);
}