diff --git a/src/FakeShader.ts b/src/FakeShader.ts
index 0954feb..3029568 100644
--- a/src/FakeShader.ts
+++ b/src/FakeShader.ts
@@ -53,6 +53,7 @@ export class FakeShader {
private isRendering: boolean = false;
private pendingRenders: string[] = [];
private renderMode: string = 'classic';
+ private valueMode: string = 'integer';
private offscreenCanvas: OffscreenCanvas | null = null;
private offscreenCtx: OffscreenCanvasRenderingContext2D | null = null;
private useOffscreen: boolean = false;
@@ -230,8 +231,11 @@ export class FakeShader {
type: 'render',
width: this.canvas.width,
height: this.canvas.height,
+ fullWidth: this.canvas.width,
+ fullHeight: this.canvas.height,
time: currentTime,
renderMode: this.renderMode,
+ valueMode: this.valueMode,
mouseX: this.mouseX,
mouseY: this.mouseY,
mousePressed: this.mousePressed,
@@ -282,8 +286,12 @@ export class FakeShader {
height: endY - startY,
// Pass the Y offset for correct coordinate calculation
startY: startY,
+ // Pass full canvas dimensions for center calculations
+ fullWidth: width,
+ fullHeight: height,
time: currentTime,
renderMode: this.renderMode,
+ valueMode: this.valueMode,
mouseX: this.mouseX,
mouseY: this.mouseY,
mousePressed: this.mousePressed,
@@ -379,6 +387,10 @@ export class FakeShader {
this.renderMode = mode;
}
+ setValueMode(mode: string): void {
+ this.valueMode = mode;
+ }
+
setMousePosition(x: number, y: number, pressed: boolean = false, vx: number = 0, vy: number = 0, clickTime: number = 0): void {
this.mouseX = x;
this.mouseY = y;
diff --git a/src/ShaderWorker.ts b/src/ShaderWorker.ts
index 33d820f..612504e 100644
--- a/src/ShaderWorker.ts
+++ b/src/ShaderWorker.ts
@@ -7,7 +7,10 @@ interface WorkerMessage {
height?: number;
time?: number;
renderMode?: string;
+ valueMode?: string; // 'integer' or 'float'
startY?: number; // Y offset for tile rendering
+ fullWidth?: number; // Full canvas width for center calculations
+ fullHeight?: number; // Full canvas height for center calculations
mouseX?: number;
mouseY?: number;
mousePressed?: boolean;
@@ -115,7 +118,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', message, message.startY || 0);
+ this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message.valueMode || 'integer', message, message.startY || 0);
break;
}
} catch (error) {
@@ -220,7 +223,7 @@ class ShaderWorker {
return hash.toString(36);
}
- private renderShader(id: string, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startY: number = 0): void {
+ private renderShader(id: string, width: number, height: number, time: number, renderMode: string, valueMode: string, message: WorkerMessage, startY: number = 0): void {
if (!this.compiledFunction) {
this.postError(id, 'No compiled shader');
return;
@@ -233,14 +236,14 @@ class ShaderWorker {
try {
// Use tiled rendering for better timeout handling
- this.renderTiled(data, width, height, time, renderMode, message, startTime, maxRenderTime, startY);
+ this.renderTiled(data, width, height, time, renderMode, valueMode, message, startTime, maxRenderTime, startY);
this.postMessage({ id, type: 'rendered', success: true, imageData });
} catch (error) {
this.postError(id, error instanceof Error ? error.message : 'Render failed');
}
}
- private renderTiled(data: Uint8ClampedArray, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startTime: number, maxRenderTime: number, yOffset: number = 0): void {
+ private renderTiled(data: Uint8ClampedArray, width: number, height: number, time: number, renderMode: string, valueMode: string, message: WorkerMessage, startTime: number, maxRenderTime: number, yOffset: number = 0): void {
const tileSize = 64; // 64x64 tiles for better granularity
const tilesX = Math.ceil(width / tileSize);
const tilesY = Math.ceil(height / tileSize);
@@ -260,12 +263,15 @@ class ShaderWorker {
const tileEndX = Math.min(tileStartX + tileSize, width);
const tileEndY = Math.min(tileStartY + tileSize, height);
- this.renderTile(data, width, tileStartX, tileStartY, tileEndX, tileEndY, time, renderMode, message, yOffset);
+ this.renderTile(data, width, tileStartX, tileStartY, tileEndX, tileEndY, time, renderMode, valueMode, message, yOffset);
}
}
}
- private renderTile(data: Uint8ClampedArray, width: number, startX: number, startY: number, endX: number, endY: number, time: number, renderMode: string, message: WorkerMessage, yOffset: number = 0): void {
+ private renderTile(data: Uint8ClampedArray, width: number, startX: number, startY: number, endX: number, endY: number, time: number, renderMode: string, valueMode: string, message: WorkerMessage, yOffset: number = 0): void {
+ // Get full canvas dimensions for special modes (use provided full dimensions or fall back)
+ const fullWidth = message.fullWidth || width;
+ const fullHeight = message.fullHeight || (message.height! + yOffset);
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const i = (y * width + x) * 4;
@@ -290,7 +296,7 @@ class ShaderWorker {
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
);
const safeValue = isFinite(value) ? value : 0;
- const [r, g, b] = this.calculateColor(safeValue, renderMode);
+ const [r, g, b] = this.calculateColor(safeValue, renderMode, valueMode, x, actualY, fullWidth, fullHeight);
data[i] = r;
data[i + 1] = g;
@@ -446,18 +452,96 @@ class ShaderWorker {
return imageData;
}
- private calculateColor(value: number, renderMode: string): [number, number, number] {
- const absValue = Math.abs(value) % 256;
+ private calculateColor(value: number, renderMode: string, valueMode: string = 'integer', x: number = 0, y: number = 0, width: number = 1, height: number = 1): [number, number, number] {
+ let processedValue: number;
+
+ switch (valueMode) {
+ case 'float':
+ // Float mode: treat value as 0.0-1.0, invert it (like original bitfield shaders)
+ processedValue = Math.max(0, Math.min(1, Math.abs(value))); // Clamp to 0-1
+ processedValue = 1 - processedValue; // Invert (like original)
+ processedValue = Math.floor(processedValue * 255); // Convert to 0-255
+ break;
+
+ case 'polar':
+ // Polar mode: angular patterns with value-based rotation and radius influence
+ const centerX = width / 2;
+ const centerY = height / 2;
+ const dx = x - centerX;
+ const dy = y - centerY;
+ const radius = Math.sqrt(dx * dx + dy * dy);
+ const angle = Math.atan2(dy, dx); // -π to π
+ const normalizedAngle = (angle + Math.PI) / (2 * Math.PI); // 0 to 1
+
+ // Combine angle with radius and value for complex patterns
+ const radiusNorm = radius / Math.max(centerX, centerY);
+ const spiralEffect = (normalizedAngle + radiusNorm * 0.5 + Math.abs(value) * 0.02) % 1;
+ const polarValue = Math.sin(spiralEffect * Math.PI * 8) * 0.5 + 0.5; // Create wave pattern
+
+ processedValue = Math.floor(polarValue * 255);
+ break;
+
+ case 'distance':
+ // Distance mode: concentric patterns with value-based frequency and phase
+ const distCenterX = width / 2;
+ const distCenterY = height / 2;
+ const distance = Math.sqrt((x - distCenterX) ** 2 + (y - distCenterY) ** 2);
+ const maxDistance = Math.sqrt(distCenterX ** 2 + distCenterY ** 2);
+ const normalizedDistance = distance / maxDistance; // 0 to 1
+
+ // Create concentric waves with value-controlled frequency and phase
+ const frequency = 8 + Math.abs(value) * 0.1; // Variable frequency
+ const phase = Math.abs(value) * 0.05; // Value affects phase shift
+ const concentricWave = Math.sin(normalizedDistance * Math.PI * frequency + phase) * 0.5 + 0.5;
+
+ // Add some radial falloff for more interesting patterns
+ const falloff = 1 - Math.pow(normalizedDistance, 0.8);
+ const distanceValue = concentricWave * falloff;
+
+ processedValue = Math.floor(distanceValue * 255);
+ break;
+
+ case 'wave':
+ // Wave mode: interference patterns from multiple wave sources
+ const baseFreq = 0.08;
+ const valueScale = Math.abs(value) * 0.001 + 1; // Scale frequency by value
+ let waveSum = 0;
+
+ // Create wave sources at strategic positions for interesting interference
+ const sources = [
+ { x: width * 0.3, y: height * 0.3 },
+ { x: width * 0.7, y: height * 0.3 },
+ { x: width * 0.5, y: height * 0.7 },
+ { x: width * 0.2, y: height * 0.8 },
+ ];
+
+ for (const source of sources) {
+ const dist = Math.sqrt((x - source.x) ** 2 + (y - source.y) ** 2);
+ const wave = Math.sin(dist * baseFreq * valueScale + Math.abs(value) * 0.02);
+ const amplitude = 1 / (1 + dist * 0.002); // Distance-based amplitude falloff
+ waveSum += wave * amplitude;
+ }
+
+ // Normalize and enhance contrast
+ const waveValue = Math.tanh(waveSum) * 0.5 + 0.5; // tanh for better contrast
+ processedValue = Math.floor(waveValue * 255);
+ break;
+
+ default:
+ // Integer mode: treat value as 0-255 (original behavior)
+ processedValue = Math.abs(value) % 256;
+ break;
+ }
// Use pre-computed color table if available
const colorTable = this.colorTables.get(renderMode);
if (colorTable) {
- const index = Math.floor(absValue) * 3;
+ const index = Math.floor(processedValue) * 3;
return [colorTable[index], colorTable[index + 1], colorTable[index + 2]];
}
// Fallback to direct calculation
- return this.calculateColorDirect(absValue, renderMode);
+ return this.calculateColorDirect(processedValue, renderMode);
}
private calculateColorDirect(absValue: number, renderMode: string): [number, number, number] {
@@ -639,42 +723,34 @@ class ShaderWorker {
}
private sanitizeCode(code: string): string {
- // Strict whitelist approach - extended to include new interaction variables
- // Variables: x, y, t, i, mouseX, mouseY, mousePressed, mouseVX, mouseVY, mouseClickTime, touchCount, touch0X, touch0Y, touch1X, touch1Y, pinchScale, pinchRotation, accelX, accelY, accelZ, gyroX, gyroY, gyroZ
- 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'
+ // Auto-prefix Math functions
+ const mathFunctions = [
+ 'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp',
+ 'floor', 'log', 'max', 'min', 'pow', 'random', 'round', 'sin',
+ 'sqrt', 'tan', 'trunc', 'sign', 'cbrt', 'hypot', 'imul', 'fround',
+ 'clz32', 'acosh', 'asinh', 'atanh', 'cosh', 'sinh', 'tanh', 'expm1',
+ 'log1p', 'log10', 'log2'
];
- const codeWords = code.toLowerCase().split(/[^a-z]/);
- for (const keyword of dangerousKeywords) {
- if (codeWords.includes(keyword)) {
- throw new Error(`Forbidden keyword: ${keyword}`);
- }
- }
+ let processedCode = code;
+
+ // Replace standalone math functions with Math.function
+ mathFunctions.forEach(func => {
+ const regex = new RegExp(`\\b${func}\\(`, 'g');
+ processedCode = processedCode.replace(regex, `Math.${func}(`);
+ });
- // Limit expression complexity
- const complexity = (code.match(/[\(\)]/g) || []).length;
- if (complexity > 20) {
- throw new Error('Expression too complex');
- }
+ // Add Math constants
+ processedCode = processedCode.replace(/\bPI\b/g, 'Math.PI');
+ processedCode = processedCode.replace(/\bE\b/g, 'Math.E');
+ processedCode = processedCode.replace(/\bLN2\b/g, 'Math.LN2');
+ processedCode = processedCode.replace(/\bLN10\b/g, 'Math.LN10');
+ processedCode = processedCode.replace(/\bLOG2E\b/g, 'Math.LOG2E');
+ processedCode = processedCode.replace(/\bLOG10E\b/g, 'Math.LOG10E');
+ processedCode = processedCode.replace(/\bSQRT1_2\b/g, 'Math.SQRT1_2');
+ processedCode = processedCode.replace(/\bSQRT2\b/g, 'Math.SQRT2');
- // Limit code length
- if (code.length > 200) {
- throw new Error('Code too long');
- }
-
- return code;
+ return processedCode;
}
private postMessage(response: WorkerResponse): void {
diff --git a/src/Storage.ts b/src/Storage.ts
index 869958d..d3c14dc 100644
--- a/src/Storage.ts
+++ b/src/Storage.ts
@@ -4,6 +4,12 @@ interface SavedShader {
code: string;
created: number;
lastUsed: number;
+ // Visual settings
+ resolution?: number;
+ fps?: number;
+ renderMode?: string;
+ valueMode?: string;
+ uiOpacity?: number;
}
interface AppSettings {
@@ -11,6 +17,7 @@ interface AppSettings {
fps: number;
lastShaderCode: string;
renderMode: string;
+ valueMode?: string;
uiOpacity?: number;
}
@@ -18,7 +25,7 @@ export class Storage {
private static readonly SHADERS_KEY = 'bitfielder_shaders';
private static readonly SETTINGS_KEY = 'bitfielder_settings';
- static saveShader(name: string, code: string): SavedShader {
+ static saveShader(name: string, code: string, settings?: Partial
): SavedShader {
const shaders = this.getShaders();
const id = this.generateId();
const timestamp = Date.now();
@@ -28,7 +35,15 @@ export class Storage {
name: name.trim() || `Shader ${shaders.length + 1}`,
code,
created: timestamp,
- lastUsed: timestamp
+ lastUsed: timestamp,
+ // Include settings if provided
+ ...(settings && {
+ resolution: settings.resolution,
+ fps: settings.fps,
+ renderMode: settings.renderMode,
+ valueMode: settings.valueMode,
+ uiOpacity: settings.uiOpacity
+ })
};
shaders.push(shader);
diff --git a/src/main.ts b/src/main.ts
index b3aa80b..6f6e90a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -50,6 +50,10 @@ class BitfielderApp {
this.setupCanvas();
this.shader = new FakeShader(this.canvas, this.editor.value);
+ // Apply initial settings to shader
+ const settings = Storage.getSettings();
+ this.shader.setValueMode(settings.valueMode || 'integer');
+
this.setupEventListeners();
this.initializeIcons();
this.loadFromURL();
@@ -90,6 +94,7 @@ class BitfielderApp {
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;
+ const valueModeSelect = document.getElementById('value-mode-select') as HTMLSelectElement;
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
const evalBtn = document.getElementById('eval-btn')!;
const helpPopup = document.getElementById('help-popup')!;
@@ -106,6 +111,7 @@ class BitfielderApp {
const resolutionSelectMobile = document.getElementById('resolution-select-mobile') as HTMLSelectElement;
const fpsSelectMobile = document.getElementById('fps-select-mobile') as HTMLSelectElement;
const renderModeSelectMobile = document.getElementById('render-mode-select-mobile') as HTMLSelectElement;
+ const valueModeSelectMobile = document.getElementById('value-mode-select-mobile') as HTMLSelectElement;
const opacitySliderMobile = document.getElementById('opacity-slider-mobile') as HTMLInputElement;
// Mobile menu buttons
@@ -131,6 +137,7 @@ class BitfielderApp {
resolutionSelect.addEventListener('change', () => this.updateResolution());
fpsSelect.addEventListener('change', () => this.updateFPS());
renderModeSelect.addEventListener('change', () => this.updateRenderMode());
+ valueModeSelect.addEventListener('change', () => this.updateValueMode());
opacitySlider.addEventListener('input', () => this.updateUIOpacity());
evalBtn.addEventListener('click', () => this.evalShader());
closeBtn.addEventListener('click', () => this.hideHelp());
@@ -164,6 +171,10 @@ class BitfielderApp {
renderModeSelect.value = renderModeSelectMobile.value;
this.updateRenderMode();
});
+ valueModeSelectMobile.addEventListener('change', () => {
+ valueModeSelect.value = valueModeSelectMobile.value;
+ this.updateValueMode();
+ });
opacitySliderMobile.addEventListener('input', () => {
opacitySlider.value = opacitySliderMobile.value;
this.updateUIOpacity();
@@ -405,7 +416,23 @@ class BitfielderApp {
}
private shareURL(): void {
- const encoded = btoa(this.editor.value);
+ // Gather all current settings
+ 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;
+ const valueModeSelect = document.getElementById('value-mode-select') as HTMLSelectElement;
+ const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
+
+ const shareData = {
+ code: this.editor.value,
+ resolution: resolutionSelect ? parseInt(resolutionSelect.value) : 1,
+ fps: fpsSelect ? parseInt(fpsSelect.value) : 30,
+ renderMode: renderModeSelect ? renderModeSelect.value : 'classic',
+ valueMode: valueModeSelect ? valueModeSelect.value : 'integer',
+ uiOpacity: opacitySlider ? parseInt(opacitySlider.value) / 100 : 0.3
+ };
+
+ const encoded = btoa(JSON.stringify(shareData));
window.location.hash = encoded;
navigator.clipboard.writeText(window.location.href).then(() => {
@@ -419,9 +446,68 @@ class BitfielderApp {
if (window.location.hash) {
try {
const decoded = atob(window.location.hash.substring(1));
- this.editor.value = decoded;
- this.shader.setCode(decoded);
- this.render();
+
+ // Try to parse as JSON first (new format with settings)
+ try {
+ const shareData = JSON.parse(decoded);
+
+ // Apply the shared settings
+ this.editor.value = shareData.code;
+
+ // Update UI controls
+ if (shareData.resolution) {
+ const resSelect = document.getElementById('resolution-select') as HTMLSelectElement;
+ const resSelectMobile = document.getElementById('resolution-select-mobile') as HTMLSelectElement;
+ if (resSelect) resSelect.value = shareData.resolution.toString();
+ if (resSelectMobile) resSelectMobile.value = shareData.resolution.toString();
+ }
+ if (shareData.fps) {
+ const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
+ const fpsSelectMobile = document.getElementById('fps-select-mobile') as HTMLSelectElement;
+ if (fpsSelect) fpsSelect.value = shareData.fps.toString();
+ if (fpsSelectMobile) fpsSelectMobile.value = shareData.fps.toString();
+ }
+ if (shareData.renderMode) {
+ const renderSelect = document.getElementById('render-mode-select') as HTMLSelectElement;
+ const renderSelectMobile = document.getElementById('render-mode-select-mobile') as HTMLSelectElement;
+ if (renderSelect) renderSelect.value = shareData.renderMode;
+ if (renderSelectMobile) renderSelectMobile.value = shareData.renderMode;
+ }
+ if (shareData.valueMode) {
+ const valueSelect = document.getElementById('value-mode-select') as HTMLSelectElement;
+ const valueSelectMobile = document.getElementById('value-mode-select-mobile') as HTMLSelectElement;
+ if (valueSelect) valueSelect.value = shareData.valueMode;
+ if (valueSelectMobile) valueSelectMobile.value = shareData.valueMode;
+ }
+ if (shareData.uiOpacity !== undefined) {
+ const opacityValue = Math.round(shareData.uiOpacity * 100);
+ const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
+ const opacitySliderMobile = document.getElementById('opacity-slider-mobile') as HTMLInputElement;
+ const opacityValueEl = document.getElementById('opacity-value');
+ const opacityValueMobileEl = document.getElementById('opacity-value-mobile');
+
+ if (opacitySlider) opacitySlider.value = opacityValue.toString();
+ if (opacitySliderMobile) opacitySliderMobile.value = opacityValue.toString();
+ if (opacityValueEl) opacityValueEl.textContent = `${opacityValue}%`;
+ if (opacityValueMobileEl) opacityValueMobileEl.textContent = `${opacityValue}%`;
+ document.documentElement.style.setProperty('--ui-opacity', shareData.uiOpacity.toString());
+ }
+
+ // Apply settings to shader and re-render
+ if (shareData.resolution) this.updateResolution();
+ if (shareData.fps) this.updateFPS();
+ if (shareData.renderMode) this.updateRenderMode();
+ if (shareData.valueMode) this.updateValueMode();
+
+ this.shader.setCode(shareData.code);
+ this.render();
+
+ } catch (jsonError) {
+ // Fall back to old format (just code as string)
+ this.editor.value = decoded;
+ this.shader.setCode(decoded);
+ this.render();
+ }
} catch (e) {
console.error('Failed to decode URL hash:', e);
}
@@ -448,6 +534,14 @@ class BitfielderApp {
Storage.saveSettings({ renderMode });
}
+ private updateValueMode(): void {
+ const valueModeSelect = document.getElementById('value-mode-select') as HTMLSelectElement;
+ const valueMode = valueModeSelect.value;
+ this.shader.setValueMode(valueMode);
+ this.render();
+ Storage.saveSettings({ valueMode });
+ }
+
private updateUIOpacity(): void {
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
const opacityValue = document.getElementById('opacity-value')!;
@@ -550,11 +644,19 @@ class BitfielderApp {
(document.getElementById('resolution-select') as HTMLSelectElement).value = settings.resolution.toString();
(document.getElementById('fps-select') as HTMLSelectElement).value = settings.fps.toString();
(document.getElementById('render-mode-select') as HTMLSelectElement).value = settings.renderMode || 'classic';
+ const valueModeSelect = document.getElementById('value-mode-select') as HTMLSelectElement;
+ if (valueModeSelect) {
+ valueModeSelect.value = settings.valueMode || 'integer';
+ }
// Sync mobile controls
(document.getElementById('resolution-select-mobile') as HTMLSelectElement).value = settings.resolution.toString();
(document.getElementById('fps-select-mobile') as HTMLSelectElement).value = settings.fps.toString();
(document.getElementById('render-mode-select-mobile') as HTMLSelectElement).value = settings.renderMode || 'classic';
+ const valueModeSelectMobile = document.getElementById('value-mode-select-mobile') as HTMLSelectElement;
+ if (valueModeSelectMobile) {
+ valueModeSelectMobile.value = settings.valueMode || 'integer';
+ }
// Apply UI opacity
const opacity = settings.uiOpacity ?? 0.3;
@@ -577,7 +679,22 @@ class BitfielderApp {
if (!code) return;
- Storage.saveShader(name, code);
+ // Gather current settings (similar to shareURL but for library)
+ 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;
+ const valueModeSelect = document.getElementById('value-mode-select') as HTMLSelectElement;
+ const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
+
+ const currentSettings = {
+ resolution: resolutionSelect ? parseInt(resolutionSelect.value) : 1,
+ fps: fpsSelect ? parseInt(fpsSelect.value) : 30,
+ renderMode: renderModeSelect ? renderModeSelect.value : 'classic',
+ valueMode: valueModeSelect ? valueModeSelect.value : 'integer',
+ uiOpacity: opacitySlider ? parseInt(opacitySlider.value) / 100 : 0.3
+ };
+
+ Storage.saveShader(name, code, currentSettings);
nameInput.value = '';
this.renderShaderLibrary();
@@ -606,10 +723,14 @@ class BitfielderApp {
return;
}
- shaderList.innerHTML = shaders.map(shader => `
+ shaderList.innerHTML = shaders.map(shader => {
+ const hasSettings = shader.resolution || shader.fps || shader.renderMode || shader.valueMode || shader.uiOpacity !== undefined;
+ const settingsIndicator = hasSettings ? ' ⚙' : '';
+
+ return `
- `).join('');
+ `}).join('');
}
private escapeHtml(text: string): string {
@@ -633,7 +754,56 @@ class BitfielderApp {
const shaders = Storage.getShaders();
const shader = shaders.find(s => s.id === id);
if (shader) {
+ // Load the code
this.editor.value = shader.code;
+
+ // Apply saved settings if they exist
+ if (shader.resolution) {
+ const resSelect = document.getElementById('resolution-select') as HTMLSelectElement;
+ const resSelectMobile = document.getElementById('resolution-select-mobile') as HTMLSelectElement;
+ if (resSelect) resSelect.value = shader.resolution.toString();
+ if (resSelectMobile) resSelectMobile.value = shader.resolution.toString();
+ this.updateResolution();
+ }
+
+ if (shader.fps) {
+ const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
+ const fpsSelectMobile = document.getElementById('fps-select-mobile') as HTMLSelectElement;
+ if (fpsSelect) fpsSelect.value = shader.fps.toString();
+ if (fpsSelectMobile) fpsSelectMobile.value = shader.fps.toString();
+ this.updateFPS();
+ }
+
+ if (shader.renderMode) {
+ const renderSelect = document.getElementById('render-mode-select') as HTMLSelectElement;
+ const renderSelectMobile = document.getElementById('render-mode-select-mobile') as HTMLSelectElement;
+ if (renderSelect) renderSelect.value = shader.renderMode;
+ if (renderSelectMobile) renderSelectMobile.value = shader.renderMode;
+ this.updateRenderMode();
+ }
+
+ if (shader.valueMode) {
+ const valueSelect = document.getElementById('value-mode-select') as HTMLSelectElement;
+ const valueSelectMobile = document.getElementById('value-mode-select-mobile') as HTMLSelectElement;
+ if (valueSelect) valueSelect.value = shader.valueMode;
+ if (valueSelectMobile) valueSelectMobile.value = shader.valueMode;
+ this.updateValueMode();
+ }
+
+ if (shader.uiOpacity !== undefined) {
+ const opacityValue = Math.round(shader.uiOpacity * 100);
+ const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
+ const opacitySliderMobile = document.getElementById('opacity-slider-mobile') as HTMLInputElement;
+ const opacityValueEl = document.getElementById('opacity-value');
+ const opacityValueMobileEl = document.getElementById('opacity-value-mobile');
+
+ if (opacitySlider) opacitySlider.value = opacityValue.toString();
+ if (opacitySliderMobile) opacitySliderMobile.value = opacityValue.toString();
+ if (opacityValueEl) opacityValueEl.textContent = `${opacityValue}%`;
+ if (opacityValueMobileEl) opacityValueMobileEl.textContent = `${opacityValue}%`;
+ document.documentElement.style.setProperty('--ui-opacity', shader.uiOpacity.toString());
+ }
+
this.shader.setCode(shader.code);
this.render();
Storage.updateShaderUsage(id);