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 +=
+ '