You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, grainAmount = 25, sepiaStrength = 0.35, vignetteStrength = 0.7, lightLeakOpacity = 0.15, scratchCount = 3) {
// Ensure the image is loaded and has dimensions
if (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
try {
await new Promise((resolve, reject) => {
// If already complete and valid, resolve immediately
if (originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
resolve();
return;
}
// If src is not even set, it's an error
if (!originalImg.src) {
reject(new Error("Image source is not set."));
return;
}
originalImg.onload = resolve;
originalImg.onerror = () => reject(new Error("Image failed to load (onerror triggered)."));
// Check if src is a data URL and if it's potentially empty, which might not trigger onload/onerror
if (originalImg.src.startsWith('data:') && originalImg.src.length < 100) { // Arbitrary small length
// This is a heuristic; a more robust check might be needed for specific empty data URLs
if (!originalImg.complete || originalImg.naturalWidth === 0) {
setTimeout(() => reject(new Error("Image source seems invalid or empty (data URL).")), 50);
}
}
});
} catch (error) {
console.error("Error loading image:", error.message);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = Math.max(250, originalImg.width || 0); // Use originalImg.width if available
errorCanvas.height = Math.max(60, originalImg.height || 0);
const errCtx = errorCanvas.getContext('2d');
if (errCtx) {
errCtx.fillStyle = 'black';
errCtx.fillRect(0,0, errorCanvas.width, errorCanvas.height);
errCtx.fillStyle = 'red';
errCtx.font = '12px Arial';
errCtx.textAlign = 'center';
errCtx.fillText("Error: Image could not be loaded.", errorCanvas.width/2, errorCanvas.height/2 - 10);
errCtx.fillText(error.message.substring(0,100), errorCanvas.width/2, errorCanvas.height/2 + 10);
}
return errorCanvas;
}
}
// Final check after attempting to load
if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.error("Image has zero dimensions after load attempt.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 250; errorCanvas.height = 30;
const errCtx = errorCanvas.getContext('2d');
if (errCtx){
errCtx.fillStyle = 'red';
errCtx.font = '12px Arial';
errCtx.fillText("Error: Image has zero dimensions.", 5, 20);
}
return errorCanvas;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
// 1. Draw original image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// 2. Get pixel data for sepia, grain
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Could not get image data (tainted canvas? Cross-origin image without CORS headers):", e);
// Draw an error message on the canvas as pixel manipulation is not possible
ctx.fillStyle = 'rgba(128,128,128,0.8)'; // Semi-transparent grey overlay
ctx.fillRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.font = `bold ${Math.min(24, canvas.width/15)}px Arial`;
ctx.textAlign = 'center';
const message = "Cannot apply effects: Cross-origin image.";
ctx.strokeText(message, canvas.width/2, canvas.height/2);
ctx.fillText(message, canvas.width/2, canvas.height/2);
return canvas; // Return canvas with original image + error message
}
let data = imageData.data;
// Apply Sepia tone and Grain
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// Sepia
if (sepiaStrength > 0) {
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
r = r * (1 - sepiaStrength) + tr * sepiaStrength;
g = g * (1 - sepiaStrength) + tg * sepiaStrength;
b = b * (1 - sepiaStrength) + tb * sepiaStrength;
}
// Grain
if (grainAmount > 0) {
const noise = (Math.random() - 0.5) * grainAmount;
r += noise;
g += noise;
b += noise;
}
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));
}
ctx.putImageData(imageData, 0, 0);
// 3. Light Leaks (draw on top)
if (lightLeakOpacity > 0 && canvas.width > 0 && canvas.height > 0) {
ctx.globalCompositeOperation = 'lighter';
const numLeaks = Math.random() < 0.6 ? 1 : 2; // 60% chance 1 leak, 40% chance 2 leaks
for (let k=0; k < numLeaks; k++) {
// Position leaks towards edges/corners
const edgeOrCorner = Math.random();
let leakX, leakY;
if (edgeOrCorner < 0.25) { // Top edge
leakX = canvas.width * Math.random(); leakY = canvas.height * (Math.random()*0.1 - 0.05);
} else if (edgeOrCorner < 0.5) { // Bottom edge
leakX = canvas.width * Math.random(); leakY = canvas.height * (1 - Math.random()*0.1 + 0.05);
} else if (edgeOrCorner < 0.75) { // Left edge
leakX = canvas.width * (Math.random()*0.1 - 0.05); leakY = canvas.height * Math.random();
} else { // Right edge
leakX = canvas.width * (1 - Math.random()*0.1 + 0.05); leakY = canvas.height * Math.random();
}
const leakRadiusStart = Math.min(canvas.width, canvas.height) * (0.02 + Math.random() * 0.08);
const leakRadiusEnd = Math.min(canvas.width, canvas.height) * (0.25 + Math.random() * 0.35);
if (leakRadiusEnd <= leakRadiusStart) continue;
const leakGradient = ctx.createRadialGradient(
leakX, leakY, leakRadiusStart,
leakX, leakY, leakRadiusEnd
);
const rCol = 255;
const gCol = 60 + Math.random() * 100; // Orange-ish to Yellow-ish
const bCol = Math.random() * 40; // Hint of red
const currentOpacity = lightLeakOpacity * (0.5 + Math.random() * 0.5);
leakGradient.addColorStop(0, `rgba(${rCol}, ${gCol}, ${bCol}, ${currentOpacity})`);
leakGradient.addColorStop(0.6, `rgba(${rCol}, ${Math.min(255,gCol + 60)}, ${Math.min(255,bCol + 30)}, ${currentOpacity * 0.4})`);
leakGradient.addColorStop(1, `rgba(${rCol}, ${Math.min(255,gCol + 120)}, ${Math.min(255,bCol + 60)}, 0)`);
ctx.fillStyle = leakGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.globalCompositeOperation = 'source-over';
}
// 4. Vignette (draw on top)
if (vignetteStrength > 0 && canvas.width > 0 && canvas.height > 0) {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const outerRadius = Math.sqrt(centerX * centerX + centerY * centerY);
const innerRadiusRatio = Math.max(0, 0.95 - vignetteStrength * 0.85);
const innerRadius = outerRadius * innerRadiusRatio;
// Only draw if the vignette has some perceivable effect
if (outerRadius > innerRadius || vignetteStrength > 0.01) {
const gradient = ctx.createRadialGradient(
centerX, centerY, Math.max(0, innerRadius), // Ensure innerRadius is not negative
centerX, centerY, outerRadius
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, `rgba(0,0,0, ${Math.min(1, vignetteStrength * 0.95)})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
// 5. Scratches and Dust (draw on top)
if (scratchCount > 0 && canvas.width > 0 && canvas.height > 0) {
// Scratches
ctx.strokeStyle = `rgba(200, 200, 200, ${0.1 + Math.random() * 0.15 * Math.min(1, scratchCount/2)})`;
ctx.lineWidth = Math.max(0.5, Math.min(1.5, (canvas.width + canvas.height) / 2000));
for (let k = 0; k < scratchCount; k++) {
const x = Math.random() * canvas.width;
const yStart = Math.random() * canvas.height;
const length = (Math.random() * 0.4 + 0.05) * canvas.height; // 5-45% of height
// Angle mostly vertical, but can be varied more
const angle = (Math.random() - 0.5) * (Math.PI / 2); // from -45 to +45 deg from vertical
const xEnd = x + Math.sin(angle) * length;
let yEnd = yStart + Math.cos(angle) * length;
if (Math.random() < 0.5) yEnd = yStart - Math.cos(angle) * length;
ctx.beginPath();
ctx.moveTo(x, yStart);
ctx.lineTo(xEnd, yEnd);
ctx.stroke();
}
// Dust specks
const dustParticleCount = Math.floor(scratchCount * (2 + Math.random() * 5));
ctx.fillStyle = `rgba(190, 190, 190, ${0.15 + Math.random() * 0.2 * Math.min(1, scratchCount/2)})`;
for (let k = 0; k < dustParticleCount; k++) {
const dustX = Math.random() * canvas.width;
const dustY = Math.random() * canvas.height;
const dustRadius = Math.random() * 1.0 + 0.3;
ctx.beginPath();
ctx.arc(dustX, dustY, dustRadius, 0, Math.PI * 2);
ctx.fill();
}
}
return canvas;
}
Apply Changes