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

@ -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();