lots of updates
This commit is contained in:
@ -2,3 +2,5 @@ x<<127*y*t
|
|||||||
x<<20*t*80*y^8
|
x<<20*t*80*y^8
|
||||||
x**10*y^200*t+20
|
x**10*y^200*t+20
|
||||||
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>
|
<option value="60">60 FPS</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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;">
|
<label style="color: #ccc; font-size: 12px; margin-right: 10px;">
|
||||||
Render Mode:
|
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;">
|
<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>
|
<option value="60">60 FPS</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="mobile-menu-item">
|
||||||
<label>Render Mode</label>
|
<label>Render Mode</label>
|
||||||
<select id="render-mode-select-mobile">
|
<select id="render-mode-select-mobile">
|
||||||
@ -960,6 +980,40 @@
|
|||||||
<p><strong>^ & |</strong> - XOR, AND, OR</p>
|
<p><strong>^ & |</strong> - XOR, AND, OR</p>
|
||||||
<p><strong><< >></strong> - Bit shift left/right</p>
|
<p><strong><< >></strong> - Bit shift left/right</p>
|
||||||
<p><strong>+ - * / %</strong> - Math operations</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>
|
||||||
|
|
||||||
<div class="help-section">
|
<div class="help-section">
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export class FakeShader {
|
|||||||
private isRendering: boolean = false;
|
private isRendering: boolean = false;
|
||||||
private pendingRenders: string[] = [];
|
private pendingRenders: string[] = [];
|
||||||
private renderMode: string = 'classic';
|
private renderMode: string = 'classic';
|
||||||
|
private valueMode: string = 'integer';
|
||||||
private offscreenCanvas: OffscreenCanvas | null = null;
|
private offscreenCanvas: OffscreenCanvas | null = null;
|
||||||
private offscreenCtx: OffscreenCanvasRenderingContext2D | null = null;
|
private offscreenCtx: OffscreenCanvasRenderingContext2D | null = null;
|
||||||
private useOffscreen: boolean = false;
|
private useOffscreen: boolean = false;
|
||||||
@ -230,8 +231,11 @@ export class FakeShader {
|
|||||||
type: 'render',
|
type: 'render',
|
||||||
width: this.canvas.width,
|
width: this.canvas.width,
|
||||||
height: this.canvas.height,
|
height: this.canvas.height,
|
||||||
|
fullWidth: this.canvas.width,
|
||||||
|
fullHeight: this.canvas.height,
|
||||||
time: currentTime,
|
time: currentTime,
|
||||||
renderMode: this.renderMode,
|
renderMode: this.renderMode,
|
||||||
|
valueMode: this.valueMode,
|
||||||
mouseX: this.mouseX,
|
mouseX: this.mouseX,
|
||||||
mouseY: this.mouseY,
|
mouseY: this.mouseY,
|
||||||
mousePressed: this.mousePressed,
|
mousePressed: this.mousePressed,
|
||||||
@ -282,8 +286,12 @@ export class FakeShader {
|
|||||||
height: endY - startY,
|
height: endY - startY,
|
||||||
// Pass the Y offset for correct coordinate calculation
|
// Pass the Y offset for correct coordinate calculation
|
||||||
startY: startY,
|
startY: startY,
|
||||||
|
// Pass full canvas dimensions for center calculations
|
||||||
|
fullWidth: width,
|
||||||
|
fullHeight: height,
|
||||||
time: currentTime,
|
time: currentTime,
|
||||||
renderMode: this.renderMode,
|
renderMode: this.renderMode,
|
||||||
|
valueMode: this.valueMode,
|
||||||
mouseX: this.mouseX,
|
mouseX: this.mouseX,
|
||||||
mouseY: this.mouseY,
|
mouseY: this.mouseY,
|
||||||
mousePressed: this.mousePressed,
|
mousePressed: this.mousePressed,
|
||||||
@ -379,6 +387,10 @@ export class FakeShader {
|
|||||||
this.renderMode = mode;
|
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 {
|
setMousePosition(x: number, y: number, pressed: boolean = false, vx: number = 0, vy: number = 0, clickTime: number = 0): void {
|
||||||
this.mouseX = x;
|
this.mouseX = x;
|
||||||
this.mouseY = y;
|
this.mouseY = y;
|
||||||
|
|||||||
@ -7,7 +7,10 @@ interface WorkerMessage {
|
|||||||
height?: number;
|
height?: number;
|
||||||
time?: number;
|
time?: number;
|
||||||
renderMode?: string;
|
renderMode?: string;
|
||||||
|
valueMode?: string; // 'integer' or 'float'
|
||||||
startY?: number; // Y offset for tile rendering
|
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;
|
mouseX?: number;
|
||||||
mouseY?: number;
|
mouseY?: number;
|
||||||
mousePressed?: boolean;
|
mousePressed?: boolean;
|
||||||
@ -115,7 +118,7 @@ class ShaderWorker {
|
|||||||
this.compileShader(message.id, message.code!);
|
this.compileShader(message.id, message.code!);
|
||||||
break;
|
break;
|
||||||
case 'render':
|
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;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -220,7 +223,7 @@ class ShaderWorker {
|
|||||||
return hash.toString(36);
|
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) {
|
if (!this.compiledFunction) {
|
||||||
this.postError(id, 'No compiled shader');
|
this.postError(id, 'No compiled shader');
|
||||||
return;
|
return;
|
||||||
@ -233,14 +236,14 @@ class ShaderWorker {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use tiled rendering for better timeout handling
|
// 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 });
|
this.postMessage({ id, type: 'rendered', success: true, imageData });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.postError(id, error instanceof Error ? error.message : 'Render failed');
|
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 tileSize = 64; // 64x64 tiles for better granularity
|
||||||
const tilesX = Math.ceil(width / tileSize);
|
const tilesX = Math.ceil(width / tileSize);
|
||||||
const tilesY = Math.ceil(height / tileSize);
|
const tilesY = Math.ceil(height / tileSize);
|
||||||
@ -260,12 +263,15 @@ class ShaderWorker {
|
|||||||
const tileEndX = Math.min(tileStartX + tileSize, width);
|
const tileEndX = Math.min(tileStartX + tileSize, width);
|
||||||
const tileEndY = Math.min(tileStartY + tileSize, height);
|
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 y = startY; y < endY; y++) {
|
||||||
for (let x = startX; x < endX; x++) {
|
for (let x = startX; x < endX; x++) {
|
||||||
const i = (y * width + x) * 4;
|
const i = (y * width + x) * 4;
|
||||||
@ -290,7 +296,7 @@ class ShaderWorker {
|
|||||||
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
|
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
|
||||||
);
|
);
|
||||||
const safeValue = isFinite(value) ? value : 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] = r;
|
||||||
data[i + 1] = g;
|
data[i + 1] = g;
|
||||||
@ -446,18 +452,96 @@ class ShaderWorker {
|
|||||||
return imageData;
|
return imageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateColor(value: number, renderMode: string): [number, number, number] {
|
private calculateColor(value: number, renderMode: string, valueMode: string = 'integer', x: number = 0, y: number = 0, width: number = 1, height: number = 1): [number, number, number] {
|
||||||
const absValue = Math.abs(value) % 256;
|
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
|
// Use pre-computed color table if available
|
||||||
const colorTable = this.colorTables.get(renderMode);
|
const colorTable = this.colorTables.get(renderMode);
|
||||||
if (colorTable) {
|
if (colorTable) {
|
||||||
const index = Math.floor(absValue) * 3;
|
const index = Math.floor(processedValue) * 3;
|
||||||
return [colorTable[index], colorTable[index + 1], colorTable[index + 2]];
|
return [colorTable[index], colorTable[index + 1], colorTable[index + 2]];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to direct calculation
|
// Fallback to direct calculation
|
||||||
return this.calculateColorDirect(absValue, renderMode);
|
return this.calculateColorDirect(processedValue, renderMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateColorDirect(absValue: number, renderMode: string): [number, number, number] {
|
private calculateColorDirect(absValue: number, renderMode: string): [number, number, number] {
|
||||||
@ -639,42 +723,34 @@ class ShaderWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeCode(code: string): string {
|
private sanitizeCode(code: string): string {
|
||||||
// Strict whitelist approach - extended to include new interaction variables
|
// Auto-prefix Math functions
|
||||||
// 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 mathFunctions = [
|
||||||
const allowedPattern = /^[0-9a-zA-Z\s\+\-\*\/\%\^\&\|\(\)\<\>\~\?:,\.xyti]+$/;
|
'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp',
|
||||||
|
'floor', 'log', 'max', 'min', 'pow', 'random', 'round', 'sin',
|
||||||
if (!allowedPattern.test(code)) {
|
'sqrt', 'tan', 'trunc', 'sign', 'cbrt', 'hypot', 'imul', 'fround',
|
||||||
throw new Error('Invalid characters in shader code');
|
'clz32', 'acosh', 'asinh', 'atanh', 'cosh', 'sinh', 'tanh', 'expm1',
|
||||||
}
|
'log1p', 'log10', 'log2'
|
||||||
|
|
||||||
// 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'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const codeWords = code.toLowerCase().split(/[^a-z]/);
|
let processedCode = code;
|
||||||
for (const keyword of dangerousKeywords) {
|
|
||||||
if (codeWords.includes(keyword)) {
|
// Replace standalone math functions with Math.function
|
||||||
throw new Error(`Forbidden keyword: ${keyword}`);
|
mathFunctions.forEach(func => {
|
||||||
}
|
const regex = new RegExp(`\\b${func}\\(`, 'g');
|
||||||
}
|
processedCode = processedCode.replace(regex, `Math.${func}(`);
|
||||||
|
});
|
||||||
|
|
||||||
// Limit expression complexity
|
// Add Math constants
|
||||||
const complexity = (code.match(/[\(\)]/g) || []).length;
|
processedCode = processedCode.replace(/\bPI\b/g, 'Math.PI');
|
||||||
if (complexity > 20) {
|
processedCode = processedCode.replace(/\bE\b/g, 'Math.E');
|
||||||
throw new Error('Expression too complex');
|
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
|
return processedCode;
|
||||||
if (code.length > 200) {
|
|
||||||
throw new Error('Code too long');
|
|
||||||
}
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private postMessage(response: WorkerResponse): void {
|
private postMessage(response: WorkerResponse): void {
|
||||||
|
|||||||
@ -4,6 +4,12 @@ interface SavedShader {
|
|||||||
code: string;
|
code: string;
|
||||||
created: number;
|
created: number;
|
||||||
lastUsed: number;
|
lastUsed: number;
|
||||||
|
// Visual settings
|
||||||
|
resolution?: number;
|
||||||
|
fps?: number;
|
||||||
|
renderMode?: string;
|
||||||
|
valueMode?: string;
|
||||||
|
uiOpacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppSettings {
|
interface AppSettings {
|
||||||
@ -11,6 +17,7 @@ interface AppSettings {
|
|||||||
fps: number;
|
fps: number;
|
||||||
lastShaderCode: string;
|
lastShaderCode: string;
|
||||||
renderMode: string;
|
renderMode: string;
|
||||||
|
valueMode?: string;
|
||||||
uiOpacity?: number;
|
uiOpacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +25,7 @@ export class Storage {
|
|||||||
private static readonly SHADERS_KEY = 'bitfielder_shaders';
|
private static readonly SHADERS_KEY = 'bitfielder_shaders';
|
||||||
private static readonly SETTINGS_KEY = 'bitfielder_settings';
|
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 shaders = this.getShaders();
|
||||||
const id = this.generateId();
|
const id = this.generateId();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
@ -28,7 +35,15 @@ export class Storage {
|
|||||||
name: name.trim() || `Shader ${shaders.length + 1}`,
|
name: name.trim() || `Shader ${shaders.length + 1}`,
|
||||||
code,
|
code,
|
||||||
created: timestamp,
|
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);
|
shaders.push(shader);
|
||||||
|
|||||||
186
src/main.ts
186
src/main.ts
@ -50,6 +50,10 @@ class BitfielderApp {
|
|||||||
this.setupCanvas();
|
this.setupCanvas();
|
||||||
this.shader = new FakeShader(this.canvas, this.editor.value);
|
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.setupEventListeners();
|
||||||
this.initializeIcons();
|
this.initializeIcons();
|
||||||
this.loadFromURL();
|
this.loadFromURL();
|
||||||
@ -90,6 +94,7 @@ class BitfielderApp {
|
|||||||
const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement;
|
const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement;
|
||||||
const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
|
const fpsSelect = document.getElementById('fps-select') as HTMLSelectElement;
|
||||||
const renderModeSelect = document.getElementById('render-mode-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 opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
||||||
const evalBtn = document.getElementById('eval-btn')!;
|
const evalBtn = document.getElementById('eval-btn')!;
|
||||||
const helpPopup = document.getElementById('help-popup')!;
|
const helpPopup = document.getElementById('help-popup')!;
|
||||||
@ -106,6 +111,7 @@ class BitfielderApp {
|
|||||||
const resolutionSelectMobile = document.getElementById('resolution-select-mobile') as HTMLSelectElement;
|
const resolutionSelectMobile = document.getElementById('resolution-select-mobile') as HTMLSelectElement;
|
||||||
const fpsSelectMobile = document.getElementById('fps-select-mobile') as HTMLSelectElement;
|
const fpsSelectMobile = document.getElementById('fps-select-mobile') as HTMLSelectElement;
|
||||||
const renderModeSelectMobile = document.getElementById('render-mode-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;
|
const opacitySliderMobile = document.getElementById('opacity-slider-mobile') as HTMLInputElement;
|
||||||
|
|
||||||
// Mobile menu buttons
|
// Mobile menu buttons
|
||||||
@ -131,6 +137,7 @@ class BitfielderApp {
|
|||||||
resolutionSelect.addEventListener('change', () => this.updateResolution());
|
resolutionSelect.addEventListener('change', () => this.updateResolution());
|
||||||
fpsSelect.addEventListener('change', () => this.updateFPS());
|
fpsSelect.addEventListener('change', () => this.updateFPS());
|
||||||
renderModeSelect.addEventListener('change', () => this.updateRenderMode());
|
renderModeSelect.addEventListener('change', () => this.updateRenderMode());
|
||||||
|
valueModeSelect.addEventListener('change', () => this.updateValueMode());
|
||||||
opacitySlider.addEventListener('input', () => this.updateUIOpacity());
|
opacitySlider.addEventListener('input', () => this.updateUIOpacity());
|
||||||
evalBtn.addEventListener('click', () => this.evalShader());
|
evalBtn.addEventListener('click', () => this.evalShader());
|
||||||
closeBtn.addEventListener('click', () => this.hideHelp());
|
closeBtn.addEventListener('click', () => this.hideHelp());
|
||||||
@ -164,6 +171,10 @@ class BitfielderApp {
|
|||||||
renderModeSelect.value = renderModeSelectMobile.value;
|
renderModeSelect.value = renderModeSelectMobile.value;
|
||||||
this.updateRenderMode();
|
this.updateRenderMode();
|
||||||
});
|
});
|
||||||
|
valueModeSelectMobile.addEventListener('change', () => {
|
||||||
|
valueModeSelect.value = valueModeSelectMobile.value;
|
||||||
|
this.updateValueMode();
|
||||||
|
});
|
||||||
opacitySliderMobile.addEventListener('input', () => {
|
opacitySliderMobile.addEventListener('input', () => {
|
||||||
opacitySlider.value = opacitySliderMobile.value;
|
opacitySlider.value = opacitySliderMobile.value;
|
||||||
this.updateUIOpacity();
|
this.updateUIOpacity();
|
||||||
@ -405,7 +416,23 @@ class BitfielderApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shareURL(): void {
|
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;
|
window.location.hash = encoded;
|
||||||
|
|
||||||
navigator.clipboard.writeText(window.location.href).then(() => {
|
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||||
@ -419,9 +446,68 @@ class BitfielderApp {
|
|||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
try {
|
try {
|
||||||
const decoded = atob(window.location.hash.substring(1));
|
const decoded = atob(window.location.hash.substring(1));
|
||||||
this.editor.value = decoded;
|
|
||||||
this.shader.setCode(decoded);
|
// Try to parse as JSON first (new format with settings)
|
||||||
this.render();
|
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) {
|
} catch (e) {
|
||||||
console.error('Failed to decode URL hash:', e);
|
console.error('Failed to decode URL hash:', e);
|
||||||
}
|
}
|
||||||
@ -448,6 +534,14 @@ class BitfielderApp {
|
|||||||
Storage.saveSettings({ renderMode });
|
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 {
|
private updateUIOpacity(): void {
|
||||||
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
||||||
const opacityValue = document.getElementById('opacity-value')!;
|
const opacityValue = document.getElementById('opacity-value')!;
|
||||||
@ -550,11 +644,19 @@ class BitfielderApp {
|
|||||||
(document.getElementById('resolution-select') as HTMLSelectElement).value = settings.resolution.toString();
|
(document.getElementById('resolution-select') as HTMLSelectElement).value = settings.resolution.toString();
|
||||||
(document.getElementById('fps-select') as HTMLSelectElement).value = settings.fps.toString();
|
(document.getElementById('fps-select') as HTMLSelectElement).value = settings.fps.toString();
|
||||||
(document.getElementById('render-mode-select') as HTMLSelectElement).value = settings.renderMode || 'classic';
|
(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
|
// Sync mobile controls
|
||||||
(document.getElementById('resolution-select-mobile') as HTMLSelectElement).value = settings.resolution.toString();
|
(document.getElementById('resolution-select-mobile') as HTMLSelectElement).value = settings.resolution.toString();
|
||||||
(document.getElementById('fps-select-mobile') as HTMLSelectElement).value = settings.fps.toString();
|
(document.getElementById('fps-select-mobile') as HTMLSelectElement).value = settings.fps.toString();
|
||||||
(document.getElementById('render-mode-select-mobile') as HTMLSelectElement).value = settings.renderMode || 'classic';
|
(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
|
// Apply UI opacity
|
||||||
const opacity = settings.uiOpacity ?? 0.3;
|
const opacity = settings.uiOpacity ?? 0.3;
|
||||||
@ -577,7 +679,22 @@ class BitfielderApp {
|
|||||||
|
|
||||||
if (!code) return;
|
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 = '';
|
nameInput.value = '';
|
||||||
this.renderShaderLibrary();
|
this.renderShaderLibrary();
|
||||||
|
|
||||||
@ -606,10 +723,14 @@ class BitfielderApp {
|
|||||||
return;
|
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">
|
||||||
<div class="shader-item-header" onclick="app.loadShader('${shader.id}')">
|
<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">
|
<div class="shader-actions">
|
||||||
<button class="shader-action rename" onclick="event.stopPropagation(); app.startRename('${shader.id}')" title="Rename">edit</button>
|
<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>
|
<button class="shader-action delete" onclick="event.stopPropagation(); app.deleteShader('${shader.id}')" title="Delete">del</button>
|
||||||
@ -617,7 +738,7 @@ class BitfielderApp {
|
|||||||
</div>
|
</div>
|
||||||
<div class="shader-code">${this.escapeHtml(shader.code)}</div>
|
<div class="shader-code">${this.escapeHtml(shader.code)}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private escapeHtml(text: string): string {
|
private escapeHtml(text: string): string {
|
||||||
@ -633,7 +754,56 @@ class BitfielderApp {
|
|||||||
const shaders = Storage.getShaders();
|
const shaders = Storage.getShaders();
|
||||||
const shader = shaders.find(s => s.id === id);
|
const shader = shaders.find(s => s.id === id);
|
||||||
if (shader) {
|
if (shader) {
|
||||||
|
// Load the code
|
||||||
this.editor.value = shader.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.shader.setCode(shader.code);
|
||||||
this.render();
|
this.render();
|
||||||
Storage.updateShaderUsage(id);
|
Storage.updateShaderUsage(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user