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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc
This commit is contained in:
1 parent
b169e9251b
commit
e793add541
3 files changed
+44
-4
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dj-visualizer",
|
"name": "dj-visualizer",
|
||||||
"version": "1.9.0",
|
"version": "1.9.1",
|
||||||
"description": "Audio-reactive fractal & abstract visualizer for external display (Mac)",
|
"description": "Audio-reactive fractal & abstract visualizer for external display (Mac)",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
+40
-3
@@ -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) {}
|
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;
|
recCtx.globalAlpha = parseFloat(getComputedStyle(bgVideo).opacity) || 1;
|
||||||
const bm = bgVideo.style.mixBlendMode;
|
const bm = bgVideo.style.mixBlendMode;
|
||||||
recCtx.globalCompositeOperation = (bm && bm !== 'normal') ? bm : 'source-over';
|
recCtx.globalCompositeOperation = (bm && bm !== 'normal') ? bm : 'source-over';
|
||||||
@@ -114,7 +115,7 @@ function composeFrame() {
|
|||||||
recCtx.globalCompositeOperation = 'source-over';
|
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;
|
recCtx.globalAlpha = parseFloat(getComputedStyle(trackVideo).opacity) || 1;
|
||||||
const bm = trackVideo.style.mixBlendMode;
|
const bm = trackVideo.style.mixBlendMode;
|
||||||
recCtx.globalCompositeOperation = (bm && bm !== 'normal') ? bm : 'source-over';
|
recCtx.globalCompositeOperation = (bm && bm !== 'normal') ? bm : 'source-over';
|
||||||
@@ -388,6 +389,7 @@ djv.onControl(async (m) => {
|
|||||||
case 'mapOn':
|
case 'mapOn':
|
||||||
mapOn = !!m.on;
|
mapOn = !!m.on;
|
||||||
$('#map-canvas').classList.toggle('show', mapOn);
|
$('#map-canvas').classList.toggle('show', mapOn);
|
||||||
|
mapLayerOrder(mapOn);
|
||||||
break;
|
break;
|
||||||
case 'mapEdit':
|
case 'mapEdit':
|
||||||
if (mapping) mapping.editOn = !!m.on;
|
if (mapping) mapping.editOn = !!m.on;
|
||||||
@@ -649,7 +651,10 @@ function frame() {
|
|||||||
const a = audio.values;
|
const a = audio.values;
|
||||||
avTick(performance.now());
|
avTick(performance.now());
|
||||||
viz.render(t * speed, a);
|
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
|
// Interactive family: tell the control panel if the camera didn't start
|
||||||
// (permission denied / missing device). Mouse interaction still works.
|
// (permission denied / missing device). Mouse interaction still works.
|
||||||
@@ -719,6 +724,38 @@ requestAnimationFrame(frame);
|
|||||||
|
|
||||||
// ------------------------------------------------------------- mapping
|
// ------------------------------------------------------------- mapping
|
||||||
let mapping = null, mapOn = false;
|
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() {
|
function ensureMapping() {
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
mapping = new window.MappingSim($('#map-canvas'), canvas);
|
mapping = new window.MappingSim($('#map-canvas'), canvas);
|
||||||
|
|||||||
@@ -1008,3 +1008,6 @@ kbd {
|
|||||||
.aux-row .atx-b { flex: 0 0 auto; display: flex; align-items: center; gap: 3px; font-size: 11px; }
|
.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; }
|
.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; }
|
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; }
|
||||||
Reference in new issue
Block a user