You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, grainIntensity = 20, vignetteIntensity = 0.6, warmth = 0.6, showSprocketsStr = "true", lightLeakOpacity = 0.2, blurAmountPx = 0.5) {
// Helper function to draw rounded rectangle Paths
function drawRoundRectPath(ctx, x, y, width, height, radiusInput) {
let radius = {tl: 0, tr: 0, br: 0, bl: 0};
if (typeof radiusInput === 'number') {
radius = {tl: radiusInput, tr: radiusInput, br: radiusInput, bl: radiusInput};
} else {
for (let side in radius) {
radius[side] = radiusInput[side] || radius[side];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
}
// Helper function to fill rounded rectangles
function fillRoundRect(ctx, x, y, width, height, radiusInput) {
drawRoundRectPath(ctx, x, y, width, height, radiusInput);
ctx.fill();
}
const _showSprockets = String(showSprocketsStr).toLowerCase() === "true";
// 1. Create an offscreen canvas for image processing (filters, grain)
const pCanvas = document.createElement('canvas');
pCanvas.width = originalImg.naturalWidth || originalImg.width;
pCanvas.height = originalImg.naturalHeight || originalImg.height;
const pCtx = pCanvas.getContext('2d');
// Clamp parameters to sensible ranges
warmth = Math.max(0, Math.min(1, warmth));
blurAmountPx = Math.max(0, blurAmountPx);
grainIntensity = Math.max(0, grainIntensity);
vignetteIntensity = Math.max(0, Math.min(1, vignetteIntensity));
lightLeakOpacity = Math.max(0, Math.min(1, lightLeakOpacity));
// Apply filters (warmth, blur, contrast, etc.)
let filterOps = [];
if (warmth > 0) { filterOps.push(`sepia(${warmth})`); }
filterOps.push(`contrast(${1 + warmth * 0.25})`); // Contrast increases slightly with warmth
filterOps.push(`brightness(${1 - warmth * 0.15})`); // Brightness decreases slightly
filterOps.push(`saturate(${Math.max(0.1, 1 - warmth * 0.7)})`); // Desaturate more significantly with warmth
if (warmth > 0.05) { filterOps.push(`hue-rotate(-${warmth * 20}deg)`); } // Shift to more orange/red
if (blurAmountPx > 0) { filterOps.push(`blur(${blurAmountPx}px)`); }
if (filterOps.length > 0) {
pCtx.filter = filterOps.join(' ');
}
pCtx.drawImage(originalImg, 0, 0, pCanvas.width, pCanvas.height);
pCtx.filter = 'none'; // Reset filter for subsequent operations like grain
// Apply grain
if (grainIntensity > 0) {
const imageData = pCtx.getImageData(0, 0, pCanvas.width, pCanvas.height);
const data = imageData.data;
const len = data.length;
// Scale grainIntensity: 0-50 maps to pixel adjustment range.
// A value of 20 means noise up to +/-20 on R,G,B.
const actualGrainAmount = grainIntensity;
for (let i = 0; i < len; i += 4) {
const noise = (Math.random() - 0.5) * actualGrainAmount;
data[i] = Math.max(0, Math.min(255, data[i] + noise));
data[i+1] = Math.max(0, Math.min(255, data[i+1] + noise));
data[i+2] = Math.max(0, Math.min(255, data[i+2] + noise));
}
pCtx.putImageData(imageData, 0, 0);
}
// 2. Create the output canvas with film strip appearance
const contentW = pCanvas.width;
const contentH = pCanvas.height;
// Define film strip padding and rounded corners for the image aperture effect
const filmStripPaddingHorizontal = Math.max(8, Math.min(contentW, contentH) * 0.025);
const filmStripPaddingVertical = Math.max(8, Math.min(contentW, contentH) * 0.025);
const cornerRadius = Math.max(5, Math.min(contentW, contentH) * 0.03);
// Define width for the sprocket holes area
const sprocketAreaWidth = _showSprockets ? Math.max(25, contentW * 0.10) : 0; // Sprocket area relative to content width
// Calculate final frame dimensions
const frameWidth = contentW + 2 * filmStripPaddingHorizontal + sprocketAreaWidth;
const frameHeight = contentH + 2 * filmStripPaddingVertical;
const outputCanvas = document.createElement('canvas');
outputCanvas.width = frameWidth;
outputCanvas.height = frameHeight;
const oCtx = outputCanvas.getContext('2d');
// Fill film base color (dark, slightly warm brown/grey)
oCtx.fillStyle = '#100C08'; // Darkest brown, almost black
oCtx.fillRect(0, 0, frameWidth, frameHeight);
// Calculate image content position within the frame
const imgX = (_showSprockets ? sprocketAreaWidth : 0) + filmStripPaddingHorizontal;
const imgY = filmStripPaddingVertical;
// Draw image content with rounded corners (aperture effect) and then apply vignette
oCtx.save();
drawRoundRectPath(oCtx, imgX, imgY, contentW, contentH, cornerRadius);
oCtx.clip(); // Clip to the rounded rectangle image area
oCtx.drawImage(pCanvas, imgX, imgY, contentW, contentH); // Draw the processed image
// Vignette (applied inside the clipped image area)
if (vignetteIntensity > 0) {
const gradCenterX = imgX + contentW / 2;
const gradCenterY = imgY + contentH / 2;
const gradOuterRadius = Math.sqrt(contentW*contentW + contentH*contentH) / 2 * 1.1; // Ensure vignette covers corners
const gradInnerRadiusFactor = 1 - Math.min(1, vignetteIntensity); // Inner clear area shrinks with intensity
const gradInnerRadius = gradOuterRadius * Math.max(0.1, gradInnerRadiusFactor * 0.9 + 0.1);
const vignetteGrad = oCtx.createRadialGradient(gradCenterX, gradCenterY, gradInnerRadius, gradCenterX, gradCenterY, gradOuterRadius);
const midStop = Math.max(0.1, 0.5 + (0.5 - vignetteIntensity * 0.5)); // Adjusted for smoother transition
vignetteGrad.addColorStop(0, 'rgba(5,0,0,0)'); // Slightly warm transparent center
vignetteGrad.addColorStop(midStop, `rgba(5,0,0,${vignetteIntensity * 0.6})`);
vignetteGrad.addColorStop(1, `rgba(5,0,0,${vignetteIntensity * 0.95})`); // Darker, slightly reddish vignette edges
oCtx.fillStyle = vignetteGrad;
oCtx.fillRect(imgX, imgY, contentW, contentH); // Fill over the image area
}
oCtx.restore(); // Remove clip, image and vignette are now on canvas
// Sprocket holes (if enabled)
if (_showSprockets) {
const spPaddingFromEdge = filmStripPaddingHorizontal * 0.1; // Small space from frame edge
const spAreaEffectiveX = spPaddingFromEdge;
const spAvailableWidth = sprocketAreaWidth - 2 * spPaddingFromEdge;
// Super 8 sprocket: ~0.91mm W x 1.22mm H. Aspect Ratio H/W ~ 1.34
let spHeight = contentH * 0.022;
let spWidth = spHeight / 1.34;
if (spWidth > spAvailableWidth * 0.85) { // Ensure sprocket fits
const scale = (spAvailableWidth * 0.85) / spWidth;
spWidth *= scale;
spHeight *= scale;
}
const spRadius = spWidth * 0.25; // Roundedness of sprockets
const spGapFactor = 2.8; // Spacing factor: 2.8 * height = center-to-center distance
const numSprockets = Math.floor(contentH / (spHeight * spGapFactor));
const spColTotalHeight = numSprockets * spHeight * spGapFactor - spHeight * (spGapFactor - 1); // Calculated occupied height
const spColStartY = imgY + (contentH - spColTotalHeight) / 2; // Center the column of sprockets relative to image
oCtx.fillStyle = '#050301'; // Very dark, almost black for holes
for (let i = 0; i < numSprockets; i++) {
const currentSpY = spColStartY + i * (spHeight * spGapFactor);
const currentSpX = spAreaEffectiveX + (spAvailableWidth - spWidth) / 2; // Center sprocket in its available width
fillRoundRect(oCtx, currentSpX, currentSpY, spWidth, spHeight, spRadius);
}
}
// Light Leaks (drawn last with 'lighter' composite op for additive effect)
if (lightLeakOpacity > 0 && Math.random() < 0.45) { // 45% chance of a light leak
oCtx.globalCompositeOperation = 'lighter';
const numLeaks = 1; // For simplicity, one leak per image
for (let i = 0; i < numLeaks; ++i) {
const leakX = Math.random() * frameWidth;
const leakY = Math.random() * frameHeight;
const rMax = Math.min(frameWidth, frameHeight) * (Math.random() * 0.4 + 0.3); // Random radius
const rMin = rMax * (Math.random() * 0.3 + 0.1); // Inner radius of gradient
const leakGrad = oCtx.createRadialGradient(leakX, leakY, rMin, leakX, leakY, rMax);
// Colors for light leaks: typically warm (red, orange, yellow)
const R = 190 + Math.random() * 65;
const G = 100 + Math.random() * 100;
const B = Math.random() * 60;
const opacityFactor = Math.random() * 0.4 + 0.6; // Randomize opacity slightly
leakGrad.addColorStop(0, `rgba(${R},${G},${B},${lightLeakOpacity * opacityFactor})`);
leakGrad.addColorStop(1, `rgba(${R},${G},${B},0)`); // Fade to transparent
oCtx.fillStyle = leakGrad;
oCtx.beginPath(); // Ensure path is new for each leak
oCtx.arc(leakX, leakY, rMax, 0, Math.PI * 2); // Circular leak shape
oCtx.fill();
}
oCtx.globalCompositeOperation = 'source-over'; // Reset composite operation
}
return outputCanvas;
}
Apply Changes