lots of updates

This commit is contained in:
2025-07-06 01:14:43 +02:00
parent 96af50ee6b
commit f84b515523
6 changed files with 382 additions and 53 deletions

View File

@ -2,3 +2,5 @@ x<<127*y*t
x<<20*t*80*y^8
x**10*y^200*t+20
x**10*y^200*t+20
x ^ Math.sin(y ^ x) * Math.sin(t) * Math.PI * 200
x ^ Math.sin(y ^ x) * Math.sin(t) * Math.PI ** 4 | x % 40

View File

@ -784,6 +784,16 @@
<option value="60">60 FPS</option>
</select>
</label>
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
Value Mode:
<select id="value-mode-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
<option value="integer" selected>Integer (0-255)</option>
<option value="float">Float (0.0-1.0)</option>
<option value="polar">Polar (angle-based)</option>
<option value="distance">Distance (radial)</option>
<option value="wave">Wave (ripple)</option>
</select>
</label>
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
Render Mode:
<select id="render-mode-select" style="background: rgba(255,255,255,0.1); border: 1px solid #555; color: #fff; padding: 4px; border-radius: 4px;">
@ -848,6 +858,16 @@
<option value="60">60 FPS</option>
</select>
</div>
<div class="mobile-menu-item">
<label>Value Mode</label>
<select id="value-mode-select-mobile">
<option value="integer" selected>Integer (0-255)</option>
<option value="float">Float (0.0-1.0)</option>
<option value="polar">Polar (angle-based)</option>
<option value="distance">Distance (radial)</option>
<option value="wave">Wave (ripple)</option>
</select>
</div>
<div class="mobile-menu-item">
<label>Render Mode</label>
<select id="render-mode-select-mobile">
@ -960,6 +980,40 @@
<p><strong>^ & |</strong> - XOR, AND, OR</p>
<p><strong>&lt;&lt; &gt;&gt;</strong> - Bit shift left/right</p>
<p><strong>+ - * / %</strong> - Math operations</p>
<p><strong>== != &lt; &gt;</strong> - Comparisons (return 0/1)</p>
<p><strong>? :</strong> - Ternary operator (condition ? true : false)</p>
<p><strong>~ **</strong> - Bitwise NOT, exponentiation</p>
</div>
<div class="help-section">
<h4>Math Functions</h4>
<p><strong>sin, cos, tan</strong> - Trigonometric functions</p>
<p><strong>abs, sqrt, pow</strong> - Absolute, square root, power</p>
<p><strong>floor, ceil, round</strong> - Rounding functions</p>
<p><strong>min, max</strong> - Minimum and maximum</p>
<p><strong>random</strong> - Random number 0-1</p>
<p><strong>log, exp</strong> - Natural logarithm, exponential</p>
<p><strong>PI, E</strong> - Math constants</p>
<p>Use without Math. prefix: <code>sin(x)</code> not <code>Math.sin(x)</code></p>
</div>
<div class="help-section">
<h4>Value Modes</h4>
<p><strong>Integer (0-255):</strong> Traditional mode for large values</p>
<p><strong>Float (0.0-1.0):</strong> Bitfield shader mode, inverts and clamps values</p>
<p><strong>Polar (angle-based):</strong> Spiral patterns combining angle and radius</p>
<p><strong>Distance (radial):</strong> Concentric wave rings with variable frequency</p>
<p><strong>Wave (ripple):</strong> Multi-source interference with amplitude falloff</p>
<p>Each mode transforms your expression differently!</p>
</div>
<div class="help-section">
<h4>Advanced Features</h4>
<p><strong>Array indexing:</strong> <code>[1,2,4,8][floor(t%4)]</code></p>
<p><strong>Complex expressions:</strong> <code>x&gt;y ? sin(x) : cos(y)</code></p>
<p><strong>Nested functions:</strong> <code>pow(sin(x), abs(y-x))</code></p>
<p><strong>Logical operators:</strong> <code>x&amp;&amp;y</code>, <code>x||y</code></p>
<p>No character or length limits - use any JavaScript!</p>
</div>
<div class="help-section">

View File

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

View File

@ -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;
// Limit expression complexity
const complexity = (code.match(/[\(\)]/g) || []).length;
if (complexity > 20) {
throw new Error('Expression too complex');
}
// Replace standalone math functions with Math.function
mathFunctions.forEach(func => {
const regex = new RegExp(`\\b${func}\\(`, 'g');
processedCode = processedCode.replace(regex, `Math.${func}(`);
});
// Limit code length
if (code.length > 200) {
throw new Error('Code too long');
}
// 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');
return code;
return processedCode;
}
private postMessage(response: WorkerResponse): void {

View File

@ -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<AppSettings>): 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);

View File

@ -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 ? ' <span style="color: #4A9EFF; font-size: 10px;" title="Includes visual settings">⚙</span>' : '';
return `
<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>
<span class="shader-name" id="name-${shader.id}">${this.escapeHtml(shader.name)}${settingsIndicator}</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>
@ -617,7 +738,7 @@ class BitfielderApp {
</div>
<div class="shader-code">${this.escapeHtml(shader.code)}</div>
</div>
`).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);