better responsiveness

This commit is contained in:
2025-07-05 18:42:17 +02:00
parent bacc6f0325
commit 7df2b49c26
5 changed files with 476 additions and 52 deletions

42
src/icons.ts Normal file
View File

@ -0,0 +1,42 @@
import { createIcons, icons } from 'lucide';
export function createIcon(name: string, size: number = 16): string {
const iconMap: Record<string, string> = {
'menu': 'menu',
'close': 'x',
'help': 'help-circle',
'fullscreen': 'maximize-2',
'show': 'eye',
'hide': 'eye-off',
'random': 'dice-3',
'share': 'share-2',
'export': 'download',
'play': 'play',
'settings': 'settings',
'resolution': 'monitor',
'fps': 'zap',
'palette': 'palette',
'library': 'book-open'
};
const iconName = iconMap[name];
if (!iconName) return '';
return `<i data-lucide="${iconName}" width="${size}" height="${size}" stroke-width="2"></i>`;
}
export function addIconToButton(button: HTMLElement, iconName: string, keepText: boolean = false): void {
const originalText = button.textContent || '';
const iconHtml = createIcon(iconName);
if (keepText) {
button.innerHTML = iconHtml + ' ' + originalText;
} else {
button.innerHTML = iconHtml;
button.setAttribute('aria-label', originalText);
}
}
export function initializeLucideIcons(): void {
createIcons({ icons });
}

View File

@ -1,5 +1,6 @@
import { FakeShader } from './FakeShader';
import { Storage } from './Storage';
import { addIconToButton, createIcon, initializeLucideIcons } from './icons';
class BitfielderApp {
private shader: FakeShader;
@ -21,6 +22,7 @@ class BitfielderApp {
this.shader = new FakeShader(this.canvas, this.editor.value);
this.setupEventListeners();
this.initializeIcons();
this.loadFromURL();
this.renderShaderLibrary();
this.render();
@ -59,11 +61,29 @@ class BitfielderApp {
const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
const renderModeSelect = document.getElementById('render-mode-select') as HTMLSelectElement;
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
const opacityValue = document.getElementById('opacity-value')!;
const evalBtn = document.getElementById('eval-btn')!;
const helpPopup = document.getElementById('help-popup')!;
const closeBtn = helpPopup.querySelector('.close-btn')!;
// Mobile elements
const hamburgerMenu = document.getElementById('hamburger-menu')!;
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay')!;
const randomBtnMobile = document.getElementById('random-btn-mobile')!;
const hideUiBtnMobile = document.getElementById('hide-ui-btn-mobile')!;
const libraryBtnMobile = document.getElementById('library-btn-mobile')!;
// Mobile menu controls
const resolutionSelectMobile = document.getElementById('resolution-select-mobile') as HTMLSelectElement;
const fpsSelectMobile = document.getElementById('fps-select-mobile') as HTMLSelectElement;
const renderModeSelectMobile = document.getElementById('render-mode-select-mobile') as HTMLSelectElement;
const opacitySliderMobile = document.getElementById('opacity-slider-mobile') as HTMLInputElement;
// Mobile menu buttons
const helpBtnMobile = document.getElementById('help-btn-mobile')!;
const fullscreenBtnMobile = document.getElementById('fullscreen-btn-mobile')!;
const shareBtnMobile = document.getElementById('share-btn-mobile')!;
const exportPngBtnMobile = document.getElementById('export-png-btn-mobile')!;
// Library elements
const saveShaderBtn = document.getElementById('save-shader-btn')!;
const shaderNameInput = document.getElementById('shader-name-input') as HTMLInputElement;
@ -92,6 +112,48 @@ class BitfielderApp {
});
shaderSearchInput.addEventListener('input', () => this.renderShaderLibrary());
// Mobile event listeners
hamburgerMenu.addEventListener('click', () => this.toggleMobileMenu());
mobileMenuOverlay.addEventListener('click', () => this.closeMobileMenu());
randomBtnMobile.addEventListener('click', () => this.generateRandom());
hideUiBtnMobile.addEventListener('click', () => this.toggleUI());
libraryBtnMobile.addEventListener('click', () => this.toggleShaderLibrary());
// Mobile menu controls sync with desktop
resolutionSelectMobile.addEventListener('change', () => {
resolutionSelect.value = resolutionSelectMobile.value;
this.updateResolution();
});
fpsSelectMobile.addEventListener('change', () => {
fpsSelect.value = fpsSelectMobile.value;
this.updateFPS();
});
renderModeSelectMobile.addEventListener('change', () => {
renderModeSelect.value = renderModeSelectMobile.value;
this.updateRenderMode();
});
opacitySliderMobile.addEventListener('input', () => {
opacitySlider.value = opacitySliderMobile.value;
this.updateUIOpacity();
});
// Mobile menu buttons
helpBtnMobile.addEventListener('click', () => {
this.closeMobileMenu();
this.showHelp();
});
fullscreenBtnMobile.addEventListener('click', () => {
this.closeMobileMenu();
this.toggleFullscreen();
});
shareBtnMobile.addEventListener('click', () => {
this.closeMobileMenu();
this.shareURL();
});
exportPngBtnMobile.addEventListener('click', () => {
this.closeMobileMenu();
this.exportPNG();
});
// Close help popup when clicking outside
helpPopup.addEventListener('click', (e) => {
@ -285,13 +347,83 @@ class BitfielderApp {
private updateUIOpacity(): void {
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
const opacityValue = document.getElementById('opacity-value')!;
const opacityValueMobile = document.getElementById('opacity-value-mobile')!;
const opacity = parseInt(opacitySlider.value) / 100;
document.documentElement.style.setProperty('--ui-opacity', opacity.toString());
opacityValue.textContent = `${opacitySlider.value}%`;
opacityValueMobile.textContent = `${opacitySlider.value}%`;
Storage.saveSettings({ uiOpacity: opacity });
}
private initializeIcons(): void {
// Desktop buttons
addIconToButton(document.getElementById('help-btn')!, 'help');
addIconToButton(document.getElementById('fullscreen-btn')!, 'fullscreen');
addIconToButton(document.getElementById('hide-ui-btn')!, 'hide');
addIconToButton(document.getElementById('random-btn')!, 'random');
addIconToButton(document.getElementById('share-btn')!, 'share');
addIconToButton(document.getElementById('export-png-btn')!, 'export');
// Mobile buttons
addIconToButton(document.getElementById('hamburger-menu')!, 'menu');
addIconToButton(document.getElementById('library-btn-mobile')!, 'library');
addIconToButton(document.getElementById('random-btn-mobile')!, 'random');
addIconToButton(document.getElementById('hide-ui-btn-mobile')!, 'hide');
addIconToButton(document.getElementById('show-ui-btn')!, 'show');
// Mobile menu buttons with text
const helpIcon = document.querySelector('#help-btn-mobile .icon') as HTMLElement;
const fullscreenIcon = document.querySelector('#fullscreen-btn-mobile .icon') as HTMLElement;
const shareIcon = document.querySelector('#share-btn-mobile .icon') as HTMLElement;
const exportIcon = document.querySelector('#export-png-btn-mobile .icon') as HTMLElement;
if (helpIcon) helpIcon.innerHTML = createIcon('help');
if (fullscreenIcon) fullscreenIcon.innerHTML = createIcon('fullscreen');
if (shareIcon) shareIcon.innerHTML = createIcon('share');
if (exportIcon) exportIcon.innerHTML = createIcon('export');
// Initialize all Lucide icons
initializeLucideIcons();
}
private toggleMobileMenu(): void {
const mobileMenu = document.getElementById('mobile-menu')!;
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay')!;
const hamburgerMenu = document.getElementById('hamburger-menu')!;
mobileMenu.classList.toggle('open');
mobileMenuOverlay.classList.toggle('open');
// Update hamburger icon
if (mobileMenu.classList.contains('open')) {
addIconToButton(hamburgerMenu, 'close');
} else {
addIconToButton(hamburgerMenu, 'menu');
}
// Reinitialize icons
initializeLucideIcons();
}
private closeMobileMenu(): void {
const mobileMenu = document.getElementById('mobile-menu')!;
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay')!;
const hamburgerMenu = document.getElementById('hamburger-menu')!;
mobileMenu.classList.remove('open');
mobileMenuOverlay.classList.remove('open');
addIconToButton(hamburgerMenu, 'menu');
// Reinitialize icons
initializeLucideIcons();
}
private toggleShaderLibrary(): void {
const shaderLibrary = document.getElementById('shader-library')!;
shaderLibrary.classList.toggle('open');
}
private evalShader(): void {
this.shader.setCode(this.editor.value);
@ -315,11 +447,18 @@ class BitfielderApp {
(document.getElementById('fps-select') as HTMLSelectElement).value = settings.fps.toString();
(document.getElementById('render-mode-select') as HTMLSelectElement).value = settings.renderMode || 'classic';
// Sync mobile controls
(document.getElementById('resolution-select-mobile') as HTMLSelectElement).value = settings.resolution.toString();
(document.getElementById('fps-select-mobile') as HTMLSelectElement).value = settings.fps.toString();
(document.getElementById('render-mode-select-mobile') as HTMLSelectElement).value = settings.renderMode || 'classic';
// Apply UI opacity
const opacity = settings.uiOpacity ?? 0.3;
document.documentElement.style.setProperty('--ui-opacity', opacity.toString());
(document.getElementById('opacity-slider') as HTMLInputElement).value = (opacity * 100).toString();
(document.getElementById('opacity-slider-mobile') as HTMLInputElement).value = (opacity * 100).toString();
document.getElementById('opacity-value')!.textContent = `${Math.round(opacity * 100)}%`;
document.getElementById('opacity-value-mobile')!.textContent = `${Math.round(opacity * 100)}%`;
// Load last shader code if no URL hash
if (!window.location.hash) {