You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
aspectRatio = 0, // Target aspect ratio (e.g., 2.35 for 2.35:1). 0 or less for original.
saturation = 0.8, // Saturation level (0=grayscale, 1=original, >1=oversaturated). Default: 0.8 (slight desaturation).
contrast = 15, // Contrast adjustment (-100 to 100). Default: 15 (slight increase).
tintColor = '#1a2a3a',// CSS color string for tint (e.g., '#FFD700' for warm, '#2a3a4a' for cool). Empty or null for no tint.
tintOpacity = 0.15, // Opacity of the tint (0 to 1). Default: 0.15.
vignetteStrength = 0.5,// Strength of the vignette (0 to 1). Default: 0.5.
vignetteSoftness = 0.5,// Softness of the vignette (0=hard edge, 1=very soft fading from center). Default: 0.5.
grainStrength = 0.03 // Strength of film grain (0 to 1). Default: 0.03 (subtle).
) {
// Ensure valid image dimensions
if (!originalImg || originalImg.width === 0 || originalImg.height === 0) {
console.error("Invalid image provided to processImage.");
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1; emptyCanvas.height = 1; // Minimal canvas
return emptyCanvas;
}
const finalCanvas = document.createElement('canvas');
const finalCtx = finalCanvas.getContext('2d');
const oAR = originalImg.width / originalImg.height; // Original aspect ratio
let finalCanvasWidth, finalCanvasHeight;
// Determine final canvas dimensions based on target aspect ratio
if (aspectRatio > 0 && Math.abs(oAR - aspectRatio) > 0.001) { // Apply new aspect ratio
finalCanvasWidth = originalImg.width; // Anchor width
finalCanvasHeight = Math.round(finalCanvasWidth / aspectRatio);
// Sanity check: if maintaining originalImg.width makes the image content need upscaling in height to fit
// (e.g. original is very wide panorama, target AR is tall), it's better to anchor by originalImg.height.
// This scenario is less common for typical photos/ cinematic aspect ratio changes.
// The current logic prioritizes using the original image's width.
} else { // Use original aspect ratio
finalCanvasWidth = originalImg.width;
finalCanvasHeight = originalImg.height;
}
finalCanvas.width = finalCanvasWidth;
finalCanvas.height = finalCanvasHeight;
// Calculate drawing parameters to fit originalImg into finalCanvas, maintaining aspect ratio
const canvasAR = finalCanvas.width / finalCanvas.height;
let drawX = 0, drawY = 0, drawWidth = finalCanvas.width, drawHeight = finalCanvas.height;
if (oAR > canvasAR) { // Original is wider than final canvas (pillarbox needed)
drawHeight = finalCanvas.height;
drawWidth = drawHeight * oAR;
drawX = (finalCanvas.width - drawWidth) / 2;
} else if (oAR < canvasAR) { // Original is taller than final canvas (letterbox needed)
drawWidth = finalCanvas.width;
drawHeight = drawWidth / oAR;
drawY = (finalCanvas.height - drawHeight) / 2;
}
// If oAR == canvasAR, image perfectly fits; drawX, drawY remain 0.
// Create a temporary canvas to hold the scaled image content for processing
const tempImageCanvas = document.createElement('canvas');
tempImageCanvas.width = Math.abs(drawWidth); // Use Math.abs in case of rounding issues causing tiny negatives
tempImageCanvas.height = Math.abs(drawHeight);
const tempImageCtx = tempImageCanvas.getContext('2d');
tempImageCtx.drawImage(originalImg, 0, 0, originalImg.width, originalImg.height, 0, 0, tempImageCanvas.width, tempImageCanvas.height);
// --- Apply pixel-level adjustments (Saturation, Contrast) ---
if (saturation !== 1.0 || contrast !== 0) {
let imgData = tempImageCtx.getImageData(0, 0, tempImageCanvas.width, tempImageCanvas.height);
let pixels = imgData.data;
const len = pixels.length;
const contrastFactor = (259 * (contrast + 255)) / (255 * (259 - contrast));
for (let i = 0; i < len; i += 4) {
if (pixels[i + 3] === 0) continue; // Skip fully transparent pixels
let r = pixels[i];
let g = pixels[i + 1];
let b = pixels[i + 2];
// Saturation
if (saturation !== 1.0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = gray + saturation * (r - gray);
g = gray + saturation * (g - gray);
b = gray + saturation * (b - gray);
}
// Contrast
if (contrast !== 0) {
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
}
pixels[i] = Math.max(0, Math.min(255, r));
pixels[i + 1] = Math.max(0, Math.min(255, g));
pixels[i + 2] = Math.max(0, Math.min(255, b));
}
tempImageCtx.putImageData(imgData, 0, 0);
}
// Draw the (black) background for letterbox/pillarbox bars on final canvas
finalCtx.fillStyle = 'black';
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
// Draw the processed image content onto the final canvas
finalCtx.drawImage(tempImageCanvas, drawX, drawY, drawWidth, drawHeight);
// Determine the actual rectangle where the image content is visible on the final canvas
const imgRectOnCanvas = {
x: Math.max(0, drawX),
y: Math.max(0, drawY),
width: finalCanvas.width - 2 * Math.max(0, drawX), // If pillarboxed, drawX is negative
height: finalCanvas.height - 2 * Math.max(0, drawY) // If letterboxed, drawY is negative
};
// More robust calculation for imgRectOnCanvas, considering the image content within the final canvas bounds
if (drawX < 0) { // Pillarboxed (image content is wider than canvas at drawY)
imgRectOnCanvas.x = 0;
imgRectOnCanvas.width = finalCanvas.width;
}
if (drawY < 0) { // Letterboxed (image content is taller than canvas at drawX)
imgRectOnCanvas.y = 0;
imgRectOnCanvas.height = finalCanvas.height;
}
// --- Apply Tint ---
if (tintColor && typeof tintColor === 'string' && tintColor.trim() !== "" && tintOpacity > 0) {
finalCtx.globalAlpha = tintOpacity;
finalCtx.fillStyle = tintColor;
finalCtx.globalCompositeOperation = 'overlay'; // 'color' or 'overlay' or 'multiply' give different feels
finalCtx.fillRect(imgRectOnCanvas.x, imgRectOnCanvas.y, imgRectOnCanvas.width, imgRectOnCanvas.height);
finalCtx.globalAlpha = 1.0;
finalCtx.globalCompositeOperation = 'source-over'; // Reset
}
// --- Apply Film Grain ---
if (grainStrength > 0 && grainStrength <= 1) {
let contentForGrainData = finalCtx.getImageData(imgRectOnCanvas.x, imgRectOnCanvas.y, imgRectOnCanvas.width, imgRectOnCanvas.height);
let grainPixels = contentForGrainData.data;
const grainPixelsLen = grainPixels.length;
const grainIntensity = 50; // Max pixel value change for grain, scaled by grainStrength
for (let i = 0; i < grainPixelsLen; i += 4) {
if (grainPixels[i + 3] === 0) continue; // Skip transparent pixels
const noise = (Math.random() - 0.5) * 2 * grainIntensity * grainStrength;
grainPixels[i] = Math.max(0, Math.min(255, grainPixels[i] + noise));
grainPixels[i + 1] = Math.max(0, Math.min(255, grainPixels[i + 1] + noise));
grainPixels[i + 2] = Math.max(0, Math.min(255, grainPixels[i + 2] + noise));
}
finalCtx.putImageData(contentForGrainData, imgRectOnCanvas.x, imgRectOnCanvas.y);
}
// --- Apply Vignette (applied last, over everything else) ---
if (vignetteStrength > 0) {
const centerX = finalCanvas.width / 2;
const centerY = finalCanvas.height / 2;
const outerRadius = Math.sqrt(centerX * centerX + centerY * centerY);
// vignetteSoftness: 0=hard edge at outerRadius, 1=gradient starts from center
const innerRadius = outerRadius * Math.max(0, Math.min(1, 1 - vignetteSoftness));
const grad = finalCtx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
grad.addColorStop(0, 'rgba(0,0,0,0)'); // Transparent center
grad.addColorStop(1, `rgba(0,0,0,${vignetteStrength})`); // Dark edges
finalCtx.fillStyle = grad;
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
}
return finalCanvas;
}
Apply Changes