feat: UI sounds, live LFO visualization, wire fix, worlds 7-12, bug fixes
- Add procedural UI sound effects (connect/disconnect, engine start/stop, level complete/fail, star earned, hint, navigation) via Tone.js - Live LFO modulation visualization: knobs animate in real-time showing modulated value, ghost dot shows base value, number glows cyan - Fix wire recalculation on zoom/pan/level re-entry (post-layout refresh) - Fix retry button to keep current patch instead of reloading level - Fix default param detection: newly added modules now populate all default params so level checkers work without manual param changes - Add worlds 7-12: Secuencias y Ritmos, Texturas de Ruido, Síntesis Sustractiva, Espacio y Stereo, Técnicas Avanzadas, Gran Final (48 new levels, 144 new possible stars, 288 total stars) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { getModuleDef } from '../engine/moduleRegistry.js';
|
||||
import { state, removeModule, updateModuleParam, isPortConnected, emit } from '../engine/state.js';
|
||||
import { updateParam } from '../engine/audioEngine.js';
|
||||
@@ -15,6 +15,17 @@ const PORT_TO_PARAM = {
|
||||
vca: { cv: 'gain' },
|
||||
};
|
||||
|
||||
// Compute a simulated LFO waveform value at time t (seconds)
|
||||
function simulateLFO(waveform, phase) {
|
||||
switch (waveform) {
|
||||
case 'sine': return Math.sin(2 * Math.PI * phase);
|
||||
case 'triangle': return 1 - 4 * Math.abs((phase % 1) - 0.5);
|
||||
case 'sawtooth': return 2 * (phase % 1) - 1;
|
||||
case 'square': return (phase % 1) < 0.5 ? 1 : -1;
|
||||
default: return Math.sin(2 * Math.PI * phase);
|
||||
}
|
||||
}
|
||||
|
||||
export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }) {
|
||||
const def = getModuleDef(mod.type);
|
||||
if (!def) return null;
|
||||
@@ -33,6 +44,59 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Live LFO modulation visualization ====================
|
||||
const [liveValues, setLiveValues] = useState({});
|
||||
const rafRef = useRef(null);
|
||||
const startTimeRef = useRef(performance.now() / 1000);
|
||||
|
||||
useEffect(() => {
|
||||
if (modulatedParams.size === 0) {
|
||||
setLiveValues({});
|
||||
return;
|
||||
}
|
||||
|
||||
const tick = () => {
|
||||
const t = performance.now() / 1000 - startTimeRef.current;
|
||||
const newValues = {};
|
||||
|
||||
for (const conn of state.connections) {
|
||||
if (conn.to.moduleId !== mod.id) continue;
|
||||
const paramName = portMap[conn.to.port];
|
||||
if (!paramName) continue;
|
||||
|
||||
const srcMod = state.modules.find(m => m.id === conn.from.moduleId);
|
||||
if (!srcMod || srcMod.type !== 'lfo') continue;
|
||||
|
||||
// Read LFO params from state
|
||||
const lfoDef = getModuleDef('lfo');
|
||||
const lfoP = { ...Object.fromEntries(Object.entries(lfoDef.params).map(([k, v]) => [k, v.default])), ...srcMod.params };
|
||||
const freq = lfoP.frequency;
|
||||
const amp = lfoP.amplitude;
|
||||
const waveform = lfoP.waveform;
|
||||
const phase = (t * freq) % 1;
|
||||
const lfoVal = simulateLFO(waveform, phase) * amp;
|
||||
|
||||
// Compute modulated value (same scaling as audioEngine)
|
||||
const baseValue = params[paramName];
|
||||
let scale;
|
||||
if (mod.type === 'oscillator' && paramName === 'frequency') scale = baseValue * 0.5;
|
||||
else if (mod.type === 'filter' && paramName === 'frequency') scale = baseValue;
|
||||
else if (mod.type === 'vca' && paramName === 'gain') scale = 1;
|
||||
else scale = baseValue || 1;
|
||||
|
||||
newValues[paramName] = baseValue + lfoVal * scale;
|
||||
}
|
||||
|
||||
setLiveValues(newValues);
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
return () => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [mod.id, mod.type, modulatedParams.size]);
|
||||
|
||||
const handleParamChange = useCallback((name, value) => {
|
||||
updateModuleParam(mod.id, name, value);
|
||||
updateParam(mod.id, name, value);
|
||||
@@ -134,12 +198,17 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
||||
onChange={v => handleParamChange(name, v)}
|
||||
color={color}
|
||||
modulated={modulatedParams.has(name)}
|
||||
liveValue={liveValues[name]}
|
||||
/>
|
||||
<span className="param-value">
|
||||
{params[name] >= 1000 ? `${(params[name] / 1000).toFixed(1)}k` :
|
||||
params[name] >= 100 ? Math.round(params[name]) :
|
||||
params[name] >= 1 ? Number(params[name]).toFixed(1) :
|
||||
Number(params[name]).toFixed(3).replace(/0+$/, '').replace(/\.$/, '')}
|
||||
<span className={`param-value ${liveValues[name] !== undefined ? 'param-value-live' : ''}`}>
|
||||
{(() => {
|
||||
const v = liveValues[name] !== undefined ? liveValues[name] : params[name];
|
||||
const s = v >= 1000 ? `${(v / 1000).toFixed(1)}k` :
|
||||
v >= 100 ? Math.round(v) :
|
||||
v >= 1 ? Number(v).toFixed(1) :
|
||||
Number(v).toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
|
||||
return s;
|
||||
})()}
|
||||
{paramDef.unit ? ` ${paramDef.unit}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user