# CRT showcase

> A 16-bit game seen through a television: one line of crt() on top of an ordinary scene.

Example `crt-showcase`, running at https://nacatamalon.com/en/examples/crt-showcase. The whole catalogue: https://nacatamalon.com/llms/examples.md.

A 16-bit game seen through a television. Everything underneath is an ordinary scene: a forest in five layers, coins, a hero and an arcade HUD at 320 × 224. The tube is one line, `usePostProcess(crt(POST_EFFECTS.CRT_PRESETS.consumer))`, added on top and knowing nothing about the game.

It runs at `pixelRatio: 'device'`: on a dense screen each scanline gets room to take the shape of a beam.

Click the preview first so it hears the keyboard. P steps through the four tubes in `POST_EFFECTS.CRT_PRESETS` (the composite one also puts `ntsc()` before the tube, the order the signal travelled in), space takes the tube away to compare, and F goes full screen, where it is scaled in whole steps and looks its best.

## Code

`index.ts`:

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

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

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

`components/arcade.ts`:

```ts
import { createSprite, createText, getColor, useGame, useKeyboard, useLoadAtlas, useLoadBitmapFont, useLoadTexture, useSpawn, useSpriteAnimation, useUpdate } from 'nacatamalon';
import { crt, ntsc, usePostProcess, POST_EFFECTS } from 'nacatamalon/effects';
import { LAYERS, TUBES } from '../settings';
import { Layer } from './layer';

export const Arcade = () => {
    for (const [src, speed] of LAYERS) {
        useSpawn(Layer)(src, speed);
    }

    // A row of coins floating over the path, each spinning at its own pace.
    const coin = useLoadAtlas({
        src: '/example-assets/assets/atlases/coin.atlas'
    });

    for (let i = 0; i < 6; i++) {
        const sprite = createSprite({
            atlas: coin,
            width: 16,
            height: 16,
            zIndex: 1,
            transform: {
                x: 70 + i * 36,
                y: 120
            }
        });

        useSpriteAnimation(sprite, {
            clips: coin.sequences,
            play: 'spin'
        }).setSpeed(0.8 + (i % 3) * 0.2);
    }

    const hero = createSprite({
        texture: useLoadTexture({
            src: '/example-assets/assets/gammix.png'
        }),
        width: 32,
        height: 32,
        zIndex: 1,
        transform: {
            x: 60,
            y: 176
        },
    });

    const font = useLoadBitmapFont({
        json: '/example-assets/fonts/bitmaps/nacatamal-arcade/nacatamal-arcade.json',
        atlas: '/example-assets/fonts/bitmaps/nacatamal-arcade/nacatamal-arcade.png',
    });

    const text = (value: string, x: number, y: number, hex: string) => createText({
        text: value,
        font,
        style: {
            fontSize: 8
        },
        tint: getColor(hex),
        zIndex: 2,
        transform: {
            x,
            y
        },
    });

    text('1UP', 12, 10, '#ff5a5a');

    const score = text('000000', 12, 20, '#ffffff');

    text('HI-SCORE', 124, 10, '#ff5a5a');
    text('050000', 132, 20, '#ffffff');
    text('WORLD 1-1', 236, 10, '#ffd75a');

    const start = text('PRESS START', 116, 150, '#ffffff');
    const label = text('', 16, 196, '#8ee6a0');
    const keysHelp = text('', 16, 206, '#8c82a6');

    // The whole look. Installed in this order, so the cable runs before the tube.
    const cable = usePostProcess({
        ...ntsc(),
        enabled: false
    });

    const tube = usePostProcess(crt(POST_EFFECTS.CRT_PRESETS.consumer));
    let current = 0;

    const show = () => {
        label.text = tube.enabled ? TUBES[current].name : 'NO TUBE';
        keysHelp.text = 'P TUBE  SPACE ' + (tube.enabled ? 'OFF' : 'ON') + '  F FULL SCREEN';
    };

    show();

    const game = useGame();
    const keys = useKeyboard();
    let points = 0;

    useUpdate((delta, time) => {
        hero.transform.y = 176 - Math.abs(Math.sin(time * 5)) * 10;
        points += delta * 140;
        score.text = String(Math.floor(points) * 10).padStart(6, '0');
        start.visible = Math.floor(time * 2) % 2 === 0;

        if (keys.justPressed('p')) {
            current = (current + 1) % TUBES.length;
            Object.assign(tube.uniforms, crt(TUBES[current].preset).uniforms);
            tube.enabled = true;
            cable.enabled = TUBES[current].cable;
            show();
        }

        if (keys.justPressed(' ')) {
            tube.enabled = !tube.enabled;
            cable.enabled = tube.enabled && TUBES[current].cable;
            show();
        }

        if (keys.justPressed('f')) {
            void game.enterFullscreen();
        }
    });
};
```

`components/layer.ts`:

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

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

export const Layer = (src: string, speed: number) => {
    const texture = useLoadTexture({
        src
    });

    const copies = [0, 1].map((i) => createSprite({
        texture,
        width: WIDTH,
        height: HEIGHT,
        anchor: {
            x: 0,
            y: 0
        },
        zIndex: -1,
        transform: {
            x: i * WIDTH,
            y: 0
        },
    }));

    useUpdate((delta) => {
        for (const copy of copies) {
            copy.transform.x -= speed * delta;

            if (copy.transform.x <= -WIDTH) {
                copy.transform.x += 2 * WIDTH;
            }
        }
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, useSpawn } from 'nacatamalon';
import { Arcade } from '../components/arcade';

export const Game = () => {
    useSpawn(Arcade)();

    return createScene();
};
```

`settings.ts`:

```ts
import { POST_EFFECTS } from 'nacatamalon/effects';

export const WIDTH = 320;
export const HEIGHT = 224;
export const FOREST = '/example-assets/assets/complete_maps/forest/background/Layers/';
export const LAYERS: [string, number][] = [[FOREST + '1.png', 6], [FOREST + '2.png', 14], [FOREST + '3.png', 26], [FOREST + '4.png', 44], [FOREST + '5.png', 70],];
export const TUBES = [
    {
        name: 'CONSUMER TV',
        preset: POST_EFFECTS.CRT_PRESETS.consumer,
        cable: false
    },
    {
        name: 'STUDIO MONITOR',
        preset: POST_EFFECTS.CRT_PRESETS.pvm,
        cable: false
    },
    {
        name: 'ARCADE CABINET',
        preset: POST_EFFECTS.CRT_PRESETS.arcade,
        cable: false
    },
    {
        name: 'COMPOSITE CABLE',
        preset: POST_EFFECTS.CRT_PRESETS.composite,
        cable: true
    },
];
```
