ok le merge
This commit is contained in:
@ -476,21 +476,36 @@ export class FakeShader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private compositeTiles(): void {
|
private async compositeTiles(): Promise<void> {
|
||||||
const width = this.canvas.width;
|
|
||||||
const height = this.canvas.height;
|
const height = this.canvas.height;
|
||||||
const tileHeight = Math.ceil(height / this.workerCount);
|
const tileHeight = Math.ceil(height / this.workerCount);
|
||||||
|
|
||||||
// Clear main canvas
|
// Use ImageBitmap for faster compositing if available
|
||||||
this.ctx.clearRect(0, 0, width, height);
|
if (typeof createImageBitmap !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const bitmapPromises: Promise<ImageBitmap>[] = [];
|
||||||
|
const positions: number[] = [];
|
||||||
|
|
||||||
// Composite all tiles directly on main canvas
|
for (let i = 0; i < this.workerCount; i++) {
|
||||||
for (let i = 0; i < this.workerCount; i++) {
|
const tileData = this.tileResults.get(i);
|
||||||
const tileData = this.tileResults.get(i);
|
if (tileData) {
|
||||||
if (tileData) {
|
bitmapPromises.push(createImageBitmap(tileData));
|
||||||
const startY = i * tileHeight;
|
positions.push(i * tileHeight);
|
||||||
this.ctx.putImageData(tileData, 0, startY);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bitmaps = await Promise.all(bitmapPromises);
|
||||||
|
|
||||||
|
for (let i = 0; i < bitmaps.length; i++) {
|
||||||
|
this.ctx.drawImage(bitmaps[i], 0, positions[i]);
|
||||||
|
bitmaps[i].close(); // Free memory
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to putImageData if ImageBitmap fails
|
||||||
|
this.fallbackCompositeTiles();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.fallbackCompositeTiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear tile results
|
// Clear tile results
|
||||||
@ -510,6 +525,18 @@ export class FakeShader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fallbackCompositeTiles(): void {
|
||||||
|
const tileHeight = Math.ceil(this.canvas.height / this.workerCount);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.workerCount; i++) {
|
||||||
|
const tileData = this.tileResults.get(i);
|
||||||
|
if (tileData) {
|
||||||
|
const startY = i * tileHeight;
|
||||||
|
this.ctx.putImageData(tileData, 0, startY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simplified method - kept for backward compatibility but always uses all cores
|
// Simplified method - kept for backward compatibility but always uses all cores
|
||||||
setMultiWorkerMode(_enabled: boolean, _workerCount?: number): void {
|
setMultiWorkerMode(_enabled: boolean, _workerCount?: number): void {
|
||||||
// Always use all available cores, ignore the enabled parameter
|
// Always use all available cores, ignore the enabled parameter
|
||||||
|
|||||||
@ -46,16 +46,20 @@ interface WorkerResponse {
|
|||||||
|
|
||||||
import { LRUCache } from './utils/LRUCache';
|
import { LRUCache } from './utils/LRUCache';
|
||||||
import { calculateColorDirect } from './utils/colorModes';
|
import { calculateColorDirect } from './utils/colorModes';
|
||||||
import { PERFORMANCE, COLOR_TABLE_SIZE } from './utils/constants';
|
import { PERFORMANCE, COLOR_TABLE_SIZE, RENDER_MODES, RENDER_MODE_INDEX } from './utils/constants';
|
||||||
|
|
||||||
type ShaderFunction = (...args: number[]) => number;
|
type ShaderFunction = (...args: number[]) => number;
|
||||||
|
|
||||||
class ShaderWorker {
|
class ShaderWorker {
|
||||||
private compiledFunction: ShaderFunction | null = null;
|
private compiledFunction: ShaderFunction | null = null;
|
||||||
private lastCode: string = '';
|
private lastCode: string = '';
|
||||||
private imageDataCache: LRUCache<string, ImageData> = new LRUCache(PERFORMANCE.IMAGE_DATA_CACHE_SIZE);
|
private imageDataCache: LRUCache<string, ImageData> = new LRUCache(
|
||||||
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(PERFORMANCE.COMPILATION_CACHE_SIZE);
|
PERFORMANCE.IMAGE_DATA_CACHE_SIZE
|
||||||
private colorTables: Map<string, Uint8Array> = new Map();
|
);
|
||||||
|
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(
|
||||||
|
PERFORMANCE.COMPILATION_CACHE_SIZE
|
||||||
|
);
|
||||||
|
private colorTables: Uint8Array[] = [];
|
||||||
private feedbackBuffer: Float32Array | null = null;
|
private feedbackBuffer: Float32Array | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -66,23 +70,14 @@ class ShaderWorker {
|
|||||||
this.initializeColorTables();
|
this.initializeColorTables();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private initializeColorTables(): void {
|
private initializeColorTables(): void {
|
||||||
const tableSize = COLOR_TABLE_SIZE;
|
const tableSize = COLOR_TABLE_SIZE;
|
||||||
|
|
||||||
// Pre-compute color tables for each render mode
|
// Pre-compute color tables for each render mode using array indexing
|
||||||
const modes = [
|
this.colorTables = new Array(RENDER_MODES.length);
|
||||||
'classic',
|
|
||||||
'grayscale',
|
|
||||||
'red',
|
|
||||||
'green',
|
|
||||||
'blue',
|
|
||||||
'rgb',
|
|
||||||
'hsv',
|
|
||||||
'rainbow',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const mode of modes) {
|
for (let modeIndex = 0; modeIndex < RENDER_MODES.length; modeIndex++) {
|
||||||
|
const mode = RENDER_MODES[modeIndex];
|
||||||
const colorTable = new Uint8Array(tableSize * 3); // RGB triplets
|
const colorTable = new Uint8Array(tableSize * 3); // RGB triplets
|
||||||
|
|
||||||
for (let i = 0; i < tableSize; i++) {
|
for (let i = 0; i < tableSize; i++) {
|
||||||
@ -92,11 +87,10 @@ class ShaderWorker {
|
|||||||
colorTable[i * 3 + 2] = b;
|
colorTable[i * 3 + 2] = b;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.colorTables.set(mode, colorTable);
|
this.colorTables[modeIndex] = colorTable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private handleMessage(message: WorkerMessage): void {
|
private handleMessage(message: WorkerMessage): void {
|
||||||
try {
|
try {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
@ -226,44 +220,10 @@ class ShaderWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isStaticExpression(code: string): boolean {
|
private isStaticExpression(code: string): boolean {
|
||||||
// Check if code contains any variables
|
// Check if code contains any variables using regex for better accuracy
|
||||||
const variables = [
|
const variablePattern = /\b(x|y|t|i|r|a|u|v|c|f|d|n|b|mouse[XY]|mousePressed|mouseV[XY]|mouseClickTime|touchCount|touch[01][XY]|pinchScale|pinchRotation|accel[XYZ]|gyro[XYZ]|audioLevel|bassLevel|midLevel|trebleLevel)\b/;
|
||||||
'x',
|
|
||||||
'y',
|
|
||||||
't',
|
|
||||||
'i',
|
|
||||||
'mouseX',
|
|
||||||
'mouseY',
|
|
||||||
'mousePressed',
|
|
||||||
'mouseVX',
|
|
||||||
'mouseVY',
|
|
||||||
'mouseClickTime',
|
|
||||||
'touchCount',
|
|
||||||
'touch0X',
|
|
||||||
'touch0Y',
|
|
||||||
'touch1X',
|
|
||||||
'touch1Y',
|
|
||||||
'pinchScale',
|
|
||||||
'pinchRotation',
|
|
||||||
'accelX',
|
|
||||||
'accelY',
|
|
||||||
'accelZ',
|
|
||||||
'gyroX',
|
|
||||||
'gyroY',
|
|
||||||
'gyroZ',
|
|
||||||
'audioLevel',
|
|
||||||
'bassLevel',
|
|
||||||
'midLevel',
|
|
||||||
'trebleLevel',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const variable of variables) {
|
return !variablePattern.test(code);
|
||||||
if (code.includes(variable)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private evaluateStaticExpression(code: string): number {
|
private evaluateStaticExpression(code: string): number {
|
||||||
@ -412,14 +372,19 @@ class ShaderWorker {
|
|||||||
const v = actualY / fullHeight;
|
const v = actualY / fullHeight;
|
||||||
const centerX = fullWidth / 2;
|
const centerX = fullWidth / 2;
|
||||||
const centerY = fullHeight / 2;
|
const centerY = fullHeight / 2;
|
||||||
const radius = Math.sqrt((x - centerX) ** 2 + (actualY - centerY) ** 2);
|
const radius = Math.sqrt(
|
||||||
|
(x - centerX) ** 2 + (actualY - centerY) ** 2
|
||||||
|
);
|
||||||
const angle = Math.atan2(actualY - centerY, x - centerX);
|
const angle = Math.atan2(actualY - centerY, x - centerX);
|
||||||
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
|
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
|
||||||
const normalizedDistance = radius / maxDistance;
|
const normalizedDistance = radius / maxDistance;
|
||||||
const frameCount = Math.floor(time * 60);
|
const frameCount = Math.floor(time * 60);
|
||||||
const manhattanDistance = Math.abs(x - centerX) + Math.abs(actualY - centerY);
|
const manhattanDistance =
|
||||||
|
Math.abs(x - centerX) + Math.abs(actualY - centerY);
|
||||||
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
|
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
|
||||||
const feedbackValue = this.feedbackBuffer ? this.feedbackBuffer[pixelIndex] || 0 : 0;
|
const feedbackValue = this.feedbackBuffer
|
||||||
|
? this.feedbackBuffer[pixelIndex] || 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
const value = this.compiledFunction!(
|
const value = this.compiledFunction!(
|
||||||
x,
|
x,
|
||||||
@ -515,7 +480,6 @@ class ShaderWorker {
|
|||||||
if (!imageData) {
|
if (!imageData) {
|
||||||
imageData = new ImageData(width, height);
|
imageData = new ImageData(width, height);
|
||||||
this.imageDataCache.set(key, imageData);
|
this.imageDataCache.set(key, imageData);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageData;
|
return imageData;
|
||||||
@ -623,13 +587,14 @@ class ShaderWorker {
|
|||||||
|
|
||||||
for (let i = 0; i < octaves; i++) {
|
for (let i = 0; i < octaves; i++) {
|
||||||
const frequency = Math.pow(2, i) * scale;
|
const frequency = Math.pow(2, i) * scale;
|
||||||
const noise = Math.sin((x + Math.abs(value) * 0.1) * frequency) *
|
const noise =
|
||||||
Math.cos((y + Math.abs(value) * 0.1) * frequency);
|
Math.sin((x + Math.abs(value) * 0.1) * frequency) *
|
||||||
|
Math.cos((y + Math.abs(value) * 0.1) * frequency);
|
||||||
fractalValue += noise * amplitude;
|
fractalValue += noise * amplitude;
|
||||||
amplitude *= 0.5;
|
amplitude *= 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
processedValue = Math.floor(((fractalValue + 1) * 0.5) * 255);
|
processedValue = Math.floor((fractalValue + 1) * 0.5 * 255);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,20 +603,24 @@ class ShaderWorker {
|
|||||||
const cellSize = 16;
|
const cellSize = 16;
|
||||||
const cellX = Math.floor(x / cellSize);
|
const cellX = Math.floor(x / cellSize);
|
||||||
const cellY = Math.floor(y / cellSize);
|
const cellY = Math.floor(y / cellSize);
|
||||||
const cellHash = (cellX * 73856093) ^ (cellY * 19349663) ^ Math.floor(Math.abs(value));
|
const cellHash =
|
||||||
|
(cellX * 73856093) ^ (cellY * 19349663) ^ Math.floor(Math.abs(value));
|
||||||
|
|
||||||
// Generate cellular pattern based on neighbors
|
// Generate cellular pattern based on neighbors
|
||||||
let neighbors = 0;
|
let neighbors = 0;
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
if (dx === 0 && dy === 0) continue;
|
if (dx === 0 && dy === 0) continue;
|
||||||
const neighborHash = ((cellX + dx) * 73856093) ^ ((cellY + dy) * 19349663) ^ Math.floor(Math.abs(value));
|
const neighborHash =
|
||||||
if ((neighborHash % 256) > 128) neighbors++;
|
((cellX + dx) * 73856093) ^
|
||||||
|
((cellY + dy) * 19349663) ^
|
||||||
|
Math.floor(Math.abs(value));
|
||||||
|
if (neighborHash % 256 > 128) neighbors++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cellState = (cellHash % 256) > 128 ? 1 : 0;
|
const cellState = cellHash % 256 > 128 ? 1 : 0;
|
||||||
const evolution = (neighbors >= 3 && neighbors <= 5) ? 1 : cellState;
|
const evolution = neighbors >= 3 && neighbors <= 5 ? 1 : cellState;
|
||||||
processedValue = evolution * 255;
|
processedValue = evolution * 255;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -668,7 +637,7 @@ class ShaderWorker {
|
|||||||
const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25;
|
const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25;
|
||||||
|
|
||||||
const combinedNoise = (noise1 + noise2 + noise3) / 1.75;
|
const combinedNoise = (noise1 + noise2 + noise3) / 1.75;
|
||||||
processedValue = Math.floor(((combinedNoise + 1) * 0.5) * 255);
|
processedValue = Math.floor((combinedNoise + 1) * 0.5 * 255);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -682,8 +651,12 @@ class ShaderWorker {
|
|||||||
const warpFreq = 0.02;
|
const warpFreq = 0.02;
|
||||||
|
|
||||||
// Calculate warped coordinates
|
// Calculate warped coordinates
|
||||||
const warpX = x + Math.sin(y * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
const warpX =
|
||||||
const warpY = y + Math.cos(x * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
x +
|
||||||
|
Math.sin(y * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
||||||
|
const warpY =
|
||||||
|
y +
|
||||||
|
Math.cos(x * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
||||||
|
|
||||||
// Create barrel/lens distortion
|
// Create barrel/lens distortion
|
||||||
const dx = warpX - centerX;
|
const dx = warpX - centerX;
|
||||||
@ -693,7 +666,8 @@ class ShaderWorker {
|
|||||||
const normDist = dist / maxDist;
|
const normDist = dist / maxDist;
|
||||||
|
|
||||||
// Apply non-linear space deformation
|
// Apply non-linear space deformation
|
||||||
const deform = 1 + Math.sin(normDist * Math.PI + Math.abs(value) * 0.05) * 0.3;
|
const deform =
|
||||||
|
1 + Math.sin(normDist * Math.PI + Math.abs(value) * 0.05) * 0.3;
|
||||||
const deformedX = centerX + dx * deform;
|
const deformedX = centerX + dx * deform;
|
||||||
const deformedY = centerY + dy * deform;
|
const deformedY = centerY + dy * deform;
|
||||||
|
|
||||||
@ -713,18 +687,18 @@ class ShaderWorker {
|
|||||||
{
|
{
|
||||||
x: centerX + Math.sin(Math.abs(value) * 0.01) * 200,
|
x: centerX + Math.sin(Math.abs(value) * 0.01) * 200,
|
||||||
y: centerY + Math.cos(Math.abs(value) * 0.01) * 200,
|
y: centerY + Math.cos(Math.abs(value) * 0.01) * 200,
|
||||||
strength: 1 + Math.abs(value) * 0.01
|
strength: 1 + Math.abs(value) * 0.01,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: centerX + Math.cos(Math.abs(value) * 0.015) * 150,
|
x: centerX + Math.cos(Math.abs(value) * 0.015) * 150,
|
||||||
y: centerY + Math.sin(Math.abs(value) * 0.015) * 150,
|
y: centerY + Math.sin(Math.abs(value) * 0.015) * 150,
|
||||||
strength: -0.8 + Math.sin(Math.abs(value) * 0.02) * 0.5
|
strength: -0.8 + Math.sin(Math.abs(value) * 0.02) * 0.5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: centerX + Math.sin(Math.abs(value) * 0.008) * 300,
|
x: centerX + Math.sin(Math.abs(value) * 0.008) * 300,
|
||||||
y: centerY + Math.cos(Math.abs(value) * 0.012) * 250,
|
y: centerY + Math.cos(Math.abs(value) * 0.012) * 250,
|
||||||
strength: 0.6 + Math.cos(Math.abs(value) * 0.018) * 0.4
|
strength: 0.6 + Math.cos(Math.abs(value) * 0.018) * 0.4,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Calculate flow field at this point
|
// Calculate flow field at this point
|
||||||
@ -746,8 +720,8 @@ class ShaderWorker {
|
|||||||
|
|
||||||
// Curl component (rotation) - creates vortices
|
// Curl component (rotation) - creates vortices
|
||||||
const curlStrength = source.strength * 0.5;
|
const curlStrength = source.strength * 0.5;
|
||||||
flowX += (-dy / normalizedDist) * curlStrength / normalizedDist;
|
flowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||||
flowY += (dx / normalizedDist) * curlStrength / normalizedDist;
|
flowY += ((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add global flow influenced by value
|
// Add global flow influenced by value
|
||||||
@ -757,9 +731,10 @@ class ShaderWorker {
|
|||||||
|
|
||||||
// Add turbulence
|
// Add turbulence
|
||||||
const turbScale = 0.05;
|
const turbScale = 0.05;
|
||||||
const turbulence = Math.sin(x * turbScale + Math.abs(value) * 0.01) *
|
const turbulence =
|
||||||
Math.cos(y * turbScale + Math.abs(value) * 0.015) *
|
Math.sin(x * turbScale + Math.abs(value) * 0.01) *
|
||||||
(Math.abs(value) * 0.02);
|
Math.cos(y * turbScale + Math.abs(value) * 0.015) *
|
||||||
|
(Math.abs(value) * 0.02);
|
||||||
|
|
||||||
flowX += turbulence;
|
flowX += turbulence;
|
||||||
flowY += turbulence * 0.7;
|
flowY += turbulence * 0.7;
|
||||||
@ -786,8 +761,10 @@ class ShaderWorker {
|
|||||||
|
|
||||||
// Curl
|
// Curl
|
||||||
const curlStrength = source.strength * 0.5;
|
const curlStrength = source.strength * 0.5;
|
||||||
localFlowX += (-dy / normalizedDist) * curlStrength / normalizedDist;
|
localFlowX +=
|
||||||
localFlowY += (dx / normalizedDist) * curlStrength / normalizedDist;
|
((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||||
|
localFlowY +=
|
||||||
|
((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move particle
|
// Move particle
|
||||||
@ -798,11 +775,14 @@ class ShaderWorker {
|
|||||||
|
|
||||||
// Calculate final value based on particle's final position and flow magnitude
|
// Calculate final value based on particle's final position and flow magnitude
|
||||||
const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY);
|
const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY);
|
||||||
const particleDistance = Math.sqrt((particleX - x) * (particleX - x) + (particleY - y) * (particleY - y));
|
const particleDistance = Math.sqrt(
|
||||||
|
(particleX - x) * (particleX - x) + (particleY - y) * (particleY - y)
|
||||||
|
);
|
||||||
|
|
||||||
// Combine flow magnitude with particle trajectory
|
// Combine flow magnitude with particle trajectory
|
||||||
const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256;
|
const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256;
|
||||||
const enhanced = Math.sin(flowValue * 0.05 + Math.abs(value) * 0.01) * 0.5 + 0.5;
|
const enhanced =
|
||||||
|
Math.sin(flowValue * 0.05 + Math.abs(value) * 0.01) * 0.5 + 0.5;
|
||||||
|
|
||||||
processedValue = Math.floor(enhanced * 255);
|
processedValue = Math.floor(enhanced * 255);
|
||||||
break;
|
break;
|
||||||
@ -814,18 +794,18 @@ class ShaderWorker {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use pre-computed color table if available
|
// Use pre-computed color table with O(1) array indexing
|
||||||
const colorTable = this.colorTables.get(renderMode);
|
const modeIndex = RENDER_MODE_INDEX[renderMode];
|
||||||
if (colorTable) {
|
if (modeIndex !== undefined && this.colorTables[modeIndex]) {
|
||||||
|
const colorTable = this.colorTables[modeIndex];
|
||||||
const index = Math.floor(processedValue) * 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 for unknown render modes
|
||||||
return calculateColorDirect(processedValue, renderMode);
|
return calculateColorDirect(processedValue, renderMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private sanitizeCode(code: string): string {
|
private sanitizeCode(code: string): string {
|
||||||
// Auto-prefix Math functions
|
// Auto-prefix Math functions
|
||||||
const mathFunctions = [
|
const mathFunctions = [
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { AppSettings } from './stores/appSettings';
|
import { AppSettings } from './stores/appSettings';
|
||||||
import { STORAGE_KEYS, PERFORMANCE, DEFAULTS, ValueMode } from './utils/constants';
|
import {
|
||||||
|
STORAGE_KEYS,
|
||||||
|
PERFORMANCE,
|
||||||
|
DEFAULTS,
|
||||||
|
ValueMode,
|
||||||
|
} from './utils/constants';
|
||||||
|
|
||||||
export interface SavedShader {
|
export interface SavedShader {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export function HelpPopup() {
|
|||||||
<strong>n</strong> - Noise value (0.0 to 1.0)
|
<strong>n</strong> - Noise value (0.0 to 1.0)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>b</strong> - Previous frame's value (feedback)
|
<strong>b</strong> - Previous frame's value (feedback)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
|
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
|
||||||
@ -139,7 +139,7 @@ export function HelpPopup() {
|
|||||||
<p>
|
<p>
|
||||||
<strong>trebleLevel</strong> - High frequencies (0.0-1.0)
|
<strong>trebleLevel</strong> - High frequencies (0.0-1.0)
|
||||||
</p>
|
</p>
|
||||||
<p>Click "Enable Audio" to activate microphone</p>
|
<p>Click "Enable Audio" to activate microphone</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="help-section">
|
<div className="help-section">
|
||||||
@ -295,10 +295,7 @@ export function HelpPopup() {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Website:{' '}
|
Website:{' '}
|
||||||
<a
|
<a href="https://raphaelforment.fr" target="_blank" rel="noreferrer">
|
||||||
href="https://raphaelforment.fr"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
raphaelforment.fr
|
raphaelforment.fr
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@ -307,6 +304,7 @@ export function HelpPopup() {
|
|||||||
<a
|
<a
|
||||||
href="https://git.raphaelforment.fr"
|
href="https://git.raphaelforment.fr"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
git.raphaelforment.fr
|
git.raphaelforment.fr
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -112,7 +112,9 @@ export function MobileMenu() {
|
|||||||
<label>Value Mode</label>
|
<label>Value Mode</label>
|
||||||
<select
|
<select
|
||||||
value={settings.valueMode}
|
value={settings.valueMode}
|
||||||
onChange={(e) => updateAppSettings({ valueMode: e.target.value as ValueMode })}
|
onChange={(e) =>
|
||||||
|
updateAppSettings({ valueMode: e.target.value as ValueMode })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{VALUE_MODES.map((mode) => (
|
{VALUE_MODES.map((mode) => (
|
||||||
<option key={mode} value={mode}>
|
<option key={mode} value={mode}>
|
||||||
|
|||||||
@ -60,17 +60,24 @@ export function TopBar() {
|
|||||||
uiOpacity: settings.uiOpacity,
|
uiOpacity: settings.uiOpacity,
|
||||||
};
|
};
|
||||||
|
|
||||||
const encoded = btoa(JSON.stringify(shareData));
|
try {
|
||||||
window.location.hash = encoded;
|
const encoded = btoa(JSON.stringify(shareData));
|
||||||
|
const url = `${window.location.origin}${window.location.pathname}#${encoded}`;
|
||||||
|
|
||||||
navigator.clipboard
|
console.log('Sharing URL:', url);
|
||||||
.writeText(window.location.href)
|
console.log('Share data:', shareData);
|
||||||
.then(() => {
|
|
||||||
console.log('URL copied to clipboard');
|
navigator.clipboard
|
||||||
})
|
.writeText(url)
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
console.log('Copy failed');
|
console.log('URL copied to clipboard');
|
||||||
});
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log('Copy failed');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create share URL:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportPNG = () => {
|
const handleExportPNG = () => {
|
||||||
@ -151,7 +158,9 @@ export function TopBar() {
|
|||||||
Value Mode:
|
Value Mode:
|
||||||
<select
|
<select
|
||||||
value={settings.valueMode}
|
value={settings.valueMode}
|
||||||
onChange={(e) => updateAppSettings({ valueMode: e.target.value as ValueMode })}
|
onChange={(e) =>
|
||||||
|
updateAppSettings({ valueMode: e.target.value as ValueMode })
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.1)',
|
background: 'rgba(255,255,255,0.1)',
|
||||||
border: '1px solid #555',
|
border: '1px solid #555',
|
||||||
|
|||||||
@ -32,11 +32,17 @@ export const WelcomePopup: React.FC = () => {
|
|||||||
<h2 className="welcome-title">Welcome to BitFielder</h2>
|
<h2 className="welcome-title">Welcome to BitFielder</h2>
|
||||||
|
|
||||||
<div className="welcome-content">
|
<div className="welcome-content">
|
||||||
<p>BitFielder is an experimental lofi bitfield shader editor made by <a href="https://raphaelforment.fr">BuboBubo</a>. Use it to create visual compositions through code. I use it for fun :) </p>
|
<p>
|
||||||
|
BitFielder is an experimental lofi bitfield shader editor made by{' '}
|
||||||
|
<a href="https://raphaelforment.fr">BuboBubo</a>. Use it to create
|
||||||
|
visual compositions through code. I use it for fun :){' '}
|
||||||
|
</p>
|
||||||
|
|
||||||
<h3>Getting Started</h3>
|
<h3>Getting Started</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Edit the shader code and press <i>Eval</i> or <i>Ctrl+Enter</i></li>
|
<li>
|
||||||
|
Edit the shader code and press <i>Eval</i> or <i>Ctrl+Enter</i>
|
||||||
|
</li>
|
||||||
<li>Use special variables to create reactive effects</li>
|
<li>Use special variables to create reactive effects</li>
|
||||||
<li>Explore/store shaders in the library (left pane)</li>
|
<li>Explore/store shaders in the library (left pane)</li>
|
||||||
<li>Export your creations as images or sharable links</li>
|
<li>Export your creations as images or sharable links</li>
|
||||||
@ -44,13 +50,24 @@ export const WelcomePopup: React.FC = () => {
|
|||||||
|
|
||||||
<h3>Key Features</h3>
|
<h3>Key Features</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Real-time editing:</strong> See your changes instantly</li>
|
<li>
|
||||||
<li><strong>Motion and touch:</strong> Mouse, touchscreen support</li>
|
<strong>Real-time editing:</strong> See your changes instantly
|
||||||
<li><strong>Audio reactive:</strong> Synchronize with a sound signal</li>
|
</li>
|
||||||
<li><strong>Export capabilities:</strong> Save and share your work</li>
|
<li>
|
||||||
|
<strong>Motion and touch:</strong> Mouse, touchscreen support
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Audio reactive:</strong> Synchronize with a sound signal
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Export capabilities:</strong> Save and share your work
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p className="help-hint">Press <kbd>?</kbd> anytime to view keyboard shortcuts and detailed help.</p>
|
<p className="help-hint">
|
||||||
|
Press <kbd>?</kbd> anytime to view keyboard shortcuts and detailed
|
||||||
|
help.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p className="dismiss-hint">Press any key to dismiss this message</p>
|
<p className="dismiss-hint">Press any key to dismiss this message</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
16
src/main.tsx
16
src/main.tsx
@ -13,11 +13,20 @@ $appSettings.set(savedSettings);
|
|||||||
function loadFromURL() {
|
function loadFromURL() {
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
try {
|
try {
|
||||||
const decoded = atob(window.location.hash.substring(1));
|
const hash = window.location.hash.substring(1);
|
||||||
|
console.log('Loading from URL hash:', hash);
|
||||||
|
|
||||||
|
const decoded = atob(hash);
|
||||||
|
console.log('Decoded data:', decoded);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shareData = JSON.parse(decoded);
|
const shareData = JSON.parse(decoded);
|
||||||
setShaderCode(shareData.code);
|
console.log('Parsed share data:', shareData);
|
||||||
|
|
||||||
|
if (shareData.code) {
|
||||||
|
setShaderCode(shareData.code);
|
||||||
|
}
|
||||||
|
|
||||||
$appSettings.set({
|
$appSettings.set({
|
||||||
resolution: shareData.resolution || savedSettings.resolution,
|
resolution: shareData.resolution || savedSettings.resolution,
|
||||||
fps: shareData.fps || savedSettings.fps,
|
fps: shareData.fps || savedSettings.fps,
|
||||||
@ -28,7 +37,10 @@ function loadFromURL() {
|
|||||||
? shareData.uiOpacity
|
? shareData.uiOpacity
|
||||||
: savedSettings.uiOpacity,
|
: savedSettings.uiOpacity,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Settings updated from URL');
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
|
console.log('JSON parse failed, falling back to old format');
|
||||||
// Fall back to old format (just code as string)
|
// Fall back to old format (just code as string)
|
||||||
setShaderCode(decoded);
|
setShaderCode(decoded);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,16 @@ export const defaultInputState: InputState = {
|
|||||||
|
|
||||||
export const $input = atom<InputState>(defaultInputState);
|
export const $input = atom<InputState>(defaultInputState);
|
||||||
|
|
||||||
|
let mouseUpdatePending = false;
|
||||||
|
let pendingMouseData = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
pressed: false,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
clickTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export function updateMousePosition(
|
export function updateMousePosition(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
@ -64,17 +74,37 @@ export function updateMousePosition(
|
|||||||
vy: number,
|
vy: number,
|
||||||
clickTime: number
|
clickTime: number
|
||||||
) {
|
) {
|
||||||
$input.set({
|
pendingMouseData = { x, y, pressed, vx, vy, clickTime };
|
||||||
...$input.get(),
|
|
||||||
mouseX: x,
|
if (!mouseUpdatePending) {
|
||||||
mouseY: y,
|
mouseUpdatePending = true;
|
||||||
mousePressed: pressed,
|
requestAnimationFrame(() => {
|
||||||
mouseVX: vx,
|
const current = $input.get();
|
||||||
mouseVY: vy,
|
$input.set({
|
||||||
mouseClickTime: clickTime,
|
...current,
|
||||||
});
|
mouseX: pendingMouseData.x,
|
||||||
|
mouseY: pendingMouseData.y,
|
||||||
|
mousePressed: pendingMouseData.pressed,
|
||||||
|
mouseVX: pendingMouseData.vx,
|
||||||
|
mouseVY: pendingMouseData.vy,
|
||||||
|
mouseClickTime: pendingMouseData.clickTime,
|
||||||
|
});
|
||||||
|
mouseUpdatePending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let touchUpdatePending = false;
|
||||||
|
let pendingTouchData = {
|
||||||
|
count: 0,
|
||||||
|
x0: 0,
|
||||||
|
y0: 0,
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
scale: 1,
|
||||||
|
rotation: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export function updateTouchPosition(
|
export function updateTouchPosition(
|
||||||
count: number,
|
count: number,
|
||||||
x0: number,
|
x0: number,
|
||||||
@ -84,16 +114,25 @@ export function updateTouchPosition(
|
|||||||
scale: number,
|
scale: number,
|
||||||
rotation: number
|
rotation: number
|
||||||
) {
|
) {
|
||||||
$input.set({
|
pendingTouchData = { count, x0, y0, x1, y1, scale, rotation };
|
||||||
...$input.get(),
|
|
||||||
touchCount: count,
|
if (!touchUpdatePending) {
|
||||||
touch0X: x0,
|
touchUpdatePending = true;
|
||||||
touch0Y: y0,
|
requestAnimationFrame(() => {
|
||||||
touch1X: x1,
|
const current = $input.get();
|
||||||
touch1Y: y1,
|
$input.set({
|
||||||
pinchScale: scale,
|
...current,
|
||||||
pinchRotation: rotation,
|
touchCount: pendingTouchData.count,
|
||||||
});
|
touch0X: pendingTouchData.x0,
|
||||||
|
touch0Y: pendingTouchData.y0,
|
||||||
|
touch1X: pendingTouchData.x1,
|
||||||
|
touch1Y: pendingTouchData.y1,
|
||||||
|
pinchScale: pendingTouchData.scale,
|
||||||
|
pinchRotation: pendingTouchData.rotation,
|
||||||
|
});
|
||||||
|
touchUpdatePending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateDeviceMotion(
|
export function updateDeviceMotion(
|
||||||
|
|||||||
@ -21,7 +21,10 @@ export const defaultUIState: UIState = {
|
|||||||
export const uiState = atom<UIState>(defaultUIState);
|
export const uiState = atom<UIState>(defaultUIState);
|
||||||
|
|
||||||
export function toggleMobileMenu() {
|
export function toggleMobileMenu() {
|
||||||
uiState.set({ ...uiState.get(), mobileMenuOpen: !uiState.get().mobileMenuOpen });
|
uiState.set({
|
||||||
|
...uiState.get(),
|
||||||
|
mobileMenuOpen: !uiState.get().mobileMenuOpen,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeMobileMenu() {
|
export function closeMobileMenu() {
|
||||||
@ -37,7 +40,10 @@ export function hideHelp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleShaderLibrary() {
|
export function toggleShaderLibrary() {
|
||||||
uiState.set({ ...uiState.get(), shaderLibraryOpen: !uiState.get().shaderLibraryOpen });
|
uiState.set({
|
||||||
|
...uiState.get(),
|
||||||
|
shaderLibraryOpen: !uiState.get().shaderLibraryOpen,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleUI() {
|
export function toggleUI() {
|
||||||
|
|||||||
@ -462,7 +462,7 @@ button [data-lucide] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-content li:before {
|
.welcome-content li:before {
|
||||||
content: "▸";
|
content: '▸';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
export function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
export function hsvToRgb(
|
||||||
|
h: number,
|
||||||
|
s: number,
|
||||||
|
v: number
|
||||||
|
): [number, number, number] {
|
||||||
const c = v * s;
|
const c = v * s;
|
||||||
const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
|
const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
|
||||||
const m = v - c;
|
const m = v - c;
|
||||||
|
|||||||
@ -7,18 +7,47 @@ export const UI_HEIGHTS = {
|
|||||||
|
|
||||||
// Performance Constants
|
// Performance Constants
|
||||||
export const PERFORMANCE = {
|
export const PERFORMANCE = {
|
||||||
DEFAULT_TILE_SIZE: 64,
|
DEFAULT_TILE_SIZE: 128,
|
||||||
MAX_RENDER_TIME_MS: 50,
|
MAX_RENDER_TIME_MS: 50,
|
||||||
MAX_SHADER_TIMEOUT_MS: 5,
|
MAX_SHADER_TIMEOUT_MS: 5,
|
||||||
TIMEOUT_CHECK_INTERVAL: 1000,
|
TIMEOUT_CHECK_INTERVAL: 1000,
|
||||||
MAX_SAVED_SHADERS: 50,
|
MAX_SAVED_SHADERS: 50,
|
||||||
IMAGE_DATA_CACHE_SIZE: 5,
|
IMAGE_DATA_CACHE_SIZE: 10,
|
||||||
COMPILATION_CACHE_SIZE: 20,
|
COMPILATION_CACHE_SIZE: 30,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Color Constants
|
// Color Constants
|
||||||
export const COLOR_TABLE_SIZE = 256;
|
export const COLOR_TABLE_SIZE = 256;
|
||||||
|
|
||||||
|
// Render Mode Constants - Keep in sync with color modes
|
||||||
|
export const RENDER_MODES = [
|
||||||
|
'classic',
|
||||||
|
'grayscale',
|
||||||
|
'red',
|
||||||
|
'green',
|
||||||
|
'blue',
|
||||||
|
'rgb',
|
||||||
|
'hsv',
|
||||||
|
'rainbow',
|
||||||
|
'thermal',
|
||||||
|
'neon',
|
||||||
|
'cyberpunk',
|
||||||
|
'vaporwave',
|
||||||
|
'dithered',
|
||||||
|
'palette',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type RenderMode = (typeof RENDER_MODES)[number];
|
||||||
|
|
||||||
|
// Create a mapping from render mode to index for O(1) lookups
|
||||||
|
export const RENDER_MODE_INDEX: Record<string, number> = RENDER_MODES.reduce(
|
||||||
|
(acc, mode, index) => {
|
||||||
|
acc[mode] = index;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
);
|
||||||
|
|
||||||
// Storage Keys
|
// Storage Keys
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
SHADERS: 'bitfielder_shaders',
|
SHADERS: 'bitfielder_shaders',
|
||||||
@ -36,10 +65,10 @@ export const VALUE_MODES = [
|
|||||||
'cellular',
|
'cellular',
|
||||||
'noise',
|
'noise',
|
||||||
'warp',
|
'warp',
|
||||||
'flow'
|
'flow',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ValueMode = typeof VALUE_MODES[number];
|
export type ValueMode = (typeof VALUE_MODES)[number];
|
||||||
|
|
||||||
// Default Values
|
// Default Values
|
||||||
export const DEFAULTS = {
|
export const DEFAULTS = {
|
||||||
|
|||||||
Reference in New Issue
Block a user