You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
sepia = 0.7, // number (0-1): Strength of sepia filter. 0 for no sepia.
desaturate = 0.3, // number (0-1): Amount of desaturation. 0 for original saturation.
vignetteStrength = 0.6, // number (0-1): Strength of the vignette effect. 0 for no vignette.
vignetteColor = "40,25,10", // string "r,g,b": Color of the vignette (e.g., "0,0,0" for black).
noiseAmount = 0.08, // number (0-1): Amount of noise. Higher values add more visible noise.
paperBaseR = 245, // number (0-255): Red component for paper base color tint.
paperBaseG = 240, // number (0-255): Green component for paper base color tint.
paperBaseB = 220, // number (0-255): Blue component for paper base color tint.
paperTintAlpha = 0.15, // number (0-1): Alpha transparency for the paper tint overlay.
numFolds = 4, // number (integer): Number of major fold lines to simulate.
foldOpacity = 0.2 // number (0-1): Opacity of the fold lines.
) {
// Helper function for clamping values between a min and max
function clamp(value, min, max) {
return Math.max(min, Math.min(value, max));
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Check if the originalImg is a valid, loaded image
if (!originalImg || typeof originalImg.naturalWidth === 'undefined' || !originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.warn("Image Expedition Map Creator: Provided image is not loaded, has no dimensions, or is not a valid image element. Returning a placeholder.");
canvas.width = 300; // Default placeholder size
canvas.height = 200;
ctx.fillStyle = "#EEEEEE"; // Light gray background for placeholder
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black"; // Text color
ctx.textAlign = "center";
ctx.font = "16px Arial";
ctx.fillText("Image not loaded or invalid", canvas.width / 2, canvas.height / 2);
return canvas; // Return the placeholder canvas
}
// Set canvas dimensions to match the original image
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
// 1. Draw the original image onto the canvas
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// 2. Apply paper tint overlay (if alpha > 0)
if (paperTintAlpha > 0) {
ctx.fillStyle = `rgba(${paperBaseR}, ${paperBaseG}, ${paperBaseB}, ${paperTintAlpha})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Get image data for pixel manipulation
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const numPixels = data.length;
// 3. Pixel manipulations: Desaturation, Sepia, Noise
for (let i = 0; i < numPixels; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// 3a. Desaturation (if desaturate > 0)
if (desaturate > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Luminance calculation
r = r * (1 - desaturate) + gray * desaturate;
g = g * (1 - desaturate) + gray * desaturate;
b = b * (1 - desaturate) + gray * desaturate;
}
// 3b. Sepia (if sepia > 0)
// Applied to potentially desaturated colors
if (sepia > 0) {
const sr = r;
const sg = g;
const sb = b;
// Standard sepia color transformation values
const tr = 0.393 * sr + 0.769 * sg + 0.189 * sb;
const tg = 0.349 * sr + 0.686 * sg + 0.168 * sb;
const tb = 0.272 * sr + 0.534 * sg + 0.131 * sb;
// Interpolate between original (or desaturated) and sepia based on sepia strength
r = sr * (1 - sepia) + tr * sepia;
g = sg * (1 - sepia) + tg * sepia;
b = sb * (1 - sepia) + tb * sepia;
}
// 3c. Noise (if noiseAmount > 0)
if (noiseAmount > 0) {
// Generate bipolar noise (can be positive or negative offset)
// Scaled by 100: noiseAmount=0.1 results in noise up to +/-10
const noiseVal = (Math.random() - 0.5) * 100 * noiseAmount;
r += noiseVal;
g += noiseVal;
b += noiseVal;
}
// Assign clamped values back to pixel data
data[i] = clamp(r, 0, 255);
data[i+1] = clamp(g, 0, 255);
data[i+2] = clamp(b, 0, 255);
// Alpha (data[i+3]) remains unchanged
}
// Put the modified pixel data back onto the canvas
ctx.putImageData(imageData, 0, 0);
// 4. Draw Folds (if numFolds > 0)
if (numFolds > 0) {
ctx.strokeStyle = `rgba(0, 0, 0, ${foldOpacity})`;
// Make fold line width adaptive to image size, but within reasonable limits
ctx.lineWidth = clamp(Math.min(canvas.width, canvas.height) * 0.002, 0.5, 1.5);
for (let i = 0; i < numFolds; i++) {
ctx.beginPath();
const isHorizontal = Math.random() > 0.5; // Randomly choose fold orientation
// Jitter adaptive to image size, capped to prevent excessive distortion
const jitter = clamp(Math.min(canvas.width, canvas.height) * 0.03, 5, 20);
if (isHorizontal) {
const yBase = (Math.random() * 0.8 + 0.1) * canvas.height; // Base y-position, avoiding extreme edges
const yStart = clamp(yBase + (Math.random() - 0.5) * jitter, 0, canvas.height);
const yEnd = clamp(yBase + (Math.random() - 0.5) * jitter, 0, canvas.height);
ctx.moveTo(0, yStart);
ctx.lineTo(canvas.width, yEnd);
} else { // Vertical fold
const xBase = (Math.random() * 0.8 + 0.1) * canvas.width; // Base x-position
const xStart = clamp(xBase + (Math.random() - 0.5) * jitter, 0, canvas.width);
const xEnd = clamp(xBase + (Math.random() - 0.5) * jitter, 0, canvas.width);
ctx.moveTo(xStart, 0);
ctx.lineTo(xEnd, canvas.height);
}
ctx.stroke(); // Draw the fold line
}
}
// 5. Vignette Effect (if vignetteStrength > 0)
if (vignetteStrength > 0) {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Outer radius ensures vignette covers the entire canvas from the center
const outerRadius = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2));
// Inner radius is relative to the smallest dimension for a proportional clear area
const innerRadius = Math.min(canvas.width, canvas.height) * 0.2; // Smaller inner solid area
const gradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
// Parse vignetteColor string "r,g,b" into numbers
const [vr, vg, vb] = vignetteColor.split(',').map(s => parseInt(s.trim(), 10));
// Define gradient stops for the vignette
gradient.addColorStop(0, `rgba(${vr},${vg},${vb},0)`); // Center is fully transparent
gradient.addColorStop(0.7, `rgba(${vr},${vg},${vb},${vignetteStrength * 0.6})`); // Mid-point of fade
gradient.addColorStop(1, `rgba(${vr},${vg},${vb},${vignetteStrength})`); // Edge at full strength
ctx.fillStyle = gradient;
// 'multiply' blending mode darkens the underlying image, good for old/burnt effects
ctx.globalCompositeOperation = 'multiply';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-over'; // Reset blending mode to default
}
return canvas; // Return the processed canvas
}
Apply Changes