You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
outputSize = 600,
borderPercent = 4,
borderStyle = "square", // "square" or "rounded"
borderColor = "#0A0A0A",
strength = 0.7, // Overall effect strength (0-1)
vignetteAmount = 0.7, // Base vignette intensity (0-1)
grainAmount = 0.3, // Base grain intensity (0-1)
sepiaAmount = 0.2, // Base sepia intensity (0-1)
saturationValue = 0.8, // Saturation level (0-2, 1 is original)
blurAmount = 0.3 // Base blur intensity (0-1, translates to small px value)
) {
// 1. Calculate dimensions
const finalCanvas = document.createElement('canvas');
finalCanvas.width = outputSize;
finalCanvas.height = outputSize;
const finalCtx = finalCanvas.getContext('2d');
const borderPx = Math.max(0, Math.floor(outputSize * (borderPercent / 100)));
const contentAreaX = borderPx;
const contentAreaY = borderPx;
const contentAreaWidth = Math.max(1, outputSize - 2 * borderPx);
const contentAreaHeight = Math.max(1, outputSize - 2 * borderPx);
// 2. Create contentCanvas & draw original image scaled into it
const contentCanvas = document.createElement('canvas');
contentCanvas.width = contentAreaWidth;
contentCanvas.height = contentAreaHeight;
const contentCtx = contentCanvas.getContext('2d');
const imgAR = originalImg.width / originalImg.height;
const contentAR = contentAreaWidth / contentAreaHeight;
let sImageW, sImageH, sImageX, sImageY; // Scaled image W, H, X, Y in contentCanvas
if (imgAR > contentAR) { // Image is wider than content area (letterbox top/bottom)
sImageW = contentAreaWidth;
sImageH = sImageW / imgAR;
sImageX = 0;
sImageY = (contentAreaHeight - sImageH) / 2;
} else { // Image is taller or same aspect as content area (pillarbox left/right)
sImageH = contentAreaHeight;
sImageW = sImageH * imgAR;
sImageY = 0;
sImageX = (contentAreaWidth - sImageW) / 2;
}
// Fill letterbox/pillarbox areas with black
contentCtx.fillStyle = 'black';
contentCtx.fillRect(0, 0, contentCanvas.width, contentCanvas.height);
contentCtx.drawImage(originalImg, sImageX, sImageY, sImageW, sImageH);
// 3. Create effectsCanvas for applying filters
const effectsCanvas = document.createElement('canvas');
effectsCanvas.width = contentAreaWidth;
effectsCanvas.height = contentAreaHeight;
const effectsCtx = effectsCanvas.getContext('2d');
effectsCtx.drawImage(contentCanvas, 0, 0); // Start with the scaled image
// 4. Apply image filters (Sepia, Saturation, Brightness, Contrast, Blur)
// These filters apply to the source drawn, so we draw effectsCanvas onto itself (via temp)
const tempFilterLayerCanvas = document.createElement('canvas');
tempFilterLayerCanvas.width = effectsCanvas.width;
tempFilterLayerCanvas.height = effectsCanvas.height;
const tempFilterLayerCtx = tempFilterLayerCanvas.getContext('2d');
tempFilterLayerCtx.drawImage(effectsCanvas, 0, 0); // Store current state of effectsCanvas
let filterArray = [];
const currentStrength = Math.max(0, Math.min(1, strength)); // Clamp strength 0-1
if (sepiaAmount * currentStrength > 0.01) {
filterArray.push(`sepia(${sepiaAmount * currentStrength})`);
}
if (Math.abs(saturationValue - 1) > 0.01) {
filterArray.push(`saturate(${saturationValue})`);
}
// Subtle brightness/contrast adjustments typical of film
let effectiveBrightness = 1 - (0.1 * currentStrength);
let effectiveContrast = 1 + (0.1 * currentStrength);
filterArray.push(`brightness(${effectiveBrightness.toFixed(3)})`);
filterArray.push(`contrast(${effectiveContrast.toFixed(3)})`);
if (blurAmount * currentStrength > 0.01) {
let blurPx = (blurAmount * currentStrength * 1.5).toFixed(2); // Max ~1.5px blur
if (parseFloat(blurPx) > 0.05) { // Avoid sub-pixel blur that does nothing
filterArray.push(`blur(${blurPx}px)`);
}
}
if (filterArray.length > 0) {
effectsCtx.filter = filterArray.join(' ');
effectsCtx.clearRect(0, 0, effectsCanvas.width, effectsCanvas.height);
effectsCtx.drawImage(tempFilterLayerCanvas, 0, 0); // Draw with all filters applied
effectsCtx.filter = 'none'; // Reset filter for subsequent operations
}
// 5. Apply Grain
if (grainAmount * currentStrength > 0.01) {
const grainPatternCanvas = document.createElement('canvas'); // Create a small canvas for grain pattern
const grainPatternSize = 80;
grainPatternCanvas.width = grainPatternSize;
grainPatternCanvas.height = grainPatternSize;
const gpCtx = grainPatternCanvas.getContext('2d');
const gpId = gpCtx.createImageData(grainPatternSize, grainPatternSize);
const gpData = gpId.data;
const noiseIntensity = grainAmount * currentStrength * 70; // Controls the +/- range of pixel noise
const grainSpotAlpha = (grainAmount * currentStrength * 0.4 + 0.1) * 255; // Alpha of individual grain spots
for (let i = 0; i < gpData.length; i += 4) {
const noiseVal = (Math.random() - 0.5) * noiseIntensity;
gpData[i] = 128 + noiseVal; // R
gpData[i+1] = 128 + noiseVal; // G
gpData[i+2] = 128 + noiseVal; // B
gpData[i+3] = Math.random() * grainSpotAlpha; // Alpha - some spots more/less visible
}
gpCtx.putImageData(gpId, 0, 0);
// Use a temporary canvas to hold the repeating grain pattern
tempFilterLayerCtx.clearRect(0, 0, tempFilterLayerCanvas.width, tempFilterLayerCanvas.height);
tempFilterLayerCtx.fillStyle = tempFilterLayerCtx.createPattern(grainPatternCanvas, 'repeat');
tempFilterLayerCtx.fillRect(0, 0, tempFilterLayerCanvas.width, tempFilterLayerCanvas.height);
// Blend the grain pattern onto the image
effectsCtx.globalAlpha = 0.2 + grainAmount * currentStrength * 0.3; // Overall opacity of grain layer
effectsCtx.globalCompositeOperation = 'overlay'; // 'soft-light' or 'hard-light' could also work
effectsCtx.drawImage(tempFilterLayerCanvas, 0, 0);
// Reset global settings
effectsCtx.globalAlpha = 1.0;
effectsCtx.globalCompositeOperation = 'source-over';
}
// 6. Apply Vignette
if (vignetteAmount * currentStrength > 0.01) {
const vigCenterX = effectsCanvas.width / 2;
const vigCenterY = effectsCanvas.height / 2;
const vigOuterRadius = Math.sqrt(Math.pow(vigCenterX, 2) + Math.pow(vigCenterY, 2));
const vigEffectStrength = vignetteAmount * currentStrength;
const vigInnerRadiusFactor = 1 - Math.min(0.95, vigEffectStrength); // Closer to 0 means vignette starts further from center
const vigInnerRadius = vigOuterRadius * vigInnerRadiusFactor;
const gradient = effectsCtx.createRadialGradient(
vigCenterX, vigCenterY, vigInnerRadius,
vigCenterX, vigCenterY, vigOuterRadius
);
const vigColorRgb = "20,15,10"; // Dark brownish base for vignette
let vigOpacity = Math.min(0.85, vigEffectStrength * 1.1); // Max opacity for vignette edge
gradient.addColorStop(0, `rgba(${vigColorRgb},0)`);
gradient.addColorStop(0.7, `rgba(${vigColorRgb},${(vigOpacity * 0.65).toFixed(2)})`);
gradient.addColorStop(1, `rgba(${vigColorRgb},${vigOpacity.toFixed(2)})`);
effectsCtx.fillStyle = gradient;
effectsCtx.fillRect(0, 0, effectsCanvas.width, effectsCanvas.height);
}
// 7. Draw to finalCanvas (background and processed image content)
finalCtx.fillStyle = borderColor;
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
if (borderStyle === "rounded" && borderPx > 0) { // Rounded corners only if there's a border
finalCtx.save();
const cornerRadius = Math.min(contentAreaWidth, contentAreaHeight) * 0.07; // 7% of content dimension for corner
finalCtx.beginPath();
finalCtx.moveTo(contentAreaX + cornerRadius, contentAreaY);
finalCtx.lineTo(contentAreaX + contentAreaWidth - cornerRadius, contentAreaY);
finalCtx.arcTo(contentAreaX + contentAreaWidth, contentAreaY, contentAreaX + contentAreaWidth, contentAreaY + cornerRadius, cornerRadius);
finalCtx.lineTo(contentAreaX + contentAreaWidth, contentAreaY + contentAreaHeight - cornerRadius);
finalCtx.arcTo(contentAreaX + contentAreaWidth, contentAreaY + contentAreaHeight, contentAreaX + contentAreaWidth - cornerRadius, contentAreaY + contentAreaHeight, cornerRadius);
finalCtx.lineTo(contentAreaX + cornerRadius, contentAreaY + contentAreaHeight);
finalCtx.arcTo(contentAreaX, contentAreaY + contentAreaHeight, contentAreaX, contentAreaY + contentAreaHeight - cornerRadius, cornerRadius);
finalCtx.lineTo(contentAreaX, contentAreaY + cornerRadius);
finalCtx.arcTo(contentAreaX, contentAreaY, contentAreaX + cornerRadius, contentAreaY, cornerRadius);
finalCtx.closePath();
finalCtx.clip();
}
finalCtx.drawImage(effectsCanvas, contentAreaX, contentAreaY, contentAreaWidth, contentAreaHeight);
if (borderStyle === "rounded" && borderPx > 0) {
finalCtx.restore(); // Remove clipping path
}
return finalCanvas;
}
Apply Changes