better responsiveness
This commit is contained in:
331
index.html
331
index.html
@ -13,6 +13,9 @@
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Bitfielder - Bitfield Shader App">
|
||||
<meta name="twitter:description" content="Interactive bitfield shader editor for creating visual patterns using bitwise operations">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ui-opacity: 0.3;
|
||||
@ -27,7 +30,7 @@
|
||||
body {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -67,6 +70,137 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#topbar .controls-desktop {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#topbar .controls-mobile {
|
||||
display: none;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#hamburger-menu {
|
||||
display: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#hamburger-menu:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
#hamburger-menu svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
#mobile-menu {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
right: -320px;
|
||||
width: 320px;
|
||||
max-width: 80vw;
|
||||
height: calc(100vh - 40px);
|
||||
background: rgba(0, 0, 0, var(--ui-opacity));
|
||||
backdrop-filter: blur(3px);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 150;
|
||||
transition: right 0.3s ease;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#mobile-menu.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#mobile-menu h3 {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mobile-menu-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mobile-menu-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.mobile-menu-item label {
|
||||
display: block;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mobile-menu-item select,
|
||||
.mobile-menu-item input[type="range"] {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-menu-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mobile-menu-buttons button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mobile-menu-buttons button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
#mobile-menu-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 149;
|
||||
}
|
||||
|
||||
#mobile-menu-overlay.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#topbar button {
|
||||
@ -76,7 +210,7 @@
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@ -84,6 +218,30 @@
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Lucide icon styles */
|
||||
[data-lucide] {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#editor-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@ -113,7 +271,7 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 16px;
|
||||
padding: 15px;
|
||||
resize: none;
|
||||
@ -127,7 +285,7 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
padding: 20px 30px;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
@ -230,7 +388,7 @@
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
@ -242,14 +400,14 @@
|
||||
|
||||
#shader-library {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -300px;
|
||||
top: 40px;
|
||||
left: -300px;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
height: calc(100vh - 40px);
|
||||
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 90;
|
||||
transition: right 0.3s ease;
|
||||
transition: left 0.3s ease;
|
||||
backdrop-filter: blur(3px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
@ -257,21 +415,21 @@
|
||||
|
||||
#shader-library-trigger {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 100vh;
|
||||
height: calc(100vh - 40px);
|
||||
z-index: 91;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#shader-library-trigger:hover + #shader-library,
|
||||
#shader-library:hover {
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#shader-library.open {
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.library-header {
|
||||
@ -302,7 +460,7 @@
|
||||
color: #fff;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@ -317,7 +475,7 @@
|
||||
color: #fff;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@ -328,7 +486,7 @@
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@ -380,7 +538,7 @@
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
.shader-action:hover {
|
||||
@ -411,7 +569,7 @@
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, var(--ui-opacity));
|
||||
color: #ccc;
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 11px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
word-break: break-all;
|
||||
@ -432,15 +590,29 @@
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
#topbar .controls {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#topbar .controls-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#topbar .controls-mobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#hamburger-menu {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#topbar .title {
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#topbar .controls {
|
||||
@ -487,11 +659,13 @@
|
||||
|
||||
#shader-library {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
left: -100%;
|
||||
top: 40px;
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
#shader-library-trigger {
|
||||
width: 30px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -558,26 +732,82 @@
|
||||
<div id="topbar">
|
||||
<div class="title">Bitfielder</div>
|
||||
<div class="controls">
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
Resolution:
|
||||
<select id="resolution-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
||||
<div class="controls-desktop">
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
Resolution:
|
||||
<select id="resolution-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 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>
|
||||
</select>
|
||||
</label>
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
FPS:
|
||||
<select id="fps-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
||||
<option value="15">15 FPS</option>
|
||||
<option value="30" selected>30 FPS</option>
|
||||
<option value="60">60 FPS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
Render Mode:
|
||||
<select id="render-mode-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
||||
<option value="classic" selected>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>
|
||||
</select>
|
||||
</label>
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
UI Opacity:
|
||||
<input type="range" id="opacity-slider" min="10" max="100" value="30" style="width: 80px; vertical-align: middle;">
|
||||
<span id="opacity-value" style="font-size: 11px;">30%</span>
|
||||
</label>
|
||||
<button id="help-btn">?</button>
|
||||
<button id="fullscreen-btn">Fullscreen</button>
|
||||
<button id="hide-ui-btn">Hide UI</button>
|
||||
<button id="random-btn">Random</button>
|
||||
<button id="share-btn">Share</button>
|
||||
<button id="export-png-btn">Export PNG</button>
|
||||
</div>
|
||||
<div class="controls-mobile">
|
||||
<button id="library-btn-mobile" class="icon-button" aria-label="Shader Library"></button>
|
||||
<button id="random-btn-mobile" class="icon-button" aria-label="Random"></button>
|
||||
<button id="hide-ui-btn-mobile" class="icon-button" aria-label="Hide UI"></button>
|
||||
<button id="hamburger-menu" class="icon-button" aria-label="Menu"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu-overlay"></div>
|
||||
<div id="mobile-menu">
|
||||
<h3>Settings</h3>
|
||||
<div class="mobile-menu-section">
|
||||
<div class="mobile-menu-item">
|
||||
<label>Resolution</label>
|
||||
<select id="resolution-select-mobile">
|
||||
<option value="1">Full (1x)</option>
|
||||
<option value="2">Half (2x)</option>
|
||||
<option value="4">Quarter (4x)</option>
|
||||
<option value="8">Eighth (8x)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
FPS:
|
||||
<select id="fps-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
||||
</div>
|
||||
<div class="mobile-menu-item">
|
||||
<label>FPS</label>
|
||||
<select id="fps-select-mobile">
|
||||
<option value="15">15 FPS</option>
|
||||
<option value="30" selected>30 FPS</option>
|
||||
<option value="60">60 FPS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
Render Mode:
|
||||
<select id="render-mode-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
||||
</div>
|
||||
<div class="mobile-menu-item">
|
||||
<label>Render Mode</label>
|
||||
<select id="render-mode-select-mobile">
|
||||
<option value="classic" selected>Classic</option>
|
||||
<option value="grayscale">Grayscale</option>
|
||||
<option value="red">Red Channel</option>
|
||||
@ -587,18 +817,19 @@
|
||||
<option value="hsv">HSV</option>
|
||||
<option value="rainbow">Rainbow</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||
UI Opacity:
|
||||
<input type="range" id="opacity-slider" min="10" max="100" value="30" style="width: 80px; vertical-align: middle;">
|
||||
<span id="opacity-value" style="font-size: 11px;">30%</span>
|
||||
</label>
|
||||
<button id="help-btn">?</button>
|
||||
<button id="fullscreen-btn">Fullscreen</button>
|
||||
<button id="hide-ui-btn">Hide UI</button>
|
||||
<button id="random-btn">Random</button>
|
||||
<button id="share-btn">Share</button>
|
||||
<button id="export-png-btn">Export PNG</button>
|
||||
</div>
|
||||
<div class="mobile-menu-item">
|
||||
<label>UI Opacity: <span id="opacity-value-mobile">30%</span></label>
|
||||
<input type="range" id="opacity-slider-mobile" min="10" max="100" value="30">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-menu-section">
|
||||
<div class="mobile-menu-buttons">
|
||||
<button id="help-btn-mobile"><span class="icon"></span> Help</button>
|
||||
<button id="fullscreen-btn-mobile"><span class="icon"></span> Fullscreen</button>
|
||||
<button id="share-btn-mobile"><span class="icon"></span> Share</button>
|
||||
<button id="export-png-btn-mobile"><span class="icon"></span> Export PNG</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@ -8,6 +8,9 @@
|
||||
"name": "bitfielder",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL3",
|
||||
"dependencies": {
|
||||
"lucide": "^0.525.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
@ -443,6 +446,12 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide": {
|
||||
"version": "0.525.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.525.0.tgz",
|
||||
"integrity": "sha512-sfehWlaE/7NVkcEQ4T9JD3eID8RNMIGJBBUq9wF3UFiJIrcMKRbU3g1KGfDk4svcW7yw8BtDLXaXo02scDtUYQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
|
||||
@ -42,5 +42,8 @@
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"lucide": "^0.525.0"
|
||||
}
|
||||
}
|
||||
|
||||
42
src/icons.ts
Normal file
42
src/icons.ts
Normal 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 });
|
||||
}
|
||||
141
src/main.ts
141
src/main.ts
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user