You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
sepiaLevel = 0.7,
grainLevel = 20,
vignetteLevel = 0.6,
creaseLines = 3,
stainPatches = 5,
customText = "",
textColor = "rgba(50,30,20,0.75)",
textFont = "italic 18px 'Times New Roman', serif",
textPlacement = "bottomRight"
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
canvas.width = w;
canvas.height = h;
// Handle cases where image might not be loaded or is empty
if (w === 0 || h === 0) {
// console.warn("Image Time Traveler: Original image has zero width or height.");
return canvas; // Return empty canvas matching original's potential dimensions
}
// 1. Draw original image
ctx.drawImage(originalImg, 0, 0, w, h);
// 2. Apply Sepia and Grain (Pixel manipulation)
if (sepiaLevel > 0 || grainLevel > 0) {
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Apply Sepia
if (sepiaLevel > 0) {
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;
// Blend current color with sepia color
r = r * (1 - sepiaLevel) + sr * sepiaLevel;
g = g * (1 - sepiaLevel) + sg * sepiaLevel;
b = b * (1 - sepiaLevel) + sb * sepiaLevel;
}
// Apply Grain
if (grainLevel > 0) {
const grainAmount = (Math.random() - 0.5) * grainLevel;
r += grainAmount;
g += grainAmount;
b += grainAmount;
}
// Clamp values
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. Draw Stain Patches
if (stainPatches > 0) {
for (let i = 0; i < stainPatches; i++) {
const stainX = Math.random() * w;
const stainY = Math.random() * h;
const baseStainRadius = (Math.random() * 0.08 + 0.05) * Math.min(w, h); // 5-13% of min dimension
const numSplotches = Math.floor(Math.random() * 3) + 2; // 2 to 4 splotches
for (let k = 0; k < numSplotches; k++) {
const offsetX = (Math.random() - 0.5) * baseStainRadius * 1.5;
const offsetY = (Math.random() - 0.5) * baseStainRadius * 1.5;
const splotchRadius = baseStainRadius * (Math.random() * 0.6 + 0.4);
const currentX = Math.max(0 - splotchRadius, Math.min(w + splotchRadius, stainX + offsetX));
const currentY = Math.max(0 - splotchRadius, Math.min(h + splotchRadius, stainY + offsetY));
ctx.beginPath();
ctx.arc(currentX, currentY, splotchRadius, 0, Math.PI * 2);
const R_stain = 120 + Math.random() * 60;
const G_stain = R_stain - (30 + Math.random() * 30);
const B_stain = G_stain - (40 + Math.random() * 30);
const alpha_stain = Math.random() * 0.06 + 0.02;
ctx.fillStyle = `rgba(${Math.floor(R_stain)}, ${Math.floor(G_stain)}, ${Math.floor(B_stain)}, ${alpha_stain})`;
ctx.fill();
}
}
}
// 4. Draw Crease Lines
if (creaseLines > 0) {
for (let i = 0; i < creaseLines; i++) {
let x1, y1, x2, y2;
const type = Math.random();
if (type < 0.4) { // Primarily horizontal crease
y1 = Math.random() * h;
y2 = y1 + (Math.random() - 0.5) * (h * 0.1);
x1 = Math.random() * w * 0.2;
x2 = w - (Math.random() * w * 0.2);
if (Math.random() < 0.5) { [x1,x2] = [x2,x1]; }
} else if (type < 0.8) { // Primarily vertical crease
x1 = Math.random() * w;
x2 = x1 + (Math.random() - 0.5) * (w * 0.1);
y1 = Math.random() * h * 0.2;
y2 = h - (Math.random() * h * 0.2);
if (Math.random() < 0.5) { [y1,y2] = [y2,y1]; }
} else { // Diagonal crease
const lengthFactor = Math.min(w,h) * (0.7 + Math.random() * 0.5);
const angle = Math.random() * Math.PI * 2;
x1 = Math.random() * w;
y1 = Math.random() * h;
x2 = x1 + Math.cos(angle) * lengthFactor;
y2 = y1 + Math.sin(angle) * lengthFactor;
}
ctx.strokeStyle = `rgba(0, 0, 0, ${Math.random() * 0.18 + 0.08})`;
ctx.lineWidth = Math.random() * 1.2 + 0.4;
ctx.beginPath();
ctx.moveTo(x1, y1);
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
const lineLength = Math.sqrt((x2-x1)**2 + (y2-y1)**2);
const crinkleMagnitude = lineLength * 0.05 * (Math.random() * 2);
const perpAngle = Math.atan2(y2-y1, x2-x1) + Math.PI/2;
const cpX = midX + Math.cos(perpAngle) * crinkleMagnitude * (Math.random() - 0.5) * 2;
const cpY = midY + Math.sin(perpAngle) * crinkleMagnitude * (Math.random() - 0.5) * 2;
ctx.quadraticCurveTo(cpX, cpY, x2, y2);
ctx.stroke();
}
}
// 5. Draw Vignette
if (vignetteLevel > 0) {
const centerX = w / 2;
const centerY = h / 2;
const R_outer = Math.sqrt(centerX * centerX + centerY * centerY);
const R_inner = Math.min(w, h) * (0.15 + Math.max(0, 1 - vignetteLevel) * 0.35); // Inner clear area inversely related to vignetteLevel strength
const gradient = ctx.createRadialGradient(centerX, centerY, R_inner, centerX, centerY, R_outer);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(0.7, `rgba(0,0,0,${vignetteLevel * 0.6})`);
gradient.addColorStop(1, `rgba(0,0,0,${vignetteLevel})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
}
// 6. Draw Custom Text
if (customText && String(customText).trim() !== "") {
ctx.font = textFont;
ctx.fillStyle = textColor;
const padding = Math.max(10, Math.min(w, h) * 0.04);
let x, y;
const normalizedPlacement = String(textPlacement).toLowerCase();
switch (normalizedPlacement) {
case "topleft":
ctx.textAlign = "left";
ctx.textBaseline = "top";
x = padding;
y = padding;
break;
case "topright":
ctx.textAlign = "right";
ctx.textBaseline = "top";
x = w - padding;
y = padding;
break;
case "bottomleft":
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
x = padding;
y = h - padding;
break;
case "center":
ctx.textAlign = "center";
ctx.textBaseline = "middle";
x = w / 2;
y = h / 2;
break;
case "bottomright":
default:
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
x = w - padding;
y = h - padding;
break;
}
ctx.fillText(String(customText), x, y);
}
return canvas;
}
Apply Changes