diff --git a/src/control.html b/src/control.html index ea9de0a..e34702d 100644 --- a/src/control.html +++ b/src/control.html @@ -213,6 +213,7 @@
+
@@ -224,8 +225,10 @@ oggetti e superfici inclinate. Fuori dalle zone: nero.

1. Attiva — l'output passa in modalità mappatura.

2. Crea le zone — ➕ Zona Visual mostra l'effetto live in corso - (qualsiasi famiglia, anche il ballerino 3D); ➕ Zona Immagine incolla - un'immagine fissa su una superficie (corner-pin).

+ (qualsiasi famiglia, anche il ballerino 3D); ➕ Zona Effetto fa girare + nella zona un preset indipendente (ogni zona il suo effetto, tutti a tempo + con la stessa musica — ma ogni zona costa un rendering in più); ➕ Zona + Immagine incolla un'immagine fissa su una superficie (corner-pin).

3. Posiziona — spunta Modifica zone: sull'output compaiono le maniglie. Trascina i 4 angoli per adattare la zona alla superficie reale (guardando la proiezione!), oppure clicca dentro la zona per spostarla diff --git a/src/control.js b/src/control.js index 9b4edb0..501de6d 100644 --- a/src/control.js +++ b/src/control.js @@ -1440,11 +1440,30 @@ function renderMapZones() { const row = document.createElement('div'); row.className = 'map-zone'; const isImg = z.src && z.src.type === 'image'; + const isFx = z.src && z.src.type === 'effect'; + let mid; + if (isImg) { + mid = '' + + String(z.src.path || '').split(/[\\/]/).pop() + ''; + } else if (isFx) { + const cur = EFFECTS.list[z.src.effectIndex] || EFFECTS.defaults(); + let famOpts = '', preOpts = ''; + EFFECTS.families.forEach((name, fi) => { + if (/^Interattivo|^Modello 3D/.test(name)) return; + famOpts += ''; + }); + EFFECTS.list.forEach((e2, ei) => { + if (e2.family !== cur.family) return; + preOpts += ''; + }); + mid = '' + + ''; + } row.innerHTML = - '' + (isImg ? '🖼' : '🌀') + '' + + '' + (isImg ? '🖼' : (isFx ? '✨' : '🌀')) + '' + '' + - (isImg ? '' + - String(z.src.path || '').split(/[\\/]/).pop() + '' + (mid !== undefined ? mid : 'Porzione ' + '%,' + '% ' + @@ -1455,6 +1474,27 @@ function renderMapZones() { row.querySelector('.mz-name').addEventListener('change', (e) => { z.name = e.target.value; mapSave(); mapSendZones(); }); + const famSel = row.querySelector('.mz-fam'); + if (famSel) { + const preSel = row.querySelector('.mz-pre'); + famSel.addEventListener('change', () => { + const fam = parseInt(famSel.value, 10); + preSel.innerHTML = ''; + EFFECTS.list.forEach((e2, ei) => { + if (e2.family !== fam) return; + const o = document.createElement('option'); + o.value = ei; + o.textContent = e2.name.replace(EFFECTS.families[fam] + ' · ', ''); + preSel.appendChild(o); + }); + z.src.effectIndex = parseInt(preSel.value, 10); + mapSave(); mapSendZones(); + }); + preSel.addEventListener('change', () => { + z.src.effectIndex = parseInt(preSel.value, 10); + mapSave(); mapSendZones(); + }); + } row.querySelectorAll('.mz-r').forEach(inp => inp.addEventListener('change', (e) => { const k = parseInt(e.target.dataset.k, 10); z.srcRect[k] = Math.max(0, Math.min(1, (parseInt(e.target.value, 10) || 0)/100)); @@ -1477,6 +1517,8 @@ $('#map-on').addEventListener('change', (e) => { }); $('#map-edit').addEventListener('change', (e) => send({ type: 'mapEdit', on: e.target.checked })); $('#map-add-vis').addEventListener('click', () => mapAddZone({ type: 'visual' })); +$('#map-add-fx').addEventListener('click', () => + mapAddZone({ type: 'effect', effectIndex: EFFECTS.list.indexOf(EFFECTS.defaults()) < 0 ? 12 : EFFECTS.list.indexOf(EFFECTS.defaults()) })); $('#map-add-img').addEventListener('click', () => $('#map-img-input').click()); $('#map-img-input').addEventListener('change', (e) => { const f = e.target.files[0]; diff --git a/src/mapping.js b/src/mapping.js index 9711615..57f0d81 100644 --- a/src/mapping.js +++ b/src/mapping.js @@ -104,6 +104,7 @@ class MappingSim { this.visTex = mkTex(); this._mkTex = mkTex; this.imgTex = {}; // path -> {tex, ok} + this.fx = {}; // zoneId -> {canvas, viz, tex, effectIndex} own engine this.zones = []; this.editOn = false; this.selected = -1; @@ -121,6 +122,33 @@ class MappingSim { opacity: z.opacity !== undefined ? z.opacity : 1 })); if (this.selected >= this.zones.length) this.selected = this.zones.length - 1; + // per-zone effect engines: an independent Visualizer on an offscreen + // canvas, so every zone can run a DIFFERENT preset in sync with the music + for (const id in this.fx) { + if (!this.zones.some(z => z.src.type === 'effect' && String(z.id) === id)) { + try { this.fx[id].viz = null; } catch (e) {} + delete this.fx[id]; + } + } + for (const z of this.zones) { + if (z.src.type !== 'effect' || !window.Visualizer || !window.EFFECTS) continue; + let f = this.fx[z.id]; + if (!f) { + const cv = document.createElement('canvas'); + cv.width = 960; cv.height = 540; + const viz = new window.Visualizer(cv); + viz.resize = () => {}; // fixed offscreen resolution + cv.width = 960; cv.height = 540; + viz.gl.viewport(0, 0, 960, 540); + f = this.fx[z.id] = { canvas: cv, viz, tex: this._mkTex(), effectIndex: -1 }; + } + if (f.effectIndex !== z.src.effectIndex) { + let e = window.EFFECTS.list[z.src.effectIndex]; + if (!e || e.isInteractive || e.isModel3d) e = window.EFFECTS.defaults(); + f.viz.setEffect(e); + f.effectIndex = z.src.effectIndex; + } + } // preload image textures const gl = this.gl; for (const z of this.zones) { @@ -193,10 +221,20 @@ class MappingSim { return hit; } - render() { + render(timeSec, audio) { const gl = this.gl, cv = this.canvas; const w = this.main.width, h = this.main.height; if (cv.width !== w || cv.height !== h) { cv.width = w; cv.height = h; } + + // step the per-zone effect engines and refresh their textures + for (const z of this.zones) { + const f = z.src.type === 'effect' && this.fx[z.id]; + if (!f) continue; + try { f.viz.render(timeSec || 0, audio || {}); } catch (e) { continue; } + gl.bindTexture(gl.TEXTURE_2D, f.tex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, f.canvas); + } + gl.viewport(0, 0, w, h); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); @@ -219,6 +257,10 @@ class MappingSim { const e = this.imgTex[z.src.url]; if (!e || !e.ok) continue; tex = e.tex; + } else if (z.src.type === 'effect') { + const f = this.fx[z.id]; + if (!f) continue; + tex = f.tex; } const c = z.corners; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ diff --git a/src/output.js b/src/output.js index 1d9cd44..1be5992 100644 --- a/src/output.js +++ b/src/output.js @@ -333,7 +333,9 @@ djv.onControl(async (m) => { 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' } }))); + : (z.src && z.src.type === 'effect' + ? { type: 'effect', effectIndex: z.src.effectIndex } + : { type: 'visual' }) }))); } catch (e) { djv.report({ type: 'error', message: 'Mappatura: ' + e.message }); } break; case 'mapOn': @@ -594,7 +596,7 @@ function frame() { const a = audio.values; avTick(performance.now()); viz.render(t * speed, a); - if (mapOn && mapping) mapping.render(); + if (mapOn && mapping) 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. @@ -661,7 +663,9 @@ function ensureMapping() { // 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' }, + src: z.src.type === 'image' ? { type: 'image', path: z.src.path } + : (z.src.type === 'effect' ? { type: 'effect', effectIndex: z.src.effectIndex } + : { type: 'visual' }), corners: z.corners, srcRect: z.srcRect, opacity: z.opacity })) }); } return mapping; diff --git a/src/style.css b/src/style.css index cef2c51..22fbb63 100644 --- a/src/style.css +++ b/src/style.css @@ -931,6 +931,8 @@ kbd { .map-zone .mz-name { width: 90px; flex: 0 0 auto; } .map-zone .mz-src { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: 0.75; } .map-zone .mz-rect { flex: 1; display: flex; align-items: center; gap: 2px; white-space: nowrap; opacity: 0.9; } +.map-zone .mz-fx { flex: 1; display: flex; gap: 4px; min-width: 0; } +.map-zone .mz-fx select { flex: 1; min-width: 0; padding: 4px 6px; font-size: 11px; } .map-zone .mz-r { width: 44px; flex: 0 0 auto; } .map-zone .mz-op { flex: 0 0 70px; } .map-zone .mz-del {