You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, colorLevel = 1.0, vignetteLevel = 0.5, filmGrain = 12, letterbox = "true") {
const canvas = document.createElement('canvas');
const width = originalImg.width;
const height = originalImg.height;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Draw the original image onto the canvas
ctx.drawImage(originalImg, 0, 0);
// Extract Image Data for pixel manipulation
let imgData;
try {
imgData = ctx.getImageData(0, 0, width, height);
} catch (e) {
// Fallback for CORS issues if happened in specific envs (though tools run locally)
console.error("CORS error getting image data", e);
return canvas;
}
const data = imgData.data;
const cLevel = Number(colorLevel) || 1.0;
const vLevel = Number(vignetteLevel) || 0.5;
const grain = Number(filmGrain) || 0;
const doLetterbox = String(letterbox).toLowerCase() === "true";
// Cinematic color grading loop (Contrast, Teal & Orange mapping, Desaturation, Film Grain)
const contrast = 1.15;
const saturation = 0.8; // Desaturate slightly for a moody look
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// Apply Contrast
r = Math.max(0, Math.min(255, (r - 128) * contrast + 128));
g = Math.max(0, Math.min(255, (g - 128) * contrast + 128));
b = Math.max(0, Math.min(255, (b - 128) * contrast + 128));
// Calculate Luminance
let luma = 0.299 * r + 0.587 * g + 0.114 * b;
// Apply Global Desaturation
r = r * saturation + luma * (1 - saturation);
g = g * saturation + luma * (1 - saturation);
b = b * saturation + luma * (1 - saturation);
// Normalized Luma mapping for color targeting (0 to 1)
let lumaN = luma / 255;
// Create factors to isolate shadows and highlights
// Shadows curve peaks at 25% brightness
let shadowFactor = Math.max(0, 1 - Math.abs(lumaN - 0.25) * 3);
// Highlights curve peaks at 75% brightness
let highFactor = Math.max(0, 1 - Math.abs(lumaN - 0.75) * 3);
// Teal push in shadows
let rShift = -40 * shadowFactor;
let gShift = 10 * shadowFactor;
let bShift = 45 * shadowFactor;
// Orange push in highlights
rShift += 45 * highFactor;
gShift += 25 * highFactor;
bShift -= 35 * highFactor;
let tR = r + (rShift * cLevel);
let tG = g + (gShift * cLevel);
let tB = b + (bShift * cLevel);
// Add Film Grain
if (grain > 0) {
let noise = (Math.random() - 0.5) * grain;
tR += noise;
tG += noise;
tB += noise;
}
// Clamp final values and write directly back
data[i] = Math.max(0, Math.min(255, tR));
data[i+1] = Math.max(0, Math.min(255, tG));
data[i+2] = Math.max(0, Math.min(255, tB));
}
// Apply processed pixels
ctx.putImageData(imgData, 0, 0);
// Apply Vignette effect
if (vLevel > 0) {
ctx.globalCompositeOperation = 'source-over';
let grad = ctx.createRadialGradient(
width / 2, height / 2, Math.min(width, height) * 0.35,
width / 2, height / 2, Math.max(width, height) * 0.75
);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(1, `rgba(0,0,0,${vLevel})`);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, width, height);
}
// Apply Cinematic Letterboxing (Cinemascope 2.35:1 aspect ratio bars)
if (doLetterbox) {
const cinemaRatio = 2.35;
let expectedHeight = width / cinemaRatio;
// Check if image is standard ratio and needs horizontal black bars (Top/Bottom)
if (expectedHeight < height * 0.98) {
let barHeight = (height - expectedHeight) / 2;
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, width, barHeight); // Top bar
ctx.fillRect(0, height - barHeight, width, barHeight); // Bottom bar
} else {
// Alternatively: if panoramic ratio is wider than 2.35:1, draw side bars
let expectedWidth = height * cinemaRatio;
if (expectedWidth < width * 0.98) {
let barWidth = (width - expectedWidth) / 2;
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, barWidth, height); // Left bar
ctx.fillRect(width - barWidth, 0, barWidth, height); // Right bar
}
}
}
return canvas;
}
Apply Changes