From e793add5410b176f98cf714307e51a64f52ef827 Mon Sep 17 00:00:00 2001 From: luciano Date: Fri, 2 Oct 2026 12:02:48 +0200 Subject: [PATCH] Mapping: videos go inside the zones instead of covering them The VJ loop and the playlist video track are full-screen DOM layers above the WebGL canvas, so with mapping on a playing video hid the zones entirely (and 'visual' zones never contained the video). While mapping is on the map canvas is now moved above both video layers (videos keep playing, covered; opaque black ground) and 'visual' zones sample a 2D composite of effect + videos with the same opacity and blend mode as on screen. Recordings skip the raw video layers while mapping is on, since they are already in the zones. v1.9.1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc --- package.json | 2 +- src/output.js | 43 ++++++++++++++++++++++++++++++++++++++++--- src/style.css | 3 +++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9bb7eda..e416411 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dj-visualizer", - "version": "1.9.0", + "version": "1.9.1", "description": "Audio-reactive fractal & abstract visualizer for external display (Mac)", "main": "main.js", "author": { diff --git a/src/output.js b/src/output.js index 687d220..74bad51 100644 --- a/src/output.js +++ b/src/output.js @@ -105,7 +105,8 @@ function composeFrame() { try { recCtx.drawImage(mapping.canvas, mr.x * sx, mr.y * sy, mr.width * sx, mr.height * sy); } catch (e) {} } - if (bgVideo.classList.contains('show') && bgVideo.readyState >= 2 && bgVideo.videoWidth) { + const vidsInMap = mapOn && mapping; // videos are composited into the zones + if (!vidsInMap && bgVideo.classList.contains('show') && bgVideo.readyState >= 2 && bgVideo.videoWidth) { recCtx.globalAlpha = parseFloat(getComputedStyle(bgVideo).opacity) || 1; const bm = bgVideo.style.mixBlendMode; recCtx.globalCompositeOperation = (bm && bm !== 'normal') ? bm : 'source-over'; @@ -114,7 +115,7 @@ function composeFrame() { recCtx.globalCompositeOperation = 'source-over'; } - if (trackVideo.classList.contains('show') && trackVideo.readyState >= 2 && trackVideo.videoWidth) { + if (!vidsInMap && trackVideo.classList.contains('show') && trackVideo.readyState >= 2 && trackVideo.videoWidth) { recCtx.globalAlpha = parseFloat(getComputedStyle(trackVideo).opacity) || 1; const bm = trackVideo.style.mixBlendMode; recCtx.globalCompositeOperation = (bm && bm !== 'normal') ? bm : 'source-over'; @@ -388,6 +389,7 @@ djv.onControl(async (m) => { case 'mapOn': mapOn = !!m.on; $('#map-canvas').classList.toggle('show', mapOn); + mapLayerOrder(mapOn); break; case 'mapEdit': if (mapping) mapping.editOn = !!m.on; @@ -649,7 +651,10 @@ function frame() { const a = audio.values; avTick(performance.now()); viz.render(t * speed, a); - if (mapOn && mapping) mapping.render(t * speed, a); + if (mapOn && mapping) { + mapping.main = mapVisualSource(); + mapping.render(t * speed, a); + } // Interactive family: tell the control panel if the camera didn't start // (permission denied / missing device). Mouse interaction still works. @@ -719,6 +724,38 @@ requestAnimationFrame(frame); // ------------------------------------------------------------- mapping let mapping = null, mapOn = false; + +// Videos (VJ loop + playlist video track) are DOM layers above the WebGL +// canvas: with mapping on they would cover the zones entirely, and the zones +// would not contain them. So while mapping is on the map canvas is moved +// ABOVE the video layers (they keep playing, just covered) and 'visual' zones +// sample a 2D composite of effect + videos, blended exactly like on screen. +let mapSrc = null, mapSrcCtx = null; +const mapCanvasEl = $('#map-canvas'); +function mapLayerOrder(on) { + if (on) trackVideo.after(mapCanvasEl); // above both videos + else canvas.after(mapCanvasEl); // back right above the visual +} +function videoLive(v) { return v.classList.contains('show') && v.readyState >= 2 && v.videoWidth; } +function mapVisualSource() { + if (!videoLive(bgVideo) && !videoLive(trackVideo)) return canvas; + const W = canvas.width, H = canvas.height; + if (!mapSrc) { mapSrc = document.createElement('canvas'); mapSrcCtx = mapSrc.getContext('2d'); } + if (mapSrc.width !== W || mapSrc.height !== H) { mapSrc.width = W; mapSrc.height = H; } + const ctx = mapSrcCtx; + ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; + ctx.drawImage(canvas, 0, 0, W, H); + for (const [v, fit] of [[bgVideo, 'cover'], [trackVideo, 'contain']]) { + if (!videoLive(v)) continue; + ctx.globalAlpha = parseFloat(getComputedStyle(v).opacity) || 1; + const bm = v.style.mixBlendMode; + ctx.globalCompositeOperation = (bm && bm !== 'normal') ? bm : 'source-over'; + try { drawMediaFit(ctx, v, W, H, v.style.objectFit || fit); } catch (e) {} + } + ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; + return mapSrc; +} + function ensureMapping() { if (!mapping) { mapping = new window.MappingSim($('#map-canvas'), canvas); diff --git a/src/style.css b/src/style.css index 4821bfb..031ca53 100644 --- a/src/style.css +++ b/src/style.css @@ -1008,3 +1008,6 @@ kbd { .aux-row .atx-b { flex: 0 0 auto; display: flex; align-items: center; gap: 3px; font-size: 11px; } .map-ed-info #map-straighten { width: auto; padding: 3px 10px; font-size: 11px; } body.control #map-grid { width: auto; flex: 0 0 auto; padding: 3px 8px; font-size: 11px; } +/* With mapping on the map canvas sits above the video layers: an opaque + ground keeps semi-transparent zone pixels from revealing the raw video. */ +#map-canvas.show { background: #000; }