# What the 2D camera skips

> What the camera cannot see is left out as the frame is collected, and a map draws only the cells on screen.

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

A sprite, a text or a panel wholly outside what its camera shows is left out as the frame is collected: not sorted, not laid out, not written for the card, and a map layer draws only the cells the camera shows. A field of small squares and a map of 131,072 cells lie far apart, and a window seen from further out shows what the camera skips.

Click the preview first so it hears the keyboard. The arrows move the camera. X adds 10,000 squares until the FPS drop below your screen's rate, then 1, 2 and 3 look at the squares, the map and nothing. Looking away, the frame gets cheap: only placing each square is left. M takes the map away and back, Z takes squares away.

## Code

`index.ts`:

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

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

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

`components/batch.ts`:

```ts
import { createSprite } from 'nacatamalon';

import { BATCH, FIELD, COLORS } from '../settings';

export const Batch = (first: number, placed: number[]) => {
    for (let i = 0; i < BATCH; i++) {
        const x = Math.random() * FIELD, y = Math.random() * FIELD;

        createSprite({
            width: 6,
            height: 6,
            tint: COLORS[(first + i) % 4],
            transform: {
                x,
                y
            }
        });
        placed.push(x, y);
    }
};
```

`components/big_map.ts`:

```ts
import { createTilemap } from 'nacatamalon';

import { MAP } from '../settings';

export const mapFile = () => {
    const data = new Array(MAP.columns * MAP.rows);

    for (let i = 0; i < data.length; i++) {
        data[i] = 1 + ((i * 7 + (i >> 9)) % 2);
    }

    const doc = {
        format: 1,
        kind: 'tilemap',
        atlas: location.origin + '/example-assets/assets/platformer/tiles.atlas',
        cell: MAP.cell,
        width: MAP.columns,
        height: MAP.rows,
        tiles: {
            1: {
                frame: 0
            },
            2: {
                frame: 1
            }
        },
        layers: [
            {
                name: 'ground',
                order: 'under',
                data
            }
        ],
    };

    return URL.createObjectURL(new Blob([JSON.stringify(doc)], {
        type: 'application/json'
    }));
};
export const BigMap = (src: string) => createTilemap({
    src,
    transform: {
        x: MAP.x,
        y: MAP.y
    }
});
```

`components/hud.ts`:

```ts
import { createSprite, createText, useScreenSpace } from 'nacatamalon';
import type { TTexture } from 'nacatamalon';

import { WIDTH, HEIGHT } from '../settings';

export const Hud = (skipped: TTexture) => {
    useScreenSpace();
    createSprite({
        texture: skipped,
        anchor: {
            x: 1,
            y: 1
        },
        transform: {
            x: WIDTH - 6,
            y: HEIGHT - 6
        },
        zIndex: 100
    });
    createSprite({
        width: WIDTH,
        height: 40,
        tint: '#0b0f18',
        anchor: {
            x: 0,
            y: 0
        },
        transform: {
            x: 0,
            y: 0
        },
        zIndex: 100
    });

    return {
        fps: createText({
            text: '',
            tint: '#ffd84f',
            transform: {
                x: 8,
                y: 6
            },
            zIndex: 101
        }),
        what: createText({
            text: '',
            tint: '#9aa7b4',
            transform: {
                x: 8,
                y: 22
            },
            zIndex: 101
        }),
    };
};
```

`scenes/demo.ts`:

```ts
import { createScene, createSpriteTexture, destroy, useCamera2d, useKeyboard, useSpawn, useUpdate, useCleanup } from 'nacatamalon';

import { WIDTH, HEIGHT, BATCH, FIELD, MAP, NOWHERE, WINDOW } from '../settings';
import { Batch } from '../components/batch';
import { BigMap, mapFile } from '../components/big_map';
import { Window } from '../views/window';
import { Hud } from '../components/hud';

export const Level = () => {
    const camera = useCamera2d();
    const mapSource = mapFile();

    useCleanup(() => URL.revokeObjectURL(mapSource));

    const placed: number[] = [];
    const addBatch = useSpawn(Batch);
    const addMap = useSpawn(() => BigMap(mapSource));
    // The map first: its layer goes under the squares, so made first the scene is already in the
    // order it is drawn in, and is not sorted every frame.
    let map: ReturnType<typeof addMap> | null = addMap();
    const batches = [addBatch(0, placed), addBatch(1, placed)];

    const skipped = createSpriteTexture({
        width: WINDOW.width,
        height: WINDOW.height,
        sees: 'world',
        background: '#05070c'
    }, Window, camera);

    const hud = useSpawn(Hud)(skipped).api;

    const go = (x: number, y: number) => {
        camera.transform.x = x;
        camera.transform.y = y;
    };

    go(FIELD / 2 - WIDTH / 2, FIELD / 2 - HEIGHT / 2);

    const keys = useKeyboard();
    const axis = (neg: string, pos: string) => (keys.isDown(pos) ? 1 : 0) - (keys.isDown(neg) ? 1 : 0);
    let frame = 1 / 60, counted = 0, onScreen = 0;

    useUpdate((dt) => {
        if (keys.justPressed('1')) {
            go(FIELD / 2 - WIDTH / 2, FIELD / 2 - HEIGHT / 2);
        }

        if (keys.justPressed('2')) {
            go(MAP.x + 200, MAP.y + 200);
        }

        if (keys.justPressed('3')) {
            go(NOWHERE.x, NOWHERE.y);
        }

        if (keys.justPressed('x')) {
            batches.push(addBatch(batches.length, placed));
        }

        const last = keys.justPressed('z') ? batches.pop() : undefined;

        if (last) {
            destroy(last);
            placed.length = batches.length * BATCH * 2;
        }

        if (keys.justPressed('m')) {
            if (map) {
                destroy(map);
                map = null;
            } else {
                map = addMap();
            }
        }

        camera.transform.x += axis('ArrowLeft', 'ArrowRight') * dt * 600;
        camera.transform.y += axis('ArrowUp', 'ArrowDown') * dt * 600;

        // Smoothed, so the number can be read.
        frame += (dt - frame) * 0.05;
        counted += dt;

        if (counted > 0.5) {
            counted = 0;

            const { x, y } = camera.transform;

            onScreen = 0;

            for (let i = 0; i < placed.length; i += 2) {
                // Each square is 6 across, held by its middle.
                if (placed[i] > x - 3 && placed[i] < x + WIDTH + 3 && placed[i + 1] > y - 3 && placed[i + 1] < y + HEIGHT + 3) {
                    onScreen++;
                }
            }
        }

        const squares = batches.length * BATCH;

        if (!hud) {
            return;
        }

        hud.fps.text = Math.round(1 / frame) + ' FPS   ' + (frame * 1000).toFixed(1) + ' MS A FRAME';
        hud.what.text = squares + ' SQUARES, ' + onScreen + ' ON SCREEN   MAP ' + (map ? MAP.columns * MAP.rows + ' CELLS' : 'OFF');
    });

    return createScene();
};
```

`settings.ts`:

```ts
import { getColor } from 'nacatamalon';

// WHAT THE 2D CAMERA SKIPS. A sprite, a text or a panel wholly outside what its camera shows is
// left out as the frame is collected: not sorted, not laid out, not written for the card. A map
// layer draws only the rows of cells the camera shows.
//
// A field of small squares and a huge map, far apart. Add squares until the FPS drop below your
// screen's rate, so the cost shows at all, then look at nothing: the frame gets cheap, because
// only placing each square is left.
//
// The window in the corner is a view of the same world from much further out, which skips what the
// main camera skips (its cullingCamera): only the squares on screen are in it, with a clear edge
// where the camera's sight ends.
//
//   1  over the squares   2  over the map   3  at nothing   Arrows  pan
//   X  10,000 more squares   Z  10,000 fewer   M  the map on and off

export const WIDTH = 480;
export const HEIGHT = 270;
export const BATCH = 10000;
export const FIELD = 4000;
// the squares live in 0..4000 on both axes
export const MAP = {
    x: 6000,
    y: 0,
    columns: 512,
    rows: 256,
    cell: 16
};
export const NOWHERE = {
    x: -40000,
    y: -40000
};
export const COLORS = ['#5bd6a0', '#e8a54b', '#6aa7ff', '#ff6b8a'].map((c) => getColor(c));
// The window: a view of the world four times further out, centred on what the main camera looks at,
// and skipping what that camera skips. The rectangle is the camera's sight, drawn only in the window.
export const WINDOW = {
    width: 200,
    height: 112,
    zoom: 1 / 4
};
```

`views/window.ts`:

```ts
import { createSprite, useCamera2d, useUpdate } from 'nacatamalon';
import type { TCamera2d } from 'nacatamalon';
import { WIDTH, HEIGHT, WINDOW } from '../settings';

export const Window = (eye: TCamera2d) => {
    const above = useCamera2d({
        zoom: WINDOW.zoom
    });

    above.cullingCamera = eye;

    const sight = createSprite({
        width: WIDTH,
        height: HEIGHT,
        tint: '#ffffff30',
        anchor: {
            x: 0,
            y: 0
        }
    });

    useUpdate(() => {
        above.transform.x = eye.transform.x + WIDTH / 2 - WINDOW.width / 2 / WINDOW.zoom;
        above.transform.y = eye.transform.y + HEIGHT / 2 - WINDOW.height / 2 / WINDOW.zoom;
        sight.transform.x = eye.transform.x;
        sight.transform.y = eye.transform.y;
    });
};
```
