From 22b6d966e14a37cd0e5849c12be6138b0b331b64 Mon Sep 17 00:00:00 2001 From: luciano Date: Fri, 2 Oct 2026 12:08:25 +0200 Subject: [PATCH] Playlist scenes drive the mapping: on/off, which zones, per-zone effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New scene element πŸ—Ί Mappatura (βž•πŸ—Ί): at its time it switches mapping on or off and, via 'Mostra su' checkboxes, picks which zones show content β€” the others go black (opacity 0 on the output only). Effect elements can target a single zone ('πŸ—Ί Zona: …') to run their own preset there; 'no effect' gives the zone back. Both are live overrides on the output: the saved mapping is never modified, and edit reports from the output return the original src/opacity. Manual 'Attiva' toggle shows every zone again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc --- src/control.html | 7 +++++ src/control.js | 77 +++++++++++++++++++++++++++++++++++++++++------- src/output.js | 50 +++++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/control.html b/src/control.html index cfc040a..5c46f25 100644 --- a/src/control.html +++ b/src/control.html @@ -273,6 +273,13 @@ Β· Slider β€” opacitΓ  della zona (le zone piΓΉ in basso nella lista coprono quelle sopra dove si sovrappongono)
Β· βœ• β€” elimina la zona

+

Dalla playlist β€” nella 🎬 Scena di un brano aggiungi βž•πŸ—Ί: accende o spegne la + mappatura in quel momento e, con le spunte Mostra su, sceglie su quali zone si vede + (le altre restano nere). Mettilo a 0:00 per decidere le zone di tutto il brano, o piΓΉ volte + per spostare il visual da una zona all'altra. Nell'elemento πŸŒ€ Effetto, il selettore + Su puΓ² puntare anche a una singola zona (πŸ—Ί Zona: …) per darle un effetto suo + in quel momento; "nessun effetto" la riporta al contenuto normale. La mappatura salvata + non viene mai modificata.

Note β€” tutto Γ¨ salvato automaticamente e rientra nelle registrazioni MP4. Le zone Visual seguono l'effetto attivo: cambi preset e cambia il contenuto di tutte le zone.

diff --git a/src/control.js b/src/control.js index da21b49..0305a07 100644 --- a/src/control.js +++ b/src/control.js @@ -658,7 +658,8 @@ function hasScene(tr) { return !!(tr.cues && tr.cues.some(c => (c.type === 'effect' && c.effectIndex != null) || (c.type === 'text' && c.text && c.text.trim()) || - (c.type === 'image' && c.image))); + (c.type === 'image' && c.image) || + c.type === 'map')); } function parseTime(str) { str = String(str).trim(); @@ -692,6 +693,8 @@ function optList(pairs, sel) { } function newEffectEl(time) { return { type: 'effect', time, effectIndex: EFFECTS.list.indexOf(currentEffect), effectName: currentEffect.name, dur: 0, target: 'main' }; } function newTextEl(time) { return { type: 'text', time, text: '', dir: 'h', fx: 'none', font: TXT_FONTS[0][0], size: 6, weight: true, color: '#ffffff', pos: 'bottom', speed: 1, dur: 0 }; } +// zones: null = every zone, otherwise the ids of the zones that show content +function newMapEl(time) { return { type: 'map', time, on: true, zones: null, dur: 0 }; } function newImageEl(time) { return { type: 'image', time, image: null, imageSize: 60, imagePos: 'center', dur: 0 }; } // Convert legacy combined cues into the new per-element list (idempotent). @@ -713,7 +716,7 @@ function migrateCues(cues) { } // Runtime: re-evaluate which element is active per channel and diff-apply it. -let lastScene = { effect: null, text: null, image: null }; +let lastScene = { effect: null, text: null, image: null, map: null }; let userTrackBlend = 'normal'; // playlist-video blend chosen by the user let sentTrackBlend = null; // last blend pushed to the output (auto or manual) @@ -724,7 +727,7 @@ function startCues(tr) { // `undefined` (not null) forces advanceCues to APPLY every channel once, so a // stale text/image from the previous track is cleared even when the new scene // has nothing active at t=0. - lastScene = { effect: undefined, text: undefined, image: undefined }; + lastScene = { effect: undefined, text: undefined, image: undefined, map: undefined }; advanceCues(0); } function sceneActive(type, t) { @@ -750,6 +753,14 @@ function sceneActiveEffects(t) { return best; } function applyEffectTo(targetKey, el) { + if (targetKey.startsWith('zone:')) { + // Live override of ONE mapping zone (the saved mapping is untouched): + // the zone runs this preset; 'no effect' gives it back its own content. + const id = parseInt(targetKey.slice(5), 10); + const ok = el.effectIndex != null && EFFECTS.list[el.effectIndex]; + send({ type: 'mapZoneFx', id, effectIndex: ok ? el.effectIndex : null }); + return; + } const e = EFFECTS.list[el.effectIndex]; if (!e) return; if (targetKey === 'main') applyEffect(e); @@ -797,6 +808,17 @@ function advanceCues(t) { if (tx !== lastScene.text) { lastScene.text = tx; applyTextEl(tx); } const im = sceneActive('image', t); if (im !== lastScene.image) { lastScene.image = im; applyImageEl(im); } + const mp = sceneActive('map', t); + if (mp !== lastScene.map) { + lastScene.map = mp; + // Apply-once like effects: when the element ends the state stays as is. + if (mp) { + mapCfg.on = !!mp.on; + $('#map-on').checked = mapCfg.on; + send({ type: 'mapShow', ids: mp.on && Array.isArray(mp.zones) ? mp.zones : null }); + send({ type: 'mapOn', on: mapCfg.on }); + } + } } function applyTextEl(el) { const show = !!(el && el.text && el.text.trim()); @@ -828,7 +850,7 @@ function applyImageEl(el) { function refreshScenePreview(i) { if (i !== currentIndex) return; activeCues = (playlist[i].cues || []).slice().sort((a, b) => a.time - b.time); - lastScene = { effect: undefined, text: undefined, image: undefined }; + lastScene = { effect: undefined, text: undefined, image: undefined, map: undefined }; advanceCues(playCur || 0); } @@ -985,8 +1007,8 @@ function firstPresetOfFamily(fam) { return -1; } -const EL_ICON = { effect: 'πŸŒ€', text: 'πŸ”€', image: 'πŸ–Ό' }; -const EL_LABEL = { effect: 'Effetto', text: 'Testo', image: 'Immagine' }; +const EL_ICON = { effect: 'πŸŒ€', text: 'πŸ”€', image: 'πŸ–Ό', map: 'πŸ—Ί' }; +const EL_LABEL = { effect: 'Effetto', text: 'Testo', image: 'Immagine', map: 'Mappatura' }; // Build the per-track scene editor: a list of independent timed elements // (effect / text / image), each with its own appearance time and duration. @@ -1023,7 +1045,13 @@ function buildSceneEditor(tr, i, li) { tgtOpts += ''; }); tgtOpts += ''; - if (tgt !== 'main' && tgt !== 'all' && !auxDisplays.some(ds => String(ds.id) === tgt)) { + mapCfg.zones.forEach(z => { + const v = 'zone:' + z.id; + tgtOpts += ''; + }); + if (tgt.startsWith('zone:') && !mapCfg.zones.some(z => 'zone:' + z.id === tgt)) { + tgtOpts += ''; + } else if (tgt !== 'main' && tgt !== 'all' && !tgt.startsWith('zone:') && !auxDisplays.some(ds => String(ds.id) === tgt)) { tgtOpts += ''; } html += @@ -1060,6 +1088,23 @@ function buildSceneEditor(tr, i, li) { '' + '
Vel
' + ''; + } else if (c.type === 'map') { + html += + '
' + + '
' + + '' + + '
' + + (c.on ? '
Mostra su' + + (mapCfg.zones.length ? mapCfg.zones.map(z => + '').join('') + : 'nessuna zona: creale nel tab Mappatura') + + '
' : '') + + '
'; } else { html += '
' + @@ -1080,6 +1125,7 @@ function buildSceneEditor(tr, i, li) { '' + '' + '' + + '' + '' + '
' + (tr.isInterlude @@ -1175,7 +1221,7 @@ function buildSceneEditor(tr, i, li) { cueEl.querySelector('.cue-eff-cur').addEventListener('click', () => { c.effectIndex = EFFECTS.list.indexOf(currentEffect); c.effectName = currentEffect.name; save(); }); cueEl.querySelector('.cue-eff-target').addEventListener('change', (e) => { const v = e.target.value; - c.target = (v === 'main' || v === 'all') ? v : parseInt(v, 10); + c.target = (v === 'main' || v === 'all' || v.startsWith('zone:')) ? v : parseInt(v, 10); saveLive(); }); } else if (c.type === 'text') { @@ -1189,6 +1235,14 @@ function buildSceneEditor(tr, i, li) { cueEl.querySelector('.cue-tx-bold').addEventListener('change', (e) => { c.weight = e.target.checked; saveLive(); }); cueEl.querySelector('.cue-tx-color').addEventListener('input', (e) => { c.color = e.target.value; saveLive(); }); cueEl.querySelector('.cue-tx-speed').addEventListener('input', (e) => { c.speed = parseFloat(e.target.value); saveLive(); }); + } else if (c.type === 'map') { + cueEl.querySelector('.cue-map-on').addEventListener('change', (e) => { c.on = e.target.value === '1'; save(); }); + cueEl.querySelectorAll('.cue-map-z').forEach(cb => cb.addEventListener('change', () => { + const ids = [...cueEl.querySelectorAll('.cue-map-z')].filter(x => x.checked).map(x => parseInt(x.dataset.id, 10)); + // every zone ticked = null, so zones created later are included too + c.zones = ids.length === mapCfg.zones.length ? null : ids; + saveLive(); + })); } else { cueEl.querySelector('.cue-img').addEventListener('click', () => { sceneImgTarget = i; sceneCueTarget = ci; $('#scene-img-input').click(); }); cueEl.querySelector('.cue-img-clear').addEventListener('click', () => { c.image = null; save(); }); @@ -1205,6 +1259,7 @@ function buildSceneEditor(tr, i, li) { ed.querySelector('.add-eff').addEventListener('click', () => addEl(newEffectEl)); ed.querySelector('.add-txt').addEventListener('click', () => addEl(newTextEl)); ed.querySelector('.add-img').addEventListener('click', () => addEl(newImageEl)); + ed.querySelector('.add-map').addEventListener('click', () => addEl(newMapEl)); ed.querySelector('.se-clear').addEventListener('click', () => { tr.cues = []; sceneEditing = -1; save(); }); const gapInput = ed.querySelector('.cue-gap'); if (gapInput) gapInput.addEventListener('change', (e) => { tr.gap = Math.max(0, parseFloat(e.target.value) || 0); savePlaylistState(); renderPlaylist(); }); @@ -1650,7 +1705,9 @@ function renderMapZones() { } $('#map-on').checked = mapCfg.on; $('#map-on').addEventListener('change', (e) => { - mapCfg.on = e.target.checked; mapSave(); send({ type: 'mapOn', on: mapCfg.on }); + mapCfg.on = e.target.checked; mapSave(); + send({ type: 'mapShow', ids: null }); // manual toggle: every zone visible + send({ type: 'mapOn', on: mapCfg.on }); }); $('#map-edit').addEventListener('change', (e) => send({ type: 'mapEdit', on: e.target.checked })); $('#map-add-vis').addEventListener('click', () => mapAddZone({ type: 'visual' })); @@ -2267,7 +2324,7 @@ function playPad(i) { clearGap(); playbackOwner = 'pad'; activePad = i; padPlaying = true; - activeCues = []; firedCue = -1; lastScene = { effect: null, text: null, image: null }; // pads don't run playlist cues + activeCues = []; firedCue = -1; lastScene = { effect: null, text: null, image: null, map: null }; // pads don't run playlist cues playCur = 0; playDur = durations[p.path] || 0; // The playlist is no longer the active source. currentIndex = -1; isPlaying = false; renderPlaylist(); diff --git a/src/output.js b/src/output.js index 74bad51..9f19eac 100644 --- a/src/output.js +++ b/src/output.js @@ -377,14 +377,18 @@ djv.onControl(async (m) => { 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 } - : (z.src && z.src.type === 'effect' - ? { type: 'effect', effectIndex: z.src.effectIndex } - : { type: 'visual' }) }))); - } catch (e) { djv.report({ type: 'error', message: 'Mappatura: ' + e.message }); } + lastMapZones = m.zones || []; + applyMapZones(); + break; + case 'mapShow': + mapShowIds = Array.isArray(m.ids) ? m.ids : null; + applyMapZones(); + break; + case 'mapZoneFx': + // Playlist scene cue: live override of one zone's content. + if (m.effectIndex == null) delete mapOverrides[m.id]; + else mapOverrides[m.id] = m.effectIndex; + applyMapZones(); break; case 'mapOn': mapOn = !!m.on; @@ -724,6 +728,25 @@ requestAnimationFrame(frame); // ------------------------------------------------------------- mapping let mapping = null, mapOn = false; +// Zones as configured in the panel + live per-zone effect overrides set by +// playlist scene cues (never written back to the panel's saved mapping). +let lastMapZones = [], mapOverrides = {}; +let mapShowIds = null; // playlist scene: only these zone ids show content (null = all) +function applyMapZones() { + try { + ensureMapping().setZones(lastMapZones.map(z => { + const ov = mapOverrides[z.id]; + const src = ov != null ? { type: 'effect', effectIndex: ov } + : (z.src && z.src.type === 'image' + ? { type: 'image', url: toFileURL(z.src.path), path: z.src.path } + : (z.src && z.src.type === 'effect' + ? { type: 'effect', effectIndex: z.src.effectIndex } + : { type: 'visual' })); + const hidden = mapShowIds && !mapShowIds.includes(z.id); + return Object.assign({}, z, { src, opacity: hidden ? 0 : z.opacity }); + })); + } catch (e) { djv.report({ type: 'error', message: 'Mappatura: ' + e.message }); } +} // 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 @@ -762,10 +785,13 @@ 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 } - : (z.src.type === 'effect' ? { type: 'effect', effectIndex: z.src.effectIndex } - : { type: 'visual' }), - corners: z.corners, srcRect: z.srcRect, opacity: z.opacity })) }); + // the panel's ORIGINAL content: a scene override must never be saved + src: ((lastMapZones.find(o => o.id === z.id) || {}).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: ((lastMapZones.find(o => o.id === z.id) || {}).opacity) ?? z.opacity })) }); } return mapping; }