fix: LFO→cutoff modulation, visual knob feedback, persistent hints

- Fix LFO→Filter cutoff: add scaling Gain nodes so LFO (-1..1) maps to
  meaningful Hz modulation (±cutoff value). Same fix for LFO→Osc freq.
  Mod scale updates dynamically when user changes the base param value.
- Visual modulation indicator: knobs receiving LFO/modulation show a
  pulsing dashed ring animation (spin + pulse) around the knob arc
- Persist hint usage per level: using a hint permanently caps that level
  at 2 stars — survives reload/restart. No more cheating by restarting!
- Hint state stored in separate localStorage key (synthquest-hints)
- Admin reset also clears hint history

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 02:44:28 +01:00
parent c4a2cb3cef
commit f0100eb64f
7 changed files with 120 additions and 15 deletions

View File

@@ -8,6 +8,13 @@ import KeyboardWidget from './KeyboardWidget.jsx';
import SequencerWidget from './SequencerWidget.jsx';
import PianoRollWidget from './PianoRollWidget.jsx';
// Map input port names → the param name they modulate (for visual feedback)
const PORT_TO_PARAM = {
filter: { cutoff: 'frequency' },
oscillator: { freq: 'frequency', detune: 'detune' },
vca: { cv: 'gain' },
};
export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }) {
const def = getModuleDef(mod.type);
if (!def) return null;
@@ -17,6 +24,15 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
// Merge default params
const params = { ...Object.fromEntries(Object.entries(def.params).map(([k, v]) => [k, v.default])), ...mod.params };
// Find which params are being modulated (have an incoming connection on their corresponding port)
const modulatedParams = new Set();
const portMap = PORT_TO_PARAM[mod.type] || {};
for (const conn of state.connections) {
if (conn.to.moduleId === mod.id && portMap[conn.to.port]) {
modulatedParams.add(portMap[conn.to.port]);
}
}
const handleParamChange = useCallback((name, value) => {
updateModuleParam(mod.id, name, value);
updateParam(mod.id, name, value);
@@ -117,6 +133,7 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
max={paramDef.max}
onChange={v => handleParamChange(name, v)}
color={color}
modulated={modulatedParams.has(name)}
/>
<span className="param-value">
{params[name] >= 1000 ? `${(params[name] / 1000).toFixed(1)}k` :