Shadow-Generator / app /core /shadowGenerator.js
karthikeya1212's picture
Update app/core/shadowGenerator.js
87e33a0 verified
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 };