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:
1 parent
a3991e0f92
commit
0daa89656f
6 files changed
+446
No files matched your search
@@ -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) : [];
|
||||
|
||||
Reference in new issue
Block a user