const { createCanvas, loadImage } = require('canvas'); const StackBlur = require('stackblur-canvas'); // Node-compatible async function generateShadow(imageBuffer, options = {}) { const { type = 'cast', // cast | drop | flat | box dir = 'back', // back | front | left | right blur = 20, // px opacity = 0.35, lightX, lightY } = options; const img = await loadImage(imageBuffer); const trimmed = trimImage(img); // precise trimming // ---------------- Use ORIGINAL object size (no fixed scaling) ---------------- const scaledW = trimmed.width; const scaledH = trimmed.height; // ---------------- Shadow padding ---------------- let shadowOffset = type === 'drop' ? 50 : type === 'flat' ? 40 : type === 'box' ? 40 : 200; let extraTop = 0, extraBottom = 0, extraLeft = 0, extraRight = 0; if (dir === 'back') { extraBottom = shadowOffset; extraLeft = shadowOffset; extraRight = shadowOffset; } if (dir === 'front') { extraTop = shadowOffset; extraBottom = shadowOffset; // add bottom to avoid shrinking extraLeft = shadowOffset; extraRight = shadowOffset; } if (dir === 'right') { extraTop = shadowOffset; extraBottom = shadowOffset; // add bottom to avoid shrinking extraLeft = shadowOffset; extraRight = shadowOffset; } if (dir === 'left') { extraTop = shadowOffset; extraBottom = shadowOffset; // add bottom to avoid shrinking extraLeft = shadowOffset; extraRight = shadowOffset; } if (type === 'cast') { extraTop += scaledH * 0.5; extraBottom += scaledH * 0.5; if (dir === 'back') { extraLeft += scaledW * 0.3; extraRight += scaledW * 0.3; } } // // ---------------- Final canvas size = original size + padding ---------------- // const canvasW = scaledW + extraLeft + extraRight; // const canvasH = scaledH + extraTop + extraBottom; // const canvas = createCanvas(canvasW, canvasH); // const ctx = canvas.getContext('2d'); // ctx.clearRect(0, 0, canvasW, canvasH); // // ---------------- Base position ---------------- // const baseX = extraLeft + scaledW / 2; // center of object // const baseY = extraTop + scaledH; // bottom of object const safePad = Math.max(blur * 4, 300); // ensures enough room for long shadows const canvasW = scaledW + safePad * 2; const canvasH = scaledH + safePad * 2; const canvas = createCanvas(canvasW, canvasH); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvasW, canvasH); // Position the image at the center-bottom area (natural shadow placement) const baseX = canvasW / 2; const baseY = canvasH / 2 + scaledH / 2; // ---------------- Draw shadow ---------------- switch (type) { case 'drop': drawDropShadow(ctx, trimmed.canvas, scaledW, scaledH, dir, blur, opacity, baseX, baseY); break; case 'flat': drawFlatShadow(ctx, trimmed.canvas, scaledW, scaledH, dir, opacity, baseX, baseY); break; case 'box': drawBoxShadow(ctx, scaledW, scaledH, dir, blur, opacity, baseX, baseY); break; case 'cast': default: drawCastShadow(ctx, trimmed.canvas, scaledW, scaledH, blur, opacity, baseX, baseY, lightX, lightY, dir); break; } // ---------------- Draw object ---------------- ctx.globalAlpha = 1; ctx.filter = "none"; ctx.drawImage(trimmed.canvas, baseX - scaledW / 2, baseY - scaledH, scaledW, scaledH); return canvas.toBuffer("image/png"); } // ---------------- Shadow helpers ---------------- function computePadOverlap(blur) { const pad = Math.max(4, Math.ceil(blur * 2)); // ensures enough padding for blur spread const overlap = Math.max(1, Math.round(blur * 0.6)); // shadow overlap with object return { pad, overlap }; } function drawDropShadow(ctx, img, w, h, dir, blur, opacity, baseX, baseY) { let dx = 0, dy = 0, off = 40; if (dir === 'back') dy = off; if (dir === 'front') dy = -off; if (dir === 'left') dx = -off; if (dir === 'right') dx = off; const { pad, overlap } = computePadOverlap(blur); const shadowCanvas = createCanvas(Math.round(w + pad), Math.round(h + pad)); const sctx = shadowCanvas.getContext('2d'); const drawX = pad / 2; const drawY = pad / 2; sctx.clearRect(0, 0, shadowCanvas.width, shadowCanvas.height); sctx.drawImage(img, drawX, drawY, w, h); sctx.globalCompositeOperation = "source-in"; sctx.fillStyle = "black"; sctx.fillRect(0, 0, shadowCanvas.width, shadowCanvas.height); StackBlur.canvasRGBA(shadowCanvas, 0, 0, shadowCanvas.width, shadowCanvas.height, blur); ctx.save(); ctx.globalAlpha = opacity; const drawPosX = baseX - w / 2 + dx - pad / 2; const drawPosY = baseY - h + dy - pad / 2 + overlap; ctx.drawImage(shadowCanvas, drawPosX, drawPosY, shadowCanvas.width, shadowCanvas.height); ctx.restore(); } function drawFlatShadow(ctx, img, w, h, dir, opacity, baseX, baseY) { let dx = 0, dy = 0, off = 30; if (dir === 'back') dy = off; if (dir === 'front') dy = -off; if (dir === 'left') dx = -off; if (dir === 'right') dx = off; ctx.save(); ctx.globalAlpha = opacity; ctx.drawImage(img, baseX - w/2 + dx, baseY - h + dy + 1, w, h); ctx.restore(); } function drawBoxShadow(ctx, w, h, dir, blur, opacity, baseX, baseY) { let dx = 0, dy = 0, off = 30; if (dir === 'back') dy = off; if (dir === 'front') dy = -off; if (dir === 'left') dx = -off; if (dir === 'right') dx = off; const { pad, overlap } = computePadOverlap(blur); const shadowCanvas = createCanvas(Math.round(w + pad), Math.round(h + pad)); const sctx = shadowCanvas.getContext('2d'); sctx.clearRect(0, 0, shadowCanvas.width, shadowCanvas.height); sctx.fillStyle = `black`; sctx.fillRect(pad/2, pad/2, w, h); StackBlur.canvasRGBA(shadowCanvas, 0, 0, shadowCanvas.width, shadowCanvas.height, blur); ctx.save(); ctx.globalAlpha = opacity; const drawPosX = baseX - w/2 + dx - pad / 2; const drawPosY = baseY - h + dy - pad / 2 + overlap; ctx.drawImage(shadowCanvas, drawPosX, drawPosY, shadowCanvas.width, shadowCanvas.height); ctx.restore(); } function drawCastShadow(ctx, img, w, h, blur, opacity, baseX, baseY, lightX, lightY, dir) { const { pad, overlap } = computePadOverlap(blur); const shadowW = Math.round(w + pad); const shadowH = Math.round(h + pad); const shadowCanvas = createCanvas(shadowW, shadowH); const sctx = shadowCanvas.getContext('2d'); sctx.clearRect(0, 0, shadowW, shadowH); const drawX = pad / 2; const drawY = pad / 2; sctx.drawImage(img, drawX, drawY, w, h); sctx.globalCompositeOperation = "source-in"; sctx.fillStyle = "black"; sctx.fillRect(0, 0, shadowW, shadowH); StackBlur.canvasRGBA(shadowCanvas, 0, 0, shadowW, shadowH, blur); const relX = lightX ? (lightX / (w * 2) - 0.5) * 2 : (dir === 'left' ? -1 : dir === 'right' ? 1 : 0); const relY = lightY ? (lightY / (h * 2)) : (dir === 'front' ? 0.2 : dir === 'back' ? 0.8 : 0.5); const skew = relX * 0.8; ctx.save(); ctx.translate(baseX, baseY); if (dir === 'front' || relY < 0.5) ctx.transform(1, 0, skew, -0.5, 0, 0); else ctx.transform(1, 0, skew, 0.3, 0, 0); ctx.globalAlpha = opacity; const drawPosX = -w / 2 - pad / 2; const drawPosY = -h - pad / 2 + overlap; ctx.drawImage(shadowCanvas, drawPosX, drawPosY-(30), shadowW, shadowH); ctx.restore(); } // ---------------- Trimming ---------------- function trimImage(image) { const temp = createCanvas(image.width, image.height); const tctx = temp.getContext('2d'); tctx.drawImage(image, 0, 0); const imgData = tctx.getImageData(0, 0, temp.width, temp.height); let top = temp.height, left = temp.width, right = 0, bottom = 0; for (let y = 0; y < temp.height; y++) { for (let x = 0; x < temp.width; x++) { const alpha = imgData.data[(y * temp.width + x) * 4 + 3]; if (alpha > 0) { if (x < left) left = x; if (x > right) right = x; if (y < top) top = y; if (y > bottom) bottom = y; } } } const w = right - left + 1; const h = bottom - top + 1; const trimmedCanvas = createCanvas(w, h); const t2 = trimmedCanvas.getContext('2d'); t2.drawImage(image, left, top, w, h, 0, 0, w, h); return { canvas: trimmedCanvas, width: w, height: h }; } module.exports = { generateShadow };