# Gammix Dance

> Gammix bobs to the music, three frames chosen by where the song is in its beat with song.beat, and turns round every four bars with onBeat.

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

Gammix bobs to the music: three frames of him, each a pixel shorter in the legs, chosen by where the song is in its beat with `song.beat`: down on the beat, up before the next. Every four bars he turns round once, started by `onBeat`. Both go by what is heard, so he stays in time at any tempo.

The arrows left and right change the song, up and down the tempo, and space plays it or stops it.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { Dance } from './scenes/demo';
import * as Settings from './settings';

const game = createGame('#app', {
    width: Settings.WIDTH,
    height: Settings.HEIGHT,
    background: '#14121c'
});

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

`components/dancer.ts`:

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

export const Dancer = (song: TSongHandle) => {
    const sheet = createSpriteAtlas({
        texture: useLoadTexture({
            src: '/example-assets/assets/gammix-bob.png',
            key: 'gammix-bob'
        }),
        columns: 3
    });

    // Three times his size, in whole pixels.
    const gammix = createSprite({
        atlas: sheet,
        frame: 0,
        width: 192,
        height: 192,
        transform: {
            x: 240,
            y: 140
        }
    });

    // Where in the song his last turn started, in beats: it goes across the first beat of a bar,
    // every four bars (1, 5, 9…).
    let turnFrom: number | null = null;

    song.onBeat((beat, bar) => {
        if (beat === 1 && (bar - 1) % 4 === 0) {
            turnFrom = song.beat;
        }
    });

    useUpdate(() => {
        const beat = song.beat;

        if (beat === null) {
            gammix.frame = 0;
            gammix.transform.scaleX = 1;
        } else {
            // 0 right on the beat, close to 1 just before the next.
            const into = beat - Math.floor(beat);

            gammix.frame = into < 0.15 ? 2 : into < 0.35 ? 1 : 0;

            // Round once on the spot: as wide as he is, then edge on, then the other way, and back.
            const turn = turnFrom === null ? 1 : beat - turnFrom;

            gammix.transform.scaleX = turn >= 0 && turn < 1 ? Math.cos(2 * Math.PI * turn) : 1;
        }
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, createText, useKeyboard, useLoadSong, useScene, useSong, useUpdate, useSpawn } from 'nacatamalon';
import type { TSceneFn } from 'nacatamalon';
import * as Settings from '../settings';

import { Dancer } from '../components/dancer';

export const Dance: TSceneFn<{ chosen?: number; }> = ({ chosen = 0 }) => {
    const keys = useKeyboard();
    const scene = useScene();

    const song = useSong(useLoadSong({
        src: 'nacatamalon:' + Settings.SONGS[chosen]
    }), {
        autoplay: true
    });

    useSpawn(Dancer)(song);
    createText({
        text: '< ' + Settings.SONGS[chosen].toUpperCase().replace(/-/g, ' ') + ' >',
        tint: Settings.ON,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 256
        }
    });

    const status = createText({
        text: '',
        tint: Settings.DIM,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 274
        }
    });

    createText({
        text: 'LEFT/RIGHT SONG   UP/DOWN TEMPO   SPACE PLAY',
        tint: Settings.DIM,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 296
        }
    });

    useUpdate(() => {
        const move = (keys.justPressed('ArrowRight') ? 1 : 0) - (keys.justPressed('ArrowLeft') ? 1 : 0);

        if (move !== 0) {
            scene.change('Dance', {
                props: {
                    chosen: (chosen + move + Settings.SONGS.length) % Settings.SONGS.length
                }
            });

            return;
        }

        if (keys.justPressed(' ')) {
            if (song.playing) {
                song.stop({
                    fade: 0.3
                });
            } else {
                song.play();
            }
        }

        if (keys.justPressed('ArrowUp')) {
            song.setTempo(Math.round(song.tempo * 1.1));
        }

        if (keys.justPressed('ArrowDown')) {
            song.setTempo(Math.round(song.tempo / 1.1));
        }

        const beat = song.beat;

        status.text = song.playing ? song.tempo + ' BPM   BEAT ' + Math.floor((beat ?? 0) + 1) : 'PRESS SPACE TO PLAY';
    });

    return createScene();
};
```

`settings.ts`:

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

export const WIDTH = 480;
export const HEIGHT = 320;
export const SONGS = ['can-can', 'korobeiniki', 'turkish-march', 'mountain-king', 'entertainer', 'stars-and-stripes'] as const;

export const ON = getColor('#f5c518');
export const DIM = getColor('#8a86a0');
```
