# Pivot in 3D

> transform.pivot: the point a model turns and grows about, a runner that stumbles over its feet.

Example `pivot-3d`, running at https://nacatamalon.com/en/examples/pivot-3d. The whole catalogue: https://nacatamalon.com/llms/examples.md.

`transform.pivot` is the point a placement turns and grows about, in its own units from its origin. Setting it moves nothing: only a turn or a growth goes about it, and everything the box holds goes with it.

The two runners are the same box: a body and a head, its origin in its middle. The orange one turns about that middle, so when it trips its feet swing up off the floor. The blue one has `pivot: { x: 0, y: -0.8, z: 0 }`, its feet, so it falls over them as a body does, with one angle, `rotationX`, and no quaternions composed by hand. A mesh or a model takes a pivot in its `transform` too.

Click the preview first so it hears the keyboard. Hold → and ← to lean them forward and back, slowly, and see what each turns about; Space trips them both.

## Code

`index.ts`:

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

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

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

`components/runner.ts`:

```ts
import { createMesh, useCubeGeometry, useSphereGeometry, useTransform } from 'nacatamalon';
import { FEET, RUNNER_HEIGHT } from '../settings';

type TRunnerProps = {
    z: number;
    tint: string;
    pivoted: boolean;
};

export const Runner = ({ z, tint, pivoted }: TRunnerProps) => {
    const place = useTransform({
        y: RUNNER_HEIGHT / 2,
        z,
        ...(pivoted ? {
            pivot: FEET
        } : {
        })
    });

    createMesh({
        geometry: useCubeGeometry(),
        tint,
        transform: {
            scaleX: 0.6,
            scaleY: RUNNER_HEIGHT,
            scaleZ: 0.4
        }
    });
    createMesh({
        geometry: useSphereGeometry({
            radius: 0.25
        }),
        tint: '#ffd9a8',
        transform: {
            y: RUNNER_HEIGHT / 2 + 0.25
        }
    });
    // This marker is attached to the same box as the body and head.
    createMesh({
        geometry: useSphereGeometry({
            radius: 0.07
        }),
        tint: '#ffffff',
        transform: pivoted ? {
            ...FEET,
            x: 0.35
        } : {
            x: 0.35
        }
    });

    return place;
};
```

`hooks/use_runner_controls.ts`:

```ts
import { useActions, useUpdate } from 'nacatamalon';
import type { TTransform3d } from 'nacatamalon';
import { clamp } from 'nacatamalon/math';
import { LEAN } from '../settings';

export const useRunnerControls = (plain: TTransform3d, pivoted: TTransform3d) => {
    const input = useActions();
    let held = 0;
    let time = -1;

    useUpdate((delta) => {
        const turn = input.value('left') - input.value('right');

        held = clamp(held + turn * delta * LEAN.speed, LEAN.min, LEAN.max);

        if (input.justPressed('jump')) {
            time = 0;
        }

        let trip = 0;

        if (time >= 0) {
            time += delta;

            // Fall for half a second, pause briefly, then rise for seven tenths.
            const fall = time < 0.5 ? time / 0.5 : time < 0.8 ? 1 : Math.max(0, 1 - (time - 0.8) / 0.7);

            trip = -1.2 * Math.sin(fall * Math.PI / 2);

            if (time > 1.5) {
                time = -1;
            }
        }

        plain.rotationX = time >= 0 ? trip : held;
        pivoted.rotationX = time >= 0 ? trip : held;
    });
};
```

`scenes/demo.ts`:

```ts
import { createMesh, createScene, useCamera3d, useCubeGeometry, useDirectionalLight, useSpawn } from 'nacatamalon';
import { Runner } from '../components/runner';
import { useRunnerControls } from '../hooks/use_runner_controls';

export const Track = () => {
    useCamera3d({
        projection: 'perspective',
        fov: 50,
        transform: {
            x: 7,
            y: 1.6,
            z: 0,
            rotationY: Math.PI / 2,
            rotationX: -0.12
        }
    });
    useDirectionalLight({
        intensity: 1,
        transform: {
            rotationX: -0.8,
            rotationY: 1.2
        }
    });
    createMesh({
        geometry: useCubeGeometry(),
        tint: '#3b3346',
        transform: {
            y: -0.05,
            scaleX: 3,
            scaleY: 0.1,
            scaleZ: 8
        }
    });

    const spawnRunner = useSpawn(Runner);

    const plain = spawnRunner({
        z: 2,
        tint: '#ff6b1a',
        pivoted: false
    });

    const pivoted = spawnRunner({
        z: -1,
        tint: '#58c4ff',
        pivoted: true
    });

    useRunnerControls(plain.api, pivoted.api);

    return createScene();
};
```

`settings.ts`:

```ts
import { INPUT } from 'nacatamalon';

export const WIDTH = 480;
export const HEIGHT = 320;
export const RUNNER_HEIGHT = 1.6;
export const ACTIONS = INPUT.ACTION_PRESETS.platformer;

export const FEET = {
    x: 0,
    y: -RUNNER_HEIGHT / 2,
    z: 0
};

export const LEAN = {
    min: -1.4,
    max: 0.8,
    speed: 1.5
};
```
