You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, dropCount = "200", minRadius = "15", maxRadius = "50", arrangement = "random") {
// Parse parameters
let count = parseInt(dropCount, 10) || 200;
let minR = parseInt(minRadius, 10) || 15;
let maxR = parseInt(maxRadius, 10) || 50;
if (minR > maxR) {
let temp = minR;
minR = maxR;
maxR = temp;
}
const width = originalImg.width;
const height = originalImg.height;
// Create canvas and draw the original image
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0, width, height);
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
// Fixed lighting vectors for the 3D crystal/water sphere effect
// Light coming from top-left
const Lx = -0.5773, Ly = -0.5773, Lz = 0.5773;
// Half vector between light and view (0,0,1) for sharp specular specular highlights
const Hx = -0.325, Hy = -0.325, Hz = 0.888;
// Vector meant for bottom-right internal reflection (caustics)
const cLx = 0.707, cLy = 0.707, cLz = 0.0;
// Generate drop positions
const drops = [];
if (typeof arrangement === 'string' && arrangement.toLowerCase() === 'grid') {
const stepX = maxR * 1.5;
const stepY = maxR * 1.5;
for (let y = stepY / 2; y < height + stepY; y += stepY) {
let offset = (Math.floor(y / stepY) % 2) * (stepX / 2); // Hexagonal layout offset
for (let x = stepX / 2 - offset; x < width + stepX; x += stepX) {
let R = minR + Math.random() * (maxR - minR);
drops.push({ cx: Math.floor(x), cy: Math.floor(y), r: Math.floor(R) });
}
}
} else {
for (let i = 0; i < count; i++) {
let cx = Math.floor(Math.random() * width);
let cy = Math.floor(Math.random() * height);
let R = minR + Math.random() * (maxR - minR);
drops.push({ cx, cy, r: Math.floor(R) });
}
}
// Apply drops sequentially over the image
for (let i = 0; i < drops.length; i++) {
const cx = drops[i].cx;
const cy = drops[i].cy;
const R = drops[i].r;
const R2 = R * R;
const shadowCx = cx + R * 0.3;
const shadowCy = cy + R * 0.3;
// Bounding box with padding to cover the cast shadow correctly
const pad = Math.ceil(R * 0.4);
const minX = Math.max(0, Math.floor(cx - R - pad));
const maxX = Math.min(width - 1, Math.ceil(cx + R + pad));
const minY = Math.max(0, Math.floor(cy - R - pad));
const maxY = Math.min(height - 1, Math.ceil(cy + R + pad));
const boxW = maxX - minX + 1;
const boxH = maxY - minY + 1;
if (boxW <= 0 || boxH <= 0) continue;
// Snapshot an isolated bounding box of the current pixels (avoid tearing reflections issue)
const srcBox = new Uint8ClampedArray(boxW * boxH * 4);
for (let y = minY; y <= maxY; y++) {
let srcYOffset = y * width;
let dstYOffset = (y - minY) * boxW;
for (let x = minX; x <= maxX; x++) {
const srcIdx = (srcYOffset + x) * 4;
const dstIdx = (dstYOffset + x - minX) * 4;
srcBox[dstIdx] = data[srcIdx];
srcBox[dstIdx + 1] = data[srcIdx + 1];
srcBox[dstIdx + 2] = data[srcIdx + 2];
srcBox[dstIdx + 3] = data[srcIdx + 3];
}
}
// Apply distortions and shadows
for (let y = minY; y <= maxY; y++) {
let yOffset = y * width;
let dy = y - cy;
let dy2 = dy * dy;
let sDy = y - shadowCy;
let sDy2 = sDy * sDy;
for (let x = minX; x <= maxX; x++) {
let dx = x - cx;
let d2 = dx * dx + dy2;
if (d2 <= R2) {
// Inside the drop: Generate spherize + glass effect
const d = Math.sqrt(d2);
const nd = d / R;
const z = Math.sqrt(1 - nd * nd);
// Drop point 3D Normal
const nx = dx / R;
const ny = dy / R;
const nz = z;
// Refraction Mapping (Magnifies the center nicely)
const fraction = nd;
const src_x = Math.floor(cx + dx * fraction);
const src_y = Math.floor(cy + dy * fraction);
const clampX = Math.max(minX, Math.min(maxX, src_x));
const clampY = Math.max(minY, Math.min(maxY, src_y));
const srcBoxIdx = ((clampY - minY) * boxW + (clampX - minX)) * 4;
const r = srcBox[srcBoxIdx];
const g = srcBox[srcBoxIdx + 1];
const b = srcBox[srcBoxIdx + 2];
const a = srcBox[srcBoxIdx + 3];
// Standard Diffuse Base
let dot = nx * Lx + ny * Ly + nz * Lz;
let lightIntensity = 0.6 + Math.max(0, dot) * 0.5;
// Shiny Specular Highlight
let specular = 0;
let specDot = nx * Hx + ny * Hy + nz * Hz;
if (specDot > 0) {
specular = Math.pow(specDot, 40) * 180;
}
// Caustic Light Gathering (bottom-right glow)
let causticDot = nx * cLx + ny * cLy + nz * cLz;
let caustic = Math.pow(Math.max(0, causticDot), 4) * 80;
// Render a thick, refractive dark surrounding edge
let edgeDarkening = 1.0;
if (nd > 0.8) {
edgeDarkening = 1.0 - ((nd - 0.8) / 0.2) * 0.4;
}
const outIdx = (yOffset + x) * 4;
data[outIdx] = Math.min(255, r * lightIntensity * edgeDarkening + specular + caustic);
data[outIdx + 1] = Math.min(255, g * lightIntensity * edgeDarkening + specular + caustic);
data[outIdx + 2] = Math.min(255, b * lightIntensity * edgeDarkening + specular + caustic);
// Force opacity as crystals have physical volume
data[outIdx + 3] = 255;
} else {
// Outside the drop: Generate a soft drop shadow slightly offset downwards
let sDx = x - shadowCx;
let sDist2 = sDx * sDx + sDy2;
if (sDist2 <= R2) {
let sd = Math.sqrt(sDist2);
let shadowIntensity = 0.6 + 0.4 * (sd / R); // Gradient fade on shadow borders
const outIdx = (yOffset + x) * 4;
data[outIdx] = data[outIdx] * shadowIntensity;
data[outIdx + 1] = data[outIdx + 1] * shadowIntensity;
data[outIdx + 2] = data[outIdx + 2] * shadowIntensity;
}
}
}
}
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
Apply Changes