import { FakeShader } from './FakeShader'; import { Storage } from './Storage'; class BitfielderApp { private shader: FakeShader; private canvas: HTMLCanvasElement; private editor: HTMLTextAreaElement; private isAnimating: boolean = false; private uiVisible: boolean = true; private performanceWarning: HTMLElement; private libraryOpen: boolean = false; constructor() { this.canvas = document.getElementById('canvas') as HTMLCanvasElement; this.editor = document.getElementById('editor') as HTMLTextAreaElement; this.performanceWarning = document.getElementById('performance-warning') as HTMLElement; this.loadSettings(); this.setupCanvas(); this.shader = new FakeShader(this.canvas, this.editor.value); this.setupEventListeners(); this.loadFromURL(); this.renderShaderLibrary(); this.render(); window.addEventListener('resize', () => this.setupCanvas()); window.addEventListener('beforeunload', () => this.saveCurrentShader()); } private setupCanvas(): void { // Calculate the actual available space const width = window.innerWidth; const height = this.uiVisible ? window.innerHeight - 180 : // subtract topbar (40px) and editor panel (140px) window.innerHeight; // full height when UI is hidden // Get resolution scale from dropdown const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement; const scale = parseInt(resolutionSelect.value) || 1; // Set canvas internal size with resolution scaling this.canvas.width = Math.floor(width / scale); this.canvas.height = Math.floor(height / scale); console.log(`Canvas setup: ${this.canvas.width}x${this.canvas.height} (scale: ${scale}x), UI visible: ${this.uiVisible}`); } private setupEventListeners(): void { const helpBtn = document.getElementById('help-btn')!; const fullscreenBtn = document.getElementById('fullscreen-btn')!; const hideUiBtn = document.getElementById('hide-ui-btn')!; const showUiBtn = document.getElementById('show-ui-btn')!; const randomBtn = document.getElementById('random-btn')!; const shareBtn = document.getElementById('share-btn')!; const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement; const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement; const helpPopup = document.getElementById('help-popup')!; const closeBtn = helpPopup.querySelector('.close-btn')!; // Library elements const saveShaderBtn = document.getElementById('save-shader-btn')!; const shaderNameInput = document.getElementById('shader-name-input') as HTMLInputElement; const shaderSearchInput = document.getElementById('shader-search-input') as HTMLInputElement; helpBtn.addEventListener('click', () => this.showHelp()); fullscreenBtn.addEventListener('click', () => this.toggleFullscreen()); hideUiBtn.addEventListener('click', () => this.toggleUI()); showUiBtn.addEventListener('click', () => this.showUI()); randomBtn.addEventListener('click', () => this.generateRandom()); shareBtn.addEventListener('click', () => this.shareURL()); resolutionSelect.addEventListener('change', () => this.updateResolution()); fpsSelect.addEventListener('change', () => this.updateFPS()); closeBtn.addEventListener('click', () => this.hideHelp()); // Library events saveShaderBtn.addEventListener('click', () => this.saveCurrentShader()); shaderNameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { this.saveCurrentShader(); } }); shaderSearchInput.addEventListener('input', () => this.renderShaderLibrary()); // Close help popup when clicking outside helpPopup.addEventListener('click', (e) => { if (e.target === helpPopup) { this.hideHelp(); } }); this.editor.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); this.shader.setCode(this.editor.value); this.render(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'F11') { e.preventDefault(); this.toggleFullscreen(); } else if (e.key === 'h' || e.key === 'H') { if (!this.editor.matches(':focus')) { this.toggleUI(); } } else if (e.key === 'r' || e.key === 'R') { if (!this.editor.matches(':focus')) { this.generateRandom(); } } else if (e.key === 's' || e.key === 'S') { if (!this.editor.matches(':focus')) { this.shareURL(); } } else if (e.key === '?') { if (!this.editor.matches(':focus')) { this.showHelp(); } } else if (e.key === 'Escape') { this.hideHelp(); this.showUI(); } }); window.addEventListener('hashchange', () => this.loadFromURL()); // Listen for performance warnings window.addEventListener('message', (e) => { if (e.data === 'performance-warning') { this.showPerformanceWarning(); } }); } private render(): void { const hasTime = this.editor.value.includes('t'); if (hasTime && !this.isAnimating) { this.isAnimating = true; this.shader.startAnimation(); } else if (!hasTime && this.isAnimating) { this.isAnimating = false; this.shader.stopAnimation(); this.shader.render(false); } else if (!hasTime) { this.shader.render(false); } } private toggleFullscreen(): void { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } else { document.exitFullscreen(); } } private toggleUI(): void { this.uiVisible = !this.uiVisible; const topbar = document.getElementById('topbar')!; const editorPanel = document.getElementById('editor-panel')!; const editor = document.getElementById('editor')!; const showUiBtn = document.getElementById('show-ui-btn')!; if (this.uiVisible) { // Show full UI topbar.classList.remove('hidden'); editorPanel.classList.remove('minimal'); editor.classList.remove('minimal'); showUiBtn.style.display = 'none'; } else { // Hide topbar, make editor minimal topbar.classList.add('hidden'); editorPanel.classList.add('minimal'); editor.classList.add('minimal'); showUiBtn.style.display = 'block'; } // Recalculate canvas size when UI is hidden/shown this.setupCanvas(); } private showUI(): void { this.uiVisible = true; const topbar = document.getElementById('topbar')!; const editorPanel = document.getElementById('editor-panel')!; const editor = document.getElementById('editor')!; const showUiBtn = document.getElementById('show-ui-btn')!; topbar.classList.remove('hidden'); editorPanel.classList.remove('minimal'); editor.classList.remove('minimal'); showUiBtn.style.display = 'none'; // Recalculate canvas size when UI is shown this.setupCanvas(); } private showHelp(): void { const helpPopup = document.getElementById('help-popup')!; helpPopup.style.display = 'block'; } private hideHelp(): void { const helpPopup = document.getElementById('help-popup')!; helpPopup.style.display = 'none'; } private showPerformanceWarning(): void { this.performanceWarning.style.display = 'block'; setTimeout(() => { this.performanceWarning.style.display = 'none'; }, 3000); } private shareURL(): void { const encoded = btoa(this.editor.value); window.location.hash = encoded; navigator.clipboard.writeText(window.location.href).then(() => { console.log('URL copied to clipboard'); }).catch(() => { console.log('Copy failed'); }); } private loadFromURL(): void { if (window.location.hash) { try { const decoded = atob(window.location.hash.substring(1)); this.editor.value = decoded; this.shader.setCode(decoded); this.render(); } catch (e) { console.error('Failed to decode URL hash:', e); } } } private updateResolution(): void { this.setupCanvas(); Storage.saveSettings({ resolution: parseInt((document.getElementById('resolution-select') as HTMLSelectElement).value) }); } private updateFPS(): void { const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement; const fps = parseInt(fpsSelect.value); this.shader.setTargetFPS(fps); Storage.saveSettings({ fps }); } private loadSettings(): void { const settings = Storage.getSettings(); // Apply settings to UI (document.getElementById('resolution-select') as HTMLSelectElement).value = settings.resolution.toString(); (document.getElementById('fps-select') as HTMLSelectElement).value = settings.fps.toString(); // Load last shader code if no URL hash if (!window.location.hash) { this.editor.value = settings.lastShaderCode; } } private saveCurrentShader(): void { const nameInput = document.getElementById('shader-name-input') as HTMLInputElement; const name = nameInput.value.trim(); const code = this.editor.value.trim(); if (!code) return; Storage.saveShader(name, code); nameInput.value = ''; this.renderShaderLibrary(); // Save as last used shader Storage.saveSettings({ lastShaderCode: code }); } private renderShaderLibrary(): void { const shaderList = document.getElementById('shader-list')!; const searchInput = document.getElementById('shader-search-input') as HTMLInputElement; const searchTerm = searchInput.value.toLowerCase().trim(); let shaders = Storage.getShaders(); // Filter by search term if (searchTerm) { shaders = shaders.filter(shader => shader.name.toLowerCase().includes(searchTerm) || shader.code.toLowerCase().includes(searchTerm) ); } if (shaders.length === 0) { const message = searchTerm ? 'No shaders match your search' : 'No saved shaders'; shaderList.innerHTML = `