# 2D campfire

> Warm lights, particles and a forest lit around a campfire.

Example `lighting-2d-campfire`, running at https://nacatamalon.com/en/examples/lighting-2d-campfire. The whole catalogue: https://nacatamalon.com/llms/examples.md.

Warm lights, particles and a forest lit around a campfire.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { Level } from './level';
import { WIDTH, HEIGHT } from './settings';

// Both backends run this same scene. Change to 'WEBGL2' to compare.
const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    renderer: 'AUTO',
    background: '#000000',
    pixelSnap: 'screen',
});

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

`clearing.ts`:

```ts
import { createSprite, useLight2d, useSpawn, useTransform, useUnlit2d, useUpdate } from 'nacatamalon';
import { createPixels, createPixelTexture, fillCircle, fillNoise, fillRect } from 'nacatamalon/pixels';
import { GROUND_Y, HEIGHT, WIDTH } from './settings';

// The night sky, the trees around the clearing and the grass, painted once in code. All of it is
// lit: far from the fire it sinks into the dark, near it it takes the fire's colour.
export const Clearing = () => {
    const sky = createPixels(WIDTH, GROUND_Y);
    fillNoise(sky, { seed: 11, scale: 60, octaves: 2, from: '#24305a', to: '#3b4d84' });
    createSprite({ texture: createPixelTexture(sky), width: WIDTH, height: GROUND_Y, anchor: { x: 0, y: 0 } });

    const tree = createPixels(64, 150);
    fillRect(tree, 28, 70, 8, 80, '#4a3527');
    fillCircle(tree, 32, 34, 30, '#2d5a3a');
    fillCircle(tree, 20, 58, 20, '#2a5236');
    fillCircle(tree, 44, 58, 20, '#2a5236');
    const pine = createPixelTexture(tree);
    for (const [x, scale] of [[30, 1.1], [92, 0.9], [150, 1], [338, 1], [400, 0.85], [452, 1.15]] as const) {
        createSprite({ texture: pine, width: 64 * scale, height: 150 * scale, anchor: { x: 0.5, y: 1 }, transform: { x, y: GROUND_Y + 4 } });
    }

    const grass = createPixels(WIDTH, HEIGHT - GROUND_Y);
    fillNoise(grass, { seed: 5, scale: 6, octaves: 3, from: '#2d4a24', to: '#5d7f3c' });
    createSprite({ texture: createPixelTexture(grass), width: WIDTH, height: HEIGHT - GROUND_Y, anchor: { x: 0, y: 0 }, transform: { y: GROUND_Y } });

    // Two logs to sit on, and the stones round the fire.
    createSprite({ width: 54, height: 10, tint: '#7a5638', transform: { x: 160, y: GROUND_Y + 8 } });
    createSprite({ width: 54, height: 10, tint: '#7a5638', transform: { x: 320, y: GROUND_Y + 8 } });
    for (let i = 0; i < 7; i++) {
        createSprite({ width: 7, height: 5, tint: '#8a8c90', transform: { x: 222 + i * 6, y: GROUND_Y + 1 } });
    }
};

// A firefly gives its own light and makes a little of it: left out of the lamps, with a small lamp
// of its own that goes where it flies.
export const Firefly = (index: number) => {
    const place = useTransform({ x: 60 + index * 70, y: 120 });
    useUnlit2d();
    const body = createSprite({ width: 2, height: 2, tint: '#d8ff6a' });
    const glow = useLight2d({ color: '#b6ff4a', radius: 22, intensity: 0.8 });
    let time = index * 2.3;
    useUpdate((delta) => {
        time += delta;
        place.x = 60 + index * 70 + Math.sin(time * 0.7) * 34 + Math.sin(time * 1.9) * 8;
        place.y = 120 + Math.sin(time * 1.1 + index) * 30;
        const on = Math.sin(time * 2.4) > -0.3;
        body.visible = on;
        glow.intensity = on ? 0.8 : 0;
    });
};

export const Fireflies = (count: number) => {
    for (let i = 0; i < count; i++) useSpawn(() => Firefly(i))();
};
```

`fire.ts`:

```ts
import { createParticles, createSprite, playParticles, stopParticles, useLight2d, useSpawn, useTransform, useUnlit2d, useUpdate } from 'nacatamalon';
import type { TParticlesEffect2d } from 'nacatamalon';
import { FIRE_X, FIRE_Y } from './settings';

// Smoke is lit like the rest of the world: orange where the fire reaches it, gone into the dark
// above. It makes no light of its own.
const smoke: TParticlesEffect2d = {
    kind: 'particles-2d', max: 160,
    emission: { rate: 14, burst: 0, duration: 0, loop: true },
    shape: { kind: 'rect', width: 10, height: 4 },
    direction: -Math.PI / 2, spread: 0.35, speed: [14, 24], life: [4, 6], size: [6, 9],
    gravity: { x: 4 },
    colorOverLife: [{ t: 0, color: '#b9b4ad', alpha: 0.75 }, { t: 1, color: '#8f8b88', alpha: 0 }],
    sizeOverLife: [{ t: 0, scale: 0.7 }, { t: 1, scale: 2.2 }],
};

// Embers glow, laid on top of what is behind them.
const embers: TParticlesEffect2d = {
    kind: 'particles-2d', max: 120, blend: 'additive',
    emission: { rate: 22, burst: 0, duration: 0, loop: true },
    shape: { kind: 'rect', width: 16, height: 4 },
    direction: -Math.PI / 2, spread: 0.8, speed: [30, 70], life: [0.8, 1.8], size: [2, 3],
    gravity: { y: -10 },
    colorOverLife: [{ t: 0, color: '#ffd27a', alpha: 1 }, { t: 1, color: '#ff4a1a', alpha: 0 }],
};

export const Campfire = () => {
    useTransform({ x: FIRE_X, y: FIRE_Y });
    createSprite({ width: 30, height: 5, tint: '#5b3b25', transform: { y: 4, rotation: 0.2 } });
    createSprite({ width: 30, height: 5, tint: '#6b4730', transform: { y: 4, rotation: -0.2 } });

    const lamp = useLight2d({ transform: { y: -8 }, color: '#ff9a45', radius: 170, falloff: 1.2 });
    createParticles({ effect: smoke, transform: { y: -14 } });

    // The flames: their own light, left out of the lamps.
    const flames = useSpawn(() => {
        useUnlit2d();
        return [
            createSprite({ width: 14, height: 14, tint: '#ff6a1f', transform: { y: -6 } }),
            createSprite({ width: 9, height: 10, tint: '#ffb43c', transform: { y: -5 } }),
            createSprite({ width: 4, height: 5, tint: '#fff1b0', transform: { y: -3 } }),
        ];
    })().api;

    // The same embers twice: left out of the lamps, so they glow, and lit, so the dark eats them.
    const glowing = useSpawn(() => {
        useUnlit2d();
        return createParticles({ effect: embers, transform: { y: -10 } });
    })().api;
    const dimmed = createParticles({ effect: embers, transform: { y: -10 } });
    stopParticles(dimmed);

    let burning = true;
    let ownLight = true;
    let time = 0;
    useUpdate((delta) => {
        time += delta;
        const flicker = Math.sin(time * 8) * 0.08 + Math.sin(time * 19) * 0.05 + Math.sin(time * 31) * 0.03;
        lamp.intensity = burning ? 1.1 + flicker : 0;
        lamp.radius = 170 + flicker * 60;
        flames.forEach((flame, i) => {
            flame.visible = burning;
            flame.transform.y = -6 + i + Math.sin(time * (11 + i * 5)) * 0.8;
        });
    });

    return {
        get burning() { return burning; },
        get ownLight() { return ownLight; },
        toggleFire: () => {
            burning = !burning;
            const embersNow = ownLight ? glowing : dimmed;
            if (burning) playParticles(embersNow); else stopParticles(embersNow);
        },
        toggleEmbers: () => {
            ownLight = !ownLight;
            stopParticles(ownLight ? dimmed : glowing);
            if (burning) playParticles(ownLight ? glowing : dimmed);
        },
    };
};
```

`level.ts`:

```ts
import { createScene, createSprite, createText, useAmbientLight2d, useKeyboard, useScreenSpace, useSpawn, useUpdate } from 'nacatamalon';
import { Clearing, Fireflies } from './clearing';
import { Campfire } from './fire';
import { FIREFLIES, HEIGHT, WIDTH } from './settings';

const Hud = () => {
    useScreenSpace();
    createSprite({ width: WIDTH, height: 40, anchor: { x: 0, y: 0 }, tint: '#0c0f1a', zIndex: 100 });
    createText({ text: '2D LIGHTS / CAMPFIRE', tint: '#e9e2d6', transform: { x: 10, y: 8 }, zIndex: 101 });
    const status = createText({ text: '', tint: '#ffbf75', style: { fontSize: 8 }, transform: { x: 10, y: 25 }, zIndex: 101 });
    createSprite({ width: WIDTH, height: 18, anchor: { x: 0, y: 0 }, tint: '#0c0f1a', transform: { y: HEIGHT - 18 }, zIndex: 100 });
    createText({ text: 'F FIRE  E EMBERS LIT OR GLOWING  M MOON', tint: '#c3c8d6', style: { fontSize: 8 },
        transform: { x: 10, y: HEIGHT - 13 }, zIndex: 101 });
    return status;
};

export const Level = () => {
    const moonlight = useAmbientLight2d({ color: '#7f8fd6', intensity: 0.14 });
    useSpawn(Clearing)();
    useSpawn(() => Fireflies(FIREFLIES))();
    const fire = useSpawn(Campfire)().api;
    const status = useSpawn(Hud)().api;
    const keys = useKeyboard();
    let moon = false;

    useUpdate(() => {
        if (keys.justPressed('f')) fire.toggleFire();
        if (keys.justPressed('e')) fire.toggleEmbers();
        if (keys.justPressed('m')) moon = !moon;
        moonlight.intensity = moon ? 0.45 : 0.14;
        status.text = 'FIRE ' + (fire.burning ? 'BURNING' : 'OUT') + '  EMBERS ' + (fire.ownLight ? 'GLOWING (useUnlit2d)' : 'LIT BY THE WORLD')
            + '  MOON ' + (moon ? 'FULL' : 'NEW');
    });
    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 270;
export const GROUND_Y = 214;
export const FIRE_X = 240;
export const FIRE_Y = GROUND_Y - 6;
export const FIREFLIES = 6;
```
