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:
@@ -19,14 +19,17 @@ function describeArc(cx, cy, r, startDeg, endDeg) {
|
||||
return `M ${start.x} ${start.y} A ${r} ${r} 0 ${large} 0 ${end.x} ${end.y}`;
|
||||
}
|
||||
|
||||
export default function Knob({ value, min, max, onChange, color = 'var(--accent)', label, unit, formatValue, modulated = false }) {
|
||||
export default function Knob({ value, min, max, onChange, color = 'var(--accent)', label, unit, formatValue, modulated = false, liveValue }) {
|
||||
const ref = useRef(null);
|
||||
const dragRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editText, setEditText] = useState('');
|
||||
|
||||
const norm = Math.max(0, Math.min(1, (value - min) / (max - min)));
|
||||
// Use liveValue for visual display when being modulated, base value for interaction
|
||||
const displayNum = liveValue !== undefined ? liveValue : value;
|
||||
const clampedDisplay = Math.max(min, Math.min(max, displayNum));
|
||||
const norm = Math.max(0, Math.min(1, (clampedDisplay - min) / (max - min)));
|
||||
const angleDeg = START_ANGLE - norm * RANGE;
|
||||
|
||||
const cx = SIZE / 2, cy = SIZE / 2;
|
||||
@@ -36,11 +39,16 @@ export default function Knob({ value, min, max, onChange, color = 'var(--accent)
|
||||
|
||||
const dotPos = polarToCart(cx, cy, RADIUS - 4, angleDeg);
|
||||
|
||||
const displayVal = formatValue ? formatValue(value) :
|
||||
value >= 1000 ? `${(value / 1000).toFixed(1)}k` :
|
||||
value >= 100 ? Math.round(value) :
|
||||
value >= 1 ? value.toFixed(1) :
|
||||
value.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
|
||||
// Also show base value indicator when modulated
|
||||
const baseNorm = Math.max(0, Math.min(1, (value - min) / (max - min)));
|
||||
const baseAngle = START_ANGLE - baseNorm * RANGE;
|
||||
const baseDotPos = polarToCart(cx, cy, RADIUS - 4, baseAngle);
|
||||
|
||||
const displayVal = formatValue ? formatValue(displayNum) :
|
||||
displayNum >= 1000 ? `${(displayNum / 1000).toFixed(1)}k` :
|
||||
displayNum >= 100 ? Math.round(displayNum) :
|
||||
displayNum >= 1 ? displayNum.toFixed(1) :
|
||||
displayNum.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
|
||||
|
||||
const handlePointerDown = useCallback((e) => {
|
||||
if (editing) return;
|
||||
@@ -131,6 +139,10 @@ export default function Knob({ value, min, max, onChange, color = 'var(--accent)
|
||||
)}
|
||||
<path className="knob-track" d={trackPath} />
|
||||
{fillPath && <path className="knob-fill" d={fillPath} style={{ stroke: color }} />}
|
||||
{/* Ghost dot at base value position when modulated */}
|
||||
{liveValue !== undefined && (
|
||||
<circle className="knob-base-dot" cx={baseDotPos.x} cy={baseDotPos.y} r={1.5} />
|
||||
)}
|
||||
<circle className="knob-dot" cx={dotPos.x} cy={dotPos.y} r={2} />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user