# cutan.impacts

Synthetic impact clips with exact ground truth, for scoring sub-frame timing.

Structured animations of simple objects — a stick or a ball — striking a
surface, or striking “the air” (a stroke that reverses with no contact), on a
known tempo grid. Each clip ships a sidecar that keeps three times apart:

- the **intended** grid time of every event (`t_grid`),
- the **executed** impact time in continuous seconds (`t_impact` — the grid
  plus controllable humanisation, never snapped to a frame),
- and **what the frames show**: every frame’s exposure interval and sample
  instants ([`an.frame_clock.FrameClock`](#cutan.impacts.FrameClock): frame rate, shutter, capture
  jitter), the 2D keypoints at each frame, and which frames bracket each impact.

The clips are ordinary `an` scenes — props moved by tweens — rendered by the
cutout backend, and the ground truth is read back from the same compiled
document the renderer draws.

Quick start:

```default
from cutan.impacts import ImpactClipSpec, write_impact_clip, write_impact_set

write_impact_clip(ImpactClipSpec(kind="air", fps=30, exposure=0.5,
                                 jitter_sd=0.01), "~/clips")
write_impact_set("~/clips/set")      # 24 clips: objects x kinds x fps x shutter
```

Or from the shell: `an impacts clip OUT_DIR` / `an impacts clip-set OUT_DIR`.

```pycon
>>> plan = plan_impact_clip(ImpactClipSpec(kind="air", beats=2, tempo=60, jitter_sd=0))
>>> [(e.t_grid, e.t_impact) for e in plan.events]
[(0.5, 0.5), (1.5, 1.5)]
```

### Functions

| [`ball`](#cutan.impacts.ball)(\*[, radius, x, floor_y, drop, ...])         | A ball moving onto a floor whose top is at `floor_y`.                                                                         |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [`build_stroke`](#cutan.impacts.build_stroke)(events, \*[, kind, rise, fall, ...]) | Chain rise / hold / fall segments through every executed impact.                                                              |
| [`gaussian_humanizer`](#cutan.impacts.gaussian_humanizer)([sd, rho, bias])               | Offsets from an AR(1) Gaussian process: `o[k] = bias + rho*(o[k-1]-bias) + e`.                                                |
| [`impact_object`](#cutan.impacts.impact_object)(name, \*\*kwargs)                   | Build a registered object by name.                                                                                            |
| [`impact_set_specs`](#cutan.impacts.impact_set_specs)(\*[, base, objects, kinds, ...]) | The cartesian product of the given axes over `base`.                                                                          |
| [`perform`](#cutan.impacts.perform)([tempo, beats, subdivision, ...])         | The impacts of `beats` beats of `pattern`, on `tempo`'s grid.                                                                 |
| [`plan_impact_clip`](#cutan.impacts.plan_impact_clip)(spec)                            | Resolve `spec` into events, a stroke, frames and a Scene IR.                                                                  |
| [`stick`](#cutan.impacts.stick)(\*[, length, thickness, pivot, ...])        | A drumstick rotating about `pivot` (its butt — the hand).                                                                     |
| [`tempo_map`](#cutan.impacts.tempo_map)(tempo)                                  | Coerce a bpm, a `[(beat, bpm), ...]` list, or a [`TempoMap`](#cutan.impacts.TempoMap).                    |
| [`write_impact_clip`](#cutan.impacts.write_impact_clip)(spec, out_dir, \*[, ...])       | Write one clip into `out_dir / (clip_dir or spec.clip_id)`; return that dir.                                                  |
| [`write_impact_set`](#cutan.impacts.write_impact_set)(out_dir[, specs, render, ...])   | Write every clip in `specs` (default: [`impact_set_specs()`](#cutan.impacts.impact_set_specs)) plus `index.json`. |

### Classes

| [`CapturedFrame`](#cutan.impacts.CapturedFrame)(index, t_nominal, t_open, ...)      | One output frame: when its exposure opened and closed, and what it saw.         |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`FrameClock`](#cutan.impacts.FrameClock)([fps, exposure, samples, ...])         | A camera's timing, as data.                                                     |
| [`ImpactClipSpec`](#cutan.impacts.ImpactClipSpec)([object, kind, tempo, beats, ...]) | Everything that determines a clip.                                              |
| [`ImpactEvent`](#cutan.impacts.ImpactEvent)(index, beat, t_grid, t_impact, ...)   | One impact: where the grid put it and when it was executed.                     |
| [`ImpactObject`](#cutan.impacts.ImpactObject)(name, art, at, channels, ...[, ...]) | One striking object, its surface, and how the stroke moves it.                  |
| [`ImpactPlan`](#cutan.impacts.ImpactPlan)(spec, events, obj, stroke, ...)        | A clip before it touches disk: events, motion, camera, and the Scene IR.        |
| [`Stroke`](#cutan.impacts.Stroke)(kind, duration, segments, kinematics)      | The whole curve, plus the kinematics of every impact on it.                     |
| [`StrokeSegment`](#cutan.impacts.StrokeSegment)(t0, t1, h0, h1, easing)             | `h` goes from `h0` to `h1` over `[t0, t1]` under `easing`.                      |
| [`TempoMap`](#cutan.impacts.TempoMap)(points)                                  | Tempo as a function of beat: piecewise-linear BPM between `(beat, bpm)` points. |

### Exceptions

| [`TruthMismatch`](#cutan.impacts.TruthMismatch)   | The ground truth and the thing it describes disagree.   |
|------------------------------------------------------------------|---------------------------------------------------------|

### *class* cutan.impacts.CapturedFrame(index, t_nominal, t_open, t_close, samples, t_reported)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One output frame: when its exposure opened and closed, and what it saw.

`t_nominal` is `index / fps` — what a constant-rate container (an mp4)
says the frame’s time is. `samples` are the instants actually rendered
and averaged into it; `t_mid` is the middle of the exposure, the single
best instant to attribute a blurred frame to. `t_reported` is the
timestamp a capture pipeline hands downstream: nominal or actual, per the
clock’s `timestamps` setting.

### *class* cutan.impacts.FrameClock(fps=30.0, exposure=0.0, samples=None, jitter_sd=0.0, phase=0.0, timestamps='nominal', report_noise_sd=0.0, seed=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A camera’s timing, as data. Every field defaults to the ideal camera.

`fps`: nominal frame rate (need not be an integer: 29.97 is a camera).
`exposure`: fraction of the frame period the shutter is open, in
`[0, 1]`; 0 is an instantaneous sample, 0.5 a 180-degree shutter.
`samples`: instants integrated per open exposure (midpoint rule);
`None` means 1 when `exposure == 0` and
`DEFAULT_EXPOSURE_SAMPLES` otherwise.
`jitter_sd`: standard deviation, in seconds, of each frame’s CAPTURE
offset from the nominal grid — the frame really is taken early or late.
Gaussian, clamped at [`max_jitter`](#cutan.impacts.FrameClock.max_jitter) so frames never overlap; a
`jitter_sd` above half that clamp is refused rather than silently
shrunk (at `exposure=1` there is no room for any).
`phase`: seconds added to every capture instant — the camera clock’s
sub-frame offset from scene time, in `(-1/fps, 1/fps)`.
`timestamps`: the base of `CapturedFrame.t_reported` —
`"nominal"` (`index / fps`, what a naive tick loop or an mp4 reports)
or `"actual"` (`t_open`, what a capture API with real timestamps
reports).
`report_noise_sd`: Gaussian noise, in seconds, added to the REPORTED
timestamp only — regular capture, noisy clock (a browser frame callback).
Clamped at `MAX_REPORT_NOISE_FRACTION` of a frame period, with the
same refuse-rather-than-shrink rule as `jitter_sd`, and reported
timestamps must still increase: a real frame clock never runs backwards.
`seed`: the random streams (capture jitter and report noise are
independent draws from it).

Every instant is clipped into `[0, duration]`, because a render cannot
sample a scene outside its own timeline; the clipped values are what
[`frames()`](#cutan.impacts.FrameClock.frames) returns and what gets rendered.

#### frames(duration)

One [`CapturedFrame`](#cutan.impacts.CapturedFrame) per output frame of a `duration` render.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`CapturedFrame`](#cutan.impacts.CapturedFrame), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

#### *property* max_jitter *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The clamp on a frame’s capture offset, in seconds.

```pycon
>>> round(FrameClock(fps=10, exposure=0.5).max_jitter, 9)
0.0245
```

#### sample_times(duration)

Per frame, the scene instants to render and average — the render seam.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

#### *property* samples_per_frame *: [int](https://docs.python.org/3/builtins/functions.html#int)*

1 for an instantaneous shutter.

```pycon
>>> FrameClock().samples_per_frame, FrameClock(exposure=0.5).samples_per_frame
(1, 8)
```

* **Type:**
  The resolved sample count

### *class* cutan.impacts.ImpactClipSpec(object='stick', kind='surface', tempo=100.0, beats=16, subdivision=1, pattern=(1.0,), lead_in=0.5, tail=0.5, jitter_sd=0.008, jitter_rho=0.0, jitter_bias=0.0, rise=0.18, fall=0.18, brake=0.03, rise_sd=0.0, fall_sd=0.0, brake_sd=0.0, arc_radius=None, show_surface=None, fps=30.0, exposure=0.0, exposure_samples=None, timestamp_jitter_sd=0.0, timestamp_noise_sd=0.0, phase=0.0, timestamps='nominal', width=640, height=360, trajectory_hz=1000.0, seed=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything that determines a clip. Defaults: a stick hitting a table at 100 BPM.

Performance: `tempo` (a bpm, or `[(beat, bpm), ...]` for a tempo
change), `beats`, `subdivision`, `pattern` (per-step stroke heights,
`0` = rest), `lead_in`, `tail`, and the humanisation `jitter_sd` /
`jitter_rho` / `jitter_bias` (seconds; see
[`cutan.impacts.performance.gaussian_humanizer()`](cutan.impacts.performance.html.md#cutan.impacts.performance.gaussian_humanizer)). Humanised by default
(`jitter_sd` = `DEFAULT_JITTER_SD`): on a perfect grid every impact
of a round tempo lands exactly on a frame at common rates, which is the one
case a sub-frame estimator cannot be scored on. Set it to 0 for a metronome.

Motion: `object` (`"stick"` or `"ball"`), `kind` (`"surface"` or
`"air"`), the stroke timings `rise` / `fall` / `brake`, and
`show_surface` (`None`: drawn for surface impacts only). Stroke-shape
variability (cutan#27): `rise_sd` / `fall_sd` / `brake_sd` (seconds)
draw each stroke’s own timings around those means from the seed’s stroke
stream, never below `MIN_TIMING_FRACTION` of the mean; `truth.json`
records each event’s actual `rise`, `fall` and `brake`. `arc_radius`
(scene px, the ball only) swings the ball on a circle about a pivot that
far above its contact point, instead of a straight fall: the contact is the
arc’s lowest point and `impact_xy` is where it lands, as before.

Camera ([`an.frame_clock.FrameClock`](#cutan.impacts.FrameClock)): `fps`, `exposure`,
`exposure_samples`, `timestamp_jitter_sd` (when frames are really
taken), `timestamp_noise_sd` (noise on the reported timestamp only),
`phase`, `timestamps`.

`seed` drives two independent streams — the performance’s and the
camera’s — so clips that differ only in camera settings share the exact same
performance.

#### *property* clip_id *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

A readable, deterministic directory name.

```pycon
>>> ImpactClipSpec().clip_id[:-8]
'stick-surface-30fps-e0-'
>>> ImpactClipSpec().clip_id == ImpactClipSpec().clip_id
True
```

#### *classmethod* from_dict(d)

The inverse of `to_dict()`. Refuses fields it does not know —
dropping one would regenerate a different clip under the same spec.

* **Return type:**
  [`ImpactClipSpec`](cutan.impacts.clip.html.md#cutan.impacts.clip.ImpactClipSpec)

### *class* cutan.impacts.ImpactEvent(index, beat, t_grid, t_impact, amplitude)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One impact: where the grid put it and when it was executed.

#### *property* offset *: [float](https://docs.python.org/3/builtins/functions.html#float)*

the humanisation actually applied (post-clamp).

* **Type:**
  `t_impact - t_grid`

### *class* cutan.impacts.ImpactObject(name, art, at, channels, keypoints, impact_keypoint, keypoint_nodes=dict(), surface_art=None, surface_at=None, params=dict())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One striking object, its surface, and how the stroke moves it.

`channels` are the properties the stroke drives — one for a stick or a
ball, two for a forearm-plus-stick limb (each affine in the SAME `h`, so
the motion stays exact). `keypoints` are local points by name; each lives
on the node `keypoint_nodes[name]` names, the entity itself by default.

#### impact_keypoint *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The keypoint that does the striking (a stick’s tip, a ball’s bottom).

#### keypoints *: [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]]*

Local points by name. What a tracker would report.

#### pose(h)

`{(node path, property): value}` at stroke height `h`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`float`](https://docs.python.org/3/builtins/functions.html#float)]

#### surface_at *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

Where the surface’s top edge is centred, in scene coordinates.

### *class* cutan.impacts.ImpactPlan(spec, events, obj, stroke, frames, scene)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A clip before it touches disk: events, motion, camera, and the Scene IR.

### *class* cutan.impacts.Stroke(kind, duration, segments, kinematics)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The whole curve, plus the kinematics of every impact on it.

#### h(t)

Stroke height at scene time `t` (clamped to the clip).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

#### velocity(t)

`dh/dt` at `t`; at a segment boundary, the LATER segment’s value.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### *class* cutan.impacts.StrokeSegment(t0, t1, h0, h1, easing)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

`h` goes from `h0` to `h1` over `[t0, t1]` under `easing`.

### *class* cutan.impacts.TempoMap(points)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Tempo as a function of beat: piecewise-linear BPM between `(beat, bpm)` points.

Constant before the first point and after the last. Linear in BEATS, not in
seconds, which is how a score writes an accelerando (“speed up over these
four bars”); the time of a beat is the exact integral of `60 / bpm`.

```pycon
>>> TempoMap(((0, 120),)).time_of(3)
1.5
>>> m = TempoMap(((0, 60), (4, 120)))
>>> round(m.time_of(4), 6)   # 4 * 60/(120-60) * ln(120/60)
2.772589
>>> m.bpm_at(2), m.bpm_at(10)
(90.0, 120.0)
```

#### time_of(beat)

Seconds from beat 0 to `beat` (`beat >= 0`).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### *exception* cutan.impacts.TruthMismatch

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The ground truth and the thing it describes disagree.

### cutan.impacts.ball(, radius=18.0, x=0.0, floor_y=90.0, drop=170.0, arc_radius=None, color='#111827', surface_color='#9ca3af', surface_size=(160.0, 24.0))

A ball moving onto a floor whose top is at `floor_y`.

A full stroke lifts it `drop` pixels. Keypoints: `center` and
`bottom` (its contact point).

Straight by default. With `arc_radius` (cutan#27, a wide stick arc: a
hard hit) it hangs on a circle of that radius about a pivot straight above
its contact point, and the stroke ROTATES the pivot: the ball swings up to
one side and falls back along the arc, its contact the arc’s lowest point.
The angle that lifts it `drop` pixels is `acos(1 - drop / arc_radius)`;
rotation is the one channel, affine in `h`, so the curve stays exact.

```pycon
>>> b = ball(arc_radius=300.0)
>>> round(b.params["arc_sweep"], 4), b.at == (0.0, b.params["pivot_y"])
(1.1226, True)
```

* **Return type:**
  [`ImpactObject`](cutan.impacts.objects.html.md#cutan.impacts.objects.ImpactObject)

### cutan.impacts.build_stroke(events, , kind='surface', duration, rise=0.18, fall=0.18, brake=0.03, rest_height=1.0, timings=None)

Chain rise / hold / fall segments through every executed impact.

The object starts and ends at `rest_height`; before impact `k` it is
raised to `events[k].amplitude` (a bigger preparation, a harder hit).
Segments tile `[0, duration]` exactly, and every impact is a segment
boundary at precisely `t_impact`. `timings` gives each impact its own
rise, fall and brake (one per event, cutan#27); without it every stroke
takes `rise`, `fall` and `brake`.

* **Return type:**
  [`Stroke`](cutan.impacts.stroke.html.md#cutan.impacts.stroke.Stroke)

### cutan.impacts.gaussian_humanizer(sd=0.0, , rho=0.0, bias=0.0)

Offsets from an AR(1) Gaussian process: `o[k] = bias + rho*(o[k-1]-bias) + e`.

`sd` is the STATIONARY standard deviation of the offsets (seconds) — the
innovation is scaled by `sqrt(1 - rho**2)` so changing `rho` changes how
the timing wanders, not how far. `rho = 0` is independent jitter;
`rho` near 1 is a player who drifts ahead or behind for several beats.
`bias` is a constant lead (negative) or lag (positive).

```pycon
>>> import numpy as np
>>> h = gaussian_humanizer(0.01, rho=0.5)
>>> offs = h([0.0] * 20000, np.random.default_rng(0))
>>> round(float(np.std(offs)), 3)
0.01
>>> gaussian_humanizer()([0.0, 1.0], None)
[0.0, 0.0]
```

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`float`](https://docs.python.org/3/builtins/functions.html#float)], [`object`](https://docs.python.org/3/builtins/functions.html#object)], [`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### cutan.impacts.impact_object(name, \*\*kwargs)

Build a registered object by name.

```pycon
>>> impact_object("ball").name
'ball'
>>> impact_object("hammer")
Traceback (most recent call last):
  ...
KeyError: "no impact object 'hammer'; known: ['ball', 'stick']"
```

* **Return type:**
  [`ImpactObject`](cutan.impacts.objects.html.md#cutan.impacts.objects.ImpactObject)

### cutan.impacts.impact_set_specs(, base=ImpactClipSpec(object='stick', kind='surface', tempo=((0.0, 96.0), (24.0, 132.0)), beats=24, subdivision=1, pattern=(1.0, 0.6, 0.8, 0.6), lead_in=0.5, tail=0.5, jitter_sd=0.012, jitter_rho=0.3, jitter_bias=0.0, rise=0.18, fall=0.18, brake=0.03, rise_sd=0.0, fall_sd=0.0, brake_sd=0.0, arc_radius=None, show_surface=None, fps=30.0, exposure=0.0, exposure_samples=None, timestamp_jitter_sd=0.0, timestamp_noise_sd=0.0, phase=0.0, timestamps='nominal', width=640, height=360, trajectory_hz=1000.0, seed=0), objects=('stick', 'ball'), kinds=('surface', 'air'), fps=(24, 30, 60), exposures=(0.0, 0.5), timestamp_jitter_sds=(0.0,), seeds=(0,))

The cartesian product of the given axes over `base`.

```pycon
>>> len(impact_set_specs())
24
>>> {s.seed for s in impact_set_specs(seeds=(0, 1), fps=(30,))}
{0, 1}
```

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`ImpactClipSpec`](cutan.impacts.clip.html.md#cutan.impacts.clip.ImpactClipSpec)]

### cutan.impacts.perform(tempo=100.0, , beats=16, subdivision=1, pattern=(1.0,), lead_in=0.5, humanizer=None, seed=0)

The impacts of `beats` beats of `pattern`, on `tempo`’s grid.

`subdivision` grid steps per beat; `pattern` is cycled over the steps,
one value per step: `0` is a rest, anything in `(0, 1]` is a hit of that
relative stroke height (an accent is a bigger stroke). `lead_in` shifts
beat 0 to that many seconds into the clip. `humanizer` turns grid times
into offsets (default: none); each offset is then clamped to
`MAX_OFFSET_FRACTION` of the gap to either neighbour, and the clamped
value is what the event records — the ground truth is what was executed.

```pycon
>>> [e.beat for e in perform(120, beats=2, subdivision=2, pattern=(1, 0))]
[0.0, 1.0]
>>> [e.amplitude for e in perform(120, beats=1, subdivision=4, pattern=(1, .5))]
[1.0, 0.5, 1.0, 0.5]
```

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`ImpactEvent`](cutan.impacts.performance.html.md#cutan.impacts.performance.ImpactEvent), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### cutan.impacts.plan_impact_clip(spec)

Resolve `spec` into events, a stroke, frames and a Scene IR. No I/O.

* **Return type:**
  [`ImpactPlan`](cutan.impacts.clip.html.md#cutan.impacts.clip.ImpactPlan)

### cutan.impacts.stick(, length=200.0, thickness=12.0, pivot=(-120.0, -30.0), contact_angle=0.35, swing=0.95, color='#111827', surface_color='#9ca3af', surface_size=(140.0, 24.0))

A drumstick rotating about `pivot` (its butt — the hand).

At contact it points `contact_angle` radians below horizontal; a full
stroke raises it by `swing` radians. Keypoints: `pivot` and `tip`
(the end of its axis). The surface’s top meets the lowest point of its
rounded end.

* **Return type:**
  [`ImpactObject`](cutan.impacts.objects.html.md#cutan.impacts.objects.ImpactObject)

### cutan.impacts.tempo_map(tempo)

Coerce a bpm, a `[(beat, bpm), ...]` list, or a [`TempoMap`](#cutan.impacts.TempoMap).

```pycon
>>> tempo_map(90).points
((0.0, 90.0),)
>>> tempo_map([(0, 90), (16, 120)]).points
((0.0, 90.0), (16.0, 120.0))
```

* **Return type:**
  [`TempoMap`](cutan.impacts.performance.html.md#cutan.impacts.performance.TempoMap)

### cutan.impacts.write_impact_clip(spec, out_dir, , render=True, clip_dir=None)

Write one clip into `out_dir / (clip_dir or spec.clip_id)`; return that dir.

With `render=False` everything but `clip.mp4` is written, and no
browser is needed: the keypoints and the truth come from the compiled
document, not from the pixels.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### cutan.impacts.write_impact_set(out_dir, specs=None, , render=True, progress=None)

Write every clip in `specs` (default: [`impact_set_specs()`](#cutan.impacts.impact_set_specs)) plus `index.json`.

`progress`, if given, is called with `(i, n, clip_dir)` after each clip.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### Modules

| [`cli`](cutan.impacts.cli.html.md#module-cutan.impacts.cli)                 | `an impacts ...` — the impact harness from the shell.                           |
|-----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`clip`](cutan.impacts.clip.html.md#module-cutan.impacts.clip)               | One impact clip, end to end: spec -> scene -> ground truth -> (video) -> files. |
| [`objects`](cutan.impacts.objects.html.md#module-cutan.impacts.objects)         | The things that strike: a stick and a ball, as ordinary `an` props.             |
| [`performance`](cutan.impacts.performance.html.md#module-cutan.impacts.performance) | The performance: a tempo grid, and when each impact was intended and executed.  |
| [`stroke`](cutan.impacts.stroke.html.md#module-cutan.impacts.stroke)           | The stroke: impact events -> one continuous height curve `h(t)`.                |
| [`truth`](cutan.impacts.truth.html.md#module-cutan.impacts.truth)             | Ground truth: what was intended, what was executed, and what each frame shows.  |
