Projection mapping: warped zones for images and the live visual

New Mappatura tab + mapping.js engine: each zone is a quad warped with a
projective homography (square->quad closed form, inverted per fragment)
on a dedicated WebGL2 canvas above the visual, black outside. Sources:
the live visual (with a source sub-rect for multi-panel LED walls) or an
image file (corner-pin onto physical surfaces). Edit mode draws handles
ON THE OUTPUT: corners (or whole quads) are dragged while looking at the
real projection, edits flow back to the panel which persists them. Zone
list with name, source portion, opacity and delete; included in
recordings; re-sent when the output restarts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-31 16:22:43 +02:00
1 parent a3991e0f92
commit 0daa89656f
6 files changed
+446

No files matched your search

+35
View File
@@ -89,6 +89,10 @@ function composeFrame() {
// 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 (mapOn && mapping) {
const mr = mapping.canvas.getBoundingClientRect();
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) {
recCtx.globalAlpha = parseFloat(getComputedStyle(bgVideo).opacity) || 1;
@@ -324,6 +328,22 @@ djv.onControl(async (m) => {
break;
case 'svg': loadCustomTexture(m.dataUrl); break;
case 'modelBg': viz.modelBg = m.mode || 'gradient'; break;
case 'mapZones':
try {
ensureMapping().setZones((m.zones || []).map(z => Object.assign({}, z,
{ src: z.src && z.src.type === 'image'
? { type: 'image', url: toFileURL(z.src.path), path: z.src.path }
: { type: 'visual' } })));
} catch (e) { djv.report({ type: 'error', message: 'Mappatura: ' + e.message }); }
break;
case 'mapOn':
mapOn = !!m.on;
$('#map-canvas').classList.toggle('show', mapOn);
break;
case 'mapEdit':
if (mapping) mapping.editOn = !!m.on;
$('#map-canvas').classList.toggle('edit', !!m.on);
break;
case 'glbAnims': viz.setClipFilter(m.names || []); break;
case 'modelBpm': viz.setManualBpm(m.bpm || 0); break;
case 'modelSpread': viz.setArmSpread(m.deg || 0); break;
@@ -574,6 +594,7 @@ function frame() {
const a = audio.values;
avTick(performance.now());
viz.render(t * speed, a);
if (mapOn && mapping) mapping.render();
// Interactive family: tell the control panel if the camera didn't start
// (permission denied / missing device). Mouse interaction still works.
@@ -632,6 +653,20 @@ function frame() {
}
requestAnimationFrame(frame);
// ------------------------------------------------------------- mapping
let mapping = null, mapOn = false;
function ensureMapping() {
if (!mapping) {
mapping = new window.MappingSim($('#map-canvas'), canvas);
// corner drags on the output flow back to the panel, which persists them
mapping.onChange = (zones) => djv.report({ type: 'mapZones',
zones: zones.map(z => ({ id: z.id, name: z.name,
src: z.src.type === 'image' ? { type: 'image', path: z.src.path } : { type: 'visual' },
corners: z.corners, srcRect: z.srcRect, opacity: z.opacity })) });
}
return mapping;
}
// Tell the panel which dance clips the current model can play.
function reportClips() {
const names = viz.model3d && viz.model3d.anims ? viz.model3d.anims.map(a => a.name) : [];