# 2D lighting

> A light follows the character and illuminates the world sprites. Move the explorer with the arrow keys and compare both renderers.

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

A light follows the character and illuminates the world sprites. Move the explorer with the arrow keys and compare both renderers.

## 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: '#080c16',
    pixelSnap: 'screen',
});

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

`level.ts`:

```ts
import {
    createScene, createSprite, createText, getColor, useAmbientLight2d, useCamera2d,
    useKeyboard, useLight2d, useScreenSpace, useSpawn, useTransform, useUpdate,
} from 'nacatamalon';
import { clamp } from 'nacatamalon/math';
import { Room, Torch } from './room';
import { WIDTH, HEIGHT, WORLD_WIDTH, WORLD_HEIGHT } from './settings';

const Explorer = () => {
    const place = useTransform({ x: WORLD_WIDTH / 2, y: WORLD_HEIGHT / 2 });
    createSprite({ width: 12, height: 15, tint: '#d9e3ed', zIndex: 10 });
    createSprite({ width: 8, height: 5, tint: '#607a9e', transform: { y: -3 }, zIndex: 11 });
    createSprite({ width: 4, height: 5, tint: '#ffc988', transform: { x: 9, y: 3 }, zIndex: 11 });
    // The light belongs to the explorer's box: moving the box moves the lamp too.
    const lamp = useLight2d({ color: '#ffd195', radius: 96, intensity: 1.1, transform: { x: 9, y: 3 } });
    return { place, lamp };
};

const Hud = () => {
    useScreenSpace();
    createSprite({ width: WIDTH, height: 44, anchor: { x: 0, y: 0 }, tint: '#101929', zIndex: 100 });
    createText({ text: '2D LIGHTS / THE LANTERN ROOM', tint: '#e1eaf6', transform: { x: 10, y: 8 }, zIndex: 101 });
    const status = createText({ text: '', tint: '#ffcf89', transform: { x: 10, y: 26 }, zIndex: 101 });
    createSprite({ width: WIDTH, height: 30, anchor: { x: 0, y: 0 }, tint: '#101929', transform: { y: HEIGHT - 30 }, zIndex: 100 });
    createText({ text: 'ARROWS/WASD MOVE  SPACE LAMP  F FALLOFF\nC COLOUR  -/+ RADIUS  B AMBIENT  R RESET',
        tint: '#bbcbdc', style: { fontSize: 8, lineSpacing: 3 }, transform: { x: 10, y: HEIGHT - 25 }, zIndex: 101 });
    return status;
};

export const Level = () => {
    const ambient = useAmbientLight2d({ color: '#b7c8ef', intensity: 0.12 });
    const camera = useCamera2d();
    useSpawn(Room)();
    useSpawn(() => Torch(224, 156, '#ff9b58', 0))();
    useSpawn(() => Torch(520, 184, '#75baff', 2))();
    useSpawn(() => Torch(392, 340, '#ce8dff', 4))();
    const { place, lamp } = useSpawn(Explorer)().api;
    const status = useSpawn(Hud)().api;
    const keys = useKeyboard();
    const colours = ['#ffd195', '#8ddaff', '#a8ffb0'];
    const falloffs = [1, 2, 0];
    let colour = 0;
    let falloff = 0;
    let on = true;
    let bright = false;

    const updateStatus = () => {
        status.text = 'LAMP ' + (on ? 'ON' : 'OFF') + '  RADIUS ' + lamp.radius +
            '  FALLOFF ' + lamp.falloff + '  AMBIENT ' + (bright ? 'DAY' : 'NIGHT');
    };
    const follow = () => {
        camera.transform.x = clamp(place.x - WIDTH / 2, 0, WORLD_WIDTH - WIDTH);
        camera.transform.y = clamp(place.y - HEIGHT / 2, 0, WORLD_HEIGHT - HEIGHT);
    };
    follow(); updateStatus();

    useUpdate((delta) => {
        const x = Number(keys.isDown('ArrowRight') || keys.isDown('d')) - Number(keys.isDown('ArrowLeft') || keys.isDown('a'));
        const y = Number(keys.isDown('ArrowDown') || keys.isDown('s')) - Number(keys.isDown('ArrowUp') || keys.isDown('w'));
        const length = Math.hypot(x, y) || 1;
        place.x = clamp(place.x + x / length * 100 * delta, 28, WORLD_WIDTH - 28);
        place.y = clamp(place.y + y / length * 100 * delta, 28, WORLD_HEIGHT - 28);
        if (keys.justPressed(' ')) on = !on;
        if (keys.justPressed('f')) { falloff = (falloff + 1) % falloffs.length; lamp.falloff = falloffs[falloff]; }
        if (keys.justPressed('c')) { colour = (colour + 1) % colours.length; lamp.color = getColor(colours[colour]); }
        if (keys.justPressed('-')) lamp.radius = Math.max(32, lamp.radius - 16);
        if (keys.justPressed('=') || keys.justPressed('+')) lamp.radius = Math.min(192, lamp.radius + 16);
        if (keys.justPressed('b')) bright = !bright;
        if (keys.justPressed('r')) {
            place.x = WORLD_WIDTH / 2; place.y = WORLD_HEIGHT / 2;
            on = true; bright = false; colour = 0; falloff = 0;
            lamp.radius = 96; lamp.falloff = 1; lamp.color = getColor(colours[0]);
        }
        lamp.intensity = on ? 1.1 : 0;
        ambient.intensity = bright ? 1 : 0.12;
        follow(); updateStatus();
    });
    return createScene();
};
```

`room.ts`:

```ts
import { createSprite, createSpriteAtlas, createTilemap, useLight2d, useSpawn, useTransform, useUnlit2d, useUpdate } from 'nacatamalon';
import { createPixelTexture, sheetFromRows } from 'nacatamalon/pixels';
import { CELL, COLUMNS, ROWS } from './settings';

export const Room = () => {
    // A real tilemap, so the floor is one layer rather than one sprite per cell.
    const tiles = sheetFromRows({
        floor: ['ssssssss', 'sSSsssss', 'sSSsssss', 'ssssssss', 'ssssssss', 'sssssSss', 'ssssssss', 'SSSSSSSS'],
        worn: ['ssssssss', 'ssssssss', 'ssSsssss', 'sssSssss', 'ssssSsss', 'ssssssss', 'ssssssss', 'SSSSSSSS'],
        wall: ['BBBBBBBB', 'bBBBbBBB', 'bbbbbbbb', 'BBbBBBbB', 'bbbbbbbb', 'bBBBbBBB', 'bbbbbbbb', 'dddddddd'],
    }, { s: '#818c9e', S: '#657285', b: '#89909b', B: '#b0b5bd', d: '#4d5668' });
    createTilemap({
        doc: {
            cell: CELL, width: COLUMNS, height: ROWS,
            tiles: { 1: { frame: tiles.frames.floor }, 2: { frame: tiles.frames.worn }, 3: { frame: tiles.frames.wall } },
            layers: [{ name: 'stone', order: 'under', data: Array.from({ length: COLUMNS * ROWS }, (_, i) => {
                const x = i % COLUMNS;
                const y = Math.floor(i / COLUMNS);
                return x === 0 || y === 0 || x === COLUMNS - 1 || y === ROWS - 1 ? 3 : (x * 7 + y * 3) % 11 === 0 ? 2 : 1;
            }) }],
        },
        atlas: createSpriteAtlas({ texture: createPixelTexture(tiles.pixels), columns: tiles.columns }),
    });

    // Ordinary sprites receive the same light as the floor. These are decorations, not occluders.
    for (const [x, y] of [[280, 140], [464, 136], [256, 288], [496, 288], [344, 328]]) {
        createSprite({ width: 30, height: 30, tint: '#b79675', transform: { x, y }, zIndex: 2 });
        createSprite({ width: 22, height: 2, tint: '#705c4a', transform: { x, y: y - 9 }, zIndex: 3 });
        createSprite({ width: 2, height: 26, tint: '#705c4a', transform: { x: x - 8, y }, zIndex: 3 });
    }
};

export const Torch = (x: number, y: number, color: string, phase: number) => {
    useTransform({ x, y });
    createSprite({ width: 12, height: 16, tint: '#424d62', zIndex: 4 });
    // The flame gives off its own light: left out of the lamps, it keeps its colour in the dark.
    useSpawn(() => {
        useUnlit2d();
        createSprite({ width: 6, height: 6, tint: color, transform: { y: -4 }, zIndex: 5 });
    })();
    const light = useLight2d({ color, radius: 110, intensity: 1.05, falloff: 1.5 });
    let time = phase;
    useUpdate((delta) => {
        time += delta;
        light.intensity = 1.05 + Math.sin(time * 7) * 0.07 + Math.sin(time * 13) * 0.03;
    });
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 300;
export const CELL = 16;
export const COLUMNS = 48;
export const ROWS = 27;
export const WORLD_WIDTH = COLUMNS * CELL;
export const WORLD_HEIGHT = ROWS * CELL;
```
