Halo + drop-shadow workflow on a transparent silhouette

Adds a coloured halo and a soft drop-shadow behind any subject that has a transparent background — the same "polished product shot" look every marketplace listing and e-commerce hero image relies on. The demo builds up the look in four panels so you can see each layer added: base, halo only, halo + shadow, finished composite on a real photo background. Pick any halo colour (`GLOW_COLOR`), dial in the radius and opacity, set how far the shadow falls and how soft it gets. Tip: works best on cut-out subjects (already transparent) — pair it with the `rmbg` background-removal tool to turn any flat photo into a placement-ready asset.

INPUT
INPUT — Halo + drop-shadow workflow on a transparent silhouette
Halo + shadow build-up
Halo + shadow build-up — Halo + drop-shadow workflow on a transparent silhouette
JavaScript
// Step-by-step glow + dropShadow on a transparent silhouette,
// finishing with a "real product placement" composite onto a photo.
// demo_halo_shadow.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: GLOW_BLUR:number=18,min=2,max=60
//!PARAM: GLOW_COLOR:string=#80c0ff
//!PARAM: GLOW_STRENGTH:number=0.85,min=0,max=1
//!PARAM: SHADOW_OFFSET:number=10,min=0,max=40
//!PARAM: SHADOW_BLUR:number=14,min=2,max=40
//!PARAM: SHADOW_STRENGTH:number=0.7,min=0,max=1
//!PARAM: MAX_W:integer=720,min=400,max=1600
//!PARAM: LABEL_SIZE:number=20,min=10,max=64

// `glow` and `dropShadow` work on alpha edges — both produce the
// classic "outer halo" / "drop shadow" only where the silhouette
// has transparency to extend into. This showcase builds up the
// effects one at a time so each one's contribution is obvious,
// and finishes with a real-world-style composite.
//
// 4-panel grid:
//   ┌──────────────────────────┬──────────────────────────┐
//   │ 1) subject only          │ 2) + glow (halo)         │
//   ├──────────────────────────┼──────────────────────────┤
//   │ 3) + dropShadow          │ 4) glow + shadow on the  │
//   │    (no glow)             │    photo background      │
//   └──────────────────────────┴──────────────────────────┘
//
// Subject is built inline via Engine.loadSVG() — no external
// tools required. Tweak GLOW_COLOR to retint the halo
// ("#ff80ff" magenta, "#ffe080" warm gold, …).

const SRC0 = Engine.loadImage(INPUT);
{
  const long = Math.max(SRC0.width, SRC0.height);
  if (long > MAX_W) {
    const s = MAX_W / long;
    SRC0.resize(Math.round(SRC0.width * s),
                Math.round(SRC0.height * s),
                Interp.Bilinear);
  }
}
const W = SRC0.width;
const H = SRC0.height;

// Stylised silhouette inline — 5-pointed star with a warm radial
// gradient. Demonstrates that loadSVG carries gradients through.
function makeStar() {
  const cx = W / 2, cy = H / 2;
  const rOuter = Math.min(W, H) * 0.30;
  const rInner = rOuter * 0.42;
  let pts = '';
  for (let i = 0; i < 10; i++) {
    const a = -Math.PI / 2 + i * Math.PI / 5;
    const r = (i % 2 === 0) ? rOuter : rInner;
    pts += `${(cx + Math.cos(a) * r).toFixed(1)},${(cy + Math.sin(a) * r).toFixed(1)} `;
  }
  return Engine.loadSVG(
    `<svg xmlns="http://www.w3.org/2000/svg"
          width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
      <defs>
        <radialGradient id="g" cx="50%" cy="40%" r="55%">
          <stop offset="0"  stop-color="#ffe0a0"/>
          <stop offset="1"  stop-color="#e85040"/>
        </radialGradient>
      </defs>
      <polygon fill="url(#g)" points="${pts.trim()}"/>
    </svg>`);
}

const subject = makeStar();

// ── Panel layout ────────────────────────────────────────────────
const bandH = LABEL_SIZE + 8;
const cellW = W;
const cellH = bandH + H;

const out = Engine.createImage(1, 1);
out.setPixel(px(0, 0), new Pixel(0.06, 0.07, 0.10, 1.0));
out.resize(cellW * 2, cellH * 2);

function panel(label, image, col, row) {
  const x0 = col * cellW;
  const y0 = row * cellH;
  out.drawText(label, x0 + 16, y0 + LABEL_SIZE - 2, {
    size: LABEL_SIZE, color: NamedColor.white,
  });
  out.blendAt(image, px(x0, y0 + bandH), 1.0, BlendMode.Over);
}

// (0, 0) ── bare subject — alpha edges visible against panel bg
panel('1) silhouette only', subject.clone(), 0, 0);

// (1, 0) ── + glow — halo OUTSIDE the silhouette
{
  const t = subject.clone();
  t.glow(GLOW_BLUR, GLOW_COLOR, GLOW_STRENGTH);
  panel(`2) + glow(${GLOW_BLUR}, "${GLOW_COLOR}", ${GLOW_STRENGTH})`, t, 1, 0);
  t.free();
}

// (0, 1) ── + dropShadow — soft offset silhouette behind
{
  const t = subject.clone();
  t.dropShadow(SHADOW_OFFSET, SHADOW_OFFSET, SHADOW_BLUR,
               '#000000', SHADOW_STRENGTH);
  panel(`3) + dropShadow(${SHADOW_OFFSET},${SHADOW_OFFSET},${SHADOW_BLUR})`, t, 0, 1);
  t.free();
}

// (1, 1) ── glow + dropShadow composited onto the photo background
//          → the practical "polished product placement" use case
{
  // Build the styled subject (shadow first, glow last so the halo
  // sits on top of the shadow rim).
  const styled = subject.clone();
  styled.dropShadow(SHADOW_OFFSET, SHADOW_OFFSET, SHADOW_BLUR,
                    '#000000', SHADOW_STRENGTH);
  styled.glow(GLOW_BLUR, GLOW_COLOR, GLOW_STRENGTH * 0.85);
  // Composite onto the photo
  const composite = SRC0.clone();
  composite.blendAt(styled, px(0, 0), 1.0, BlendMode.Over);
  panel('4) shadow + glow on photo background', composite, 1, 1);
  styled.free();
  composite.free();
}

SRC0.free();
subject.free();
out.save(OUTPUT);
out.free();

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0