Phone remote: stop a scene element or the whole scene
Live elements get a ■ button on the phone: stopping clears text/image/video from their destination, gives a zone back its own content, returns the main effect to the one active before the scene (aux displays back to their own setting) and switches a mapping element off. '■ Ferma scena' clears everything the scene shows and halts the rest of the track's timeline (advanceCues no-ops) until '▶ Riprendi', which re-applies the scene at the current position, or the next track. Hand-stopped elements stay un-flagged until the timeline moves their channel on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc
This commit is contained in:
1 parent
ea91fe35f4
commit
c832dc1647
4 files changed
+75
-6
No files matched your search
+55
-3
@@ -777,6 +777,9 @@ function startCues(tr) {
|
|||||||
// has nothing active at t=0.
|
// has nothing active at t=0.
|
||||||
lastScene = { effect: undefined, text: undefined, image: undefined, map: undefined, video: undefined };
|
lastScene = { effect: undefined, text: undefined, image: undefined, map: undefined, video: undefined };
|
||||||
send({ type: 'mapZoneReset' }); // zones fall back to their Mappatura content
|
send({ type: 'mapZoneReset' }); // zones fall back to their Mappatura content
|
||||||
|
sceneHalted = false;
|
||||||
|
firedByHand.clear();
|
||||||
|
sceneBaseEffect = currentEffect; // what 'stop' on the main effect returns to
|
||||||
advanceCues(0);
|
advanceCues(0);
|
||||||
}
|
}
|
||||||
function sceneActive(type, t) {
|
function sceneActive(type, t) {
|
||||||
@@ -832,7 +835,12 @@ function applyEffectTo(targetKey, el) {
|
|||||||
send({ type: 'auxFx', displayId: id, effectIndex: el.effectIndex });
|
send({ type: 'auxFx', displayId: id, effectIndex: el.effectIndex });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 'Ferma scena' (phone remote): the rest of this track's timeline is skipped
|
||||||
|
// until 'Riprendi' or the next track.
|
||||||
|
let sceneHalted = false;
|
||||||
|
let sceneBaseEffect = null;
|
||||||
function advanceCues(t) {
|
function advanceCues(t) {
|
||||||
|
if (sceneHalted) return;
|
||||||
const efMap = sceneActiveEffects(t);
|
const efMap = sceneActiveEffects(t);
|
||||||
const force = lastScene.effect === undefined;
|
const force = lastScene.effect === undefined;
|
||||||
const prevMap = (lastScene.effect && typeof lastScene.effect === 'object') ? lastScene.effect : {};
|
const prevMap = (lastScene.effect && typeof lastScene.effect === 'object') ? lastScene.effect : {};
|
||||||
@@ -897,6 +905,38 @@ function fireCue(el) {
|
|||||||
else if (el.type === 'map') applyMapEl(el);
|
else if (el.type === 'map') applyMapEl(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Undo what one element put on screen.
|
||||||
|
function stopCue(el) {
|
||||||
|
const k = String(el.target || 'main');
|
||||||
|
if (el.type === 'effect') {
|
||||||
|
if (k === 'main' || k === 'all') { if (sceneBaseEffect) applyEffect(sceneBaseEffect); }
|
||||||
|
if (k === 'all') Object.keys(auxCfg).forEach(id => auxSendCfg(id));
|
||||||
|
else if (k.startsWith('zone:')) send({ type: 'mapZoneSet', id: zoneIdOf(k), kind: 'effect', data: null });
|
||||||
|
else if (k !== 'main') auxSendCfg(k); // aux display back to its own setting
|
||||||
|
} else if (el.type === 'text') applyTextTo(k, null);
|
||||||
|
else if (el.type === 'image') applyImageTo(k, null);
|
||||||
|
else if (el.type === 'video') applyVideoTo(k, null);
|
||||||
|
else if (el.type === 'map') applyMapEl({ on: false });
|
||||||
|
}
|
||||||
|
// Stop the whole scene: clear everything it shows and freeze its timeline.
|
||||||
|
function stopScene() {
|
||||||
|
sceneHalted = true;
|
||||||
|
firedByHand.clear();
|
||||||
|
applyTextEl(null);
|
||||||
|
applyImageEl(null);
|
||||||
|
send({ type: 'mapZoneReset' });
|
||||||
|
send({ type: 'mapShow', ids: null });
|
||||||
|
if (sceneBaseEffect) applyEffect(sceneBaseEffect);
|
||||||
|
Object.keys(auxCfg).forEach(id => auxSendCfg(id));
|
||||||
|
}
|
||||||
|
function resumeScene() {
|
||||||
|
sceneHalted = false;
|
||||||
|
firedByHand.clear();
|
||||||
|
lastScene = { effect: undefined, text: undefined, image: undefined, map: undefined, video: undefined };
|
||||||
|
send({ type: 'mapZoneReset' });
|
||||||
|
advanceCues(sceneTime());
|
||||||
|
}
|
||||||
|
|
||||||
// Human-readable line for a scene element (phone remote list).
|
// Human-readable line for a scene element (phone remote list).
|
||||||
function cueLabel(c) {
|
function cueLabel(c) {
|
||||||
const tgt = String(c.target || 'main');
|
const tgt = String(c.target || 'main');
|
||||||
@@ -2879,6 +2919,17 @@ function remoteCommand(m) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'cueStop': {
|
||||||
|
const tr = playlist[currentIndex];
|
||||||
|
const el = tr && tr.cues && tr.cues[m.i];
|
||||||
|
if (el) {
|
||||||
|
firedByHand.set(cueChannel(el), { el: null, base: timelineByChannel(sceneTime()).get(cueChannel(el)) || null });
|
||||||
|
stopCue(el);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'sceneStop': stopScene(); break;
|
||||||
|
case 'sceneResume': resumeScene(); break;
|
||||||
}
|
}
|
||||||
pushRemoteState();
|
pushRemoteState();
|
||||||
}
|
}
|
||||||
@@ -2903,10 +2954,10 @@ function timelineByChannel(t) {
|
|||||||
const sceneTime = () => segTimer ? (segBase + (playCur || 0)) : (playCur || 0);
|
const sceneTime = () => segTimer ? (segBase + (playCur || 0)) : (playCur || 0);
|
||||||
function remoteScene(tr) {
|
function remoteScene(tr) {
|
||||||
if (!tr || !tr.cues || !tr.cues.length) return [];
|
if (!tr || !tr.cues || !tr.cues.length) return [];
|
||||||
const tl = timelineByChannel(sceneTime());
|
const tl = sceneHalted ? new Map() : timelineByChannel(sceneTime());
|
||||||
for (const [ch, f] of [...firedByHand]) {
|
for (const [ch, f] of [...firedByHand]) {
|
||||||
if ((tl.get(ch) || null) !== f.base || !tr.cues.includes(f.el)) { firedByHand.delete(ch); continue; }
|
if ((tl.get(ch) || null) !== f.base || (f.el && !tr.cues.includes(f.el))) { firedByHand.delete(ch); continue; }
|
||||||
tl.set(ch, f.el);
|
if (f.el) tl.set(ch, f.el); else tl.delete(ch); // null = stopped by hand
|
||||||
}
|
}
|
||||||
const live = new Set(tl.values());
|
const live = new Set(tl.values());
|
||||||
return tr.cues.map((c, i) => ({ i, type: c.type, time: c.time || 0, dur: c.dur || 0,
|
return tr.cues.map((c, i) => ({ i, type: c.type, time: c.time || 0, dur: c.dur || 0,
|
||||||
@@ -2924,6 +2975,7 @@ function pushRemoteState() {
|
|||||||
tracks: playlist.slice(0, 60).map(t => (t.isInterlude ? '✨ ' : '') + t.name),
|
tracks: playlist.slice(0, 60).map(t => (t.isInterlude ? '✨ ' : '') + t.name),
|
||||||
sceneCounts: playlist.slice(0, 60).map(t => (t.cues || []).length),
|
sceneCounts: playlist.slice(0, 60).map(t => (t.cues || []).length),
|
||||||
scene: remoteScene(tr),
|
scene: remoteScene(tr),
|
||||||
|
sceneHalted,
|
||||||
effect: currentEffect ? currentEffect.name : '',
|
effect: currentEffect ? currentEffect.name : '',
|
||||||
family: currentEffect ? currentEffect.family : -1,
|
family: currentEffect ? currentEffect.family : -1,
|
||||||
families: remoteFamilies,
|
families: remoteFamilies,
|
||||||
|
|||||||
@@ -151,10 +151,17 @@ select { appearance: none; -webkit-appearance: none; width: 100%; background-col
|
|||||||
/* scene elements of the current track */
|
/* scene elements of the current track */
|
||||||
.cap-hint { font-size: 10.5px; letter-spacing: 0.5px; color: var(--dim); font-weight: 600; }
|
.cap-hint { font-size: 10.5px; letter-spacing: 0.5px; color: var(--dim); font-weight: 600; }
|
||||||
.cues { display: flex; flex-direction: column; gap: 7px; }
|
.cues { display: flex; flex-direction: column; gap: 7px; }
|
||||||
|
.cue-wrap { display: flex; gap: 7px; }
|
||||||
.cue {
|
.cue {
|
||||||
|
flex: 1; min-width: 0;
|
||||||
display: grid; grid-template-columns: 30px 52px 1fr; align-items: center; gap: 8px;
|
display: grid; grid-template-columns: 30px 52px 1fr; align-items: center; gap: 8px;
|
||||||
text-align: left; min-height: 52px; padding: 8px 12px; font-size: 14px;
|
text-align: left; min-height: 52px; padding: 8px 12px; font-size: 14px;
|
||||||
}
|
}
|
||||||
|
.cue-stop { flex: 0 0 52px; font-size: 18px; padding: 0; color: var(--red); border-color: rgba(255, 61, 110, 0.5); }
|
||||||
|
.cue-stop:active { box-shadow: 0 0 18px rgba(255, 61, 110, 0.7); }
|
||||||
|
#scene-hint { margin: -4px 0 10px; }
|
||||||
|
#scene-card.halted .cues { opacity: 0.45; }
|
||||||
|
#btn-scene-stop.halted { color: var(--lime); border-color: var(--lime); }
|
||||||
.cue .ico { font-size: 20px; text-align: center; }
|
.cue .ico { font-size: 20px; text-align: center; }
|
||||||
.cue .at { font-family: var(--display); font-weight: 800; font-size: 11px; color: var(--cyan); }
|
.cue .at { font-family: var(--display); font-weight: 800; font-size: 11px; color: var(--cyan); }
|
||||||
.cue .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.cue .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|||||||
+2
-1
@@ -42,7 +42,8 @@
|
|||||||
|
|
||||||
<!-- scene of the current track -->
|
<!-- scene of the current track -->
|
||||||
<section class="card" id="scene-card" hidden>
|
<section class="card" id="scene-card" hidden>
|
||||||
<div class="cap">🎬 SCENA DEL BRANO <span class="cap-hint">tocca per lanciare ora</span></div>
|
<div class="cap">🎬 SCENA DEL BRANO <button class="mini" id="btn-scene-stop">■ FERMA SCENA</button></div>
|
||||||
|
<div class="cap-hint" id="scene-hint">tocca un elemento per lanciarlo ora · ■ per fermarlo</div>
|
||||||
<div id="scene" class="cues"></div>
|
<div id="scene" class="cues"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -37,6 +37,7 @@
|
|||||||
$('#btn-blackout').addEventListener('click', () => cmd('blackout', { on: !(st && st.blackout) }));
|
$('#btn-blackout').addEventListener('click', () => cmd('blackout', { on: !(st && st.blackout) }));
|
||||||
$('#btn-freeze').addEventListener('click', () => cmd('freeze', { on: !(st && st.freeze) }));
|
$('#btn-freeze').addEventListener('click', () => cmd('freeze', { on: !(st && st.freeze) }));
|
||||||
$('#btn-map').addEventListener('click', () => cmd('mapOn', { on: !(st && st.map.on) }));
|
$('#btn-map').addEventListener('click', () => cmd('mapOn', { on: !(st && st.map.on) }));
|
||||||
|
$('#btn-scene-stop').addEventListener('click', () => cmd(st && st.sceneHalted ? 'sceneResume' : 'sceneStop'));
|
||||||
$('#btn-allzones').addEventListener('click', () => cmd('mapShow', { ids: null }));
|
$('#btn-allzones').addEventListener('click', () => cmd('mapShow', { ids: null }));
|
||||||
$('#fam').addEventListener('change', (e) => { if (e.target.value !== '') cmd('fxFamily', { fam: parseInt(e.target.value, 10) }); });
|
$('#fam').addEventListener('change', (e) => { if (e.target.value !== '') cmd('fxFamily', { fam: parseInt(e.target.value, 10) }); });
|
||||||
|
|
||||||
@@ -63,14 +64,22 @@
|
|||||||
$('#scene-card').hidden = !sc.length;
|
$('#scene-card').hidden = !sc.length;
|
||||||
if (changed('scene', sc.map(c => [c.i, c.type, c.time, c.label]))) {
|
if (changed('scene', sc.map(c => [c.i, c.type, c.time, c.label]))) {
|
||||||
const ICO = { effect: '🌀', text: '🔤', image: '🖼', video: '🎞', map: '🗺' };
|
const ICO = { effect: '🌀', text: '🔤', image: '🖼', video: '🎞', map: '🗺' };
|
||||||
$('#scene').innerHTML = sc.map(c => '<button class="cue" data-i="' + c.i + '">' +
|
$('#scene').innerHTML = sc.map(c => '<div class="cue-wrap"><button class="cue" data-i="' + c.i + '">' +
|
||||||
'<span class="ico">' + (ICO[c.type] || '•') + '</span>' +
|
'<span class="ico">' + (ICO[c.type] || '•') + '</span>' +
|
||||||
'<span class="at">' + fmt(c.time) + '</span>' +
|
'<span class="at">' + fmt(c.time) + '</span>' +
|
||||||
'<span class="lbl">' + esc(c.label) + '</span></button>').join('');
|
'<span class="lbl">' + esc(c.label) + '</span></button>' +
|
||||||
|
'<button class="cue-stop" data-i="' + c.i + '" hidden aria-label="Ferma">■</button></div>').join('');
|
||||||
$('#scene').querySelectorAll('.cue').forEach(b => b.addEventListener('click', () => cmd('cue', { i: +b.dataset.i })));
|
$('#scene').querySelectorAll('.cue').forEach(b => b.addEventListener('click', () => cmd('cue', { i: +b.dataset.i })));
|
||||||
|
$('#scene').querySelectorAll('.cue-stop').forEach(b => b.addEventListener('click', () => cmd('cueStop', { i: +b.dataset.i })));
|
||||||
}
|
}
|
||||||
const liveSet = new Set(sc.filter(c => c.live).map(c => c.i));
|
const liveSet = new Set(sc.filter(c => c.live).map(c => c.i));
|
||||||
$('#scene').querySelectorAll('.cue').forEach(b => b.classList.toggle('live', liveSet.has(+b.dataset.i)));
|
$('#scene').querySelectorAll('.cue').forEach(b => b.classList.toggle('live', liveSet.has(+b.dataset.i)));
|
||||||
|
$('#scene').querySelectorAll('.cue-stop').forEach(b => { b.hidden = !liveSet.has(+b.dataset.i); });
|
||||||
|
$('#scene-card').classList.toggle('halted', !!s.sceneHalted);
|
||||||
|
$('#btn-scene-stop').classList.toggle('halted', !!s.sceneHalted);
|
||||||
|
$('#btn-scene-stop').textContent = s.sceneHalted ? '▶ RIPRENDI' : '■ FERMA SCENA';
|
||||||
|
$('#scene-hint').textContent = s.sceneHalted ? 'scena ferma: nessun elemento parte finché non riprendi'
|
||||||
|
: 'tocca un elemento per lanciarlo ora · ■ per fermarlo';
|
||||||
|
|
||||||
// effects
|
// effects
|
||||||
$('#fx-name').textContent = s.effect || '—';
|
$('#fx-name').textContent = s.effect || '—';
|
||||||
|
|||||||
Reference in new issue
Block a user