From 48ac3dfa85707d1e55c3b21171b1e391b8ce7a26 Mon Sep 17 00:00:00 2001 From: luciano Date: Fri, 17 Jul 2026 15:37:25 +0200 Subject: [PATCH] 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 --- src/control.html | 6 ++++++ src/control.js | 36 +++++++++++++++++++++++++++++++++++- src/output.js | 17 ++++++++++++++--- src/style.css | 5 +++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/control.html b/src/control.html index 846c2ed..2b6df82 100644 --- a/src/control.html +++ b/src/control.html @@ -109,9 +109,15 @@
+
+
diff --git a/src/control.js b/src/control.js index 472ca23..5c0fde7 100644 --- a/src/control.js +++ b/src/control.js @@ -17,8 +17,42 @@ function applyEffect(effect) { } // 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; -$('#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) --- const familySel = $('#effect-family'); diff --git a/src/output.js b/src/output.js index 0d614ab..ffbc2dc 100644 --- a/src/output.js +++ b/src/output.js @@ -306,6 +306,8 @@ djv.onControl(async (m) => { break; case 'autoVj': 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; } djv.report({ type: 'autoVj', on: autoVj }); break; @@ -480,10 +482,19 @@ const AV_POOLS = { peak: ['Julia', 'Iperspazio', 'Tunnel', 'Moiré', 'Fluido Anello', 'Fluido Fuoco', 'Vortice', 'Griglia Neon', 'Cristalli'] }; +let avMode = 'smart', avFamilies = []; // 'smart' | 'all' | 'custom' function avPick(mood) { - const fams = AV_POOLS[mood] || AV_POOLS.groove; - const cands = window.EFFECTS.list.filter(e => fams.indexOf(e.familyName) >= 0 && - (mood === 'peak' ? e.speed >= 1.0 : e.speed <= 1.4) && avRecent.indexOf(e.name) < 0); + // Family pool: smart = curated per-mood lists; custom = the user's checklist + // (mood still shapes the speed variant); all = no family restriction. + 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]; avRecent = [e.name].concat(avRecent).slice(0, 6); // don't repeat the last few return e; diff --git a/src/style.css b/src/style.css index 69c1e13..e3c8b55 100644 --- a/src/style.css +++ b/src/style.css @@ -799,3 +799,8 @@ kbd { /* Auto VJ toggle */ #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; }