You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
numFrames = 3,
filmColor = "black",
sprocketHoleColor = "white",
photoBorderColor = "#333333",
photoBorderWidth = 2,
photoTargetHeight = 150,
photoPadding = 5,
sprocketAreaHeight = 25,
frameGap = 5,
otherFramesDimFactor = "0.5" // String "0.0" to "1.0"
) {
// Helper function to draw rounded rectangles (for sprocket holes)
// Uses arcTo for robust rounded corners
function drawRoundedRect(ctx, x, y, width, height, radius) {
if (width < 2 * radius) radius = width / 2;
if (height < 2 * radius) radius = height / 2;
if (radius < 0) radius = 0;
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
}
// Validate input image
if (!originalImg || !originalImg.naturalWidth || !originalImg.naturalHeight) {
const errorCanvas = document.createElement('canvas');
const errorWidth = Math.max(200, Number(photoTargetHeight) * 1.5 || 200);
const errorHeight = Math.max(100, Number(photoTargetHeight) || 100);
errorCanvas.width = errorWidth;
errorCanvas.height = errorHeight;
const errCtx = errorCanvas.getContext('2d');
errCtx.fillStyle = 'lightgray';
errCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
errCtx.fillStyle = 'red';
errCtx.font = '14px Arial';
errCtx.textAlign = 'center';
errCtx.textBaseline = 'middle';
errCtx.fillText('Image not loaded or invalid.', errorCanvas.width / 2, errorCanvas.height / 2);
return errorCanvas;
}
// Sanitize numeric parameters
const actualNumFrames = Math.max(1, Number(numFrames));
const actualPhotoBorderWidth = Math.max(0, Number(photoBorderWidth));
const actualPhotoTargetHeight = Math.max(10, Number(photoTargetHeight));
const actualPhotoPadding = Math.max(0, Number(photoPadding));
const actualSprocketAreaHeight = Math.max(5, Number(sprocketAreaHeight));
const actualFrameGap = Math.max(0, Number(frameGap));
let dimFactor = parseFloat(otherFramesDimFactor);
if (isNaN(dimFactor)) {
dimFactor = 0.5;
}
dimFactor = Math.max(0, Math.min(1, dimFactor));
// 1. Scale photo based on target height
const aspectRatio = originalImg.naturalWidth / originalImg.naturalHeight;
const scaledPhotoHeight = actualPhotoTargetHeight;
const scaledPhotoWidth = scaledPhotoHeight * aspectRatio;
// 2. Calculate dimensions for the "photo box" (photo + its border)
const photoBoxWidth = scaledPhotoWidth + 2 * actualPhotoBorderWidth;
const photoBoxHeight = scaledPhotoHeight + 2 * actualPhotoBorderWidth;
// 3. Calculate dimensions for the content area of a single film frame (photo box + padding around it)
const frameContentWidth = photoBoxWidth + 2 * actualPhotoPadding;
const frameContentHeight = photoBoxHeight + 2 * actualPhotoPadding;
// 4. Calculate full dimensions for a single film frame (including top/bottom sprocket areas)
const singleFrameWidth = frameContentWidth;
const singleFrameHeight = frameContentHeight + 2 * actualSprocketAreaHeight;
// 5. Calculate total canvas dimensions
const canvasWidth = (actualNumFrames * singleFrameWidth) + ((actualNumFrames > 1 ? actualNumFrames - 1 : 0) * actualFrameGap);
const canvasHeight = singleFrameHeight;
const canvas = document.createElement('canvas');
canvas.width = Math.max(1, canvasWidth); // Ensure canvas dimensions are at least 1x1
canvas.height = Math.max(1, canvasHeight);
const ctx = canvas.getContext('2d');
// Fallback for extremely small calculated dimensions
if (canvas.width <= 1 || canvas.height <= 1 ) {
canvas.width = Math.max(canvas.width, 50);
canvas.height = Math.max(canvas.height, 50);
ctx.fillStyle = 'lightpink'; // Use a different color for this specific error
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = 'black';
ctx.font = '10px Arial';
ctx.fillText('Calc. Error', 5, 15);
return canvas;
}
let currentXOffset = 0;
const centralFrameIndex = (actualNumFrames === 1) ? 0 : Math.floor(actualNumFrames / 2);
for (let i = 0; i < actualNumFrames; i++) {
const frameStartX = currentXOffset;
// Draw film background for this frame segment
ctx.fillStyle = filmColor;
ctx.fillRect(frameStartX, 0, singleFrameWidth, singleFrameHeight);
// Draw photo border area
const borderX = frameStartX + actualPhotoPadding;
const borderY = actualSprocketAreaHeight + actualPhotoPadding;
ctx.fillStyle = photoBorderColor;
ctx.fillRect(borderX, borderY, photoBoxWidth, photoBoxHeight);
// Draw the photo itself
const actualPhotoX = borderX + actualPhotoBorderWidth;
const actualPhotoY = borderY + actualPhotoBorderWidth;
const isCentralFrame = (i === centralFrameIndex);
if (actualNumFrames === 1 || isCentralFrame || dimFactor === 1.0) {
ctx.globalAlpha = 1.0;
} else {
ctx.globalAlpha = dimFactor;
}
// Enable image smoothing for better quality when scaling
ctx.imageSmoothingEnabled = true;
if (ctx.imageSmoothingQuality) { // Not supported in all older browsers
ctx.imageSmoothingQuality = "high";
}
ctx.drawImage(originalImg, actualPhotoX, actualPhotoY, scaledPhotoWidth, scaledPhotoHeight);
ctx.globalAlpha = 1.0; // Reset alpha after drawing
// Draw sprocket holes for this frame
const sprocketHoleMinHeight = 2; // Minimum height for a sprocket hole to be drawn
const sprocketHoleMinEffectiveAreaHeight = 5; // Minimum sprocket area height to attempt drawing holes
if (actualSprocketAreaHeight >= sprocketHoleMinEffectiveAreaHeight) {
let sprocketHoleHeight = actualSprocketAreaHeight * 0.6;
if (sprocketHoleHeight < sprocketHoleMinHeight) sprocketHoleHeight = 0; // Don't draw if too small
if (sprocketHoleHeight > 0) {
const sprocketHoleWidth = Math.max(1, sprocketHoleHeight * 0.8);
const sprocketHoleRadius = Math.min(sprocketHoleWidth / 2, sprocketHoleHeight / 2, Math.max(0,sprocketHoleWidth * 0.25));
// Calculate number of sprockets and their spacing
const numSprockets = Math.max(1, Math.floor(singleFrameWidth / (sprocketHoleWidth * 1.8))); // *1.8 for hole + typical gap
const totalSprocketWidthOccupied = numSprockets * sprocketHoleWidth;
const totalGapWidth = singleFrameWidth - totalSprocketWidthOccupied;
const sprocketSpacingX = totalGapWidth / (numSprockets + 1); // Distribute gaps evenly, including ends
const sprocketVerticalMargin = (actualSprocketAreaHeight - sprocketHoleHeight) / 2;
ctx.fillStyle = sprocketHoleColor;
for (let j = 0; j < numSprockets; j++) {
const sx = frameStartX + sprocketSpacingX + j * (sprocketHoleWidth + sprocketSpacingX);
// Check if sprocket is reasonably within the frame width boundaries before drawing
if (sx + sprocketHoleWidth > frameStartX && sx < frameStartX + singleFrameWidth) {
// Top sprockets
const syTop = sprocketVerticalMargin;
if (syTop >=0 && syTop + sprocketHoleHeight <= actualSprocketAreaHeight) {
drawRoundedRect(ctx, sx, syTop, sprocketHoleWidth, sprocketHoleHeight, sprocketHoleRadius);
ctx.fill();
}
// Bottom sprockets
const syBottom = singleFrameHeight - actualSprocketAreaHeight + sprocketVerticalMargin;
if (syBottom >= singleFrameHeight - actualSprocketAreaHeight && syBottom + sprocketHoleHeight <= singleFrameHeight) {
drawRoundedRect(ctx, sx, syBottom, sprocketHoleWidth, sprocketHoleHeight, sprocketHoleRadius);
ctx.fill();
}
}
}
}
}
currentXOffset += singleFrameWidth + actualFrameGap;
}
return canvas;
}
Apply Changes