# Piano geometry

The closures in a grand: an action that lets its hammer go before the blow, a back check, a late damper, a scale that cannot be ideal, the bent side that is the envelope of it, and a lid solved from its prop.

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

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

## Notes

- Pure functions over plain objects. No React, no dependencies, and no acoustics: nothing computes a frequency, an inharmonicity, a tension, a soundboard impedance or a decay, and there is no hammer mass and no velocity.
- The escapement is the piece that earns the file. Driving the hammer from the key all the way to the string is the mistake every drawing of a piano makes, and it is the one thing the mechanism exists to prevent.
- An ideal scale halves the speaking length every octave; run that down 88 notes and the bottom string wants six metres. The exponent is compressed toward the bass instead, and `foreshortening` reports how much — which is what the wound strings are for.
- The bridge lands one speaking length behind an agraffe one strike point in front of the hammers, so the bent side of a grand is a consequence of the scale. An overstrung bass string reaches less far down the case than its own length, which is the entire point of crossing it.

## Usage

```tsx
import { actionPose, hammerPose, pianoLayout, lidPose } from "@/lib/robocn/piano"

const key = actionPose(1, { dip: 1, balance: 0.55, wippen: 1.3, lever: 7, blow: 4.6 })
key.ratio        // 5.005 — the product of the three levers
key.escaped      // true: the jack tripped before the key bottomed
key.gap          // what the hammer still has to cover on its own

const plan = pianoLayout({ notes: 88, strike: 20, halfWidth: 21 })
plan.rim         // the case, drawn around the scale
lidPose("full", { width: 52 }).angle    // 47°, solved from three sides
```

## API

| name | type | default | description |
| --- | --- | --- | --- |
| `actionPose` | `(dip, options?) => ActionPose` | — | One action solved from the key dip. The ratio is key × wippen × hammer lever, the jack trips at (blow − letOff) / ratio, and past that the hammer holds where it was left — with the after-touch, the gap and `regulated` reported. |
| `hammerPose` | `(lift, ring, options?) => HammerPose` | — | The whole stroke from the two numbers a pattern gives you — `combLift` for the approach and `combRelease` for the ring. Off the string at once, caught by the back check, and home only when the key lets it go. |
| `damperLift` | `(press, options?) => number` | — | How far the damper has left the string. Nothing happens for the first half of the dip; the sustain pedal lifts every damper on its own. |
| `pianoScale` | `(options?) => PianoString[]` | — | The speaking length of every note, what an ideal halving scale would have asked for, and how far short of it this one falls — plus the strike point, the choir and whether the string is wound. |
| `pianoLayout` | `(options?) => PianoLayout` | — | The plan: every string from its tuning pin through its agraffe and bridge pin to its hitch, the long and bass bridges, the capo line, and the rim drawn around all of it. |
| `lidPose` | `(stage, options?) => LidPose` | — | The lid on its prop: hinge to notch, hinge to the stick's foot, and the stick. A stick that cannot close the triangle will not stand, and says so. |
| `pianoKeys` | `(notes, options?) => PianoKey[]` | — | The compass laid across the keyboard: 52 naturals and 36 sharps for an 88, each sharp leaning outward from the middle of its own group rather than sitting on a boundary. |

## Source

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