# Hopper dynamics

The bounce solver behind both hoppers: an exact parabola in the air, an exact spring-mass stance on the ground, and the split between them derived rather than dialled.

> For the complete index, see [llms.txt](https://robocn.dev/llms.txt). A Markdown version of any page is available by appending `.md` to its URL or by sending an `Accept: text/markdown` header.

## Install

```bash
bunx --bun shadcn@latest add https://robocn.dev/r/hopper-dynamics.json
```

Registry item: `hopper-dynamics` · [`https://robocn.dev/r/hopper-dynamics.json`](https://robocn.dev/r/hopper-dynamics.json)

## Notes

- Units are mass 1 and gravity 1, so a hop unit is whatever the drawing decides one is, and a load of 1 is the machine's own weight. Static sag is 1 / stiffness.
- Contact ends when the spring force returns to zero, which is not half a period — gravity biases the oscillation, and the closed form is (2/ω)(π − atan(vω/g)). That is the number a duty-factor knob would have got wrong.
- No damping inside the stance, no horizontal travel, no friction, no spin-up from contact, no material and no actuator. The loss is taken at take-off as a restitution coefficient, which is how a bounce is actually measured.

## Usage

```tsx
import { solveHop, hopTimings, solveDrop, springCoils, squashRadii } from "@/lib/robocn/hopper"

const timings = hopTimings({ height: 0.5, stiffness: 40 })
timings.duty  // fraction of the cycle spent touching the ground

const state = solveHop({ phase: 0.3, stiffness: 40 })
state.altitude  // negative while the compliance is loaded

solveDrop({ time: 4, height: 1, restitution: 0.6 }).resting
springCoils({ length: 40, turns: 6, radius: 7, wire: 1.4 }).bottomedOut
squashRadii(30, 0.25)  // { rx, ry }, at constant volume
```

## API

| name | type | default | description |
| --- | --- | --- | --- |
| `solveHop` | `(options?: HopOptions) => HopState` | — | The steady bounce as a function of the cycle fraction: touchdown at phase 0, stance, take-off, flight. Wraps in both directions. |
| `HopState` | `{ altitude, compression, squeeze, contact, velocity, load, bounce, resting }` | — | Altitude is height above the machine's free-standing rest and goes negative while the compliance is loaded; load is in machine weights, so 1 is standing still. |
| `hopTimings` | `(options?: HopOptions) => HopTimings` | — | Contact, flight, cycle, duty, depth, take-off speed, static sag and natural frequency — every one a consequence of the drop height and the spring rate. |
| `solveDrop` | `(options?: DropOptions) => HopState` | — | A machine released from height at time 0 and left alone: apexes decay by the square of the restitution until the rebound cannot lift it clear, after which it sits at its static sag. |
| `dropTimings` | `(options?: DropOptions) => { bounces, settleTime, apexAt }` | — | How many contacts the sequence makes, when it is over, and when each apex falls — enough to loop a settling behaviour on. |
| `springCoils` | `(options?: SpringOptions) => SpringCoils` | — | A helix seen side-on, which is a sinusoid. Coil count and radius are invariant, and the length is clamped at the solid height and reported as bottomedOut. |
| `squashRadii` | `(radius: number, squeeze: number) => { rx, ry }` | — | An oblate spheroid at constant volume: rx² ry = r³. |

## Source

- `src/lib/robocn/hopper.ts`
