You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Transforms a portrait image into a classic Hollywood cinematic still by applying
* a series of filters for lighting, color, sharpness, and texture.
*
* @param {Image} originalImg The original Image object to process.
* @param {number} [contrast=60] The contrast level (0-100). 50 is neutral. Higher values create more dramatic lighting.
* @param {number} [desaturation=90] The amount of desaturation (0-100). 100 is full grayscale.
* @param {number} [sharpen=25] The amount of sharpening to apply (0-100).
* @param {number} [grain=10] The amount of cinematic grain to add (0-100).
* @param {number} [vignette=50] The strength of the vignette effect to isolate the subject (0-100).
* @param {string} [tintColor='#1a2530'] The hexadecimal color code for the moody tint (e.g., '#RRGGBB').
* @param {number} [tintAmount=20] The intensity of the color tint (0-100).
* @returns {Promise<HTMLCanvasElement>} A canvas element with the processed image.
*/
async function processImage(originalImg, contrast = 60, desaturation = 90, sharpen = 25, grain = 10, vignette = 50, tintColor = '#1a2530', tintAmount = 20) {
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can optimize frequent getImageData/putImageData calls.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const w = originalImg.naturalWidth;
const h = originalImg.naturalHeight;
canvas.width = w;
canvas.height = h;
// 1. Draw the original image onto the canvas
ctx.drawImage(originalImg, 0, 0, w, h);
// 2. Apply pixel-level filters: Desaturation, Contrast, and Tint
if (desaturation > 0 || contrast !== 50 || tintAmount > 0) {
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
// Pre-calculate factors and parse colors for efficiency
const desat = desaturation / 100;
const tintMix = tintAmount / 100;
// Map contrast from 0-100 to a more effective range for the formula
const contrastValue = ((contrast - 50) / 50) * 128;
const contrastFactor = (259 * (contrastValue + 255)) / (255 * (259 - contrastValue));
const tr = parseInt(tintColor.slice(1, 3), 16);
const tg = parseInt(tintColor.slice(3, 5), 16);
const tb = parseInt(tintColor.slice(5, 7), 16);
// Iterate through each pixel
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// A. Apply Desaturation
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = r * (1 - desat) + gray * desat;
g = g * (1 - desat) + gray * desat;
b = b * (1 - desat) + gray * desat;
// B. Apply Contrast
r = contrastFactor * (r - 127) + 127;
g = contrastFactor * (g - 127) + 127;
b = contrastFactor * (b - 127) + 127;
// C. Apply Color Tint
if (tintMix > 0) {
r = r * (1 - tintMix) + tr * tintMix;
g = g * (1 - tintMix) + tg * tintMix;
b = b * (1 - tintMix) + tb * tintMix;
}
// Clamp values to the 0-255 range
data[i] = Math.max(0, Math.min(255, r));
data[i + 1] = Math.max(0, Math.min(255, g));
data[i + 2] = Math.max(0, Math.min(255, b));
}
ctx.putImageData(imageData, 0, 0);
}
// 3. Apply Sharpening using a convolution filter (Unsharp Mask)
if (sharpen > 0) {
const srcData = ctx.getImageData(0, 0, w, h);
const src = srcData.data;
const dstData = ctx.createImageData(w, h);
const dst = dstData.data;
dst.set(src); // Copy original image data, including alpha
const amount = sharpen / 100.0;
// Process pixels, skipping a 1-pixel border to avoid edge issues
for (let y = 1; y < h - 1; y++) {
for (let x = 1; x < w - 1; x++) {
const i = (y * w + x) * 4;
for (let c = 0; c < 3; c++) { // Iterate over R, G, B channels
const sc = i + c;
// Simple 3x3 high-pass filter kernel
const highPass = 5 * src[sc] - (src[sc - 4] + src[sc + 4] + src[sc - w * 4] + src[sc + w * 4]);
// Blend original pixel with the high-pass filtered version
dst[sc] = src[sc] * (1 - amount) + highPass * amount;
}
}
}
ctx.putImageData(dstData, 0, 0);
}
// 4. Apply Vignette effect using a radial gradient and multiply blend mode
if (vignette > 0) {
const strength = vignette / 100;
const outerRadius = Math.sqrt(Math.pow(w / 2, 2) + Math.pow(h / 2, 2));
const innerRadius = outerRadius * (1 - strength);
const gradient = ctx.createRadialGradient(w / 2, h / 2, innerRadius, w / 2, h / 2, outerRadius);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
// A black vignette can be too harsh; a dark grey is often more subtle.
gradient.addColorStop(1, `rgba(0,0,0,${strength})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
}
// 5. Apply Cinematic Grain using a tiled noise pattern with 'overlay' blend mode
if (grain > 0) {
const grainAmount = grain / 100;
const grainSize = 128; // Use a small, tileable texture for performance
const grainCanvas = document.createElement('canvas');
grainCanvas.width = grainSize;
grainCanvas.height = grainSize;
const grainCtx = grainCanvas.getContext('2d');
const grainData = grainCtx.createImageData(grainSize, grainSize);
const grainPixels = grainData.data;
for (let i = 0; i < grainPixels.length; i += 4) {
const value = Math.floor(Math.random() * 255);
grainPixels[i] = value;
grainPixels[i + 1] = value;
grainPixels[i + 2] = value;
grainPixels[i + 3] = 255;
}
grainCtx.putImageData(grainData, 0, 0);
ctx.globalAlpha = grainAmount * 0.2; // Control grain visibility
ctx.globalCompositeOperation = 'overlay';
const pattern = ctx.createPattern(grainCanvas, 'repeat');
if (pattern) {
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, w, h);
}
// Reset context properties to defaults
ctx.globalAlpha = 1.0;
ctx.globalCompositeOperation = 'source-over';
}
return canvas;
}
Apply Changes