You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, noiseStrength = 30, tintColor = "#654321", tintStrength = 0.6, brightness = -20, contrast = 20) {
// Helper function to parse hex color string to RGB object
function hexToRgb(hex) {
if (!hex || typeof hex !== 'string') return null;
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// Validate and ensure the image is loaded
try {
if (!originalImg || typeof originalImg.naturalWidth === 'undefined') {
throw new Error("Invalid Image object provided.");
}
// Check if image needs loading
if (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
// If src is not set and it's not already loaded, it cannot load.
if (!originalImg.src && !originalImg.currentSrc) { // currentSrc for potential dynamic changes
if (!(originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0)) {
throw new Error("Image has no src and is not loaded.");
}
}
if (!(originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0)) {
// Only await if not already loaded with valid dimensions
await new Promise((resolve, reject) => {
originalImg.onload = () => {
if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
reject(new Error("Image loaded with zero dimensions."));
} else {
resolve();
}
};
originalImg.onerror = () => reject(new Error("Image failed to load (onerror triggered)."));
});
}
}
// Final check after potential load
if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
throw new Error("Image has zero dimensions after load attempt.");
}
} catch (error) {
console.error("Image loading failed:", error.message);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 300;
errorCanvas.height = 80;
const ctx = errorCanvas.getContext('2d');
if (ctx) {
ctx.fillStyle = 'rgb(240,240,240)';
ctx.fillRect(0,0,errorCanvas.width, errorCanvas.height);
ctx.fillStyle = 'black';
ctx.font = "12px Arial";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const message = error.message || "Failed to load image.";
// Basic text wrapping
const words = message.split(' ');
let line = '';
let y = errorCanvas.height / 2 - ( (Math.ceil(ctx.measureText(message).width / (errorCanvas.width - 20)) -1) * 14 / 2); // Adjust y for multi-line
for(let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > errorCanvas.width - 20 && n > 0) {
ctx.fillText(line, errorCanvas.width / 2, y);
line = words[n] + ' ';
y += 14; // Line height
} else {
line = testLine;
}
}
ctx.fillText(line, errorCanvas.width/2, y);
}
return errorCanvas;
}
const canvas = document.createElement('canvas');
// Use { willReadFrequently: true } for potential performance boost with getImageData
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Could not get ImageData (e.g., tainted canvas from cross-origin image):", e.message);
// Clear canvas and draw an error message
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = 'rgb(240,240,240)';
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = 'red';
// Responsive font size, capped
const fontSize = Math.min(24, Math.max(12, Math.floor(canvas.width/25)));
ctx.font = `bold ${fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const lines = ["Error: Could not process image pixels.", "(Possibly a cross-origin issue)"];
const lineHeight = fontSize * 1.2;
let yPos = canvas.height/2 - (lines.length-1) * lineHeight / 2;
for(const line of lines){
ctx.fillText(line, canvas.width/2, yPos);
yPos += lineHeight;
}
return canvas; // Return the canvas with the error message
}
const data = imageData.data;
let parsedTintColor = hexToRgb(tintColor);
if (!parsedTintColor) {
console.warn(`Invalid tintColor: "${tintColor}". Defaulting to #654321 (dark brown).`);
parsedTintColor = { r: 101, g: 67, b: 33 };
}
const { r: tintR, g: tintG, b: tintB } = parsedTintColor;
// Ensure parameters are numeric and appropriately scaled/clamped
const contrastVal = Math.max(-255, Math.min(255, parseFloat(contrast))); // Clamp contrast to avoid extreme factors / division by zero
const contrastFactor = (259 * (contrastVal + 255)) / (255 * (259 - contrastVal));
const brightnessVal = parseFloat(brightness);
const tintStrengthVal = Math.max(0, Math.min(1, parseFloat(tintStrength)));
const noiseStrengthVal = parseFloat(noiseStrength);
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Apply Tint
r = r * (1 - tintStrengthVal) + tintR * tintStrengthVal;
g = g * (1 - tintStrengthVal) + tintG * tintStrengthVal;
b = b * (1 - tintStrengthVal) + tintB * tintStrengthVal;
// 2. Apply Brightness
r += brightnessVal;
g += brightnessVal;
b += brightnessVal;
// Clamp after brightness before contrast, as contrast pivot is 128
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 3. Apply Contrast
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
// Clamp after contrast
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 4. Apply Noise (Affects R, G, B similarly to simulate grain)
// noiseStrength parameter is used as the amplitude of noise, e.g. if 30, noise is +/- 15.
const noise = (Math.random() - 0.5) * noiseStrengthVal;
r += noise;
g += noise;
b += noise;
// Final clamp for all channels
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));
// Alpha channel (data[i+3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes