import { useEffect } from 'react'; import { useStore } from '@nanostores/react'; import { TopBar } from './TopBar'; import { MobileMenu } from './MobileMenu'; import { EditorPanel } from './EditorPanel'; import { ShaderLibrary } from './ShaderLibrary'; import { HelpPopup } from './HelpPopup'; import { WelcomePopup } from './WelcomePopup'; import { ShaderCanvas } from './ShaderCanvas'; import { PerformanceWarning } from './PerformanceWarning'; import { uiState, showUI } from '../stores/ui'; import { $appSettings, updateAppSettings, cycleValueMode, cycleRenderMode, handleTapTempo } from '../stores/appSettings'; import { $shader } from '../stores/shader'; import { loadShaders } from '../stores/library'; import { Storage } from '../Storage'; import { LucideIcon } from '../hooks/useLucideIcon'; export function App() { const ui = useStore(uiState); const settings = useStore($appSettings); const shader = useStore($shader); useEffect(() => { // Load initial settings from storage const savedSettings = Storage.getSettings(); $appSettings.set(savedSettings); // Load saved shaders loadShaders(); // Set CSS custom property for UI opacity document.documentElement.style.setProperty( '--ui-opacity', (settings.uiOpacity ?? 0.3).toString() ); }, []); useEffect(() => { // Update CSS custom property when opacity changes document.documentElement.style.setProperty( '--ui-opacity', (settings.uiOpacity ?? 0.3).toString() ); }, [settings.uiOpacity]); // Keyboard controls for hue shift and value mode when editor not focused useEffect(() => { let lastKeyTime = 0; const DEBOUNCE_DELAY = 150; // ms between key presses const handleKeyDown = (e: KeyboardEvent) => { // Only activate if editor is not focused and no control/meta/alt keys are pressed const editorElement = document.getElementById('editor') as HTMLTextAreaElement; const isEditorFocused = editorElement && document.activeElement === editorElement; if (isEditorFocused || e.ctrlKey || e.metaKey || e.altKey) { return; } // Debounce rapid key repeats const now = Date.now(); if (now - lastKeyTime < DEBOUNCE_DELAY) { e.preventDefault(); return; } lastKeyTime = now; switch (e.key) { case 'ArrowLeft': e.preventDefault(); // Decrease hue shift by 10 degrees (wrapping at 0) const currentHue = settings.hueShift ?? 0; const newHueLeft = currentHue - 10; updateAppSettings({ hueShift: newHueLeft < 0 ? 360 + newHueLeft : newHueLeft }); break; case 'ArrowRight': e.preventDefault(); // Increase hue shift by 10 degrees (wrapping at 360) const currentHueRight = settings.hueShift ?? 0; const newHueRight = (currentHueRight + 10) % 360; updateAppSettings({ hueShift: newHueRight }); break; case 'ArrowUp': e.preventDefault(); if (e.shiftKey) { // Shift + Up: Cycle to previous render mode (color palette) cycleRenderMode('backward'); } else { // Up: Cycle to previous value mode cycleValueMode('backward'); } break; case 'ArrowDown': e.preventDefault(); if (e.shiftKey) { // Shift + Down: Cycle to next render mode (color palette) cycleRenderMode('forward'); } else { // Down: Cycle to next value mode cycleValueMode('forward'); } break; case ' ': e.preventDefault(); // Spacebar: Tap tempo to control time speed handleTapTempo(); break; } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [settings.hueShift]); // Save settings changes to localStorage useEffect(() => { Storage.saveSettings({ resolution: settings.resolution, fps: settings.fps, renderMode: settings.renderMode, valueMode: settings.valueMode, uiOpacity: settings.uiOpacity, hueShift: settings.hueShift, timeSpeed: settings.timeSpeed, lastShaderCode: shader.code, }); }, [settings, shader.code]); return ( <> {ui.uiVisible ? ( <> {!ui.mobileMenuOpen && } ) : ( <> )} ); }