smol update
This commit is contained in:
16
index.html
16
index.html
@ -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
4
package-lock.json
generated
@ -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": {
|
||||
|
||||
@ -34,9 +34,6 @@
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"gif.js": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
24
src/gif.d.ts
vendored
@ -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;
|
||||
}
|
||||
90
src/main.ts
90
src/main.ts
@ -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();
|
||||
|
||||
Reference in New Issue
Block a user