first commit
This commit is contained in:
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Bitfielder
|
||||
|
||||
A minimalist live shader editor for bitfield patterns.
|
||||
|
||||
## Features
|
||||
|
||||
- **Live coding** - Real-time shader compilation and rendering
|
||||
- **Performance controls** - Adjustable FPS (15/30/60) and resolution scaling
|
||||
- **Shader library** - Save, search, and organize shaders
|
||||
- **Live performance mode** - Minimal UI for fullscreen presentations
|
||||
- **URL sharing** - Share shaders via encoded URLs
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Controls
|
||||
|
||||
- **Ctrl+Enter** - Execute shader code
|
||||
- **H** - Toggle minimal UI mode
|
||||
- **F11** - Fullscreen
|
||||
- **R** - Random shader
|
||||
- **S** - Share URL
|
||||
|
||||
### Shader Variables
|
||||
|
||||
- `x, y` - Pixel coordinates
|
||||
- `t` - Time (enables animation)
|
||||
- `i` - Pixel index
|
||||
|
||||
### Operators
|
||||
|
||||
- `^ & |` - XOR, AND, OR
|
||||
- `<< >>` - Bit shift
|
||||
- `+ - * / %` - Math operations
|
||||
|
||||
### Library
|
||||
|
||||
Hover the right edge of the screen to access the shader library. Save shaders with custom names and search through your collection.
|
||||
|
||||
## Examples
|
||||
|
||||
```javascript
|
||||
x^y // Basic XOR pattern
|
||||
(x*y)%256 // Multiplication pattern
|
||||
(x+y+t*10)%256 // Animated pattern
|
||||
((x>>4)^(y>>4))<<4 // Bit-shifted XOR
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
475
index.html
Normal file
475
index.html
Normal file
@ -0,0 +1,475 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bitfielder</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#topbar .title {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#topbar .controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#topbar button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#topbar button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
#editor-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 140px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(5px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#editor-panel.minimal {
|
||||
height: 50px;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
#editor {
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
padding: 15px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#editor.minimal {
|
||||
padding: 12px 15px;
|
||||
font-size: 14px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#help-popup {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
z-index: 1000;
|
||||
max-width: 500px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#help-popup h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#help-popup .help-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#help-popup .help-section h4 {
|
||||
color: #ccc;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#help-popup .help-section p {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#help-popup .close-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#show-ui-btn {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#show-ui-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
#shader-library {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -300px;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 90;
|
||||
transition: right 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
#shader-library-trigger {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 100vh;
|
||||
z-index: 91;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#shader-library-trigger:hover + #shader-library,
|
||||
#shader-library:hover {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#shader-library.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.library-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.library-header h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-shader {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.search-shader {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.search-shader input {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search-shader input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.save-shader input {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.save-shader button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.save-shader button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.shader-list {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.shader-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shader-item-header {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shader-item-header:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.shader-name {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shader-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.shader-action {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.shader-action:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.shader-action.rename {
|
||||
background: rgba(52, 152, 219, 0.3);
|
||||
border-color: rgba(52, 152, 219, 0.5);
|
||||
}
|
||||
|
||||
.shader-action.rename:hover {
|
||||
background: rgba(52, 152, 219, 0.5);
|
||||
}
|
||||
|
||||
.shader-action.delete {
|
||||
background: rgba(231, 76, 60, 0.3);
|
||||
border-color: rgba(231, 76, 60, 0.5);
|
||||
}
|
||||
|
||||
.shader-action.delete:hover {
|
||||
background: rgba(231, 76, 60, 0.5);
|
||||
}
|
||||
|
||||
.shader-code {
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #ccc;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#performance-warning {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
background: rgba(255, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 1001;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<button id="show-ui-btn">Show UI</button>
|
||||
|
||||
<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;">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editor-panel">
|
||||
<textarea id="editor" placeholder="Enter shader code... (x, y, t, i)" spellcheck="false">x^y</textarea>
|
||||
</div>
|
||||
|
||||
<div id="shader-library-trigger"></div>
|
||||
<div id="shader-library">
|
||||
<div class="library-header">
|
||||
<h3>Shader Library</h3>
|
||||
<div class="save-shader">
|
||||
<input type="text" id="shader-name-input" placeholder="Shader name..." maxlength="30">
|
||||
<button id="save-shader-btn">Save</button>
|
||||
</div>
|
||||
<div class="search-shader">
|
||||
<input type="text" id="shader-search-input" placeholder="Search shaders...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="shader-list" id="shader-list">
|
||||
<!-- Saved shaders will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="help-popup">
|
||||
<button class="close-btn">×</button>
|
||||
<h3>Bitfielder Help</h3>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Keyboard Shortcuts</h4>
|
||||
<p><strong>Ctrl+Enter</strong> - Execute shader code</p>
|
||||
<p><strong>F11</strong> - Toggle fullscreen</p>
|
||||
<p><strong>H</strong> - Hide/show UI</p>
|
||||
<p><strong>R</strong> - Generate random shader</p>
|
||||
<p><strong>S</strong> - Share current shader (copy URL)</p>
|
||||
<p><strong>?</strong> - Show this help</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Shader Library</h4>
|
||||
<p>Hover over the <strong>right edge</strong> of the screen to access the shader library</p>
|
||||
<p>Save shaders with custom names and search through them</p>
|
||||
<p>Use <strong>edit</strong> to rename, <strong>del</strong> to delete</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Variables</h4>
|
||||
<p><strong>x, y</strong> - Pixel coordinates</p>
|
||||
<p><strong>t</strong> - Time (enables animation)</p>
|
||||
<p><strong>i</strong> - Pixel index</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Operators</h4>
|
||||
<p><strong>^ & |</strong> - XOR, AND, OR</p>
|
||||
<p><strong><< >></strong> - Bit shift left/right</p>
|
||||
<p><strong>+ - * / %</strong> - Math operations</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="performance-warning">
|
||||
Performance warning: Shader taking too long to render!
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
595
package-lock.json
generated
Normal file
595
package-lock.json
generated
Normal file
@ -0,0 +1,595 @@
|
||||
{
|
||||
"name": "bitfielder",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bitfielder",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.18.20",
|
||||
"@esbuild/android-arm64": "0.18.20",
|
||||
"@esbuild/android-x64": "0.18.20",
|
||||
"@esbuild/darwin-arm64": "0.18.20",
|
||||
"@esbuild/darwin-x64": "0.18.20",
|
||||
"@esbuild/freebsd-arm64": "0.18.20",
|
||||
"@esbuild/freebsd-x64": "0.18.20",
|
||||
"@esbuild/linux-arm": "0.18.20",
|
||||
"@esbuild/linux-arm64": "0.18.20",
|
||||
"@esbuild/linux-ia32": "0.18.20",
|
||||
"@esbuild/linux-loong64": "0.18.20",
|
||||
"@esbuild/linux-mips64el": "0.18.20",
|
||||
"@esbuild/linux-ppc64": "0.18.20",
|
||||
"@esbuild/linux-riscv64": "0.18.20",
|
||||
"@esbuild/linux-s390x": "0.18.20",
|
||||
"@esbuild/linux-x64": "0.18.20",
|
||||
"@esbuild/netbsd-x64": "0.18.20",
|
||||
"@esbuild/openbsd-x64": "0.18.20",
|
||||
"@esbuild/sunos-x64": "0.18.20",
|
||||
"@esbuild/win32-arm64": "0.18.20",
|
||||
"@esbuild/win32-ia32": "0.18.20",
|
||||
"@esbuild/win32-x64": "0.18.20"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
|
||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
|
||||
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
"rollup": "^3.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
package.json
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "bitfielder",
|
||||
"version": "1.0.0",
|
||||
"description": "A minimalist live shader editor for bitfield patterns",
|
||||
"type": "module",
|
||||
"main": "./dist/index.html",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "echo 'No linter configured'"
|
||||
},
|
||||
"keywords": [
|
||||
"shader",
|
||||
"bitfield",
|
||||
"live-coding",
|
||||
"visual",
|
||||
"performance",
|
||||
"canvas",
|
||||
"procedural",
|
||||
"graphics"
|
||||
],
|
||||
"author": "bitfielder",
|
||||
"license": "AGPL3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://git.raphaelforment.fr/BuboBubo/bitfielder"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://git.raphaelforment.fr/BuboBubo/bitfielder/issues"
|
||||
},
|
||||
"homepage": "https://git.raphaelforment.fr/BuboBubo/bitfielder",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
237
src/FakeShader.ts
Normal file
237
src/FakeShader.ts
Normal file
@ -0,0 +1,237 @@
|
||||
interface WorkerMessage {
|
||||
id: string;
|
||||
type: 'compile' | 'render';
|
||||
code?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
interface WorkerResponse {
|
||||
id: string;
|
||||
type: 'compiled' | 'rendered' | 'error';
|
||||
success: boolean;
|
||||
imageData?: ImageData;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class FakeShader {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private code: string;
|
||||
private worker: Worker;
|
||||
private animationId: number | null = null;
|
||||
private startTime: number = Date.now();
|
||||
private isCompiled: boolean = false;
|
||||
private isRendering: boolean = false;
|
||||
private pendingRenders: string[] = [];
|
||||
|
||||
// Frame rate limiting
|
||||
private targetFPS: number = 30;
|
||||
private frameInterval: number = 1000 / this.targetFPS;
|
||||
private lastFrameTime: number = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, code: string = 'x^y') {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.code = code;
|
||||
|
||||
// Initialize worker
|
||||
this.worker = new Worker(new URL('./ShaderWorker.ts', import.meta.url), { type: 'module' });
|
||||
this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => this.handleWorkerMessage(e.data);
|
||||
this.worker.onerror = (error) => console.error('Worker error:', error);
|
||||
|
||||
this.compile();
|
||||
}
|
||||
|
||||
private handleWorkerMessage(response: WorkerResponse): void {
|
||||
switch (response.type) {
|
||||
case 'compiled':
|
||||
this.isCompiled = response.success;
|
||||
if (!response.success) {
|
||||
console.error('Compilation failed:', response.error);
|
||||
this.fillBlack();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rendered':
|
||||
this.isRendering = false;
|
||||
if (response.success && response.imageData) {
|
||||
this.ctx.putImageData(response.imageData, 0, 0);
|
||||
} else {
|
||||
console.error('Render failed:', response.error);
|
||||
this.fillBlack();
|
||||
}
|
||||
|
||||
// Process pending renders
|
||||
if (this.pendingRenders.length > 0) {
|
||||
this.pendingRenders.shift(); // Remove completed render
|
||||
if (this.pendingRenders.length > 0) {
|
||||
// Skip to latest render request
|
||||
const latestId = this.pendingRenders[this.pendingRenders.length - 1];
|
||||
this.pendingRenders = [latestId];
|
||||
this.executeRender(latestId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.isRendering = false;
|
||||
console.error('Worker error:', response.error);
|
||||
this.fillBlack();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private fillBlack(): void {
|
||||
this.ctx.fillStyle = '#000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
private compile(): void {
|
||||
this.isCompiled = false;
|
||||
const id = `compile_${Date.now()}`;
|
||||
this.worker.postMessage({
|
||||
id,
|
||||
type: 'compile',
|
||||
code: this.code
|
||||
} as WorkerMessage);
|
||||
}
|
||||
|
||||
private executeRender(id: string): void {
|
||||
if (!this.isCompiled || this.isRendering) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRendering = true;
|
||||
const currentTime = (Date.now() - this.startTime) / 1000;
|
||||
|
||||
this.worker.postMessage({
|
||||
id,
|
||||
type: 'render',
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
time: currentTime
|
||||
} as WorkerMessage);
|
||||
}
|
||||
|
||||
setCode(code: string): void {
|
||||
this.code = code;
|
||||
this.compile();
|
||||
}
|
||||
|
||||
render(animate: boolean = false): void {
|
||||
const currentTime = performance.now();
|
||||
|
||||
// Frame rate limiting
|
||||
if (animate && currentTime - this.lastFrameTime < this.frameInterval) {
|
||||
if (animate) {
|
||||
this.animationId = requestAnimationFrame(() => this.render(true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastFrameTime = currentTime;
|
||||
|
||||
if (!this.isCompiled) {
|
||||
this.fillBlack();
|
||||
if (animate) {
|
||||
this.animationId = requestAnimationFrame(() => this.render(true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const renderId = `render_${Date.now()}_${Math.random()}`;
|
||||
|
||||
// Add to pending renders queue
|
||||
this.pendingRenders.push(renderId);
|
||||
|
||||
// If not currently rendering, start immediately
|
||||
if (!this.isRendering) {
|
||||
this.executeRender(renderId);
|
||||
}
|
||||
|
||||
// Continue animation
|
||||
if (animate) {
|
||||
this.animationId = requestAnimationFrame(() => this.render(true));
|
||||
}
|
||||
}
|
||||
|
||||
startAnimation(): void {
|
||||
this.stopAnimation();
|
||||
this.startTime = Date.now();
|
||||
this.lastFrameTime = 0; // Reset frame timing
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
stopAnimation(): void {
|
||||
if (this.animationId !== null) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
|
||||
// Clear pending renders
|
||||
this.pendingRenders = [];
|
||||
}
|
||||
|
||||
setTargetFPS(fps: number): void {
|
||||
this.targetFPS = Math.max(1, Math.min(120, fps)); // Clamp between 1-120 FPS
|
||||
this.frameInterval = 1000 / this.targetFPS;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.stopAnimation();
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
static generateRandomCode(): string {
|
||||
const presets = [
|
||||
'x^y',
|
||||
'x&y',
|
||||
'x|y',
|
||||
'(x*y)%256',
|
||||
'(x+y+t*10)%256',
|
||||
'((x>>4)^(y>>4))<<4',
|
||||
'(x^y^(x*y))%256',
|
||||
'((x&y)|(x^y))%256',
|
||||
'(x+y)&255',
|
||||
'x%y',
|
||||
'(x^(y<<2))%256',
|
||||
'((x*t)^y)%256',
|
||||
'(x&(y|t*8))%256',
|
||||
'((x>>2)|(y<<2))%256',
|
||||
'(x*y*t)%256',
|
||||
'(x+y*t)%256',
|
||||
'(x^y^(t*16))%256',
|
||||
'((x*t)&(y*t))%256',
|
||||
'(x+(y<<(t%4)))%256',
|
||||
'((x*t%128)^y)%256',
|
||||
'(x^(y*t*2))%256',
|
||||
'((x+t)*(y+t))%256',
|
||||
'(x&y&(t*8))%256',
|
||||
'((x|t)^(y|t))%256'
|
||||
];
|
||||
|
||||
const vars = ['x', 'y', 't', 'i'];
|
||||
const ops = ['^', '&', '|', '+', '-', '*', '%'];
|
||||
const shifts = ['<<', '>>'];
|
||||
const numbers = ['2', '4', '8', '16', '32', '64', '128', '256'];
|
||||
|
||||
const randomChoice = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
const dynamicExpressions = [
|
||||
() => `${randomChoice(vars)}${randomChoice(ops)}${randomChoice(vars)}`,
|
||||
() => `(${randomChoice(vars)}${randomChoice(ops)}${randomChoice(vars)})%${randomChoice(numbers)}`,
|
||||
() => `${randomChoice(vars)}${randomChoice(shifts)}${Math.floor(Math.random() * 8)}`,
|
||||
() => `(${randomChoice(vars)}*${randomChoice(vars)})%${randomChoice(numbers)}`,
|
||||
() => `${randomChoice(vars)}^${randomChoice(vars)}^${randomChoice(vars)}`,
|
||||
];
|
||||
|
||||
// 70% chance to pick from presets, 30% chance to generate dynamic
|
||||
if (Math.random() < 0.7) {
|
||||
return randomChoice(presets);
|
||||
} else {
|
||||
return randomChoice(dynamicExpressions)();
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/ShaderWorker.ts
Normal file
182
src/ShaderWorker.ts
Normal file
@ -0,0 +1,182 @@
|
||||
// WebWorker for safe shader compilation and execution
|
||||
interface WorkerMessage {
|
||||
id: string;
|
||||
type: 'compile' | 'render';
|
||||
code?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
interface WorkerResponse {
|
||||
id: string;
|
||||
type: 'compiled' | 'rendered' | 'error';
|
||||
success: boolean;
|
||||
imageData?: ImageData;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class ShaderWorker {
|
||||
private compiledFunction: Function | null = null;
|
||||
private lastCode: string = '';
|
||||
|
||||
constructor() {
|
||||
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
||||
this.handleMessage(e.data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(message: WorkerMessage): void {
|
||||
try {
|
||||
switch (message.type) {
|
||||
case 'compile':
|
||||
this.compileShader(message.id, message.code!);
|
||||
break;
|
||||
case 'render':
|
||||
this.renderShader(message.id, message.width!, message.height!, message.time!);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this.postError(message.id, error instanceof Error ? error.message : 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
private compileShader(id: string, code: string): void {
|
||||
if (code === this.lastCode && this.compiledFunction) {
|
||||
this.postMessage({ id, type: 'compiled', success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const safeCode = this.sanitizeCode(code);
|
||||
this.compiledFunction = new Function('x', 'y', 't', 'i', `
|
||||
// Timeout protection
|
||||
const startTime = performance.now();
|
||||
let iterations = 0;
|
||||
|
||||
function checkTimeout() {
|
||||
iterations++;
|
||||
if (iterations % 1000 === 0 && performance.now() - startTime > 5) {
|
||||
throw new Error('Shader timeout');
|
||||
}
|
||||
}
|
||||
|
||||
return (function() {
|
||||
checkTimeout();
|
||||
return ${safeCode};
|
||||
})();
|
||||
`);
|
||||
|
||||
this.lastCode = code;
|
||||
this.postMessage({ id, type: 'compiled', success: true });
|
||||
} catch (error) {
|
||||
this.compiledFunction = null;
|
||||
this.postError(id, error instanceof Error ? error.message : 'Compilation failed');
|
||||
}
|
||||
}
|
||||
|
||||
private renderShader(id: string, width: number, height: number, time: number): void {
|
||||
if (!this.compiledFunction) {
|
||||
this.postError(id, 'No compiled shader');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageData = new ImageData(width, height);
|
||||
const data = imageData.data;
|
||||
const startTime = performance.now();
|
||||
const maxRenderTime = 50; // 50ms max render time
|
||||
|
||||
try {
|
||||
for (let y = 0; y < height; y++) {
|
||||
// Check timeout every row
|
||||
if (performance.now() - startTime > maxRenderTime) {
|
||||
// Fill remaining pixels with black and break
|
||||
for (let remainingY = y; remainingY < height; remainingY++) {
|
||||
for (let remainingX = 0; remainingX < width; remainingX++) {
|
||||
const i = (remainingY * width + remainingX) * 4;
|
||||
data[i] = 0; // R
|
||||
data[i + 1] = 0; // G
|
||||
data[i + 2] = 0; // B
|
||||
data[i + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
const pixelIndex = y * width + x;
|
||||
|
||||
try {
|
||||
const value = this.compiledFunction(x, y, time, pixelIndex);
|
||||
const safeValue = isFinite(value) ? value : 0;
|
||||
const color = Math.abs(safeValue) % 256;
|
||||
|
||||
data[i] = color; // R
|
||||
data[i + 1] = (color * 2) % 256; // G
|
||||
data[i + 2] = (color * 3) % 256; // B
|
||||
data[i + 3] = 255; // A
|
||||
} catch (error) {
|
||||
data[i] = 0; // R
|
||||
data[i + 1] = 0; // G
|
||||
data[i + 2] = 0; // B
|
||||
data[i + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.postMessage({ id, type: 'rendered', success: true, imageData });
|
||||
} catch (error) {
|
||||
this.postError(id, error instanceof Error ? error.message : 'Render failed');
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeCode(code: string): string {
|
||||
// Strict whitelist approach
|
||||
const allowedPattern = /^[0-9a-zA-Z\s\+\-\*\/\%\^\&\|\(\)\<\>\~\?:,\.xyti]+$/;
|
||||
|
||||
if (!allowedPattern.test(code)) {
|
||||
throw new Error('Invalid characters in shader code');
|
||||
}
|
||||
|
||||
// Check for dangerous keywords
|
||||
const dangerousKeywords = [
|
||||
'eval', 'Function', 'constructor', 'prototype', '__proto__',
|
||||
'window', 'document', 'global', 'process', 'require',
|
||||
'import', 'export', 'class', 'function', 'var', 'let', 'const',
|
||||
'while', 'for', 'do', 'if', 'else', 'switch', 'case', 'break',
|
||||
'continue', 'return', 'throw', 'try', 'catch', 'finally'
|
||||
];
|
||||
|
||||
const codeWords = code.toLowerCase().split(/[^a-z]/);
|
||||
for (const keyword of dangerousKeywords) {
|
||||
if (codeWords.includes(keyword)) {
|
||||
throw new Error(`Forbidden keyword: ${keyword}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit expression complexity
|
||||
const complexity = (code.match(/[\(\)]/g) || []).length;
|
||||
if (complexity > 20) {
|
||||
throw new Error('Expression too complex');
|
||||
}
|
||||
|
||||
// Limit code length
|
||||
if (code.length > 200) {
|
||||
throw new Error('Code too long');
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private postMessage(response: WorkerResponse): void {
|
||||
self.postMessage(response);
|
||||
}
|
||||
|
||||
private postError(id: string, error: string): void {
|
||||
this.postMessage({ id, type: 'error', success: false, error });
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize worker
|
||||
new ShaderWorker();
|
||||
156
src/Storage.ts
Normal file
156
src/Storage.ts
Normal file
@ -0,0 +1,156 @@
|
||||
interface SavedShader {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
created: number;
|
||||
lastUsed: number;
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
resolution: number;
|
||||
fps: number;
|
||||
lastShaderCode: string;
|
||||
}
|
||||
|
||||
export class Storage {
|
||||
private static readonly SHADERS_KEY = 'bitfielder_shaders';
|
||||
private static readonly SETTINGS_KEY = 'bitfielder_settings';
|
||||
|
||||
static saveShader(name: string, code: string): SavedShader {
|
||||
const shaders = this.getShaders();
|
||||
const id = this.generateId();
|
||||
const timestamp = Date.now();
|
||||
|
||||
const shader: SavedShader = {
|
||||
id,
|
||||
name: name.trim() || `Shader ${shaders.length + 1}`,
|
||||
code,
|
||||
created: timestamp,
|
||||
lastUsed: timestamp
|
||||
};
|
||||
|
||||
shaders.push(shader);
|
||||
this.setShaders(shaders);
|
||||
return shader;
|
||||
}
|
||||
|
||||
static getShaders(): SavedShader[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.SHADERS_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load shaders:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static deleteShader(id: string): void {
|
||||
const shaders = this.getShaders().filter(s => s.id !== id);
|
||||
this.setShaders(shaders);
|
||||
}
|
||||
|
||||
static updateShaderUsage(id: string): void {
|
||||
const shaders = this.getShaders();
|
||||
const shader = shaders.find(s => s.id === id);
|
||||
if (shader) {
|
||||
shader.lastUsed = Date.now();
|
||||
this.setShaders(shaders);
|
||||
}
|
||||
}
|
||||
|
||||
static renameShader(id: string, newName: string): void {
|
||||
const shaders = this.getShaders();
|
||||
const shader = shaders.find(s => s.id === id);
|
||||
if (shader) {
|
||||
shader.name = newName.trim() || shader.name;
|
||||
this.setShaders(shaders);
|
||||
}
|
||||
}
|
||||
|
||||
private static setShaders(shaders: SavedShader[]): void {
|
||||
try {
|
||||
// Keep only the 50 most recent shaders
|
||||
const sortedShaders = shaders
|
||||
.sort((a, b) => b.lastUsed - a.lastUsed)
|
||||
.slice(0, 50);
|
||||
localStorage.setItem(this.SHADERS_KEY, JSON.stringify(sortedShaders));
|
||||
} catch (error) {
|
||||
console.error('Failed to save shaders:', error);
|
||||
}
|
||||
}
|
||||
|
||||
static saveSettings(settings: Partial<AppSettings>): void {
|
||||
try {
|
||||
const current = this.getSettings();
|
||||
const updated = { ...current, ...settings };
|
||||
localStorage.setItem(this.SETTINGS_KEY, JSON.stringify(updated));
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
static getSettings(): AppSettings {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.SETTINGS_KEY);
|
||||
const defaults: AppSettings = {
|
||||
resolution: 1,
|
||||
fps: 30,
|
||||
lastShaderCode: 'x^y'
|
||||
};
|
||||
return stored ? { ...defaults, ...JSON.parse(stored) } : defaults;
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
return {
|
||||
resolution: 1,
|
||||
fps: 30,
|
||||
lastShaderCode: 'x^y'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static clearAll(): void {
|
||||
try {
|
||||
localStorage.removeItem(this.SHADERS_KEY);
|
||||
localStorage.removeItem(this.SETTINGS_KEY);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private static generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
static exportShaders(): string {
|
||||
const shaders = this.getShaders();
|
||||
return JSON.stringify(shaders, null, 2);
|
||||
}
|
||||
|
||||
static importShaders(jsonData: string): boolean {
|
||||
try {
|
||||
const imported = JSON.parse(jsonData) as SavedShader[];
|
||||
if (!Array.isArray(imported)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate structure
|
||||
const valid = imported.every(shader =>
|
||||
shader.id && shader.name && shader.code &&
|
||||
typeof shader.created === 'number' &&
|
||||
typeof shader.lastUsed === 'number'
|
||||
);
|
||||
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existing = this.getShaders();
|
||||
const merged = [...existing, ...imported];
|
||||
this.setShaders(merged);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to import shaders:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
405
src/main.ts
Normal file
405
src/main.ts
Normal file
@ -0,0 +1,405 @@
|
||||
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 = `<div style="padding: 20px; text-align: center; color: #666; font-size: 12px;">${message}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
shaderList.innerHTML = shaders.map(shader => `
|
||||
<div class="shader-item">
|
||||
<div class="shader-item-header" onclick="app.loadShader('${shader.id}')">
|
||||
<span class="shader-name" id="name-${shader.id}">${this.escapeHtml(shader.name)}</span>
|
||||
<div class="shader-actions">
|
||||
<button class="shader-action rename" onclick="event.stopPropagation(); app.startRename('${shader.id}')" title="Rename">edit</button>
|
||||
<button class="shader-action delete" onclick="event.stopPropagation(); app.deleteShader('${shader.id}')" title="Delete">del</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shader-code">${this.escapeHtml(shader.code)}</div>
|
||||
</div>
|
||||
`).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;
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
server: {
|
||||
port: 3000
|
||||
},
|
||||
worker: {
|
||||
format: 'es'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user