You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, scale = 60.0, strength = 0.5, brightness = 1.0, lightColorRgb = "255,255,220", numLayers = 4, distortion = 20.0, sharpness = 15.0, phaseOffset = 0.0) {
// Ensure numeric parameters are indeed numbers and have sensible fallbacks/ranges
scale = Number(scale) || 60.0;
strength = Math.max(0, Math.min(1, Number(strength) || 0.5));
brightness = Math.max(0, Number(brightness) || 1.0);
numLayers = Math.max(0, Math.floor(Number(numLayers) || 4));
distortion = Number(distortion) || 20.0;
sharpness = Math.max(0.1, Number(sharpness) || 15.0); // Sharpness must be > 0 for Math.pow
phaseOffset = Number(phaseOffset) || 0.0;
const canvas = document.createElement('canvas');
// Use willReadFrequently hint for potential performance optimization by the browser
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Use naturalWidth/Height for HTMLImageElement to ensure image is loaded, fallback to width/height
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
console.error("Image has zero dimensions. Ensure the image is loaded and has valid dimensions.");
// Return an empty (or minimal) canvas if image dimensions are invalid
canvas.width = canvas.width || 1;
canvas.height = canvas.height || 1;
return canvas;
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Could not process image due to getImageData error (possibly CORS issue): ", e);
// If getImageData fails (e.g., CORS), return the canvas with the original image drawn.
return canvas;
}
const data = imageData.data;
const colorParts = String(lightColorRgb).split(',').map(s => s.trim());
const lightR = parseInt(colorParts[0], 10) || 255;
const lightG = parseInt(colorParts[1], 10) || 255;
const lightB = parseInt(colorParts[2], 10) || 255;
const layerParams = [];
let totalAmplitude = 0;
for (let i = 0; i < numLayers; i++) {
const amp = Math.pow(0.65, i); // Amplitude decreases for each layer
layerParams.push({
// Angle offset varies per layer to introduce directional variety
angleOffset: (Math.random() - 0.5) * 0.5 * Math.PI + (i * Math.PI * 0.3 / numLayers),
phaseX: (Math.random() * Math.PI * 2) + phaseOffset,
phaseY: (Math.random() * Math.PI * 2) + phaseOffset,
amplitude: amp,
frequencyFactor: Math.pow(1.75, i) // Frequency increases for each layer (smaller details)
});
totalAmplitude += amp;
}
// This function calculates the caustic intensity (0-1) for a given pixel
function getCausticValueAtPixel(x, y) {
let Px = x; // Use copies for coordinate perturbation
let Py = y;
let value = 0; // Accumulated wave value
if (numLayers === 0) return 0; // No effect if no layers
for (let i = 0; i < numLayers; i++) {
const lp = layerParams[i];
const currentScale = Math.max(0.1, scale / lp.frequencyFactor); // Prevent division by zero or tiny scale
// Calculate two orthogonal wave components for this layer
const dirX = Math.cos(lp.angleOffset);
const dirY = Math.sin(lp.angleOffset);
const waveVal1 = Math.sin((Px * dirX + Py * dirY) / currentScale + lp.phaseX);
const waveVal2 = Math.cos((Px * -dirY + Py * dirX) / currentScale + lp.phaseY); // Orthogonal wave
// Combine waves; multiplication creates more complex interference
const layerValue = waveVal1 * waveVal2; // Result is in [-1, 1]
value += layerValue * lp.amplitude;
// Perturb coordinates for the next layer, scaled by distortion and current layer's amplitude
if (i < numLayers - 1) {
// Use components of the current layer's waves to drive distortion
const angleForDistortX = (Py / currentScale + lp.phaseY) * (lp.frequencyFactor / 2 + 0.5);
const angleForDistortY = (Px / currentScale + lp.phaseX) * (lp.frequencyFactor / 2 + 0.5);
Px += distortion * Math.cos(angleForDistortX) * lp.amplitude;
Py += distortion * Math.sin(angleForDistortY) * lp.amplitude;
}
}
// Normalize accumulated value (expected range approx -totalAmplitude to +totalAmplitude)
if (totalAmplitude > 0.0001) {
value = value / totalAmplitude; // Normalize to approx [-1, 1]
} else {
value = 0; // Handle cases where totalAmplitude is ~0
}
// Shape the normalized value to create caustic lines
// `1.0 - Math.abs(value)` maps values near 0 (wave cancellations) to high intensity (1.0)
// and values near -1 or 1 (wave peaks) to low intensity (0.0).
// This tends to create networks of bright lines.
value = 1.0 - Math.abs(value);
value = Math.pow(value, sharpness); // Higher sharpness makes lines thinner and brighter
return Math.max(0, Math.min(1, value)); // Clamp final positive_value to [0, 1]
}
// Apply the caustics effect pixel by pixel
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const idx = (y * canvas.width + x) * 4;
const origR = data[idx];
const origG = data[idx + 1];
const origB = data[idx + 2];
const causticPatternValue = getCausticValueAtPixel(x, y);
// Calculate the light intensity to add from the caustics
const effectiveLightR = lightR * brightness * causticPatternValue;
const effectiveLightG = lightG * brightness * causticPatternValue;
const effectiveLightB = lightB * brightness * causticPatternValue;
// Additive blending: original_color + caustic_light * strength
// Strength controls how much of the caustic effect is applied.
data[idx] = Math.min(255, Math.max(0, origR + effectiveLightR * strength));
data[idx + 1] = Math.min(255, Math.max(0, origG + effectiveLightG * strength));
data[idx + 2] = Math.min(255, Math.max(0, origB + effectiveLightB * strength));
// Alpha (data[idx + 3]) remains unchanged
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes