# Port notes

Working notes for quack, the web port of celphase's duck-pond. The line-by-line
study of the Rust project lives in `duck-pond/exploration.md`, beside the
vendored clone (both kept out of this repo). This file is about *this* project:
what was ported, what changed, and the conventions the code relies on.

[`behavior-divergence.md`](behavior-divergence.md) is the balance sheet: what
came across from the Rust model, what has **not** been ported yet, what quack
invented, and where the behaviour differs because quack has flight and winter
and the Rust has neither.

## What came from where

| in quack | source | lives in |
| --- | --- | --- |
| Activity model, time budget, families, swimming speeds | duck-pond `scene.rs` | `src/pond.ts` |
| Calling model: intervals, sleeping suppression, hen lottery, social answers, brood recall | duck-pond `scene.rs` | `src/pond.ts`, `src/config.ts` (`CALLS`) |
| Duck rig: body, wings, bill, eyes, poses | our own bird world's `bird.js` | `src/duck.ts` |
| Flight: the glide, the climb-off beat and the landing flare, and the flap phase under them | our own bird world's `bird.js` air states | `src/duck.ts` |
| Season-keyframe idea (re-paced: 7 minutes a season) | our own bird world's `seasons.js` | `src/seasons.ts` |
| Bread, begging, the sight cone, flying to a far crust, the curious visitor, cursor gaze, the camera, pacing | new in quack | `src/pond.ts`, `src/main.ts`, `src/camera.ts` |
| The lead: a hole the freeze never closes, and the rules that keep a duck from being caught by the ice | our own bird world's `js/world.js`, `js/bird.js` | `src/world.ts`, `src/pond.ts` |

The research citations behind the upstream constants live in
`duck-pond/ATTRIBUTION.md`.

Ported in v2: the audio stack, unchanged from the Rust, driving the flock quack
already has. `Stage` in the vendored `duck-pond/src/scene.rs` (kept as
`audio/duck-pond-stage.patch`, built by `scripts/build-audio.mjs` into
`public/quack_audio.wasm`) adds a manual sound layer: quack supplies the ducks'
voices, their places in the listener's frame, and the sound kinds; the Rust
answers with the same synthesis, distance attenuation, speed-of-sound delay,
head shading and reverb as the live pond. It runs in a worker behind an
AudioWorklet, wired in `src/audio.ts`; `docs/audio.md` maps every sound to the
action that makes it.

The calling model itself was ported as behaviour in v1: the intervals, lottery,
one-hop answer chain and brood recall run in `src/pond.ts`, tuned by `CALLS`. A
call is still a head/bill gesture in the rig (and a `CallEvent` on
`Pond.onCall`), but the event now also plays, through `AudioCore.call`; a lost
duckling's "peep peep" and a content duckling's "pip-pip" are separate call
kinds, not a random chirp.

## The flock

Twelve mallards, built in `src/pond.ts`:

- Hens: **Mabel** (brood: Pip, Dot, Bean, Fig), **Clover**, **Hazel**
  (brood: Nib, Tuft, Sprout).
- Drakes: **Rufus** (Clover's mate; trails her), **Bramble** (lone).
- Ducklings follow their mother; drakes follow their mate.

Activities: `swim | dabble | rest | preen | stray | feed | beg | fly`, each
running sub-actions (`idle`, `look`, `sleep`, `stretch`, `preen`, `peck`,
`drink`, `dabble`, `upend`, `chirp`, `beg`). `feed`, `beg` and `fly` are ours;
the rest carry over duck-pond's time budget (~40 % feeding, ~30 % resting,
~15 % preening). A duck on the wing is flown by the pond along a straight line
and posed by the rig; only an unencumbered duck ever flies — a hen with a brood,
or a duckling, keeps to the water.

Calls are scheduled beside the activities, not inside them: `CALLS.interval`
sets the next one by occasion (lost young, lost duckling, feeding, a drake's
mate out of reach, dabbling, everything else), a resting duck skips 80–90 % of
them, and a hen's series or decrescendo can set one hop of answers going.
Ducklings peep when lost and pip-pip otherwise; a hen gathering her brood or
answering lost young gives the `kwek-kwek` brood call and cues stray ducklings
back. Each is heard as the matching Rust call — see `docs/audio.md`.

## Pond geometry

| piece | value |
| --- | --- |
| pond ellipse (`WORLD.pond`: cx, cz, rx, rz) | 0, −22, 46, 64 |
| beach strip (`WORLD.bank`) | x ≤ 26, z 41.5–52.5 |
| thrower's stand (`WORLD.thrower`, what the flock reacts to) | (0, 47) |
| viewer's stand (`WORLD.viewStand`, the camera's eye) | (0, 48.3); the water in front of it is kept clear of reeds |
| depth bands (`WORLD.bands`, world z) | 22–38 / 0–22 / −38–4 |
| chase region for bread (`WORLD.chase`) | x ±120, z −120…43.5, and any water a duck can be in — open, shallow or frozen |
| walkable shore (`WORLD.shore`) | x ±130 on the waterline, 3 m (edge) to 21 m inland |

The near land is a **bank**, not a meadow that happens to be near the water: south
of `shorelineZ` the ground climbs clear of the water (0.7 m at the water's edge to
about 3 m, 90 m inland) and stays clear, so the camera always stands on dry ground
and the water's edge is a clean line rather than a patch of land afloat in the
shallows. That climb is steep — most of it inside 2.5 m — so the mesh has to
resolve it: at 6.94 m steps the corners straddle the bank's foot, the interpolated
surface floats above the ground, and the *drawn* water's edge lands 2.7 m out into
the water, stranding the reeds and any duck the field puts in the shallows. At
3.5 m steps the drawn edge lands within 0.1 m of the field's. `WORLD.shore` +
`shorePoint` / `shorePointAt` / `shoreNormal` / `onShore` describe the walkable
stretch of that bank, ready for later interaction.

## Tuned constants

Every dial lives in `src/config.ts`, one export per scope: `APP`, `INPUT`,
`LOADER`, `HINTS`, `CAMERA`, `SEASONS`, `WORLD`, `FLOCK`, `CALLS`, `SHORE`,
`WINTER`, `LAP`, `FLIGHT`, `BREAD`, `SIGHT`, `CURIOUS`, `SPACE`, `DUCK`,
`EFFECTS`. A module imports only the groups it spends, and each field carries the reasoning
that used to sit beside the code. Two things deliberately stay where they were
written: the season colour keys in `src/seasons.ts` and the species data in
`src/duck.ts` (read as whole pieces of art), and the rig's pose curves in
`src/duck.ts` (a shape, not a number).

The table gives the tuned value; `config.ts` is the source of truth. The last
column is the module that spends it.

| knob | value | spent in |
| --- | --- | --- |
| `SEASONS.seconds` | 420 (7 min; a year = 28 min) | `src/seasons.ts` |
| `APP.openSeason` | the year opens in summer | `src/main.ts` |
| winter ice | season key `ice` 1: the pond locks solid, but for the lead | `src/seasons.ts` |
| `WORLD.lead` | centre −3.5, 29; r0 8.5, r1 11.5 m; rim wobbled ±1.85 m | `src/world.ts` |
| `WORLD.ice.walk` | 0.5 — how thick the lid must be to carry a duck | `src/world.ts` |
| `WORLD.ice.still` | 0.3 — how frozen a spot must be for the swell under it to be pinned: the lid is perfectly still, and only the open pool still answers the clock (the water shader repeats it) | `src/world.ts` |
| `WORLD.water` | wade 0.05 / swim 0.3 / deep 0.8 m: the shallows are walked, deeper water paddled, and 0.8 m is enough to upend | `src/pond.ts` |
| winter goals | inside the lead (within 0.9 × r0); the odd swim bout (`WINTER.loaf` 0.045) bound for the lid's rim instead | `src/pond.ts` |
| a visit ashore | gated on a swim bout starting within `SHORE.gate` 26 m of the thrower: `SHORE.visit` 0.35, `SHORE.broodVisit` 0.5 for a hen with ducklings, both halved once the pond is iced over (`SHORE.winterVisit`) | `src/pond.ts` |
| a spell ashore | `SHORE.stay` 35–90 s on the bank, `SHORE.iceStay` 15–45 s on the lid; the water pulls at each bout end — `SHORE.returnToWater` 0.75, `SHORE.returnFromIce` 0.85 | `src/pond.ts` |
| the lap of the lake | `LAP.chance` 0.14 of the bout ends of an in-frame duck in the water — a wandering round drawn fresh through the bird's position each time: radius `LAP.radius` 4.5–9.5 m (`LAP.winterRadius` 3–5.5 m in the lead), a random way round, and a two-harmonic wobble (`LAP.wobble` 0.15, `LAP.wobble2` 0.1) bowing the path in and out by up to 25% of the radius; `LAP.turns` 0.55–1.5 turns a bout, `LAP.out` 0.3 of them swinging out of the picture; a duck out of the picture is sent back at `FLOCK.returnToView` 0.8, its pastime cut to `FLOCK.outViewBout` 50 s | `src/pond.ts` |
| the flight | `FLIGHT.chance` 0.18 of the bout ends of an unencumbered, in-frame duck (× `WINTER.bouts` 0.55 in the freeze), plus any far piece of bread (below); a low pass `FLIGHT.reach` 16–34 m, `FLIGHT.peak` 0.8–1.45 m × body scale up, `FLIGHT.time` 4.2–8.5 s, the landing inside `FLIGHT.far` 40 m of the thrower; `FLIGHT.cool` 20–45 s after each landing | `src/pond.ts` |
| hull speed, adult | `FLOCK.hullSpeed` 0.71 m/s | `src/pond.ts` |
| hull speed / burst, duckling | `FLOCK.ducklingHullSpeed` 0.43 / `FLOCK.ducklingBurst` 1.26 m/s | `src/pond.ts` |
| brood pace (hen with ducklings) | `FLOCK.broodPace` 0.3 m/s | `src/pond.ts` |
| cruise speeds (hen / drake / duckling) | 0.45 / 0.5 / 0.33 m/s (per kind, in the rig) | `src/duck.ts` |
| lost duckling | `FLOCK.lost` — stops beyond 4 m from mother | `src/pond.ts` |
| stray mean interval | `FLOCK.strayEvery` 900 s | `src/pond.ts` |
| calls | `CALLS.first` 0.5–8 s before the first; then `CALLS.interval.lostYoung` 1.5–3, `.lost` 0.2–1, `.ducklingFeed` 1.5–5, `.duckling` 4–15, `.feed` 2.5–6, `.drakeAway` 8–20, `.drake` 45–180, `.dabble` 15–60, `.other` 20–90 s, in that precedence; a resting duck skips `CALLS.sleepSkip` 0.9 (adult) / 0.8 (duckling) | `src/pond.ts` |
| answers | a social call reaches 40 m: mate p = `CALLS.answer.mate` 0.6, others `CALLS.answer.other` 0.3 × (1 − dist/40); answer due in `CALLS.answerDelay` 0.8–2.5 s, one hop | `src/pond.ts` |
| brood recall | a lost duckling cuts its mother's timer to `CALLS.motherDelay` 0.5–2.5 s; her brood call runs `CALLS.broodNotes.lost` 6–10 notes (gathering: 3–6) and cues stray ducklings | `src/pond.ts` |
| `INPUT.tapSlop` | 12 px | `src/main.ts` |
| cursor gaze limits | `DUCK.look.yawMax` 1.55, `DUCK.look.pitchMax` 0.72 rad | `src/duck.ts` |
| gaze shaping | `DUCK.look.sweep` 0.9, `DUCK.look.tilt` 1.1 | `src/pond.ts` |
| bread flight time | `BREAD.time`: clamp(0.45 + dist * 0.04, 0.6, 1.9) s | `src/pond.ts` |
| bites per piece | `BREAD.bites`: 2 or 3 (50/50) | `src/pond.ts` |
| bread lifetime | `BREAD.lifetime` 100 s, then it sinks | `src/pond.ts` |
| begging time | `BREAD.begTime` 25–40 s at the landing spot | `src/pond.ts` |
| seeing the bread | a duck head-up with the landing spot within `SIGHT.range` 55 m and inside `SIGHT.half` 1.15 rad of its bill starts for the splash at 25–70% of the piece's flight; everyone else only hears the splash and comes late | `src/pond.ts` |
| flying for bread | a free duck with a piece `FLIGHT.food.near` 15 m or more away, rising to `FLIGHT.food.max` 0.55 at `FLIGHT.food.far` 42 m; it comes down `FLIGHT.food.gap` 1.4–2.6 m off the piece on open water, one bird to a piece | `src/pond.ts` |
| the curious duck | `CURIOUS.chance` 0.08 of bout ends for a duck in the water within `CURIOUS.gate` 46 m of the stand with no bread down; one at a time, `CURIOUS.every` 150–420 s apart, `CURIOUS.time` 25–60 s of begging after the swim over | `src/pond.ts` |
| floating bread rings | `BREAD.ring.every` 1.4–4.2 s, `BREAD.ring.scale` 0.2–0.3 (2–3× the piece), in open water only — a piece out on the lid lies still | `src/pond.ts` |
| bread scatter | a tap within `BREAD.scatter.crowded` 0.55 m of floating bread is skewed `BREAD.scatter.near`–`BREAD.scatter.far` 0.24–0.6 m off the cursor, re-rolled until it clears every piece by `BREAD.scatter.gap` 0.24 m | `src/pond.ts` |
| personal space | hull `SPACE.hullLen` 0.31 m long × `SPACE.hullBeam` 0.18 m beam, per unit of body scale; gap `SPACE.familyGap` 0.03 m inside a family, `SPACE.strangerGap` 0.10 m between strangers | `src/pond.ts` |
| the waddle | one hinge per leg at the hip; a step covers `2·DUCK.legs.length·sin(DUCK.legs.swing)` ≈ 0.14 m × body scale over `DUCK.legs.stance` 0.62 of the cycle, so the cadence falls out of the speed; swing sole lifts `DUCK.legs.lift` 0.022, and the body rides ~0.015 m × body scale through a cycle | `src/duck.ts` |
| the sleeping sit | a duck asleep on solid ground folds its hips to `DUCK.sit.tuck` 1.55 rad and settles `DUCK.sit.drop` 0.04 rig units × body scale, until the belly is 7–9 mm × body scale *into* the shore or lid; eases at 2.6/s | `src/duck.ts` |
| breathing | `DUCK.breath.amount` 1.4% at `DUCK.breath.rate` 3.2 rad/s, on the torso mesh alone — head, bill, wings, tail and legs keep their size | `src/duck.ts` |
| avoidance field | a soft nudge from `SPACE.reach` 1.3× the hull distance out, up to `SPACE.push` 0.28 m/s | `src/pond.ts` |
| camera | first person: the eye planted at `WORLD.viewStand`, `CAMERA.eyeHeight` 2 m above the ground; `CAMERA.yawLimit` ±0.85, pitch `CAMERA.pitchMin`…`CAMERA.pitchMax` −1.0…0.45, resting at `CAMERA.pitchStart` −0.06 | `src/camera.ts` |
| camera zoom | `CAMERA.fov`: 10–78°, opens at 52°; wheel and pinch ask for equal *ratios* per pixel, and the ease runs in log space. The floor holds a slice of horizon rather than a number of degrees: 10° vertical on a 16:9 screen (`CAMERA.fov.refAspect`), opening up on narrower ones (`fovFloor(aspect)`) so a phone's portrait frame always shows the same width of pond at full lean-in | `src/camera.ts` |
| shore bank floor | `WORLD.bankFloor` 0.7 m at the edge → 3.0 m at 90 m inland | `src/world.ts` |
| walkable shore | `WORLD.shore`: ±130 m along the waterline; edge 3 m, depth 18 m | `src/world.ts` |
| terrain mesh | `WORLD.segments` 432 (3.5 m) over `WORLD.size` 1500 m; the slope is read off the grid, not re-sampled | `src/world.ts` |
| ripples | `EFFECTS.ripple`: lift 0.035, life 1.3–1.8 s, growth 0.4 + 2.4 t, alpha 0.55, 24 rings | `src/effects.ts` |
| particle pools | `EFFECTS.capacity`: 180 feathers, 320 droplets, 220 leaves, 300 snowflakes; `EFFECTS.weather` holds the shedding rates | `src/effects.ts` |
| tone mapping | ACES, `APP.exposure` 0.98 | `src/main.ts` |
| default seed | `APP.seed` 20261018 (`?seed=N`) | `src/main.ts` |

## Conventions

- Units are meters and seconds; y-up; a duck faces +z before posing.
- **Head pitch sign**: positive X tips a +z-facing part (the bill) down — for an
  unyawed bill. In the sleep pose the head is yawed 2.85 rad (~163°), so the
  sign flips and a *negative* pitch is what lowers the bill onto the back.
- **Sleep pose**: the head is drawn right around over a shoulder with the bill
  trailed back along the flank. Tangency of the jaw's underside with the
  torso/wing shell sits near −0.18 rad, but flush is the wrong answer — the tip
  disappears into the body volume from most angles, the player's included. The
  code holds the bill at **+0.06 rad**: still lying on the feathers, top edge
  clear of the body line, so the tip reads from either flank. Checked on hen,
  drake and duckling, close up and from the stand.
- **Sleeping is sitting**: a duck that dozes off on solid ground folds its legs
  up under itself and settles onto its belly; dozing upright on straight legs
  read as standing with the head tucked, which is a horse, not a duck. The
  trigger is `action === 'sleep'` while on land or the lid and still; the hips
  fold to `DUCK.sit.tuck` (1.55 rad, the swimming tuck) and the standing height
  drops by `DUCK.sit.drop` (0.04 rig units × body scale), a little *more* than
  it takes to bring the belly down to the surface — the body presses a feather's
  depth into the shore or ice instead of touching it tangentially. Tangency
  reads as hovering, and the ice would draw the contact as a hard line; sunk in,
  the belly and the stern are cut by the ground plane and the bird sits in it.
  The idle bob fades out with `sit` so the settled body does not breathe itself
  clear of the ground, and the whole settle eases at 2.6/s, so lying down and
  getting up are movements. Sleeping afloat is untouched: a loafing duck already
  has its legs at 1.55 and rides the swell. (Measured at drake scale 1.46: the
  body drops 5.8 cm, the belly's lowest point crosses the ice by ~7 mm, the
  folded feet ride 13 cm up inside the belly and never break its silhouette.)
- **Breathing is the body's alone**: the idle breath is a scale on the torso
  mesh, not on the root. Scaling the whole rig made the head, bill and feet
  pulse along with the chest, and quietly changed the legs' length every breath
  — so a planted sole crept a millimetre or two while the duck stood still.
  Now the chest fills and empties and everything else holds its size; the neck
  is short enough that the 1.4% seam stays buried in the plumage. Measured at
  drake scale 1.456: torso world scale runs 1.436–1.477, while head, bill,
  wings, tail and feet all stay at 1.456.
- **Wing stretch**: both wings come up and wave, one a little higher than the
  other, and both *stay* up. A floating duck's shoulders sit barely above the
  water, so a wing swinging down out of the stretch dips straight into it and the
  pair reads as one broken diagonal; the asymmetric part is therefore added on
  both sides rather than being a mirror of the flap.
- **Cursor gaze**: every duck pans and tilts for the pointer across the whole
  viewport; the easing keeps the middle gentle while the corners reach the full
  gaze limits. Driven by `pond.setCursor(x, y)` with NDC coordinates.
- **Tap vs drag**: no time limit — press as long as you like, release to throw.
  Only a drift past `INPUT.tapSlop` turns a press into a camera drag. Right-button
  presses and second fingers are ignored. A duck that never sees the throw reacts
  only once the bread lands; one that does (below) is off while it is still in
  the air.
- **Bread**: leaves the hand just below the camera (1.2 m along the throw
  direction, at least 1 m above the water), arcs to the landing spot, then
  splashes or kicks up dust. Landing spots are clamped into the chase region so
  the flock can always reach them, on open water, in the shallows or out on the
  ice. A piece left floating nudges the surface now
  and then — a small ring, a couple of times the size of the piece — so it stays
  findable without shouting like the splash it landed with.
- **Seeing the throw**: a duck with its head up has the whole pond in front of it,
  but no reason to look up — unless something arcs over the water. A piece whose
  landing spot lies within `SIGHT.range` (55 m) and inside `SIGHT.half`
  (1.15 rad) of the bird's bill is *seen*: that duck sets off for the splash at
  25–70 % of the piece's flight (`sees`, `maybeFlyToFood`), so it is already
  moving — and sometimes already eating — by the time the ring spreads. The rest
  only hear the splash, the near ones first (`T/12` per metre), so the flock
  comes in waves, and the birds facing the other way or asleep always come late.
  A duck on the wing keeps its invitation until it is down, so a flight for one
  piece is never cut short by another. Measured (20 min, a throw every 110 s):
  22 ducks set off for a piece before it touched down, against 101 that came
  after the splash.
- **Rings ride the swell**: every ripple — splash, wake, nibble, bread nudge —
  is pinned to `waterHeightAt(x, z, time)` for its whole life, never to the
  height it was spawned at. The swell runs ±0.3 m here, so a ring left at a
  fixed height hangs in the air beside its duck the moment the surface drops
  away from it; a wake spawned at the duck's own waterline would otherwise start
  6 cm *under* the surface. `Effects.update` stores the world and clock so
  `ripple()` and the pool can re-seat every ring each frame.
- **Repeated taps**: a landing spot within `BREAD.scatter.crowded` (0.55 m) of
  bread already there is skewed off the cursor — a random bearing,
  `BREAD.scatter.near`–`BREAD.scatter.far` (0.24–0.6 m) out — and re-rolled
  until it clears every piece by `BREAD.scatter.gap` (0.24 m, a piece being a
  0.1 m pebble). A few taps on one spot then make a little scatter of crusts to
  pick at, close together but never intersecting. The skew stands only if the
  ducks can still reach the spot.
- **Winter, and the lead**: the cold comes in from every edge, but it never
  closes one patch of water — `WORLD.lead` (centre −3.5, 29; rim 8.5 to
  11.5 m out), set a little left of the middle of the gaze the camera opens on.
  `iceAt(x, z)` is how frozen a spot is: 0 inside the lead, 1 far from it, ramped
  across the rim, and the rim is wobbled by `leadWobble(angle)` in integer
  harmonics so it closes on itself without a seam. The water shader repeats both
  formulas from the same constants (passed as uniforms), so the sheet that is
  drawn and the sheet the rules know about agree to the metre. Its swell is
  pinned as soon as the sheet starts to skin over — `WORLD.ice.still` 0.3, well
  short of the `WORLD.ice.walk` 0.5 that lets a duck stand on it — and the
  fragment shader repeats the same pinning, so the whole lid is perfectly
  still: on screen, and in `waterHeightAt`, which is what a duck standing on
  it, and a ring that lands on it, both ride. Only the open pool still answers
  the clock. Measured at full ice: all 78 sampled lid points had zero swell
  amplitude across 24 s of clock, and the ice read 0 changed pixels in a
  straight-down render two seconds later, while the pool moved 45 % of its
  pixels in the same frame (and open water in summer 44 %, the control). Through
  the winter the lid reaches further in, so the flock is pushed off the
  shallows, gathers in the lead, and begins to stand about on the ice.
- **What a duck may be in**: `solidAt` — ice at `WORLD.ice.walk` (0.5) or thicker —
  joins the media a duck may occupy. It swims open water, wades the shallows
  (0.05–0.3 m: too shallow to paddle, shallow enough to walk through), and walks
  the lid; those are the only rules that changed, and steering is the same in
  all three. `Pond.stranded()` is the net under all of it: a duck standing where
  nothing allows it any more — the freeze closed around it, the thaw took the lid
  from under it, a shove left it high and dry — is walked out to the nearest spot
  that is allowed, because steering cannot free a duck that has nowhere to steer,
  and one that is not freed stands still for good and a hen waiting on her brood
  waits for ever. Measured through a freeze (30 s of sim once the pond is locked):
  no duck-frame had a duck standing where the rules forbade it, and none was left
  stuck — five of the twelve paddling in the lead and seven about on the lid, and
  a crust thrown on the lid is walked to and eaten.
- **Ice that reads as ice**: the sheet takes the lead's rim as a slush edge that
  rides the freezing front, then a grained, patched lid — snow lying on it in
  patches and the ice scoured bare between them a shade darker, so that even at
  the grazing angle the camera opens on the sheet is a surface rather than a
  mist. It turns properly solid just *past* `WORLD.ice.walk`, so nothing is ever
  seen paddling on something that already reads as ice, and the open water
  deepens and cools as the season turns, which is what makes the lead read as the one hole in
  the lid. (borb's sheet was a single colour-and-alpha mix; the grain, the slush
  and the patches are ours.)
- **Bread on ice, and in the shallows**: a crust may land on the lid or in the
  shallows, and the flock walks out to it — nothing is ever re-landed on water
  they cannot reach. A piece on the lid does not bob and does not ring (it is not
  floating); it kicks up dust when it lands, like one on the bank.
- **Ashore is a visit, not a move**: the bank and the winter lid are places to
  pass the time, never somewhere to go *to*. A duck leaves the water only on a
  whim of the swim model — a swim bout starting within `SHORE.gate` (26 m of the
  thrower) turns into a visit with `SHORE.visit` 0.35 (`SHORE.broodVisit` 0.5
  for a hen with ducklings; `SHORE.winterVisit` halves both, because a spell
  ashore then means a march over the frozen lid) — and once it is out, no bout
  targets another spot out there: a pastime is all the footing model offers, and
  each bout end walks the duck back to the water, `SHORE.returnToWater` 0.75 on
  the bank and `SHORE.returnFromIce` 0.85 on the lid. The odd bout takes another
  pastime instead, so a visit is one to three bouts — not instant, and not a
  migration.
  Measured in summer (45 min): four adult visits for the flock, 112–226 s each,
  ~12 % of duck-time out of the water, and most of that one hen's brood on the
  bank; in winter (20 min × 3 seeds): 7–12 % of duck-time on the lid, over half
  of it the walk across to the bank, the rest loafing in 15–45 s bouts. No duck
  ever stood anywhere the rules forbade, and none was left out for long.
- **One year, one cast**: quack is a single year in a timeless place. The same
  twelve birds are here in January as in June, and the ducklings never grow up;
  the seasons turn around them. This is a deliberate simplification, not a
  migration model, and it is the reason the winter cast can stay fixed.
- **The picture keeps the flock**: a duck that ends a bout outside the camera's
  wedge is sent back to its band's water (`FLOCK.returnToView` 0.8), and a
  pastime already running out there is cut to `FLOCK.outViewBout` 50 s. `dabble`
  is offered only in water inside the wedge, which was the leak that let a
  dabbler drift over the line and then hold position out there for a whole bout;
  a duck already outside keeps its ground (and dabbles where it stands) until
  the bout ends.
- **The lap of the lake**: a duck that would settle to the nearest pastime
  sometimes goes off around the water instead — `LAP.chance` 0.14 of the bout
  ends of an in-frame duck, 0.55 of that in the freeze (`WINTER.bouts`). The
  round is drawn fresh every time, *through the duck's own position*, so the
  whole bout is the round and never a swim out to some far-off circle: a random
  radius (`LAP.radius` 4.5–9.5 m, `LAP.winterRadius` 3–5.5 m in the lead), a
  random way round, and two radial wobbles (`LAP.wobble` 0.15, `LAP.wobble2`
  0.1, incommensurate with the circle and with each other) that bow the path in
  and out by up to a quarter of the radius, so no two laps read the same and
  none closes into a stiff circle or ellipse. The pond walks the mark along the
  arc a few metres ahead of the bird (`stepLap`),
  and a bout swims `LAP.turns` 0.55–1.5 turns of it — part of the round, or all
  the way about — before the bird gets its day back. A round that is asked
  for out of the picture (`LAP.out` 0.3) skips the wedge test, so part of it
  plays off the edge of the frame and back in. Measured (30 min summer): four
  laps, radii 4.6–8.9 m, three one way round and one the other, 0.7–1.16 turns
  each, the path holding the round to 0.91–0.96 of its radius, the net turn
  matching the lap's own direction every time, all four inside the wedge.
- **The flight**: now and then a duck takes the short way across on the wing
  instead of swimming it. `FLIGHT.chance` 0.18 of the bout ends of an
  unencumbered duck that is inside the picture become a low pass over the water,
  `FLIGHT.reach` 16–34 m long and `FLIGHT.time` 4.2–8.5 s in the air, riding
  `FLIGHT.peak` 0.8–1.45 m × body scale over the surface at its peak; the
  landing spot is re-rolled until it is clear of the ducks already there and
  within `FLIGHT.far` (40 m of the thrower), where the flock still reads — a
  duck out in the far water flies shorter and tends back towards it.
  A bird that has just come down stays down for `FLIGHT.cool` 20–45 s, so flights
  never chain into a shuttle. The pond flies the bird (a straight line, held off
  and on the water, climb — glide — settle), and the rig does the wings, from
  borb's air poses: a hard beat to get up, a long glide with barely a tip moving,
  then the flare and backbeat that sets it down, with the bill lifted on the
  climb and again on the flare and the wingbeat carrying the body up and down.
  Splashes at both ends — a puff of snow if it sets down on the lid — legs
  tucked, tail spread. Nothing on the water touches a bird on the wing — it is
  listed for no hull, no ripple wake, no cursor and no unclaimed bread, and
  steering leaves it alone — so it passes over the flock and over the ice with no
  trouble. Measured (30 min summer, no bread): eight flights from the flock's two
  eligible birds, 17.8–32.9 m, every one landing on open water, none chained; in
  winter, three short hops over the lead in 25 min.
- **Flying for bread**: a crust that comes down across the pond is worth the
  wing rather than the paddle. A duck that has decided to come for a piece
  measures it, and if the bird is `FLIGHT.food.near` (15 m) or more away the
  chance rises with the distance to `FLIGHT.food.max` 0.55 at `FLIGHT.food.far`
  42 m (`maybeFlyToFood`); it comes down in the water `FLIGHT.food.gap` 1.4–2.6 m
  off the piece on a random bearing and paddles the last of the way, so it never
  lands on its own mouthful, and only one bird flies for a piece — the rest
  swim, which is what keeps a far throw from emptying the pond
  into the air. Only ducks with nothing to look after fly for it (a hen with a
  brood stays on the water), and a piece out on the bank is left to the walkers. Measured: with a throw every 110 s, six food flights in 20 min at
  25.7–51.4 m, every one onto water and five of six still going for the bread
  at touchdown.
- **The curious duck**: now and then it is the duck that notices the visitor,
  not the other way round. At a bout end, a duck in the water within
  `CURIOUS.gate` (46 m of the stand), with no bread down, may take it into its
  head to come over: `CURIOUS.chance` 0.08, and the flock keeps at least
  `CURIOUS.every` 150–420 s between visits, so it reads as one bird's idea
  rather than a habit. It crosses at a purpose (`hurry` until it is within 4 m
  of the shallows in front of the stand), then stands about there for
  `CURIOUS.time` 25–60 s, head up and the odd imploring bill-open, begging at
  nothing — until a piece of bread lands anywhere and the flock's usual business
  resumes. In a freeze the
  bank is out of reach, so it comes to the nearest open water instead: the rim of
  the lead. Measured (25 min winter): two such visits, 66–83 s each, each ending
  9.3–9.7 m from the stand at the lead's near rim; in summer one bird crossed
  43 m to beg 5.8 m from the stand.
- **Personal space**: every duck carries a hull — an ellipse half a body long
  (`SPACE.hullLen`, 0.31 m) and a quarter wide (`SPACE.hullBeam`, 0.18 m), in
  its own units of body scale, so a drake is a larger obstacle than a duckling —
  plus a little water between hulls: `SPACE.familyGap` (0.03 m) for a mother and
  her brood, a pair of mates or siblings, `SPACE.strangerGap` (0.10 m) between
  strangers. It is a nudge first: steering eases every duck off the space its
  neighbours hold, from `SPACE.reach` 1.3× the hull distance out (up to
  `SPACE.push` 0.28 m/s), which is what keeps ordinary meetings from turning
  into contact. Anything that does touch is settled after everyone has moved:
  `Pond.separate()` measures every pair against
  that hull distance and shares out any overlap, then takes the closing speed out
  of the pair, so the next frame cannot drive them straight back in. A duck that
  cannot give ground (a bank at its back) hands its share to the other; a
  sleeper or an upended duck takes a quarter share and is given no new velocity
  at all, so nothing can set it adrift or swing it about. Two ducks therefore
  meet nose to nose further apart than they meet flank to flank, and a scrum on
  one crust shoulders up like real dabbling ducks instead of intersecting.
- **Anchored poses**: a duck in `sleep` or `upend` takes no steering input at
  all — no wander, no drift on the spot, and no slow turn as its mark shifts.
  Only a neighbour's crowd-avoidance push can move it (a quarter of the parting
  push above, and never any new velocity). A duck standing on its
  bill is planted in the mud; if it kept paddling after a mark it would drag the
  pose off the spot it chose.
- **The waddle**: a duck on land hinges each leg exactly once, at the hip, and
  keeps the step on the ground. Each leg is a hip group (`hipL`/`hipR`), an
  ankle group and a foot mesh, so the sole can be held flat while the shank
  swings; both sides share one shank and one foot geometry, mirrored by the
  hinge's own position. Through the stance (`p < DUCK.legs.stance`, 0.62 of a
  cycle) the hinge angle is the arcsine of a linear ramp —
  `asin(sin(DUCK.legs.swing)·(2p/DUCK.legs.stance − 1))` — which is exactly the
  angle that holds a foot of length `DUCK.legs.length` (0.15) on the spot it
  landed on while the body crosses the ground over it: the planted sole never
  skates, and it lies flat the whole time, because the ankle cancels the hinge.
  The swing carries the foot back to the front on a smooth ease, sole lifted
  clear of the ground (`DUCK.legs.lift`, 0.022, the shank telescoping up into
  the belly) and toe up. A step covers
  `2·DUCK.legs.length·sin(DUCK.legs.swing)`, about 0.14 m per unit of body
  scale, spread over `DUCK.legs.stance` of the cycle, so the cadence falls
  straight out of the speed —
  `speed·DUCK.legs.stance/(2·DUCK.legs.length·sin(DUCK.legs.swing)·scale)`,
  near 1.9 cycles/s at a hen's cruise, quicker for a duckling's shorter legs
  because the
  step in metres shrinks with it. The body rides the leg that is on the
  ground — the one hanging lowest of them — at exactly that leg's vertical
  reach (`legReach`), so the planted sole holds its height as well as its
  spot, and the body rides back up out of the lean, which presses the outside
  foot into the ground by as much again; what is left over is the step's own
  weight: high over the straight planted leg, low as the feet spread. Over
  that it leans onto the planted foot (`DUCK.waddle.roll` 0.13 rad) and the
  stern answers once a step (`DUCK.waddle.wag`). The hips answer the lean in
  turn, so the legs hang plumb and the body rocks *over* the planted foot
  rather than swinging the foot through an arc — which is what holds the sole
  still
  sideways as well as fore-and-aft. A duck without a step's worth of weight
  in it — standing, floating, or swimming with the legs tucked — damps
  `stride` to 0, so the legs ease back under the body and a stop never
  freezes mid-stride. Measured on the ice with `scripts/gait.mjs` (a hen at
  body scale 1.46, walking 0.45 m/s): the planted sole holds its spot — no
  forward travel at all over a stance, and sideways under 2% of the ground the
  body covers — and its height to within a centimetre, most of that the body's
  own breathing. The swing sole clears by up to 4.7 cm, the lean runs ±5°, and
  the body rides 1.4 cm per unit of body scale through a cycle. Cadence came
  out at 1.32 cycles/s against 1.32 predicted, sweeping 100% of the ground it
  crossed.
- **Bottom feeding (`upend`)**: the whole body tips forward about the waterline
  pivot — the duck's own `root` origin, ~9 cm under the surface, roughly the
  body's centre, which is where a real upender turns. `DUCK.upend.pitch` is
  1.62 rad (a little past vertical) reached over a 0.8 s ramp, held with the feet
  paddling and the stern wagging, then rocked back in 0.75 s. The `dabble`
  activity offers it only to a duck that is afloat — never out on the lid — and
  only where `depthAt > DUCK.upend.depth` (0.8 m), so the bill really is in the
  mud; ducklings never upend. Rings and droplets fire at both ends of the move.
  Measured: the head goes ~0.24 m under the surface (visible through the
  transparent water) and the tail ~0.47 m above it.
- **Camera**: first person, planted on the shore. Drag turns yaw (right = turn
  right) and pitch (down = the gaze drops); pinch and wheel change the field of
  view. The eye never moves, so the pond keeps its place and only the gaze
  swings across it. Reeds are kept clear of the water directly in front of the
  stand (`WORLD.viewStand`), so no stem parks itself in the eye.
  Zooming has no centre of its own — it narrows the frame around wherever the
  gaze rests — so the gaze rests a touch below level (`CAMERA.pitchStart`,
  −0.06 rad): that is the line a full lean-in keeps, and it frames the whole
  flock from the near families to the far band.

## The loading card

The pond is held behind a card for its first moment: the view it opens on, shot
without a duck in it and thrown out of focus, with "quack ... quack" coming up a
letter at a time over it. When the world is up, the card fades away and the live
pond is left behind, framed the same way.

- **The face**: `public/pond-loader-portrait.jpg` and `-landscape.jpg`, shot by
  `scripts/loader-shot.mjs` from the camera's opening pose (summer, default
  seed, `app.paused`), with the flock and the particle pools hidden. Progressive
  JPEG (mozjpeg, 4:2:0, q50) at 480 and 880 px wide — 3.9 and 5.4 kB, so the
  card has its picture before the module is down.
- **The wash**: `filter: blur(17px)`, with the image stretched to the element
  (`100% 100%`) rather than cover-cropped. Under that blur the stretch never
  shows, and it keeps the far shore on the same line of the screen as the live
  camera's at any aspect ratio — card 43.0–43.4 % down, live 44.0 %, measured on
  a phone and a 4:3 tablet — so the handover has no jump to hide.
- **The word**: five spans — word, three dots, word — each easing out of a 6 px
  blur and *fully in* before the next starts (word 0.38 s, dot 0.2 s, 0.05 s
  between; 1.61 s end to end), so the line reads in five distinct steps rather
  than as one smear.
- **The handover**: `hideLoadingSoon()` keeps the card to `LOADER.min` = 1950 ms
  from navigation — enough for the last word even on a fast load, with a beat on
  the finished line — then `.done` fades it over `LOADER.fade` 900 ms and removes
  it `LOADER.removeDelay` 400 ms later. The class also drops `pointer-events`,
  so the pond can be dragged while the fade is still going.
  `prefers-reduced-motion` puts the line up at once and fades in 0.3 s.

## The title cards

Five lines the pond cannot show for itself come up low over the water after the
loading card has gone, one at a time, and leave again: the throw instruction,
the look instruction, the zoom instruction, the credit to celphase's duck-pond,
and the source card.

- **One line, one card**: `HINTS.tipHold` 3 s holds for the three tips,
  `HINTS.creditHold` 7.5 s for the credit and `SOURCE.hold` 6.5 s for the
  source, `HINTS.fade` 0.6 s fades (the CSS runs 0.55 s), `HINTS.gap` 0.5 s of
  quiet water between. The first starts 1.9 s after the loading card begins to
  fade — with a normal boot that is about four seconds after navigation, so the
  pond is revealed first and the tips come second.
- **The source card**: `SOURCE.src_url` in `src/config.ts` is the public
  repository. When it is a non-empty string the card opens that URL in a new
  tab; when it is empty or null the card instead offers
  `public/quack-source.zip`, a download of this working tree. The zip is built
  by `scripts/source-zip.mjs` from `src/`, `public/`, `scripts/`, `docs/`,
  `audio/` and the root project files, and leaves out `node_modules`, `dist`,
  `temp`, `duck-pond`, `.wrangler` and every local credential file. The build
  runs it before Vite, so the copy in `dist/` is current. It is generated, not
  committed.
- **Source maps**: `vite.config.ts` sets `build.sourcemap: true`, so every
  production build ships its `.js.map` beside the bundle. That keeps the live
  site debuggable and lets anyone compare the deployed code with the source zip
  module by module.
- **Device wording**: `(hover: none) and (pointer: coarse)` picks "tap to throw
  bread", "drag to look around" and "pinch to zoom"; a fine pointer gets
  "click", "drag" and "scroll to zoom". The wording follows the *primary* input,
  so a touchscreen laptop still gets mouse words. The controls are described as
  they are: drag turns the gaze, pinch or wheel changes the field of view.
- **Hardly there**: an 0.33-alpha panel with a 9 px backdrop blur, a 1 px border
  and a 13 px radius — legible over bright water without turning into a HUD. The
  container is `pointer-events: none`; only the links re-enable the pointer, so
  taps and drags pass through the card to the pond everywhere but the link
  itself.
- **Stays for the link**: on hover-capable devices the card does not leave while
  the pointer is on its link, so the credit or source can be read or followed at
  leisure. `prefers-reduced-motion` collapses the fades to 0.01 s; the cards
  still come and go.

## Debugging

- `window.quack = { app, world, seasons, effects, pond, rig, camera, renderer, scene }`
  is set once the pond is up; `app.paused`, `app.timeScale` and `seasons.hold(i)`
  are the quick levers. `world.ice`, `world.iceAt(x, z)` and `world.solidAt(x, z)`
  answer what the freeze is doing where.
- `app.step(raw = 1/60)` advances the world by exactly `raw` seconds whatever
  the clock is doing — pause and `timeScale` are ignored — and renders, so a
  probe can hold `app.paused = true` and walk the sim in fixed slices without
  the frame rate colouring the answer. `scripts/gait.mjs` uses it to film and
  measure the waddle; each duck carries its two feet at `d.feet`
  (`[footL, footR]`, index 0 is the +x side) for a probe to watch, and
  `d.stride`, `d.walkPhase`, `d.roll` and `d.vel` say what the gait is doing,
  and `d.sit` how far a sleeping duck has settled onto its belly.
- Holding winter: `seasons.hold(3)` glides there in about a second. To catch the
  freeze part way, hold it with `app.timeScale = 1` and poll `world.ice` on a
  timer, pausing the app as it passes the value you want — 0.4 is a good
  mid-freeze, lid advancing and the lead still wide. `world.waterHeightAt(0, 45, t)`
  is the check that the shore has stopped moving: every `t` must give the same
  number, or the lid at the edge of a frozen pond is still heaving; the same
  check on screen is two renders of the lid two seconds of clock apart with every
  mesh but the water hidden — 0 differing pixels, while the pool in the same
  frame moves.
- `pond.force(name, activity, left)` pins a duck; `pond.throwBread(x, z, ...)`
  lands a piece wherever you like; `pond.setCursor(nx, ny)` / `setCursor(null)`
  drives the gaze (works headless).
- Forcing a flight: `d.setActivity('fly')` then
  `pond.beginFlight(d, { x, z })` (the target needs open water) — the pond flies
  it from where it stands while the rig poses it. `d.flying`, `d.flyH` (height
  over the water), `d.flyPhase` (0–1 through the pass) and `d.flyPitch` say what
  it is doing; `d.flight` is the plan itself. Walking `app.step` through a pass
  at 0.05 s slices gives the glide and the landing for a shot. `d.flyCool` is
  the seconds left before that bird will take the wing again.
- Forcing a lap: `pond.startLap(d, true)` (the flag is the wedge test) returns
  whether it found a round, and `d.lap` holds it (`x`, `z`, `r`, `a`, `dir`,
  `wob`, `left`). The path is measured from the plan, not the frame: sample
  `hypot(d.pos.x - d.lap.x, d.pos.z - d.lap.z) / d.lap.r` each second (it holds
  ~0.9–1.1), and add up the signed heading change around `d.facing` — the net
  turn must have the sign of `d.lap.dir` and about its size (turns × 2π).
- Bread in the air: `pond.sees(d, x, z)` is the sight cone (awake, within
  `SIGHT.range`, inside `SIGHT.half` of the bill); `d.notice` is the seconds
  until that duck sets off, so a probe can read the early reaction before the
  splash. `pond.maybeFlyToFood(d)` makes the fly-or-swim roll for the nearest
  unclaimed piece; `piece.claimed` says who is coming for it.
- Curiosity: `pond.curious` is the seconds until the flock may notice the visitor
  again; `pond.begPoint(d)` is where that duck would come to, and 0 if there is
  no water it can reach in front of the stand. `pond.force(name, 'beg', left)`
  poses a visit for a shot.
- Overlap checks: `pond.contact(a, b, ux, uz)` is the centre distance at which
  two hulls touch — reach it through a unit vector and compare with the real
  gap, so a probe can report the worst clearance in a crowd rather than trusting
  a screenshot. `pond.separate()` can be called by hand to settle a planted
  crowd before a shot.
- Shore: `world.shore` is the walkable section; `world.shorePoint(x, across)`
  returns a point on it (`across` 0 = water's edge, 1 = inland edge),
  `world.shorePointAt(t)` marches it west to east, `world.onShore(x, z)` tests a
  spot and `world.shoreNormal(x)` gives the inland direction.
- Screenshots: `node scripts/shot.mjs <url> <out> [w] [h] [waitMs] ["js"]` —
  the optional JS string runs in the page first (to pose the camera or season).
  Output goes under `temp/`, which is git-ignored.
- The loading card is plain HTML/CSS in `index.html`, up before any module is:
  `document.getAnimations()` lists its five span animations, so a probe can
  pause them and seek a frame (`a.currentTime = ms`). `scripts/loader-shot.mjs
  <url> <out> [w] [h] [dpr] [outWidth] [q]` re-shoots its backdrop from the
  pond's opening pose and writes a progressive JPEG (needs
  `npm i --no-save sharp`; the raw PNG lands in `temp/`). Hide the flock
  (`d.root.visible = false`), the particle pools and the ripples first — the
  card is the pond with no bird in it.
- Reading a shot back as data: `node scripts/sample.mjs <png> [x0..1]` prints the
  colour down that column, sampling every 2.5 % of the height. Colours on the way
  down a column settle questions a glance cannot ("is that band the bank or the
  shallow water?"): compare the shot against the same frame with
  `world.water.visible = false`.
- Forcing a sleep pose: `m.setActivity('rest')`, `m.activityLeft = 600`, wait
  ~0.5 s, then `m.startAction('sleep', 600)`, wait ~2.6 s. `m._sleepSide = 1`
  pins which way the head turns. A duckling needs the pair re-asserted every
  ~100 ms — family glue replaces the action otherwise.
- Freezing an action mid-pose: re-assert `d.startAction('upend', 4.5)` and
  `d.actionTime = d.actionDuration - 1.6` every ~16 ms to hold it in the middle
  of the move, and pin `pos`/`target`/`facing` in the same tick — `force()`
  clears the action, and a drifting duck is a moving frame. `app.timeScale = 6`
  fits a whole upend cycle into the shot window.
- Camera: the eye is planted, so pose it directly — `rig.look(yaw, pitch, fov)`
  aims it at once with no easing, `rig.stand(x, z)` puts the stand down
  somewhere else (it re-reads the ground), then `app.paused = true` freezes the
  scene for the shot.
- TypeScript 7 quirk: `instanceof` does not narrow `unknown` values from
  `import * as THREE`; use explicit casts.

## Open threads

- **Night**: the seasons already carry the sun across the year; a proper
  day/night cycle within a season is the next step.
- **Audio**: synthesis, propagation and spatialization are wired (v2) — the
  Rust `Stage` behind `src/audio.ts`. What is still ours to do is the finer
  matching: a call's gesture and its sound are offset by the worker queue
  (`AUDIO.queue`), the dabbling ripple rhythm is a single Rust patter rather
  than visual ripples, and bread lands as a pea plop scaled up. An attention
  model for the mix (who deserves the foreground) is the open question, not the
  synth.