# Method

How the two films in this repository are made, and what carried over from
the first one to the second. The code is plain JavaScript on Canvas 2D; the
only external tools are a headless browser for rendering frames and ffmpeg
for encoding.

## 1. Every frame is computed

Nothing is drawn by hand and nothing is loaded. Shapes come from geometry,
tone from fields, motion from simulation, sound from synthesis, and quality
is checked by measurement. The unit of work is not a canvas primitive but a
placed mark: a dot of a halftone screen, a hatch line, a brush stroke.

A shape is always an array of points, never `ctx.arc()`. A circle is 120
points, so it can be bent, clipped, stroked, filled, projected into 3D or
fed to a tone rule in exactly the same way as anything else.

## 2. Invariants first, frames second

Before any drawing, FLIP THE COIN fixed a handful of quantities that never
change: the frame centre, the hero's radius, its spin rate, the fall speed,
the near-miss gap, and a cut grid of 12 frames (half a second at 24 fps,
one beat of the score). Eleven unrelated scenes read as one film because the
object in the middle is always the same size, at the same place, spinning
at the same rate.

The invariants also do real work:

- One turn per second with phase π/2 puts the hero exactly edge-on on every
  multiple of 12 frames. Every cut lands there, and the swap of one object
  for another (coin, moon, apple, ball, bottle cap, disc, ring, chip,
  tablet, eye) hides in that one-pixel sliver.
- Film length is derived from the scene table, never typed.
- The opening and closing holds are made by the driver not advancing scene
  time, so the held frames are frozen byte for byte, boil included.
- Every one of these is measurable in finished frames, so it can be checked
  (see section 9).

## 3. Scenes declare fields; rules print them

A scene never draws tone. It declares a field, `dens(x, y) -> 0..1`: how
much ink falls on each point of the plane, derived from lighting, curvature,
depth or material. A separate rule decides how that field becomes marks.

| rule | tone is carried by | reads as | needs from the scene |
|---|---|---|---|
| `dot` | dot radius on a fixed grid (by area, r = .564·cell·√v) | risograph, comics | nothing |
| `hatch` | line width | woodcut, engraving | a scalar coordinate for form-following lines |
| `dither` | ordered or blue-noise threshold | one-bit graphics | an honest field, there is nothing to hide behind |
| `stipple` | dot frequency | scientific plates | nothing |
| `flow` | line length | pen drawing | a direction or gradient |
| `band` | quantised steps | cel animation | real normals |

`TONE.rule = 'hatch'` restyles a whole film without touching a scene
(`test/toneproof.html` prints one sphere through all of them). The
practical consequence: effort goes into fields. No rule rescues an empty
field; a flat fill stays flat in any style.

Findings that shaped the rules:

- **Hatch lines are isolines, not traced paths.** Integrating strokes along
  a direction field fails by construction: nothing keeps neighbouring paths
  apart, so they converge at the terminator into blots, or spiral around a
  singular point. Isolines of a scalar `u = const` at a fixed step can never
  cross or merge, because `u` has one value at every point. So the rule
  asks the scene for a coordinate, not an angle. Straight woodcut hatching
  is the special case `u = x·cos + y·sin`; a sphere uses distance from its
  highlight and gets engraved in rings. Only the scene knows the form; the
  rule should never try to recover geometry from tone.
- **The screen belongs to the sheet.** One pitch per film: two pitches in a
  frame make moiré. The pitch does not scale with objects, which is also a
  depth cue: a screen that zooms with the object reads as a zoomed photo,
  a fixed one reads as a print with depth inside it.
- **Stepped printing needs stepped design** (HYDRO MASSACRE). The band rule
  adds a little noise before quantising so edges are ragged, which means any
  tone within about .07 of a threshold prints as speckle. Tones are designed
  per step (the middle of each level), gradients are pushed away from
  thresholds so a step becomes a clean line, and a print gate measures the
  field that was actually printed (section 9).
- **Separation by ink cannot be a post-process** (FLIP THE COIN, `?v=2`). A
  fixed ink set cannot reproduce colours outside its gamut. Instead the
  scenes draw into a proxy context that routes each fill into a coverage
  layer for its ink; each layer is screened once, at its own angle, with a
  1–3 px misregistration, and the inks add like light on dark paper.

## 4. Marks, not primitives

Measured on real drawings, the thing that reads as a hand is not tremor
(0.03–0.58 px, invisible) but decision noise: where a mark lands, whether it
is laid at all, how long it is. A quarter of human marks miss by more than a
millimetre. Visible waviness is 2–5 cycles per stroke, pressure follows a
minimum-jerk profile (`p(τ) = 10τ³ − 15τ⁴ + 6τ⁵`: thin slow ends, a
confident middle), and strokes close in time share one oscillator.
Thousands of strokes are collected into one `Path2D` and filled once.

## 5. Determinism

- Randomness is `makeR(key)`, a seeded generator per purpose, never
  `Math.random()`. Texture stays on the object from frame to frame, and the
  same frame renders to the same bytes.
- All random draws happen before any `continue`. Skipping one mark
  otherwise shifts the sequence for every mark after it, and the whole field
  is redrawn every frame. It looks like crawling texture and is miserable to
  find, because the code looks right.
- Animation is on twos: one drawing holds two frames. Boil never reseeds;
  structure stays put and only positions and sizes breathe by a fraction of
  a cell. Texture printed into an object does not boil at all.
- A frame is a pure function of its number. HYDRO MASSACRE simulates the
  whole act once at load at 1/960 s and records it; `renderFrame(f)` only
  reads the record. FLIP THE COIN replays its only physics (the tail)
  deterministically.

That last property pays off everywhere: any frame can be rendered alone, a
film can be split across processes with no synchronisation, a changed range
can be re-rendered without touching the rest, and a refactor can be checked
by comparing frames byte for byte, which is how the comment clean-up of this
repository was verified.

## 6. Motion from simulation, choreography from arithmetic

Tweens give "moved from A to B"; simulation gives weight. The chain is
springs plus one root-to-tip pass that clamps each joint at exact link
length (no iterations, no stretch). Fur is individual hair around a hidden
core. Branches grow by space colonisation with pipe-model thickness.

The drops of HYDRO MASSACRE are a centre of mass plus one deformation mode,
the Rayleigh l = 2 mode, with exaggerated surface tension. Deformation is a
nematic vector `Q = q·(cos 2φ, sin 2φ)` (a stretch axis φ is the same as
φ + π), so squash along a support normal and stretch along velocity add as
plain vectors, and the whole deformation is one damped 2D oscillator.

Choreography is solved rather than tuned: **path first, then the world**.
Contacts are fixed in time and space and starts are solved backwards. The
two drops launched from different launchers ended 3.6 m apart in height at
the meeting; since gravity is equal, the difference grows linearly in time,
so one correction to one launch speed closes it, found by re-running the
flight with a secant update (four runs to a centimetre). Sets are then
placed where the drops will be on their reference frames, measured in
frame pixels, so a change to the air model moves the sets with the path.
Near misses are guaranteed by geometry: a bat pivots outside the frame at
exactly bat length + ball radius + gap from the ball, so its tip cannot get
closer at any angle.

Slow motion is a time map: the film runs evenly while world time slows, and
every simulation, including rain and sign jitter, steps by world time.

## 7. Space without models

The camera is one function, `project(x, y, z)`. Everything else keeps working
because the output is again an array of flat points. The number of
vanishing points is not a setting: a family of parallel lines converges only
if it is not parallel to the image plane, so yaw alone gives two points and
yaw with pitch gives three (`test/spaceproof.html` computes and marks them).
Depth comes from marks rather than scale: a fixed screen pitch, contrast
lost to air, fewer marks rather than smaller ones, and parallax.

## 8. Water, optics and focus

"Water" is three different problems with three models (`lib/water.js`):

| problem | model |
|---|---|
| open surface | Gerstner waves: analytic height anywhere, the Jacobian doubles as a foam field |
| response to a disturbance | the wave equation on a grid, nine-point Laplacian (five points turn rings into squares) |
| liquid changing shape | position-based fluids, with constants calibrated on the first step |

A single drop is solved analytically: ray-ellipsoid intersection, Snell,
Fresnel (Schlick, with the cosine taken on the less dense side), total
internal reflection inside air bubbles, Beer–Lambert absorption, and floor
caustics by pushing sun rays forward through the drop. The inverted world
inside the drop, both highlights and the silver ring of a bubble are not
drawn; they come out of the optics.

HYDRO MASSACRE builds its frames as a stage of depth cards
(`lib/stage.js`): each sample is a ray that accumulates ink through cards
with coverage and refracts through drops. Two ideas matter most:

- **Defocus happens in the field, not on pixels.** Blurring a halftone
  gives grey mush. Each card is baked in its own coordinates and blurred by
  its own circle of confusion before printing, so the marks that print it
  stay sharp. Tone is stored premultiplied by coverage, or a dark object in
  defocus grows a light halo no lens makes. A focus pull bakes a card twice
  and blends the bakes per frame.
- **Mirrors are cards that send the ray into another world.** A reflected
  object sits as far behind the glass as the real one is in front,
  `z' = 2·z_glass − z_drop`; the ray to it crosses the glass at
  `K = z_glass / (2·z_glass − z_drop)` of the way, so the image is K times
  smaller and can be placed exactly. The reflected world has its own
  contents (spikes that exist only there), which is how a drop's double can
  be killed in the glass while the real drop flies on. Shards reflect about
  their own normals; which shard shows the drop on which frame is decided
  by solving for the normal that bisects the view and the target direction.

## 9. Checking by numbers, frame by frame

A contact sheet does not show holes in a composition, broken invariants or
an object that popped in between two frames. The checks here measure
finished frames:

- `flip-the-coin/tools/verify.py`: film length, cut grid, edge-on cuts,
  disc and sphere spinning the same way, the sakuga schedule, byte-identical
  holds, and every frame rendering without an exception. Each frame is
  wrapped in its own try, because an exception inside `page.evaluate`
  never reaches the `pageerror` listener and the frame is silently blank.
- `hydro-massacre/tools/act.py`: every scene registers its objects each
  frame; the tool walks every frame of a range and flags anything that
  appears or vanishes mid-frame without entering past an edge, growing from
  zero or fading in; jumps more than three times an object's own typical
  step; spikes whose root is not on their own pane; living things stuck
  inside a nearer opaque object; heroes changing course or drifting apart.
- `hydro-massacre/tools/pop.py`: the same question asked of pixels, for
  everything that is not registered (backgrounds, walls, reflections). Each
  frame is reduced to a grid of mean tones; a compact region that changes
  many times more in one frame than in the frames around it is a pop-in.
  Lightning is told apart by sign (the whole frame brightens), objects
  entering past an edge by their position, and declared cuts are skipped.
- The print gate (`window.GATE` in `hydro.html`) finds flat, compact patches
  of the printed field sitting on a step threshold, which print as speckle.
- `test/measure.py` checks the library numerically: ring isotropy of the
  wave equation, PBF calibration and settling.

Numbers do not replace looking; they catch what looking misses, mostly in
motion.

## 10. Sound, the same way

No samples in FLIP THE COIN. A note is a waveguide string (delay line
SR/f, one-pole loss filter, fractional-delay allpass, DC blocker) followed
by a body of three band-pass biquads; bells are modal synthesis at
inharmonic ratios; the room is eight combs and four allpasses with early
reflections. Instruments at equal gain differed up to 9x in loudness, so
each is measured on a test note and scaled by its RMS. Layer balance is
measured with stems: a layer's share is `sqrt(full² − without²)`, which is
how the harp turned out to take 38% of the mix.

Harmony follows the same rule as the colour chain: neighbouring chords
share exactly two tones, and at every cut two voices hold through it. The
beat is 12 frames, so every pluck lands on an edge-on frame. In HYDRO
MASSACRE the heart beats on the frames where the divider's ECG crosses the
middle of the screen (the same formula), and the rain's density follows
the time map.

## 11. Rendering and delivery

FLIP THE COIN was rendered at four times the size (4320×4320) and reduced
with Lanczos. Beyond a cleaner screen, the encoder stops spending bits on
shimmering dots, so the file got both more accurate and smaller. Measured
against the source PNGs, the master went from 436 MB to 149 MB while SSIM
rose from 0.9843 to 0.9891. `tune=grain` was not worth it: it treats the
screen as noise to preserve, but the screen is deterministic geometry, and
it doubled the bitrate. HYDRO MASSACRE renders natively at 3840×2160
(`?scale=2`), since everything is vector marks under a base transform.

A lesson from uploading: FLIP THE COIN played fine locally but jumped back
to the start on YouTube after 14 seconds, because of rare open-GOP
keyframes. HYDRO MASSACRE's masters use a closed GOP of exactly 48 frames
with no scene-cut keyframes, and the build counts keyframes and fails
otherwise.

## 12. Silent failures worth remembering

Each of these looked correct in code and was only visible in a frame or a
waveform:

1. `Batch.stroke` with `closed: true` outlines but does not fill; spikes
   read as barbed wire.
2. A `difference` blend over an empty canvas gives white, and white strokes
   drawn over it vanish.
3. Random draws after a `continue` (section 5).
4. A flat disc spinning about the vertical axis has no visible direction
   (`x = u·cos θ` only squashes); the eye flips it every half turn. A
   slightly raised camera puts points on ellipses and fixes it.
5. `fbm` summed raw sits around ±0.31, so thresholds like
   `clamp(fbm() * 1.8 - .78, 0, 1)` were always zero and whole texture
   layers silently disappeared. It is normalised to 0..1.
6. A five-point Laplacian makes circular waves square after a few dozen
   steps.
7. PBF constants set as numbers were off by 196x (density) and nine orders
   of magnitude (eps): no pressure at all. They are measured on step one.
8. A state-variable filter blows up on a cutoff sweep, and one NaN poisons
   the rest of the track. RBJ biquads do not.
9. A unipolar excitation accumulates DC in a waveguide loop until the note
   decays to silence.
10. Notes started a few beats before a shutter kept ringing after it
    closed; limits must be in time, not in beats.
11. An exception inside `page.evaluate` does not reach `pageerror`.
