You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
eachFrameMaxHeight = 400,
borderColor = "black",
borderWidth = 15,
cornerRadiusFraction = 0.05,
grainAmount = 0.08,
sepiaAmount = 0.25,
saturationAmount = 0.9,
vignetteStrength = 0.3
) {
// Helper function for creating a rounded rectangle path
function createRoundedRectPath(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.arcTo(x + width, y, x + width, y + radius, radius);
ctx.lineTo(x + width, y + height - radius);
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
ctx.lineTo(x + radius, y + height);
ctx.arcTo(x, y + height, x, y + height - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
}
// Ensure originalImg is valid and has dimensions
if (!originalImg || typeof originalImg.width !== 'number' || typeof originalImg.height !== 'number' || originalImg.width === 0 || originalImg.height === 0) {
console.error("Original image is invalid or has zero dimensions.");
// Create a small canvas indicating an error
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 200;
errorCanvas.height = 100;
const errCtx = errorCanvas.getContext('2d');
if (errCtx) { // Check if context was obtainable (e.g. not in Node.js without canvas lib)
errCtx.fillStyle = 'rgb(200, 200, 200)';
errCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
errCtx.fillStyle = 'red';
errCtx.textAlign = 'center';
errCtx.textBaseline = 'middle';
errCtx.fillText("Error: Invalid Image Input", errorCanvas.width / 2, errorCanvas.height / 2);
}
return errorCanvas;
}
// 1. Calculate frame content dimensions
// Use originalImg.height as basis for scaling, capped by eachFrameMaxHeight
const frameContentRenderHeight = Math.max(1, Math.min(originalImg.height, eachFrameMaxHeight));
const halfFrameAspectRatio = 3 / 4; // Width / Height for typical portrait half-frame
const frameContentRenderWidth = Math.max(1, frameContentRenderHeight * halfFrameAspectRatio);
// 2. Calculate overall canvas dimensions
const canvas = document.createElement('canvas');
// Total width: 2 frames + 3 borders (left, middle, right)
canvas.width = Math.max(1, (2 * frameContentRenderWidth) + (3 * borderWidth));
// Total height: 1 frame height + 2 borders (top, bottom)
canvas.height = Math.max(1, frameContentRenderHeight + (2 * borderWidth));
const ctx = canvas.getContext('2d');
if (!ctx) { // Fallback if canvas context cannot be created
console.error("Could not get 2D context from canvas");
return document.createElement('div'); // Return empty div or throw error
}
// 3. Fill background with borderColor
ctx.fillStyle = borderColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 4. Calculate source image crop ('cover' style) for the 3:4 aspect ratio frames
let sx, sy, sWidth, sHeight;
const originalImgAspectRatio = originalImg.width / originalImg.height;
if (originalImgAspectRatio > halfFrameAspectRatio) {
// Image is wider than frame aspect ratio: fit height, crop width
sHeight = originalImg.height;
sWidth = originalImg.height * halfFrameAspectRatio;
sx = (originalImg.width - sWidth) / 2;
sy = 0;
} else {
// Image is taller or same aspect ratio as frame: fit width, crop height
sWidth = originalImg.width;
sHeight = originalImg.width / halfFrameAspectRatio;
sx = 0;
sy = (originalImg.height - sHeight) / 2;
}
// Ensure sx, sy are non-negative and sWidth, sHeight are positive and within source image bounds
sx = Math.max(0, sx);
sy = Math.max(0, sy);
sWidth = Math.max(1, Math.min(sWidth, originalImg.width - sx));
sHeight = Math.max(1, Math.min(sHeight, originalImg.height - sy));
// 5. Define draw positions (destinations) for the two frames' content on the canvas
const frameDestinations = [
{ x: borderWidth, y: borderWidth, w: frameContentRenderWidth, h: frameContentRenderHeight },
{ x: borderWidth + frameContentRenderWidth + borderWidth, y: borderWidth, w: frameContentRenderWidth, h: frameContentRenderHeight }
];
const actualCornerRadius = Math.max(0, frameContentRenderWidth * cornerRadiusFraction);
// 6. Process and draw each frame
for (const dest of frameDestinations) {
if (dest.w <= 0 || dest.h <= 0) continue; // Skip if frame dimension is zero/negative
ctx.save();
// Create rounded rectangle clipping path
createRoundedRectPath(ctx, dest.x, dest.y, dest.w, dest.h, actualCornerRadius);
ctx.clip();
// Apply color filters (sepia, saturation)
let filterString = "";
if (sepiaAmount > 0 && sepiaAmount <=1) filterString += `sepia(${sepiaAmount}) `;
if (saturationAmount >=0 ) filterString += `saturate(${saturationAmount}) `;
if (filterString.trim() !== "") {
ctx.filter = filterString.trim();
}
// Draw the (cropped part of the) original image into the frame
try {
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, dest.x, dest.y, dest.w, dest.h);
} catch (e) {
console.error("Error drawing image:", e);
// Could draw an error message within the frame
ctx.fillStyle = "red";
ctx.fillRect(dest.x, dest.y, dest.w, dest.h);
ctx.restore(); // Restore before next iteration or returning
continue; // Skip further processing for this frame
}
ctx.filter = 'none'; // Reset filter before applying grain or vignette manually
// Apply film grain
if (grainAmount > 0 && grainAmount <=1) {
try {
const frameImageData = ctx.getImageData(dest.x, dest.y, dest.w, dest.h);
const data = frameImageData.data;
const dLength = data.length;
for (let i = 0; i < dLength; i += 4) {
if (data[i + 3] === 0) continue; // Skip fully transparent pixels
const grainVal = (Math.random() - 0.5) * 255 * grainAmount;
data[i] = Math.max(0, Math.min(255, data[i] + grainVal));
data[i+1] = Math.max(0, Math.min(255, data[i+1] + grainVal));
data[i+2] = Math.max(0, Math.min(255, data[i+2] + grainVal));
}
ctx.putImageData(frameImageData, dest.x, dest.y);
} catch (e) {
console.warn("Could not apply grain (possibly due to tainted canvas from cross-origin image without CORS):", e);
}
}
// Apply vignette
if (vignetteStrength > 0 && vignetteStrength <= 1) {
const centerX = dest.x + dest.w / 2;
const centerY = dest.y + dest.h / 2;
const outerEffectiveRadius = Math.sqrt(Math.pow(dest.w / 2, 2) + Math.pow(dest.h / 2, 2));
const gradient = ctx.createRadialGradient(
centerX, centerY, outerEffectiveRadius * Math.max(0, (1 - vignetteStrength)), // Inner circle (transparent part)
centerX, centerY, outerEffectiveRadius // Outer circle (edge of vignette)
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, `rgba(0,0,0,${vignetteStrength})`);
ctx.fillStyle = gradient;
ctx.fillRect(dest.x, dest.y, dest.w, dest.h);
}
ctx.restore(); // Remove clipping path, restore filters and other context states
}
return canvas;
}
Apply Changes