# Pictures on shapes

> createGeometry with its uvs worked out by name: a building and a rock as a box, a barrel as a cylinder, a ball as a sphere and a floor from above, laid by the shape's own units with uvScale.

Example `pictures-on-shapes`, running at https://nacatamalon.com/en/examples/pictures-on-shapes. The whole catalogue: https://nacatamalon.com/llms/examples.md.

Shapes made with `createGeometry` from corners and triangles alone, their picture places worked out by name: `'box'` for the building and the rock, `'cylinder'` for the barrel, `'sphere'` for the ball and `'planar'` for the floor. Where two faces want one corner at two places of the picture, the engine splits it.

Press space to lay the bricks stretched once over the building, or by the shape's own units with `uvScale`, where the long wall and the short one get bricks of one size. The floor is laid by its own units too, so its tiles stay put while the swell moves it.

## Code

`index.ts`:

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

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

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

`art/textures.ts`:

```ts
import { getColor } from 'nacatamalon';
import { createPixels, fillChecker, fillNoise, fillRect, mapPixels } from 'nacatamalon/pixels';
import type { TPixels } from 'nacatamalon/pixels';

// The pictures, painted here. Each is made to repeat, so laid again and again it meets itself.

const c = (hex: string) => getColor(hex);

// Bricks: rows of them, every other row shifted half a brick, with mortar between.
export const paintBricks = (): TPixels => mapPixels(
    fillNoise(createPixels(32, 32), {
        seed: 4,
        scale: 6,
        octaves: 2,
        from: c('#8a3424'),
        to: c('#bd5a3c')
    }),
    (color, x, y) => (y % 8 === 7 || (x + (Math.floor(y / 8) % 2) * 8) % 16 === 15 ? c('#cfc6b2') : color),
);

// Grey stone, speckled.
export const paintStone = (): TPixels =>
    fillNoise(createPixels(32, 32), {
        seed: 9,
        scale: 4,
        octaves: 3,
        from: c('#4d4a52'),
        to: c('#9c97a3')
    });

// Staves of wood, upright, and two iron hoops across them.
export const paintStaves = (): TPixels => {
    const wood = mapPixels(
        fillNoise(createPixels(64, 32), {
            seed: 3,
            scale: 3,
            octaves: 2,
            from: c('#7a4a22'),
            to: c('#a8703a')
        }),
        (color, x) => (x % 8 === 0 ? c('#4a2a12') : color),
    );

    fillRect(wood, 0, 5, 64, 3, c('#3c3c44'));
    fillRect(wood, 0, 24, 64, 3, c('#3c3c44'));

    return wood;
};

// A beach ball: six coloured wedges round, white at the poles.
export const paintBall = (): TPixels => {
    const colors = ['#e84a4a', '#ffffff', '#3d7bff', '#ffd23f', '#ffffff', '#5fd068'].map(c);

    return mapPixels(createPixels(48, 24), (_, x, y) => (y < 2 || y > 21 ? c('#ffffff') : colors[Math.floor(x / 8)]));
};

// Floor tiles.
export const paintTiles = (): TPixels => fillChecker(createPixels(16, 16), 8, c('#3a3560'), c('#4b4578'));
```

`components/gallery.ts`:

```ts
import { createGeometry, createMesh, createText, updateGeometry, useKeyboard, useUpdate } from 'nacatamalon';
import { createPixelTexture } from 'nacatamalon/pixels';
import { ball, barrel, building, field, rock } from '../geometry/shapes';
import { paintBall, paintBricks, paintStaves, paintStone, paintTiles } from '../art/textures';

export const Gallery = () => {
    const bricks = createPixelTexture(paintBricks());

    const fitted = createGeometry({
        ...building(),
        uvs: 'box'
    });

    const laid = createGeometry({
        ...building(),
        uvs: 'box',
        uvScale: 0.5
    });

    const house = createMesh({
        geometry: laid,
        texture: bricks,
        transform: {
            x: -3.2,
            z: -1
        }
    });

    const spinning = [
        createMesh({
            geometry: createGeometry({
                ...rock(),
                uvs: 'box',
                flat: true
            }),
            texture: createPixelTexture(paintStone()),
            transform: {
                x: -0.4,
                y: 0.6,
                z: 1.4
            }
        }),
        createMesh({
            geometry: createGeometry({
                ...barrel(),
                uvs: 'cylinder'
            }),
            texture: createPixelTexture(paintStaves()),
            cull: 'none',
            transform: {
                x: 1.8,
                z: 0.6
            }
        }),
        createMesh({
            geometry: createGeometry({
                ...ball(),
                uvs: 'sphere'
            }),
            texture: createPixelTexture(paintBall()),
            transform: {
                x: 4,
                y: 0.8,
                z: -0.4
            }
        }),
    ];

    let time = 0;

    const floor = createGeometry({
        ...field(time),
        uvs: 'planar',
        uvScale: 0.5
    });

    createMesh({
        geometry: floor,
        texture: createPixelTexture(paintTiles()),
        transform: {
            y: -0.05
        }
    });

    const label = createText({
        text: '',
        transform: {
            x: 8,
            y: 8
        }
    });

    createText({
        text: 'SPACE: BRICKS STRETCHED OR LAID',
        tint: '#8c82a6',
        transform: {
            x: 8,
            y: 340
        }
    });

    const keys = useKeyboard();
    let even = true;

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

        if (keys.justPressed(' ')) {
            even = !even;
            house.geometry = even ? laid : fitted;
        }

        for (const mesh of spinning) {
            mesh.transform.rotationY += delta * 0.5;
        }

        updateGeometry(floor, {
            ...field(time),
            uvs: 'planar',
            uvScale: 0.5
        });
        label.text = even ? 'BRICKS: ONE EVERY 2 UNITS (uvScale)' : 'BRICKS: STRETCHED ONCE OVER IT';
    });
};
```

`geometry/shapes.ts`:

```ts
// Every shape here is corners and triangles, nothing more: no picture places.
// Triangles go counter-clockwise as seen from outside, the side that is drawn.

type TShape = { positions: number[]; indices: number[]; };

// Turns any triangle facing in towards the middle round, for shapes that close round their middle.
const outwards = (shape: TShape, middle: [number, number, number]): TShape => {
    const { positions: p, indices } = shape;
    const fixed: number[] = [];

    for (let t = 0; t < indices.length; t += 3) {
        const [a, b, c] = [indices[t], indices[t + 1], indices[t + 2]];
        const e1 = [p[b * 3] - p[a * 3], p[b * 3 + 1] - p[a * 3 + 1], p[b * 3 + 2] - p[a * 3 + 2]];
        const e2 = [p[c * 3] - p[a * 3], p[c * 3 + 1] - p[a * 3 + 1], p[c * 3 + 2] - p[a * 3 + 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 out = [p[a * 3] - middle[0], p[a * 3 + 1] - middle[1], p[a * 3 + 2] - middle[2]];

        fixed.push(...(n[0] * out[0] + n[1] * out[1] + n[2] * out[2] >= 0 ? [a, b, c] : [a, c, b]));
    }

    return {
        positions: p,
        indices: fixed
    };
};

// A block: eight corners, shared by the three faces meeting at each.
const block = (x: number, z: number, w: number, h: number, d: number, into: TShape): void => {
    const first = into.positions.length / 3;

    for (let i = 0; i < 8; i++) {
        into.positions.push(x + (i & 1 ? w : 0), i & 2 ? h : 0, z + (i & 4 ? d : 0));
    }

    const faces = [[0, 2, 6, 4], [1, 5, 7, 3], [0, 4, 5, 1], [2, 3, 7, 6], [0, 1, 3, 2], [4, 6, 7, 5]];

    const own = {
        positions: into.positions,
        indices: faces.flatMap(([a, b, c, e]) => [a, b, c, a, c, e].map((k) => first + k))
    };

    into.indices.push(...outwards(own, [x + w / 2, h / 2, z + d / 2]).indices);
};

// A long hall and a tall tower: two blocks, one shape.
export const building = (): TShape => {
    const shape: TShape = {
        positions: [],
        indices: []
    };

    block(0, 0, 3, 1.5, 1, shape);
    block(2.2, -1, 1, 2.6, 1, shape);

    return shape;
};

// Rings of corners round the upright axis, from the bottom up, joined into bands, each ring's last
// corner joined to its first: the seam is given once, and the engine splits it for the picture.
const turned = (rings: Array<{ radius: number; y: number; }>, sides: number): TShape => {
    const positions: number[] = [];
    const indices: number[] = [];

    for (const { radius, y } of rings) {
        for (let i = 0; i < sides; i++) {
            const phi = (i / sides) * Math.PI * 2;

            positions.push(Math.cos(phi) * radius, y, -Math.sin(phi) * radius);
        }
    }

    for (let j = 0; j < rings.length - 1; j++) {
        for (let i = 0; i < sides; i++) {
            const a = j * sides + i;
            const b = j * sides + ((i + 1) % sides);

            indices.push(a, b, b + sides, a, b + sides, a + sides);
        }
    }

    return {
        positions,
        indices
    };
};

// A barrel: rings bulging out at the middle, open at both ends.
export const barrel = (): TShape => turned(
    Array.from({
        length: 9
    }, (_, j) => ({
        radius: 0.5 + Math.sin((j / 8) * Math.PI) * 0.12,
        y: j * 0.2
    })),
    16,
);

// A ball: rings by latitude, and one corner at each pole.
export const ball = (): TShape => {
    const rings = 10;
    const sides = 16;

    const shape = turned(
        Array.from({
            length: rings - 1
        }, (_, j) => {
            const theta = ((j + 1) / rings) * Math.PI;

            return {
                radius: Math.sin(theta) * 0.8,
                y: -Math.cos(theta) * 0.8
            };
        }),
        sides,
    );

    const bottom = shape.positions.length / 3;

    shape.positions.push(0, -0.8, 0, 0, 0.8, 0);

    const top = (rings - 2) * sides;

    for (let i = 0; i < sides; i++) {
        shape.indices.push(bottom, (i + 1) % sides, i);
        shape.indices.push(bottom + 1, top + i, top + ((i + 1) % sides));
    }

    return outwards(shape, [0, 0, 0]);
};

// A rock: a ball pushed in and out at random, cut into facets by flat.
export const rock = (): TShape => {
    const shape = ball();
    let seed = 7;
    const random = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
    const moved: number[] = [];

    for (let i = 0; i < shape.positions.length; i += 3) {
        const push = 0.65 + random() * 0.35;

        moved.push(shape.positions[i] * push, shape.positions[i + 1] * push * 0.8, shape.positions[i + 2] * push);
    }

    return outwards({
        positions: moved,
        indices: shape.indices
    }, [0, 0, 0]);
};

// The floor: a grid of corners with a swell running through it.
export const field = (time: number): TShape => {
    const size = 24;
    const wide = 14;
    const positions: number[] = [];
    const indices: number[] = [];

    for (let z = 0; z < size; z++) {
        for (let x = 0; x < size; x++) {
            const px = (x / (size - 1) - 0.5) * wide;
            const pz = (z / (size - 1) - 0.5) * 8 - 1;
            const swell = pz > 2 ? Math.sin(px * 0.8 + time * 1.5) * 0.08 * (pz - 2) : 0;

            positions.push(px, swell, pz);
        }
    }

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

            // Counter-clockwise seen from above.
            indices.push(a, a + size, a + 1, a + 1, a + size, a + size + 1);
        }
    }

    return {
        positions,
        indices
    };
};
```

`scenes/demo.ts`:

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

import { Gallery } from '../components/gallery';

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

    cameraLookAt(camera, {
        x: 0,
        y: 0.8,
        z: 0
    });
    useDirectionalLight({
        color: '#fff4dc',
        intensity: 1,
        ambient: 0.35,
        transform: {
            rotationX: -0.8,
            rotationY: 0.5
        }
    });

    useSpawn(Gallery)();

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 640;
export const HEIGHT = 360;
```
