You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, puddleCount = "12", strength = "5", wavelength = "20", perspective = "0.7", brightness = "40", wetDarkness = "0.15", seed = "42") {
if (!originalImg || !originalImg.width || !originalImg.height) return originalImg;
let w = originalImg.width;
let h = originalImg.height;
let canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
let ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0);
let imgData = ctx.getImageData(0, 0, w, h);
let data = imgData.data;
let outData = ctx.createImageData(w, h);
let out = outData.data;
// Parse parameters
let p_count = isNaN(parseInt(puddleCount, 10)) ? 12 : Math.max(1, parseInt(puddleCount, 10));
let p_strength = isNaN(parseFloat(strength)) ? 5 : parseFloat(strength);
let p_wavelength = isNaN(parseFloat(wavelength)) ? 20 : Math.max(1, parseFloat(wavelength));
let p_perspective = isNaN(parseFloat(perspective)) ? 0.7 : Math.max(0.1, parseFloat(perspective));
let p_brightness = isNaN(parseFloat(brightness)) ? 40 : parseFloat(brightness);
let p_wetDarkness = isNaN(parseFloat(wetDarkness)) ? 0.15 : parseFloat(wetDarkness);
let s = isNaN(parseInt(seed, 10)) ? 42 : parseInt(seed, 10);
// Simple PRNG to ensure distinct but deterministic puddles
function PRNG(seed) {
this.seed = seed % 2147483647;
if (this.seed <= 0) this.seed += 2147483646;
}
PRNG.prototype.next = function() {
this.seed = this.seed * 16807 % 2147483647;
return this.seed / 2147483647;
}
let rng = new PRNG(s);
let puddles = [];
let dim = Math.max(w, h);
// Generate random puddle centers and properties
for (let i = 0; i < p_count; i++) {
puddles.push({
x: rng.next() * w,
y: rng.next() * h,
radius: (rng.next() * 0.15 + 0.1) * dim,
phase: rng.next() * Math.PI * 2
});
}
// Auxiliary arrays to accumulate displacement and lighting
let dx_arr = new Float32Array(w * h);
let dy_arr = new Float32Array(w * h);
let light_arr = new Float32Array(w * h);
let env_arr = new Float32Array(w * h);
// Accumulate ripple effects
for (let i = 0; i < p_count; i++) {
let p = puddles[i];
let maxY_offset = p.radius * p_perspective;
let minX = Math.max(0, Math.floor(p.x - p.radius));
let maxX = Math.min(w, Math.ceil(p.x + p.radius));
let minY = Math.max(0, Math.floor(p.y - maxY_offset));
let maxY = Math.min(h, Math.ceil(p.y + maxY_offset));
let radSq = p.radius * p.radius;
for (let y = minY; y < maxY; y++) {
let offset = y * w;
for (let x = minX; x < maxX; x++) {
let dx = x - p.x;
let dy = y - p.y;
let dy_eff = dy / p_perspective;
let distSq = dx * dx + dy_eff * dy_eff;
if (distSq < radSq && distSq > 0.0001) {
let dist = Math.sqrt(distSq);
let normalizedDist = dist / p.radius;
// Envelope function for smooth ripple fade-out (Hann window)
let envelope = (Math.cos(normalizedDist * Math.PI) + 1) / 2;
let phase = dist / p_wavelength + p.phase;
let sinPhase = Math.sin(phase);
let cosPhase = Math.cos(phase);
let amplitude = p_strength * envelope * sinPhase;
let idx = offset + x;
dx_arr[idx] += (dx / dist) * amplitude;
dy_arr[idx] += (dy_eff / dist) * amplitude * p_perspective;
env_arr[idx] += envelope;
// Ripple slope for dynamic shading
let d_envelope = (-Math.PI / (2 * p.radius)) * Math.sin(normalizedDist * Math.PI);
let slope = p_strength * (d_envelope * sinPhase + envelope * cosPhase / p_wavelength);
// Surface Normal
let nx = -slope * (dx / dist);
let ny = -slope * (dy_eff / dist) * p_perspective;
// Lighting vector calculation (light from top-left)
light_arr[idx] += (nx * -0.707 + ny * -0.707);
}
}
}
}
// Apply displacement and lighting to output pixels using bilinear interpolation
for (let y = 0; y < h; y++) {
let offset = y * w;
for (let x = 0; x < w; x++) {
let idx = offset + x;
let final_dx = dx_arr[idx];
let final_dy = dy_arr[idx];
let shading = light_arr[idx] * p_brightness;
let wetness = 1.0 - p_wetDarkness * Math.min(1.0, env_arr[idx]);
let sx = x - final_dx;
let sy = y - final_dy;
let x0 = Math.floor(sx);
let y0 = Math.floor(sy);
let x1 = x0 + 1;
let y1 = y0 + 1;
let fx = sx - x0;
let fy = sy - y0;
// Constrain source coordinates to image boundaries
x0 = Math.max(0, Math.min(w - 1, x0));
x1 = Math.max(0, Math.min(w - 1, x1));
y0 = Math.max(0, Math.min(h - 1, y0));
y1 = Math.max(0, Math.min(h - 1, y1));
let idx00 = (y0 * w + x0) * 4;
let idx10 = (y0 * w + x1) * 4;
let idx01 = (y1 * w + x0) * 4;
let idx11 = (y1 * w + x1) * 4;
let outIdx = idx * 4;
for (let c = 0; c < 3; c++) {
let v00 = data[idx00 + c];
let v10 = data[idx10 + c];
let v01 = data[idx01 + c];
let v11 = data[idx11 + c];
let val0 = v00 * (1 - fx) + v10 * fx;
let val1 = v01 * (1 - fx) + v11 * fx;
let val = val0 * (1 - fy) + val1 * fy;
// Darken area for wetness impression, then apply highlight shading reflection
val *= wetness;
val += shading;
out[outIdx + c] = Math.max(0, Math.min(255, val));
}
// Alpha Channel
let a00 = data[idx00 + 3];
let a10 = data[idx10 + 3];
let a01 = data[idx01 + 3];
let a11 = data[idx11 + 3];
let aval0 = a00 * (1 - fx) + a10 * fx;
let aval1 = a01 * (1 - fx) + a11 * fx;
let aval = aval0 * (1 - fy) + aval1 * fy;
out[outIdx + 3] = Math.max(0, Math.min(255, aval));
}
}
ctx.putImageData(outData, 0, 0);
return canvas;
}
Apply Changes