lots of updates
This commit is contained in:
@ -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
|
||||
|
||||
54
index.html
54
index.html
@ -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><< >></strong> - Bit shift left/right</p>
|
||||
<p><strong>+ - * / %</strong> - Math operations</p>
|
||||
<p><strong>== != < ></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>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&&y</code>, <code>x||y</code></p>
|
||||
<p>No character or length limits - use any JavaScript!</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
186
src/main.ts
186
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 ? ' <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);
|
||||
|
||||
Reference in New Issue
Block a user