# Shapes of your own

> useLatheGeometry turns half an outline into a round solid, and createGeometry builds a shape corner by corner, changed every frame with updateGeometry.

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

On the shelf, five pieces turned on a lathe with `useLatheGeometry`: each is half its outline, a handful of points from the bottom up, spun about the upright axis into a solid, as a turner shapes a chair leg or a potter a vase. A point on the axis closes the shape; a point given twice in a row is a hard edge. Press space for the faceted look of the time, six faces round.

In front, dunes made corner by corner with `createGeometry`, and moved by the wind every frame with `updateGeometry`: the corners are worked out again and written over what the card already holds. A shape made this way belongs to the object that made it and goes with it.

## Code

`index.ts`:

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

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

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

`components/dunes.ts`:

```ts
import { createGeometry, createMesh, updateGeometry, useTransform, useUpdate } from 'nacatamalon';
import { DUNES } from '../settings';

export const Dunes = () => {
    const { size, width, depth } = DUNES;

    useTransform({
        x: -width / 2,
        y: -0.25,
        z: 1
    });

    const positions = new Float32Array(size * size * 3);
    const uvs = new Float32Array(size * size * 2);
    const indices: number[] = [];

    for (let row = 0; row < size; row++) {
        for (let column = 0; column < size; column++) {
            const point = row * size + column;
            const u = column / (size - 1);
            const v = row / (size - 1);

            positions[point * 3] = u * width;
            positions[point * 3 + 2] = v * depth;
            uvs[point * 2] = u;
            uvs[point * 2 + 1] = v;

            if (row < size - 1 && column < size - 1) {
                indices.push(point, point + size, point + 1, point + 1, point + size, point + size + 1);
            }
        }
    }

    const data = {
        positions,
        indices,
        uvs
    };

    const blow = (time: number) => {
        for (let point = 0; point < size * size; point++) {
            const x = (point % size) / (size - 1) * width;
            const z = Math.floor(point / size) / (size - 1) * depth;

            positions[point * 3 + 1] = Math.sin(x * 0.9 + time * 0.8) * 0.22
                + Math.sin(x * 0.35 - z * 1.3 + time * 0.5) * 0.15;
        }
    };

    blow(0);

    const sand = createGeometry(data);

    createMesh({
        geometry: sand,
        tint: '#d9b779'
    });

    // The whole geometry is supplied, with the same topology and UV arrays each time.
    useUpdate((_delta, time) => {
        blow(time);
        updateGeometry(sand, data);
    });
};
```

`components/hud.ts`:

```ts
import { createText, useAction, useScreenSpace, useUpdate } from 'nacatamalon';
import type { TLathePiece } from './lathe_piece';
import { HEIGHT, SEGMENTS } from '../settings';

export const Hud = (pieces: TLathePiece[]) => {
    useScreenSpace();

    const label = createText({
        text: 'LATHE: ' + SEGMENTS.round + ' FACES ROUND',
        transform: {
            x: 8,
            y: 8
        }
    });

    const toggle = useAction('facets');
    let round = true;

    createText({
        text: 'SPACE: ROUND OR FACETED',
        tint: '#8c82a6',
        transform: {
            x: 8,
            y: HEIGHT - 20
        }
    });

    useUpdate(() => {
        if (toggle.justPressed()) {
            round = !round;

            for (const piece of pieces) {
                piece.setRound(round);
            }

            label.text = 'LATHE: ' + (round ? SEGMENTS.round : SEGMENTS.faceted) + ' FACES ROUND';
        }
    });
};
```

`components/lathe_piece.ts`:

```ts
import { createMesh, useLatheGeometry, useUpdate } from 'nacatamalon';
import type { TPiece } from '../data/profiles';
import { SEGMENTS, TURN_SPEED } from '../settings';

type TPieceProps = {
    piece: TPiece;
    index: number;
};

export const LathePiece = ({ piece, index }: TPieceProps) => {
    const round = useLatheGeometry({
        profile: piece.profile,
        segments: SEGMENTS.round
    });

    const faceted = useLatheGeometry({
        profile: piece.profile,
        segments: SEGMENTS.faceted
    });

    const mesh = createMesh({
        geometry: round,
        tint: piece.color,
        cull: piece.open ? 'none' : 'back',
        transform: {
            x: -4 + index * 2,
            y: 0.2
        }
    });

    useUpdate((delta) => {
        mesh.transform.rotationY += delta * TURN_SPEED;
    });

    return {
        setRound: (on: boolean) => {
            mesh.geometry = on ? round : faceted;
        }
    };
};

export type TLathePiece = ReturnType<typeof LathePiece>;
```

`data/profiles.ts`:

```ts
// Five profiles, each half an outline from the bottom up: x is how far from the axis, y how high.
// A point given twice in a row is a hard edge.

type TProfile = ReadonlyArray<{ x: number; y: number }>;

const bottle: TProfile = [
    {
        x: 0,
        y: 0
    },
    {
        x: 0.45,
        y: 0
    },
    {
        x: 0.45,
        y: 0
    },
    {
        x: 0.45,
        y: 1.1
    },
    {
        x: 0.2,
        y: 1.5
    },
    {
        x: 0.14,
        y: 1.9
    },
    {
        x: 0.17,
        y: 2
    },
    {
        x: 0.17,
        y: 2
    },
    {
        x: 0,
        y: 2
    },
];

const glass: TProfile = [
    {
        x: 0,
        y: 0
    },
    {
        x: 0.4,
        y: 0
    },
    {
        x: 0.4,
        y: 0
    },
    {
        x: 0.4,
        y: 0.05
    },
    {
        x: 0.06,
        y: 0.12
    },
    {
        x: 0.05,
        y: 0.8
    },
    {
        x: 0.1,
        y: 0.95
    },
    {
        x: 0.42,
        y: 1.3
    },
    {
        x: 0.5,
        y: 1.8
    },
];

const vase: TProfile = [
    {
        x: 0,
        y: 0
    },
    {
        x: 0.3,
        y: 0
    },
    {
        x: 0.3,
        y: 0
    },
    {
        x: 0.55,
        y: 0.5
    },
    {
        x: 0.6,
        y: 0.9
    },
    {
        x: 0.35,
        y: 1.4
    },
    {
        x: 0.25,
        y: 1.6
    },
    {
        x: 0.35,
        y: 1.8
    },
];

const pawn: TProfile = [
    {
        x: 0,
        y: 0
    },
    {
        x: 0.5,
        y: 0
    },
    {
        x: 0.5,
        y: 0
    },
    {
        x: 0.5,
        y: 0.15
    },
    {
        x: 0.38,
        y: 0.25
    },
    {
        x: 0.22,
        y: 0.4
    },
    {
        x: 0.16,
        y: 0.9
    },
    {
        x: 0.32,
        y: 1
    },
    {
        x: 0.12,
        y: 1.1
    },
    {
        x: 0.28,
        y: 1.3
    },
    {
        x: 0.25,
        y: 1.5
    },
    {
        x: 0.12,
        y: 1.62
    },
    {
        x: 0,
        y: 1.65
    },
];

const pin: TProfile = [
    {
        x: 0,
        y: 0
    },
    {
        x: 0.22,
        y: 0
    },
    {
        x: 0.22,
        y: 0
    },
    {
        x: 0.38,
        y: 0.6
    },
    {
        x: 0.3,
        y: 1.1
    },
    {
        x: 0.14,
        y: 1.5
    },
    {
        x: 0.2,
        y: 1.85
    },
    {
        x: 0.12,
        y: 2.05
    },
    {
        x: 0,
        y: 2.1
    },
];

export const PIECES: Array<{ name: string; profile: TProfile; color: string; open?: boolean }> = [
    {
        name: 'bottle',
        profile: bottle,
        color: '#5fd068'
    },
    {
        name: 'glass',
        profile: glass,
        color: '#d8e8ff',
        open: true
    },
    {
        name: 'vase',
        profile: vase,
        color: '#e15554',
        open: true
    },
    {
        name: 'pawn',
        profile: pawn,
        color: '#ffd23f'
    },
    {
        name: 'pin',
        profile: pin,
        color: '#f4f1e8'
    },
];

export type TPiece = typeof PIECES[number];
```

`scenes/demo.ts`:

```ts
import { cameraLookAt, createScene, useCamera3d, useDirectionalLight, usePointLight, useSpawn } from 'nacatamalon';
import { LathePiece } from '../components/lathe_piece';
import { Dunes } from '../components/dunes';
import { Hud } from '../components/hud';
import { PIECES } from '../data/profiles';

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

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

    const spawnPiece = useSpawn(LathePiece);

    const pieces = PIECES.map((piece, index) => spawnPiece({
        piece,
        index
    }).api);

    useSpawn(Dunes)();
    useSpawn(Hud)(pieces);

    return createScene();
};
```

`settings.ts`:

```ts
import type { TActionMap } from 'nacatamalon';

export const WIDTH = 640;
export const HEIGHT = 360;
export const SEGMENTS = {
    round: 32,
    faceted: 6
};
export const TURN_SPEED = 0.4;
export const DUNES = {
    size: 28,
    width: 12,
    depth: 3.5
};
export const ACTIONS: TActionMap = [
    {
        name: 'facets',
        bindings: [
            {
                type: 'key',
                key: ' '
            },
            {
                type: 'button',
                button: 'a'
            }
        ]
    }
];
```
