optimisations
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import { WorkerMessage, WorkerResponse } from './shader/types';
|
||||
import { TIMING, WORKER, DEFAULTS } from './utils/constants';
|
||||
|
||||
export class FakeShader {
|
||||
private canvas: HTMLCanvasElement;
|
||||
@ -15,8 +16,8 @@ export class FakeShader {
|
||||
private renderMode: string = 'classic';
|
||||
private valueMode: string = 'integer';
|
||||
private hueShift: number = 0;
|
||||
private timeSpeed: number = 1.0;
|
||||
private currentBPM: number = 120;
|
||||
private timeSpeed: number = DEFAULTS.TIME_SPEED;
|
||||
private currentBPM: number = TIMING.DEFAULT_BPM;
|
||||
|
||||
// ID generation optimization
|
||||
private idCounter: number = 0;
|
||||
@ -56,7 +57,7 @@ export class FakeShader {
|
||||
bassLevel: 0,
|
||||
midLevel: 0,
|
||||
trebleLevel: 0,
|
||||
bpm: 120,
|
||||
bpm: TIMING.DEFAULT_BPM,
|
||||
startY: 0,
|
||||
};
|
||||
|
||||
@ -76,7 +77,7 @@ export class FakeShader {
|
||||
private touch0Y: number = 0;
|
||||
private touch1X: number = 0;
|
||||
private touch1Y: number = 0;
|
||||
private pinchScale: number = 1;
|
||||
private pinchScale: number = WORKER.DEFAULT_PINCH_SCALE;
|
||||
private pinchRotation: number = 0;
|
||||
private accelX: number = 0;
|
||||
private accelY: number = 0;
|
||||
@ -90,8 +91,8 @@ export class FakeShader {
|
||||
private trebleLevel: number = 0;
|
||||
|
||||
// Frame rate limiting
|
||||
private targetFPS: number = 30;
|
||||
private frameInterval: number = 1000 / this.targetFPS;
|
||||
private targetFPS: number = TIMING.DEFAULT_FPS;
|
||||
private frameInterval: number = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS;
|
||||
private lastFrameTime: number = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, code: string = 'x^y') {
|
||||
@ -103,10 +104,10 @@ export class FakeShader {
|
||||
this.initializeOffscreenCanvas();
|
||||
|
||||
// Always use maximum available cores
|
||||
this.workerCount = navigator.hardwareConcurrency || 4;
|
||||
this.workerCount = navigator.hardwareConcurrency || WORKER.FALLBACK_CORE_COUNT;
|
||||
// Some browsers report logical processors (hyperthreading), which is good
|
||||
// But cap at a reasonable maximum to avoid overhead
|
||||
this.workerCount = Math.min(this.workerCount, 32);
|
||||
this.workerCount = Math.min(this.workerCount, WORKER.MAX_WORKERS);
|
||||
console.log(
|
||||
`Auto-detected ${this.workerCount} CPU cores, using all for maximum performance`
|
||||
);
|
||||
@ -221,7 +222,7 @@ export class FakeShader {
|
||||
|
||||
this.isRendering = true;
|
||||
// this._currentRenderID = id; // Removed unused property
|
||||
const currentTime = (Date.now() - this.startTime) / 1000 * this.timeSpeed;
|
||||
const currentTime = (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed;
|
||||
|
||||
// Always use multiple workers if available
|
||||
if (this.workerCount > 1) {
|
||||
@ -366,8 +367,8 @@ export class FakeShader {
|
||||
}
|
||||
|
||||
setTargetFPS(fps: number): void {
|
||||
this.targetFPS = Math.max(1, Math.min(120, fps)); // Clamp between 1-120 FPS
|
||||
this.frameInterval = 1000 / this.targetFPS;
|
||||
this.targetFPS = Math.max(TIMING.MIN_FPS, Math.min(TIMING.MAX_FPS, fps)); // Clamp between 1-120 FPS
|
||||
this.frameInterval = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS;
|
||||
}
|
||||
|
||||
setRenderMode(mode: string): void {
|
||||
|
||||
281
src/RefactoredShader.ts
Normal file
281
src/RefactoredShader.ts
Normal file
@ -0,0 +1,281 @@
|
||||
import { WorkerMessage } from './shader/types';
|
||||
import { InputManager } from './shader/core/InputManager';
|
||||
import { WorkerPool } from './shader/core/WorkerPool';
|
||||
import { RenderController } from './shader/core/RenderController';
|
||||
|
||||
/**
|
||||
* Refactored shader renderer with separated concerns
|
||||
* Demonstrates the benefits of extracting responsibilities from the God class
|
||||
*/
|
||||
export class RefactoredShader {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private code: string;
|
||||
|
||||
// Extracted components with single responsibilities
|
||||
private inputManager: InputManager;
|
||||
private workerPool: WorkerPool;
|
||||
private renderController: RenderController;
|
||||
|
||||
// Render state
|
||||
private compiled: boolean = false;
|
||||
private renderMode: string = 'classic';
|
||||
private valueMode: string = 'integer';
|
||||
private hueShift: number = 0;
|
||||
|
||||
// Reusable message object for performance
|
||||
private reusableMessage: WorkerMessage = {
|
||||
id: '',
|
||||
type: 'render',
|
||||
width: 0,
|
||||
height: 0,
|
||||
fullWidth: 0,
|
||||
fullHeight: 0,
|
||||
time: 0,
|
||||
renderMode: 'classic',
|
||||
valueMode: 'integer',
|
||||
hueShift: 0,
|
||||
mouseX: 0,
|
||||
mouseY: 0,
|
||||
mousePressed: false,
|
||||
mouseVX: 0,
|
||||
mouseVY: 0,
|
||||
mouseClickTime: 0,
|
||||
touchCount: 0,
|
||||
touch0X: 0,
|
||||
touch0Y: 0,
|
||||
touch1X: 0,
|
||||
touch1Y: 0,
|
||||
pinchScale: 1,
|
||||
pinchRotation: 0,
|
||||
accelX: 0,
|
||||
accelY: 0,
|
||||
accelZ: 0,
|
||||
gyroX: 0,
|
||||
gyroY: 0,
|
||||
gyroZ: 0,
|
||||
audioLevel: 0,
|
||||
bassLevel: 0,
|
||||
midLevel: 0,
|
||||
trebleLevel: 0,
|
||||
bpm: 120,
|
||||
startY: 0,
|
||||
};
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, code: string = 'x^y') {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.code = code;
|
||||
|
||||
// Initialize separated components
|
||||
this.inputManager = new InputManager();
|
||||
this.workerPool = new WorkerPool();
|
||||
this.renderController = new RenderController();
|
||||
|
||||
this.setupEventHandlers();
|
||||
this.compile();
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
// Set up render controller callback
|
||||
this.renderController.setRenderFrameHandler((time, renderId) => {
|
||||
this.render(renderId, time);
|
||||
});
|
||||
|
||||
// Set up worker pool callbacks
|
||||
this.workerPool.setRenderCompleteHandler((imageData) => {
|
||||
this.ctx.putImageData(imageData, 0, 0);
|
||||
this.renderController.setRenderingState(false);
|
||||
});
|
||||
|
||||
this.workerPool.setErrorHandler((error) => {
|
||||
console.error('Rendering error:', error);
|
||||
this.renderController.setRenderingState(false);
|
||||
});
|
||||
}
|
||||
|
||||
async compile(): Promise<void> {
|
||||
try {
|
||||
await this.workerPool.compile(this.code);
|
||||
this.compiled = true;
|
||||
console.log('Shader compiled successfully');
|
||||
} catch (error) {
|
||||
console.error('Compilation failed:', error);
|
||||
this.compiled = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private render(id: string, currentTime: number): void {
|
||||
if (!this.compiled || this.renderController.isCurrentlyRendering()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderController.setRenderingState(true);
|
||||
this.renderController.addPendingRender(id);
|
||||
|
||||
// Update reusable message to avoid allocations
|
||||
this.reusableMessage.id = id;
|
||||
this.reusableMessage.width = this.canvas.width;
|
||||
this.reusableMessage.height = this.canvas.height;
|
||||
this.reusableMessage.fullWidth = this.canvas.width;
|
||||
this.reusableMessage.fullHeight = this.canvas.height;
|
||||
this.reusableMessage.time = currentTime;
|
||||
this.reusableMessage.renderMode = this.renderMode;
|
||||
this.reusableMessage.valueMode = this.valueMode;
|
||||
this.reusableMessage.hueShift = this.hueShift;
|
||||
|
||||
// Populate input data from InputManager
|
||||
this.inputManager.populateWorkerMessage(this.reusableMessage);
|
||||
|
||||
// Choose rendering strategy based on worker count
|
||||
if (this.workerPool.getWorkerCount() > 1) {
|
||||
this.workerPool.renderMultiWorker(
|
||||
this.reusableMessage,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
} else {
|
||||
this.workerPool.renderSingleWorker(this.reusableMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
start(): void {
|
||||
if (!this.compiled) {
|
||||
console.warn('Cannot start rendering: shader not compiled');
|
||||
return;
|
||||
}
|
||||
this.renderController.start();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.renderController.stop();
|
||||
}
|
||||
|
||||
setCode(code: string): Promise<void> {
|
||||
this.code = code;
|
||||
this.compiled = false;
|
||||
return this.compile();
|
||||
}
|
||||
|
||||
setRenderMode(mode: string): void {
|
||||
this.renderMode = mode;
|
||||
}
|
||||
|
||||
setValueMode(mode: string): void {
|
||||
this.valueMode = mode;
|
||||
}
|
||||
|
||||
setHueShift(shift: number): void {
|
||||
this.hueShift = shift;
|
||||
}
|
||||
|
||||
setTargetFPS(fps: number): void {
|
||||
this.renderController.setTargetFPS(fps);
|
||||
}
|
||||
|
||||
setTimeSpeed(speed: number): void {
|
||||
this.renderController.setTimeSpeed(speed);
|
||||
}
|
||||
|
||||
// Input methods - delegated to InputManager
|
||||
setMousePosition(x: number, y: number): void {
|
||||
this.inputManager.setMousePosition(x, y);
|
||||
}
|
||||
|
||||
setMousePressed(pressed: boolean): void {
|
||||
this.inputManager.setMousePressed(pressed);
|
||||
}
|
||||
|
||||
setMouseVelocity(vx: number, vy: number): void {
|
||||
this.inputManager.setMouseVelocity(vx, vy);
|
||||
}
|
||||
|
||||
setTouchData(
|
||||
count: number,
|
||||
x0: number = 0,
|
||||
y0: number = 0,
|
||||
x1: number = 0,
|
||||
y1: number = 0,
|
||||
scale: number = 1,
|
||||
rotation: number = 0
|
||||
): void {
|
||||
this.inputManager.setTouchData(count, x0, y0, x1, y1, scale, rotation);
|
||||
}
|
||||
|
||||
setAccelerometer(x: number, y: number, z: number): void {
|
||||
this.inputManager.setAccelerometer(x, y, z);
|
||||
}
|
||||
|
||||
setGyroscope(x: number, y: number, z: number): void {
|
||||
this.inputManager.setGyroscope(x, y, z);
|
||||
}
|
||||
|
||||
setAudioLevels(
|
||||
level: number,
|
||||
bass: number,
|
||||
mid: number,
|
||||
treble: number,
|
||||
bpm: number
|
||||
): void {
|
||||
this.inputManager.setAudioLevels(level, bass, mid, treble, bpm);
|
||||
}
|
||||
|
||||
// Getters
|
||||
isCompiled(): boolean {
|
||||
return this.compiled;
|
||||
}
|
||||
|
||||
isAnimating(): boolean {
|
||||
return this.renderController.isAnimating();
|
||||
}
|
||||
|
||||
getCurrentTime(): number {
|
||||
return this.renderController.getCurrentTime();
|
||||
}
|
||||
|
||||
getFrameRate(): number {
|
||||
return this.renderController.getFrameRate();
|
||||
}
|
||||
|
||||
getWorkerCount(): number {
|
||||
return this.workerPool.getWorkerCount();
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
destroy(): void {
|
||||
this.renderController.stop();
|
||||
this.workerPool.destroy();
|
||||
}
|
||||
|
||||
// Helper methods for generating example shader code
|
||||
static getExamples(): string[] {
|
||||
return [
|
||||
'x^y',
|
||||
'x|y',
|
||||
'(x+y+t*10)%256',
|
||||
'((x>>4)^(y>>4))<<4',
|
||||
'(x^y^(x*y))%256',
|
||||
'd * t / 2.0',
|
||||
'((x&y)|(x^y))%256',
|
||||
'(x+y)&255',
|
||||
'x%y',
|
||||
'((x*t)^y)%256',
|
||||
'(x&(y|t*8))%256',
|
||||
'a+d*t',
|
||||
'n*t*400',
|
||||
'((x>>2)|(y<<2))%88',
|
||||
'(x*y*t)%256',
|
||||
'(x+y*t)%256',
|
||||
'(x^y^(t*16))%256',
|
||||
'((x*t)&(y*t))%256',
|
||||
'(x+(y<<(t%4)))%256',
|
||||
'((x*t%128)^y)%256',
|
||||
'(x^(y*t*2))%256',
|
||||
'((x+t)*(y+t))%256',
|
||||
'(x&y&(t*8))%256',
|
||||
'((x|t)^(y|t))%256',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import {
|
||||
STORAGE_KEYS,
|
||||
PERFORMANCE,
|
||||
DEFAULTS,
|
||||
FORMAT,
|
||||
ValueMode,
|
||||
} from './utils/constants';
|
||||
|
||||
@ -146,12 +147,12 @@ export class Storage {
|
||||
}
|
||||
|
||||
private static generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
return Date.now().toString(FORMAT.ID_RADIX) + Math.random().toString(FORMAT.ID_RADIX).substr(FORMAT.ID_SUBSTRING_START);
|
||||
}
|
||||
|
||||
static exportShaders(): string {
|
||||
const shaders = this.getShaders();
|
||||
return JSON.stringify(shaders, null, 2);
|
||||
return JSON.stringify(shaders, null, FORMAT.JSON_INDENT);
|
||||
}
|
||||
|
||||
static importShaders(jsonData: string): boolean {
|
||||
|
||||
188
src/shader/core/InputManager.ts
Normal file
188
src/shader/core/InputManager.ts
Normal file
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Manages all input tracking (mouse, touch, accelerometer, audio)
|
||||
* Extracted from FakeShader to follow Single Responsibility Principle
|
||||
*/
|
||||
export class InputManager {
|
||||
// Mouse state
|
||||
private mouseX: number = 0;
|
||||
private mouseY: number = 0;
|
||||
private mousePressed: boolean = false;
|
||||
private mouseVX: number = 0;
|
||||
private mouseVY: number = 0;
|
||||
private mouseClickTime: number = 0;
|
||||
|
||||
// Touch state
|
||||
private touchCount: number = 0;
|
||||
private touch0X: number = 0;
|
||||
private touch0Y: number = 0;
|
||||
private touch1X: number = 0;
|
||||
private touch1Y: number = 0;
|
||||
private pinchScale: number = 1;
|
||||
private pinchRotation: number = 0;
|
||||
|
||||
// Accelerometer state
|
||||
private accelX: number = 0;
|
||||
private accelY: number = 0;
|
||||
private accelZ: number = 0;
|
||||
|
||||
// Gyroscope state
|
||||
private gyroX: number = 0;
|
||||
private gyroY: number = 0;
|
||||
private gyroZ: number = 0;
|
||||
|
||||
// Audio state
|
||||
private audioLevel: number = 0;
|
||||
private bassLevel: number = 0;
|
||||
private midLevel: number = 0;
|
||||
private trebleLevel: number = 0;
|
||||
private currentBPM: number = 120;
|
||||
|
||||
setMousePosition(x: number, y: number): void {
|
||||
this.mouseX = x;
|
||||
this.mouseY = y;
|
||||
}
|
||||
|
||||
setMousePressed(pressed: boolean): void {
|
||||
this.mousePressed = pressed;
|
||||
if (pressed) {
|
||||
this.mouseClickTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
setMouseVelocity(vx: number, vy: number): void {
|
||||
this.mouseVX = vx;
|
||||
this.mouseVY = vy;
|
||||
}
|
||||
|
||||
setTouchData(
|
||||
count: number,
|
||||
x0: number = 0,
|
||||
y0: number = 0,
|
||||
x1: number = 0,
|
||||
y1: number = 0,
|
||||
scale: number = 1,
|
||||
rotation: number = 0
|
||||
): void {
|
||||
this.touchCount = count;
|
||||
this.touch0X = x0;
|
||||
this.touch0Y = y0;
|
||||
this.touch1X = x1;
|
||||
this.touch1Y = y1;
|
||||
this.pinchScale = scale;
|
||||
this.pinchRotation = rotation;
|
||||
}
|
||||
|
||||
setAccelerometer(x: number, y: number, z: number): void {
|
||||
this.accelX = x;
|
||||
this.accelY = y;
|
||||
this.accelZ = z;
|
||||
}
|
||||
|
||||
setGyroscope(x: number, y: number, z: number): void {
|
||||
this.gyroX = x;
|
||||
this.gyroY = y;
|
||||
this.gyroZ = z;
|
||||
}
|
||||
|
||||
setAudioLevels(
|
||||
level: number,
|
||||
bass: number,
|
||||
mid: number,
|
||||
treble: number,
|
||||
bpm: number
|
||||
): void {
|
||||
this.audioLevel = level;
|
||||
this.bassLevel = bass;
|
||||
this.midLevel = mid;
|
||||
this.trebleLevel = treble;
|
||||
this.currentBPM = bpm;
|
||||
}
|
||||
|
||||
// Getters for all input values
|
||||
getMouseData() {
|
||||
return {
|
||||
x: this.mouseX,
|
||||
y: this.mouseY,
|
||||
pressed: this.mousePressed,
|
||||
vx: this.mouseVX,
|
||||
vy: this.mouseVY,
|
||||
clickTime: this.mouseClickTime,
|
||||
};
|
||||
}
|
||||
|
||||
getTouchData() {
|
||||
return {
|
||||
count: this.touchCount,
|
||||
x0: this.touch0X,
|
||||
y0: this.touch0Y,
|
||||
x1: this.touch1X,
|
||||
y1: this.touch1Y,
|
||||
scale: this.pinchScale,
|
||||
rotation: this.pinchRotation,
|
||||
};
|
||||
}
|
||||
|
||||
getAccelerometerData() {
|
||||
return {
|
||||
x: this.accelX,
|
||||
y: this.accelY,
|
||||
z: this.accelZ,
|
||||
};
|
||||
}
|
||||
|
||||
getGyroscopeData() {
|
||||
return {
|
||||
x: this.gyroX,
|
||||
y: this.gyroY,
|
||||
z: this.gyroZ,
|
||||
};
|
||||
}
|
||||
|
||||
getAudioData() {
|
||||
return {
|
||||
level: this.audioLevel,
|
||||
bass: this.bassLevel,
|
||||
mid: this.midLevel,
|
||||
treble: this.trebleLevel,
|
||||
bpm: this.currentBPM,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method to populate worker message with all input data
|
||||
populateWorkerMessage(message: any): void {
|
||||
const mouse = this.getMouseData();
|
||||
const touch = this.getTouchData();
|
||||
const accel = this.getAccelerometerData();
|
||||
const gyro = this.getGyroscopeData();
|
||||
const audio = this.getAudioData();
|
||||
|
||||
message.mouseX = mouse.x;
|
||||
message.mouseY = mouse.y;
|
||||
message.mousePressed = mouse.pressed;
|
||||
message.mouseVX = mouse.vx;
|
||||
message.mouseVY = mouse.vy;
|
||||
message.mouseClickTime = mouse.clickTime;
|
||||
|
||||
message.touchCount = touch.count;
|
||||
message.touch0X = touch.x0;
|
||||
message.touch0Y = touch.y0;
|
||||
message.touch1X = touch.x1;
|
||||
message.touch1Y = touch.y1;
|
||||
message.pinchScale = touch.scale;
|
||||
message.pinchRotation = touch.rotation;
|
||||
|
||||
message.accelX = accel.x;
|
||||
message.accelY = accel.y;
|
||||
message.accelZ = accel.z;
|
||||
|
||||
message.gyroX = gyro.x;
|
||||
message.gyroY = gyro.y;
|
||||
message.gyroZ = gyro.z;
|
||||
|
||||
message.audioLevel = audio.level;
|
||||
message.bassLevel = audio.bass;
|
||||
message.midLevel = audio.mid;
|
||||
message.trebleLevel = audio.treble;
|
||||
message.bpm = audio.bpm;
|
||||
}
|
||||
}
|
||||
114
src/shader/core/RenderController.ts
Normal file
114
src/shader/core/RenderController.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { TIMING } from '../../utils/constants';
|
||||
|
||||
/**
|
||||
* Manages animation timing and frame rate control
|
||||
* Extracted from FakeShader for better separation of concerns
|
||||
*/
|
||||
export class RenderController {
|
||||
private animationId: number | null = null;
|
||||
private startTime: number = Date.now();
|
||||
private targetFPS: number = TIMING.DEFAULT_FPS;
|
||||
private frameInterval: number = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS;
|
||||
private lastFrameTime: number = 0;
|
||||
private timeSpeed: number = 1.0;
|
||||
private isRendering: boolean = false;
|
||||
private pendingRenders: string[] = [];
|
||||
private idCounter: number = 0;
|
||||
|
||||
private onRenderFrame?: (time: number, renderId: string) => void;
|
||||
|
||||
setRenderFrameHandler(handler: (time: number, renderId: string) => void): void {
|
||||
this.onRenderFrame = handler;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.animationId !== null) return;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (timestamp - this.lastFrameTime >= this.frameInterval) {
|
||||
const currentTime = (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed;
|
||||
const renderId = this.generateId();
|
||||
|
||||
this.onRenderFrame?.(currentTime, renderId);
|
||||
this.lastFrameTime = timestamp;
|
||||
}
|
||||
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.animationId !== null) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
}
|
||||
|
||||
setTargetFPS(fps: number): void {
|
||||
this.targetFPS = Math.max(TIMING.MIN_FPS, Math.min(TIMING.MAX_FPS, fps));
|
||||
this.frameInterval = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS;
|
||||
}
|
||||
|
||||
setTimeSpeed(speed: number): void {
|
||||
this.timeSpeed = speed;
|
||||
}
|
||||
|
||||
getTimeSpeed(): number {
|
||||
return this.timeSpeed;
|
||||
}
|
||||
|
||||
getCurrentTime(): number {
|
||||
return (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed;
|
||||
}
|
||||
|
||||
isAnimating(): boolean {
|
||||
return this.animationId !== null;
|
||||
}
|
||||
|
||||
generateId(): string {
|
||||
return `render_${this.idCounter++}_${Date.now()}`;
|
||||
}
|
||||
|
||||
setRenderingState(isRendering: boolean): void {
|
||||
this.isRendering = isRendering;
|
||||
}
|
||||
|
||||
isCurrentlyRendering(): boolean {
|
||||
return this.isRendering;
|
||||
}
|
||||
|
||||
addPendingRender(renderId: string): void {
|
||||
this.pendingRenders.push(renderId);
|
||||
|
||||
// Keep only the latest render to avoid backlog
|
||||
if (this.pendingRenders.length > 3) {
|
||||
const latestId = this.pendingRenders[this.pendingRenders.length - 1];
|
||||
this.pendingRenders = [latestId];
|
||||
}
|
||||
}
|
||||
|
||||
removePendingRender(renderId: string): void {
|
||||
const index = this.pendingRenders.indexOf(renderId);
|
||||
if (index !== -1) {
|
||||
this.pendingRenders.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
getPendingRenders(): string[] {
|
||||
return [...this.pendingRenders];
|
||||
}
|
||||
|
||||
clearPendingRenders(): void {
|
||||
this.pendingRenders = [];
|
||||
}
|
||||
|
||||
getFrameRate(): number {
|
||||
return this.targetFPS;
|
||||
}
|
||||
|
||||
getFrameInterval(): number {
|
||||
return this.frameInterval;
|
||||
}
|
||||
}
|
||||
188
src/shader/core/WorkerPool.ts
Normal file
188
src/shader/core/WorkerPool.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import { WorkerMessage, WorkerResponse } from '../types';
|
||||
import { WORKER } from '../../utils/constants';
|
||||
|
||||
/**
|
||||
* Manages worker lifecycle and multi-worker rendering
|
||||
* Extracted from FakeShader for better separation of concerns
|
||||
*/
|
||||
export class WorkerPool {
|
||||
private workers: Worker[] = [];
|
||||
private workerCount: number;
|
||||
private tileResults: Map<number, ImageData> = new Map();
|
||||
private tilesCompleted: number = 0;
|
||||
private totalTiles: number = 0;
|
||||
private onRenderComplete?: (imageData: ImageData) => void;
|
||||
private onError?: (error: any) => void;
|
||||
|
||||
constructor() {
|
||||
this.workerCount = navigator.hardwareConcurrency || WORKER.FALLBACK_CORE_COUNT;
|
||||
this.workerCount = Math.min(this.workerCount, WORKER.MAX_WORKERS);
|
||||
console.log(`WorkerPool: Using ${this.workerCount} workers for rendering`);
|
||||
this.initializeWorkers();
|
||||
}
|
||||
|
||||
private initializeWorkers(): void {
|
||||
for (let i = 0; i < this.workerCount; i++) {
|
||||
const worker = new Worker(new URL('../worker/ShaderWorker.ts', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
this.handleWorkerMessage(event.data, i);
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
console.error(`Worker ${i} error:`, error);
|
||||
this.onError?.(error);
|
||||
};
|
||||
|
||||
this.workers.push(worker);
|
||||
}
|
||||
}
|
||||
|
||||
private handleWorkerMessage(response: WorkerResponse, workerIndex: number): void {
|
||||
switch (response.type) {
|
||||
case 'compiled':
|
||||
// Handle compilation response if needed
|
||||
break;
|
||||
|
||||
case 'rendered':
|
||||
if (this.workerCount > 1) {
|
||||
this.handleTileResult(response, workerIndex);
|
||||
} else {
|
||||
this.onRenderComplete?.(response.imageData!);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error(`Worker ${workerIndex} error:`, response.error);
|
||||
this.onError?.(response.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleTileResult(response: WorkerResponse, workerIndex: number): void {
|
||||
if (!response.imageData || response.tileIndex === undefined) return;
|
||||
|
||||
this.tileResults.set(response.tileIndex, response.imageData);
|
||||
this.tilesCompleted++;
|
||||
|
||||
if (this.tilesCompleted >= this.totalTiles) {
|
||||
this.assembleTiles();
|
||||
}
|
||||
}
|
||||
|
||||
private assembleTiles(): void {
|
||||
if (this.tileResults.size === 0) return;
|
||||
|
||||
const firstTile = this.tileResults.get(0);
|
||||
if (!firstTile) return;
|
||||
|
||||
const tileWidth = firstTile.width;
|
||||
const tileHeight = firstTile.height;
|
||||
const tilesPerRow = Math.ceil(Math.sqrt(this.totalTiles));
|
||||
const totalWidth = tileWidth;
|
||||
const totalHeight = tileHeight * this.totalTiles;
|
||||
|
||||
const canvas = new OffscreenCanvas(totalWidth, totalHeight);
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const finalImageData = ctx.createImageData(totalWidth, totalHeight);
|
||||
|
||||
for (let i = 0; i < this.totalTiles; i++) {
|
||||
const tileData = this.tileResults.get(i);
|
||||
if (!tileData) continue;
|
||||
|
||||
const startY = i * tileHeight;
|
||||
const sourceData = tileData.data;
|
||||
const targetData = finalImageData.data;
|
||||
|
||||
for (let y = 0; y < tileHeight; y++) {
|
||||
for (let x = 0; x < tileWidth; x++) {
|
||||
const sourceIndex = (y * tileWidth + x) * 4;
|
||||
const targetIndex = ((startY + y) * totalWidth + x) * 4;
|
||||
|
||||
targetData[targetIndex] = sourceData[sourceIndex];
|
||||
targetData[targetIndex + 1] = sourceData[sourceIndex + 1];
|
||||
targetData[targetIndex + 2] = sourceData[sourceIndex + 2];
|
||||
targetData[targetIndex + 3] = sourceData[sourceIndex + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onRenderComplete?.(finalImageData);
|
||||
this.tileResults.clear();
|
||||
this.tilesCompleted = 0;
|
||||
}
|
||||
|
||||
compile(code: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = this.workers[0]; // Use first worker for compilation
|
||||
|
||||
const compileMessage = {
|
||||
type: 'compile',
|
||||
code,
|
||||
};
|
||||
|
||||
const handleResponse = (event: MessageEvent) => {
|
||||
const response = event.data;
|
||||
if (response.type === 'compiled') {
|
||||
worker.removeEventListener('message', handleResponse);
|
||||
resolve();
|
||||
} else if (response.type === 'error') {
|
||||
worker.removeEventListener('message', handleResponse);
|
||||
reject(response.error);
|
||||
}
|
||||
};
|
||||
|
||||
worker.addEventListener('message', handleResponse);
|
||||
worker.postMessage(compileMessage);
|
||||
});
|
||||
}
|
||||
|
||||
renderSingleWorker(message: WorkerMessage): void {
|
||||
const worker = this.workers[0];
|
||||
worker.postMessage(message);
|
||||
}
|
||||
|
||||
renderMultiWorker(baseMessage: WorkerMessage, width: number, height: number): void {
|
||||
this.tileResults.clear();
|
||||
this.tilesCompleted = 0;
|
||||
this.totalTiles = this.workerCount;
|
||||
|
||||
const tileHeight = Math.ceil(height / this.totalTiles);
|
||||
|
||||
for (let i = 0; i < this.totalTiles; i++) {
|
||||
const worker = this.workers[i];
|
||||
const startY = i * tileHeight;
|
||||
const endY = Math.min((i + 1) * tileHeight, height);
|
||||
const actualTileHeight = endY - startY;
|
||||
|
||||
const tileMessage: WorkerMessage = {
|
||||
...baseMessage,
|
||||
startY,
|
||||
height: actualTileHeight,
|
||||
tileIndex: i,
|
||||
};
|
||||
|
||||
worker.postMessage(tileMessage);
|
||||
}
|
||||
}
|
||||
|
||||
setRenderCompleteHandler(handler: (imageData: ImageData) => void): void {
|
||||
this.onRenderComplete = handler;
|
||||
}
|
||||
|
||||
setErrorHandler(handler: (error: any) => void): void {
|
||||
this.onError = handler;
|
||||
}
|
||||
|
||||
getWorkerCount(): number {
|
||||
return this.workerCount;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.workers.forEach(worker => worker.terminate());
|
||||
this.workers = [];
|
||||
this.tileResults.clear();
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import { LUMINANCE_WEIGHTS } from '../../utils/constants';
|
||||
|
||||
/**
|
||||
* Manages feedback buffers for shader rendering
|
||||
*/
|
||||
@ -35,7 +37,7 @@ export class FeedbackSystem {
|
||||
if (!this.feedbackBuffer) return;
|
||||
|
||||
// Use the actual displayed luminance as feedback (0-255 range)
|
||||
const luminance = (r * 0.299 + g * 0.587 + b * 0.114);
|
||||
const luminance = (r * LUMINANCE_WEIGHTS.RED + g * LUMINANCE_WEIGHTS.GREEN + b * LUMINANCE_WEIGHTS.BLUE);
|
||||
|
||||
// Frame rate independent decay
|
||||
const decayFactor = Math.pow(0.95, deltaTime * 60); // 5% decay at 60fps
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { ShaderFunction, ShaderContext, WorkerMessage } from '../types';
|
||||
import { FeedbackSystem } from './FeedbackSystem';
|
||||
import { calculateColorDirect } from '../../utils/colorModes';
|
||||
import {
|
||||
ValueModeProcessorRegistry,
|
||||
PixelContext,
|
||||
} from './ValueModeProcessor';
|
||||
|
||||
/**
|
||||
* Handles pixel-level rendering operations
|
||||
@ -8,10 +12,12 @@ import { calculateColorDirect } from '../../utils/colorModes';
|
||||
export class PixelRenderer {
|
||||
private feedbackSystem: FeedbackSystem;
|
||||
private shaderContext: ShaderContext;
|
||||
private valueModeRegistry: ValueModeProcessorRegistry;
|
||||
|
||||
constructor(feedbackSystem: FeedbackSystem, shaderContext: ShaderContext) {
|
||||
this.feedbackSystem = feedbackSystem;
|
||||
this.shaderContext = shaderContext;
|
||||
this.valueModeRegistry = ValueModeProcessorRegistry.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,15 +56,15 @@ export class PixelRenderer {
|
||||
// Calculate coordinate variables with optimized math
|
||||
const u = x * invFullWidth;
|
||||
const v = actualY * invFullHeight;
|
||||
|
||||
|
||||
// Pre-calculate deltas for reuse
|
||||
const dx = x - centerX;
|
||||
const dy = actualY - centerY;
|
||||
|
||||
|
||||
// Use more efficient radius calculation
|
||||
const radiusSquared = dx * dx + dy * dy;
|
||||
const radius = Math.sqrt(radiusSquared);
|
||||
|
||||
|
||||
// Optimize angle calculation - avoid atan2 for common cases
|
||||
let angle: number;
|
||||
if (dx === 0) {
|
||||
@ -68,32 +74,36 @@ export class PixelRenderer {
|
||||
} else {
|
||||
angle = Math.atan2(dy, dx);
|
||||
}
|
||||
|
||||
|
||||
// Use pre-computed max distance inverse to avoid division
|
||||
const normalizedDistance = radius * invMaxDistance;
|
||||
|
||||
|
||||
// Optimize Manhattan distance using absolute values of pre-computed deltas
|
||||
const manhattanDistance = Math.abs(dx) + Math.abs(dy);
|
||||
|
||||
// Optimize noise calculation with cached sin/cos values
|
||||
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
|
||||
|
||||
// Pre-compute noise factors
|
||||
const sinX01 = Math.sin(x * 0.1);
|
||||
const cosY01 = Math.cos(actualY * 0.1);
|
||||
const noise = (sinX01 * cosY01 + 1) * 0.5;
|
||||
|
||||
// Cache canvas dimensions
|
||||
const canvasWidth = message.fullWidth || width;
|
||||
const canvasHeight = message.fullHeight || message.height! + (message.startY || 0);
|
||||
|
||||
// Get feedback values
|
||||
const feedbackValue = this.feedbackSystem.getFeedback(pixelIndex);
|
||||
const neighbors = this.feedbackSystem.getNeighborFeedback(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0));
|
||||
const neighbors = this.feedbackSystem.getNeighborFeedback(pixelIndex, x, y, width, canvasHeight);
|
||||
const momentum = this.feedbackSystem.getMomentum(pixelIndex);
|
||||
const laplacian = this.feedbackSystem.getLaplacian(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0));
|
||||
const curvature = this.feedbackSystem.getCurvature(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0));
|
||||
const laplacian = this.feedbackSystem.getLaplacian(pixelIndex, x, y, width, canvasHeight);
|
||||
const curvature = this.feedbackSystem.getCurvature(pixelIndex, x, y, width, canvasHeight);
|
||||
const stateValue = this.feedbackSystem.getState(pixelIndex, feedbackValue, deltaTime);
|
||||
const echoValue = this.feedbackSystem.getEcho(pixelIndex, time);
|
||||
|
||||
// Calculate other variables
|
||||
const canvasWidth = message.fullWidth || width;
|
||||
const canvasHeight = message.fullHeight || message.height! + (message.startY || 0);
|
||||
const pseudoZ = Math.sin(radius * 0.01 + time) * 50;
|
||||
const jitter = ((x * 73856093 + actualY * 19349663) % 256) / 255;
|
||||
const oscillation = Math.sin(timeTwoPi + radius * 0.1);
|
||||
|
||||
|
||||
// Calculate block coordinates
|
||||
const bx = x >> 4;
|
||||
const by = actualY >> 4;
|
||||
@ -168,9 +178,9 @@ export class PixelRenderer {
|
||||
// Execute shader
|
||||
const value = compiledFunction(ctx);
|
||||
const safeValue = isFinite(value) ? value : 0;
|
||||
|
||||
|
||||
// Calculate color
|
||||
const [r, g, b] = this.calculateColor(
|
||||
const color = this.calculateColor(
|
||||
safeValue,
|
||||
renderMode,
|
||||
valueMode,
|
||||
@ -182,13 +192,13 @@ export class PixelRenderer {
|
||||
);
|
||||
|
||||
// Set pixel data
|
||||
data[i] = r;
|
||||
data[i + 1] = g;
|
||||
data[i + 2] = b;
|
||||
data[i] = color[0];
|
||||
data[i + 1] = color[1];
|
||||
data[i + 2] = color[2];
|
||||
data[i + 3] = 255;
|
||||
|
||||
// Update feedback system
|
||||
this.feedbackSystem.updateFeedback(pixelIndex, r, g, b, deltaTime);
|
||||
this.feedbackSystem.updateFeedback(pixelIndex, color[0], color[1], color[2], deltaTime);
|
||||
this.feedbackSystem.updateState(pixelIndex, stateValue);
|
||||
|
||||
} catch (error) {
|
||||
@ -213,584 +223,20 @@ export class PixelRenderer {
|
||||
width: number = 1,
|
||||
height: number = 1
|
||||
): [number, number, number] {
|
||||
// Use optimized strategy pattern for ALL modes
|
||||
const context: PixelContext = { x, y, width, height, value };
|
||||
const processor = this.valueModeRegistry.getProcessor(valueMode);
|
||||
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;
|
||||
}
|
||||
|
||||
case 'fractal': {
|
||||
// Fractal mode: recursive pattern generation
|
||||
const scale = 0.01;
|
||||
let fractalValue = 0;
|
||||
let amplitude = 1;
|
||||
const octaves = 4;
|
||||
|
||||
for (let i = 0; i < octaves; i++) {
|
||||
const frequency = Math.pow(2, i) * scale;
|
||||
const noise =
|
||||
Math.sin((x + Math.abs(value) * 0.1) * frequency) *
|
||||
Math.cos((y + Math.abs(value) * 0.1) * frequency);
|
||||
fractalValue += noise * amplitude;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
|
||||
processedValue = Math.floor((fractalValue + 1) * 0.5 * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'cellular': {
|
||||
// Cellular automata-inspired patterns
|
||||
const cellSize = 16;
|
||||
const cellX = Math.floor(x / cellSize);
|
||||
const cellY = Math.floor(y / cellSize);
|
||||
const cellHash =
|
||||
(cellX * 73856093) ^ (cellY * 19349663) ^ Math.floor(Math.abs(value));
|
||||
|
||||
// Generate cellular pattern based on neighbors
|
||||
let neighbors = 0;
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const neighborHash =
|
||||
((cellX + dx) * 73856093) ^
|
||||
((cellY + dy) * 19349663) ^
|
||||
Math.floor(Math.abs(value));
|
||||
if (neighborHash % 256 > 128) neighbors++;
|
||||
}
|
||||
}
|
||||
|
||||
const cellState = cellHash % 256 > 128 ? 1 : 0;
|
||||
const evolution = neighbors >= 3 && neighbors <= 5 ? 1 : cellState;
|
||||
processedValue = evolution * 255;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'noise': {
|
||||
// Perlin-like noise pattern
|
||||
const noiseScale = 0.02;
|
||||
const nx = x * noiseScale + Math.abs(value) * 0.001;
|
||||
const ny = y * noiseScale + Math.abs(value) * 0.001;
|
||||
|
||||
// Simple noise approximation using sine waves
|
||||
const noise1 = Math.sin(nx * 6.28) * Math.cos(ny * 6.28);
|
||||
const noise2 = Math.sin(nx * 12.56) * Math.cos(ny * 12.56) * 0.5;
|
||||
const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25;
|
||||
|
||||
const combinedNoise = (noise1 + noise2 + noise3) / 1.75;
|
||||
processedValue = Math.floor((combinedNoise + 1) * 0.5 * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'warp': {
|
||||
// Warp mode: space deformation based on value
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Create warping field based on value
|
||||
const warpStrength = Math.abs(value) * 0.001;
|
||||
const warpFreq = 0.02;
|
||||
|
||||
// Calculate warped coordinates
|
||||
const warpX =
|
||||
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
|
||||
const dx = warpX - centerX;
|
||||
const dy = warpY - centerY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
|
||||
const normDist = dist / maxDist;
|
||||
|
||||
// Apply non-linear space deformation
|
||||
const deform =
|
||||
1 + Math.sin(normDist * Math.PI + Math.abs(value) * 0.05) * 0.3;
|
||||
const deformedX = centerX + dx * deform;
|
||||
const deformedY = centerY + dy * deform;
|
||||
|
||||
// Sample from deformed space
|
||||
const finalValue = (deformedX + deformedY + Math.abs(value)) % 256;
|
||||
processedValue = Math.floor(Math.abs(finalValue));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'flow': {
|
||||
// Flow field mode: large-scale fluid dynamics simulation
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Create multiple flow sources influenced by value
|
||||
const flowSources = [
|
||||
{
|
||||
x: centerX + Math.sin(Math.abs(value) * 0.01) * 200,
|
||||
y: centerY + Math.cos(Math.abs(value) * 0.01) * 200,
|
||||
strength: 1 + Math.abs(value) * 0.01,
|
||||
},
|
||||
{
|
||||
x: centerX + Math.cos(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,
|
||||
},
|
||||
{
|
||||
x: centerX + Math.sin(Math.abs(value) * 0.008) * 300,
|
||||
y: centerY + Math.cos(Math.abs(value) * 0.012) * 250,
|
||||
strength: 0.6 + Math.cos(Math.abs(value) * 0.018) * 0.4,
|
||||
},
|
||||
];
|
||||
|
||||
// Calculate flow field at this point
|
||||
let flowX = 0;
|
||||
let flowY = 0;
|
||||
|
||||
for (const source of flowSources) {
|
||||
const dx = x - source.x;
|
||||
const dy = y - source.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const normalizedDist = Math.max(distance, 1); // Avoid division by zero
|
||||
|
||||
// Create flow vectors (potential field + curl)
|
||||
const flowStrength = source.strength / (normalizedDist * 0.01);
|
||||
|
||||
// Radial component (attraction/repulsion)
|
||||
flowX += (dx / normalizedDist) * flowStrength;
|
||||
flowY += (dy / normalizedDist) * flowStrength;
|
||||
|
||||
// Curl component (rotation) - creates vortices
|
||||
const curlStrength = source.strength * 0.5;
|
||||
flowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||
flowY += ((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||
}
|
||||
|
||||
// Add global flow influenced by value
|
||||
const globalFlowAngle = Math.abs(value) * 0.02;
|
||||
flowX += Math.cos(globalFlowAngle) * (Math.abs(value) * 0.1);
|
||||
flowY += Math.sin(globalFlowAngle) * (Math.abs(value) * 0.1);
|
||||
|
||||
// Add turbulence
|
||||
const turbScale = 0.05;
|
||||
const turbulence =
|
||||
Math.sin(x * turbScale + Math.abs(value) * 0.01) *
|
||||
Math.cos(y * turbScale + Math.abs(value) * 0.015) *
|
||||
(Math.abs(value) * 0.02);
|
||||
|
||||
flowX += turbulence;
|
||||
flowY += turbulence * 0.7;
|
||||
|
||||
// Simulate particle flowing through the field
|
||||
let particleX = x;
|
||||
let particleY = y;
|
||||
|
||||
// Multiple flow steps for more interesting trajectories
|
||||
for (let step = 0; step < 5; step++) {
|
||||
// Sample flow field at current particle position
|
||||
let localFlowX = 0;
|
||||
let localFlowY = 0;
|
||||
|
||||
for (const source of flowSources) {
|
||||
const dx = particleX - source.x;
|
||||
const dy = particleY - source.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const normalizedDist = Math.max(distance, 1);
|
||||
|
||||
const flowStrength = source.strength / (normalizedDist * 0.01);
|
||||
localFlowX += (dx / normalizedDist) * flowStrength;
|
||||
localFlowY += (dy / normalizedDist) * flowStrength;
|
||||
|
||||
// Curl
|
||||
const curlStrength = source.strength * 0.5;
|
||||
localFlowX +=
|
||||
((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||
localFlowY +=
|
||||
((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||
}
|
||||
|
||||
// Move particle
|
||||
const stepSize = 0.5;
|
||||
particleX += localFlowX * stepSize;
|
||||
particleY += localFlowY * stepSize;
|
||||
}
|
||||
|
||||
// Calculate final value based on particle's final position and flow magnitude
|
||||
const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY);
|
||||
const particleDistance = Math.sqrt(
|
||||
(particleX - x) * (particleX - x) + (particleY - y) * (particleY - y)
|
||||
);
|
||||
|
||||
// Combine flow magnitude with particle trajectory
|
||||
const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256;
|
||||
const enhanced =
|
||||
Math.sin(flowValue * 0.05 + Math.abs(value) * 0.01) * 0.5 + 0.5;
|
||||
|
||||
processedValue = Math.floor(enhanced * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'spiral': {
|
||||
// Creates logarithmic spirals based on the shader value
|
||||
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 spiralTightness = 1 + Math.abs(value) * 0.01;
|
||||
const spiralValue = Math.atan2(dy, dx) + Math.log(Math.max(radius, 1)) * spiralTightness;
|
||||
processedValue = Math.floor((Math.sin(spiralValue) * 0.5 + 0.5) * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'turbulence': {
|
||||
// Multi-octave turbulence with value-controlled chaos
|
||||
let turbulence = 0;
|
||||
const chaos = Math.abs(value) * 0.001;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const freq = Math.pow(2, i) * (0.01 + chaos);
|
||||
turbulence += Math.abs(Math.sin(x * freq) * Math.cos(y * freq)) / Math.pow(2, i);
|
||||
}
|
||||
processedValue = Math.floor(Math.min(turbulence, 1) * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'crystal': {
|
||||
// Crystalline lattice patterns
|
||||
const latticeSize = 32 + Math.abs(value) * 0.1;
|
||||
const gridX = Math.floor(x / latticeSize);
|
||||
const gridY = Math.floor(y / latticeSize);
|
||||
const crystal = Math.sin(gridX + gridY + Math.abs(value) * 0.01) *
|
||||
Math.cos(gridX * gridY + Math.abs(value) * 0.005);
|
||||
processedValue = Math.floor((crystal * 0.5 + 0.5) * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'marble': {
|
||||
// Marble-like veining patterns
|
||||
const noiseFreq = 0.005 + Math.abs(value) * 0.00001;
|
||||
const turbulence = Math.sin(x * noiseFreq) * Math.cos(y * noiseFreq) +
|
||||
Math.sin(x * noiseFreq * 2) * Math.cos(y * noiseFreq * 2) * 0.5;
|
||||
const marble = Math.sin((x + turbulence * 50) * 0.02 + Math.abs(value) * 0.001);
|
||||
processedValue = Math.floor((marble * 0.5 + 0.5) * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'quantum': {
|
||||
// Quantum uncertainty visualization
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const uncertainty = Math.abs(value) * 0.001;
|
||||
const probability = Math.exp(-(
|
||||
(x - centerX) ** 2 + (y - centerY) ** 2
|
||||
) / (2 * (100 + uncertainty * 1000) ** 2));
|
||||
const quantum = probability * (1 + Math.sin(x * y * uncertainty) * 0.5);
|
||||
processedValue = Math.floor(Math.min(quantum, 1) * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'logarithmic': {
|
||||
// Simple mathematical transform: logarithmic scaling
|
||||
const logValue = Math.log(1 + Math.abs(value));
|
||||
processedValue = Math.floor((logValue / Math.log(256)) * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'mirror': {
|
||||
// Mirror/kaleidoscope effect - creates symmetrical patterns
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const dx = Math.abs(x - centerX);
|
||||
const dy = Math.abs(y - centerY);
|
||||
const mirrorX = centerX + (dx % centerX);
|
||||
const mirrorY = centerY + (dy % centerY);
|
||||
const mirrorDistance = Math.sqrt(mirrorX * mirrorX + mirrorY * mirrorY);
|
||||
const mirrorValue = (Math.abs(value) + mirrorDistance) % 256;
|
||||
processedValue = mirrorValue;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'rings': {
|
||||
// Concentric rings with value-controlled spacing and interference
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
|
||||
const ringSpacing = 20 + Math.abs(value) * 0.1;
|
||||
const rings = Math.sin((distance / ringSpacing) * Math.PI * 2);
|
||||
const interference = Math.sin((distance + Math.abs(value)) * 0.05);
|
||||
processedValue = Math.floor(((rings * interference) * 0.5 + 0.5) * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'mesh': {
|
||||
// Grid/mesh patterns with value-controlled density and rotation
|
||||
const angle = Math.abs(value) * 0.001;
|
||||
const rotX = x * Math.cos(angle) - y * Math.sin(angle);
|
||||
const rotY = x * Math.sin(angle) + y * Math.cos(angle);
|
||||
const gridSize = 16 + Math.abs(value) * 0.05;
|
||||
const gridX = Math.sin((rotX / gridSize) * Math.PI * 2);
|
||||
const gridY = Math.sin((rotY / gridSize) * Math.PI * 2);
|
||||
const mesh = Math.max(Math.abs(gridX), Math.abs(gridY));
|
||||
processedValue = Math.floor(mesh * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'glitch': {
|
||||
// Digital glitch/corruption effects
|
||||
const seed = Math.floor(x + y * width + Math.abs(value));
|
||||
const random = ((seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
|
||||
const glitchThreshold = 0.95 - Math.abs(value) * 0.0001;
|
||||
let glitchValue = Math.abs(value) % 256;
|
||||
|
||||
if (random > glitchThreshold) {
|
||||
// Digital corruption: bit shifts, XOR, scrambling
|
||||
glitchValue = (glitchValue << 1) ^ (glitchValue >> 3) ^ ((x + y) & 0xFF);
|
||||
}
|
||||
|
||||
processedValue = glitchValue % 256;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'diffusion': {
|
||||
// Heat diffusion simulation - simple rule creates complex emergent patterns
|
||||
const diffusionRate = 0.1 + Math.abs(value) * 0.0001;
|
||||
const kernelSize = 3;
|
||||
const halfKernel = Math.floor(kernelSize / 2);
|
||||
|
||||
// Create heat sources based on value
|
||||
const heatSource = Math.abs(value) * 0.01;
|
||||
let totalHeat = heatSource;
|
||||
let sampleCount = 1;
|
||||
|
||||
// Sample neighboring pixels and diffuse heat
|
||||
for (let dy = -halfKernel; dy <= halfKernel; dy++) {
|
||||
for (let dx = -halfKernel; dx <= halfKernel; dx++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
|
||||
const neighborX = x + dx;
|
||||
const neighborY = y + dy;
|
||||
|
||||
// Check bounds
|
||||
if (neighborX >= 0 && neighborX < width && neighborY >= 0 && neighborY < height) {
|
||||
// Use position-based pseudo-random for neighbor heat
|
||||
const neighborSeed = neighborX + neighborY * width;
|
||||
const neighborHeat = ((neighborSeed * 1103515245 + 12345) % 256) / 256;
|
||||
|
||||
// Distance-based diffusion weight
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const weight = Math.exp(-distance * distance * 0.5);
|
||||
|
||||
totalHeat += neighborHeat * weight * diffusionRate;
|
||||
sampleCount += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Average heat with temporal decay
|
||||
const averageHeat = totalHeat / sampleCount;
|
||||
const decay = 0.95 + Math.sin(Math.abs(value) * 0.01) * 0.04;
|
||||
|
||||
// Add convection currents for more complex patterns
|
||||
const convectionX = Math.sin(x * 0.01 + Math.abs(value) * 0.001) * 0.1;
|
||||
const convectionY = Math.cos(y * 0.01 + Math.abs(value) * 0.001) * 0.1;
|
||||
const convection = (convectionX + convectionY) * 0.5 + 0.5;
|
||||
|
||||
// Final heat value with non-linear response
|
||||
const finalHeat = (averageHeat * decay + convection * 0.3) % 1;
|
||||
const enhancedHeat = Math.pow(finalHeat, 1.2); // Gamma correction for contrast
|
||||
|
||||
processedValue = Math.floor(enhancedHeat * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'cascade': {
|
||||
// Cascade system - avalanche-like propagation with multiple scales of complexity
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Create multiple cascade trigger points based on value
|
||||
const triggerPoints = [
|
||||
{
|
||||
x: centerX + Math.sin(Math.abs(value) * 0.01) * 150,
|
||||
y: centerY + Math.cos(Math.abs(value) * 0.01) * 150,
|
||||
threshold: 100 + Math.abs(value) * 0.05,
|
||||
strength: 1.0 + Math.abs(value) * 0.001
|
||||
},
|
||||
{
|
||||
x: centerX + Math.cos(Math.abs(value) * 0.015) * 200,
|
||||
y: centerY + Math.sin(Math.abs(value) * 0.018) * 120,
|
||||
threshold: 80 + Math.abs(value) * 0.08,
|
||||
strength: 0.8 + Math.sin(Math.abs(value) * 0.02) * 0.4
|
||||
},
|
||||
{
|
||||
x: centerX + Math.sin(Math.abs(value) * 0.012) * 180,
|
||||
y: centerY + Math.cos(Math.abs(value) * 0.008) * 160,
|
||||
threshold: 120 + Math.abs(value) * 0.03,
|
||||
strength: 0.6 + Math.cos(Math.abs(value) * 0.025) * 0.3
|
||||
}
|
||||
];
|
||||
|
||||
let cascadeValue = 0;
|
||||
const baseValue = Math.abs(value) % 256;
|
||||
|
||||
// Process each trigger point
|
||||
for (const trigger of triggerPoints) {
|
||||
const dx = x - trigger.x;
|
||||
const dy = y - trigger.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const maxDistance = Math.sqrt(width * width + height * height);
|
||||
const normalizedDistance = distance / maxDistance;
|
||||
|
||||
// Check if this point would trigger cascade
|
||||
if (baseValue > trigger.threshold) {
|
||||
// Create expanding wave from trigger point
|
||||
const waveFreq = 0.1 + Math.abs(value) * 0.0001;
|
||||
const wave = Math.sin(distance * waveFreq + Math.abs(value) * 0.02);
|
||||
|
||||
// Distance-based amplitude with non-linear falloff
|
||||
const amplitude = trigger.strength * Math.exp(-distance * distance * 0.000001);
|
||||
const cascadeWave = wave * amplitude;
|
||||
|
||||
// Add interference with perpendicular waves
|
||||
const perpWave = Math.cos(distance * waveFreq * 1.3 + Math.abs(value) * 0.015);
|
||||
const interference = cascadeWave + perpWave * amplitude * 0.3;
|
||||
|
||||
cascadeValue += interference;
|
||||
}
|
||||
}
|
||||
|
||||
// Add multi-scale turbulence for organic complexity
|
||||
let turbulence = 0;
|
||||
const turbFreq = 0.02 + Math.abs(value) * 0.00005;
|
||||
|
||||
for (let octave = 0; octave < 4; octave++) {
|
||||
const freq = turbFreq * Math.pow(2, octave);
|
||||
const amplitude = 1.0 / Math.pow(2, octave);
|
||||
|
||||
turbulence += Math.sin(x * freq + Math.abs(value) * 0.01) *
|
||||
Math.cos(y * freq + Math.abs(value) * 0.012) * amplitude;
|
||||
}
|
||||
|
||||
// Create threshold-based cascading effects
|
||||
const combinedValue = baseValue + cascadeValue * 50 + turbulence * 30;
|
||||
let finalValue = combinedValue;
|
||||
|
||||
// Multi-level cascade thresholds
|
||||
const thresholds = [150, 100, 200, 175];
|
||||
for (const threshold of thresholds) {
|
||||
if (Math.abs(combinedValue) > threshold) {
|
||||
// Cascade triggered - create amplification
|
||||
const amplification = (Math.abs(combinedValue) - threshold) * 0.5;
|
||||
finalValue += amplification;
|
||||
|
||||
// Add local distortion when cascade triggers
|
||||
const distortionX = Math.sin(y * 0.05 + Math.abs(value) * 0.01) * amplification * 0.1;
|
||||
const distortionY = Math.cos(x * 0.05 + Math.abs(value) * 0.015) * amplification * 0.1;
|
||||
|
||||
finalValue += distortionX + distortionY;
|
||||
}
|
||||
}
|
||||
|
||||
// Add feedback loops for complexity
|
||||
const feedback = Math.sin(finalValue * 0.02 + Math.abs(value) * 0.005) * 20;
|
||||
finalValue += feedback;
|
||||
|
||||
// Non-linear response for richer patterns
|
||||
const nonLinear = Math.tanh(finalValue * 0.01) * 128 + 128;
|
||||
|
||||
// Final enhancement with edge detection
|
||||
const edgeDetection = Math.abs(
|
||||
Math.sin(x * 0.1 + Math.abs(value) * 0.001) -
|
||||
Math.sin(y * 0.1 + Math.abs(value) * 0.001)
|
||||
) * 30;
|
||||
|
||||
processedValue = Math.floor(Math.max(0, Math.min(255, nonLinear + edgeDetection)));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Integer mode: treat value as 0-255 (original behavior)
|
||||
processedValue = Math.abs(value) % 256;
|
||||
break;
|
||||
|
||||
if (processor) {
|
||||
const precomputed = ValueModeProcessorRegistry.precomputeContext(context);
|
||||
processedValue = processor(context, precomputed);
|
||||
} else {
|
||||
// Fallback for unknown modes
|
||||
processedValue = Math.abs(value) % 256;
|
||||
}
|
||||
|
||||
|
||||
return calculateColorDirect(processedValue, renderMode, hueShift);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
872
src/shader/rendering/ValueModeProcessor.ts
Normal file
872
src/shader/rendering/ValueModeProcessor.ts
Normal file
@ -0,0 +1,872 @@
|
||||
import { RGB, MATH } from '../../utils/constants';
|
||||
|
||||
export interface PixelContext {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface PrecomputedContext {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
distance: number;
|
||||
angle: number;
|
||||
normalizedDistance: number;
|
||||
normalizedAngle: number;
|
||||
}
|
||||
|
||||
export type ValueModeProcessor = (
|
||||
context: PixelContext,
|
||||
precomputed: PrecomputedContext
|
||||
) => number;
|
||||
|
||||
export class ValueModeProcessorRegistry {
|
||||
private static instance: ValueModeProcessorRegistry;
|
||||
private processors: Map<string, ValueModeProcessor> = new Map();
|
||||
|
||||
private constructor() {
|
||||
this.initializeProcessors();
|
||||
}
|
||||
|
||||
static getInstance(): ValueModeProcessorRegistry {
|
||||
if (!ValueModeProcessorRegistry.instance) {
|
||||
ValueModeProcessorRegistry.instance = new ValueModeProcessorRegistry();
|
||||
}
|
||||
return ValueModeProcessorRegistry.instance;
|
||||
}
|
||||
|
||||
getProcessor(mode: string): ValueModeProcessor | undefined {
|
||||
return this.processors.get(mode);
|
||||
}
|
||||
|
||||
private initializeProcessors(): void {
|
||||
this.processors.set('integer', this.integerMode);
|
||||
this.processors.set('float', this.floatMode);
|
||||
this.processors.set('polar', this.polarMode);
|
||||
this.processors.set('distance', this.distanceMode);
|
||||
this.processors.set('wave', this.waveMode);
|
||||
this.processors.set('fractal', this.fractalMode);
|
||||
this.processors.set('cellular', this.cellularMode);
|
||||
this.processors.set('noise', this.noiseMode);
|
||||
this.processors.set('warp', this.warpMode);
|
||||
this.processors.set('flow', this.flowMode);
|
||||
this.processors.set('spiral', this.spiralMode);
|
||||
this.processors.set('turbulence', this.turbulenceMode);
|
||||
this.processors.set('crystal', this.crystalMode);
|
||||
this.processors.set('marble', this.marbleMode);
|
||||
this.processors.set('quantum', this.quantumMode);
|
||||
this.processors.set('logarithmic', this.logarithmicMode);
|
||||
this.processors.set('mirror', this.mirrorMode);
|
||||
this.processors.set('rings', this.ringsMode);
|
||||
this.processors.set('mesh', this.meshMode);
|
||||
this.processors.set('glitch', this.glitchMode);
|
||||
this.processors.set('diffusion', this.diffusionMode);
|
||||
this.processors.set('cascade', this.cascadeMode);
|
||||
this.processors.set('echo', this.echoMode);
|
||||
this.processors.set('mosh', this.moshMode);
|
||||
this.processors.set('fold', this.foldMode);
|
||||
}
|
||||
|
||||
static precomputeContext(context: PixelContext): PrecomputedContext {
|
||||
const centerX = context.width * 0.5;
|
||||
const centerY = context.height * 0.5;
|
||||
const dx = context.x - centerX;
|
||||
const dy = context.y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
|
||||
const normalizedDistance = distance / maxDistance;
|
||||
const normalizedAngle = (angle + Math.PI) / MATH.TWO_PI;
|
||||
|
||||
return {
|
||||
centerX,
|
||||
centerY,
|
||||
dx,
|
||||
dy,
|
||||
distance,
|
||||
angle,
|
||||
normalizedDistance,
|
||||
normalizedAngle,
|
||||
};
|
||||
}
|
||||
|
||||
private integerMode = (context: PixelContext): number => {
|
||||
return Math.abs(context.value);
|
||||
};
|
||||
|
||||
private floatMode = (context: PixelContext): number => {
|
||||
let processedValue = Math.max(0, Math.min(1, Math.abs(context.value)));
|
||||
processedValue = 1 - processedValue;
|
||||
return Math.floor(processedValue * RGB.MAX_VALUE);
|
||||
};
|
||||
|
||||
private polarMode = (
|
||||
context: PixelContext,
|
||||
precomputed: PrecomputedContext
|
||||
): number => {
|
||||
const radiusNorm = precomputed.normalizedDistance;
|
||||
const spiralEffect =
|
||||
(precomputed.normalizedAngle + radiusNorm * 0.5 + Math.abs(context.value) * 0.02) % 1;
|
||||
const polarValue = Math.sin(spiralEffect * Math.PI * 8) * 0.5 + 0.5;
|
||||
return Math.floor(polarValue * RGB.MAX_VALUE);
|
||||
};
|
||||
|
||||
private distanceMode = (
|
||||
context: PixelContext,
|
||||
precomputed: PrecomputedContext
|
||||
): number => {
|
||||
const frequency = 8 + Math.abs(context.value) * 0.1;
|
||||
const phase = Math.abs(context.value) * 0.05;
|
||||
const concentricWave =
|
||||
Math.sin(precomputed.normalizedDistance * Math.PI * frequency + phase) * 0.5 + 0.5;
|
||||
const falloff = 1 - Math.pow(precomputed.normalizedDistance, 0.8);
|
||||
const distanceValue = concentricWave * falloff;
|
||||
return Math.floor(distanceValue * RGB.MAX_VALUE);
|
||||
};
|
||||
|
||||
private waveMode = (
|
||||
context: PixelContext,
|
||||
precomputed: PrecomputedContext
|
||||
): number => {
|
||||
const baseFreq = 0.08;
|
||||
const valueScale = Math.abs(context.value) * 0.001 + 1;
|
||||
let waveSum = 0;
|
||||
|
||||
const sources = WaveConstants.SOURCES;
|
||||
const scaledSources = sources.map(source => ({
|
||||
x: context.width * source.x,
|
||||
y: context.height * source.y,
|
||||
}));
|
||||
|
||||
for (const source of scaledSources) {
|
||||
const dx = context.x - source.x;
|
||||
const dy = context.y - source.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const wave = Math.sin(dist * baseFreq * valueScale + Math.abs(context.value) * 0.02);
|
||||
const amplitude = 1 / (1 + dist * 0.002);
|
||||
waveSum += wave * amplitude;
|
||||
}
|
||||
|
||||
const waveValue = Math.tanh(waveSum) * 0.5 + 0.5;
|
||||
return Math.floor(waveValue * RGB.MAX_VALUE);
|
||||
};
|
||||
|
||||
private fractalMode = (context: PixelContext, _precomputed: PrecomputedContext): number => {
|
||||
const scale = 0.01;
|
||||
let fractalValue = 0;
|
||||
let frequency = 1;
|
||||
let amplitude = 1;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const nx = context.x * scale * frequency + Math.abs(context.value) * 0.01;
|
||||
const ny = context.y * scale * frequency;
|
||||
const noise = this.simplexNoise(nx, ny);
|
||||
fractalValue += noise * amplitude;
|
||||
frequency *= 2;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
|
||||
fractalValue = (fractalValue + 1) * 0.5;
|
||||
return Math.floor(fractalValue * RGB.MAX_VALUE);
|
||||
};
|
||||
|
||||
private cellularMode = (context: PixelContext): number => {
|
||||
const cellSize = 8;
|
||||
const cellX = Math.floor(context.x / cellSize);
|
||||
const cellY = Math.floor(context.y / cellSize);
|
||||
let liveNeighbors = 0;
|
||||
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const nx = cellX + dx;
|
||||
const ny = cellY + dy;
|
||||
const neighborValue = Math.abs(
|
||||
this.pseudoRandom(nx * 73856093 + ny * 19349663 + Math.floor(Math.abs(context.value)))
|
||||
);
|
||||
if (neighborValue > 0.5) liveNeighbors++;
|
||||
}
|
||||
}
|
||||
|
||||
const cellValue = liveNeighbors >= 4 ? 1 : 0;
|
||||
return Math.floor(cellValue * RGB.MAX_VALUE);
|
||||
};
|
||||
|
||||
private simplexNoise(x: number, y: number): number {
|
||||
const F2 = 0.5 * (Math.sqrt(3) - 1);
|
||||
const G2 = (3 - Math.sqrt(3)) / 6;
|
||||
|
||||
const s = (x + y) * F2;
|
||||
const i = Math.floor(x + s);
|
||||
const j = Math.floor(y + s);
|
||||
|
||||
const t = (i + j) * G2;
|
||||
const X0 = i - t;
|
||||
const Y0 = j - t;
|
||||
const x0 = x - X0;
|
||||
const y0 = y - Y0;
|
||||
|
||||
const i1 = x0 > y0 ? 1 : 0;
|
||||
const j1 = x0 > y0 ? 0 : 1;
|
||||
|
||||
const x1 = x0 - i1 + G2;
|
||||
const y1 = y0 - j1 + G2;
|
||||
const x2 = x0 - 1 + 2 * G2;
|
||||
const y2 = y0 - 1 + 2 * G2;
|
||||
|
||||
const ii = i & 255;
|
||||
const jj = j & 255;
|
||||
|
||||
const gi0 = this.permMod12[ii + this.perm[jj]] % 12;
|
||||
const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1]] % 12;
|
||||
const gi2 = this.permMod12[ii + 1 + this.perm[jj + 1]] % 12;
|
||||
|
||||
let n0, n1, n2;
|
||||
|
||||
let t0 = 0.5 - x0 * x0 - y0 * y0;
|
||||
if (t0 < 0) n0 = 0;
|
||||
else {
|
||||
t0 *= t0;
|
||||
n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0);
|
||||
}
|
||||
|
||||
let t1 = 0.5 - x1 * x1 - y1 * y1;
|
||||
if (t1 < 0) n1 = 0;
|
||||
else {
|
||||
t1 *= t1;
|
||||
n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1);
|
||||
}
|
||||
|
||||
let t2 = 0.5 - x2 * x2 - y2 * y2;
|
||||
if (t2 < 0) n2 = 0;
|
||||
else {
|
||||
t2 *= t2;
|
||||
n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2);
|
||||
}
|
||||
|
||||
return 70 * (n0 + n1 + n2);
|
||||
}
|
||||
|
||||
private pseudoRandom(seed: number): number {
|
||||
const x = Math.sin(seed) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
private dot(g: number[], x: number, y: number): number {
|
||||
return g[0] * x + g[1] * y;
|
||||
}
|
||||
|
||||
private grad3 = [
|
||||
[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
|
||||
[1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
|
||||
[0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1]
|
||||
];
|
||||
|
||||
private perm = [
|
||||
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225,
|
||||
140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148,
|
||||
247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32,
|
||||
57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175,
|
||||
74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
|
||||
60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54,
|
||||
65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169,
|
||||
200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64,
|
||||
52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212,
|
||||
207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
|
||||
119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
|
||||
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
|
||||
218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241,
|
||||
81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
|
||||
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
|
||||
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
|
||||
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225,
|
||||
140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148,
|
||||
247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32,
|
||||
57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175,
|
||||
74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
|
||||
60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54,
|
||||
65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169,
|
||||
200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64,
|
||||
52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212,
|
||||
207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
|
||||
119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
|
||||
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
|
||||
218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241,
|
||||
81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
|
||||
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
|
||||
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180
|
||||
];
|
||||
|
||||
private permMod12 = this.perm.map(p => p % 12);
|
||||
|
||||
private noiseMode = (context: PixelContext): number => {
|
||||
const noiseScale = 0.02;
|
||||
const nx = context.x * noiseScale + Math.abs(context.value) * 0.001;
|
||||
const ny = context.y * noiseScale + Math.abs(context.value) * 0.001;
|
||||
|
||||
const noise1 = Math.sin(nx * 6.28) * Math.cos(ny * 6.28);
|
||||
const noise2 = Math.sin(nx * 12.56) * Math.cos(ny * 12.56) * 0.5;
|
||||
const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25;
|
||||
|
||||
const combinedNoise = (noise1 + noise2 + noise3) / 1.75;
|
||||
return Math.floor((combinedNoise + 1) * 0.5 * RGB.MAX_VALUE);
|
||||
};
|
||||
private warpMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const warpStrength = Math.abs(context.value) * 0.001;
|
||||
const warpFreq = 0.02;
|
||||
|
||||
const warpX = context.x + Math.sin(context.y * warpFreq + Math.abs(context.value) * 0.01) * warpStrength * 100;
|
||||
const warpY = context.y + Math.cos(context.x * warpFreq + Math.abs(context.value) * 0.01) * warpStrength * 100;
|
||||
|
||||
const dx = warpX - precomputed.centerX;
|
||||
const dy = warpY - precomputed.centerY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const maxDist = Math.sqrt(precomputed.centerX * precomputed.centerX + precomputed.centerY * precomputed.centerY);
|
||||
const normDist = dist / maxDist;
|
||||
|
||||
const deform = 1 + Math.sin(normDist * Math.PI + Math.abs(context.value) * 0.05) * 0.3;
|
||||
const deformedX = precomputed.centerX + dx * deform;
|
||||
const deformedY = precomputed.centerY + dy * deform;
|
||||
|
||||
const finalValue = (deformedX + deformedY + Math.abs(context.value)) % 256;
|
||||
return Math.floor(Math.abs(finalValue));
|
||||
};
|
||||
private flowMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const flowSources = [
|
||||
{
|
||||
x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.01) * 200,
|
||||
y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.01) * 200,
|
||||
strength: 1 + Math.abs(context.value) * 0.01,
|
||||
},
|
||||
{
|
||||
x: precomputed.centerX + Math.cos(Math.abs(context.value) * 0.015) * 150,
|
||||
y: precomputed.centerY + Math.sin(Math.abs(context.value) * 0.015) * 150,
|
||||
strength: -0.8 + Math.sin(Math.abs(context.value) * 0.02) * 0.5,
|
||||
},
|
||||
{
|
||||
x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.008) * 300,
|
||||
y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.012) * 250,
|
||||
strength: 0.6 + Math.cos(Math.abs(context.value) * 0.018) * 0.4,
|
||||
},
|
||||
];
|
||||
|
||||
let flowX = 0;
|
||||
let flowY = 0;
|
||||
|
||||
for (const source of flowSources) {
|
||||
const dx = context.x - source.x;
|
||||
const dy = context.y - source.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const normalizedDist = Math.max(distance, 1);
|
||||
|
||||
const flowStrength = source.strength / (normalizedDist * 0.01);
|
||||
flowX += (dx / normalizedDist) * flowStrength;
|
||||
flowY += (dy / normalizedDist) * flowStrength;
|
||||
|
||||
const curlStrength = source.strength * 0.5;
|
||||
flowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||
flowY += ((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||
}
|
||||
|
||||
const globalFlowAngle = Math.abs(context.value) * 0.02;
|
||||
flowX += Math.cos(globalFlowAngle) * (Math.abs(context.value) * 0.1);
|
||||
flowY += Math.sin(globalFlowAngle) * (Math.abs(context.value) * 0.1);
|
||||
|
||||
const turbScale = 0.05;
|
||||
const turbulence = Math.sin(context.x * turbScale + Math.abs(context.value) * 0.01) *
|
||||
Math.cos(context.y * turbScale + Math.abs(context.value) * 0.015) *
|
||||
(Math.abs(context.value) * 0.02);
|
||||
|
||||
flowX += turbulence;
|
||||
flowY += turbulence * 0.7;
|
||||
|
||||
let particleX = context.x;
|
||||
let particleY = context.y;
|
||||
|
||||
for (let step = 0; step < 5; step++) {
|
||||
let localFlowX = 0;
|
||||
let localFlowY = 0;
|
||||
|
||||
for (const source of flowSources) {
|
||||
const dx = particleX - source.x;
|
||||
const dy = particleY - source.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const normalizedDist = Math.max(distance, 1);
|
||||
|
||||
const flowStrength = source.strength / (normalizedDist * 0.01);
|
||||
localFlowX += (dx / normalizedDist) * flowStrength;
|
||||
localFlowY += (dy / normalizedDist) * flowStrength;
|
||||
|
||||
const curlStrength = source.strength * 0.5;
|
||||
localFlowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||
localFlowY += ((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||
}
|
||||
|
||||
const stepSize = 0.5;
|
||||
particleX += localFlowX * stepSize;
|
||||
particleY += localFlowY * stepSize;
|
||||
}
|
||||
|
||||
const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY);
|
||||
const particleDistance = Math.sqrt(
|
||||
(particleX - context.x) * (particleX - context.x) + (particleY - context.y) * (particleY - context.y)
|
||||
);
|
||||
|
||||
const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256;
|
||||
const enhanced = Math.sin(flowValue * 0.05 + Math.abs(context.value) * 0.01) * 0.5 + 0.5;
|
||||
|
||||
return Math.floor(enhanced * RGB.MAX_VALUE);
|
||||
};
|
||||
private spiralMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const spiralTightness = 1 + Math.abs(context.value) * 0.01;
|
||||
const spiralValue = precomputed.angle + Math.log(Math.max(precomputed.distance, 1)) * spiralTightness;
|
||||
return Math.floor((Math.sin(spiralValue) * 0.5 + 0.5) * RGB.MAX_VALUE);
|
||||
};
|
||||
private turbulenceMode = (context: PixelContext): number => {
|
||||
let turbulence = 0;
|
||||
const chaos = Math.abs(context.value) * 0.001;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const freq = Math.pow(2, i) * (0.01 + chaos);
|
||||
turbulence += Math.abs(Math.sin(context.x * freq) * Math.cos(context.y * freq)) / Math.pow(2, i);
|
||||
}
|
||||
return Math.floor(Math.min(turbulence, 1) * RGB.MAX_VALUE);
|
||||
};
|
||||
private crystalMode = (context: PixelContext): number => {
|
||||
const latticeSize = 32 + Math.abs(context.value) * 0.1;
|
||||
const gridX = Math.floor(context.x / latticeSize);
|
||||
const gridY = Math.floor(context.y / latticeSize);
|
||||
const crystal = Math.sin(gridX + gridY + Math.abs(context.value) * 0.01) *
|
||||
Math.cos(gridX * gridY + Math.abs(context.value) * 0.005);
|
||||
return Math.floor((crystal * 0.5 + 0.5) * RGB.MAX_VALUE);
|
||||
};
|
||||
private marbleMode = (context: PixelContext): number => {
|
||||
const noiseFreq = 0.005 + Math.abs(context.value) * 0.00001;
|
||||
const turbulence = Math.sin(context.x * noiseFreq) * Math.cos(context.y * noiseFreq) +
|
||||
Math.sin(context.x * noiseFreq * 2) * Math.cos(context.y * noiseFreq * 2) * 0.5;
|
||||
const marble = Math.sin((context.x + turbulence * 50) * 0.02 + Math.abs(context.value) * 0.001);
|
||||
return Math.floor((marble * 0.5 + 0.5) * RGB.MAX_VALUE);
|
||||
};
|
||||
private quantumMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const uncertainty = Math.abs(context.value) * 0.001;
|
||||
const distSquared = precomputed.dx * precomputed.dx + precomputed.dy * precomputed.dy;
|
||||
const sigmaSquared = (100 + uncertainty * 1000);
|
||||
const probability = Math.exp(-distSquared / (2 * sigmaSquared * sigmaSquared));
|
||||
const quantum = probability * (1 + Math.sin(context.x * context.y * uncertainty) * 0.5);
|
||||
return Math.floor(Math.min(quantum, 1) * RGB.MAX_VALUE);
|
||||
};
|
||||
private logarithmicMode = (context: PixelContext): number => {
|
||||
const logValue = Math.log(1 + Math.abs(context.value));
|
||||
return Math.floor((logValue / Math.log(256)) * RGB.MAX_VALUE);
|
||||
};
|
||||
private mirrorMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const dx = Math.abs(context.x - precomputed.centerX);
|
||||
const dy = Math.abs(context.y - precomputed.centerY);
|
||||
const mirrorX = precomputed.centerX + (dx % precomputed.centerX);
|
||||
const mirrorY = precomputed.centerY + (dy % precomputed.centerY);
|
||||
const mirrorDistance = Math.sqrt(mirrorX * mirrorX + mirrorY * mirrorY);
|
||||
const mirrorValue = (Math.abs(context.value) + mirrorDistance) % 256;
|
||||
return mirrorValue;
|
||||
};
|
||||
private ringsMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const ringSpacing = 20 + Math.abs(context.value) * 0.1;
|
||||
const rings = Math.sin((precomputed.distance / ringSpacing) * Math.PI * 2);
|
||||
const interference = Math.sin((precomputed.distance + Math.abs(context.value)) * 0.05);
|
||||
return Math.floor(((rings * interference) * 0.5 + 0.5) * RGB.MAX_VALUE);
|
||||
};
|
||||
private meshMode = (context: PixelContext): number => {
|
||||
const angle = Math.abs(context.value) * 0.001;
|
||||
const rotX = context.x * Math.cos(angle) - context.y * Math.sin(angle);
|
||||
const rotY = context.x * Math.sin(angle) + context.y * Math.cos(angle);
|
||||
const gridSize = 16 + Math.abs(context.value) * 0.05;
|
||||
const gridX = Math.sin((rotX / gridSize) * Math.PI * 2);
|
||||
const gridY = Math.sin((rotY / gridSize) * Math.PI * 2);
|
||||
const mesh = Math.max(Math.abs(gridX), Math.abs(gridY));
|
||||
return Math.floor(mesh * RGB.MAX_VALUE);
|
||||
};
|
||||
private glitchMode = (context: PixelContext): number => {
|
||||
const seed = Math.floor(context.x + context.y * context.width + Math.abs(context.value));
|
||||
const random = ((seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
|
||||
const glitchThreshold = 0.95 - Math.abs(context.value) * 0.0001;
|
||||
let glitchValue = Math.abs(context.value) % 256;
|
||||
|
||||
if (random > glitchThreshold) {
|
||||
glitchValue = (glitchValue << 1) ^ (glitchValue >> 3) ^ ((context.x + context.y) & 0xFF);
|
||||
}
|
||||
|
||||
return glitchValue % 256;
|
||||
};
|
||||
private diffusionMode = (context: PixelContext): number => {
|
||||
const diffusionRate = 0.1 + Math.abs(context.value) * 0.0001;
|
||||
const kernelSize = 3;
|
||||
const halfKernel = Math.floor(kernelSize / 2);
|
||||
|
||||
const heatSource = Math.abs(context.value) * 0.01;
|
||||
let totalHeat = heatSource;
|
||||
let sampleCount = 1;
|
||||
|
||||
for (let dy = -halfKernel; dy <= halfKernel; dy++) {
|
||||
for (let dx = -halfKernel; dx <= halfKernel; dx++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
|
||||
const neighborX = context.x + dx;
|
||||
const neighborY = context.y + dy;
|
||||
|
||||
if (neighborX >= 0 && neighborX < context.width && neighborY >= 0 && neighborY < context.height) {
|
||||
const neighborSeed = neighborX + neighborY * context.width;
|
||||
const neighborHeat = ((neighborSeed * 1103515245 + 12345) % 256) / 256;
|
||||
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const weight = Math.exp(-distance * distance * 0.5);
|
||||
|
||||
totalHeat += neighborHeat * weight * diffusionRate;
|
||||
sampleCount += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const averageHeat = totalHeat / sampleCount;
|
||||
const decay = 0.95 + Math.sin(Math.abs(context.value) * 0.01) * 0.04;
|
||||
|
||||
const convectionX = Math.sin(context.x * 0.01 + Math.abs(context.value) * 0.001) * 0.1;
|
||||
const convectionY = Math.cos(context.y * 0.01 + Math.abs(context.value) * 0.001) * 0.1;
|
||||
const convection = (convectionX + convectionY) * 0.5 + 0.5;
|
||||
|
||||
const finalHeat = (averageHeat * decay + convection * 0.3) % 1;
|
||||
const enhancedHeat = Math.pow(finalHeat, 1.2);
|
||||
|
||||
return Math.floor(enhancedHeat * RGB.MAX_VALUE);
|
||||
};
|
||||
private cascadeMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const triggerPoints = [
|
||||
{
|
||||
x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.01) * 150,
|
||||
y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.01) * 150,
|
||||
threshold: 100 + Math.abs(context.value) * 0.05,
|
||||
strength: 1.0 + Math.abs(context.value) * 0.001
|
||||
},
|
||||
{
|
||||
x: precomputed.centerX + Math.cos(Math.abs(context.value) * 0.015) * 200,
|
||||
y: precomputed.centerY + Math.sin(Math.abs(context.value) * 0.018) * 120,
|
||||
threshold: 80 + Math.abs(context.value) * 0.08,
|
||||
strength: 0.8 + Math.sin(Math.abs(context.value) * 0.02) * 0.4
|
||||
},
|
||||
{
|
||||
x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.012) * 180,
|
||||
y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.008) * 160,
|
||||
threshold: 120 + Math.abs(context.value) * 0.03,
|
||||
strength: 0.6 + Math.cos(Math.abs(context.value) * 0.025) * 0.3
|
||||
}
|
||||
];
|
||||
|
||||
let cascadeValue = 0;
|
||||
const baseValue = Math.abs(context.value) % 256;
|
||||
|
||||
for (const trigger of triggerPoints) {
|
||||
const dx = context.x - trigger.x;
|
||||
const dy = context.y - trigger.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const maxDistance = Math.sqrt(context.width * context.width + context.height * context.height);
|
||||
const normalizedDistance = distance / maxDistance;
|
||||
|
||||
if (baseValue > trigger.threshold) {
|
||||
const waveFreq = 0.1 + Math.abs(context.value) * 0.0001;
|
||||
const wave = Math.sin(distance * waveFreq + Math.abs(context.value) * 0.02);
|
||||
|
||||
const amplitude = trigger.strength * Math.exp(-distance * distance * 0.000001);
|
||||
const cascadeWave = wave * amplitude;
|
||||
|
||||
const perpWave = Math.cos(distance * waveFreq * 1.3 + Math.abs(context.value) * 0.015);
|
||||
const interference = cascadeWave + perpWave * amplitude * 0.3;
|
||||
|
||||
cascadeValue += interference;
|
||||
}
|
||||
}
|
||||
|
||||
let turbulence = 0;
|
||||
const turbFreq = 0.02 + Math.abs(context.value) * 0.00005;
|
||||
|
||||
for (let octave = 0; octave < 4; octave++) {
|
||||
const freq = turbFreq * Math.pow(2, octave);
|
||||
const amplitude = 1.0 / Math.pow(2, octave);
|
||||
|
||||
turbulence += Math.sin(context.x * freq + Math.abs(context.value) * 0.01) *
|
||||
Math.cos(context.y * freq + Math.abs(context.value) * 0.012) * amplitude;
|
||||
}
|
||||
|
||||
const combinedValue = baseValue + cascadeValue * 50 + turbulence * 30;
|
||||
let finalValue = combinedValue;
|
||||
|
||||
const thresholds = [150, 100, 200, 175];
|
||||
for (const threshold of thresholds) {
|
||||
if (Math.abs(combinedValue) > threshold) {
|
||||
const amplification = (Math.abs(combinedValue) - threshold) * 0.5;
|
||||
finalValue += amplification;
|
||||
|
||||
const distortionX = Math.sin(context.y * 0.05 + Math.abs(context.value) * 0.01) * amplification * 0.1;
|
||||
const distortionY = Math.cos(context.x * 0.05 + Math.abs(context.value) * 0.015) * amplification * 0.1;
|
||||
|
||||
finalValue += distortionX + distortionY;
|
||||
}
|
||||
}
|
||||
|
||||
const feedback = Math.sin(finalValue * 0.02 + Math.abs(context.value) * 0.005) * 20;
|
||||
finalValue += feedback;
|
||||
|
||||
const nonLinear = Math.tanh(finalValue * 0.01) * 128 + 128;
|
||||
|
||||
const edgeDetection = Math.abs(
|
||||
Math.sin(context.x * 0.1 + Math.abs(context.value) * 0.001) -
|
||||
Math.sin(context.y * 0.1 + Math.abs(context.value) * 0.001)
|
||||
) * 30;
|
||||
|
||||
return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, nonLinear + edgeDetection)));
|
||||
};
|
||||
private echoMode = (context: PixelContext): number => {
|
||||
const baseValue = Math.abs(context.value) % 256;
|
||||
|
||||
const echoSources = [
|
||||
{ delay: 0.1, decay: 0.8, spatial: 0.02, twist: 1.0 },
|
||||
{ delay: 0.25, decay: 0.6, spatial: 0.05, twist: -0.7 },
|
||||
{ delay: 0.4, decay: 0.45, spatial: 0.03, twist: 1.3 },
|
||||
{ delay: 0.6, decay: 0.3, spatial: 0.08, twist: -0.9 }
|
||||
];
|
||||
|
||||
let echoSum = baseValue;
|
||||
let totalWeight = 1.0;
|
||||
|
||||
for (const echo of echoSources) {
|
||||
const spatialOffsetX = Math.sin(context.x * echo.spatial + echo.twist) * echo.delay * 100;
|
||||
const spatialOffsetY = Math.cos(context.y * echo.spatial + echo.twist * 0.7) * echo.delay * 80;
|
||||
|
||||
const offsetX = context.x + spatialOffsetX;
|
||||
const offsetY = context.y + spatialOffsetY;
|
||||
|
||||
const offsetSeed = Math.floor(offsetX) + Math.floor(offsetY) * context.width;
|
||||
const offsetNoise = ((offsetSeed * 1103515245 + 12345) % 256) / 256;
|
||||
const delayedValue = (baseValue * (1 - echo.delay) + offsetNoise * 255 * echo.delay) % 256;
|
||||
|
||||
const harmonic = Math.sin(delayedValue * 0.05 + echo.twist) * 30;
|
||||
const distortedEcho = delayedValue + harmonic;
|
||||
|
||||
const feedback = Math.sin(distortedEcho * 0.02 + context.x * 0.01 + context.y * 0.01) * echo.decay * 40;
|
||||
const finalEcho = distortedEcho + feedback;
|
||||
|
||||
echoSum += finalEcho * echo.decay;
|
||||
totalWeight += echo.decay;
|
||||
}
|
||||
|
||||
const interference = Math.sin(echoSum * 0.03) * Math.cos(baseValue * 0.04) * 25;
|
||||
echoSum += interference;
|
||||
|
||||
const compressed = Math.tanh(echoSum / totalWeight * 0.02) * RGB.MAX_VALUE;
|
||||
|
||||
return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, compressed)));
|
||||
};
|
||||
private moshMode = (context: PixelContext): number => {
|
||||
const baseValue = Math.abs(context.value) % 256;
|
||||
|
||||
const pseudoTime = (context.x + context.y + baseValue) * 0.1;
|
||||
const temporalDrift = Math.floor(pseudoTime * 100) % 1024;
|
||||
const microJitter = Math.sin(pseudoTime * 20) * 0.8;
|
||||
|
||||
const blockSize = 8;
|
||||
const driftedX = context.x + Math.sin(temporalDrift * 0.01 + context.y * 0.1) * microJitter;
|
||||
const driftedY = context.y + Math.cos(temporalDrift * 0.008 + context.x * 0.12) * microJitter;
|
||||
|
||||
const blockX = Math.floor(driftedX / blockSize);
|
||||
const blockY = Math.floor(driftedY / blockSize);
|
||||
const blockId = blockX + blockY * Math.floor(context.width / blockSize);
|
||||
|
||||
const corruptionLevel = (baseValue / 255.0) * 0.8 + 0.1;
|
||||
const blockSeed = blockId * 1103515245 + baseValue + temporalDrift;
|
||||
const blockRandom = ((blockSeed % 65536) / 65536);
|
||||
|
||||
let corruptedValue = baseValue;
|
||||
|
||||
if (blockRandom < corruptionLevel * 0.3) {
|
||||
const motionX = ((blockSeed >> 8) & 7) - 4 + Math.sin(temporalDrift * 0.02) * 0.5;
|
||||
const motionY = ((blockSeed >> 11) & 7) - 4 + Math.cos(temporalDrift * 0.018) * 0.5;
|
||||
|
||||
const sourceX = context.x + motionX * 2;
|
||||
const sourceY = context.y + motionY * 2;
|
||||
|
||||
if (sourceX >= 0 && sourceX < context.width && sourceY >= 0 && sourceY < context.height) {
|
||||
const sourceSeed = Math.floor(sourceX) + Math.floor(sourceY) * context.width;
|
||||
const sourceNoise = ((sourceSeed * 1664525 + 1013904223) % 256) / 256;
|
||||
corruptedValue = Math.floor(sourceNoise * 255);
|
||||
}
|
||||
}
|
||||
else if (blockRandom < corruptionLevel * 0.6) {
|
||||
const localX = context.x % blockSize;
|
||||
const localY = context.y % blockSize;
|
||||
|
||||
const dctFreqX = Math.floor(localX / 2);
|
||||
const dctFreqY = Math.floor(localY / 2);
|
||||
const dctCoeff = Math.sin((dctFreqX + dctFreqY) * Math.PI / 4);
|
||||
|
||||
const corruption = dctCoeff * (blockRandom - 0.5) * 100;
|
||||
corruptedValue = Math.floor(baseValue + corruption);
|
||||
}
|
||||
else if (blockRandom < corruptionLevel * 0.8) {
|
||||
const bleedIntensity = (blockRandom - 0.6) * 5;
|
||||
const temporalPhase = temporalDrift * 0.03;
|
||||
const bleedX = Math.sin(context.x * 0.1 + baseValue * 0.05 + temporalPhase) * bleedIntensity;
|
||||
const bleedY = Math.cos(context.y * 0.1 + baseValue * 0.03 + temporalPhase * 0.8) * bleedIntensity;
|
||||
|
||||
corruptedValue = Math.floor(baseValue + bleedX + bleedY);
|
||||
}
|
||||
else if (blockRandom < corruptionLevel) {
|
||||
const quantLevels = 8 + Math.floor((1 - corruptionLevel) * 16);
|
||||
const quantStep = 256 / quantLevels;
|
||||
corruptedValue = Math.floor(baseValue / quantStep) * quantStep;
|
||||
|
||||
const quantNoise = ((blockSeed >> 16) & 15) - 8;
|
||||
corruptedValue += quantNoise;
|
||||
}
|
||||
|
||||
if (blockRandom > 0.95) {
|
||||
const tempSeed = context.x * 73856093 + context.y * 19349663 + baseValue;
|
||||
const tempNoise = ((tempSeed % 256) / 256);
|
||||
const mixRatio = corruptionLevel * 0.7;
|
||||
|
||||
corruptedValue = Math.floor(corruptedValue * (1 - mixRatio) + tempNoise * 255 * mixRatio);
|
||||
}
|
||||
|
||||
const ringFreq = 0.3 + corruptionLevel * 0.5;
|
||||
const temporalRingPhase = temporalDrift * 0.01;
|
||||
const ringing = Math.sin(context.x * ringFreq + temporalRingPhase) * Math.cos(context.y * ringFreq + temporalRingPhase * 0.7) * corruptionLevel * 15;
|
||||
corruptedValue += ringing;
|
||||
|
||||
const edgeDetect = Math.abs(Math.sin(context.x * 0.2 + temporalDrift * 0.005)) + Math.abs(Math.sin(context.y * 0.2 + temporalDrift * 0.007));
|
||||
if (edgeDetect > 1.5) {
|
||||
const mosquitoNoise = ((blockSeed >> 20) & 31) - 16 + Math.sin(temporalDrift * 0.1) * 3;
|
||||
corruptedValue += mosquitoNoise * corruptionLevel;
|
||||
}
|
||||
|
||||
return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, corruptedValue)));
|
||||
};
|
||||
private foldMode = (context: PixelContext, precomputed: PrecomputedContext): number => {
|
||||
const baseValue = Math.abs(context.value) % 256;
|
||||
const normalizedValue = baseValue / 255.0;
|
||||
|
||||
const foldLines = [
|
||||
{
|
||||
position: 0.2 + Math.sin(context.x * 0.01 + normalizedValue * 4) * 0.15,
|
||||
angle: Math.PI * 0.25 + normalizedValue * Math.PI * 0.5,
|
||||
strength: 1.0,
|
||||
type: 'valley'
|
||||
},
|
||||
{
|
||||
position: 0.5 + Math.cos(context.y * 0.008 + normalizedValue * 3) * 0.2,
|
||||
angle: Math.PI * 0.75 + Math.sin(normalizedValue * 6) * Math.PI * 0.3,
|
||||
strength: 0.8,
|
||||
type: 'mountain'
|
||||
},
|
||||
{
|
||||
position: 0.75 + Math.sin((context.x + context.y) * 0.005 + normalizedValue * 2) * 0.1,
|
||||
angle: Math.PI * 1.1 + Math.cos(normalizedValue * 8) * Math.PI * 0.4,
|
||||
strength: 0.6,
|
||||
type: 'valley'
|
||||
},
|
||||
{
|
||||
position: 0.35 + Math.cos(context.x * 0.012 - context.y * 0.008 + normalizedValue * 5) * 0.18,
|
||||
angle: Math.PI * 1.5 + normalizedValue * Math.PI,
|
||||
strength: 0.9,
|
||||
type: 'mountain'
|
||||
}
|
||||
];
|
||||
|
||||
let foldedValue = normalizedValue;
|
||||
let geometryComplexity = 1.0;
|
||||
|
||||
for (const fold of foldLines) {
|
||||
const cos_a = Math.cos(fold.angle);
|
||||
const sin_a = Math.sin(fold.angle);
|
||||
|
||||
const rotX = (context.x - precomputed.centerX) * cos_a + (context.y - precomputed.centerY) * sin_a;
|
||||
const rotY = -(context.x - precomputed.centerX) * sin_a + (context.y - precomputed.centerY) * cos_a;
|
||||
|
||||
const foldDistance = Math.abs(rotY) / context.height;
|
||||
const foldPosition = (rotX / context.width + 1) * 0.5;
|
||||
|
||||
const foldSide = Math.sign(rotY);
|
||||
|
||||
if (Math.abs(foldedValue - fold.position) < 0.3) {
|
||||
const foldInfluence = Math.exp(-foldDistance * 8) * fold.strength;
|
||||
|
||||
if (fold.type === 'valley') {
|
||||
if (foldedValue > fold.position) {
|
||||
const excess = foldedValue - fold.position;
|
||||
const foldedExcess = excess * (1 - foldInfluence) - excess * foldInfluence * 0.5;
|
||||
foldedValue = fold.position + foldedExcess;
|
||||
} else {
|
||||
const deficit = fold.position - foldedValue;
|
||||
const foldedDeficit = deficit * (1 - foldInfluence) - deficit * foldInfluence * 0.5;
|
||||
foldedValue = fold.position - foldedDeficit;
|
||||
}
|
||||
} else {
|
||||
if (foldedValue > fold.position) {
|
||||
const excess = foldedValue - fold.position;
|
||||
const expandedExcess = excess * (1 + foldInfluence * 0.8);
|
||||
foldedValue = fold.position + expandedExcess;
|
||||
} else {
|
||||
const deficit = fold.position - foldedValue;
|
||||
const expandedDeficit = deficit * (1 + foldInfluence * 0.8);
|
||||
foldedValue = fold.position - expandedDeficit;
|
||||
}
|
||||
}
|
||||
|
||||
const creaseSharpness = Math.exp(-Math.abs(foldedValue - fold.position) * 20) * fold.strength;
|
||||
const creaseEffect = Math.sin(foldPosition * Math.PI * 8 + fold.angle) * creaseSharpness * 0.1;
|
||||
foldedValue += creaseEffect;
|
||||
|
||||
geometryComplexity *= (1 + foldInfluence * 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
const recursiveFolds = 3;
|
||||
for (let r = 0; r < recursiveFolds; r++) {
|
||||
const recursiveScale = Math.pow(0.6, r);
|
||||
const recursiveFreq = Math.pow(2, r + 2);
|
||||
|
||||
const recursiveFoldPos = 0.5 + Math.sin(foldedValue * Math.PI * recursiveFreq + r) * 0.2 * recursiveScale;
|
||||
|
||||
if (Math.abs(foldedValue - recursiveFoldPos) < 0.1 * recursiveScale) {
|
||||
const recursiveInfluence = Math.exp(-Math.abs(foldedValue - recursiveFoldPos) * 30 / recursiveScale) * recursiveScale;
|
||||
|
||||
const microFold = (foldedValue - recursiveFoldPos) * (1 - recursiveInfluence * 0.7);
|
||||
foldedValue = recursiveFoldPos + microFold;
|
||||
|
||||
const microCrease = Math.sin(foldedValue * Math.PI * recursiveFreq * 4) * recursiveInfluence * 0.05;
|
||||
foldedValue += microCrease;
|
||||
}
|
||||
}
|
||||
|
||||
const distortion = Math.sin(foldedValue * Math.PI * geometryComplexity) * Math.cos(geometryComplexity * 2) * 0.15;
|
||||
foldedValue += distortion;
|
||||
|
||||
const edgeDetection = Math.abs(Math.sin(foldedValue * Math.PI * 16)) * 0.2;
|
||||
const edgeEnhancement = Math.pow(edgeDetection, 2) * geometryComplexity * 0.1;
|
||||
foldedValue += edgeEnhancement;
|
||||
|
||||
const materialResponse = Math.tanh(foldedValue * 3) * 0.85 + 0.15;
|
||||
const paperTexture = Math.sin(materialResponse * Math.PI * 32 + geometryComplexity) * 0.05;
|
||||
|
||||
const finalValue = (materialResponse + paperTexture) * RGB.MAX_VALUE;
|
||||
|
||||
return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, finalValue)));
|
||||
};
|
||||
}
|
||||
|
||||
class WaveConstants {
|
||||
static readonly SOURCES = [
|
||||
{ x: 0.3, y: 0.3 },
|
||||
{ x: 0.7, y: 0.3 },
|
||||
{ x: 0.5, y: 0.7 },
|
||||
{ x: 0.2, y: 0.8 },
|
||||
];
|
||||
}
|
||||
@ -1,7 +1,14 @@
|
||||
import {
|
||||
RGB,
|
||||
HSV,
|
||||
COLOR_TRANSITIONS,
|
||||
COLOR_MODE_CONSTANTS,
|
||||
} from './constants';
|
||||
|
||||
export function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
r /= RGB.MAX_VALUE;
|
||||
g /= RGB.MAX_VALUE;
|
||||
b /= RGB.MAX_VALUE;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
@ -13,13 +20,13 @@ export function rgbToHsv(r: number, g: number, b: number): [number, number, numb
|
||||
|
||||
if (delta !== 0) {
|
||||
if (max === r) {
|
||||
h = ((g - b) / delta) % 6;
|
||||
h = ((g - b) / delta) % HSV.HUE_SECTORS;
|
||||
} else if (max === g) {
|
||||
h = (b - r) / delta + 2;
|
||||
h = (b - r) / delta + HSV.SECTOR_OFFSETS.GREEN;
|
||||
} else {
|
||||
h = (r - g) / delta + 4;
|
||||
h = (r - g) / delta + HSV.SECTOR_OFFSETS.BLUE;
|
||||
}
|
||||
h /= 6;
|
||||
h /= HSV.HUE_SECTORS;
|
||||
}
|
||||
|
||||
if (h < 0) h += 1;
|
||||
@ -33,30 +40,30 @@ export function hsvToRgb(
|
||||
v: number
|
||||
): [number, number, number] {
|
||||
const c = v * s;
|
||||
const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
|
||||
const x = c * (1 - Math.abs(((h * HSV.HUE_SECTORS) % 2) - 1));
|
||||
const m = v - c;
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0;
|
||||
|
||||
if (h < 1 / 6) {
|
||||
if (h < HSV.SECTOR_BOUNDARIES.SIXTH) {
|
||||
r = c;
|
||||
g = x;
|
||||
b = 0;
|
||||
} else if (h < 2 / 6) {
|
||||
} else if (h < HSV.SECTOR_BOUNDARIES.THIRD) {
|
||||
r = x;
|
||||
g = c;
|
||||
b = 0;
|
||||
} else if (h < 3 / 6) {
|
||||
} else if (h < HSV.SECTOR_BOUNDARIES.HALF) {
|
||||
r = 0;
|
||||
g = c;
|
||||
b = x;
|
||||
} else if (h < 4 / 6) {
|
||||
} else if (h < HSV.SECTOR_BOUNDARIES.TWO_THIRDS) {
|
||||
r = 0;
|
||||
g = x;
|
||||
b = c;
|
||||
} else if (h < 5 / 6) {
|
||||
} else if (h < HSV.SECTOR_BOUNDARIES.FIVE_SIXTHS) {
|
||||
r = x;
|
||||
g = 0;
|
||||
b = c;
|
||||
@ -67,9 +74,9 @@ export function hsvToRgb(
|
||||
}
|
||||
|
||||
return [
|
||||
Math.round((r + m) * 255),
|
||||
Math.round((g + m) * 255),
|
||||
Math.round((b + m) * 255),
|
||||
Math.round((r + m) * RGB.MAX_VALUE),
|
||||
Math.round((g + m) * RGB.MAX_VALUE),
|
||||
Math.round((b + m) * RGB.MAX_VALUE),
|
||||
];
|
||||
}
|
||||
|
||||
@ -87,7 +94,7 @@ export function applyHueShift(rgb: [number, number, number], hueShiftDegrees: nu
|
||||
}
|
||||
|
||||
export function rainbowColor(value: number): [number, number, number] {
|
||||
const phase = (value / 255.0) * 6;
|
||||
const phase = (value / RGB.MAX_VALUE) * COLOR_MODE_CONSTANTS.RAINBOW_PHASE_MULTIPLIER;
|
||||
const segment = Math.floor(phase);
|
||||
const remainder = phase - segment;
|
||||
const t = remainder;
|
||||
@ -95,112 +102,142 @@ export function rainbowColor(value: number): [number, number, number] {
|
||||
|
||||
switch (segment % 6) {
|
||||
case 0:
|
||||
return [255, Math.round(t * 255), 0];
|
||||
return [RGB.MAX_VALUE, Math.round(t * RGB.MAX_VALUE), 0];
|
||||
case 1:
|
||||
return [Math.round(q * 255), 255, 0];
|
||||
return [Math.round(q * RGB.MAX_VALUE), RGB.MAX_VALUE, 0];
|
||||
case 2:
|
||||
return [0, 255, Math.round(t * 255)];
|
||||
return [0, RGB.MAX_VALUE, Math.round(t * RGB.MAX_VALUE)];
|
||||
case 3:
|
||||
return [0, Math.round(q * 255), 255];
|
||||
return [0, Math.round(q * RGB.MAX_VALUE), RGB.MAX_VALUE];
|
||||
case 4:
|
||||
return [Math.round(t * 255), 0, 255];
|
||||
return [Math.round(t * RGB.MAX_VALUE), 0, RGB.MAX_VALUE];
|
||||
case 5:
|
||||
return [255, 0, Math.round(q * 255)];
|
||||
return [RGB.MAX_VALUE, 0, Math.round(q * RGB.MAX_VALUE)];
|
||||
default:
|
||||
return [255, 255, 255];
|
||||
return [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE];
|
||||
}
|
||||
}
|
||||
|
||||
export function thermalColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
if (t < 0.25) {
|
||||
return [0, 0, Math.round(t * 4 * 255)];
|
||||
return [0, 0, Math.round(t * 4 * RGB.MAX_VALUE)];
|
||||
} else if (t < 0.5) {
|
||||
return [0, Math.round((t - 0.25) * 4 * 255), 255];
|
||||
return [0, Math.round((t - 0.25) * 4 * RGB.MAX_VALUE), RGB.MAX_VALUE];
|
||||
} else if (t < 0.75) {
|
||||
return [
|
||||
Math.round((t - 0.5) * 4 * 255),
|
||||
255,
|
||||
Math.round((0.75 - t) * 4 * 255),
|
||||
Math.round((t - 0.5) * 4 * RGB.MAX_VALUE),
|
||||
RGB.MAX_VALUE,
|
||||
Math.round((0.75 - t) * 4 * RGB.MAX_VALUE),
|
||||
];
|
||||
} else {
|
||||
return [255, 255, Math.round((t - 0.75) * 4 * 255)];
|
||||
return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round((t - 0.75) * 4 * RGB.MAX_VALUE)];
|
||||
}
|
||||
}
|
||||
|
||||
export function neonColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const intensity = Math.pow(Math.sin(t * Math.PI), 2);
|
||||
const glow = Math.pow(intensity, 0.5);
|
||||
return [
|
||||
Math.round(glow * 255),
|
||||
Math.round(intensity * 255),
|
||||
Math.round(Math.pow(intensity, 2) * 255),
|
||||
Math.round(glow * RGB.MAX_VALUE),
|
||||
Math.round(intensity * RGB.MAX_VALUE),
|
||||
Math.round(Math.pow(intensity, 2) * RGB.MAX_VALUE),
|
||||
];
|
||||
}
|
||||
|
||||
export function cyberpunkColor(value: number): [number, number, number] {
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const pulse = Math.sin(t * Math.PI * COLOR_TRANSITIONS.CYBERPUNK.PULSE_FREQUENCY) * COLOR_TRANSITIONS.CYBERPUNK.PULSE_AMPLITUDE + COLOR_TRANSITIONS.CYBERPUNK.PULSE_OFFSET;
|
||||
|
||||
if (t < COLOR_TRANSITIONS.CYBERPUNK.LOW) {
|
||||
return [Math.round(t * 5 * 50), 0, Math.round(t * 5 * 100)];
|
||||
} else if (t < COLOR_TRANSITIONS.CYBERPUNK.MID) {
|
||||
const p = (t - COLOR_TRANSITIONS.CYBERPUNK.LOW) / COLOR_TRANSITIONS.CYBERPUNK.LOW;
|
||||
return [
|
||||
Math.round(50 + p * 205 * pulse),
|
||||
Math.round(p * 50),
|
||||
Math.round(100 + p * 155)
|
||||
];
|
||||
} else if (t < COLOR_TRANSITIONS.CYBERPUNK.HIGH) {
|
||||
const p = (t - COLOR_TRANSITIONS.CYBERPUNK.MID) / (COLOR_TRANSITIONS.CYBERPUNK.HIGH - COLOR_TRANSITIONS.CYBERPUNK.MID);
|
||||
return [
|
||||
Math.round(RGB.MAX_VALUE * pulse),
|
||||
Math.round(50 + p * 205 * pulse),
|
||||
Math.round(RGB.MAX_VALUE - p * 100)
|
||||
];
|
||||
} else {
|
||||
const p = (t - 0.7) / 0.3;
|
||||
return [
|
||||
Math.round((RGB.MAX_VALUE - p * 155) * pulse),
|
||||
Math.round(RGB.MAX_VALUE * pulse),
|
||||
Math.round(155 + p * 100 * pulse)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export function sunsetColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
if (t < 0.3) {
|
||||
return [Math.round(t * 3.33 * 255), 0, Math.round(t * 1.67 * 255)];
|
||||
return [Math.round(t * 3.33 * RGB.MAX_VALUE), 0, Math.round(t * 1.67 * RGB.MAX_VALUE)];
|
||||
} else if (t < 0.6) {
|
||||
const p = (t - 0.3) / 0.3;
|
||||
return [255, Math.round(p * 100), Math.round(50 * (1 - p))];
|
||||
return [RGB.MAX_VALUE, Math.round(p * 100), Math.round(50 * (1 - p))];
|
||||
} else {
|
||||
const p = (t - 0.6) / 0.4;
|
||||
return [255, Math.round(100 + p * 155), Math.round(p * 100)];
|
||||
return [RGB.MAX_VALUE, Math.round(100 + p * 155), Math.round(p * 100)];
|
||||
}
|
||||
}
|
||||
|
||||
export function oceanColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
if (t < 0.25) {
|
||||
return [0, Math.round(t * 2 * 255), Math.round(100 + t * 4 * 155)];
|
||||
return [0, Math.round(t * 2 * RGB.MAX_VALUE), Math.round(100 + t * 4 * 155)];
|
||||
} else if (t < 0.5) {
|
||||
const p = (t - 0.25) / 0.25;
|
||||
return [0, Math.round(128 + p * 127), 255];
|
||||
return [0, Math.round(128 + p * 127), RGB.MAX_VALUE];
|
||||
} else if (t < 0.75) {
|
||||
const p = (t - 0.5) / 0.25;
|
||||
return [Math.round(p * 100), 255, Math.round(255 - p * 100)];
|
||||
return [Math.round(p * 100), RGB.MAX_VALUE, Math.round(RGB.MAX_VALUE - p * 100)];
|
||||
} else {
|
||||
const p = (t - 0.75) / 0.25;
|
||||
return [Math.round(100 + p * 155), 255, Math.round(155 + p * 100)];
|
||||
return [Math.round(100 + p * 155), RGB.MAX_VALUE, Math.round(155 + p * 100)];
|
||||
}
|
||||
}
|
||||
|
||||
export function forestColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
if (t < 0.3) {
|
||||
return [Math.round(t * 2 * 255), Math.round(50 + t * 3 * 205), 0];
|
||||
return [Math.round(t * 2 * RGB.MAX_VALUE), Math.round(50 + t * 3 * 205), 0];
|
||||
} else if (t < 0.6) {
|
||||
const p = (t - 0.3) / 0.3;
|
||||
return [Math.round(150 - p * 100), 255, Math.round(p * 100)];
|
||||
return [Math.round(150 - p * 100), RGB.MAX_VALUE, Math.round(p * 100)];
|
||||
} else {
|
||||
const p = (t - 0.6) / 0.4;
|
||||
return [Math.round(50 + p * 100), Math.round(255 - p * 100), Math.round(100 + p * 55)];
|
||||
return [Math.round(50 + p * 100), Math.round(RGB.MAX_VALUE - p * 100), Math.round(100 + p * 55)];
|
||||
}
|
||||
}
|
||||
|
||||
export function copperColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
if (t < 0.4) {
|
||||
return [Math.round(t * 2.5 * 255), Math.round(t * 1.5 * 255), Math.round(t * 0.5 * 255)];
|
||||
return [Math.round(t * 2.5 * RGB.MAX_VALUE), Math.round(t * 1.5 * RGB.MAX_VALUE), Math.round(t * 0.5 * RGB.MAX_VALUE)];
|
||||
} else if (t < 0.7) {
|
||||
const p = (t - 0.4) / 0.3;
|
||||
return [255, Math.round(153 + p * 102), Math.round(51 + p * 51)];
|
||||
return [RGB.MAX_VALUE, Math.round(153 + p * 102), Math.round(51 + p * 51)];
|
||||
} else {
|
||||
const p = (t - 0.7) / 0.3;
|
||||
return [255, 255, Math.round(102 + p * 153)];
|
||||
return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(102 + p * 153)];
|
||||
}
|
||||
}
|
||||
|
||||
export function ditheredColor(value: number): [number, number, number] {
|
||||
const levels = 4;
|
||||
const step = 255 / (levels - 1);
|
||||
const levels = COLOR_MODE_CONSTANTS.DITHER_LEVELS;
|
||||
const step = RGB.MAX_VALUE / (levels - 1);
|
||||
const quantized = Math.round(value / step) * step;
|
||||
const error = value - quantized;
|
||||
const dither = (Math.random() - 0.5) * 32;
|
||||
const final = Math.max(0, Math.min(255, quantized + error + dither));
|
||||
const dither = (Math.random() - 0.5) * COLOR_MODE_CONSTANTS.DITHER_NOISE_AMPLITUDE;
|
||||
const final = Math.max(RGB.MIN_VALUE, Math.min(RGB.MAX_VALUE, quantized + error + dither));
|
||||
return [final, final, final];
|
||||
}
|
||||
|
||||
@ -210,12 +247,12 @@ export function paletteColor(value: number): [number, number, number] {
|
||||
[87, 29, 149],
|
||||
[191, 82, 177],
|
||||
[249, 162, 162],
|
||||
[255, 241, 165],
|
||||
[RGB.MAX_VALUE, 241, 165],
|
||||
[134, 227, 206],
|
||||
[29, 161, 242],
|
||||
[255, 255, 255],
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE],
|
||||
];
|
||||
const index = Math.floor((value / 255.0) * (palette.length - 1));
|
||||
const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1));
|
||||
return palette[index] as [number, number, number];
|
||||
}
|
||||
|
||||
@ -230,12 +267,12 @@ export function vintageColor(value: number): [number, number, number] {
|
||||
[166, 124, 82],
|
||||
[245, 222, 179],
|
||||
];
|
||||
const index = Math.floor((value / 255.0) * (palette.length - 1));
|
||||
const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1));
|
||||
return palette[index] as [number, number, number];
|
||||
}
|
||||
|
||||
export function plasmaColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const freq = 2.4;
|
||||
const phase1 = 0.0;
|
||||
const phase2 = 2.094;
|
||||
@ -246,50 +283,50 @@ export function plasmaColor(value: number): [number, number, number] {
|
||||
const b = Math.sin(freq * t + phase3) * 0.5 + 0.5;
|
||||
|
||||
return [
|
||||
Math.round(r * 255),
|
||||
Math.round(g * 255),
|
||||
Math.round(b * 255)
|
||||
Math.round(r * RGB.MAX_VALUE),
|
||||
Math.round(g * RGB.MAX_VALUE),
|
||||
Math.round(b * RGB.MAX_VALUE)
|
||||
];
|
||||
}
|
||||
|
||||
export function fireColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
if (t < 0.2) {
|
||||
return [Math.round(t * 5 * 255), 0, 0];
|
||||
return [Math.round(t * 5 * RGB.MAX_VALUE), 0, 0];
|
||||
} else if (t < 0.5) {
|
||||
const p = (t - 0.2) / 0.3;
|
||||
return [255, Math.round(p * 165), 0];
|
||||
return [RGB.MAX_VALUE, Math.round(p * 165), 0];
|
||||
} else if (t < 0.8) {
|
||||
const p = (t - 0.5) / 0.3;
|
||||
return [255, Math.round(165 + p * 90), Math.round(p * 100)];
|
||||
return [RGB.MAX_VALUE, Math.round(165 + p * 90), Math.round(p * 100)];
|
||||
} else {
|
||||
const p = (t - 0.8) / 0.2;
|
||||
return [255, 255, Math.round(100 + p * 155)];
|
||||
return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(100 + p * 155)];
|
||||
}
|
||||
}
|
||||
|
||||
export function iceColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
if (t < 0.25) {
|
||||
return [Math.round(t * 2 * 255), Math.round(t * 3 * 255), 255];
|
||||
return [Math.round(t * 2 * RGB.MAX_VALUE), Math.round(t * 3 * RGB.MAX_VALUE), RGB.MAX_VALUE];
|
||||
} else if (t < 0.5) {
|
||||
const p = (t - 0.25) / 0.25;
|
||||
return [Math.round(128 + p * 127), Math.round(192 + p * 63), 255];
|
||||
return [Math.round(128 + p * 127), Math.round(192 + p * 63), RGB.MAX_VALUE];
|
||||
} else if (t < 0.75) {
|
||||
const p = (t - 0.5) / 0.25;
|
||||
return [255, 255, Math.round(255 - p * 100)];
|
||||
return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(RGB.MAX_VALUE - p * 100)];
|
||||
} else {
|
||||
const p = (t - 0.75) / 0.25;
|
||||
return [255, 255, Math.round(155 + p * 100)];
|
||||
return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(155 + p * 100)];
|
||||
}
|
||||
}
|
||||
|
||||
export function infraredColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const intensity = Math.pow(t, 0.6);
|
||||
const heat = Math.sin(t * Math.PI * 1.5) * 0.5 + 0.5;
|
||||
|
||||
const r = Math.round(255 * intensity);
|
||||
const r = Math.round(RGB.MAX_VALUE * intensity);
|
||||
const g = Math.round(128 * heat * intensity);
|
||||
const b = Math.round(64 * (1 - intensity) * heat);
|
||||
|
||||
@ -297,12 +334,12 @@ export function infraredColor(value: number): [number, number, number] {
|
||||
}
|
||||
|
||||
export function xrayColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const inverted = 1.0 - t;
|
||||
const contrast = Math.pow(inverted, 1.8);
|
||||
const glow = Math.sin(t * Math.PI) * 0.3;
|
||||
|
||||
const intensity = Math.round(contrast * 255);
|
||||
const intensity = Math.round(contrast * RGB.MAX_VALUE);
|
||||
const cyan = Math.round((contrast + glow) * 180);
|
||||
const blue = Math.round((contrast + glow * 0.5) * 120);
|
||||
|
||||
@ -310,7 +347,7 @@ export function xrayColor(value: number): [number, number, number] {
|
||||
}
|
||||
|
||||
export function spectrumColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const hue = t * 360;
|
||||
const saturation = 0.7;
|
||||
const lightness = 0.6 + (Math.sin(t * Math.PI * 4) * 0.2);
|
||||
@ -336,14 +373,14 @@ export function spectrumColor(value: number): [number, number, number] {
|
||||
}
|
||||
|
||||
return [
|
||||
Math.round((r + m) * 255),
|
||||
Math.round((g + m) * 255),
|
||||
Math.round((b + m) * 255)
|
||||
Math.round((r + m) * RGB.MAX_VALUE),
|
||||
Math.round((g + m) * RGB.MAX_VALUE),
|
||||
Math.round((b + m) * RGB.MAX_VALUE)
|
||||
];
|
||||
}
|
||||
|
||||
export function acidColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const phase = t * Math.PI * 2;
|
||||
|
||||
const r = Math.sin(phase) * 0.5 + 0.5;
|
||||
@ -354,28 +391,115 @@ export function acidColor(value: number): [number, number, number] {
|
||||
const glow = Math.sin(t * Math.PI * 6) * 0.2 + 0.8;
|
||||
|
||||
return [
|
||||
Math.round(r * intensity * glow * 255),
|
||||
Math.round(g * intensity * glow * 255),
|
||||
Math.round(b * intensity * glow * 255)
|
||||
Math.round(r * intensity * glow * RGB.MAX_VALUE),
|
||||
Math.round(g * intensity * glow * RGB.MAX_VALUE),
|
||||
Math.round(b * intensity * glow * RGB.MAX_VALUE)
|
||||
];
|
||||
}
|
||||
|
||||
export function palette16Color(value: number): [number, number, number] {
|
||||
const palette = [
|
||||
[0, 0, 0], // Black
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE], // White
|
||||
[RGB.MAX_VALUE, 0, 0], // Red
|
||||
[0, RGB.MAX_VALUE, 0], // Green
|
||||
[0, 0, RGB.MAX_VALUE], // Blue
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, 0], // Yellow
|
||||
[RGB.MAX_VALUE, 0, RGB.MAX_VALUE], // Magenta
|
||||
[0, RGB.MAX_VALUE, RGB.MAX_VALUE], // Cyan
|
||||
[RGB.MAX_VALUE, 128, 0], // Orange
|
||||
[128, 0, RGB.MAX_VALUE], // Purple
|
||||
[0, RGB.MAX_VALUE, 128], // Spring Green
|
||||
[RGB.MAX_VALUE, 0, 128], // Pink
|
||||
[128, RGB.MAX_VALUE, 0], // Lime
|
||||
[0, 128, RGB.MAX_VALUE], // Sky Blue
|
||||
[128, 128, 128], // Gray
|
||||
[192, 192, 192] // Light Gray
|
||||
];
|
||||
|
||||
const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1));
|
||||
return palette[index] as [number, number, number];
|
||||
}
|
||||
|
||||
export function quantumColor(value: number): [number, number, number] {
|
||||
const palette = [
|
||||
[0, 0, 0], // Void Black
|
||||
[128, 0, RGB.MAX_VALUE], // Quantum Purple
|
||||
[0, RGB.MAX_VALUE, 128], // Energy Green
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE] // Pure White
|
||||
];
|
||||
|
||||
const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1));
|
||||
return palette[index] as [number, number, number];
|
||||
}
|
||||
|
||||
export function neonStrikeColor(value: number): [number, number, number] {
|
||||
const palette = [
|
||||
[10, 0, 20], // Deep Dark
|
||||
[RGB.MAX_VALUE, 20, 147], // Hot Pink
|
||||
[0, RGB.MAX_VALUE, RGB.MAX_VALUE], // Electric Cyan
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, 0], // Neon Yellow
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE] // Blinding White
|
||||
];
|
||||
|
||||
const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1));
|
||||
return palette[index] as [number, number, number];
|
||||
}
|
||||
|
||||
export function eightBitColor(value: number): [number, number, number] {
|
||||
const levels = 4;
|
||||
const r = Math.floor((value / RGB.MAX_VALUE) * levels) * (RGB.MAX_VALUE / (levels - 1));
|
||||
const g = Math.floor(((value * 2) % 256 / RGB.MAX_VALUE) * levels) * (RGB.MAX_VALUE / (levels - 1));
|
||||
const b = Math.floor(((value * 3) % 256 / RGB.MAX_VALUE) * levels) * (RGB.MAX_VALUE / (levels - 1));
|
||||
|
||||
return [
|
||||
Math.min(RGB.MAX_VALUE, Math.max(0, r)),
|
||||
Math.min(RGB.MAX_VALUE, Math.max(0, g)),
|
||||
Math.min(RGB.MAX_VALUE, Math.max(0, b))
|
||||
];
|
||||
}
|
||||
|
||||
export function silkColor(value: number): [number, number, number] {
|
||||
const t = value / RGB.MAX_VALUE;
|
||||
const smoothT = t * t * (3.0 - 2.0 * t);
|
||||
|
||||
const r = Math.sin(smoothT * Math.PI * 2.0) * 0.5 + 0.5;
|
||||
const g = Math.sin(smoothT * Math.PI * 2.0 + Math.PI * 0.66) * 0.5 + 0.5;
|
||||
const b = Math.sin(smoothT * Math.PI * 2.0 + Math.PI * 1.33) * 0.5 + 0.5;
|
||||
|
||||
const fade = Math.pow(smoothT, 0.3);
|
||||
|
||||
return [
|
||||
Math.round(r * fade * RGB.MAX_VALUE),
|
||||
Math.round(g * fade * RGB.MAX_VALUE),
|
||||
Math.round(b * fade * RGB.MAX_VALUE)
|
||||
];
|
||||
}
|
||||
|
||||
export function binaryColor(value: number): [number, number, number] {
|
||||
const threshold = COLOR_MODE_CONSTANTS.BINARY_THRESHOLD;
|
||||
return value < threshold
|
||||
? [RGB.MIN_VALUE, RGB.MIN_VALUE, RGB.MIN_VALUE] // Pure Black
|
||||
: [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE]; // Pure White
|
||||
}
|
||||
|
||||
|
||||
export function palette32Color(value: number): [number, number, number] {
|
||||
const palette = [
|
||||
[0, 0, 0], // Black
|
||||
[255, 255, 255], // White
|
||||
[255, 0, 0], // Red
|
||||
[0, 255, 0], // Green
|
||||
[0, 0, 255], // Blue
|
||||
[255, 255, 0], // Yellow
|
||||
[255, 0, 255], // Magenta
|
||||
[0, 255, 255], // Cyan
|
||||
[255, 128, 0], // Orange
|
||||
[128, 0, 255], // Purple
|
||||
[0, 255, 128], // Spring Green
|
||||
[255, 0, 128], // Pink
|
||||
[128, 255, 0], // Lime
|
||||
[0, 128, 255], // Sky Blue
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE], // White
|
||||
[RGB.MAX_VALUE, 0, 0], // Red
|
||||
[0, RGB.MAX_VALUE, 0], // Green
|
||||
[0, 0, RGB.MAX_VALUE], // Blue
|
||||
[RGB.MAX_VALUE, RGB.MAX_VALUE, 0], // Yellow
|
||||
[RGB.MAX_VALUE, 0, RGB.MAX_VALUE], // Magenta
|
||||
[0, RGB.MAX_VALUE, RGB.MAX_VALUE], // Cyan
|
||||
[RGB.MAX_VALUE, 128, 0], // Orange
|
||||
[128, 0, RGB.MAX_VALUE], // Purple
|
||||
[0, RGB.MAX_VALUE, 128], // Spring Green
|
||||
[RGB.MAX_VALUE, 0, 128], // Pink
|
||||
[128, RGB.MAX_VALUE, 0], // Lime
|
||||
[0, 128, RGB.MAX_VALUE], // Sky Blue
|
||||
[128, 128, 128], // Gray
|
||||
[192, 192, 192], // Light Gray
|
||||
[64, 64, 64], // Dark Gray
|
||||
@ -385,18 +509,18 @@ export function palette32Color(value: number): [number, number, number] {
|
||||
[128, 0, 64], // Maroon
|
||||
[64, 0, 128], // Indigo
|
||||
[0, 128, 64], // Teal
|
||||
[255, 192, 128], // Peach
|
||||
[128, 255, 192], // Mint
|
||||
[192, 128, 255], // Lavender
|
||||
[255, 128, 192], // Rose
|
||||
[128, 192, 255], // Light Blue
|
||||
[192, 255, 128], // Light Green
|
||||
[RGB.MAX_VALUE, 192, 128], // Peach
|
||||
[128, RGB.MAX_VALUE, 192], // Mint
|
||||
[192, 128, RGB.MAX_VALUE], // Lavender
|
||||
[RGB.MAX_VALUE, 128, 192], // Rose
|
||||
[128, 192, RGB.MAX_VALUE], // Light Blue
|
||||
[192, RGB.MAX_VALUE, 128], // Light Green
|
||||
[64, 32, 16], // Dark Brown
|
||||
[16, 64, 32], // Forest
|
||||
[32, 16, 64] // Deep Purple
|
||||
];
|
||||
|
||||
const index = Math.floor((value / 255.0) * (palette.length - 1));
|
||||
const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1));
|
||||
return palette[index] as [number, number, number];
|
||||
}
|
||||
|
||||
@ -408,13 +532,12 @@ const COLOR_PALETTE_REGISTRY: Record<string, (value: number) => [number, number,
|
||||
green: (value) => [0, value, 0],
|
||||
blue: (value) => [0, 0, value],
|
||||
rgb: (value) => [value, (value * 2) % 256, (value * 3) % 256], // Same as classic for now
|
||||
hsv: (value) => rainbowColor(value), // Use rainbow for HSV
|
||||
forest: forestColor,
|
||||
copper: copperColor,
|
||||
rainbow: rainbowColor,
|
||||
thermal: thermalColor,
|
||||
neon: neonColor,
|
||||
cyberpunk: neonColor, // Use neon for cyberpunk theme
|
||||
cyberpunk: cyberpunkColor,
|
||||
vaporwave: plasmaColor, // Use plasma for vaporwave theme
|
||||
sunset: sunsetColor,
|
||||
ocean: oceanColor,
|
||||
@ -428,6 +551,12 @@ const COLOR_PALETTE_REGISTRY: Record<string, (value: number) => [number, number,
|
||||
xray: xrayColor,
|
||||
spectrum: spectrumColor,
|
||||
acid: acidColor,
|
||||
quantum: quantumColor,
|
||||
neonstrike: neonStrikeColor,
|
||||
eightbit: eightBitColor,
|
||||
silk: silkColor,
|
||||
binary: binaryColor,
|
||||
palette16: palette16Color,
|
||||
palette32: palette32Color,
|
||||
};
|
||||
|
||||
@ -437,7 +566,7 @@ export function calculateColorDirect(
|
||||
hueShift: number = 0
|
||||
): [number, number, number] {
|
||||
const colorFunction = COLOR_PALETTE_REGISTRY[renderMode];
|
||||
const color = colorFunction ? colorFunction(absValue) : [absValue, absValue, absValue];
|
||||
const color = colorFunction ? colorFunction(absValue) : [absValue, absValue, absValue] as [number, number, number];
|
||||
|
||||
return applyHueShift(color, hueShift);
|
||||
}
|
||||
|
||||
@ -19,15 +19,98 @@ export const PERFORMANCE = {
|
||||
// Color Constants
|
||||
export const COLOR_TABLE_SIZE = 256;
|
||||
|
||||
// Color Calculation Constants
|
||||
export const RGB = {
|
||||
MAX_VALUE: 255,
|
||||
MIN_VALUE: 0,
|
||||
} as const;
|
||||
|
||||
// Luminance calculation constants (ITU-R BT.709)
|
||||
export const LUMINANCE_WEIGHTS = {
|
||||
RED: 0.299,
|
||||
GREEN: 0.587,
|
||||
BLUE: 0.114,
|
||||
} as const;
|
||||
|
||||
// HSV Color Constants
|
||||
export const HSV = {
|
||||
HUE_SECTORS: 6,
|
||||
HUE_MAX_DEGREES: 360,
|
||||
SECTOR_OFFSETS: {
|
||||
GREEN: 2,
|
||||
BLUE: 4,
|
||||
},
|
||||
SECTOR_BOUNDARIES: {
|
||||
SIXTH: 1/6,
|
||||
THIRD: 2/6,
|
||||
HALF: 3/6,
|
||||
TWO_THIRDS: 4/6,
|
||||
FIVE_SIXTHS: 5/6,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Color Transition Thresholds
|
||||
export const COLOR_TRANSITIONS = {
|
||||
THERMAL: {
|
||||
LOW: 0.25,
|
||||
MID: 0.5,
|
||||
HIGH: 0.75,
|
||||
},
|
||||
CYBERPUNK: {
|
||||
LOW: 0.2,
|
||||
MID: 0.4,
|
||||
HIGH: 0.7,
|
||||
PULSE_FREQUENCY: 8,
|
||||
PULSE_AMPLITUDE: 0.3,
|
||||
PULSE_OFFSET: 0.7,
|
||||
},
|
||||
SUNSET: {
|
||||
LOW: 0.3,
|
||||
HIGH: 0.6,
|
||||
},
|
||||
FIRE: {
|
||||
LOW: 0.2,
|
||||
MID: 0.5,
|
||||
HIGH: 0.8,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Color Mode Specific Constants
|
||||
export const COLOR_MODE_CONSTANTS = {
|
||||
DITHER_LEVELS: 4,
|
||||
DITHER_NOISE_AMPLITUDE: 32,
|
||||
BINARY_THRESHOLD: 128,
|
||||
RAINBOW_PHASE_MULTIPLIER: 6,
|
||||
PLASMA: {
|
||||
FREQUENCY_X: 2.4,
|
||||
FREQUENCY_Y: 2.094,
|
||||
FREQUENCY_Z: 4.188,
|
||||
PHASE_OFFSET: 0.0,
|
||||
},
|
||||
INFRARED: {
|
||||
INTENSITY_POWER: 0.6,
|
||||
HEAT_FREQUENCY: 1.5,
|
||||
},
|
||||
XRAY: {
|
||||
CONTRAST_POWER: 1.8,
|
||||
},
|
||||
SPECTRUM: {
|
||||
HUE_DEGREES: 360,
|
||||
SATURATION: 0.7,
|
||||
LIGHTNESS_BASE: 0.6,
|
||||
LIGHTNESS_FREQUENCY: 4,
|
||||
LIGHTNESS_AMPLITUDE: 0.2,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Render Mode Constants - Keep in sync with color modes
|
||||
export const RENDER_MODES = [
|
||||
'classic',
|
||||
'grayscale',
|
||||
'grayscale',
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'rgb',
|
||||
'hsv',
|
||||
'rainbow',
|
||||
'thermal',
|
||||
'neon',
|
||||
@ -47,6 +130,12 @@ export const RENDER_MODES = [
|
||||
'xray',
|
||||
'spectrum',
|
||||
'acid',
|
||||
'quantum',
|
||||
'neonstrike',
|
||||
'eightbit',
|
||||
'silk',
|
||||
'binary',
|
||||
'palette16',
|
||||
'palette32',
|
||||
] as const;
|
||||
|
||||
@ -91,10 +180,44 @@ export const VALUE_MODES = [
|
||||
'glitch',
|
||||
'diffusion',
|
||||
'cascade',
|
||||
'echo',
|
||||
'mosh',
|
||||
'fold',
|
||||
] as const;
|
||||
|
||||
export type ValueMode = (typeof VALUE_MODES)[number];
|
||||
|
||||
// Frame Rate and Timing Constants
|
||||
export const TIMING = {
|
||||
DEFAULT_FPS: 30,
|
||||
MIN_FPS: 1,
|
||||
MAX_FPS: 120,
|
||||
MILLISECONDS_PER_SECOND: 1000,
|
||||
DEFAULT_TIME_SPEED: 1.0,
|
||||
DEFAULT_BPM: 120,
|
||||
} as const;
|
||||
|
||||
// Worker and Threading Constants
|
||||
export const WORKER = {
|
||||
FALLBACK_CORE_COUNT: 4,
|
||||
MAX_WORKERS: 32,
|
||||
DEFAULT_PINCH_SCALE: 1,
|
||||
} as const;
|
||||
|
||||
// Mathematical Constants
|
||||
export const MATH = {
|
||||
DEGREES_IN_CIRCLE: 360,
|
||||
RADIANS_TO_DEGREES: 180 / Math.PI,
|
||||
TWO_PI: 2 * Math.PI,
|
||||
} as const;
|
||||
|
||||
// JSON and String Constants
|
||||
export const FORMAT = {
|
||||
JSON_INDENT: 2,
|
||||
ID_RADIX: 36,
|
||||
ID_SUBSTRING_START: 2,
|
||||
} as const;
|
||||
|
||||
// Default Values
|
||||
export const DEFAULTS = {
|
||||
RESOLUTION: 8,
|
||||
|
||||
Reference in New Issue
Block a user