# ASCII

> ascii() draws the finished frame in letters, as dense as the picture is bright there.

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

The nightcrawler, the fox, the duck, Suzanne, two fires and a ring of turning shapes are drawn as always, and `ascii()` turns the finished frame into letters: it cuts the frame into cells, reads the colour in the middle of each, and draws a letter as dense as that colour is bright, a space in the dark and `@` in the light. Lights, gradients and reflections come through on their own, because the letters are chosen after they are drawn.

`ascii()` carries its letters inside the shader, so nothing has to be loaded. `size` is the side of a cell in game pixels, `ownColor` goes from every letter in `ink` (a terminal) to the picture's own colour, and `glyphs` picks the letters, darkest first. `bloom()` after it makes them glow.

Click the preview first so it hears the keyboard. The arrows change the size of a cell, C the picture's colour or a green terminal, B the glow, G another set of letters, F a shower of sparks from the fires and space switches it off.

## 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,
    background: '#0b0710'
});

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

`components/effect_controls.ts`:

```ts
import { emitParticles, useKeyboard, useUpdate } from 'nacatamalon';
import { clamp } from 'nacatamalon/math';
import { ascii, bloom, usePostProcess } from 'nacatamalon/effects';

import type { TParticles3d } from 'nacatamalon';
import { GLYPH_SETS, LETTER_SETTINGS, GLOW_SETTINGS } from '../settings';

// The game is drawn as always, lights and gradients included, and ascii() turns the finished frame
// into letters. It cuts the frame into cells, reads the colour in the middle of each, and draws a
// letter as dense as that colour is bright: a space in the dark, '@' in the light.
//
//   size        the side of one cell, in game pixels, rounded to a multiple of 6: 6 is the
//               smallest, 12 letters twice as big
//   ownColor    0 is every letter in ink, a terminal; 1 is the picture's own colour
//   ink         the terminal's colour
//   background  how much of the picture shows behind the letters
//   brightness  how bright the letters are against the colour they were read from
//   glyphs      which letters, darkest first. Written into the shader when the effect is made, so
//               each set here is an effect of its own, and only one of them is on at a time

export const EffectControls = (fires: TParticles3d[]) => {
    const all = GLYPH_SETS.map((glyphs, i) => usePostProcess({
        ...ascii({
            glyphs,
            ...LETTER_SETTINGS,
        }),
        enabled: i === 0,
    }));

    let set = 0;

    let letters = all[0];

    // After the letters, so it is the letters that glow.
    const glow = usePostProcess(bloom(GLOW_SETTINGS));

    const keys = useKeyboard();

    useUpdate(() => {
        const size = keys.justPressed('ArrowUp') ? 6 : keys.justPressed('ArrowDown') ? -6 : 0;

        if (size !== 0) {
            letters.uniforms.size = clamp(letters.uniforms.size + size, 6, 24);
        }

        if (keys.justPressed('c')) {
            letters.uniforms.ownColor = letters.uniforms.ownColor > 0.5 ? 0 : 1;
        }

        if (keys.justPressed('b')) {
            glow.enabled = !glow.enabled;
        }

        // A shower of sparks from both fires, to see how a burst reads in letters.
        if (keys.justPressed('f')) {
            fires.forEach((sparks) => emitParticles(sparks, 140));
        }

        // The next set takes over the knobs of the one before, so only the letters change.
        if (keys.justPressed('g') && letters.enabled) {
            const next = all[(set + 1) % all.length];

            Object.assign(next.uniforms, letters.uniforms);
            letters.enabled = false;
            next.enabled = true;
            set = (set + 1) % all.length;
            letters = next;
        }

        if (keys.justPressed(' ')) {
            letters.enabled = !letters.enabled;
        }
    });
};
```

`components/fire.ts`:

```ts
import { createMesh, createParticles3d, useCylinderGeometry, useLoadParticles, usePointLight, useUpdate, type TParticles3d } from 'nacatamalon';

export const Fire = (x: number, z: number): TParticles3d => {
    // The fire's own light, kept so the flicker below can move it.
    const light: ReturnType<typeof usePointLight> | null = usePointLight({
        color: '#ff9a3c',
        intensity: 1.6,
        range: 6,
        transform: {
            x,
            y: 0.7,
            z
        }
    });

    const log = useCylinderGeometry({
        radius: 0.07,
        height: 1.1,
        segments: 8
    });

    for (let i = 0; i < 5; i++) {
        const turn = (i / 5) * Math.PI * 2;

        createMesh({
            geometry: log,
            tint: '#4a2d18',
            emissive: '#5a1c04',
            transform: {
                x: x + Math.cos(turn) * 0.22,
                y: 0.36,
                z: z + Math.sin(turn) * 0.22,
                rotationY: -turn,
                rotation: 0.55
            },
        });
    }

    createParticles3d({
        effect: useLoadParticles({
            src: '/example-assets/effects/campfire3d.particles'
        }),
        transform: {
            x,
            y: 0.15,
            z
        }
    });

    const sparks = createParticles3d({
        effect: useLoadParticles({
            src: '/example-assets/effects/embers3d.particles'
        }),
        transform: {
            x,
            y: 0.4,
            z
        }
    });

    createParticles3d({
        effect: useLoadParticles({
            src: '/example-assets/effects/smoke3d.particles'
        }),
        transform: {
            x,
            y: 1.3,
            z
        }
    });

    // A few waves out of step add up to a flicker that never quite repeats. Each fire starts at its
    // own place in it, so the two do not flicker together.
    let time = x * 3.7;

    useUpdate((delta) => {
        time += delta;

        const flicker = 0.75 + 0.12 * Math.sin(time * 11) + 0.08 * Math.sin(time * 23 + 1.3) + 0.05 * Math.sin(time * 37 + 2.1);

        if (light !== null) {
            light.intensity = 1.6 * flicker;
            light.range = 5 + flicker * 2;
        }
    });

    return sparks;
};
```

`components/stage.ts`:

```ts
import { createModel, createMesh, getColor, useCamera3d, useConeGeometry, useCubeGeometry, useCylinderGeometry, useIcoSphereGeometry, useDirectionalLight, useLoadModel, usePlaneGeometry, useSkeletalAnimation, useTorusGeometry, useUpdate, useSpawn } from 'nacatamalon';
import { Fire } from './fire';

export const Stage = () => {
    // Up and looking down at about twenty-six degrees: from above, like a top-down game, but tilted
    // enough that the models keep their sides.
    useCamera3d({
        projection: 'perspective',
        fov: 45,
        transform: {
            y: 4.6,
            z: 9,
            rotationX: -0.45
        },
    });

    const light = useDirectionalLight({
        color: '#fff2e0',
        intensity: 1.25,
        ambient: 0.22,
        transform: {
            rotationX: -0.75
        },
    });

    createMesh({
        geometry: usePlaneGeometry({
            width: 40,
            depth: 40
        }),
        tint: '#2f6b3a',
    });

    const crawler = useLoadModel({
        src: '/example-assets/assets/3d/gltf/nightcrawler/nightcrawler.glb'
    });

    const nightcrawler = createModel({
        model: crawler,
        tint: '#aecde2',
        specular: '#5a6e87',
        shininess: 28,
    });

    useSkeletalAnimation(nightcrawler, {
        play: 'Walk'
    });

    // The fox is measured in centimetres, so a fiftieth of it stands beside the others.
    const vixen = useLoadModel({
        src: '/example-assets/assets/3d/gltf/Fox/Fox.gltf'
    });

    const fox = createModel({
        model: vixen,
        transform: {
            x: -2.2,
            z: 0.6,
            scaleX: 0.02,
            scaleY: 0.02,
            scaleZ: 0.02
        },
    });

    useSkeletalAnimation(fox, {
        play: 'Run'
    });

    const duck = createModel({
        model: useLoadModel({
            src: '/example-assets/assets/3d/gltf/Duck/Duck.gltf'
        }),
        transform: {
            x: 2.2,
            z: 0.6
        },
    });

    const suzanne = createModel({
        model: useLoadModel({
            src: '/example-assets/assets/3d/gltf/Suzanne/Suzanne.gltf'
        }),
        tint: '#ff7a3d',
        transform: {
            y: 1.2,
            z: -2.4,
            scaleX: 0.7,
            scaleY: 0.7,
            scaleZ: 0.7
        },
    });

    // A ring of shapes round the models, each turning on its own. Every one is lifted by half its
    // height so it stands on the floor rather than sinking into it.
    const cube = useCubeGeometry({
        width: 0.9,
        height: 0.9,
        depth: 0.9
    });

    const shapes = [
        {
            geometry: cube,
            colour: '#ffd166',
            y: 0.8
        },
        {
            geometry: useTorusGeometry({
                radius: 0.5,
                tube: 0.18
            }),
            colour: '#ef476f',
            y: 0.9
        },
        {
            geometry: cube,
            colour: '#3dd6c6',
            y: 0.8
        },
        {
            geometry: useConeGeometry({
                radius: 0.5,
                height: 1.1
            }),
            colour: '#f78c6b',
            y: 0.55
        },
        {
            geometry: cube,
            colour: '#9b5cff',
            y: 0.8
        },
        {
            geometry: useIcoSphereGeometry({
                radius: 0.5,
                subdivisions: 1
            }),
            colour: '#06d6a0',
            y: 0.6
        },
        {
            geometry: cube,
            colour: '#ff7a3d',
            y: 0.8
        },
        {
            geometry: useCylinderGeometry({
                radius: 0.4,
                height: 1
            }),
            colour: '#4cc9f0',
            y: 0.5
        },
    ].map((shape, i, all) => {
        const turn = (i / all.length) * Math.PI * 2;

        return createMesh({
            geometry: shape.geometry,
            tint: getColor(shape.colour),
            transform: {
                x: Math.cos(turn) * 4.6,
                y: shape.y,
                z: Math.sin(turn) * 3.6
            },
        });
    });

    // Two fires in front, between the models and the ring.
    const spawnFire = useSpawn(Fire);
    const fires = [spawnFire(-1.4, 2.4).api, spawnFire(1.4, 2.4).api];

    useUpdate((delta, time) => {
        light.transform.rotationY = Math.sin(time * 0.7) * 1.1;
        nightcrawler.transform.rotationY += delta * 0.5;
        fox.transform.rotationY -= delta * 0.6;
        duck.transform.rotationY += delta * 0.8;
        suzanne.transform.rotationY = Math.sin(time * 0.9) * 0.8;
        suzanne.transform.y = 1.2 + Math.sin(time * 1.6) * 0.15;
        // Each on two axes, at its own speed, so no two show the same face at the same time.
        shapes.forEach((shape, i) => {
            shape.transform.rotationY += delta * (0.6 + i * 0.12);
            shape.transform.rotationX += delta * (0.4 + (i % 3) * 0.2);
        });
    });

    return fires;
};
```

`scenes/demo.ts`:

```ts
import { createScene, useSpawn } from 'nacatamalon';
import { Stage } from '../components/stage';
import { EffectControls } from '../components/effect_controls';

export const Game = () => {
    const stage = useSpawn(Stage)();

    useSpawn(EffectControls)(stage.api);

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 960;
export const HEIGHT = 600;
export const GLYPH_SETS = [' .:-=+*#W@', ' .oO0@', ' ░▒▓█', ' .,:;irsXA253hMHGS#9B&@'];
export const LETTER_SETTINGS = {
    size: 6,
    ink: '#4dff73'
};
export const GLOW_SETTINGS = {
    threshold: 0.45,
    intensity: 0.8
};
```
