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 = `
${message}
`; return; } shaderList.innerHTML = shaders.map(shader => `
${this.escapeHtml(shader.name)}
${this.escapeHtml(shader.code)}
`).join(''); } private escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Public methods for global access loadShader(id: string): void { const shaders = Storage.getShaders(); const shader = shaders.find(s => s.id === id); if (shader) { this.editor.value = shader.code; this.shader.setCode(shader.code); this.render(); Storage.updateShaderUsage(id); this.renderShaderLibrary(); // Refresh to update order } } deleteShader(id: string): void { Storage.deleteShader(id); this.renderShaderLibrary(); } startRename(id: string): void { const nameElement = document.getElementById(`name-${id}`); if (!nameElement) return; const shaders = Storage.getShaders(); const shader = shaders.find(s => s.id === id); if (!shader) return; // Replace span with input const input = document.createElement('input'); input.type = 'text'; input.value = shader.name; input.style.cssText = ` background: rgba(255, 255, 255, 0.2); border: 1px solid #666; color: #fff; padding: 2px 4px; border-radius: 2px; font-family: monospace; font-size: 12px; width: 100%; `; nameElement.replaceWith(input); input.focus(); input.select(); const finishRename = () => { const newName = input.value.trim(); if (newName && newName !== shader.name) { Storage.renameShader(id, newName); } this.renderShaderLibrary(); }; input.addEventListener('blur', finishRename); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); finishRename(); } else if (e.key === 'Escape') { e.preventDefault(); this.renderShaderLibrary(); } }); } renameShader(id: string): void { // This method is kept for backward compatibility but now uses startRename this.startRename(id); } private generateRandom(): void { const randomCode = FakeShader.generateRandomCode(); this.editor.value = randomCode; this.shader.setCode(randomCode); this.render(); } } const app = new BitfielderApp(); (window as any).app = app;