You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
desaturation = 0.1,
warmth = 0.03,
contrast = 8,
liftedBlacks = 8,
shadowCoolness = 0.05,
vignetteStrength = 0.7,
vignetteOpacity = 0.35,
grainAmount = 6
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgWidth = originalImg.naturalWidth;
const imgHeight = originalImg.naturalHeight;
canvas.width = imgWidth;
canvas.height = imgHeight;
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
if (imgWidth === 0 || imgHeight === 0) {
// Handle cases of invalid image dimensions
return canvas;
}
const imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
const data = imageData.data;
// Parameter clamping to ensure they are within a reasonable range
desaturation = Math.max(0, Math.min(1, desaturation));
warmth = Math.max(-0.5, Math.min(0.5, warmth)); // Allows noticeable warming/cooling
contrast = Math.max(-100, Math.min(100, contrast)); // Range for contrast adjustment
liftedBlacks = Math.max(0, Math.min(50, liftedBlacks));
shadowCoolness = Math.max(0, Math.min(1, shadowCoolness));
vignetteStrength = Math.max(0, Math.min(1, vignetteStrength));
vignetteOpacity = Math.max(0, Math.min(1, vignetteOpacity));
grainAmount = Math.max(0, Math.min(50, grainAmount));
for (let i = 0; i < data.length; i += 4) {
const r_orig = data[i];
const g_orig = data[i + 1];
const b_orig = data[i + 2];
let r = r_orig;
let g = g_orig;
let b = b_orig;
// 1. Desaturation
// Calculates grayscale value and interpolates towards it
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = r + (gray - r) * desaturation;
g = g + (gray - g) * desaturation;
b = b + (gray - b) * desaturation;
// 2. Warmth (Color Temperature Shift)
// Positive warmth adds red tones and reduces blue tones. Negative does the opposite.
// Scaled by 30 for a perceptible but controlled effect.
r = r + warmth * 30;
b = b - warmth * 30;
// 3. Contrast Adjustment
// Maps a user-friendly contrast value (-100 to 100) to a factor for a standard contrast formula.
// cVal is mapped to roughly -128 to 128 for strong control.
const cVal = contrast * 1.28;
const contrastFactor = (259 * (cVal + 255)) / (255 * (259 - cVal));
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
// 4. Lifted Blacks
// Additively lifts the black levels. Clamping later handles overflow.
r += liftedBlacks;
g += liftedBlacks;
b += liftedBlacks;
// Intermediate clamping after major tonal adjustments
r = Math.min(255, Math.max(0, r));
g = Math.min(255, Math.max(0, g));
b = Math.min(255, Math.max(0, b));
// 5. Shadow Coolness
// Applies a cool (blue/cyan) tint to darker areas of the image.
// Based on original luminance to prevent feedback loops with other adjustments.
const originalLuminance = 0.299 * r_orig + 0.587 * g_orig + 0.114 * b_orig;
const shadowThreshold = 85; // Defines what's considered a "shadow"
if (originalLuminance < shadowThreshold && shadowCoolness > 0) {
const coolFactor = (1 - originalLuminance / shadowThreshold); // Effect stronger in darker parts
const coolAmountBlue = shadowCoolness * 30 * coolFactor; // Blue component of tint
const coolAmountGreen = shadowCoolness * 15 * coolFactor; // Green component for cyan tint
b += coolAmountBlue;
g += coolAmountGreen;
}
// Clamp again before adding grain
r = Math.min(255, Math.max(0, r));
g = Math.min(255, Math.max(0, g));
b = Math.min(255, Math.max(0, b));
// 6. Grain
// Adds monochromatic noise to simulate film grain.
if (grainAmount > 0) {
const grainVal = (Math.random() - 0.5) * grainAmount;
r += grainVal;
g += grainVal;
b += grainVal;
}
// Final clamp for all color channels
data[i] = Math.min(255, Math.max(0, r));
data[i + 1] = Math.min(255, Math.max(0, g));
data[i + 2] = Math.min(255, Math.max(0, b));
}
ctx.putImageData(imageData, 0, 0);
// 7. Vignetting
// Applies a darkening effect to the corners of the image.
if (vignetteOpacity > 0 && vignetteStrength > 0) {
const centerX = imgWidth / 2;
const centerY = imgHeight / 2;
// vignetteStrength determines how far the vignette effect extends inwards.
// High strength = smaller clear center area = more pronounced vignette.
const innerRadiusRatio = 1.0 - vignetteStrength;
const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY);
const innerRadius = maxRadius * Math.max(0, innerRadiusRatio); // Ensure innerRadius is not negative
const gradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, maxRadius);
// Gradient stops for a smoother falloff
gradient.addColorStop(0, `rgba(0,0,0,0)`); // Center is fully transparent
gradient.addColorStop(0.7, `rgba(0,0,0,${vignetteOpacity * 0.75})`); // Mid-point of vignette
gradient.addColorStop(1, `rgba(0,0,0,${vignetteOpacity})`); // Edge is darkest
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, imgWidth, imgHeight);
}
return canvas;
}
Apply Changes