You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, poreDensity = 0.5, poreSize = 1.0, poreDarkness = 0.3, skinRoughness = 0.15, effectStrength = 0.5) {
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
// Helper to create a new canvas
const createCanvas = (w, h) => {
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
return canvas;
};
// --- Step 1: Create a Skin Mask ---
// This mask will isolate the effect to skin-toned areas.
const maskCanvas = createCanvas(width, height);
const maskCtx = maskCanvas.getContext('2d', {
willReadFrequently: true
});
maskCtx.drawImage(originalImg, 0, 0, width, height);
const imageData = maskCtx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// A common method for skin detection is to convert RGB to a color space
// like YCbCr and check for values within a specific range.
const y = 0.299 * r + 0.587 * g + 0.114 * b;
const cb = 128 - 0.168736 * r - 0.331264 * g + 0.5 * b;
const cr = 128 + 0.5 * r - 0.418688 * g - 0.081312 * b;
// Thresholds for skin tones in YCbCr space.
const isSkin = (y > 80 && cb > 85 && cb < 135 && cr > 135 && cr < 180);
// Set pixel to white for skin, black for non-skin.
const value = isSkin ? 255 : 0;
data[i] = data[i + 1] = data[i + 2] = value;
}
maskCtx.putImageData(imageData, 0, 0);
// Feather the mask edges for a smooth transition.
// A blur radius relative to image size scales well.
const blurRadius = Math.max(1, Math.min(width, height) * 0.01);
maskCtx.filter = `blur(${blurRadius}px)`;
maskCtx.drawImage(maskCanvas, 0, 0, width, height); // Re-draw to apply filter
maskCtx.filter = 'none'; // Reset filter
// --- Step 2: Create a Texture Layer ---
const textureCanvas = createCanvas(width, height);
const textureCtx = textureCanvas.getContext('2d');
// Fill with neutral gray (#808080), which is neutral for 'overlay' blend mode.
textureCtx.fillStyle = '#808080';
textureCtx.fillRect(0, 0, width, height);
// Add fine-grained noise for general skin roughness.
if (skinRoughness > 0) {
const roughnessData = textureCtx.getImageData(0, 0, width, height);
const roughData = roughnessData.data;
for (let i = 0; i < roughData.length; i += 4) {
// Add or subtract a small random value from the gray.
const noise = (Math.random() - 0.5) * 50 * skinRoughness;
roughData[i] += noise;
roughData[i + 1] += noise;
roughData[i + 2] += noise;
}
textureCtx.putImageData(roughnessData, 0, 0);
}
// Add pores.
if (poreDensity > 0) {
const numPores = Math.floor(width * height * 0.005 * poreDensity);
for (let i = 0; i < numPores; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
// Randomize pore characteristics for a more natural look.
const currentPoreSize = poreSize * (0.5 + Math.random() * 0.75);
const currentPoreAlpha = poreDarkness * (0.7 + Math.random() * 0.6);
// A radial gradient simulates a pore's depth better than a flat circle.
const gradient = textureCtx.createRadialGradient(x, y, 0, x, y, currentPoreSize);
gradient.addColorStop(0, `rgba(0, 0, 0, ${currentPoreAlpha})`); // Dark center
gradient.addColorStop(0.6, `rgba(0, 0, 0, ${currentPoreAlpha * 0.3})`);
gradient.addColorStop(1, 'rgba(128, 128, 128, 0)'); // Fade to transparent neutral gray
textureCtx.fillStyle = gradient;
// Use fillRect as it's often faster than drawing arcs.
textureCtx.fillRect(x - currentPoreSize, y - currentPoreSize, currentPoreSize * 2, currentPoreSize * 2);
}
}
// --- Step 3: Mask the Texture Layer ---
// This uses the skin mask to "cut out" the texture, leaving it only in skin areas.
textureCtx.globalCompositeOperation = 'destination-in';
textureCtx.drawImage(maskCanvas, 0, 0);
// --- Step 4: Composite the Final Image ---
const resultCanvas = createCanvas(width, height);
const resultCtx = resultCanvas.getContext('2d');
// Start with the original image.
resultCtx.drawImage(originalImg, 0, 0, width, height);
// Set blend mode and opacity, then overlay the masked texture.
resultCtx.globalAlpha = Math.max(0, Math.min(1, effectStrength));
resultCtx.globalCompositeOperation = 'overlay';
resultCtx.drawImage(textureCanvas, 0, 0);
// Reset context properties for safety.
resultCtx.globalAlpha = 1.0;
resultCtx.globalCompositeOperation = 'source-over';
return resultCanvas;
}
Apply Changes