# Sprite maps

> Extra pictures on a sprite's material, read at localUv(): a coin that crumbles in its own shape while it spins, a text letter by letter, water through noise.

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

A sprite's material carries extra pictures by name, as a model's does, and its shader reads them with `sampleMap_name(uv)`. `localUv()` is where the pixel is across the sprite, so a grain mask stays put while the coin spins (the left coin reads it at the sheet's `uv` and it jumps with every frame). A text crumbles letter by letter, water drifts through noise.

On a map, `layerUv()` runs across the whole layer and `localUv()` across each tile.

Click the preview first so it hears the keyboard. L switches the shine between the two.

## Code

`index.ts`:

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

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

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

`components/gallery.ts`:

```ts
import { createSprite, createText, createTilemap, useKeyboard, useLoadAtlas, useSpriteAnimation, useUpdate } from 'nacatamalon';
import { createPixels, createPixelTexture, fillNoise, mapPixels } from 'nacatamalon/pixels';
import { WIDTH, HEIGHT } from '../settings';
import { createMapMaterials } from '../materials/maps';

export const Gallery = () => {
    const keys = useKeyboard();

    const coin = useLoadAtlas({
        src: '/example-assets/assets/atlases/coin.atlas'
    });

    // Pictures painted in code belong to the scene that paints them, so they are made here.
    // Grain: what crumbles first is darkest. Repeated, for the water to drift through.
    const grain = createPixelTexture(fillNoise(createPixels(32, 32), {
        seed: 7,
        scale: 4,
        octaves: 2
    }));

    const ripple = createPixelTexture(fillNoise(createPixels(64, 64), {
        seed: 2,
        scale: 6,
        octaves: 3
    }));

    // A narrow band of light, the rest dark: the shine that runs over the map.
    const band = createPixelTexture(mapPixels(createPixels(64, 1), (_, x) => {
        const g = Math.max(0, 1 - Math.abs(x - 32) / 6);

        return {
            r: g,
            g,
            b: g,
            a: 1
        };
    }));

    const { crumble, water, shine } = createMapMaterials(grain, ripple, band);

    createSprite({
        anchor: {
            x: 0,
            y: 0
        },
        width: WIDTH,
        height: HEIGHT,
        tint: '#141a33'
    });

    const label = (text: string, x: number, y: number) =>
        createText({
            text,
            style: {
                fontSize: 8
            },
            tint: '#f2f6fa',
            transform: {
                x,
                y
            }
        });

    // Two coins spinning, crumbling with the same grain: read at the sheet's uv, and across the coin.
    const atSheet = crumble('uv * 4.0', 'uv * 4.0');
    const across = crumble('localUv()', 'localUv()');

    [atSheet, across].forEach((material, i) => {
        const sprite = createSprite({
            atlas: coin,
            width: 48,
            height: 48,
            material,
            transform: {
                x: 50 + i * 110,
                y: 70
            }
        });

        useSpriteAnimation(sprite, {
            clips: coin.sequences,
            play: 'spin'
        });
    });
    label('AT THE SHEET', 18, 104);
    label('AT LOCALUV', 140, 104);

    // A text with the same crumble: each letter is a sprite of its own.
    const letters = crumble('localUv()', 'localUv()');

    createText({
        text: 'CRUMBLE',
        style: {
            fontSize: 24
        },
        tint: '#ffd34d',
        material: letters,
        transform: {
            x: 18,
            y: 130
        }
    });

    // Water, a plain blue rectangle drawn through its ripples.
    createSprite({
        anchor: {
            x: 0,
            y: 0
        },
        width: 200,
        height: 60,
        tint: '#2d6cb5',
        material: water,
        transform: {
            x: 14,
            y: 180
        }
    });
    label('WATER', 18, 244);

    // The jungle map, half size, its ground shining.
    const map = createTilemap({
        src: '/example-assets/assets/maps/jungle.tilemap',
        transform: {
            x: 236,
            y: 30,
            scaleX: 0.5,
            scaleY: 0.5
        }
    });

    label('THE SHINE CROSSES THE LAYER', 236, 194);

    const help = label('', 16, 256);
    let dressed = false;

    useUpdate((_, time) => {
        if (!dressed && map.layers.length > 0) {
            map.layers.forEach((layer) => {
                layer.material = shine;
            });
            dressed = true;
        }

        // In and out again, slowly, with a moment whole and a moment gone.
        const gone = Math.min(1, Math.max(0, 0.5 - 0.6 * Math.cos(time * 0.9)));

        for (const material of [atSheet, across, letters]) {
            material.uniforms.gone = gone;
        }

        if (keys.justPressed('l')) {
            shine.uniforms.perTile = shine.uniforms.perTile === 1 ? 0 : 1;
        }

        help.text = `L SHINE: ${shine.uniforms.perTile === 1 ? 'EACH TILE (LOCALUV)' : 'THE LAYER (LAYERUV)'}`;
    });
};
```

`materials/maps.ts`:

```ts
import { createMaterial, type TTexture } from 'nacatamalon';

export const createMapMaterials = (grain: TTexture, ripple: TTexture, band: TTexture) => {
    /**
     * Crumbles what wears it as `gone` goes from 0 to 1, with a glowing edge, reading the grain at `at`.
     */
    const crumble = (at: string, atGlsl: string) => createMaterial({
        maps: {
            grain
        },
        uniforms: {
            gone: 0
        },
        fragment: `fn effect(color: vec4f, uv: vec2f) -> vec4f {
        let g = sampleMap_grain(${at}).r;
        if (g < mu.gone) { return vec4f(0.0); }
        let edge = 1.0 - smoothstep(mu.gone, mu.gone + 0.08, g);
        return vec4f(mix(color.rgb, vec3f(1.0, 0.55, 0.15), edge * step(0.01, mu.gone)), color.a);
    }`,
        fragmentGlsl: `vec4 effect(vec4 color, vec2 uv) {
        float g = sampleMap_grain(${atGlsl}).r;
        if (g < mu.gone) { return vec4(0.0); }
        float edge = 1.0 - smoothstep(mu.gone, mu.gone + 0.08, g);
        return vec4(mix(color.rgb, vec3(1.0, 0.55, 0.15), edge * step(0.01, mu.gone)), color.a);
    }`,
    });

    // Water: two drifts of the same noise over the blue, the light where both are high.
    const water = createMaterial({
        maps: {
            ripple
        },
        fragment: `fn effect(color: vec4f, uv: vec2f) -> vec4f {
        let a = sampleMap_ripple(localUv() * vec2f(3.0, 1.0) + vec2f(mu.time * 0.05, 0.0)).r;
        let b = sampleMap_ripple(localUv() * vec2f(2.0, 1.5) - vec2f(mu.time * 0.03, mu.time * 0.02)).r;
        let light = smoothstep(0.55, 0.72, (a + b) * 0.5);
        return vec4f(color.rgb + light * 0.4, color.a);
    }`,
        fragmentGlsl: `vec4 effect(vec4 color, vec2 uv) {
        float a = sampleMap_ripple(localUv() * vec2(3.0, 1.0) + vec2(mu.time * 0.05, 0.0)).r;
        float b = sampleMap_ripple(localUv() * vec2(2.0, 1.5) - vec2(mu.time * 0.03, mu.time * 0.02)).r;
        float light = smoothstep(0.55, 0.72, (a + b) * 0.5);
        return vec4(color.rgb + light * 0.4, color.a);
    }`,
    });

    // The shine: the band, slid along the diagonal, across the layer or across each tile.
    const shine = createMaterial({
        maps: {
            band
        },
        uniforms: {
            perTile: 0
        },
        fragment: `fn effect(color: vec4f, uv: vec2f) -> vec4f {
        let p = mix(layerUv(), localUv(), mu.perTile);
        let light = sampleMap_band(vec2f(p.x + p.y * 0.5 - mu.time * 0.4, 0.5)).r;
        return vec4f(color.rgb + light * 0.6 * color.a, color.a);
    }`,
        fragmentGlsl: `vec4 effect(vec4 color, vec2 uv) {
        vec2 p = mix(layerUv(), localUv(), mu.perTile);
        float light = sampleMap_band(vec2(p.x + p.y * 0.5 - mu.time * 0.4, 0.5)).r;
        return vec4(color.rgb + light * 0.6 * color.a, color.a);
    }`,
    });

    return {
        crumble,
        water,
        shine
    };
};
```

`scenes/demo.ts`:

```ts
import { createScene, useSpawn } from 'nacatamalon';
import { Gallery } from '../components/gallery';

export const Room = () => {
    useSpawn(Gallery)();

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 270;
```
