Auto VJ: selectable family pool (smart / all / custom checklist)
Three modes for where the director draws presets from: 'smart' keeps the curated per-mood pools, 'all' removes the family restriction, 'custom' limits picks to a user checklist (mood still shapes the speed variant, with a fallback when the checklist exhausts it). Config persists in localStorage and is re-pushed live while the director runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46afd5d318
commit
48ac3dfa85
4 files changed
+60
-4
No files matched your search
@@ -109,9 +109,15 @@
|
|||||||
<label>Libreria effetti <span id="effects-count"></span></label>
|
<label>Libreria effetti <span id="effects-count"></span></label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="auto-vj" title="L'Auto VJ ascolta il brano (energia, BPM, drop) e cambia effetto da solo a tempo di musica">🤖 Auto VJ</button>
|
<button id="auto-vj" title="L'Auto VJ ascolta il brano (energia, BPM, drop) e cambia effetto da solo a tempo di musica">🤖 Auto VJ</button>
|
||||||
|
<select id="av-mode" title="Da quali famiglie pesca l'Auto VJ">
|
||||||
|
<option value="smart">🎩 Smart (segue il mood)</option>
|
||||||
|
<option value="all">🌐 Tutte le famiglie</option>
|
||||||
|
<option value="custom">🎯 Solo famiglie scelte…</option>
|
||||||
|
</select>
|
||||||
<select id="effect-family"><option value="-1">Tutte le famiglie</option></select>
|
<select id="effect-family"><option value="-1">Tutte le famiglie</option></select>
|
||||||
<input type="text" id="effect-search" class="textfield" placeholder="Cerca effetto…" />
|
<input type="text" id="effect-search" class="textfield" placeholder="Cerca effetto…" />
|
||||||
</div>
|
</div>
|
||||||
|
<div id="av-fams" class="av-fams hidden"></div>
|
||||||
<div id="effect-list" class="effect-list"></div>
|
<div id="effect-list" class="effect-list"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+35
-1
@@ -17,8 +17,42 @@ function applyEffect(effect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto VJ: the output-side director picks presets in time with the music.
|
// Auto VJ: the output-side director picks presets in time with the music.
|
||||||
|
// avMode: 'smart' = curated mood pools, 'all' = every family, 'custom' = the
|
||||||
|
// user's checklist. Persisted in localStorage.
|
||||||
let autoVjOn = false;
|
let autoVjOn = false;
|
||||||
$('#auto-vj').addEventListener('click', () => send({ type: 'autoVj', on: !autoVjOn }));
|
let avCfg = { mode: 'smart', families: [] };
|
||||||
|
try { avCfg = Object.assign(avCfg, JSON.parse(localStorage.getItem('autovj') || '{}')); } catch (e) {}
|
||||||
|
|
||||||
|
function avSend(on) { send({ type: 'autoVj', on, mode: avCfg.mode, families: avCfg.families }); }
|
||||||
|
function avSave() {
|
||||||
|
localStorage.setItem('autovj', JSON.stringify(avCfg));
|
||||||
|
if (autoVjOn) avSend(true); // re-push the new config while running
|
||||||
|
}
|
||||||
|
function renderAvFams() {
|
||||||
|
const box = $('#av-fams');
|
||||||
|
box.classList.toggle('hidden', avCfg.mode !== 'custom');
|
||||||
|
if (avCfg.mode !== 'custom') return;
|
||||||
|
box.innerHTML = '';
|
||||||
|
EFFECTS.families.forEach((name) => {
|
||||||
|
const l = document.createElement('label');
|
||||||
|
l.className = 'av-fam' + (avCfg.families.includes(name) ? ' on' : '');
|
||||||
|
const c = document.createElement('input');
|
||||||
|
c.type = 'checkbox'; c.checked = avCfg.families.includes(name);
|
||||||
|
c.addEventListener('change', () => {
|
||||||
|
avCfg.families = c.checked
|
||||||
|
? avCfg.families.concat(name)
|
||||||
|
: avCfg.families.filter((f) => f !== name);
|
||||||
|
l.classList.toggle('on', c.checked);
|
||||||
|
avSave();
|
||||||
|
});
|
||||||
|
l.appendChild(c); l.appendChild(document.createTextNode(' ' + name));
|
||||||
|
box.appendChild(l);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('#av-mode').value = avCfg.mode;
|
||||||
|
$('#av-mode').addEventListener('change', (e) => { avCfg.mode = e.target.value; renderAvFams(); avSave(); });
|
||||||
|
renderAvFams();
|
||||||
|
$('#auto-vj').addEventListener('click', () => avSend(!autoVjOn));
|
||||||
|
|
||||||
// --- Library browser (hundreds of presets, filterable) ---
|
// --- Library browser (hundreds of presets, filterable) ---
|
||||||
const familySel = $('#effect-family');
|
const familySel = $('#effect-family');
|
||||||
|
|||||||
+14
-3
@@ -306,6 +306,8 @@ djv.onControl(async (m) => {
|
|||||||
break;
|
break;
|
||||||
case 'autoVj':
|
case 'autoVj':
|
||||||
autoVj = !!m.on;
|
autoVj = !!m.on;
|
||||||
|
if (m.mode) avMode = m.mode;
|
||||||
|
avFamilies = Array.isArray(m.families) ? m.families : avFamilies;
|
||||||
if (autoVj) { avLastSwitch = 0; avBeatMark = audio.beatCount || 0; }
|
if (autoVj) { avLastSwitch = 0; avBeatMark = audio.beatCount || 0; }
|
||||||
djv.report({ type: 'autoVj', on: autoVj });
|
djv.report({ type: 'autoVj', on: autoVj });
|
||||||
break;
|
break;
|
||||||
@@ -480,10 +482,19 @@ const AV_POOLS = {
|
|||||||
peak: ['Julia', 'Iperspazio', 'Tunnel', 'Moiré', 'Fluido Anello', 'Fluido Fuoco',
|
peak: ['Julia', 'Iperspazio', 'Tunnel', 'Moiré', 'Fluido Anello', 'Fluido Fuoco',
|
||||||
'Vortice', 'Griglia Neon', 'Cristalli']
|
'Vortice', 'Griglia Neon', 'Cristalli']
|
||||||
};
|
};
|
||||||
|
let avMode = 'smart', avFamilies = []; // 'smart' | 'all' | 'custom'
|
||||||
function avPick(mood) {
|
function avPick(mood) {
|
||||||
const fams = AV_POOLS[mood] || AV_POOLS.groove;
|
// Family pool: smart = curated per-mood lists; custom = the user's checklist
|
||||||
const cands = window.EFFECTS.list.filter(e => fams.indexOf(e.familyName) >= 0 &&
|
// (mood still shapes the speed variant); all = no family restriction.
|
||||||
(mood === 'peak' ? e.speed >= 1.0 : e.speed <= 1.4) && avRecent.indexOf(e.name) < 0);
|
let fams = null;
|
||||||
|
if (avMode === 'smart') fams = AV_POOLS[mood] || AV_POOLS.groove;
|
||||||
|
else if (avMode === 'custom' && avFamilies.length) fams = avFamilies;
|
||||||
|
const speedOk = (e) => (mood === 'peak' ? e.speed >= 1.0 : e.speed <= 1.4);
|
||||||
|
let cands = window.EFFECTS.list.filter(e => (!fams || fams.indexOf(e.familyName) >= 0) &&
|
||||||
|
speedOk(e) && avRecent.indexOf(e.name) < 0);
|
||||||
|
// A tight custom list can exhaust the speed filter: retry without it.
|
||||||
|
if (!cands.length) cands = window.EFFECTS.list.filter(e =>
|
||||||
|
(!fams || fams.indexOf(e.familyName) >= 0) && avRecent.indexOf(e.name) < 0);
|
||||||
const e = cands.length ? cands[Math.floor(Math.random() * cands.length)] : window.EFFECTS.list[0];
|
const e = cands.length ? cands[Math.floor(Math.random() * cands.length)] : window.EFFECTS.list[0];
|
||||||
avRecent = [e.name].concat(avRecent).slice(0, 6); // don't repeat the last few
|
avRecent = [e.name].concat(avRecent).slice(0, 6); // don't repeat the last few
|
||||||
return e;
|
return e;
|
||||||
|
|||||||
@@ -799,3 +799,8 @@ kbd {
|
|||||||
|
|
||||||
/* Auto VJ toggle */
|
/* Auto VJ toggle */
|
||||||
#auto-vj.vj-on { background: #2e7d32; border-color: #4caf50; color: #fff; }
|
#auto-vj.vj-on { background: #2e7d32; border-color: #4caf50; color: #fff; }
|
||||||
|
.av-fams { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 10px; }
|
||||||
|
.av-fams.hidden { display: none; }
|
||||||
|
.av-fam { font-size: 11px; padding: 4px 8px; border: 1px solid #444; border-radius: 8px; cursor: pointer; opacity: .75; }
|
||||||
|
.av-fam.on { border-color: #4caf50; background: rgba(76,175,80,.15); opacity: 1; }
|
||||||
|
.av-fam input { display: none; }
|
||||||
Reference in new issue
Block a user