You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, foamIntensity = 0.9, noiseStrength = 50, noiseScale = 2, foamBaseColorStr = "255,250,230") {
// foamIntensity (0-1): How much the foam effect overtakes the original. 0 for original, 1 for full foam.
// noiseStrength (0-255): Max deviation for random noise for grain/bubble texture. Noise will be in [-noiseStrength/2, +noiseStrength/2].
// noiseScale (integer >= 1): Size of noise blocks. 1 for pixel-level noise, >1 for clumpier noise.
// foamBaseColorStr ("r,g,b"): The dominant color of the foam (e.g., "255,250,230" for creamy off-white).
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
// Handle zero-size image gracefully, return empty canvas
return canvas;
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
let parsedFoamBaseColor = foamBaseColorStr.split(',').map(s => parseInt(s.trim(), 10));
if (parsedFoamBaseColor.length !== 3 || parsedFoamBaseColor.some(isNaN)) {
console.warn("Invalid foamBaseColorStr, defaulting to 255,250,230. Received: " + foamBaseColorStr);
parsedFoamBaseColor = [255, 250, 230];
}
// Clamp base color components to valid 0-255 range
const baseR = Math.max(0, Math.min(255, parsedFoamBaseColor[0]));
const baseG = Math.max(0, Math.min(255, parsedFoamBaseColor[1]));
const baseB = Math.max(0, Math.min(255, parsedFoamBaseColor[2]));
const width = canvas.width;
const height = canvas.height;
const effectiveNoiseScale = Math.max(1, Math.floor(noiseScale));
const clampedNoiseStrength = Math.max(0, noiseStrength);
// Generate random noise map for blocky noise if noiseScale > 1 and noise is active
const useBlockNoise = effectiveNoiseScale > 1 && clampedNoiseStrength > 0;
let noiseMap;
let noiseMapWidth = 0; // Initialize to avoid potential issues if not set
if (useBlockNoise) {
noiseMapWidth = Math.ceil(width / effectiveNoiseScale);
const noiseMapHeight = Math.ceil(height / effectiveNoiseScale);
noiseMap = new Float32Array(noiseMapWidth * noiseMapHeight);
for (let i = 0; i < noiseMap.length; i++) {
noiseMap[i] = (Math.random() - 0.5); // Noise from -0.5 to 0.5
}
}
// K_lum_mod: Factor for how much original luminance affects foam brightness variation.
// Higher values mean original image structure is more visible in foam brightness.
const K_lum_mod = 0.4;
const clampedFoamIntensity = Math.max(0, Math.min(1, foamIntensity));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const r_orig = data[i];
const g_orig = data[i + 1];
const b_orig = data[i + 2];
// 1. Calculate original pixel's luminance
const luminance_orig = 0.299 * r_orig + 0.587 * g_orig + 0.114 * b_orig;
// 2. Determine the foam color for this pixel, modulated by original luminance
// Calculate luminance of the foamBaseColor itself
const baseLuminance = 0.299 * baseR + 0.587 * baseG + 0.114 * baseB;
// Modulate foam brightness based on original pixel's luminance
// This means darker original pixels result in slightly darker foam, brighter ones in brighter foam,
// all relative to the foamBaseColor's brightness.
let currentFoamTargetLuminance = baseLuminance + (luminance_orig - 128) * K_lum_mod;
currentFoamTargetLuminance = Math.max(0, Math.min(255, currentFoamTargetLuminance));
let foamR_calc, foamG_calc, foamB_calc;
if (baseLuminance > 0.001) { // If base color is not black, scale its components
const scaleFactor = currentFoamTargetLuminance / baseLuminance;
foamR_calc = baseR * scaleFactor;
foamG_calc = baseG * scaleFactor;
foamB_calc = baseB * scaleFactor;
} else { // If base color is black (or very dark), foam color becomes grayscale based on target luminance
foamR_calc = currentFoamTargetLuminance;
foamG_calc = currentFoamTargetLuminance;
foamB_calc = currentFoamTargetLuminance;
}
// 3. Add noise/grain for texture
let noiseVal = 0;
if (clampedNoiseStrength > 0) {
if (useBlockNoise) {
const noiseMapX = Math.floor(x / effectiveNoiseScale);
const noiseMapY = Math.floor(y / effectiveNoiseScale);
// Ensure noiseMapX and noiseMapY are within bounds for safety, though Math.floor should handle it
const safeNoiseMapX = Math.max(0, Math.min(noiseMapX, noiseMapWidth - 1));
// noiseMapHeight is implicitly defined by noiseMap.length / noiseMapWidth
noiseVal = noiseMap[noiseMapY * noiseMapWidth + safeNoiseMapX] * clampedNoiseStrength;
} else { // Pixel-level noise (if noiseScale is 1 or effectiveNoiseScale becomes 1)
noiseVal = (Math.random() - 0.5) * clampedNoiseStrength;
}
}
let foamR_final = foamR_calc + noiseVal;
let foamG_final = foamG_calc + noiseVal;
let foamB_final = foamB_calc + noiseVal;
// Clamp foam colors after adding noise
foamR_final = Math.max(0, Math.min(255, foamR_final));
foamG_final = Math.max(0, Math.min(255, foamG_final));
foamB_final = Math.max(0, Math.min(255, foamB_final));
// 4. Blend with original based on foamIntensity
data[i] = Math.round(r_orig * (1 - clampedFoamIntensity) + foamR_final * clampedFoamIntensity);
data[i + 1] = Math.round(g_orig * (1 - clampedFoamIntensity) + foamG_final * clampedFoamIntensity);
data[i + 2] = Math.round(b_orig * (1 - clampedFoamIntensity) + foamB_final * clampedFoamIntensity);
// Alpha (data[i+3]) remains unchanged
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes