166 lines
4.9 KiB
TypeScript
166 lines
4.9 KiB
TypeScript
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 (
|
|
<>
|
|
<ShaderCanvas />
|
|
|
|
{ui.uiVisible ? (
|
|
<>
|
|
<TopBar />
|
|
{!ui.mobileMenuOpen && <EditorPanel />}
|
|
</>
|
|
) : (
|
|
<>
|
|
<button
|
|
id="show-ui-btn"
|
|
onClick={showUI}
|
|
style={{ display: 'block' }}
|
|
>
|
|
<LucideIcon name="show" />
|
|
</button>
|
|
<EditorPanel minimal={true} />
|
|
</>
|
|
)}
|
|
|
|
<MobileMenu />
|
|
<ShaderLibrary />
|
|
<HelpPopup />
|
|
<WelcomePopup />
|
|
<PerformanceWarning />
|
|
</>
|
|
);
|
|
}
|