You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
tintColor = "atompunk_orange", // "sepia", "atompunk_teal", "atompunk_yellow_glow", "#RRGGBB", or "none"
tintStrength = 0.5, // 0.0 to 1.0
grainAmount = 25, // 0 to 100 (noise intensity)
vignetteStrength = 0.6, // 0.0 to 1.0 (opacity of vignette)
vignetteSoftness = 0.5, // 0.1 to 1.0 (spread of vignette)
contrast = 1.2, // 0.5 to 2.0 (1.0 = no change)
brightness = 10, // -100 to 100 (0 = no change)
saturation = 0.7 // 0.0 (grayscale) to 2.0 (double saturation), 1.0 = no change
) {
// Create canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // willReadFrequently for performance with getImageData/putImageData
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Draw original image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
// Helper: Clamp value to 0-255
function clamp(value) {
return Math.max(0, Math.min(Math.floor(value), 255));
}
// Helper: Hex to RGB
function hexToRgb(hex) {
if (!hex || typeof hex !== 'string' || hex.charAt(0) !== '#') return null;
hex = hex.slice(1);
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
if (hex.length !== 6) return null;
const bigint = parseInt(hex, 16);
if (isNaN(bigint)) return null;
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
let tintR_val, tintG_val, tintB_val;
let applyTint = tintColor.toLowerCase() !== "none" && tintStrength > 0;
if (applyTint) {
const lowerTintColor = tintColor.toLowerCase();
if (lowerTintColor === "sepia") {
// Sepia is handled differently in the loop due to its formula
} else if (lowerTintColor === "atompunk_orange") {
tintR_val = 255; tintG_val = 170; tintB_val = 85; // Warm, slightly desaturated orange
} else if (lowerTintColor === "atompunk_teal") {
tintR_val = 60; tintG_val = 150; tintB_val = 160; // Muted teal
} else if (lowerTintColor === "atompunk_yellow_glow") {
tintR_val = 255; tintG_val = 220; tintB_val = 100; // Retro CRT glow yellow
} else {
const rgb = hexToRgb(tintColor);
if (rgb) {
tintR_val = rgb.r;
tintG_val = rgb.g;
tintB_val = rgb.b;
} else {
console.warn("Invalid tintColor provided:", tintColor);
applyTint = false;
}
}
}
// Process pixels
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Brightness
if (brightness !== 0) {
r += brightness;
g += brightness;
b += brightness;
}
// 2. Contrast
if (contrast !== 1.0) {
// Adjust contrast factor to be in a more common range if needed,
// but the current formula works directly with contrast parameter.
// For example, a contrast param of 1.2 increases contrast by 20%.
r = ((r / 255 - 0.5) * contrast + 0.5) * 255;
g = ((g / 255 - 0.5) * contrast + 0.5) * 255;
b = ((b / 255 - 0.5) * contrast + 0.5) * 255;
}
r = clamp(r); g = clamp(g); b = clamp(b); // Clamp after brightness/contrast
// 3. Saturation
if (saturation !== 1.0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Luminance
r = gray + saturation * (r - gray);
g = gray + saturation * (g - gray);
b = gray + saturation * (b - gray);
}
r = clamp(r); g = clamp(g); b = clamp(b); // Clamp after saturation
// 4. Tint
if (applyTint) {
if (tintColor.toLowerCase() === "sepia") {
const sr = 0.393 * r + 0.769 * g + 0.189 * b;
const sg = 0.349 * r + 0.686 * g + 0.168 * b;
const sb = 0.272 * r + 0.534 * g + 0.131 * b;
r = (1 - tintStrength) * r + tintStrength * sr;
g = (1 - tintStrength) * g + tintStrength * sg;
b = (1 - tintStrength) * b + tintStrength * sb;
} else if (tintR_val !== undefined) { // Custom hex or named Atompunk color
r = (1 - tintStrength) * r + tintStrength * tintR_val;
g = (1 - tintStrength) * g + tintStrength * tintG_val;
b = (1 - tintStrength) * b + tintStrength * tintB_val;
}
}
r = clamp(r); g = clamp(g); b = clamp(b); // Clamp after tint
// 5. Grain
if (grainAmount > 0) {
// grainAmount determines magnitude of noise, e.g., 25 means noise up to +/- 12.5
const noise = (Math.random() - 0.5) * grainAmount;
r += noise;
g += noise;
b += noise;
}
// Final clamp and assignment
data[i] = clamp(r);
data[i + 1] = clamp(g);
data[i + 2] = clamp(b);
// Alpha (data[i+3]) remains unchanged
}
// Put modified image data back
ctx.putImageData(imageData, 0, 0);
// 6. Vignette (applied post-pixel processing)
if (vignetteStrength > 0 && vignetteStrength <= 1) {
const centerX = width / 2;
const centerY = height / 2;
const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY);
// vignetteSoftness: 0.1 (harder edge, gradient near perimeter) to 1.0 (softer edge, gradient starts near center).
// Clamp vignetteSoftness to a safe range to prevent extreme values for innerRadius.
const effectiveSoftness = Math.min(Math.max(vignetteSoftness, 0.01), 0.99);
const innerRadius = maxRadius * (1 - effectiveSoftness); // Smaller softness -> larger innerRadius -> sharper vignette
// Larger softness -> smaller innerRadius -> wider vignette gradient
const gradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, maxRadius);
gradient.addColorStop(0, 'rgba(0,0,0,0)'); // Transparent center
// Vignette color is black, strength controls its opacity at the edge
gradient.addColorStop(1, `rgba(0,0,0,${vignetteStrength})`);
ctx.fillStyle = gradient;
ctx.globalCompositeOperation = 'source-atop'; // Apply vignette on top of existing image content
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = 'source-over'; // Reset composite operation
}
return canvas;
}
Apply Changes