# Pixel snap scroll

> A platformer level scrolled slowly at zoom 3: on the screen's pixels even the far clouds glide, on the game's they step three at once, and between pixels the tiles ripple.

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

A level scrolled slowly by a camera moved by hand, a fraction of a pixel a frame, at zoom 3, with clouds and hills behind it at their own pace. Gammix flies in front, pinned to the screen at its own size.

With `pixelSnap: 'screen'`, the default for pixel art, everything (the camera and the far layers too) is drawn on whole pixels of the screen: every pixel of the art stays three wide, and even the clouds, at a tenth of the camera, glide one screen pixel at a time.

Press P to go through the modes while it scrolls (`useGame().setPixelSnap`). `'game'` draws on whole pixels of the art, as the consoles did: just as crisp, but the slow clouds jump three screen pixels at once. `'off'` draws between pixels: watch the edge of the grass, its columns come out a screen pixel narrower or wider from frame to frame, and the level ripples. Slower is easier to see: 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: '#9fd4ff',
    step: 60,
    actions: ACTIONS
});

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

`components/flyer.ts`:

```ts
import { createSprite, useLoadTexture, useScreenSpace } from 'nacatamalon';

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

    return createSprite({
        texture: useLoadTexture({
            src: '/example-assets/assets/gammix.png'
        }),
        transform: {
            x: 150,
            y: 120
        }
    });
};
```

`components/hud.ts`:

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

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

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

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

    createText({
        text: 'P SNAP MODE   LEFT RIGHT SPEED   SPACE STOP',
        transform: {
            x: 8,
            y: 26
        },
        tint: '#40507a'
    });

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

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

`components/scenery.ts`:

```ts
import { createBackdrop, createTilemap, useLoadTexture } from 'nacatamalon';

export const Scenery = () => {
    createBackdrop({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/clouds.png'
        }),
        parallax: {
            x: 0.1,
            y: 1
        },
        y: 216
    });
    createBackdrop({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/hills.png'
        }),
        parallax: {
            x: 0.4,
            y: 1
        },
        y: 226
    });
    createTilemap({
        src: '/example-assets/assets/platformer/wide.tilemap'
    });
};
```

`hooks/use_scroll_controls.ts`:

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

export const useScrollControls = (camera: TCamera2d, flyer: TSprite, hud: THud) => {
    const input = useActions();
    const game = useGame();
    let speed = INITIAL_SPEED;
    let way = 1;

    hud.showSnap(game.getPixelSnap());

    useUpdate(() => {
        if (input.justPressed('mode')) {
            game.setPixelSnap(MODES[(MODES.indexOf(game.getPixelSnap()) + 1) % MODES.length]);
            hud.showSnap(game.getPixelSnap());
        }

        if (input.justPressed('right')) {
            speed = Math.min(2, speed + 0.05);
        }

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

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

        // Deliberately measured per frame with step: 60, preserving subpixel positions.
        camera.transform.x += speed * way;

        if (camera.transform.x > LEVEL_WIDTH - VIEW.width) {
            way = -1;
        }

        if (camera.transform.x < 0) {
            way = 1;
        }

        flyer.flipX = way < 0;
        hud.showPosition(camera.transform.x, speed);
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, useCamera2d, useSpawn } from 'nacatamalon';
import { Scenery } from '../components/scenery';
import { Flyer } from '../components/flyer';
import { Hud } from '../components/hud';
import { useScrollControls } from '../hooks/use_scroll_controls';
import { VIEW, ZOOM } from '../settings';

export const Level = () => {
    const camera = useCamera2d({
        zoom: ZOOM,
        transform: {
            y: 320 - VIEW.height
        }
    });

    useSpawn(Scenery)();

    const flyer = useSpawn(Flyer)();
    const hud = useSpawn(Hud)();

    useScrollControls(camera, flyer.api, 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: 160,
    height: 107
};
export const WIDTH = VIEW.width * ZOOM;
export const HEIGHT = VIEW.height * ZOOM;
export const LEVEL_WIDTH = 90 * 16;
export const INITIAL_SPEED = 0.25;
export const MODES: TPixelSnap[] = [RENDER.PIXEL_SNAP.SCREEN, RENDER.PIXEL_SNAP.GAME, RENDER.PIXEL_SNAP.OFF];
export const COLORS = {
    screen: '#1f8a3b',
    game: '#2a5db0',
    off: '#c0392b'
};
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'
            }
        ]
    }
];
```
