# Sport geometry

The kit a game is played with: the sphere and its seams, the puck, and the four dynamics that make five objects behave unlike one another — restitution bounce, Magnus curve, Coulomb slide, and the bat-ball collision.

> 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/sport-geometry.json
```

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

## Notes

- Pure functions over plain objects: no React, no three.js, no dependencies.
- There is no air in here. Nothing has drag, no spin decays, and the Magnus term is held at its release value — a curve is a constant-acceleration approximation of a flight that really is not one.
- Contact is a coefficient, not a deformation: `contactSquash` is illustration, and the boards are a specular reflection, so a puck never leaves one at an angle it did not arrive at.
- Lengths are in whatever units the caller draws in; `gravity` is in those units per second squared. Angles are degrees on the surface and radians inside.

## API

| name | type | default | description |
| --- | --- | --- | --- |
| `spinFrame(axis, turns, base?)` | `(axis: Partial<Vec3>, turns: number, base?: SurfaceFrame) => SurfaceFrame` | — | The frame a body reaches after `turns` revolutions about `axis`. Revolutions, because that is what a spin rate gives once it has met a clock. |
| `sphereSilhouette(radius, viewDir, steps?)` | `(radius: number, viewDir: Vec3, steps?: number) => Vec3[]` | — | The outline, exactly: the great circle perpendicular to the view. |
| `surfaceCurve(frame, radius, dirs, viewDir)` | `(frame, radius, dirs: Vec3[], viewDir) => SurfaceMark[]` | — | A curve of unit directions carried onto the ball, each point carrying the sign of its own normal against the camera. |
| `visibleRuns(marks, closed?)` | `(marks: SurfaceMark[], closed?: boolean) => SurfaceMark[][]` | — | Splits a ring into the runs the camera can see, so the far half is never painted over the near one. |
| `clipToLimb(direction, viewDir)` | `(direction: Vec3, viewDir: Vec3) => Vec3` | — | Pulls a direction that has gone round the back onto the limb, which is how a panel straddling the horizon is clipped rather than folded. |
| `baseballSeam(shape?, steps?)` | `(shape?: number, steps?: number) => Vec3[]` | — | The figure-eight, lying exactly on the unit sphere for every t at every shape ratio — `x = a cos t + b cos 3t`, `y = a sin t − b sin 3t`, `z = 2√(ab) sin 2t`. |
| `basketballSeams(amplitude?, steps?)` | `(amplitude?: number, steps?: number) => Vec3[][]` | — | Two orthogonal great circles and one wavy closed curve: four lunes, eight panels, three curves. |
| `soccerPanels(detail?)` | `(detail?: number) => SpherePanel[]` | — | The truncated icosahedron, built rather than drawn: 12 pentagons and 20 hexagons, pushed onto the sphere and subdivided along great circles. |
| `puckRim / puckSilhouette` | `(frame, shape, …) => Vec3[] | Vec2[]` | — | A cylinder's two rims, and the convex hull of them projected — which is its exact outline from any angle. |
| `bounceAt(time, options)` | `(time: number, options?: BounceOptions) => BounceState` | — | The restitution ladder in closed form: every apex is the last one times e², so any instant can be asked for without running the ones before it. |
| `bounceDuration(options?)` | `(options?: BounceOptions) => number` | — | When it stops bouncing: `t₀(1 + e)/(1 − e)`, exactly. |
| `dribbleAt(cycle, options?)` | `(cycle: number, options?: DribbleOptions) => DribbleState` | — | The periodic bounce — the exact constant-gravity parabola, rewritten in cycles — plus where the paddle has to be to meet it. |
| `contactSquash(contact, impact, …)` | `(contact: number, impact: number, reference?, most?) => number` | — | How flat it goes at contact. Illustration: impact speed against a reference, not a deformation model. |
| `flightAt(time, options?)` | `(time: number, options?: FlightOptions) => FlightState` | — | Gravity plus a Magnus term held at its release value, so the whole flight is one quadratic. Returns the position, the break off the spinless line, and the turns racked up. |
| `pitchSpin(pitch)` | `(pitch: PitchName) => PitchSpin` | — | A rate and an axis, and nothing else: fastball, curveball, slider, sinker, knuckler. |
| `rollTurns(distance, radius)` | `(distance: number, radius: number) => number` | — | Rolling without slipping, which is the whole of `θ = s / r`. |
| `slideTrack(options?)` | `(options?: SlideOptions) => SlideTrack` | — | The whole slide solved once. Coulomb friction is a constant μg, so the distance left is `v²/2μg` and a board that takes e of the speed takes e² of it. |
| `slideAt(track, time, spin?)` | `(track: SlideTrack, time: number, spin?: number) => SlideState` | — | Where along that track the puck is, and how fast it is still going. |
| `effectiveMass(bat, contact)` | `(bat: Partial<BatGeometry>, contact: number) => number` | — | `1/M = 1/m + d²/I` — the mass the ball actually meets, which collapses away from the centre of mass. |
| `swingImpact(options?) / sweetSpot(options?)` | `(options?: ImpactOptions) => ImpactResult | { contact, exitSpeed }` | — | The collision itself, and where on the barrel it does its best work — found by sampling, not declared. |
| `swingAngle(phase, from?, to?, contactAt?)` | `(phase: number, from?, to?, contactAt?) => number` | — | The sweep: a long load, a fast pass through the zone, a follow-through that slows. The phase is clamped, not wrapped. |
| `defaultBall / defaultPuck / defaultBat / defaultRink` | `const` | — | The dimensions the family ships with. |

## Source

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