#### <sup>[Ollin](../../index.md) → [Documentation](../index.md) → [Output](index.md) → `Export`</sup>

---

## Export

Save what a sketch draws. The output comes in four families:

- **Raster**, meaning PNG frames and image sequences.
- **Motion**, meaning a video file or an animated GIF, encoded directly.
- **Vector**, meaning SVG for pen plotters and any vector pipeline, and PDF for print.
- **Linear**, meaning an OpenEXR frame for a compositor: the light before the tone map, with the scene's depth beside it.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/41-FinishingASketch/ExportMap-dark.jpg">
  <img src="../../Guide/Images/41-FinishingASketch/ExportMap.jpg" alt="A diagram with a box labeled your sketch in the middle, arrows fanning left to five file outputs (still, sequence, video, GIF, SVG) and right to three live feeds (the window, Syphon, virtual camera)" width="680">
</picture>

Export runs the sketch *headlessly*. It calls `setup()`, advances the clock to the frame you ask for, runs `draw()`, and writes the result. No window opens, so the same call works from a script or a render farm. A sketch that stops its loop is held where it stopped, as a window holds it. After [`noLoop()`](../core/sketch.md#noLoop), the frame it stopped on is the picture from then on, drawn once.

Most exports are reached by a command-line flag on any example's executable. The same work is available as functions on `OllinApp` when you drive the export yourself. A loose sketch file has no target of its own, so you run it through the live host, `OllinLive`, which takes the same flags. `OllinLive` compiles the file and runs the export headlessly instead of opening a window:

```sh
swift run OllinLive MySketches/Loop.swift --export-gif loop.gif --seconds 4 --gif-width 540
swift run OllinLive MySketches/Loop.swift --export poster.png --frame 90
```

### Contents

- [Raster: PNG and sequences](#raster-png-and-sequences) - `--export` (PNG, or HEIC to keep highlights), `--export-sequence`
- [Linear frames: EXR](#linear-frames-exr) - `--export-exr`, `--export-sequence --exr`, the frame before the tone map, with depth
- [Render quality](#render-quality) - `--render-quality`, the live vs. export default
- [Render scale](#render-scale) - `--render-scale`, drawing a frame more finely than it is saved
- [Path-traced render](pathtraced.md) - `--path-traced`, the offline light-tracing mode for 3D scenes (its own page)
- [Sound](#sound) - a sketch's own music, in the file
- [Video](#video) - `--export-video`, `OllinApp.exportVideo`
- [Frame rates](#frame-rates) - `--fps` by number or by name, `FrameRate`, the broadcast fractions kept exact
- [Transparent output](#transparent-output) - `background(.clear)` kept as alpha in the PNG, the sequence, the GIF, and a `proRes4444` / `hevcWithAlpha` clip
- [Spatial video](spatial.md#spatial-video) - `--export-spatial`, a stereo pair per frame for a headset (its own page)
- [Web page](web.md) - `--export-web`, a page that plays what the sketch drew back in a browser, standalone or inline (its own page)
- [Animated GIF](#animated-gif) - `--export-gif`, `--gif-palette`, `OllinApp.exportGIF`, and `GIFWriter` for frames of your own
- [Perfect loops](#perfect-loops) - `--export-loop`, `Sketch.loopDuration`
- [Slow motion](#slow-motion) - `--slow-motion`, `--made-frames`, a file that plays slower than the sketch ran
- [Settled frames](#settled-frames) - `--settle`, each written frame drawn several times with the clock held, for a picture that converges
- [Vector: SVG](#vector-svg) - `--export-svg`, `OllinApp.svg` / `exportSVG`
- [Vector: PDF](#vector-pdf) - `--export-pdf`, `OllinApp.pdf` / `exportPDF`, paper sizes
- [What SVG export records](#what-svg-export-records) - the shape mapping and the limits
- [Hatching: solid fills for a pen plotter](#hatching-solid-fills-for-a-pen-plotter) - `--hatch`, `Hatching`
- [When an export fails](#when-an-export-fails) - `ExportError`, what every `OllinApp.export…` call throws
- [Reproducibility metadata](#reproducibility-metadata) - the regeneration recipe every export carries
- [Captures that know their source](#captures-that-know-their-source) - `--capture-source`, keeping the exact code a file came from
- [Rendering a chosen variation](#rendering-a-chosen-variation) - `--seed`, on every export path
- [Setting a parameter for the run](#setting-a-parameter-for-the-run) - `--param name=value`, a declared `@Param` set from the command line
- [Saying what a sketch declares](#saying-what-a-sketch-declares) - `--list-params`, every `@Param` with its kind, what it accepts, and what it holds
- [Driving the parameters from a file](#driving-the-parameters-from-a-file) - `--automation`, keyframed parameters on every export path
- [Contact sheets](#contact-sheets-proofing-a-variation-space) - `--export-grid` (seeds) and `--export-sweep` (a `@Param`), `OllinApp.contactSheet` / `exportContactSheet`
- [Print separations](printseparations.md) - `--export-separations`, per-ink masters for risograph and screen printing (its own page)
- [Recording](recording.md) - keep a *live* run instead of re-rendering one: real-time capture with sound (its own page)

---

### Raster: PNG and sequences

Write one frame to a PNG:

```sh
swift run --package-path Examples Example-Basic-HelloCircle --export /tmp/frame.png            # frame 0
swift run --package-path Examples Example-Basic-HelloCircle --export /tmp/frame.png --frame 90 # a later frame
```

`--frame` counts at 60 frames a second unless `--fps` says otherwise, so `--frame 30 --fps 10` is the moment a 10 fps sequence puts at its thirty-first frame, three seconds in. Every one-frame export reads the two flags the same way: the still, the EXR, the plates, and the vector files.

Write a deterministic, fixed-timestep PNG sequence (ready for `ffmpeg`):

```sh
swift run --package-path Examples Example-Basic-HelloCircle --export-sequence /tmp/out --seconds 5 --fps 60
```

Both render through Metal off-screen, with MSAA and then a resolve, so the pixels match the live window. The same capability is available in code as `OllinApp.image(of:frame:)`, `OllinApp.export(_:to:frame:)`, and `OllinApp.exportSequence(...)`. The first of these returns a `CGImage`, and all three are called with `try` (see [When an export fails](#when-an-export-fails)). For a *reproducible* sequence, seed the sketch with `seed(…)` in `setup()`. Sources that follow the export clock stay reproducible too. A [`VideoPlayer`](../video/video.md) decodes by the sketch clock, so frame `k` shows the clip at `k / fps`. An [`AudioPlayer`](../helpers/audio.md#audioplayer) feeds its analyzer the same slice of its file each frame. So an audio-reactive sketch exports with its beats in the same places every run.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/41-FinishingASketch/HeadlessCapture-dark.jpg">
  <img src="../../Guide/Images/41-FinishingASketch/HeadlessCapture.jpg" alt="Four dark square tiles in a row, each showing the same cluster of green-to-orange circles in a different arrangement, labeled frame 0, frame 30, frame 60, and frame 90, above the caption OllinApp.image(of: Pulse(), frame:), four renders, no window" width="680">
</picture>

Use the sequence when you want raw material. It keeps every frame as a lossless PNG for an external encoder or an edit. When the goal is only a file to share, the next two sections encode a file directly and skip the stitching step.

The file name picks the format for a single frame. A `.heic` (or `.heif`) path writes HEIC instead of PNG:

```sh
swift run --package-path Examples Example-Rendering-ColorOutput --export /tmp/frame.heic
```

For an ordinary sketch, that is the same picture in a smaller file. For a sketch that declared [`colorOutput`](../drawing/coloroutput.md) `.extended`, HEIC is the only still format that keeps brightness above white. HEIC stores that brightness in a gain map beside the picture, and PNG has nowhere to put it.

### Linear frames: EXR

Every other still export ends at the same place. The frame is tone-mapped into the range a screen can show, then dithered, then quantized to eight bits per channel. A `.wide` or `.extended` frame keeps more: 16 bits in a PNG and 10 in a HEIC, with no dither needed at that depth. That is the right file to look at and the wrong one to work on. `--export-exr` writes the frame one step earlier, in linear light, as an OpenEXR file:

```sh
swift run --package-path Examples Example-Export-LinearFrame --export-exr /tmp/frame.exr
swift run --package-path Examples Example-Export-LinearFrame --export-exr /tmp/frame.exr --frame 90
swift run --package-path Examples Example-Export-LinearFrame --export-sequence /tmp/frames --seconds 2 --exr
```

The sequence form writes `frame-00001.exr`, `frame-00002.exr`, and so on, from the same fixed-timestep drive the PNG sequence uses, so it takes `--frames` / `--seconds`, `--fps`, `--skip`, and `--start` unchanged. In code the same two calls are `OllinApp.exportEXR(_:to:frame:)` and `OllinApp.exportSequence(..., writesEXR: true)`.

**What the file holds.** Five channels at most:

| Channel | Type | What it is |
|---|---|---|
| `R`, `G`, `B` | half | The light the renderer composited, in linear light with Rec. 709 primaries, premultiplied by the coverage in `A`. Free to run above 1, which is where a highlight lives. |
| `A` | half | The frame's own coverage, the same alpha [transparent output](#transparent-output) writes into a PNG. 1 everywhere on an opaque canvas, and never above 1: additive marks sum their alpha along with their light, and the file stops it at 1. |
| `Z` | float | Distance from the eye in the sketch's own world units, for a frame drawn through a 3D camera. A pixel nothing drew to carries the camera's far distance. |

A flat sketch writes no `Z`. Its depth buffer holds a sort key rather than a distance, and a compositor reading that as depth would be wrong about the whole frame.

The export prints what came through, which is worth reading before taking the file anywhere:

```
Ollin: exported frame 0 → /tmp/frame.exr (1080×1080, ABGRZ linear, 14.0 MB, peak 10.41× white, depth 3.358…40.000)
```

**What it is for.** Grading, glow, haze, and depth of field are all done better on the light than on the picture of it. A highlight ten times brighter than white is one number in this file and a flat 255 in the PNG. Pull the exposure down on each: the first has detail, the second a gray disk. The `Z` channel lets a compositor put fog or a defocus in afterward, at a distance chosen later. The alpha lets the frame sit over other footage. None of this needs the sketch re-rendered.

**What it does not hold.** There is no surface-normal channel and no per-object matte. The renderer shades in one forward pass and keeps no geometry buffer to write them from. Producing either means a second pass that does not exist today. Beauty, coverage, and depth are what a frame can say about itself here.

**The file is large.** It is written uncompressed, one scanline per block, which every reader opens and no reader has to guess at. That is about 14 MB for a 1080 square frame with depth, so a hundred-frame sequence runs well over a gigabyte. Keep sequences short, or export the moments you need.

**Two notes.** Under `--render-scale`, the color is averaged down to canvas size as usual. Each canvas pixel's depth is then the nearest of the samples that covered it, which is the front-most surface. `--made-frames` cannot be combined with `--exr`: a made frame is built from two finished frames, and the linear frame is what comes before that.

The recipe that reproduces the frame is stored in the file's standard `comments` attribute, the same text the PNG carries in its own chunks. See [Reproducibility metadata](#reproducibility-metadata).

### Render quality

A few features trade visual quality for frame rate through the shared `RenderQuality` setting. They are soft shadows, depth of field, ambient occlusion, and the render resolution of the raymarched 3D SDF (`drawSDF3D`). The tiers are `.performance`, `.default`, and `.detail`. **Export and the live window pick a different default**, because they work under different constraints:

- The **live window** defaults to `.default`, the tier that protects the frame rate. For example, the raymarcher runs at half resolution there, so a busy field stays smooth.
- **Export and headless** rendering (`--export`, `--export-sequence`, `--export-video`, `--export-gif`, `OllinApp.image(of:)`) default to **`.detail`**, the best quality. There is no frame-rate pressure when writing a file, and exported art should never be downscaled. So an export is full resolution with the richest samples by default.

Override the export default with `--render-quality`:

```sh
swift run --package-path Examples Example-3D-Raymarching-RaymarchedSDF --export field.png                              # .detail (default, full quality)
swift run --package-path Examples Example-3D-Raymarching-RaymarchedSDF --export field.png --render-quality performance # fast/low: quarter-res raymarch, fewer samples
swift run --package-path Examples Example-Effects-Defocus      --export-video dof.mp4 --seconds 6 --render-quality default
```

The flag takes `performance`, `default`, or `detail` (or the aliases `fast`, `balanced`, and `high`) and applies to every raster, video, and GIF export. It is **distinct from `--quality`**, which sets the video *encoding* quality (0…1) for `--export-video`.

**The sketch's own quality setting always takes precedence.** `--render-quality` and the automatic live and export defaults only fill in the features the sketch left alone. If a sketch sets a feature's quality explicitly (`raymarchQuality(.performance)`, `shadowQuality(.detail)`, `.defocus(…, quality: .performance)`), that choice holds on every path, live and export alike. In code the same control is the `quality:` argument on `OllinApp.image(of:frame:fps:quality:)`, `export`, and `exportSequence`. On `exportVideo` and `exportGIF` it is `renderQuality:`. In both places it defaults to `.detail`.

### Render scale

`--render-scale N` draws each exported frame at N times the canvas resolution in each direction. It then averages every block of N by N pixels back into one. The picture keeps its canvas size. What changes is how finely it was sampled:

```sh
swift run --package-path Examples Example-Text-TypeAsGeometry --export poster.png --render-scale 2
swift run --package-path Examples Example-Shapes-Polygons --export poster.png --render-scale 4
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/41-FinishingASketch/SamplingFiner-dark.png">
  <img src="../../Guide/Images/41-FinishingASketch/SamplingFiner.png" alt="Two magnified pixel grids side by side showing the same fan of blue rays meeting at a point, labeled render scale 1 with one drawn pixel each and render scale 4 with sixteen drawn pixels averaged; the second fan has softer, more graded edges and a cleaner center" width="680">
</picture>

It is the quality-over-speed control for a frame that has no frame-rate deadline. The work grows with the square of the number: four times the pixels at 2, sixteen at 4. That is why the live window never uses it, and why 4 is the ceiling. If you ask for more, the export renders at the ceiling and prints a line saying so.

**What it sharpens.** It sharpens filled shapes and polygons, the fill of an elliptical or full-turn arc, the glyph outlines that text is made of, and fine dense detail. Those are the things that reach the screen as triangles. It leaves the analytic shapes (circles, rectangles, circular arcs under a full turn, the marker catalog), every stroked path, and atlas text (`textMode(.atlas)`) as they were. Each of those carries its own coverage, so it is already crisp at 1.

**What it does not change.** Everything that treats the finished frame as a picture runs at canvas size at every scale. That is motion blur, the lens flare, `postProcess` filters, and the tone map. So a `.gaussianBlur(radius: 12)` is twelve canvas pixels wide whatever the scale is. Two other things stay at 1 as well. A layer built with `renderTarget` keeps the size it was made at. A piling canvas (`noClear`) carries one surface across frames, so it renders at 1 and prints a line saying so.

In code the same control is `OllinApp.exportRenderScale`, set before the export call:

```swift
OllinApp.exportRenderScale = 2
try OllinApp.export(sketch, to: "poster.png")
```

### Video

Encode an animated sketch straight to a `.mp4` or `.mov`. One command takes a sketch to a file you can post, with no external tool:

```sh
swift run --package-path Examples Example-Basic-HelloCircle --export-video breathing.mp4 --seconds 6
swift run --package-path Examples Example-Motion-Orbits --export-video orbits.mov --seconds 10 --codec hevc --bitrate 8
```

It runs on the same deterministic fixed-timestep drive as `--export-sequence`, so `--seconds` or `--frames`, `--fps`, and `--skip` work the same way. A render that takes ten minutes still writes a file that plays smoothly at the requested rate. In code it is `OllinApp.exportVideo(_:to:frames:fps:codec:bitsPerSecond:encodeQuality:renderQuality:skipSeconds:slowMotion:)`.

The flags:

| Flag | Effect |
|---|---|
| `--codec h264` \| `hevc` \| `hevcWithAlpha` \| `proRes422` \| `proRes4444` | the encoder (default `h264`); the last two keep an [alpha channel](#transparent-output) |
| `--bitrate MBPS` | average bitrate in Mbit/s, the file-size control |
| `--quality 0..1` | constant-quality rate control instead of a bitrate (Apple silicon only) |

**Picking a codec.** `h264` plays everywhere and is the safe default for posting. `hevc` gives better quality per byte, and it encodes 10-bit, which keeps smooth gradients smoother. So it is a good first switch when a file needs to be smaller, at a small compatibility cost on older players. The two ProRes profiles are mastering codecs. They are visually lossless and an order of magnitude larger. They are meant for an edit timeline or a later re-encode rather than for sharing, and they need a `.mov` path.

**Size and quality.** Without `--bitrate` the encoder picks its own generous rate. With it, the file size is predictable, because a clip's size is roughly `bitrate × seconds`. As a starting point, a 1080×1080 clip at 60 fps looks clean at about 10 to 15 Mbit/s in `h264`. In `hevc` it is 6 to 9. Halve those for slow, flat-color motion, and raise them for full-frame noise or grain. On Apple silicon, `--quality` (0…1) targets a constant quality and lets the rate float instead, which is closer to how `crf` works in `ffmpeg`. The encoders are the hardware ones, which are fast and power-efficient. If you want a specific software encoder or two-pass tuning, `--export-sequence` still gives you lossless frames and prints the `ffmpeg` line.

Exported tracks are tagged Rec. 709, so what players show matches what the canvas rendered.

**HDR.** A sketch that declares [`colorOutput`](../drawing/coloroutput.md) `.extended` (which is the setting `isHighDynamicRange` reads true for) is written as **HDR10** instead, with no extra flag to pass. `ColorOutput.referenceWhiteNits`, 203, is the luminance 1.0 stands for in that file, the standard's own reference white, so an exported clip's paper white lands where every other HDR video's does, and `ColorOutput.peakNits`, 1000, is the brightest highlight it carries and the figure the file declares it was mastered for. That means Rec. 2020 primaries, the PQ transfer, 10-bit HEVC, and the mastering-display and content-light metadata the format expects. If the codec was left at the `h264` default, it is forced to `hevc`, because eight bits cannot carry HDR. A `.wide` sketch's track is tagged P3-D65, which is the same standard range through wider primaries.

**Spatial video.** A 3D sketch can be exported as [spatial video](spatial.md#spatial-video) instead. It uses the same fixed-clock drive, but each frame is rendered from two eyes. The two views are muxed into the stereo format that Apple's platforms play with real depth.

### Frame rates

Every export that takes `--fps` takes a number, a broadcast name, or a fraction: `--fps 24`, `--fps ntsc`, `--fps 30000/1001`. In code the same parameter is a `FrameRate`, and a plain number still stands in for one, so `fps: 60` reads as it always did. The names are for the rates whose number is awkward to type and easy to get slightly wrong. `.film` is 24 and `.pal` is 25. `.ntsc` is 30000/1001, the 29.97 of broadcast. `.ntscFilm` is 24000/1001, which is 23.976, and `.ntscDouble` is 60000/1001, which is 59.94.

A named rate is the exact fraction, not its decimal. The video writers put every frame on it. At `.ntsc` the frames sit at multiples of 1001/30000 of a second rather than of 1/29.97. That is the grid a broadcast timeline expects. `FrameRate(30000, per: 1001)` spells any other fraction. `FrameRate(29.97)` is the decimal as written, 2997/100, which differs from `.ntsc` by one part in a million. `rate.frameDuration` is one frame's length in seconds, and `rate.frames(in: 10)` is the count a ten-second export writes.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../Images/BroadcastGrid-dark.jpg">
  <img src="../Images/BroadcastGrid.jpg" alt="A table of the five named frame rates with the fraction each keeps, its decimal, and the frames in ten seconds; under it one second of a clip at film, PAL, and NTSC rates with a tick per frame and a loupe on the second mark where NTSC frame 30 lands a millisecond past it; and at the foot an hour of frames at 30, at .ntsc, and at FrameRate(29.97)" width="680">
</picture>

The loupe shows NTSC frame 30 landing a millisecond past the one-second mark, because that is where the fraction puts it. A broadcast timeline has the same grid. The foot shows what goes wrong when a writer is not told the fraction. It rounds the frames onto a plain 30, and a clip meant to run at 29.97 frames a second drifts off its timeline. By the end of an hour it is 3.6 seconds, or 108 frames, off. The decimal rate, `FrameRate(29.97)`, is much closer, a tenth of a frame an hour away from `.ntsc`. It is still a different rate, so name the broadcast rate when that is the one you mean.

```swift
try OllinApp.exportVideo(sketch, to: "spot.mp4", frames: FrameRate.ntsc.frames(in: 30), fps: .ntsc)
try OllinApp.exportVideo(sketch, to: "reel.mp4", frames: 240, fps: .film)
```

The rate a live window runs at is the display's, not one of these. `frameRate` on the sketch reads what it measured.

### Transparent output

A canvas whose background is see-through stays see-through in the file. Call `background(.clear)`, or any color with an alpha under 1, and every export keeps the frame's coverage as its alpha. `--export` writes a PNG with an alpha channel and `--export-sequence` a folder of them. `--export-gif` writes a GIF with its one transparent index, a hard edge, since GIF has no partial alpha. `--export-video` writes a clip with an alpha channel through the two codecs that carry one:

```sh
swift run --package-path Examples Example-Export-Cutout --export /tmp/cutout.png
swift run --package-path Examples Example-Export-Cutout --export-video /tmp/cutout.mov --codec proRes4444 --seconds 4
swift run --package-path Examples Example-Export-Cutout --export-video /tmp/cutout.mp4 --codec hevcWithAlpha --seconds 4
```

`proRes4444` keeps the alpha losslessly and is the choice for an edit timeline. `hevcWithAlpha` is HEVC with an alpha channel beside the picture, a fraction of the size, and it plays wherever HEVC plays. A player that ignores the alpha shows the picture over black. The other codecs have no alpha channel. A see-through canvas through `h264`, `hevc`, or `proRes422` is composited over black, which is what the window shows for it too. The export says so as it starts. In code, `VideoCodec.carriesAlpha` tells the two kinds apart.

A still stores each pixel's color as it was drawn, beside its coverage. This is straight alpha, the form a PNG defines, and the dither goes on the stored color. So a faint fill keeps every level it was drawn at. Sixty-four grays drawn at a tenth of full alpha are sixty-four values in the file. Scaled by its coverage first, the same fill would have room for only 26. A clip is premultiplied and says so in its metadata. The tone map sees the straight color, so a half-covered pixel keeps the color the sketch drew at half the coverage rather than turning darker. A frame filter runs before that, on the premultiplied frame, so a blur spreads the coverage along with the color. A live feed keeps the alpha as well, with the color premultiplied, the form a shared texture is read in. A [Syphon](../integration/syphon.md) client and a [recording](recording.md) of a see-through canvas receive the frame with its coverage. A VJ program then layers it as it would any other source. The window itself is opaque and paints the frame over black. Open the PNG to see the transparency. An opaque canvas keeps its opaque tag and its bytes.

An `extended` (HDR) clip carries no alpha channel, and `hevcWithAlpha` on such a sketch falls back to `hevc`.

### Sound

A sketch that plays sound carries that sound into the video. Nothing needs to be switched on. If the sketch holds an instrument and plays it, `--export-video` and `--export-loop` write an audio track beside the picture. A sketch that holds no instrument writes exactly the file it always wrote, with no audio track.

The sound reproduces the way the picture does. The exporters drive the sketch on a fixed clock with nothing playing aloud, and the notes are written down as the frames are drawn. Then the soundtrack is rendered through the same code that would have fed the speakers, with one difference: the mixed soundtrack bends anything above 0.7 so that several instruments at once never clip, and a single instrument that loud meets the same bend. Export twice, and the audio comes back identical sample for sample.

A sound the sketch [placed in the scene](../helpers/synthesis.md#placing-a-sound) is placed in the file as well. The position of each instrument, and the position it was heard from, are written down alongside the notes. So something that moves past you on screen also moves past you in the audio.

GIF has no way to hold sound. The details, and what stays out of the file, are on the [Synthesis](../helpers/synthesis.md#sound-in-an-export) page.

---

### Animated GIF

Write a short, infinitely looping GIF:

```sh
swift run --package-path Examples Example-Basic-HelloCircle --export-gif breathing.gif --seconds 4 --gif-width 540
```

In code it is `OllinApp.exportGIF(_:to:frames:fps:width:skipSeconds:)`. GIF is limited to a palette of 256 colors a frame, and it is heavy per second next to video. So the format suits **short loops at modest sizes**. `--gif-width` downscales the output, and the height follows the canvas aspect. Downscaling is usually the difference between a few hundred kilobytes and many megabytes. For anything long or subtle, `--export-video` is the better tool.

Watch out for **full-frame motion**. A sketch where every pixel moves every frame, such as a drifting field or a full-canvas texture, defeats GIF's frame-to-frame compression. Even at a modest width, such a file can reach tens of megabytes. Lowering `--fps` cuts the file roughly in proportion, and sparse motion over a stable background compresses better.

A GIF is **written as it is drawn**. Each frame goes into the file the moment it is rendered, so what an export asks of memory does not rise with its length: a 600-frame loop at 1080 by 1080 costs the same as a 20-frame one. The system's own image writer keeps every frame until the file is laid down, at about ten bytes per pixel per frame, which is why Ollin writes the format itself.

The format holds 256 colors a frame. By default one table is chosen from the first frame and shared by every frame after it, so a color never shifts between frames. Colors that arrive later join that table as they come, so a sketch that draws itself in over time keeps every color it gains. Only when the format's 256 are spent does a frame that needs more get a table of its own, which the frames after it share. A sketch that fades in from black spends them on its first frames, so `--skip` past the fade. `--gif-palette per-frame` chooses a table for every frame from that frame alone. It is truer to a sketch where every frame's colors are its own, at a table's worth of bytes a frame. An area that changes slowly can then shimmer as the table under it moves. In code it is `palette: .perFrame` on `exportGIF`.

Pixels that did not change since the frame before are left alone and each frame is cropped to what moved, which is what keeps a still background cheap. A picture of few colors is coded in fewer bits per pixel.

The writer is yours to drive too. `GIFWriter` takes any `CGImage`, drawn into its own size on the way in, so an extension or a sketch can write a GIF of frames it made itself:

```swift
let gif = try GIFWriter(path: "out.gif", width: 540, height: 540)
for image in images {
    try gif.append(image, delay: 0.04)
}
try gif.finish()
```

`palette:` takes `.shared` or `.perFrame`, and `loops:` says how many times the file plays, 0 forever and 1 once through.

One timing limit comes from the format itself. GIF stores each frame's delay in whole centiseconds, so the achievable rates are 50, 33.3, 25, 20, … fps. The requested `--fps` (default 25, which is exact) is rounded to the closest achievable rate, and the sketch's clock runs at *that* rate. So motion always plays back at true speed, and the clip keeps its requested duration.

### Perfect loops

A sketch whose motion repeats exactly can declare its period. The export can then render exactly one period. You no longer need to match `--seconds` to the loop length by hand, or trim the seam by eye. Declare the period once:

```swift
override var loopDuration: Double? { 6 }   // this sketch repeats every 6 seconds
```

and export one seamless lap:

```sh
swift run --package-path Examples Example-Motion-PerfectLoop --export-loop loop.gif
swift run --package-path Examples Example-Motion-PerfectLoop --export-loop loop.mp4 --fps 30
```

The frame count is `loopDuration × fps`. The output format follows the file extension. A `.gif` takes the GIF options, which means `--gif-width`. Any other extension encodes video with `--codec`, `--bitrate`, and `--quality`. `--fps` (default 25 for GIF, 60 for video), `--skip`, and `--render-quality` work as they do everywhere else. Under [`motionBlur()`](../3d/3d.md#motion-blur), skip one frame's time, `1 / fps` seconds: the first frame drawn has no previous one to blur against, so a lap that starts there is the only unblurred frame of the loop. For a GIF, the lap is computed against the centisecond-rounded rate the format can actually play (see the timing note above). So the loop stays exact at that rate. If `loopDuration × fps` is not a whole number of frames, the export warns and rounds, so pick a rate that divides the loop.

To make a sketch loop cleanly, drive every moving part from a phase that repeats over the period. That means `loopProgress(over:)` or `pingPong(over:)`, [looping noise](../generators/noise.md) with `noise(loop:)` or `signedNoise(loop:)`, or an angle built as `phase * .tau`. One term of plain `time`, or a `sin(time * k)` whose period does not divide the loop, breaks the seam. The [`Motion/PerfectLoop`](../../examples/motion/perfectloop.md) example is the worked reference. It declares `loopDuration`, and the frame one period after frame zero renders pixel-identical to it.

There is no separate entry point in code. In Swift, derive the frame count yourself and call the encoder: `OllinApp.exportGIF(sketch, to: path, frames: Int(duration * fps), fps: fps)`.

### Slow motion

An export writes the frames a sketch drew, at the rate it drew them, so a video plays at the speed you watched. `--slow-motion` is the one place where those two rates separate:

```sh
swift run --package-path Examples Example-Basic-HelloCircle --export-video slow.mp4 --seconds 4 --slow-motion 4
```

That renders four seconds of the sketch's own time and writes sixteen seconds of video. The file keeps its `--fps`. The run is covered by four times as many frames, so the motion takes four times as long to play. The flag works on `--export-sequence`, `--export-video`, `--export-gif`, and `--export-loop`. For a loop, one lap still closes, but it takes longer to watch.

Two numbers are easy to confuse, and the export prints both:

- `--seconds` counts the **sketch's** own time, as it always has. Four seconds at factor 4 is a sixteen-second file.
- `--frames` counts the frames written to the **file**. Ninety-six frames at 30 fps is 3.2 seconds of video whatever the factor is.

In code the same setting is a `SlowMotion`, whose `factor` is how much longer the file runs than the sketch's own time.

By default every one of those frames is drawn. The clock steps `factor` times finer, `deltaTime` shrinks to match, and the sketch is asked to draw each moment in between. That is exact and it works for any sketch, but it costs the factor's worth of render time.

**Motion measured in seconds slows down. Motion measured in frames does not.** A radius built from `sin(time)`, or a position advanced by `speed * deltaTime`, is a function of the clock. So a finer clock slows it down. A position advanced by a fixed step once per `draw()`, with no `deltaTime` in it, is not a function of the clock. It moves the same amount per frame however fast the clock runs. A finer clock hands that sketch more frames, so it arrives at the same place at the same time. Made frames are the answer for that sketch.

#### Frames the GPU makes

`--made-frames` draws only the frames it would have drawn anyway. It then asks the GPU to build each frame between them out of the pair on either side:

```sh
swift run --package-path Examples Example-3D-Effects-FrameInterpolation --export-video half.mp4 --seconds 4 --slow-motion 2 --made-frames
```

It uses the same platform interpolator that [`frameInterpolation()`](../3d/3d.md) puts on the live window, pointed at a file instead. It exists for two reasons. First, drawing half the pictures costs less than half the render time. Second, it is the only form of slow motion that slows a sketch whose motion is measured in frames.

It writes pictures the sketch never drew, so you have to ask for it by name. It also records that fact twice: on the console while it runs, and in the [reproduction recipe](#reproducibility-metadata) the file carries, as `"madeFrames":true`.

What it needs, and what it costs:

| | Drawn (the default) | Made (`--made-frames`) |
|---|---|---|
| Works on | any sketch | a 3D scene under a perspective camera |
| Factors | any number above 1 | 2, since the platform fills one frame per gap |
| Every frame is | drawn by the sketch | half drawn, half built from the pair around it |
| Cost | the factor's worth of render time | measured at 1.9 s where drawing every frame took 2.4 s |

The saving grows with the cost of drawing a frame. On a busy 3D scene at 1080 square, a made frame took about 11 ms against 20 ms for a drawn one. At nine times the sampling (`--render-scale 3`), the same clip took 3.3 seconds against 5.1.

Know three limits before you use it:

- **The first gap of a clip repeats.** The interpolator needs history, and one pair of frames is not enough to build a frame from. So the start of a made clip holds one picture twice. The live window does the same thing when it starts.
- **Flat marks over the scene can smear.** The interpolator reads the depth buffer. A caption or a HUD drawn in 2D has no depth, so it can be warped where the scene behind it moves. You notice it first on text.
- **A sketch with no 3D camera is refused** before anything is written, and the message says why. Drop the flag, and every frame is drawn instead, which costs more time and is never worse.

#### What else moves with it

The **recipe** each file carries records `"slowMotion"`. Its `"fps"` is the rate the *clock* ran at, not the rate the file plays at. So a still re-renders from the recipe unchanged, and `--export --frame N --fps F` lands on the same moment.

The **sound** a sketch makes is laid on the file's own timeline. So a slow-motion clip has sound across its full length rather than falling silent early, and each note arrives with the picture it belongs to.

A **take** cannot be replayed in slow motion. A take carries one frame of recorded input per drawn frame, so a finer clock has nothing to read between them. `--replay` beside `--slow-motion` is therefore refused rather than played wrong.

In code it is one argument, `slowMotion:`, on `exportSequence`, `exportVideo`, and `exportGIF`:

```swift
try OllinApp.exportVideo(sketch, to: "slow.mp4", frames: 480, fps: 60, slowMotion: .drawn(4))
try OllinApp.exportSequence(sketch, to: "frames", frames: 240, fps: 30, slowMotion: .made(2))
```

### Settled frames

Some pictures are not finished by one draw. A `LineSpray`, or anything drawn into an `Accumulator`, is a running mean. It starts grainy, settles as passes accumulate, and starts over whenever the camera moves. In a live window that is fine, because the camera rests and the picture catches up. In a video of a turning scene, every frame has a new camera, so every frame is the grainy first one. `--settle N` draws each written frame N times with the clock held and writes the last draw:

```sh
swift run --package-path Examples Example-Rendering-LineSpray --export-video turn.mp4 --seconds 10 --settle 40
swift run --package-path Examples Example-Rendering-LineSpray --export still.png --frame 0 --settle 120
```

The first draw of a frame advances the sketch's clock as always. The next N−1 draws repeat the same moment: `time` does not move, `deltaTime` is zero, and only `frameCount` counts on. So a camera or a motion written against the clock holds still while the layers underneath keep stepping. The mean converges, the frame is written, and the clock moves to the next frame. It costs N times a plain export. The example runs five passes a draw, so 40 draws is 200 passes of the ring sphere, which looks clean. At 120 draws it is the 600-pass still that [depth of field from light](../drawing/depthoffield.md) opens with.

It applies to `--export`, `--export-sequence`, `--export-video`, `--export-gif`, and `--export-loop`, and the recipe each file carries records `"settle"`. Three things do not work with it. A **take** cannot be replayed or recorded, because a take carries one frame of input per drawn frame. You cannot ask for **made frames**, because they are built from the motion between two drawn frames, and a held clock has no motion. A **piling canvas** (`noClear`) keeps adding through the held draws, so it brightens N times faster instead of settling. A pile has no mean to converge to.

In code the same control is `OllinApp.exportSettle`, set before the export call:

```swift
OllinApp.exportSettle = 40
try OllinApp.exportVideo(sketch, to: "turn.mp4", frames: 300, fps: 30)
```

### Motion blur from subframes

A camera's shutter stays open for part of each frame, and anything that moves while it is open smears. `--subframes N` makes that picture the slow way. It draws each exported frame at N moments across the open shutter and averages them in linear light:

```sh
swift run --package-path Examples Example-3D-Effects-MotionBlur --export-video streak.mp4 --seconds 4 --subframes 16
swift run --package-path Examples Example-Motion-Orbits --export still.png --frame 60 --subframes 32 --shutter 1
```

It blurs anything a sketch draws: 2D shapes, text, layers, and shaders as well as a 3D scene. It costs N times the render time. [`motionBlur()`](../3d/3d.md#motion-blur) is cheap enough for the live window, but it works on the 3D scene alone and builds the streak from one frame's picture. The subframe average is the streak itself, which is why `motionBlur()` is measured against it.

**The shutter.** `--shutter F` is the share of a frame interval the shutter stays open: 0.5 is the half-open shutter of film, and 1 is open the whole frame. Left out, it is the shutter the sketch gives `motionBlur(shutter:)`, or 0.5 if the sketch gives none. The opening is centered on the frame's own instant, as the streak of `motionBlur()` is, so trading one for the other moves nothing. The moments sit evenly across the opening.

**How many moments.** Each moment is a separate picture. Something that moves more than about a pixel between two moments shows up as separate copies rather than a smooth streak. So take N at least as large as the distance, in pixels, that the fastest thing travels while the shutter is open. A ball crossing 20 pixels a frame under a 0.5 shutter travels 10 pixels, so it wants 10 moments or more.

**Every moment is a draw.** `time` steps through the opening, and `deltaTime` is the time since the draw before, so a simulation stepped by `deltaTime` stays exact. `frameCount` counts every draw, as it does under `--settle` and slow motion. So motion counted in frames, a fixed step once per `draw()`, runs N times as fast. Write it against `time` or `deltaTime` and it blurs instead. The first frame is half blurred: the run begins at time 0, so the moments that would fall before it are drawn at 0.

**What stands down, and what is refused.** The sketch's own `motionBlur()` stands down, since the moments are the blur. A picture the GPU carries from one draw to the next steps once per draw: a piling canvas (`noClear`), a feedback layer, an `Accumulator` or a `LineSpray`, a simulation field, and the history of screen-space reflections. N moments would run it N times as fast, so the export refuses such a sketch before it writes anything, and the message names what stopped it. A take cannot be replayed or recorded, and `--settle` and `--made-frames` do not combine with it.

It applies to `--export`, `--export-exr`, `--export-sequence`, `--export-video`, `--export-gif`, `--export-loop`, `--export-grid`, and `--export-sweep`. Slow motion drawn on a finer clock combines with it, and the shutter is then a share of the finer frame. The recipe each file carries records `"subframes"` and `"shutter"`. A linear-light export (`--export-exr`, or `--exr` on a sequence) writes the averaged color beside the depth of the last moment.

In code the same controls are `OllinApp.exportSubframes` and `OllinApp.exportShutter`, set before the export call:

```swift
OllinApp.exportSubframes = 16
OllinApp.exportShutter = 0.5     // nil takes the sketch's own shutter, else 0.5
try OllinApp.exportVideo(sketch, to: "streak.mp4", frames: 240, fps: 60)
```

### Vector: SVG

Write one frame's geometry as an SVG document instead of pixels:

```sh
swift run --package-path Examples Example-Export-VectorExport --export-svg /tmp/shapes.svg            # frame 0
swift run --package-path Examples Example-Export-VectorExport --export-svg /tmp/shapes.svg --frame 30 # a later frame
```

Unlike raster export, SVG export records the draw calls on the **CPU**. So it never touches the GPU, runs anywhere, and is deterministic. The same export is available as functions:

```swift
let document = try OllinApp.svg(of: MySketch(), frame: 0)   // -> String
try OllinApp.exportSVG(MySketch(), to: "/tmp/shapes.svg")   // writes the file
```

The output is **standard, general-purpose SVG**. Native `<circle>`, `<ellipse>`, `<rect>`, `<polygon>`, and `<path>` elements keep their fills, opacity, and transforms. So the file opens cleanly in a browser, Inkscape, or Illustrator, and works as scalable vector art in its own right. A **pen plotter** is one common destination for the file, and an AxiDraw plots an SVG through its own tooling. Stroke-based sketches map most naturally to a plotter, because a pen has no fill. The single-line [stroke fonts](../drawing/text.md) and stroked geometry are exactly what a plotter draws, though the export is not limited to plotting.

### Vector: PDF

Write the same frame as a single-page PDF, the print-ready form of the vector output:

```sh
swift run --package-path Examples Example-Export-VectorExport --export-pdf /tmp/shapes.pdf            # frame 0
swift run --package-path Examples Example-Export-VectorExport --export-pdf /tmp/shapes.pdf --frame 30 # a later frame
```

In code it is `try OllinApp.pdf(of:frame:)`, which returns `Data`, and `OllinApp.exportPDF(_:to:frame:)`. PDF export replays the **same recorded geometry** as the SVG export, so the two documents of a frame always agree. Everything under [What SVG export records](#what-svg-export-records), including the limits, applies equally, and `--hatch` works the same way. Both flags can be given in one invocation (`--export-svg a.svg --export-pdf b.pdf`). Like the SVG path, it runs on the CPU with no GPU. Gradients, clipping, transforms, and alpha all come through as native PDF constructs.

**Paper sizes.** One canvas pixel maps to one PDF point, at 72 per inch. So the paper presets on [`CanvasSize`](../core/canvas.md) produce true-to-size pages. A sketch on `.a4` (595×842) exports as an actual A4 page, ready to print with no scaling. `.usLetter`, `.usLegal`, `.a3`, `.a4`, and `.a5` are built in. Each is portrait, like the physical sheet, and `.landscape` turns one on its side:

```swift
override var canvasSize: CanvasSize { .usLetter }            // 8.5×11 in, portrait
override var canvasSize: CanvasSize { .a4.landscape }        // 297×210 mm
```

Any other canvas exports at its pixel size in points. Because the geometry is vector, it prints sharp at any scale either way. When the *raster* export should be print resolution too, add `.dpi(_:)` to the preset. `.usLetter.dpi(300)` renders and `--export`s at 2550×3300 pixels, and `--export-pdf` still writes a true 8.5×11 in page, with the geometry scaled back onto it.

### What SVG export records

The exporter captures each draw call at its semantic level, before tessellation. So the output is clean vector primitives rather than a mesh of triangles. The PDF export replays the same recording, so this table and its limits describe both formats. Each SVG element maps to the equivalent PDF path:

| You draw | SVG element |
|---|---|
| `drawCircle` / `drawEllipse` | `<circle>` / `<ellipse>` |
| `drawRect` (with `cornerRadius`) | `<rect>` (with `rx`); a radius per corner (`cornerRadii`) is a `<path>` with an arc at each rounded corner |
| `drawLine`, `drawBezier` | `<line>`, `<path>` |
| `drawPolyline` / `drawPolygon` | `<polyline>` / `<polygon>` |
| `drawShape`, `drawCurve`, curve-builder, **outline text** | `<path>` (with `fill-rule`) |
| `drawTriangle` / `drawNgon` / `drawStar` and the polygonal shapes | `<polygon>` |
| the curved analytic shapes (heart, egg, moon, vesica, …) | `<path>` (traced to a fine outline) |
| `drawArc` (open / chord / pie) | `<polyline>` / `<polygon>` |
| **bitmap text** | a grid of `<rect>` |
| **stroke text** | `<polyline>` per glyph |

The current transform is written as each element's `transform="matrix(…)"`. Fills and strokes become `fill` and `stroke`, with `*-opacity` for alpha, and the background is a full-canvas `<rect>`. Coordinates use the same top-left, y-down system as the canvas, so positions match what you see on screen.

The vector format cannot express a few things exactly. This is how each one is handled:

- **Images** are skipped, because raster pixels have no place in a vector file. The omission is noted as a comment in the output.
- **`strokeAlign(.inside` / `.outside)`** falls back to a centered stroke, because SVG strokes are always centered on the path.
- **`hollow(_:)`** band fills are approximated by a centered stroke of the band width.
- Curves and the analytic SDF-only shapes are written as fine polyline or path **approximations** of their outline, which look identical at print scale. A sharp corner on a traced shape (the chord meeting the circle on a cut disk, a horseshoe's square arm ends) is placed where its two sides meet, not cut across. A traced boundary with more than one loop exports as a single even-odd path, with its holes intact. A moon whose cut disk sits fully inside is one such boundary. The Cool S's interior lines are exported as stroke line work.

### Hatching: solid fills for a pen plotter

A pen plotter draws with a pen, so it has no fill. A solid shape would plot as a bare outline. To fix that, **hatching** turns each fill into line work. The lines are parallel, or cross-hatched, and they are clipped to the shape's outline and spaced by the fill's tone. The plotter then shades the shape with those lines. Add `--hatch` to the SVG export (the PDF export takes the same flags):

```sh
swift run --package-path Examples Example-Export-Hatching --export-svg /tmp/hatched.svg --hatch
swift run --package-path Examples Example-Export-Hatching --export-svg /tmp/hatched.svg --cross-hatch --hatch-angle 30
```

The flags:

| Flag | Effect |
|---|---|
| `--hatch` | turn fills into hatch lines (defaults: 4 pt spacing, 45°) |
| `--cross-hatch` | add a second, perpendicular set of lines |
| `--hatch-spacing N` | line spacing in canvas points for a solid black fill |
| `--hatch-angle DEG` | hatch direction in degrees |

Darker, more opaque fills hatch **densely**, lighter ones hatch **sparsely**, and a near-white fill gets no hatch lines at all. Each shape keeps its outline as a stroke, so the border stays clean. Stroke-only geometry (lines, curves, stroke fonts) passes through unchanged, because it is already what a plotter draws.

The same control is available in code as a value:

```swift
try OllinApp.exportSVG(MySketch(), to: "/tmp/hatched.svg",
                   hatching: Hatching(spacing: 6, angle: .pi / 4, crossHatches: true))
```

`penWidth` is the width the emitted hatch strokes are written at, the plotter's pen. `keepsOutline` strokes each hatched shape's border in its fill color as well, so the region keeps a clean edge rather than only interior lines. `usesToneDensity` scales the spacing by the fill's tone as the exporter hatches it, so a dark fill hatches densely, a light one sparsely, and a near-white one drops out; the export path alone reads it, since `lines(filling:)` always uses `spacing`. Hatching transforms geometry rather than the rendered picture, so a sketch can ask for the lines directly. You can draw them on the canvas or feed them anywhere else:

```swift
for line in Hatching(spacing: 8, penWidth: 0.5).lines(filling: someShape) {
    drawPolyline(line)
}
```

`lines(filling:)` takes a `Shape`, `Rectangle`, or `Circle`. It returns the hatch lines as open polylines in that shape's coordinates. The shape's `winding` rule decides which regions are interior, so holes and concavities are respected. The [Hatching example](../../examples/export/hatching.md) draws these lines live.

### When an export fails

Every export call on `OllinApp` throws an `ExportError` when it cannot finish, so a call in code is written with `try`. The process keeps running, and nothing half-written is left at the path: a video, a GIF, or a spatial video stopped partway is removed. A sequence keeps the frames it wrote before the one that failed. The calls that hand back a picture instead of writing one, `image(of:)`, `contactSheet(of:)`, `plates(of:)`, and `separations(of:)`, throw the same error with an empty `path`.

```swift
do {
    try OllinApp.exportVideo(sketch, to: "loop.mp4", frames: 240)
} catch let error as ExportError {
    print(error)   // loop.mp4: the encoder refused frame 17: …
}
```

The error says three things. `path` is the file the export was writing, or the folder for the calls that write several. `frame` is the written frame it had reached, counted from 0, or `nil` when it stopped before the first frame or after the last. `problem` is what went wrong, in a sentence. Printing the error prints the path and the sentence together.

Its `kind` sorts the failures three ways:

| Kind | What it means |
|---|---|
| `unsupported` | The request cannot be met as asked. A file extension no writer takes, a codec the container cannot carry, zero frames, or a parameter the sketch does not declare. |
| `unrendered` | A frame was not drawn. No Metal device, a renderer that would not start, or a frame that did not come back from the GPU. |
| `unwritable` | The frame was drawn and the file would not take it. The writer would not start, stopped taking frames, refused one, or could not finish. |

The GPU can throw a frame away when another program's work faults it, most often a second export or render sharing the machine. A frame whose picture builds on nothing from the frame before is drawn again, up to twice, with a line on standard error saying so, and the file holds the frame it would have held. A frame that does build on the one before cannot be drawn again, since the discarded work may have left part of it behind: a `noClear()` pile, a persistent layer, a simulation, or a moment of a `--subframes` average. That export stops with an `unrendered` error carrying the GPU's own reason, at the frame that failed or at the next frame written after a warmup frame that failed. Running one export at a time keeps this from happening.

From the command line, an export flag that fails prints the sentence and exits with status 1, so a script running the export sees it fail. A web page that cannot carry the sketch is an `unsupported` failure whose sentence names the call that stopped it. An extension's own export can throw one too, through `ExportError(_:path:frame:problem:)`.

### Reproducibility metadata

Every export carries the recipe to regenerate itself, embedded as one compact JSON line:

```json
{"tool":"Ollin","seed":42,"params":{"radius":120},"git":"8167de3","ollin":"dde11f8a","frame":0,"fps":60}
```

The fields are:

- the sketch's [`variation`](../core/variations.md), the seed both generators started from,
- every `@Param`'s value at export time,
- the short git commit of the directory the export ran in,
- and the frame and fps that produced the file.

When `randomSeed` and `noiseSeed` were set individually, they appear as separate fields instead. `git` is the sketch package's commit and `ollin` the framework's, read from the checkout the sketch depends on (a package SwiftPM fetched sits at its tag's commit); a sketch that depends on the framework by path can change its frame with no change of its own, so the pair is what reproduces a frame. A `-dirty` suffix marks uncommitted changes on either, and a commit field is absent where its repository cannot be read. Every sketch starts from a seed, so even an unseeded run records the number that reproduces it. Each format keeps the recipe in a different place:

| Format | Where |
|---|---|
| PNG (`--export`, `--export-sequence`) | the `Description` text chunk (`Software` reads "Ollin") |
| SVG | a comment right after the opening tag |
| PDF | the document's Subject field |
| Video | a description metadata item (`ffprobe` or any tag inspector shows it) |

GIF is the one format without a writable slot. For a quick look at a PNG, run `exiftool frame.png` or `strings frame.png | grep tool`. A file found months later names its own seed and parameter values, so the same sketch source plus `--seed` and `--frame` regenerates it exactly. Seeding is covered in [Random](../generators/random.md).

---

### Captures that know their source

The recipe names the commit, and marks it `-dirty` when the tree carried uncommitted edits. That marker is accurate, but you cannot recover the edits from it, because they are gone. `--capture-source` saves them instead. Add the flag beside any export flag:

```sh
swift run --package-path Examples Example-Randomness-Variations --export keeper.png --capture-source
```

```
Ollin: source captured as 93ae989, uncommitted work included. Read it with: git show 93ae989
Ollin: exported frame 0 → keeper-93ae989.png (1080×1080)
```

Three things happen. First, the working tree is written into the repository as a commit, tracked edits and new files alike. Then the exported file is named after that commit. Finally, the recipe gains a `capture` field holding the same hash, so the name and the metadata agree.

Nothing of yours moves. Your index, your working tree, `HEAD`, and every branch stay exactly as they were. The capture sits on no branch. It is reachable only through a ref under `refs/ollin/captures/`, and that ref is what keeps `git gc` from collecting it.

To get the code back, you need only the file name:

```sh
git show 93ae989:./Sketch.swift    # read one file as it was, run from its folder
git checkout 93ae989               # stand the whole tree up, detached
git diff HEAD 93ae989              # see what was uncommitted at the time
```

A clean tree needs no capture, because the commit is already `HEAD`. In that case the flag only names the file after `HEAD` and says which commit that is.

Know the rest before you rely on it:

- **Ignored files stay out.** The capture honors `.gitignore`, so build products never travel with it. Exports are not ignored by default, so write them outside the repository or add them to `.gitignore`. Otherwise a capture carries your last render as well.
- **The same tree captures once.** The tree identifies the source. So a second export of unchanged files reuses the first commit instead of writing a near twin.
- **The name suffix travels.** A directory takes it too, so `--export-sequence frames` writes into `frames-93ae989`. For print separations, the file stem takes the suffix before the ink names.
- **Listing and clearing.** `git for-each-ref refs/ollin/captures` lists the captures. To drop them all, run `git for-each-ref --format='%(refname)' refs/ollin/captures | xargs -n1 git update-ref -d`.
- **Outside a repository** the flag prints a note, and the files keep the names you gave.

---

### Rendering a chosen variation

`--seed N` reseeds the sketch before its `setup()` on **every** export path above. A variation you found in the inspector or on a contact sheet comes back exactly:

```sh
swift run --package-path Examples Example-Randomness-Variations --export keeper.png --seed 10
swift run --package-path Examples Example-Randomness-Variations --export-video keeper.mp4 --seconds 6 --seed 10
```

The same seed always renders the same pixels. A sketch that sets its own seed in `setup()` ignores the flag, just as it ignores every other way of setting a seed.

A seed holds still only what comes from the sketch's own generator and clock. A `Date()` read, a system random call, the order a `Set` walks, and live input all reach past it; [Why a run repeats](../concepts/determinism.md#what-breaks-it) lists them with the fix for each.

---

### Setting a parameter for the run

`--param <name>=<value>` sets one declared [`@Param`](../helpers/parameters.md) for this run, on **every** export path above. It works on a standalone window too, where it is a launch value, and the inspector keeps whatever you change it to afterwards. The flag can be repeated, so a run carries as many parameters as it needs:

```sh
swift run --package-path Examples Example-Live-Parameters --export keeper.png --param radius=40 --param rings=2
swift run --package-path Examples Example-Live-Parameters --export dark.png --param paper=#101018 --param style=dots
```

The value is on the sketch before its canvas is sized, so a `canvasSize` that reads a parameter exports at the run's size. A sketch that renders once per aspect for two feeds can pick its canvas that way, with `frame == .photo ? .photo1080 : .vertical1080`. The value is there before `setup()` too, and the [Parameters](../helpers/parameters.md) page has the order.

The [recipe](#reproducibility-metadata) and `--param` work as a pair. Every export writes the parameter values it rendered with into the file, and `--param` puts those values back. A frame re-renders from its own recipe, with the sketch untouched.

The value is read against the parameter's own kind:

| Kind | Written as |
|---|---|
| `Double`, `Int` | `radius=40`, `rings=2` |
| `Bool` | `breathe=true` (`yes`, `no`, `on`, `off`, `1` and `0` read too) |
| a menu choice | `style=dots`, matched loosely, so `easeOut`, `ease-out` and `"Ease Out"` all land on the same one |
| `Color` | `paper=#101018` (`#RGB`, `#RGBA`, `#RRGGBB` and `#RRGGBBAA`), or `paper=0,0.5,1` in numbers from 0 to 1 |
| `Vector2`, `Vector3` | `anchor=100,900`, `pull=1,-2,3` |
| `Rectangle` | `plate=10,20,30,40`, as x, y, width, height |
| `Insets` | `margin=12` for every edge, or `margin=12,8,12,8` as top, right, bottom, left |
| `ClosedRange<Double>` | `band=0.25...0.75`, or `band=0.25,0.75` |
| `String` | `"caption=a longer line"`, quoted for the shell when it holds a space |
| `Palette`, `Ramp` | `inks=#000,#FFF,#F06`, spread evenly, or `#000@0,#F06@0.75` to place a stop |

The value is on the parameter before `setup()` runs, so a count built there or an extension registered behind a switch reads it. It is applied again after `setup()` and before the first frame, so a value the sketch set for itself in `setup()` does not undo it, and the recipe names what the frame was drawn with. Both passes go through the same restore path the live hosts use across a reload. So it clamps to the declared range the way dragging the row does. Beside a `--replay`, it overrides the take's recorded parameters, the way `--seed` overrides the take's seed. So the same gestures land on a different setting. A parameter named twice ends on the last value given. A name the sketch does not have, or a value its kind cannot read, stops the run. The error says what value it expected, so the run never renders something you did not ask for.

`--cue <name>` lands a saved [cue](../helpers/cues.md) the same way, every parameter it holds at once, before and after `setup()` like `--param` and under the `--param` values, so a value given by hand still wins. `--cues <file>` names the sheet when the sketch is not carrying one; under OllinLive the `Sketch.cues.json` beside the sketch is installed first, so `--cue` alone finds it.

---

### Saying what a sketch declares

`--list-params` prints the sketch's parameters and stops. It is the other half of `--param`: one sets a value, the other says what there is to set.

```sh
swift run --package-path Examples Example-Live-Parameters --list-params
```

```
14 parameters, as --param takes them

  Rings
  radius   Double   175                      10...375
  rings    Int      5                        1...12
  style    menu     rings                    Rings, Dots, Beads
  anchor   Vector2  540,540                  x 0...1080, y 0...1080

  Motion
  speed    Double   1                        0...4
  breathe  Bool     true
  spacing  menu     linear                   Linear, Ease In Sine, Ease Out Sine, Ease In Out Sine, Ease In Quad, Ease Out Quad, Ease In Out Quad, Ease In Cubic, Ease Out Cubic, Ease In Out Cubic, Ease In Quart, Ease Out Quart, Ease In Out Quart, Ease In Quint, Ease Out Quint, Ease In Out Quint, Ease In Expo, Ease Out Expo, Ease In Out Expo, Ease In Circ, Ease Out Circ, Ease In Out Circ, Ease In Back, Ease Out Back, Ease In Out Back, Ease In Elastic, Ease Out Elastic, Ease In Out Elastic, Ease In Bounce, Ease Out Bounce, Ease In Out Bounce, Smoothstep

  Look
  paper    Color    #FFFFFF
  inks     colors   #1B1B1B,#E4572E,#2E86AB  2 to 8 colors
  fade     colors   #E4572E,#2E86AB          2 to 6 colors
  weight   Double   2.5                      0.5...12, by 0.5
  caption  String   rings

  Advanced
  jitter   Double   0                        0...30
  orbit    Double   0                        -2...2, by 0.25
```

The columns are the name, the kind of value it holds, the value it holds, and what it accepts. Groups are blocks under their own names, the way the inspector stacks its cards. A parameter a [show-rule](../helpers/parameters.md#show-rules) is currently hiding says so.

The values are the ones the first frame would be drawn with. The sketch's `setup()` runs first, and any `--param`, `--cue`, and `--seed` given beside it land too. So the listing describes that run, not the file it was written in.

Every value is printed in the spelling `--param` reads back, so a line can be pasted into a flag. That is exact for every kind but a color, which is said in hex and so comes back to the nearest eight-bit step.

---

### Driving the parameters from a file

`--automation <file>` attaches [keyframed parameters](../core/automation.md) to the run, on **every** export path above and on a standalone window as well:

```sh
swift run --package-path Examples Example-Motion-Automation --automation slow.json --export-video out.mp4 --seconds 12
```

Every export drives the clock at a fixed step, so the curves land on the same frames every time. The file is applied before `setup()`, so a sketch that writes its own track for the same parameter takes precedence.

---

### Contact sheets: proofing a variation space

`--export-grid` renders one frame at each seed in a run of seeds and tiles them into a single labeled proof sheet. A photographer contact-prints a roll the same way before choosing an enlargement:

```sh
swift run --package-path Examples Example-Randomness-Variations --export-grid sheet.png --seeds 25
swift run --package-path Examples Example-Randomness-Variations --export-grid sheet.png --seeds 12 --columns 4 --tile 400
```

| Flag | Meaning |
|---|---|
| `--export-grid <path.png>` | the sheet to write |
| `--seeds N` | how many variations (default 16) |
| `--seed FIRST` | the first seed (default 1), with seeds running consecutively |
| `--columns C` | grid columns (default: the squarest fit) |
| `--tile PX` | each thumbnail's width, floored at 64 (default 320) |
| `--frame N` | which frame of the sketch to render (default 0) |
| `--fps F` | the clock rate that frame is timed against |

Each tile is a fresh instance of the sketch, seeded before `setup()` runs. So a stateful sketch, one with accumulation, feedback, or a growing simulation, cannot leak from one tile into the next. One renderer serves the whole sheet, so a 25-seed sheet costs about what 25 `--export` calls would, minus the startup. The sheet's PNG carries the seed list in its own recipe.

From code:

```swift
try OllinApp.exportContactSheet({ MySketch() }, to: "sheet.png", seeds: Array(1...25))
let sheet: CGImage = try OllinApp.contactSheet(of: { MySketch() }, seeds: [3, 17, 92], columns: 3)
```

Pick a tile you like, then render it at full size with `--export … --seed N`. The whole loop, including the live inspector half of it, is in [Variations](../core/variations.md).

`--export-sweep` is the same sheet used as a tuning tool. Instead of stepping through the sketch's seeds, it steps through one of its [`@Param`](../helpers/parameters.md) parameters. Name the parameter and a range, or explicit values. Every tile renders at the same seed with only that parameter changing, which is what makes the sheet a fair comparison. A seed is still what a sketch reproduces from. Use a sweep to choose a parameter's value before you commit to it:

```sh
swift run --package-path Examples Example-Live-Parameters --export-sweep sweep.png --sweep-param radius --from 40 --to 360 --steps 9
swift run --package-path Examples Example-Live-Parameters --export-sweep sweep.png --sweep-param rings --values "2,3,5,8" --seed 7
```

| Flag | Meaning |
|---|---|
| `--export-sweep <path.png>` | the sheet to write |
| `--sweep-param <name>` | the `@Param` property to sweep, by its Swift name (`radius`, not `Radius`) |
| `--from A --to B` | the range, spread evenly over `--steps` (default 9) |
| `--values "a,b,c"` | explicit values instead of a range |
| `--seed N` | the seed every tile is pinned to (one is rolled and recorded if omitted) |
| `--columns`, `--tile`, `--frame`, `--fps` | as on `--export-grid` |

The swept parameter is named with `--sweep-param` rather than `--param`, because `--param` sets a value on every tile alike. That is how the rest of the sketch is held still while one parameter moves. Naming the same parameter both ways asks for both at once, so the run stops. Values apply through the same restore path the live hosts use to carry parameters across reloads. So a tile matches what dragging the parameter to that value would show. Only numeric parameters (`Double`, `Int`) can be swept. An unknown name stops the run, and the error lists the parameters the sketch has. The sheet's PNG records the parameter, its values, and the pinned seed.

From code:

```swift
try OllinApp.exportContactSheet({ MySketch() }, to: "sweep.png",
                            sweeping: "radius", values: [40, 120, 360], seed: 7)
// Or name the parameter by its own handle, which the compiler checks:
try OllinApp.exportContactSheet({ MySketch() }, to: "sweep.png",
                            sweeping: \.$radius, values: [40, 120, 360], seed: 7)
```

`\.$radius` is the key path to the parameter itself, the projected value of a `@Param`. So a typo is a build error rather than an empty sheet. An `Int` parameter takes whole values the same way.
