# Shadows & effects

> Blur a fill or stroke into glows and soft shadows, grow it with spread, add Figma-style inner shadows, progressive blur and plus-lighter blends, without filters.

Source: https://pixi-silk.schmooky.dev/docs/shadows-and-effects/

Every paint takes a `blur`. A blurred copy under a crisp shape is a glow or a soft shadow, drawn
like any other shape, without a filter or a render texture.

Demo sources run in the docs demo runner, `defineDemo({ setup({ stage, params, tick }) })`. In your own app, `stage` is `app.stage` (or a container on it), `params` holds the values of the demo controls, and `tick(fn)` is `app.ticker.add`, with `t` and `dt` in seconds.

Live demo `glow` (source):

```ts
import { SilkGraphics } from 'pixi-silk';
import { defineDemo } from './runtime';

// `blur` is a gaussian applied to the distance field: no filters, no extra passes.
export default defineDemo({
    title: 'Glow',
    size: [480, 220],
    background: 0x050507,
    controls: {
        blur: { type: 'range', label: 'blur (σ)', min: 0, max: 30, step: 0.5, value: 12, unit: 'px' },
    },
    setup({ stage, params, tick }) {
        const g = new SilkGraphics();

        stage.addChild(g);
        tick((t) => {
            const pulse = 0.8 + Math.sin(t * 2) * 0.2;

            g.clear();
            // neon: a blurred copy under a crisp stroke
            g.circle(110, 110, 60).stroke({ width: 8, color: 0xff375f, blur: params.blur * pulse, alpha: 0.9 });
            g.circle(110, 110, 60).stroke({ width: 3, color: 0xffd1dc });
            // soft shadow: offset, blurred, translucent
            g.roundRect(260, 60 + 18, 180, 100, 24, 0.6).fill({ color: 0x000000, alpha: 0.9, blur: params.blur });
            g.roundRect(260, 60, 180, 100, 24, 0.6).fill(0x2c2c2e);
            g.circle(300, 110, 16).fill({ color: 0x30d158, blur: params.blur * 0.4 });
            g.circle(300, 110, 7).fill(0xe8ffe8);
        });
    },
});
```

```ts
g.circle(110, 110, 60).stroke({ width: 8, color: 0xff375f, blur: 12, alpha: 0.9 }); // glow
g.circle(110, 110, 60).stroke({ width: 3, color: 0xffd1dc });                        // core
```

The first ring is blurred, the second is drawn sharp on top. `blur` is the standard deviation of a
gaussian, in local units. Silk blurs the shape's distance to its edge, so a blurred shape costs
what a sharp one does. Its quad grows by three times the blur.

## Drop a soft shadow

```ts
g.roundRect(20, 20 + 18, 180, 100, 24, 0.6).fill({ color: 0x000000, alpha: 0.45, blur: 24 });
g.roundRect(20, 20, 180, 100, 24, 0.6).fill(0x1c1c1e);
```

Offset, blur and fade a dark copy, then draw the shape. The Live Activity cards on the
[Showcase](/docs/showcase/) use this pattern, measured from the reference: blur 30.5, 37.5 px
down, 45% black.

A CSS shadow's blur radius is twice the standard deviation. Halve a CSS blur to get `blur`.

## Grow a glow with spread

Live demo `effects-spread` (source):

```ts
import { Text } from 'pixi.js';
import { SilkGraphics } from 'pixi-silk';
import { defineDemo, INK, MONO } from './runtime';

// Blur versus blur with spread, the white-square glow from the design tutorials.
// Spread grows the shape before the blur, so the glow keeps a bright core.
const STEPS = [
    { label: 'shape', glows: [] },
    { label: 'blur 8', glows: [{ blur: 8, spread: 0 }] },
    {
        label: 'blur 32\nspread 8',
        glows: [
            { blur: 8, spread: 0 },
            { blur: 32, spread: 8 },
        ],
    },
    {
        label: 'blur 128\nspread 32',
        glows: [
            { blur: 8, spread: 0 },
            { blur: 32, spread: 8 },
            { blur: 128, spread: 32 },
        ],
    },
];

export default defineDemo({
    title: 'Blur and spread',
    size: [720, 300],
    // phones: two rows of two squares
    phone: { size: [360, 420] },
    controls: {
        scale: { type: 'range', label: 'glow', min: 0, max: 1.5, step: 0.01, value: 1 },
    },
    setup({ stage, params, tick, phone }) {
        const g = new SilkGraphics();
        // top left corner of each 70x70 square: one row of four, or a 2x2 grid on phones
        const at = (i: number): [number, number] =>
            phone ? [70 + (i % 2) * 150, 50 + Math.floor(i / 2) * 200] : [100 + i * 150, 105];
        let last = -1;

        stage.addChild(g);
        STEPS.forEach((step, i) => {
            const t = new Text({
                text: step.label,
                style: { fill: INK.muted, fontFamily: MONO, fontSize: 13, align: 'center', lineHeight: 18 },
            });

            t.anchor.set(0.5, 0);
            const [x, y] = at(i);

            t.position.set(x + 35, y + 117);
            stage.addChild(t);
        });
        tick(() => {
            if (params.scale === last) return;
            last = params.scale;
            g.clear();
            STEPS.forEach((step, i) => {
                const [x, y] = at(i);
                const k = params.scale;

                for (const glow of step.glows) {
                    // each glow layer is 1/4 of the blur in sigma (Figma blur is a diameter-ish radius)
                    g.roundRect(x, y, 70, 70, 8).fill({
                        color: 0xffffff,
                        alpha: 0.9,
                        blur: (glow.blur / 4) * k,
                        spread: glow.spread * k * 0.7,
                        blend: 'add',
                    });
                }
                g.roundRect(x, y, 70, 70, 8).fill(0xffffff);
            });
        });

        return undefined;
    },
});
```

```ts
g.rect(0, 0, 80, 80).fill({ color: 0xffffff, blur: 16, spread: 8, blend: 'add' });
g.rect(0, 0, 80, 80).fill(0xffffff);
```

`spread` grows the shape by that many local units before the blur, like a Figma shadow spread.
The glow keeps a bright core instead of fading straight from the edge. Negative values shrink the
shape. Stacking a tight glow and a wide one reads better than one large blur.

`blend: 'add'` paints plus-lighter: colours add to what is underneath, so overlapping glows bloom
towards white. It stays in the same draw as the other shapes.

## Add inner shadows

Live demo `effects-inner-shadow` (source):

```ts
import { type InnerShadow, SilkGraphics, vertical } from 'pixi-silk';
import { defineDemo } from './runtime';

// The four inner shadows of a glowing app icon, straight from a Figma spec.
// Each one is an extra instance of the same shape, so the icon stays one draw call.
export default defineDemo({
    title: 'Inner shadows',
    size: [560, 360],
    background: 0x1d1f22,
    controls: {
        deep: { type: 'toggle', label: 'blue 0, -80', value: true },
        soft: { type: 'toggle', label: 'light 0, -40', value: true },
        rim: { type: 'toggle', label: 'white, plus lighter', value: true },
        lip: { type: 'toggle', label: 'blue 0, 6', value: true },
    },
    setup({ stage, params, tick }) {
        const g = new SilkGraphics();
        const body = vertical([0x0a0909, 0x09101f]);
        let key = '';

        stage.addChild(g);
        tick(() => {
            const next = `${params.deep}${params.soft}${params.rim}${params.lip}`;

            if (next === key) return;
            key = next;
            g.clear();
            // drop shadow under the icon
            g.roundRect(186, 92, 188, 188, 40).fill({ color: 0x000000, alpha: 0.55, blur: 18, spread: -6 });
            const shadows: (InnerShadow | false)[] = [
                params.deep && { color: 0x144ccd, x: 0, y: -40, blur: 30, spread: -15 },
                params.soft && { color: 0x6694ff, alpha: 0.5, x: 0, y: -20, blur: 15, spread: -4 },
                params.rim && { color: 0xffffff, alpha: 0.4, x: 0, y: -10, blur: 10, spread: -3, blend: 'add' },
                params.lip && { color: 0x2365ff, alpha: 0.15, x: 0, y: 3, blur: 3, spread: -1 },
            ];

            g.roundRect(180, 80, 200, 200, 40).fill({
                gradient: body,
                innerShadow: shadows.filter((s): s is InnerShadow => s !== false),
            });
        });

        return undefined;
    },
});
```

```ts
g.roundRect(0, 0, 200, 200, 40).fill({
    color: 0x0a0909,
    innerShadow: [
        { color: 0x144ccd, y: -40, blur: 30, spread: -15 },
        { color: 0xffffff, alpha: 0.4, y: -10, blur: 10, spread: -3, blend: 'add' },
    ],
});
```

An inner shadow moves a copy of the shape, shrinks it by `spread` and blurs it. What the copy no
longer covers is shaded. Shadows draw in order on top of the fill, gradients included, and a
`stroke()` after the fill lands on top of them. Inner shadows work on closed shapes.

| Option | Default | What it does |
| --- | --- | --- |
| `color` | black | Shadow colour |
| `alpha` | 1 | Opacity, 0 to 1 |
| `x`, `y` | 0 | Offset in local units. Positive moves right and down. |
| `blur` | 0 | Gaussian standard deviation, in local units |
| `spread` | 0 | Grows the shadow inwards. Negative makes it thinner. |
| `blend` | `'normal'` | `'add'` brightens what is underneath |

The option names follow Figma's inner shadow, so a spec ports over option by option.

## Blur progressively

Live demo `effects-progressive` (source):

```ts
import { SilkGraphics, vertical } from 'pixi-silk';
import { defineDemo } from './runtime';

// Progressive blur: the violet hill is sharp at its bottom edge and melts at the top. The white
// bloom under it is painted plus-lighter, so where they overlap the violet lifts towards white.
export default defineDemo({
    title: 'Progressive blur',
    size: [560, 380],
    controls: {
        top: { type: 'range', label: 'blur at top', min: 0, max: 80, step: 1, value: 48 },
        bottom: { type: 'range', label: 'blur at bottom', min: 0, max: 20, step: 1, value: 1 },
        bloom: { type: 'toggle', label: 'plus-lighter bloom', value: true },
    },
    setup({ stage, params, tick }) {
        const g = new SilkGraphics();
        const violet = vertical([0x6a3cf0, 0x4b22c9]);
        let drawn = '';

        stage.addChild(g);
        tick((t) => {
            const sway = Math.sin(t * 0.6) * 0.04;
            const key = `${params.top}|${params.bottom}|${params.bloom}|${sway.toFixed(3)}`;

            if (key === drawn) return;
            drawn = key;
            g.clear();
            g.roundRect(80, 30, 400, 320, 18).fill(0x1c1c1e);
            g.save()
                .translateTransform(290, 245)
                .rotateTransform(-0.22 + sway)
                .ellipse(0, 0, 240, 112)
                .fill({ gradient: violet, blur: { from: params.top, to: params.bottom } })
                .restore();
            if (params.bloom) {
                g.ellipse(300, 318, 150, 30).fill({ color: 0xffffff, alpha: 0.5, blur: 18, blend: 'add' });
                g.ellipse(300, 318, 80, 12).fill({ color: 0xf6efff, alpha: 0.55, blur: 8, blend: 'add' });
            }
        });

        return undefined;
    },
});
```

```ts
g.area(points, 350, { smooth: 'monotone' }).fill({
    color: 0x5b2ee0,
    blur: { from: 20, to: 1, angle: Math.PI / 2 },
});
```

A `ProgressiveBlur` changes the blur across the shape. `from` is the blur at the start edge and
`to` at the far edge, along `angle` in radians. The default angle, `Math.PI / 2`, runs from top to
bottom.

## Limits

The blur is exact on straight edges and close on corners. Large blurs on complex paths, or on
the results of [shape operations](/docs/shape-operations/), can round inner corners a little. For
those, a Pixi `BlurFilter` over the object still works. With `createSilkApp`, filters render at
the screen resolution.

Blurred paints are dithered like gradients, so a large soft shadow does not band. See
[Gradients](/docs/gradients/#banding).
