# Pixel snap camera

> A camera moved by hand a fraction of a pixel a frame over stripes one pixel wide: with pixelSnap they keep their width. P goes through screen, game and off.

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

A camera moved by hand a fraction of a pixel a frame, over stripes one pixel wide, at zoom 3. With `pixelSnap` the scene's camera is drawn on whole pixels like everything else, so the stripes stay the same width: on the screen's pixels (`'screen'`, the default for pixel art) they slide one screen pixel at a time, on the game's (`'game'`) three at once, as the consoles drew.

Where the camera is keeps its fractions, so it moves exactly as fast as it says, and the pointer reads the world through the camera as it is drawn. Press P to go through the modes (`useGame().setPixelSnap`, with `RENDER.PIXEL_SNAP`) and, with `'off'`, see the stripes flicker: now and then, as the camera lands half a pixel between two, they come out two and four pixels wide. The arrows change the speed and Space stops it.

## Code

`index.ts`:

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

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: '#1b1f3a',
    step: 60,
    actions: ACTIONS
});

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

`components/hud.ts`:

```ts
import { createText, getColor, useScreenSpace } from 'nacatamalon';
import type { TPixelSnap } from 'nacatamalon';
import { COLORS, HEIGHT } from '../settings';

export const Hud = () => {
    useScreenSpace();

    const position = createText({
        text: '',
        transform: {
            x: 8,
            y: 8
        },
        tint: '#9aa3c7'
    });

    const snap = createText({
        text: '',
        transform: {
            x: 8,
            y: HEIGHT - 20
        }
    });

    return {
        showPosition: (x: number, speed: number) => {
            position.text = 'CAMERA X ' + x.toFixed(2) + '  SPEED ' + speed.toFixed(1) + ' PX/FRAME';
        },
        showSnap: (mode: TPixelSnap) => {
            snap.text = 'PIXEL SNAP ' + mode.toUpperCase() + '  (P)';
            snap.tint = getColor(COLORS[mode]);
        }
    };
};

export type THud = ReturnType<typeof Hud>;
```

`components/stripes.ts`:

```ts
import { createSprite } from 'nacatamalon';
import { createPixelTexture, createPixels, fillRect } from 'nacatamalon/pixels';

export const Stripes = () => {
    const pixels = createPixels(240, 40, '#1b1f3a');

    for (let x = 0; x < 240; x += 2) {
        fillRect(pixels, x, 0, 1, 40, '#f2d16b');
    }

    createSprite({
        texture: createPixelTexture(pixels),
        anchor: {
            x: 0,
            y: 0
        },
        transform: {
            y: 10
        }
    });
};
```

`hooks/use_camera_controls.ts`:

```ts
import { useActions, useGame, useUpdate } from 'nacatamalon';
import type { TCamera2d } from 'nacatamalon';
import type { THud } from '../components/hud';
import { INITIAL_SPEED, MODES, VIEW } from '../settings';

export const useCameraControls = (camera: TCamera2d, hud: THud) => {
    const input = useActions();
    const game = useGame();
    let speed = INITIAL_SPEED;

    hud.showSnap(game.getPixelSnap());

    useUpdate(() => {
        if (input.justPressed('mode')) {
            const next = (MODES.indexOf(game.getPixelSnap()) + 1) % MODES.length;

            game.setPixelSnap(MODES[next]);
            hud.showSnap(game.getPixelSnap());
        }

        if (input.justPressed('right')) {
            speed += 0.1;
        }

        if (input.justPressed('left')) {
            speed = Math.max(0, speed - 0.1);
        }

        if (input.justPressed('stop')) {
            speed = speed === 0 ? INITIAL_SPEED : 0;
        }

        // Deliberately pixels per frame at step: 60: fractions are the point of this comparison.
        camera.transform.x = (camera.transform.x + speed) % VIEW.width;
        hud.showPosition(camera.transform.x, speed);
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, useCamera2d, useSpawn } from 'nacatamalon';
import { Stripes } from '../components/stripes';
import { Hud } from '../components/hud';
import { useCameraControls } from '../hooks/use_camera_controls';
import { ZOOM } from '../settings';

export const Level = () => {
    const camera = useCamera2d({
        zoom: ZOOM
    });

    useSpawn(Stripes)();

    const hud = useSpawn(Hud)();

    useCameraControls(camera, hud.api);

    return createScene();
};
```

`settings.ts`:

```ts
import { INPUT, RENDER } from 'nacatamalon';
import type { TActionMap, TPixelSnap } from 'nacatamalon';

export const ZOOM = 3;
export const VIEW = {
    width: 120,
    height: 60
};
export const WIDTH = VIEW.width * ZOOM;
export const HEIGHT = VIEW.height * ZOOM;
export const INITIAL_SPEED = 0.3;
export const MODES: TPixelSnap[] = [RENDER.PIXEL_SNAP.SCREEN, RENDER.PIXEL_SNAP.GAME, RENDER.PIXEL_SNAP.OFF];
export const COLORS = {
    screen: '#7be38f',
    game: '#7bb4ff',
    off: '#ff7a7a'
};
export const ACTIONS: TActionMap = [
    ...INPUT.ACTION_PRESETS.platformer.filter(action => ['left', 'right'].includes(action.name)),
    {
        name: 'mode',
        bindings: [
            {
                type: 'key',
                key: 'p'
            },
            {
                type: 'button',
                button: 'y'
            }
        ]
    },
    {
        name: 'stop',
        bindings: [
            {
                type: 'key',
                key: ' '
            },
            {
                type: 'button',
                button: 'a'
            }
        ]
    }
];
```
