Spaces:
Sleeping
Sleeping
| 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 }; | |