You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, caption = "Sweet Memories", woodColor = "#8B5A2B", liningColor = "#4A0E13") {
// 1. Calculate dimensions (constrain photo to a max dimension to ensure beautiful proportions)
const maxPhotoDim = 500;
const scale = Math.min(1, maxPhotoDim / Math.max(originalImg.width, originalImg.height));
const imgW = originalImg.width * scale;
const imgH = originalImg.height * scale;
// Layout configuration
const pPadX = 25; // Polaroid side borders
const pPadTop = 25; // Polaroid top border
const pPadBot = 90; // Polaroid bottom border (for caption)
const prW = imgW + pPadX * 2;
const prH = imgH + pPadTop + pPadBot;
const floorPadX = 60; // Padding around polaroid on the box floor
const floorPadY = 60;
const flW = prW + floorPadX * 2;
const flH = prH + floorPadY * 2;
const wallDepth = 45; // Depth of the shadow box walls
const fW = flW + wallDepth * 2;
const fH = flH + wallDepth * 2;
const outMarg = 50; // Outer wood frame thickness
const cw = fW + outMarg * 2;
const ch = fH + outMarg * 2;
// Create main canvas
const canvas = document.createElement('canvas');
canvas.width = cw;
canvas.height = ch;
const ctx = canvas.getContext('2d');
// 2. Helper to generate a repeating procedural wood texture
function createWoodPattern(color, vertical = false) {
const size = 300;
const pc = document.createElement('canvas');
pc.width = size;
pc.height = size;
const pctx = pc.getContext('2d');
pctx.fillStyle = color;
pctx.fillRect(0, 0, size, size);
// Fine grain
pctx.globalAlpha = 0.04;
pctx.fillStyle = '#000';
for (let i = 0; i < size; i += 2) {
if (vertical) pctx.fillRect(i, 0, 1, size);
else pctx.fillRect(0, i, size, 1);
}
// Broad curved grain
pctx.globalAlpha = 0.08;
pctx.strokeStyle = '#000';
for (let i = -20; i < size + 20; i += 25) {
pctx.beginPath();
if (vertical) {
pctx.moveTo(i, 0);
pctx.bezierCurveTo(i + 20, size / 3, i - 15, 2 * size / 3, i, size);
} else {
pctx.moveTo(0, i);
pctx.bezierCurveTo(size / 3, i + 20, 2 * size / 3, i - 15, size, i);
}
pctx.lineWidth = 1.5 + Math.random() * 2;
pctx.stroke();
}
return ctx.createPattern(pc, 'repeat');
}
const patH = createWoodPattern(woodColor, false);
const patV = createWoodPattern(woodColor, true);
// 3. Frame drawing logic (Miters)
function drawFrame(rectOut, rectIn, shades) {
const {x: xo, y: yo, w: wo, h: ho} = rectOut;
const {x: xi, y: yi, w: wi, h: hi} = rectIn;
const drawPiece = (pts, pat, shadeColor) => {
ctx.save();
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
ctx.lineTo(pts[1].x, pts[1].y);
ctx.lineTo(pts[2].x, pts[2].y);
ctx.lineTo(pts[3].x, pts[3].y);
ctx.closePath();
ctx.clip();
ctx.fillStyle = pat;
ctx.fillRect(0, 0, cw, ch); // Fill whole, clip truncates
if (shadeColor) {
ctx.fillStyle = shadeColor;
ctx.fillRect(0, 0, cw, ch);
}
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
};
// Top Piece
drawPiece([{x: xo, y: yo}, {x: xo + wo, y: yo}, {x: xi + wi, y: yi}, {x: xi, y: yi}], patH, shades.top);
// Bottom Piece
drawPiece([{x: xo, y: yo + ho}, {x: xo + wo, y: yo + ho}, {x: xi + wi, y: yi + hi}, {x: xi, y: yi + hi}], patH, shades.bottom);
// Left Piece
drawPiece([{x: xo, y: yo}, {x: xi, y: yi}, {x: xi, y: yi + hi}, {x: xo, y: yo + ho}], patV, shades.left);
// Right Piece
drawPiece([{x: xo + wo, y: yo}, {x: xi + wi, y: yi}, {x: xi + wi, y: yi + hi}, {x: xo + wo, y: yo + ho}], patV, shades.right);
}
// Geometry Definitions
const outerRect = {x: 0, y: 0, w: cw, h: ch};
const frameRect = {x: outMarg, y: outMarg, w: fW, h: fH};
const floorRect = {x: outMarg + wallDepth, y: outMarg + wallDepth, w: flW, h: flH};
// Draw Outer Frame
drawFrame(outerRect, frameRect, {
top: 'rgba(0,0,0,0)',
bottom: 'rgba(0,0,0,0.15)',
left: 'rgba(0,0,0,0.1)',
right: 'rgba(255,255,255,0.05)'
});
// Draw Inner Sloped Walls (Darker for depth)
drawFrame(frameRect, floorRect, {
top: 'rgba(0,0,0,0.65)',
bottom: 'rgba(0,0,0,0.1)',
left: 'rgba(0,0,0,0.45)',
right: 'rgba(0,0,0,0.05)'
});
// 4. Draw Inner Floor (Velvet lining)
ctx.save();
ctx.beginPath();
ctx.rect(floorRect.x, floorRect.y, floorRect.w, floorRect.h);
ctx.clip();
ctx.fillStyle = liningColor;
ctx.fill();
// Velvet noise pattern
const noiseCv = document.createElement('canvas');
noiseCv.width = 100; noiseCv.height = 100;
const nCtx = noiseCv.getContext('2d');
nCtx.fillStyle = '#000';
for(let i=0; i<100; i+=2) {
for(let j=0; j<100; j+=2) {
if(Math.random() < 0.4) nCtx.fillRect(i, j, 2, 2);
}
}
ctx.globalAlpha = 0.15;
ctx.fillStyle = ctx.createPattern(noiseCv, 'repeat');
ctx.fill();
ctx.globalAlpha = 1.0;
// Floor shadow from walls
const floorShade = ctx.createLinearGradient(floorRect.x, floorRect.y, floorRect.x + 100, floorRect.y + 100);
floorShade.addColorStop(0, 'rgba(0,0,0,0.8)');
floorShade.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = floorShade;
ctx.fill();
ctx.restore();
// 5. Load Font for Caption
const fontName = 'Caveat';
if (!document.getElementById('m-box-font')) {
const style = document.createElement('style');
style.id = 'm-box-font';
style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@600&display=swap');`;
document.head.appendChild(style);
}
try {
await document.fonts.load(`600 36px "${fontName}"`);
} catch(e) {
console.warn('Font load failed, falling back to system fonts.');
}
// 6. Draw Polaroid / Photo Memory
ctx.save();
ctx.translate(cw / 2, ch / 2);
ctx.rotate(0.04 - Math.random() * 0.08); // Subtle authentic rotation
// Polaroid Paper and Drop Shadow
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowOffsetX = 12;
ctx.shadowOffsetY = 16;
ctx.shadowBlur = 20;
ctx.fillStyle = '#FDFCF6'; // Slightly aged paper white
ctx.fillRect(-prW / 2, -prH / 2, prW, prH);
// Disable shadow for internal drawings
ctx.shadowColor = 'transparent';
// Draw the actual image
const photoX = -prW / 2 + pPadX;
const photoY = -prH / 2 + pPadTop;
ctx.drawImage(originalImg, photoX, photoY, imgW, imgH);
// Add a very subtle warm vintage wash
ctx.fillStyle = 'rgba(150, 100, 50, 0.05)';
ctx.fillRect(photoX, photoY, imgW, imgH);
// Draw Caption
ctx.fillStyle = '#222';
ctx.font = `600 42px "${fontName}", "Comic Sans MS", cursive`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(caption, 0, prH / 2 - pPadBot / 2 + 5);
// Draw Vintage Photo Corners
ctx.fillStyle = '#1A1A1A';
const cSize = 25; // Corner triangle size
function drawPhotoCorner(cx, cy, rotation) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(rotation);
ctx.beginPath();
ctx.moveTo(0, 0); ctx.lineTo(cSize, 0); ctx.lineTo(0, cSize);
ctx.closePath();
ctx.fill();
// Corner sleeve shine
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.beginPath();
ctx.moveTo(2, 2); ctx.lineTo(cSize - 3, 2); ctx.lineTo(2, cSize - 3);
ctx.closePath();
ctx.fill();
ctx.restore();
}
drawPhotoCorner(photoX, photoY, 0); // Top-Left
drawPhotoCorner(photoX + imgW, photoY, Math.PI / 2); // Top-Right
drawPhotoCorner(photoX + imgW, photoY + imgH, Math.PI); // Bottom-Right
drawPhotoCorner(photoX, photoY + imgH, -Math.PI / 2); // Bottom-Left
// Draw a small decorative dried leaf tucked beside the photo
ctx.restore(); // reset to canvas coords
ctx.save();
ctx.translate(floorRect.x + 30, ch / 2 - 20);
ctx.rotate(-0.3);
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 6;
ctx.fillStyle = '#8B7355';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(25, -15, 45, 0);
ctx.quadraticCurveTo(25, 15, 0, 0);
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.strokeStyle = '#5A4A36';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(43, 0);
ctx.stroke();
ctx.restore();
return canvas;
}
Apply Changes