# Strokes & dashes

> Paint styles for fill() and stroke(), inside or outside stroke alignment, hairlines that fade instead of breaking up, and dashes and dots with caps.

Source: https://pixi-silk.schmooky.dev/docs/strokes-and-dashes/

`fill()` and `stroke()` take a colour, a gradient or a style object. This page covers the stroke
side: width, alignment, hairlines and dashes.

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 `dashes` (source):

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

export default defineDemo({
    title: 'Dashes',
    size: [480, 220],
    controls: {
        dash: { type: 'range', label: 'dash', min: 0, max: 30, step: 1, value: 10 },
        gap: { type: 'range', label: 'gap', min: 2, max: 30, step: 1, value: 8 },
        cap: { type: 'select', label: 'cap', options: ['butt', 'round', 'square'], value: 'round' },
        march: { type: 'toggle', label: 'animate', value: true },
    },
    setup({ stage, params, tick }) {
        const g = new SilkGraphics();

        stage.addChild(g);
        tick((t) => {
            const style: StrokeStyle = {
                width: 6,
                cap: params.cap as StrokeStyle['cap'],
                dash: [params.dash, params.gap],
                dashOffset: params.march ? t * 24 : 0,
            };

            g.clear();
            g.circle(90, 110, 70).stroke({ ...style, color: 0x64d2ff }); // on closed outlines the pattern tiles without a seam
            g.line(200, 40, 440, 40).stroke({ ...style, color: 0xffd60a });
            g.polyline([200, 180, 260, 90, 320, 160, 380, 80, 440, 150], { smooth: 'catmull' }).stroke({
                ...style,
                color: 0xff375f,
            });
            g.line(200, 110, 440, 110).stroke({ width: 8, cap: 'round', dash: [0, 16], color: 0x30d158 }); // 0-length dashes = dots
        });
    },
});
```

```ts
g.circle(90, 90, 60).stroke({ width: 4, color: 0x64d2ff, dash: [10, 6] });
```

That draws a dashed ring: 10 units of dash, 6 of gap. The pattern is computed in the shader along
the path length, and every dash gets its own caps.

## Paint styles

```ts
g.circle(0, 0, 20).fill(0xff375f);                                   // any Pixi ColorSource
g.circle(0, 0, 20).fill('rgba(255, 55, 95, 0.5)');
g.circle(0, 0, 20).fill(linear([0x0a84ff, 0xbf5af2]));               // a gradient
g.circle(0, 0, 20).fill({ color: 0xff375f, alpha: 0.5, blur: 6 });   // a FillStyle
g.circle(0, 0, 20).stroke({ width: 4, color: 0xffffff, alignment: 'inside' });
```

`FillStyle` holds `color`, `alpha`, `gradient` and the effects on
[Shadows & effects](/docs/shadows-and-effects/). With a gradient, `color` tints it.
`StrokeStyle` adds these:

| Option | Default | What it does |
| --- | --- | --- |
| `width` | 1 | Stroke width in local units. An array tapers it ([Lines & paths](/docs/lines-and-paths/#taper-a-stroke)). |
| `alignment` | `'center'` | `'inside'`, `'center'` or `'outside'` the edge, or Pixi's numbers (1, 0.5, 0) |
| `cap` | `'butt'` | Ends of open paths and of every dash: `'butt'`, `'round'` or `'square'` |
| `join` | `'miter'` | Corners: `'miter'`, `'bevel'` or `'round'` |
| `miterLimit` | 10 | A longer miter, in half widths, becomes a bevel |
| `dash` | none | `[dash, gap]`, or one number for equal dash and gap |
| `dashOffset` | 0 | Shifts the pattern along the path, in local units |
| `dashFit` | `true` | Stretches the pattern on closed outlines so it tiles without a short dash |

## Align a stroke inside or outside

Live demo `alignment` (source):

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

const MODES = ['inside', 'center', 'outside'] as const;

export default defineDemo({
    title: 'Stroke alignment',
    size: [480, 200],
    // phones: smaller squares, so the three fit at full size text
    phone: { size: [360, 180] },
    controls: {
        width: { type: 'range', label: 'stroke width', min: 1, max: 24, step: 1, value: 12, unit: 'px' },
    },
    setup({ stage, params, tick, phone }) {
        // left edge, step and size of the squares
        const [left, step, side] = phone ? [24, 120, 72] : [40, 150, 100];
        const g = new SilkGraphics();

        stage.addChild(g);
        MODES.forEach((mode, i) => {
            const label = new Text({ text: mode, style: { fill: 0x8e8e93, fontSize: 13, fontFamily: 'system-ui' } });

            label.anchor.set(0.5, 0);
            label.position.set(left + side / 2 + i * step, 40 + side + 28);
            stage.addChild(label);
        });
        tick(() => {
            g.clear();
            MODES.forEach((alignment, i) => {
                const x = left + i * step;

                // fill + stroke on the same shape share one instance and never leave a seam
                g.roundRect(x, 40, side, side, side * 0.22, 0.6)
                    .fill(0x2c2c2e)
                    .stroke({ width: params.width, color: 0x30d158, alpha: 0.85, alignment });
                g.roundRect(x, 40, side, side, side * 0.22, 0.6).stroke({ width: 1, color: 0xffffff, alpha: 0.6 }); // the geometric edge
            });
        });
    },
});
```

Strokes on closed shapes sit inside the edge, centred on it or outside it. Strokes on open shapes,
such as lines and arcs, are always centred.

When `stroke()` follows `fill()` directly, both paints go into one primitive. The shader splits
each pixel's coverage between the fill and the stroke band. An inside stroke drawn this way has no
halo of fill colour along its outer edge, which a stroke drawn on top of a separate fill shows.

## Hairlines

Live demo `hairlines` (source):

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

// Lines thinner than a device pixel keep one pixel of width and fade instead of breaking up.
// Widths grow from left to right.
export default defineDemo({
    title: 'Hairlines',
    size: [720, 240],
    phone: { size: [360, 230] },
    controls: {
        scale: { type: 'range', label: 'width scale', min: 0.05, max: 2, step: 0.01, value: 0.5 },
    },
    setup({ stage, params, tick, width, phone }) {
        const g = new SilkGraphics();
        const cx = width / 2;
        const cy = phone ? 180 : 210;
        const r = phone ? 150 : 180;
        // the width readouts sit beside the fan, or under its ends on phones
        const thinnest = phone ? label('', cx - r, cy + 12) : label('', cx - r - 12, cy - 8, {}, 1);
        const widest = phone ? label('', cx + r, cy + 12) : label('', cx + r + 12, cy - 8);

        if (phone) widest.anchor.set(1, 0);

        stage.addChild(g, thinnest, widest);
        tick((t) => {
            g.clear();
            const width = (i: number) => (0.1 + i * 0.12) * params.scale;

            for (let i = 0; i < 24; i++) {
                const a = -Math.PI + (i / 23) * Math.PI + Math.sin(t * 0.3) * 0.02;

                g.line(cx, cy, cx + Math.cos(a) * r, cy + Math.sin(a) * r).stroke({ width: width(i), color: INK.text });
            }
            g.circle(cx, cy, 3).fill(INK.muted);
            thinnest.text = `${width(0).toFixed(3)} px`;
            widest.text = `${width(23).toFixed(2)} px`;
        });
    },
});
```

A screen cannot show a stroke thinner than one device pixel. Silk keeps such a stroke one pixel
wide and scales its alpha by its width. A 0.25 px line is drawn 1 px wide at 25% alpha, so the ink
stays in proportion. Lines never break into dots, and a zoomed-out drawing keeps every line.

The threshold is `minStrokePx`, a per-object setting:

```ts
const lines = new SilkGraphics({ minStrokePx: 1, aaWidth: 1, dither: 1 });

lines.aaWidth = 1.5;   // softer edges
```

| Setting | Default | What it does |
| --- | --- | --- |
| `minStrokePx` | 1 | Strokes thinner than this many device pixels keep this width and fade |
| `aaWidth` | 1 | Width of the anti-aliasing ramp, in device pixels |
| `dither` | 1 | Dither of gradients and blurs, in 8-bit steps. 0 turns it off |

`SilkGraphics.defaults` holds the defaults for new objects.

## Dots

```ts
g.line(0, 20, 300, 20).stroke({ width: 8, cap: 'round', dash: [0, 16], color: 0x30d158 });
```

A dash of length 0 draws a dot with `cap: 'round'` and a square with `cap: 'square'`. The pattern
repeats every `dash + gap` units, so `[0, 16]` places a dot every 16 units.

## Marching ants

```ts
app.ticker.add(() => {
    g.clear().circle(80, 80, 56).stroke({ width: 3, color: 0xffffff, dash: [6, 6], dashOffset: t * 30 });
});
```

Animate `dashOffset` to move the pattern along the path. A selection outline or a loading
spinner is a dashed stroke with a growing offset.

## Where dashes work

Dashes work on lines, polylines, paths, arcs, circles and ellipses. Sharp rects, triangles and
polygons dash too with a miter or bevel join, because their strokes then run as paths. Rounded rects, rounded polygons, sectors
and hearts draw a solid stroke. For a dashed card border, stroke a path through the corners, or
`svgPath()` with arcs.

On closed outlines, `dashFit` stretches the pattern a little so it tiles the perimeter. Without
it, a short dash appears where the outline starts. Set `dashFit: false` to keep the exact lengths.
