# Particles from a sheet

> Particles that take their picture from a sprite sheet: a fire playing its run at its own pace, and an explosion that goes from a flash to smoke.

Example `particles-sheet`, running at https://nacatamalon.com/en/examples/particles-sheet. The whole catalogue: https://nacatamalon.com/llms/examples.md.

A particle can take its picture from a sprite sheet: `atlas` in the effect, then one `frame` by its place or its name, or an `anim` every particle plays from its birth. `animPace: 'clip'` plays the run at its own `fps`, as a sprite would: the fire on the left, from a `.particles` file naming an `.atlas`. `animPace: 'life'`, the default, stretches it over each particle's life, so the explosion on the right starts as a flash and dies as smoke however long each puff lives.

The explosion's sheet is painted in pixels and its run written in code. The rocks are one frame each, with a tail that shows the frame its particle shows. Click to set one off.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { Game } from './scenes/demo';
import { WIDTH, HEIGHT } from './settings';

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: '#141822'
});

// Declare Scenes
game({
    Game
});
```

`art/paint_sheet.ts`:

```ts
import { getColor } from 'nacatamalon';
import { createPixels, fillCircle, fillRect } from 'nacatamalon/pixels';

export const paintSheet = () => {
    const sheet = createPixels(16 * 8, 16);

    const ball = (frame: number, radius: number, colour: string, core?: string) => {
        fillCircle(sheet, frame * 16 + 8, 8, radius, getColor(colour));

        if (core !== undefined) {
            fillCircle(sheet, frame * 16 + 8, 8, Math.max(1, radius - 3), getColor(core));
        }
    };

    ball(0, 3, '#ffffff');
    ball(1, 5, '#ffd34a', '#ffffff');
    ball(2, 7, '#ff8a1e', '#ffd34a');
    ball(3, 7, '#d8401a', '#ff8a1e');
    ball(4, 6, '#6b4a42', '#8c3a1c');
    ball(5, 5, '#5a5560');
    fillRect(sheet, 6 * 16 + 5, 6, 6, 4, '#7a6a5a');
    fillRect(sheet, 6 * 16 + 6, 5, 3, 1, '#a8957e');
    fillRect(sheet, 7 * 16 + 6, 5, 4, 5, '#5e5248');
    fillRect(sheet, 7 * 16 + 6, 5, 2, 2, '#8c7c6a');

    return sheet;
};
```

`components/explosions.ts`:

```ts
import { createParticles, emitParticles, usePointer, useUpdate } from 'nacatamalon';

import type { TSpriteAtlas } from 'nacatamalon';
import { useRandom } from 'nacatamalon/math';
import { puffs, rock } from '../effects';
import { EXPLOSION } from '../settings';

export const Explosions = ({ atlas }: { atlas: TSpriteAtlas }) => {
    const random = useRandom();

    const blast = createParticles({
        effect: puffs(atlas),
        autoplay: false
    });

    const bits = [
        createParticles({
            effect: rock(atlas, 6),
            autoplay: false
        }),
        createParticles({
            effect: rock(atlas, 7),
            autoplay: false
        })
    ];

    const explode = (x: number, y: number) => {
        for (const emitter of [blast, ...bits]) {
            emitter.transform.x = x;
            emitter.transform.y = y;
        }

        emitParticles(blast, 24);

        for (const emitter of bits) {
            emitParticles(emitter, 5);
        }
    };

    usePointer().onDown((info) => explode(info.worldX, info.worldY));

    // One on its own every so often, so there is always something to look at.
    let wait = 0.3;

    useUpdate((delta) => {
        wait -= delta;

        if (wait <= 0) {
            wait = EXPLOSION.interval;
            explode(330 + random.rand() * 60 - 30, 170 + random.rand() * 40 - 20);
        }
    });
};
```

`effects.ts`:

```ts
import type { TParticlesEffect2d, TSpriteAtlas } from 'nacatamalon';

export const puffs = (atlas: TSpriteAtlas): TParticlesEffect2d => ({
    kind: 'particles-2d',
    atlas,
    anim: {
        frames: [0, 1, 2, 3, 4, 5]
    },
    max: 120,
    emission: {
        rate: 0,
        burst: 0,
        loop: false
    },
    shape: {
        kind: 'circle',
        radius: 6,
        edge: false
    },
    spread: Math.PI * 2,
    life: [0.6, 1.4],
    speed: [10, 60],
    size: [20, 34],
    damping: 1.5,
    gravity: {
        x: 0,
        y: -30
    },
    colorOverLife: [
        {
            t: 0,
            color: '#ffffff',
            alpha: 1
        },
        {
            t: 0.8,
            color: '#ffffff',
            alpha: 1
        },
        {
            t: 1,
            color: '#ffffff',
            alpha: 0
        }
    ],
});

// The rocks: one frame each, with a short tail.
export const rock = (atlas: TSpriteAtlas, frame: number): TParticlesEffect2d => ({
    kind: 'particles-2d',
    atlas,
    frame,
    max: 40,
    emission: {
        rate: 0,
        burst: 0,
        loop: false
    },
    spread: Math.PI * 2,
    life: [0.8, 1.2],
    speed: [90, 160],
    size: [18, 24],
    spin: [-8, 8],
    gravity: {
        x: 0,
        y: 260
    },
    trail: {
        length: 3,
        width: 0.7,
        fade: true
    },
});
```

`scenes/demo.ts`:

```ts
import { createParticles, createScene, createSpriteAtlas, createText, useLoadParticles, useSpawn } from 'nacatamalon';
import { createPixelTexture } from 'nacatamalon/pixels';

import { paintSheet } from '../art/paint_sheet';
import { Explosions } from '../components/explosions';

export const Game = () => {
    // A texture is made inside the scene, which is what owns it.
    const atlas = createSpriteAtlas({
        texture: createPixelTexture(paintSheet()),
        columns: 8
    });

    const fire = useLoadParticles({
        src: '/example-assets/effects/fire_sheet.particles'
    });

    createParticles({
        effect: fire,
        transform: {
            x: 120,
            y: 270
        }
    });

    useSpawn(Explosions)({
        atlas
    });
    createText({
        text: "FIRE: 'clip'",
        transform: {
            x: 60,
            y: 20
        }
    });
    createText({
        text: "EXPLOSION: 'life'",
        transform: {
            x: 270,
            y: 20
        }
    });
    createText({
        text: 'CLICK TO SET ONE OFF',
        transform: {
            x: 150,
            y: 296
        }
    });

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 320;
export const EXPLOSION = {
    interval: 1.6
};
```
