You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, density = "100") {
const canvas = document.createElement('canvas');
const w = originalImg.width;
const h = originalImg.height;
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
const maxDim = Math.max(w, h);
// Background - wet glass effect
ctx.filter = `blur(${maxDim * 0.015}px) brightness(0.8) contrast(1.1)`;
ctx.drawImage(originalImg, 0, 0);
ctx.filter = 'none';
// Shades the drop to look 3D and lit
function addDropShading(cx, cy, r) {
if (r < maxDim * 0.001) return; // Skip detail on tiny droplets
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
// Inner shadow for volume
const grad = ctx.createRadialGradient(cx - r*0.3, cy - r*0.3, r*0.2, cx, cy, r);
grad.addColorStop(0, 'rgba(0,0,0,0.0)');
grad.addColorStop(0.7, 'rgba(0,0,0,0.1)');
grad.addColorStop(1, 'rgba(0,0,0,0.6)');
ctx.fillStyle = grad;
ctx.fill();
// Top-left Specular highlight
ctx.beginPath();
ctx.arc(cx - r*0.35, cy - r*0.35, r*0.4, 0, Math.PI * 2);
const gradLight = ctx.createRadialGradient(cx - r*0.35, cy - r*0.35, 0, cx - r*0.35, cy - r*0.35, r*0.4);
gradLight.addColorStop(0, 'rgba(255,255,255,0.7)');
gradLight.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = gradLight;
ctx.fill();
// Bottom inner rim light reflection
ctx.beginPath();
ctx.arc(cx, cy, r*0.85, 0.15*Math.PI, 0.85*Math.PI);
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = Math.max(1, r*0.1);
ctx.lineCap = 'round';
ctx.stroke();
ctx.restore();
}
// Renders the wide-angle, upside-down "lens" of a water drop
function drawSingleDrop(cx, cy, r) {
if (r < 1) return;
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
// Water droplet refraction scale and flip mapping
ctx.translate(cx, cy);
const dropScale = 0.5;
ctx.scale(-dropScale, -dropScale);
ctx.translate(-cx, -cy);
// Optimization: check intersection so we only draw necessary seamless reflection tiles
const paintMinX = cx - r / dropScale;
const paintMaxX = cx + r / dropScale;
const paintMinY = cy - r / dropScale;
const paintMaxY = cy + r / dropScale;
// Loop to safely draw original image out of physical bounds if the lens bends far (mirrored tiling)
for (let i = -1; i <= 1; i++) {
const tileMinX = i * w;
const tileMaxX = (i + 1) * w;
if (paintMaxX < tileMinX || paintMinX > tileMaxX) continue;
for (let j = -1; j <= 1; j++) {
const tileMinY = j * h;
const tileMaxY = (j + 1) * h;
if (paintMaxY < tileMinY || paintMinY > tileMaxY) continue;
ctx.save();
ctx.translate(i * w, j * h);
const flipH = (i !== 0);
const flipV = (j !== 0);
if (flipH) {
ctx.translate(w, 0);
ctx.scale(-1, 1);
}
if (flipV) {
ctx.translate(0, h);
ctx.scale(1, -1);
}
ctx.drawImage(originalImg, 0, 0, w, h);
ctx.restore();
}
}
ctx.restore();
addDropShading(cx, cy, r);
}
// Draws a falling droplet and its water trail
function drawDrop(cx, cy, r, baseDropSize) {
if (r > baseDropSize * 2 && Math.random() > 0.4) {
let ty = cy;
let tLen = r * (3 + Math.random() * 4);
let tr = r * 0.5;
let tx = cx;
let waver = (Math.random() - 0.5) * r * 0.5;
// Build the trailing water squiggles
while (cy - ty < tLen && tr > 0.5) {
ty -= tr * 1.5;
tr *= 0.85;
tx += waver;
waver = (Math.random() - 0.5) * r * 0.3;
drawSingleDrop(tx, ty, tr);
}
}
// Base main drop
drawSingleDrop(cx, cy, r);
}
const intensity = Number(density) || 100;
const effectiveArea = Math.min(w * h, 2000000);
const baseCount = Math.floor(effectiveArea / (100000 / intensity));
const baseDropSize = Math.max(2, maxDim * 0.002);
const drops = [];
// Distribute small drops
for(let i=0; i<baseCount; i++) {
drops.push({
x: Math.random() * w,
y: Math.random() * h,
r: baseDropSize + Math.random() * baseDropSize,
type: 'small'
});
}
// Distribute medium drops
for(let i=0; i<baseCount * 0.2; i++) {
drops.push({
x: Math.random() * w,
y: Math.random() * h,
r: baseDropSize * 2 + Math.random() * baseDropSize * 2,
type: 'med'
});
}
// Distribute large drops
for(let i=0; i<baseCount * 0.05; i++) {
drops.push({
x: Math.random() * w,
y: Math.random() * h,
r: baseDropSize * 4 + Math.random() * baseDropSize * 4,
type: 'large'
});
}
// Render smaller drops first so larger cascading drops naturally overlap their view
drops.sort((a,b) => a.r - b.r);
for (const d of drops) {
if (d.type === 'small') {
drawSingleDrop(d.x, d.y, d.r);
} else {
drawDrop(d.x, d.y, d.r, baseDropSize);
}
}
return canvas;
}
Apply Changes