# Meshes by hand

> createGeometry corner by corner: a star smooth and flat, a ramp painted at its corners, a field to dig and a comet's growing trail, changed with updateGeometry.

Example `meshes-by-hand`, running at https://nacatamalon.com/en/examples/meshes-by-hand. The whole catalogue: https://nacatamalon.com/llms/examples.md.

Shapes made corner by corner with `createGeometry`: where each corner is, and which three make each triangle, counter-clockwise from the side that is drawn. Behind, the same star twice, its facings worked out smooth on the left and `flat: true` on the right, and a ramp with a colour painted at each corner.

Hold the button down on the field and it sinks there: `updateGeometry` writes the corners over what the card already holds, and only while they change. The comet's trail grows a step every frame until it is as long as it goes, outgrowing the room its shape was given, and is given more.

## Code

`index.ts`:

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

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

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

`components/comet.ts`:

```ts
import * as Settings from '../settings';
import { createGeometry, createMesh, updateGeometry, useUpdate, useSphereGeometry } from 'nacatamalon';

export const Comet = () => {
    const head = createMesh({
        geometry: useSphereGeometry({
            radius: 0.14
        }),
        tint: '#ffffff',
        emissive: '#ffb35c'
    });

    const steps: Array<[number, number, number]> = [];

    const ribbon = () => {
        const positions: number[] = [];
        const colors: number[] = [];
        const indices: number[] = [];

        steps.forEach(([x, y, z], i) => {
            const age = i / Math.max(steps.length - 1, 1);
            const width = 0.12 * age;

            positions.push(x, y - width, z, x, y + width, z);
            colors.push(1, 0.6 * age + 0.2, 0.2, age, 1, 0.6 * age + 0.2, 0.2, age);

            if (i > 0) {
                const a = (i - 1) * 2;

                indices.push(a, a + 2, a + 1, a + 1, a + 2, a + 3);
            }
        });

        return {
            positions,
            colors,
            indices
        };
    };

    let time = 0;

    steps.push([0, 0, 0], [0, 0, 0]);

    const trail = createGeometry(ribbon());

    createMesh({
        geometry: trail,
        tint: '#ffffff',
        emissive: '#ff7a1a',
        cull: 'none',
        transparent: true
    });

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

        // A figure of eight round the scene.
        const x = Math.sin(time * 0.9) * 4.2;
        const z = Math.sin(time * 1.8) * 1.6;
        const y = 1.2 + Math.sin(time * 2.7) * 0.35;

        head.transform.x = x;
        head.transform.y = y;
        head.transform.z = z;
        steps.push([x, y, z]);

        if (steps.length > Settings.LONGEST) {
            steps.shift();
        }

        updateGeometry(trail, ribbon());
    });
};
```

`components/ground.ts`:

```ts
import * as Settings from '../settings';
import { createGeometry, createMesh, screenToRay, updateGeometry, useGame, usePointer, useTransform, useUpdate } from 'nacatamalon';
import type { TCamera3d } from 'nacatamalon';

export const Ground = (camera: TCamera3d) => {
    const place = useTransform({
        x: -Settings.SPAN / 2,
        z: 0.2 - Settings.SPAN / 2 + 2.4
    });

    const game = useGame();
    const pointer = usePointer();

    const heights = new Float32Array(Settings.SIZE * Settings.SIZE);
    const positions = new Float32Array(Settings.SIZE * Settings.SIZE * 3);
    const colors = new Float32Array(Settings.SIZE * Settings.SIZE * 4);
    const indices: number[] = [];

    for (let z = 0; z < Settings.SIZE - 1; z++) {
        for (let x = 0; x < Settings.SIZE - 1; x++) {
            const a = z * Settings.SIZE + x;

            // Counter-clockwise seen from above, the side that is drawn.
            indices.push(a, a + Settings.SIZE, a + 1, a + 1, a + Settings.SIZE, a + Settings.SIZE + 1);
        }
    }

    const shape = () => {
        for (let i = 0; i < Settings.SIZE * Settings.SIZE; i++) {
            positions.set([(i % Settings.SIZE) * Settings.STEP, heights[i], Math.floor(i / Settings.SIZE) * Settings.STEP], i * 3);

            const shade = 1 + heights[i] * 1.4;

            colors.set([0.45 * shade, 0.75 * shade, 0.35 * shade, 1], i * 4);
        }

        return {
            positions,
            indices,
            colors
        };
    };

    const field = createGeometry(shape());

    createMesh({
        geometry: field,
        tint: '#ffffff'
    });

    useUpdate((delta) => {
        const at = pointer.position();

        if (!pointer.isDown() || at === null) {
            return;
        }

        // Where the pointer meets the ground, the plane the field lies on.
        const ray = screenToRay(camera, game.getWidth(), game.getHeight(), at.screenX, at.screenY);

        if (ray.direction.y >= 0) {
            return;
        }

        const t = -ray.origin.y / ray.direction.y;
        const x = ray.origin.x + ray.direction.x * t - place.x;
        const z = ray.origin.z + ray.direction.z * t - place.z;
        let changed = false;

        for (let i = 0; i < Settings.SIZE * Settings.SIZE; i++) {
            const distance = Math.hypot((i % Settings.SIZE) * Settings.STEP - x, Math.floor(i / Settings.SIZE) * Settings.STEP - z);

            if (distance < 0.6) {
                heights[i] = Math.max(-0.5, heights[i] - (1 - distance / 0.6) * delta * 0.8);
                changed = true;
            }
        }

        // Only while it changes: a field nobody is digging is not sent again.
        if (changed) {
            updateGeometry(field, shape());
        }
    });
};
```

`geometry/shapes.ts`:

```ts
import { createGeometry, createMesh, useTransform, useUpdate } from 'nacatamalon';

/**
 * A triangle, its corners put in the order that faces it the way it is meant to: counter-clockwise
 * from the side that is drawn. Worked out rather than written by hand, which is where a shape made
 * corner by corner usually goes wrong: a triangle given the other way round is thrown away as
 * facing away, and the shape has a hole.
 */
const facing = (positions: number[], indices: number[], a: number, b: number, c: number, out: [number, number, number]) => {
    const at = (i: number, k: number) => positions[i * 3 + k];
    const e1 = [at(b, 0) - at(a, 0), at(b, 1) - at(a, 1), at(b, 2) - at(a, 2)];
    const e2 = [at(c, 0) - at(a, 0), at(c, 1) - at(a, 1), at(c, 2) - at(a, 2)];
    const n = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]];
    const towards = n[0] * out[0] + n[1] * out[1] + n[2] * out[2];

    if (towards >= 0) {
        indices.push(a, b, c);
    } else {
        indices.push(a, c, b);
    }
};

/**
 * A five-pointed star, pushed up into a solid: a lid, a base, and a wall round its edge. Each face
 * has corners of its own, so a lid's corner and the wall's beside it can face different ways.
 */
const star = (height: number) => {
    const positions: number[] = [];
    const indices: number[] = [];

    const rim = Array.from({
        length: 10
    }, (_, i) => {
        const angle = (i / 10) * Math.PI * 2;
        const r = i % 2 === 0 ? 1 : 0.45;

        return [Math.cos(angle) * r, Math.sin(angle) * r];
    });

    const corner = (x: number, y: number, z: number) => {
        positions.push(x, y, z);

        return positions.length / 3 - 1;
    };

    for (const [y, up] of [[height, 1], [0, -1]] as const) {
        const middle = corner(0, y, 0);
        const ring = rim.map(([x, z]) => corner(x, y, z));

        for (let i = 0; i < 10; i++) {
            facing(positions, indices, middle, ring[i], ring[(i + 1) % 10], [0, up, 0]);
        }
    }

    for (let i = 0; i < 10; i++) {
        const [ax, az] = rim[i];
        const [bx, bz] = rim[(i + 1) % 10];
        const a = corner(ax, 0, az);
        const b = corner(bx, 0, bz);
        const c = corner(bx, height, bz);
        const d = corner(ax, height, az);
        const out: [number, number, number] = [(ax + bx) / 2, 0, (az + bz) / 2];

        facing(positions, indices, a, b, c, out);
        facing(positions, indices, a, c, d, out);
    }

    return {
        positions,
        indices
    };
};

/** A wedge to roll down: a slope, a back, two sides and a floor, a colour at each of its corners. */
const ramp = () => {
    const positions = [
        // The four floor corners, followed by the top two corners at the back.
        [-1, 0, 0.8], [1, 0, 0.8], [1, 0, -0.8], [-1, 0, -0.8], [-1, 1, -0.8], [1, 1, -0.8]
    ].flat();

    const indices: number[] = [];

    facing(positions, indices, 0, 1, 5, [0, 1, 1]);
    facing(positions, indices, 0, 5, 4, [0, 1, 1]);
    facing(positions, indices, 3, 2, 5, [0, 0, -1]);
    facing(positions, indices, 3, 5, 4, [0, 0, -1]);
    facing(positions, indices, 0, 4, 3, [-1, 0, 0]);
    facing(positions, indices, 1, 2, 5, [1, 0, 0]);
    facing(positions, indices, 0, 3, 2, [0, -1, 0]);
    facing(positions, indices, 0, 2, 1, [0, -1, 0]);

    const colors = [1, 0.35, 0.3, 1, 1, 0.8, 0.25, 1, 0.3, 0.8, 0.45, 1, 0.3, 0.55, 1, 1, 0.75, 0.4, 1, 1, 0.95, 0.95, 1, 1,];

    return {
        positions,
        indices,
        colors,
        flat: true
    };
};

export const Shapes = () => {
    useTransform({
        z: -2.2
    });

    const shape = star(0.4);

    const smooth = createMesh({
        geometry: createGeometry(shape),
        tint: '#ffd23f',
        transform: {
            x: -3.2,
            y: 0.3
        }
    });

    const flat = createMesh({
        geometry: createGeometry({
            ...shape,
            flat: true
        }),
        tint: '#ffd23f',
        transform: {
            x: -0.8,
            y: 0.3
        }
    });

    createMesh({
        geometry: createGeometry(ramp()),
        transform: {
            x: 2.6
        }
    });

    useUpdate((delta) => {
        smooth.transform.rotationY += delta * 0.5;
        flat.transform.rotationY += delta * 0.5;
    });
};
```

`scenes/demo.ts`:

```ts
import { cameraLookAt, createScene, createText, useCamera3d, useDirectionalLight, usePointLight, useSpawn } from 'nacatamalon';

import { Shapes } from '../geometry/shapes';
import { Ground } from '../components/ground';
import { Comet } from '../components/comet';

export const Scene = () => {
    const camera = useCamera3d({
        projection: 'perspective',
        fov: 50,
        transform: {
            x: 0,
            y: 6,
            z: 9
        }
    });

    cameraLookAt(camera, {
        x: 0,
        y: 0,
        z: 0
    });
    useDirectionalLight({
        color: '#fff1d0',
        intensity: 0.9,
        ambient: 0.18,
        transform: {
            rotationX: -0.8,
            rotationY: 0.5
        }
    });
    usePointLight({
        color: '#3d7bff',
        intensity: 1.2,
        range: 10,
        transform: {
            x: -4,
            y: 2,
            z: 3
        }
    });

    useSpawn(Shapes)();
    // The field is told the scene's camera, to find where the pointer meets it.
    useSpawn(Ground)(camera);
    useSpawn(Comet)();

    createText({
        text: 'HOLD THE BUTTON ON THE FIELD TO DIG',
        tint: '#8c82a6',
        transform: {
            x: 8,
            y: 340
        }
    });

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 640;
export const HEIGHT = 360;
// A field of corners, a grid of heights. Held down on, it sinks under the pointer, deeper in the
// middle of the spade than at its edge, and darker the deeper it goes.

export const SIZE = 40;
export const SPAN = 6;
export const STEP = SPAN / (SIZE - 1);
// A comet and its trail: a ribbon standing up behind it, one more step long every frame until it
// is as long as it goes, fading as it gets older. Both sides kept, since it is seen from both.

export const LONGEST = 160;
```
