You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, intensity = 1.0, agingColorStr = "200,170,120", darkenEdgesFactor = 0.7, noiseAmount = 20, detailContrast = 1.5) {
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can be an optimization hint for some browsers
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// If image has no dimensions (e.g., not loaded yet or invalid), return empty canvas
if (canvas.width === 0 || canvas.height === 0) {
// console.warn("Image has zero dimensions. Returning empty canvas."); // Optional warning
return canvas;
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
// This can happen if the image is cross-origin and canvas is tainted
// console.error("Could not getImageData due to cross-origin restrictions or other error: ", e);
// In this case, return the canvas with the original image drawn, as processing is not possible.
return canvas;
}
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
// Parse agingColorStr: "R,G,B" and set defaults if parsing fails or invalid
const defaultAgingR = 200, defaultAgingG = 170, defaultAgingB = 120;
let paperR = defaultAgingR, paperG = defaultAgingG, paperB = defaultAgingB;
const colorParts = agingColorStr.split(',');
if (colorParts.length === 3) {
const rVal = parseInt(colorParts[0].trim(), 10);
const gVal = parseInt(colorParts[1].trim(), 10);
const bVal = parseInt(colorParts[2].trim(), 10);
// Ensure all parsed values are finite numbers before using them
if (Number.isFinite(rVal) && Number.isFinite(gVal) && Number.isFinite(bVal)) {
paperR = Math.max(0, Math.min(255, rVal));
paperG = Math.max(0, Math.min(255, gVal));
paperB = Math.max(0, Math.min(255, bVal));
}
}
// Define base ink color (dark, desaturated)
const inkR_base = 40, inkG_base = 30, inkB_base = 20;
// Clamp intensity and darkenEdgesFactor to a [0, 1] range for safety
const safeIntensity = Math.max(0, Math.min(1, intensity));
const safeDarkenEdgesFactor = Math.max(0, Math.min(1, darkenEdgesFactor));
const safeNoiseAmount = Math.max(0, noiseAmount); // Noise cannot be negative
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];
// const a_orig = data[i+3]; // Original alpha is preserved
// Initialize effect colors with original pixel values
let r_effect = r_orig;
let g_effect = g_orig;
let b_effect = b_orig;
// 1. Convert to grayscale (luminosity method) as basis for tinting
const gray = 0.299 * r_effect + 0.587 * g_effect + 0.114 * b_effect;
// 2. Apply aging color: Lerp between ink and paper color based on grayscale
const normGray = gray / 255; // Normalized grayscale (0=black, 1=white)
r_effect = inkR_base * (1 - normGray) + paperR * normGray;
g_effect = inkG_base * (1 - normGray) + paperG * normGray;
b_effect = inkB_base * (1 - normGray) + paperB * normGray;
// Clamp after color transformation
r_effect = Math.max(0, Math.min(255, r_effect));
g_effect = Math.max(0, Math.min(255, g_effect));
b_effect = Math.max(0, Math.min(255, b_effect));
// 3. Increase Contrast for details/ink
if (detailContrast !== 1.0) { // Avoid processing if no change intended
const contrastMidpoint = 128;
r_effect = (r_effect - contrastMidpoint) * detailContrast + contrastMidpoint;
g_effect = (g_effect - contrastMidpoint) * detailContrast + contrastMidpoint;
b_effect = (b_effect - contrastMidpoint) * detailContrast + contrastMidpoint;
r_effect = Math.max(0, Math.min(255, r_effect));
g_effect = Math.max(0, Math.min(255, g_effect));
b_effect = Math.max(0, Math.min(255, b_effect));
}
// 4. Add Noise
if (safeNoiseAmount > 0) {
// Symmetrical noise: adds or subtracts randomly
const noiseVal = (Math.random() - 0.5) * safeNoiseAmount;
r_effect += noiseVal;
g_effect += noiseVal;
b_effect += noiseVal;
r_effect = Math.max(0, Math.min(255, r_effect));
g_effect = Math.max(0, Math.min(255, g_effect));
b_effect = Math.max(0, Math.min(255, b_effect));
}
// 5. Vignette (Darken Edges)
if (safeDarkenEdgesFactor > 0) {
const x = (i / 4) % width;
const y = Math.floor((i / 4) / width);
const dx = x - width / 2;
const dy = y - height / 2;
const maxDist = Math.sqrt( (width/2)**2 + (height/2)**2 );
// Avoid division by zero for tiny images if maxDist is 0
if (maxDist > 0) {
const currentDist = Math.sqrt(dx*dx + dy*dy);
let vignetteAmount = currentDist / maxDist;
// Power curve for falloff: higher exponent means vignette is more concentrated at edges
vignetteAmount = Math.pow(vignetteAmount, 2.5);
const darkening = 1.0 - (vignetteAmount * safeDarkenEdgesFactor);
r_effect *= darkening;
g_effect *= darkening;
b_effect *= darkening;
r_effect = Math.max(0, Math.min(255, r_effect));
g_effect = Math.max(0, Math.min(255, g_effect));
b_effect = Math.max(0, Math.min(255, b_effect));
}
}
// Final blend: Mix original pixel with the fully effected pixel based on `safeIntensity`
data[i] = r_orig * (1 - safeIntensity) + r_effect * safeIntensity;
data[i+1] = g_orig * (1 - safeIntensity) + g_effect * safeIntensity;
data[i+2] = b_orig * (1 - safeIntensity) + b_effect * safeIntensity;
// Alpha (data[i+3]) remains untouched (effectively data[i+3] = a_orig; from original imageData)
// Final clamp (mostly for safety, as individual steps should ideally have clamped values correctly)
data[i] = Math.max(0, Math.min(255, data[i]));
data[i+1] = Math.max(0, Math.min(255, data[i+1]));
data[i+2] = Math.max(0, Math.min(255, data[i+2]));
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes