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