You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
numPhotos = 4,
photoDisplaySize = 150, // The size of the image content area in pixels
frameBorderColor = '#FFFFFF',
frameBorderWidth = 10, // Border width around each photo in pixels
frameCornerRadius = 8, // Outer corner radius of the photo frame in pixels
spacingBetweenFrames = 10, // Space between photo frames in pixels
stripBackgroundColor = '#222222',
stripOverallPadding = 15, // Padding around the entire strip content in pixels
vintageEffect = 'sepia' // 'none', 'grayscale', 'sepia'
) {
// Helper function to create a rounded rectangle path
function createRoundedRectPath(ctx, x, y, width, height, radius) {
// Clamp radius to be valid for the given width/height
if (typeof radius === 'undefined') radius = 0;
// Ensure radius is not more than half the shortest side
let r = Math.min(radius, width / 2, height / 2);
// Ensure radius is non-negative
if (r < 0) r = 0;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + width - r, y); // Top edge
ctx.quadraticCurveTo(x + width, y, x + width, y + r); // Top-right corner
ctx.lineTo(x + width, y + height - r); // Right edge
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); // Bottom-right corner
ctx.lineTo(x + r, y + height); // Bottom edge
ctx.quadraticCurveTo(x, y + height, x, y + height - r); // Bottom-left corner
ctx.lineTo(x, y + r); // Left edge
ctx.quadraticCurveTo(x, y, x + r, y); // Top-left corner
ctx.closePath();
}
// --- Parameter Sanitization ---
numPhotos = parseInt(String(numPhotos), 10);
if (isNaN(numPhotos) || numPhotos <= 0) numPhotos = 4;
photoDisplaySize = parseFloat(String(photoDisplaySize));
if (isNaN(photoDisplaySize) || photoDisplaySize <= 0) photoDisplaySize = 150;
frameBorderWidth = parseFloat(String(frameBorderWidth));
if (isNaN(frameBorderWidth) || frameBorderWidth < 0) frameBorderWidth = 10;
frameCornerRadius = parseFloat(String(frameCornerRadius));
if (isNaN(frameCornerRadius) || frameCornerRadius < 0) frameCornerRadius = 8;
spacingBetweenFrames = parseFloat(String(spacingBetweenFrames));
if (isNaN(spacingBetweenFrames) || spacingBetweenFrames < 0) spacingBetweenFrames = 10;
stripOverallPadding = parseFloat(String(stripOverallPadding));
if (isNaN(stripOverallPadding) || stripOverallPadding < 0) stripOverallPadding = 15;
frameBorderColor = String(frameBorderColor);
stripBackgroundColor = String(stripBackgroundColor);
vintageEffect = String(vintageEffect).toLowerCase(); // Normalize to lowercase
// --- Dimension Calculations ---
const frameOuterWidth = photoDisplaySize + 2 * frameBorderWidth;
const frameOuterHeight = photoDisplaySize + 2 * frameBorderWidth;
const canvasWidth = frameOuterWidth + 2 * stripOverallPadding;
const canvasHeight = (numPhotos * frameOuterHeight) + ((numPhotos - 1) * spacingBetweenFrames) + (2 * stripOverallPadding);
// --- Canvas Setup ---
const canvas = document.createElement('canvas');
canvas.width = Math.max(1, canvasWidth); // Ensure canvas dimensions are at least 1px
canvas.height = Math.max(1, canvasHeight);
const ctx = canvas.getContext('2d');
// 1. Fill strip background
ctx.fillStyle = stripBackgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// --- Draw each photo frame and image ---
for (let i = 0; i < numPhotos; i++) {
const currentFrameX = stripOverallPadding;
const currentFrameY = stripOverallPadding + i * (frameOuterHeight + spacingBetweenFrames);
// 2. Draw the frame's border (as a filled shape)
ctx.fillStyle = frameBorderColor;
createRoundedRectPath(ctx, currentFrameX, currentFrameY, frameOuterWidth, frameOuterHeight, frameCornerRadius);
ctx.fill();
// Calculate image destination properties
const imgDestX = currentFrameX + frameBorderWidth;
const imgDestY = currentFrameY + frameBorderWidth;
const imgDestWidth = photoDisplaySize;
const imgDestHeight = photoDisplaySize;
// Inner corner radius for the image clipping area
const innerCornerRadius = Math.max(0, frameCornerRadius - frameBorderWidth);
ctx.save(); // Save context state (incl. current filter, transform, etc.)
// 3. Create clipping path for the image content area
createRoundedRectPath(ctx, imgDestX, imgDestY, imgDestWidth, imgDestHeight, innerCornerRadius);
ctx.clip();
// 4. Apply vintage effect if specified (modifies current drawing state)
if (vintageEffect === 'grayscale') {
ctx.filter = 'grayscale(100%)';
} else if (vintageEffect === 'sepia') {
ctx.filter = 'sepia(100%)';
} // If 'none' or unrecognized, current GState filter (likely 'none') remains.
// 5. Draw the image (or fallback)
const imgSrc = originalImg;
let imageDrawnSuccessfully = false;
if (imgSrc && imgSrc.complete && imgSrc.naturalWidth > 0 && imgSrc.naturalHeight > 0) {
const imgAspect = imgSrc.naturalWidth / imgSrc.naturalHeight;
const destAspect = imgDestWidth / imgDestHeight; // Typically 1.0 for square photos
let sx = 0, sy = 0, sWidth = imgSrc.naturalWidth, sHeight = imgSrc.naturalHeight;
if (imgAspect > destAspect) { // Source image is wider than destination aspect ratio
sHeight = imgSrc.naturalHeight;
sWidth = sHeight * destAspect; // Crop width to fit destination aspect ratio
sx = (imgSrc.naturalWidth - sWidth) / 2; // Center horizontally
} else { // Source image is taller or has the same aspect ratio
sWidth = imgSrc.naturalWidth;
sHeight = sWidth / destAspect; // Crop height to fit destination aspect ratio
sy = (imgSrc.naturalHeight - sHeight) / 2; // Center vertically
}
// Ensure calculated source dimensions are valid
if (sWidth > 0 && sHeight > 0 &&
sx >= 0 && sy >= 0 &&
sx + sWidth <= imgSrc.naturalWidth + 0.0001 && // Add epsilon for float comparisons
sy + sHeight <= imgSrc.naturalHeight + 0.0001) {
ctx.drawImage(imgSrc, sx, sy, sWidth, sHeight, imgDestX, imgDestY, imgDestWidth, imgDestHeight);
imageDrawnSuccessfully = true;
}
}
if (!imageDrawnSuccessfully) {
// Fallback: Draw a placeholder if image is invalid, not loaded, or calculations failed
// This fallback will also be clipped by the rounded rectangle path.
// It should be drawn without the vintage filter.
let tempFilter = ctx.filter; // Store current filter (e.g., 'sepia(100%)')
ctx.filter = 'none'; // Temporarily disable filter for placeholder
ctx.fillStyle = '#CCCCCC'; // Light gray for placeholder background
ctx.fillRect(imgDestX, imgDestY, imgDestWidth, imgDestHeight);
ctx.fillStyle = '#555555'; // Darker gray for text
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const fontSize = Math.min(imgDestWidth, imgDestHeight) / 6; // Adjust font size based on photo size
ctx.font = `${fontSize}px Arial`; // Use a common font
ctx.fillText('Error', imgDestX + imgDestWidth / 2, imgDestY + imgDestHeight / 2);
ctx.filter = tempFilter; // Restore filter to what it was before drawing placeholder
}
ctx.restore(); // Restore context state: removes clip, and restores filter to its value at ctx.save()
}
return canvas;
}
Apply Changes