smol update

This commit is contained in:
2025-07-05 01:07:53 +00:00
parent c5bd2ba29c
commit 59abe5f5a1
7 changed files with 43 additions and 117 deletions

View File

@ -4,6 +4,15 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bitfielder</title>
<meta name="description" content="Bitfielder is a live bitfield shader editor for creating visual patterns using bitwise operations.">
<meta name="author" content="BuboBubo">
<meta name="keywords" content="shader, bitfield, visual, patterns, programming, interactive, editor">
<meta property="og:title" content="Bitfielder - Bitfield Shader App">
<meta property="og:description" content="Interactive bitfield shader editor for creating visual patterns using bitwise operations">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Bitfielder - Bitfield Shader App">
<meta name="twitter:description" content="Interactive bitfield shader editor for creating visual patterns using bitwise operations">
<style>
* {
margin: 0;
@ -420,12 +429,11 @@
<button id="random-btn">Random</button>
<button id="share-btn">Share</button>
<button id="export-png-btn">Export PNG</button>
<button id="export-gif-btn">Export GIF</button>
</div>
</div>
<div id="editor-panel">
<textarea id="editor" placeholder="Enter shader code... (x, y, t, i)" spellcheck="false">x^y</textarea>
<textarea id="editor" placeholder="Enter shader code... (x, y, t, i, mouseX, mouseY)" spellcheck="false">x^y</textarea>
</div>
<div id="shader-library-trigger"></div>
@ -471,6 +479,7 @@
<p><strong>x, y</strong> - Pixel coordinates</p>
<p><strong>t</strong> - Time (enables animation)</p>
<p><strong>i</strong> - Pixel index</p>
<p><strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)</p>
</div>
<div class="help-section">
@ -492,7 +501,6 @@
<div class="help-section">
<h4>Export</h4>
<p><strong>Export PNG</strong> - Save current frame as image</p>
<p><strong>Export GIF</strong> - Save 2-second animation (if animated)</p>
</div>
</div>
@ -502,4 +510,4 @@
<script type="module" src="/src/main.ts"></script>
</body>
</html>
</html>

4
package-lock.json generated
View File

@ -7,9 +7,13 @@
"": {
"name": "bitfielder",
"version": "1.0.0",
"license": "AGPL3",
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@esbuild/android-arm": {

View File

@ -34,9 +34,6 @@
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"gif.js": "^0.2.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"

View File

@ -6,6 +6,8 @@ interface WorkerMessage {
height?: number;
time?: number;
renderMode?: string;
mouseX?: number;
mouseY?: number;
}
interface WorkerResponse {
@ -27,6 +29,8 @@ export class FakeShader {
private isRendering: boolean = false;
private pendingRenders: string[] = [];
private renderMode: string = 'classic';
private mouseX: number = 0;
private mouseY: number = 0;
// Frame rate limiting
private targetFPS: number = 30;
@ -114,7 +118,9 @@ export class FakeShader {
width: this.canvas.width,
height: this.canvas.height,
time: currentTime,
renderMode: this.renderMode
renderMode: this.renderMode,
mouseX: this.mouseX,
mouseY: this.mouseY
} as WorkerMessage);
}
@ -186,6 +192,11 @@ export class FakeShader {
this.renderMode = mode;
}
setMousePosition(x: number, y: number): void {
this.mouseX = x;
this.mouseY = y;
}
destroy(): void {
this.stopAnimation();
this.worker.terminate();

View File

@ -7,6 +7,8 @@ interface WorkerMessage {
height?: number;
time?: number;
renderMode?: string;
mouseX?: number;
mouseY?: number;
}
interface WorkerResponse {
@ -34,7 +36,7 @@ class ShaderWorker {
this.compileShader(message.id, message.code!);
break;
case 'render':
this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic');
this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message.mouseX || 0, message.mouseY || 0);
break;
}
} catch (error) {
@ -50,7 +52,7 @@ class ShaderWorker {
try {
const safeCode = this.sanitizeCode(code);
this.compiledFunction = new Function('x', 'y', 't', 'i', `
this.compiledFunction = new Function('x', 'y', 't', 'i', 'mouseX', 'mouseY', `
// Timeout protection
const startTime = performance.now();
let iterations = 0;
@ -76,7 +78,7 @@ class ShaderWorker {
}
}
private renderShader(id: string, width: number, height: number, time: number, renderMode: string): void {
private renderShader(id: string, width: number, height: number, time: number, renderMode: string, mouseX: number, mouseY: number): void {
if (!this.compiledFunction) {
this.postError(id, 'No compiled shader');
return;
@ -109,7 +111,7 @@ class ShaderWorker {
const pixelIndex = y * width + x;
try {
const value = this.compiledFunction(x, y, time, pixelIndex);
const value = this.compiledFunction(x, y, time, pixelIndex, mouseX, mouseY);
const safeValue = isFinite(value) ? value : 0;
const [r, g, b] = this.calculateColor(safeValue, renderMode);

24
src/gif.d.ts vendored
View File

@ -1,24 +0,0 @@
declare module 'gif.js' {
interface GIFOptions {
workers?: number;
quality?: number;
width?: number;
height?: number;
transparent?: string;
background?: string;
dither?: boolean;
debug?: boolean;
repeat?: number;
workerScript?: string;
}
class GIF {
constructor(options?: GIFOptions);
addFrame(canvas: HTMLCanvasElement, options?: { delay?: number; copy?: boolean; }): void;
render(): void;
on(event: 'finished', callback: (blob: Blob) => void): void;
on(event: 'progress', callback: (progress: number) => void): void;
}
export default GIF;
}

View File

@ -8,6 +8,8 @@ class BitfielderApp {
private isAnimating: boolean = false;
private uiVisible: boolean = true;
private performanceWarning: HTMLElement;
private mouseX: number = 0;
private mouseY: number = 0;
constructor() {
this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
@ -53,7 +55,6 @@ class BitfielderApp {
const randomBtn = document.getElementById('random-btn')!;
const shareBtn = document.getElementById('share-btn')!;
const exportPngBtn = document.getElementById('export-png-btn')!;
const exportGifBtn = document.getElementById('export-gif-btn')!;
const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement;
const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
const renderModeSelect = document.getElementById('render-mode-select') as HTMLSelectElement;
@ -72,7 +73,6 @@ class BitfielderApp {
randomBtn.addEventListener('click', () => this.generateRandom());
shareBtn.addEventListener('click', () => this.shareURL());
exportPngBtn.addEventListener('click', () => this.exportPNG());
exportGifBtn.addEventListener('click', () => this.exportGIF());
resolutionSelect.addEventListener('change', () => this.updateResolution());
fpsSelect.addEventListener('change', () => this.updateFPS());
renderModeSelect.addEventListener('change', () => this.updateRenderMode());
@ -138,6 +138,13 @@ class BitfielderApp {
this.showPerformanceWarning();
}
});
// Mouse tracking
window.addEventListener('mousemove', (e) => {
this.mouseX = e.clientX / window.innerWidth;
this.mouseY = 1.0 - (e.clientY / window.innerHeight); // Invert Y to match shader coordinates
this.shader.setMousePosition(this.mouseX, this.mouseY);
});
}
private render(): void {
@ -272,86 +279,7 @@ class BitfielderApp {
link.click();
}
private exportGIF(): void {
const hasAnimation = this.editor.value.includes('t');
if (!hasAnimation) {
// Static image - just export as PNG
this.exportPNG();
return;
}
// For animated GIFs, we need to capture multiple frames
this.captureGIF();
}
private async captureGIF(): Promise<void> {
const hasAnimation = this.editor.value.includes('t');
if (!hasAnimation) {
this.exportPNG();
return;
}
// Show progress indicator
const exportBtn = document.getElementById('export-gif-btn')!;
const originalText = exportBtn.textContent;
exportBtn.textContent = 'Capturing...';
exportBtn.setAttribute('disabled', 'true');
try {
// Import GIF.js dynamically
const { default: GIF } = await import('gif.js');
const gif = new GIF({
workers: 2,
quality: 10,
width: this.canvas.width,
height: this.canvas.height
});
const frameDuration = 1000 / 30; // 30 FPS
const totalFrames = 60; // 2 seconds at 30 FPS
const frameInterval = 1000 / 30;
// Capture frames
for (let frame = 0; frame < totalFrames; frame++) {
exportBtn.textContent = `Capturing ${frame + 1}/${totalFrames}`;
// Wait for frame to render
await new Promise(resolve => setTimeout(resolve, frameInterval));
// Create a copy of the canvas for this frame
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.canvas.width;
tempCanvas.height = this.canvas.height;
const tempCtx = tempCanvas.getContext('2d')!;
tempCtx.drawImage(this.canvas, 0, 0);
gif.addFrame(tempCanvas, { delay: frameDuration });
}
exportBtn.textContent = 'Rendering GIF...';
gif.on('finished', (blob: Blob) => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = `bitfielder-${Date.now()}.gif`;
link.href = url;
link.click();
URL.revokeObjectURL(url);
exportBtn.textContent = originalText;
exportBtn.removeAttribute('disabled');
});
gif.render();
} catch (error) {
console.error('GIF export failed:', error);
exportBtn.textContent = originalText;
exportBtn.removeAttribute('disabled');
// Fallback to PNG
this.exportPNG();
}
}
private loadSettings(): void {
const settings = Storage.getSettings();