You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, upscaleFactor = 2, sharpening = 0.5, wrinkles = 0.15, pores = 12) {
/**
* Helper Class: Perlin Noise Generator
* This class creates procedural noise used for generating the wrinkle texture.
* It's embedded here to make the function self-contained.
* Original source: https://gist.github.com/banksean/304522 (Public Domain)
* Modernized to ES6 class syntax.
*/
class PerlinNoise {
constructor() {
this.p = new Uint8Array(512);
this.PERLIN_YWRAPB = 4;
this.PERLIN_YWRAP = 1 << this.PERLIN_YWRAPB;
this.PERLIN_ZWRAPB = 8;
this.PERLIN_ZWRAP = 1 << this.PERLIN_ZWRAPB;
this.PERLIN_SIZE = 4095;
this.perlin_octaves = 4;
this.perlin_amp_falloff = 0.5;
}
noiseDetail(lod, falloff) {
if (lod > 0) this.perlin_octaves = lod;
if (falloff > 0) this.perlin_amp_falloff = falloff;
}
seed(seed) {
const lcg = (() => {
let m = 4294967296, a = 1664525, c = 1013904223, s = seed;
return () => (s = (a * s + c) % m) / m;
})();
for (let i = 0; i < 256; i++) {
this.p[i] = Math.floor(lcg() * 256);
}
for (let i = 0; i < 256; i++) {
this.p[i + 256] = this.p[i];
}
}
fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
lerp(t, a, b) { return a + t * (b - a); }
grad(hash, x, y, z) {
let h = hash & 15;
let u = h < 8 ? x : y;
let v = h < 4 ? y : h === 12 || h === 14 ? x : z;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
noise(x, y = 0, z = 0) {
if (this.p[0] == null) this.seed(Math.random());
let xi = Math.floor(x) & this.PERLIN_SIZE;
let yi = Math.floor(y) & this.PERLIN_SIZE;
let zi = Math.floor(z) & this.PERLIN_SIZE;
let xf = x - Math.floor(x);
let yf = y - Math.floor(y);
let zf = z - Math.floor(z);
let rxf, ryf;
let r = 0;
let ampl = 0.5;
let n1, n2, n3;
for (let o = 0; o < this.perlin_octaves; o++) {
let of = xi + (yi << this.PERLIN_YWRAPB) + (zi << this.PERLIN_ZWRAPB);
rxf = this.fade(xf);
ryf = this.fade(yf);
n1 = this.grad(this.p[of & this.PERLIN_SIZE], xf, yf, zf);
n1 += rxf * (this.grad(this.p[(of + 1) & this.PERLIN_SIZE], xf - 1, yf, zf) - n1);
n2 = this.grad(this.p[(of + this.PERLIN_YWRAP) & this.PERLIN_SIZE], xf, yf - 1, zf);
n2 += rxf * (this.grad(this.p[(of + this.PERLIN_YWRAP + 1) & this.PERLIN_SIZE], xf - 1, yf - 1, zf) - n2);
n1 += ryf * (n2 - n1);
of += this.PERLIN_ZWRAP;
n2 = this.grad(this.p[of & this.PERLIN_SIZE], xf, yf, zf - 1);
n2 += rxf * (this.grad(this.p[(of + 1) & this.PERLIN_SIZE], xf - 1, yf, zf - 1) - n2);
n3 = this.grad(this.p[(of + this.PERLIN_YWRAP) & this.PERLIN_SIZE], xf, yf - 1, zf - 1);
n3 += rxf * (this.grad(this.p[(of + this.PERLIN_YWRAP + 1) & this.PERLIN_SIZE], xf - 1, yf - 1, zf - 1) - n3);
n2 += ryf * (n3 - n2);
n1 += this.fade(zf) * (n2 - n1);
r += n1 * ampl;
ampl *= this.perlin_amp_falloff;
xi <<= 1; xf *= 2;
yi <<= 1; yf *= 2;
zi <<= 1; zf *= 2;
if (xf >= 1.0) {xi++; xf--;}
if (yf >= 1.0) {yi++; yf--;}
if (zf >= 1.0) {zi++; zf--;}
}
return r;
}
}
// 1. SETUP
// The "4K" part is simulated by upscaling. A factor of 2 is a good balance of quality and performance.
const targetWidth = originalImg.width * upscaleFactor;
const targetHeight = originalImg.height * upscaleFactor;
const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// 2. UPSCALE IMAGE
// Draw the original image onto the larger canvas. The browser performs interpolation.
ctx.drawImage(originalImg, 0, 0, targetWidth, targetHeight);
// 3. SHARPENING (Unsharp Mask)
// To counteract the blur from upscaling, we apply a sharpening filter.
if (sharpening > 0) {
const blurCanvas = document.createElement('canvas');
blurCanvas.width = targetWidth;
blurCanvas.height = targetHeight;
const blurCtx = blurCanvas.getContext('2d', { willReadFrequently: true });
// Create a blurred version of the upscaled image
blurCtx.filter = `blur(${1 * upscaleFactor}px)`;
blurCtx.drawImage(canvas, 0, 0);
const originalData = ctx.getImageData(0, 0, targetWidth, targetHeight);
const blurData = blurCtx.getImageData(0, 0, targetWidth, targetHeight).data;
const oData = originalData.data;
for (let i = 0; i < oData.length; i += 4) {
// Calculate luminance to avoid color shifts during sharpening
const originalLuminance = 0.299 * oData[i] + 0.587 * oData[i + 1] + 0.114 * oData[i + 2];
const blurLuminance = 0.299 * blurData[i] + 0.587 * blurData[i + 1] + 0.114 * blurData[i + 2];
const diff = originalLuminance - blurLuminance;
const sharpeningEffect = diff * sharpening;
// Apply sharpening diff to all color channels
oData[i] = Math.max(0, Math.min(255, oData[i] + sharpeningEffect));
oData[i + 1] = Math.max(0, Math.min(255, oData[i + 1] + sharpeningEffect));
oData[i + 2] = Math.max(0, Math.min(255, oData[i + 2] + sharpeningEffect));
}
ctx.putImageData(originalData, 0, 0);
}
// 4. WRINKLE TEXTURE (Perlin Noise)
// We generate a procedural texture and blend it over the image to simulate larger skin details.
if (wrinkles > 0) {
const perlin = new PerlinNoise();
perlin.seed(Math.random());
const textureCanvas = document.createElement('canvas');
textureCanvas.width = targetWidth;
textureCanvas.height = targetHeight;
const textureCtx = textureCanvas.getContext('2d', { willReadFrequently: true });
const textureData = textureCtx.createImageData(targetWidth, targetHeight);
const tData = textureData.data;
// Scale determines the "size" of the wrinkles
const wrinkleScaleX = 0.03 / upscaleFactor;
const wrinkleScaleY = 0.05 / upscaleFactor; // Slightly different scales for more natural look
for (let y = 0; y < targetHeight; y++) {
for (let x = 0; x < targetWidth; x++) {
// Get a noise value between -1 and 1
let noise = perlin.noise(x * wrinkleScaleX, y * wrinkleScaleY, 0);
// Convert to a grayscale value in the 0-255 range
const value = (noise * 0.5 + 0.5) * 255;
const grayValue = 128 + (value - 128);
const index = (y * targetWidth + x) * 4;
tData[index] = grayValue;
tData[index + 1] = grayValue;
tData[index + 2] = grayValue;
tData[index + 3] = 255;
}
}
textureCtx.putImageData(textureData, 0, 0);
// Blend the texture over the main image
ctx.globalCompositeOperation = 'overlay';
ctx.globalAlpha = wrinkles;
ctx.drawImage(textureCanvas, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1.0;
}
// 5. PORES (Fine-grained noise)
// We add a final layer of random noise to simulate skin pores and fine imperfections.
if (pores > 0) {
const finalImageData = ctx.getImageData(0, 0, targetWidth, targetHeight);
const fData = finalImageData.data;
for (let i = 0; i < fData.length; i += 4) {
// Add monochromatic noise to avoid colorful speckles
const noise = (Math.random() - 0.5) * pores;
fData[i] = Math.max(0, Math.min(255, fData[i] + noise));
fData[i+1] = Math.max(0, Math.min(255, fData[i+1] + noise));
fData[i+2] = Math.max(0, Math.min(255, fData[i+2] + noise));
}
ctx.putImageData(finalImageData, 0, 0);
}
return canvas;
}
Apply Changes