diff --git a/public/css/mobile.css b/public/css/mobile.css index 8d402f4..842f5db 100644 --- a/public/css/mobile.css +++ b/public/css/mobile.css @@ -37,6 +37,10 @@ button.wide { width: 100%; margin-top: 10px; font-size: 15px; } #audio-src { width: 100%; } button.danger { border-color: rgba(255,120,120,.5); color: #ffb3b3; background: rgba(255,80,80,.10); } #auto-vj.vj-on { background: #2e7d32; border-color: #4caf50; color: #fff; } +.av-fams { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; } +.av-fams.hidden { display: none; } +.av-fam { font-size: 11px; padding: 6px 10px; border-radius: 9px; opacity: .7; } +.av-fam.on { border-color: #4caf50; background: rgba(76,175,80,.18); color: #d9ffd9; opacity: 1; } button.danger.armed { background: #c0392b; color: #fff; border-color: #c0392b; animation: pulse 1s infinite; } @keyframes pulse { 50% { filter: brightness(1.35); } } input[type=range] { -webkit-appearance: none; appearance: none; background: transparent; padding: 0; height: 34px; border: none; } diff --git a/public/index.html b/public/index.html index 223c41e..8fab5ac 100644 --- a/public/index.html +++ b/public/index.html @@ -21,7 +21,15 @@
- +
+ + +
+
diff --git a/public/js/control.js b/public/js/control.js index bf373e8..096d334 100644 --- a/public/js/control.js +++ b/public/js/control.js @@ -22,13 +22,42 @@ let curEffect = EFFECTS.list[0]; function applyEffect(e) { curEffect = e; send({ type: 'effect', effect: e }); $('#now').textContent = e.name; renderFx(); } // 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 chip selection. Persisted on the device via saveState. let autoVjOn = false; -$('#auto-vj').addEventListener('click', () => send({ type: 'autoVj', on: !autoVjOn })); +let avCfg = { mode: 'smart', families: [] }; +function avSend(on) { send({ type: 'autoVj', on, mode: avCfg.mode, families: avCfg.families }); } +function avSave() { djv.saveState('autovj', avCfg); if (autoVjOn) avSend(true); } +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 b = document.createElement('button'); + b.className = 'av-fam' + (avCfg.families.includes(name) ? ' on' : ''); + b.textContent = name; + b.addEventListener('click', () => { + avCfg.families = avCfg.families.includes(name) + ? avCfg.families.filter((f) => f !== name) + : avCfg.families.concat(name); + b.classList.toggle('on'); + avSave(); + }); + box.appendChild(b); + }); +} +$('#av-mode').addEventListener('change', (e) => { avCfg.mode = e.target.value; renderAvFams(); avSave(); }); +djv.loadState('autovj').then((s) => { + if (s && s.mode) avCfg = { mode: s.mode, families: s.families || [] }; + $('#av-mode').value = avCfg.mode; renderAvFams(); +}); +$('#auto-vj').addEventListener('click', () => avSend(!autoVjOn)); function setAutoVj(m) { autoVjOn = !!m.on; const b = $('#auto-vj'); b.classList.toggle('vj-on', autoVjOn); - b.textContent = autoVjOn ? '🤖 Auto VJ ● attivo' : '🤖 Auto VJ'; + b.textContent = autoVjOn ? '🤖 Auto VJ ●' : '🤖 Auto VJ'; if (autoVjOn && m.name) $('#now').textContent = m.name + (m.bpm ? ' · ' + m.bpm + ' BPM' : ''); } function renderFx() { diff --git a/public/js/output.js b/public/js/output.js index 26e424a..ddad003 100644 --- a/public/js/output.js +++ b/public/js/output.js @@ -321,6 +321,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; @@ -498,10 +500,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;