You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, coverColor = "#3a2318", bgColor = "#e4d9c5", curveFactor = 0.05, thicknessFactor = 0.04) {
// Parse parameters
let factorC = parseFloat(curveFactor) || 0.05;
let factorT = parseFloat(thicknessFactor) || 0.04;
// Scale image reasonably to maintain performance and high quality
let targetW = originalImg.width;
if (targetW < 600) targetW = 600;
if (targetW > 1200) targetW = 1200;
let scale = targetW / originalImg.width;
let w = Math.floor(originalImg.width * scale);
let h = Math.floor(originalImg.height * scale);
// Render image to a temp canvas for fast slice sampling
let srcCanvas = document.createElement('canvas');
srcCanvas.width = w;
srcCanvas.height = h;
let srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true });
srcCtx.drawImage(originalImg, 0, 0, w, h);
// Book geometry parameters
let curve = w * factorC;
let thickness = w * factorT;
let margin = w * 0.15; // padding for shadow and visuals
let outW = w + margin * 2;
let outH = h + curve * 4 + thickness + margin * 2;
let baseY = margin + curve * 2; // initial Y position of the book edges
let destCanvas = document.createElement('canvas');
destCanvas.width = outW;
destCanvas.height = outH;
let ctx = destCanvas.getContext('2d');
// 1. Draw flat background
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, outW, outH);
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
// Helper for book curvature offset (Z axis mapped to Y screen axis)
// t goes from 0 (spine) to 1 (outer edge)
function getCurveZ(t) {
let foldD = curve * 1.5; // depth of the center fold
let peakD = curve * 1.2; // height of the page curve peak
// Smooth sine wave mapping simulating realistic page foreshortening and slump
return -Math.sin(Math.pow(t, 0.6) * Math.PI) * peakD + (1 - t) * foldD;
}
let step = 8; // detail step for drawing thick block lines
// 2. Draw shadow and book cover
let cvPad = w * 0.012; // padding so the cover extends past the pages
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur = w * 0.025;
ctx.shadowOffsetX = w * 0.005;
ctx.shadowOffsetY = w * 0.015;
ctx.fillStyle = coverColor;
ctx.beginPath();
ctx.moveTo(margin - cvPad, baseY + getCurveZ(1) - cvPad);
for (let x = 0; x <= w; x += step) {
let t = x < w / 2 ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
ctx.lineTo(margin + x, baseY + getCurveZ(t) - cvPad);
}
// Curve down the right edge
ctx.lineTo(margin + w + cvPad, baseY + getCurveZ(1) - cvPad);
ctx.lineTo(margin + w + cvPad, baseY + getCurveZ(1) + h + thickness + cvPad);
// Trace back bottom curve
for (let x = w; x >= 0; x -= step) {
let t = x < w / 2 ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
ctx.lineTo(margin + x, baseY + getCurveZ(t) + h + thickness + cvPad);
}
ctx.lineTo(margin - cvPad, baseY + getCurveZ(1) + h + thickness + cvPad);
ctx.closePath();
ctx.fill();
// Disable shadow for remaining inner elements
ctx.shadowColor = 'transparent';
// 3. Draw bottom edge of the pages (thickness showing paper block)
ctx.fillStyle = '#e5e5df';
ctx.beginPath();
ctx.moveTo(margin, baseY + getCurveZ(1) + h);
for (let x = 0; x <= w; x += step) {
let t = x < w / 2 ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
ctx.lineTo(margin + x, baseY + getCurveZ(t) + h);
}
ctx.lineTo(margin + w, baseY + getCurveZ(1) + h + thickness);
for (let x = w; x >= 0; x -= step) {
let t = x < w / 2 ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
ctx.lineTo(margin + x, baseY + getCurveZ(t) + h + thickness);
}
ctx.closePath();
ctx.fill();
// 4. Draw side depths (Left and Right page stacks)
// Left side
ctx.fillStyle = '#dcdcd4'; // darker due to light logic
ctx.beginPath();
ctx.moveTo(margin, baseY + getCurveZ(1));
ctx.lineTo(margin, baseY + getCurveZ(1) + h);
ctx.lineTo(margin, baseY + getCurveZ(1) + h + thickness);
ctx.lineTo(margin, baseY + getCurveZ(1) + thickness);
ctx.closePath();
ctx.fill();
// Right side
ctx.fillStyle = '#f4f4ec'; // lighter
ctx.beginPath();
ctx.moveTo(margin + w, baseY + getCurveZ(1));
ctx.lineTo(margin + w, baseY + getCurveZ(1) + h);
ctx.lineTo(margin + w, baseY + getCurveZ(1) + h + thickness);
ctx.lineTo(margin + w, baseY + getCurveZ(1) + thickness);
ctx.closePath();
ctx.fill();
// 5. Draw realistic page stratification lines along the exposed thickness
ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
ctx.lineWidth = 1;
for (let dy = 2.5; dy < thickness; dy += 3.5) {
// Bottom paper lines
ctx.beginPath();
for (let x = 0; x <= w; x += step) {
let t = x < w / 2 ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
let px = margin + x;
let py = baseY + getCurveZ(t) + h + dy;
if (x === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.stroke();
// Right side paper lines
ctx.beginPath();
ctx.moveTo(margin + w, baseY + getCurveZ(1) + dy);
ctx.lineTo(margin + w, baseY + getCurveZ(1) + h + dy);
ctx.stroke();
// Left side paper lines
ctx.beginPath();
ctx.moveTo(margin, baseY + getCurveZ(1) + dy);
ctx.lineTo(margin, baseY + getCurveZ(1) + h + dy);
ctx.stroke();
}
// 6. Establish clipping region to draw perfectly antialiased curve bounds for pages
ctx.save();
ctx.beginPath();
ctx.moveTo(margin, baseY + getCurveZ(1));
for (let x = 0; x <= w; x += 1) {
let t = x < w / 2 ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
ctx.lineTo(margin + x, baseY + getCurveZ(t));
}
ctx.lineTo(margin + w, baseY + getCurveZ(1) + h);
for (let x = w; x >= 0; x -= 1) {
let t = x < w / 2 ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
ctx.lineTo(margin + x, baseY + getCurveZ(t) + h);
}
ctx.lineTo(margin, baseY + getCurveZ(1) + h);
ctx.closePath();
// Fill white paper color behind image first (fixes images with transparency)
ctx.fillStyle = '#fdfcf7';
ctx.fill();
ctx.clip(); // Mask subsequent rendering
// 7. Draw the image wrapping across the 3D curves via 1px column slicing
for (let x = 0; x < w; x++) {
let isLeft = x < w / 2;
let t = isLeft ? (w / 2 - x) / (w / 2) : (x - w / 2) / (w / 2);
let z = getCurveZ(t);
let destX = margin + x;
let destY = baseY + z;
// Draw overlapping column slices (+0.2 width overlaps prevent seam gaps)
ctx.drawImage(srcCanvas, x, 0, 1, h, destX, destY - 1, 1.2, h + 2);
// Map surface slope to geometric lighting intensity
let nextT = isLeft ? (w / 2 - (x + 1)) / (w / 2) : ((x + 1) - w / 2) / (w / 2);
let nextZ = getCurveZ(nextT);
let slope = nextZ - z;
let alpha = 0;
if (slope > 0) { // Tilt facing away from pseudo top surface
alpha = Math.min(0.65, slope * 3.0);
ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
} else { // Tilt facing towards theoretical light
alpha = Math.min(0.55, -slope * 3.0);
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
}
if (alpha > 0) ctx.fillRect(destX, destY - 1, 1.2, h + 2);
// Extra dynamic shadows for physical fold realism
if (t < 0.22) {
let spineShadow = Math.min(0.65, (0.22 - t) * 3);
ctx.fillStyle = `rgba(0, 0, 0, ${spineShadow})`;
ctx.fillRect(destX, destY - 1, 1.2, h + 2);
}
if (t < 0.015) { // Sharp deep center gap
ctx.fillStyle = `rgba(0, 0, 0, 0.45)`;
ctx.fillRect(destX, destY - 1, 1.2, h + 2);
}
if (t > 0.92) { // Outer edge darkening to simulate page curvature
let edgeShadow = (t - 0.92) * 5;
ctx.fillStyle = `rgba(0, 0, 0, ${edgeShadow})`;
ctx.fillRect(destX, destY - 1, 1.2, h + 2);
}
}
ctx.restore(); // Drop clip bounds
// 8. Visual details over the mapped pages
let spineX = margin + w / 2;
let spineTopY = baseY + getCurveZ(0);
// Crisp center crease
ctx.beginPath();
ctx.moveTo(spineX, spineTopY);
ctx.lineTo(spineX, spineTopY + h);
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.lineWidth = 1;
ctx.stroke();
// Crease highlight
ctx.beginPath();
ctx.moveTo(spineX + 1, spineTopY);
ctx.lineTo(spineX + 1, spineTopY + h);
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.stroke();
return destCanvas;
}
Apply Changes