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:card" content="summary">
|
||||||
<meta name="twitter:title" content="Bitfielder - Bitfield Shader App">
|
<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">
|
<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>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--ui-opacity: 0.3;
|
--ui-opacity: 0.3;
|
||||||
@ -27,7 +30,7 @@
|
|||||||
body {
|
body {
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +70,137 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-left: auto;
|
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 {
|
#topbar button {
|
||||||
@ -76,7 +210,7 @@
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +218,30 @@
|
|||||||
background: rgba(255, 255, 255, 0.2);
|
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 {
|
#editor-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -113,7 +271,7 @@
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
resize: none;
|
resize: none;
|
||||||
@ -127,7 +285,7 @@
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 20px 30px;
|
padding: 20px 30px;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -230,7 +388,7 @@
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: none;
|
display: none;
|
||||||
@ -242,14 +400,14 @@
|
|||||||
|
|
||||||
#shader-library {
|
#shader-library {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 40px;
|
||||||
right: -300px;
|
left: -300px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 100vh;
|
height: calc(100vh - 40px);
|
||||||
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
|
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;
|
z-index: 90;
|
||||||
transition: right 0.3s ease;
|
transition: left 0.3s ease;
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@ -257,21 +415,21 @@
|
|||||||
|
|
||||||
#shader-library-trigger {
|
#shader-library-trigger {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 40px;
|
||||||
right: 0;
|
left: 0;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 100vh;
|
height: calc(100vh - 40px);
|
||||||
z-index: 91;
|
z-index: 91;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#shader-library-trigger:hover + #shader-library,
|
#shader-library-trigger:hover + #shader-library,
|
||||||
#shader-library:hover {
|
#shader-library:hover {
|
||||||
right: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#shader-library.open {
|
#shader-library.open {
|
||||||
right: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-header {
|
.library-header {
|
||||||
@ -302,7 +460,7 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,7 +475,7 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +486,7 @@
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,7 +538,7 @@
|
|||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shader-action:hover {
|
.shader-action:hover {
|
||||||
@ -411,7 +569,7 @@
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background: rgba(0, 0, 0, var(--ui-opacity));
|
background: rgba(0, 0, 0, var(--ui-opacity));
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-family: monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@ -432,15 +590,29 @@
|
|||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
#topbar .controls {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar .controls-desktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar .controls-mobile {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hamburger-menu {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
#topbar {
|
#topbar {
|
||||||
flex-wrap: wrap;
|
height: 40px;
|
||||||
height: auto;
|
padding: 0 10px;
|
||||||
padding: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#topbar .title {
|
#topbar .title {
|
||||||
margin-right: 10px;
|
margin-right: auto;
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#topbar .controls {
|
#topbar .controls {
|
||||||
@ -487,11 +659,13 @@
|
|||||||
|
|
||||||
#shader-library {
|
#shader-library {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
right: -100%;
|
left: -100%;
|
||||||
|
top: 40px;
|
||||||
|
height: calc(100vh - 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#shader-library-trigger {
|
#shader-library-trigger {
|
||||||
width: 30px;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,26 +732,82 @@
|
|||||||
<div id="topbar">
|
<div id="topbar">
|
||||||
<div class="title">Bitfielder</div>
|
<div class="title">Bitfielder</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
<div class="controls-desktop">
|
||||||
Resolution:
|
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||||
<select id="resolution-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
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="1">Full (1x)</option>
|
||||||
<option value="2">Half (2x)</option>
|
<option value="2">Half (2x)</option>
|
||||||
<option value="4">Quarter (4x)</option>
|
<option value="4">Quarter (4x)</option>
|
||||||
<option value="8">Eighth (8x)</option>
|
<option value="8">Eighth (8x)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
<div class="mobile-menu-item">
|
||||||
FPS:
|
<label>FPS</label>
|
||||||
<select id="fps-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
<select id="fps-select-mobile">
|
||||||
<option value="15">15 FPS</option>
|
<option value="15">15 FPS</option>
|
||||||
<option value="30" selected>30 FPS</option>
|
<option value="30" selected>30 FPS</option>
|
||||||
<option value="60">60 FPS</option>
|
<option value="60">60 FPS</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
<div class="mobile-menu-item">
|
||||||
Render Mode:
|
<label>Render Mode</label>
|
||||||
<select id="render-mode-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
|
<select id="render-mode-select-mobile">
|
||||||
<option value="classic" selected>Classic</option>
|
<option value="classic" selected>Classic</option>
|
||||||
<option value="grayscale">Grayscale</option>
|
<option value="grayscale">Grayscale</option>
|
||||||
<option value="red">Red Channel</option>
|
<option value="red">Red Channel</option>
|
||||||
@ -587,18 +817,19 @@
|
|||||||
<option value="hsv">HSV</option>
|
<option value="hsv">HSV</option>
|
||||||
<option value="rainbow">Rainbow</option>
|
<option value="rainbow">Rainbow</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
<div class="mobile-menu-item">
|
||||||
UI Opacity:
|
<label>UI Opacity: <span id="opacity-value-mobile">30%</span></label>
|
||||||
<input type="range" id="opacity-slider" min="10" max="100" value="30" style="width: 80px; vertical-align: middle;">
|
<input type="range" id="opacity-slider-mobile" min="10" max="100" value="30">
|
||||||
<span id="opacity-value" style="font-size: 11px;">30%</span>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
<button id="help-btn">?</button>
|
<div class="mobile-menu-section">
|
||||||
<button id="fullscreen-btn">Fullscreen</button>
|
<div class="mobile-menu-buttons">
|
||||||
<button id="hide-ui-btn">Hide UI</button>
|
<button id="help-btn-mobile"><span class="icon"></span> Help</button>
|
||||||
<button id="random-btn">Random</button>
|
<button id="fullscreen-btn-mobile"><span class="icon"></span> Fullscreen</button>
|
||||||
<button id="share-btn">Share</button>
|
<button id="share-btn-mobile"><span class="icon"></span> Share</button>
|
||||||
<button id="export-png-btn">Export PNG</button>
|
<button id="export-png-btn-mobile"><span class="icon"></span> Export PNG</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
9
package-lock.json
generated
9
package-lock.json
generated
@ -8,6 +8,9 @@
|
|||||||
"name": "bitfielder",
|
"name": "bitfielder",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "AGPL3",
|
"license": "AGPL3",
|
||||||
|
"dependencies": {
|
||||||
|
"lucide": "^0.525.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
@ -443,6 +446,12 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"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": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
|||||||
@ -42,5 +42,8 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"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 { FakeShader } from './FakeShader';
|
||||||
import { Storage } from './Storage';
|
import { Storage } from './Storage';
|
||||||
|
import { addIconToButton, createIcon, initializeLucideIcons } from './icons';
|
||||||
|
|
||||||
class BitfielderApp {
|
class BitfielderApp {
|
||||||
private shader: FakeShader;
|
private shader: FakeShader;
|
||||||
@ -21,6 +22,7 @@ class BitfielderApp {
|
|||||||
this.shader = new FakeShader(this.canvas, this.editor.value);
|
this.shader = new FakeShader(this.canvas, this.editor.value);
|
||||||
|
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
this.initializeIcons();
|
||||||
this.loadFromURL();
|
this.loadFromURL();
|
||||||
this.renderShaderLibrary();
|
this.renderShaderLibrary();
|
||||||
this.render();
|
this.render();
|
||||||
@ -59,11 +61,29 @@ class BitfielderApp {
|
|||||||
const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
|
const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
|
||||||
const renderModeSelect = document.getElementById('render-mode-select') as HTMLSelectElement;
|
const renderModeSelect = document.getElementById('render-mode-select') as HTMLSelectElement;
|
||||||
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
||||||
const opacityValue = document.getElementById('opacity-value')!;
|
|
||||||
const evalBtn = document.getElementById('eval-btn')!;
|
const evalBtn = document.getElementById('eval-btn')!;
|
||||||
const helpPopup = document.getElementById('help-popup')!;
|
const helpPopup = document.getElementById('help-popup')!;
|
||||||
const closeBtn = helpPopup.querySelector('.close-btn')!;
|
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
|
// Library elements
|
||||||
const saveShaderBtn = document.getElementById('save-shader-btn')!;
|
const saveShaderBtn = document.getElementById('save-shader-btn')!;
|
||||||
const shaderNameInput = document.getElementById('shader-name-input') as HTMLInputElement;
|
const shaderNameInput = document.getElementById('shader-name-input') as HTMLInputElement;
|
||||||
@ -92,6 +112,48 @@ class BitfielderApp {
|
|||||||
});
|
});
|
||||||
shaderSearchInput.addEventListener('input', () => this.renderShaderLibrary());
|
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
|
// Close help popup when clicking outside
|
||||||
helpPopup.addEventListener('click', (e) => {
|
helpPopup.addEventListener('click', (e) => {
|
||||||
@ -285,13 +347,83 @@ class BitfielderApp {
|
|||||||
private updateUIOpacity(): void {
|
private updateUIOpacity(): void {
|
||||||
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
||||||
const opacityValue = document.getElementById('opacity-value')!;
|
const opacityValue = document.getElementById('opacity-value')!;
|
||||||
|
const opacityValueMobile = document.getElementById('opacity-value-mobile')!;
|
||||||
const opacity = parseInt(opacitySlider.value) / 100;
|
const opacity = parseInt(opacitySlider.value) / 100;
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--ui-opacity', opacity.toString());
|
document.documentElement.style.setProperty('--ui-opacity', opacity.toString());
|
||||||
opacityValue.textContent = `${opacitySlider.value}%`;
|
opacityValue.textContent = `${opacitySlider.value}%`;
|
||||||
|
opacityValueMobile.textContent = `${opacitySlider.value}%`;
|
||||||
|
|
||||||
Storage.saveSettings({ uiOpacity: opacity });
|
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 {
|
private evalShader(): void {
|
||||||
this.shader.setCode(this.editor.value);
|
this.shader.setCode(this.editor.value);
|
||||||
@ -315,11 +447,18 @@ class BitfielderApp {
|
|||||||
(document.getElementById('fps-select') as HTMLSelectElement).value = settings.fps.toString();
|
(document.getElementById('fps-select') as HTMLSelectElement).value = settings.fps.toString();
|
||||||
(document.getElementById('render-mode-select') as HTMLSelectElement).value = settings.renderMode || 'classic';
|
(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
|
// Apply UI opacity
|
||||||
const opacity = settings.uiOpacity ?? 0.3;
|
const opacity = settings.uiOpacity ?? 0.3;
|
||||||
document.documentElement.style.setProperty('--ui-opacity', opacity.toString());
|
document.documentElement.style.setProperty('--ui-opacity', opacity.toString());
|
||||||
(document.getElementById('opacity-slider') as HTMLInputElement).value = (opacity * 100).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')!.textContent = `${Math.round(opacity * 100)}%`;
|
||||||
|
document.getElementById('opacity-value-mobile')!.textContent = `${Math.round(opacity * 100)}%`;
|
||||||
|
|
||||||
// Load last shader code if no URL hash
|
// Load last shader code if no URL hash
|
||||||
if (!window.location.hash) {
|
if (!window.location.hash) {
|
||||||
|
|||||||
Reference in New Issue
Block a user