switching
This commit is contained in:
282
src/components/TopBar.tsx
Normal file
282
src/components/TopBar.tsx
Normal file
@ -0,0 +1,282 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $appSettings, updateAppSettings } from '../stores/appSettings';
|
||||
import { VALUE_MODES, ValueMode } from '../utils/constants';
|
||||
import {
|
||||
uiState,
|
||||
toggleMobileMenu,
|
||||
showHelp,
|
||||
toggleUI,
|
||||
toggleShaderLibrary,
|
||||
} from '../stores/ui';
|
||||
import { $shader, setShaderCode } from '../stores/shader';
|
||||
import { $input } from '../stores/input';
|
||||
import { FakeShader } from '../FakeShader';
|
||||
import { useAudio } from '../hooks/useAudio';
|
||||
import { LucideIcon } from '../hooks/useLucideIcon';
|
||||
|
||||
function getValueModeLabel(mode: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
integer: 'Integer (0-255)',
|
||||
float: 'Float (0.0-1.0)',
|
||||
polar: 'Polar (angle-based)',
|
||||
distance: 'Distance (radial)',
|
||||
wave: 'Wave (ripple)',
|
||||
fractal: 'Fractal (recursive)',
|
||||
cellular: 'Cellular (automata)',
|
||||
noise: 'Noise (perlin-like)',
|
||||
warp: 'Warp (space deformation)',
|
||||
flow: 'Flow (fluid dynamics)',
|
||||
};
|
||||
return labels[mode] || mode;
|
||||
}
|
||||
|
||||
export function TopBar() {
|
||||
const settings = useStore($appSettings);
|
||||
const ui = useStore(uiState);
|
||||
const shader = useStore($shader);
|
||||
const input = useStore($input);
|
||||
const { setupAudio, disableAudio } = useAudio();
|
||||
|
||||
const handleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRandom = () => {
|
||||
const randomCode = FakeShader.generateRandomCode();
|
||||
setShaderCode(randomCode);
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
const shareData = {
|
||||
code: shader.code,
|
||||
resolution: settings.resolution,
|
||||
fps: settings.fps,
|
||||
renderMode: settings.renderMode,
|
||||
valueMode: settings.valueMode,
|
||||
uiOpacity: settings.uiOpacity,
|
||||
};
|
||||
|
||||
const encoded = btoa(JSON.stringify(shareData));
|
||||
window.location.hash = encoded;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(window.location.href)
|
||||
.then(() => {
|
||||
console.log('URL copied to clipboard');
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('Copy failed');
|
||||
});
|
||||
};
|
||||
|
||||
const handleExportPNG = () => {
|
||||
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
const link = document.createElement('a');
|
||||
link.download = `bitfielder-${Date.now()}.png`;
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAudioToggle = async () => {
|
||||
if (input.audioEnabled) {
|
||||
disableAudio();
|
||||
} else {
|
||||
await setupAudio();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="topbar" className={ui.uiVisible ? '' : 'hidden'}>
|
||||
<div className="title">Bitfielder</div>
|
||||
<div className="controls">
|
||||
<div className="controls-desktop">
|
||||
<label
|
||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
||||
>
|
||||
Resolution:
|
||||
<select
|
||||
value={settings.resolution}
|
||||
onChange={(e) =>
|
||||
updateAppSettings({ resolution: parseInt(e.target.value) })
|
||||
}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid #555',
|
||||
color: '#fff',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<option value="1">Full (1x)</option>
|
||||
<option value="2">Half (2x)</option>
|
||||
<option value="4">Quarter (4x)</option>
|
||||
<option value="8">Eighth (8x)</option>
|
||||
<option value="16">Sixteenth (16x)</option>
|
||||
<option value="32">Thirty-second (32x)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label
|
||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
||||
>
|
||||
FPS:
|
||||
<select
|
||||
value={settings.fps}
|
||||
onChange={(e) =>
|
||||
updateAppSettings({ fps: parseInt(e.target.value) })
|
||||
}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid #555',
|
||||
color: '#fff',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<option value="15">15 FPS</option>
|
||||
<option value="30">30 FPS</option>
|
||||
<option value="60">60 FPS</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label
|
||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
||||
>
|
||||
Value Mode:
|
||||
<select
|
||||
value={settings.valueMode}
|
||||
onChange={(e) => updateAppSettings({ valueMode: e.target.value as ValueMode })}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid #555',
|
||||
color: '#fff',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
{VALUE_MODES.map((mode) => (
|
||||
<option key={mode} value={mode}>
|
||||
{getValueModeLabel(mode)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label
|
||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
||||
>
|
||||
Render Mode:
|
||||
<select
|
||||
value={settings.renderMode}
|
||||
onChange={(e) =>
|
||||
updateAppSettings({ renderMode: e.target.value })
|
||||
}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid #555',
|
||||
color: '#fff',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<option value="classic">Classic</option>
|
||||
<option value="grayscale">Grayscale</option>
|
||||
<option value="red">Red Channel</option>
|
||||
<option value="green">Green Channel</option>
|
||||
<option value="blue">Blue Channel</option>
|
||||
<option value="rgb">RGB Split</option>
|
||||
<option value="hsv">HSV</option>
|
||||
<option value="rainbow">Rainbow</option>
|
||||
<option value="thermal">Thermal</option>
|
||||
<option value="neon">Neon</option>
|
||||
<option value="cyberpunk">Cyberpunk</option>
|
||||
<option value="vaporwave">Vaporwave</option>
|
||||
<option value="dithered">Dithered</option>
|
||||
<option value="palette">Palette</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label
|
||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
||||
>
|
||||
UI Opacity:
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="100"
|
||||
value={Math.round((settings.uiOpacity ?? 0.3) * 100)}
|
||||
onChange={(e) =>
|
||||
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
|
||||
}
|
||||
style={{ width: '80px', verticalAlign: 'middle' }}
|
||||
/>
|
||||
<span style={{ fontSize: '11px' }}>
|
||||
{Math.round((settings.uiOpacity ?? 0.3) * 100)}%
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button id="help-btn" onClick={showHelp}>
|
||||
<LucideIcon name="help" />
|
||||
</button>
|
||||
<button id="fullscreen-btn" onClick={handleFullscreen}>
|
||||
<LucideIcon name="fullscreen" />
|
||||
</button>
|
||||
<button id="hide-ui-btn" onClick={toggleUI}>
|
||||
<LucideIcon name="hide" />
|
||||
</button>
|
||||
<button id="random-btn" onClick={handleRandom}>
|
||||
<LucideIcon name="random" />
|
||||
</button>
|
||||
<button id="audio-btn" onClick={handleAudioToggle}>
|
||||
<LucideIcon
|
||||
name={input.audioEnabled ? 'microphone' : 'microphone-off'}
|
||||
/>
|
||||
</button>
|
||||
<button id="share-btn" onClick={handleShare}>
|
||||
<LucideIcon name="share" />
|
||||
</button>
|
||||
<button id="export-png-btn" onClick={handleExportPNG}>
|
||||
<LucideIcon name="export" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="controls-mobile">
|
||||
<button
|
||||
className="icon-button"
|
||||
aria-label="Shader Library"
|
||||
onClick={toggleShaderLibrary}
|
||||
>
|
||||
<LucideIcon name="library" />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button"
|
||||
aria-label="Random"
|
||||
onClick={handleRandom}
|
||||
>
|
||||
<LucideIcon name="random" />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button"
|
||||
aria-label="Hide UI"
|
||||
onClick={toggleUI}
|
||||
>
|
||||
<LucideIcon name="hide" />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button"
|
||||
aria-label="Menu"
|
||||
onClick={toggleMobileMenu}
|
||||
>
|
||||
<LucideIcon name={ui.mobileMenuOpen ? 'close' : 'menu'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user