# Changelog

Notable changes to Ollin, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version numbers follow semantic versioning at major zero: a breaking change or a new feature bumps the minor, and a fix bumps the patch. Each release names its breaking renames here, so pin `.upToNextMinor` and read the entry before you move up.

## [Unreleased]

### Added

- **Typed text.** A sketch reads the keyboard the way a text field does, through `typedText()`: an accent typed with a dead key (Option-E, then E) arrives as "é", the emoji viewer's pick arrives as itself, and Chinese or Japanese typed through an input method arrives once a candidate is chosen, with `composingText` holding the reading until then so the sketch can draw it underlined. `textTyped()` fires as text arrives, a held key repeats, Return arrives as `"\n"`, and Delete as a backspace that `caption.append(typing: typedText())` reads as taking a character away. `caretPosition` tells the input method where the line is, so its candidates open under it. `key` and `keyPressed()` still report the keys themselves, unchanged. A take records the text and a replay repeats it. [`Examples/Input/TypedText`](examples/input/typedtext.md) puts what you type on the canvas beside the last key and the last text it typed.
- **A curve and a row of bars as parameters.** `@Param var falloff = ResponseCurve([(0, 1), (0.5, 0.8), (1, 0)])` is a small graph in the inspector whose points drag, read by the sketch as a function from 0 to 1 (`falloff(t)`), for a falloff, a response, or a tone curve; a press on the graph adds a point on the curve, and `count:` bounds how many it holds. `ResponseCurve` runs smooth through its points and never overshoots them (monotone cubic interpolation, after Fritsch and Carlson), so it is a useful value outside the inspector too, and `.easing` hands it to a call that takes an `Easing`. A list of numbers, `@Param(0...1) var accents = [1, 0.2, 0.6, 0.2]` or a list of whole numbers, is a row of bars you paint by dragging across them, for a step pattern. Both are written back into the sketch by Save, held by a cue and a timeline key (moving bar by bar and point by point between two of the same length), set for a run with `--param falloff=1@0,0.7@0.4,0@1` or `--param accents=1,0.2,0.6`, served over OSCQuery one address per bar or point, and edited by touch on the phone's remote surface. [`Examples/Live/Parameters`](examples/live/parameters.md) has one of each.
- **Pinch and turn.** Two fingers on the trackpad reach a sketch as plain values in `draw()`: `pinchScale` is how much the pinch changed the size this frame, as a factor (`1` while nothing pinches), and `turnAngle` is how far the fingers turned, in radians clockwise on the screen (`0` while nothing turns), the way `rotate` turns. So `size *= pinchScale` and `spin += turnAngle` keep a shape under the fingers. `fingersPinched()` and `fingersTurned()` fire once per event for a response in steps. `viewControl()` zooms with a pinch about the pointer and `cameraControl()` dollies with one, a take records and replays both, and the phone showing the sketch (`PhoneDevice.show`) sends its own two fingers to the same two values. [`Examples/Input/PinchAndTurn`](examples/input/pinchandturn.md) sizes and spins a rose and clicks its petal count with the turn; [Input](docs/helpers/input.md#pinch) has the details, and Guide Chapter 38 teaches it.
- **Dóra Maurer joins the recreations.** [`Examples/Recreations/DoraMaurer`](examples/recreations/doramaurer/index.md) is new, after the Hungarian artist (1937 to 2026) whose *Displacements* moved eight colored fields across a grid by a rule she stated herself: a surface of 4:5 divided into 10 by 10 units, two planes each divided into four parts, the cold parts displaced one unit a step diagonally and the warm ones vertically and horizontally. `Displacements` runs that system: eight parts of 5 by 5 units start as contrasting pairs in the corners, a warm part walks the border and a cold one the diagonal, one unit a step, so every part rests in a corner again after five steps and all come home after twenty; the parts are ruled with a diagonal a cell in lanes of their own so coinciding lines bundle, on raw canvas with cells chosen by random numbers painted in full, or on a white sheet. `QuasiImage` is one such detail enlarged until every line is a band of flat color, the window chosen by random numbers around the middle of the grid, in the system's colors, in the broken colors of a relative quasi-image, or cut away from its white and hung on the wall over its shadow; one seed deals one system in both. Both export SVG that gives the rule back: every part a rectangle 5 by 5 cells apart from its lane, where the rule puts it at that step, one unit from where it was a step before, with its nine diagonals at their own two ends.
- **A soft body from any outline.** `World.addSoftBody(from:)` turns a closed `Shape` (a lumpy circle, a star, a letter) into a squishy body in the 2D physics world: a ring of particles round each outline that keeps its area, keeps its corners, springs back toward its own shape, and never lets another soft body's outline in. Four settings decide the feel: `stiffness` (stretch), `bend` (corners), `pressure` (area, held by default), and `shapeMemory` (a disc dropped from its own height squashes by a third at `0`, a tenth at the default `0.3`, and is rigid at `1`), each meaning the same body at any frame rate. A letter's counter stays open, a shape of several pieces is one body, and loose particles with a radius are pushed out when `particlesCollide` is on. The `SoftBody` it returns reads its `shape`, `area`, `center`, and `velocity`, takes `push`, `applyForce`, and `move(by:)`, and inflates when its `restArea` is set. Rigid bodies pass through soft ones and nothing rubs yet. The `Physics/Blobs` example is rewritten over it: nine lumpy blobs sloshing under a gravity that swings from side to side.
- **Eduardo Mac Entyre and Miguel Ángel Vidal join the recreations.** [`Examples/Recreations/MacEntyreVidal`](examples/recreations/macentyrevidal/index.md) is new, after the two painters who named *Arte Generativo* in Buenos Aires in 1959 and wrote in their 1960 manifesto that a generator is the line or figure that by its movement engenders a new geometric body. `Circunferencias` is Mac Entyre's circle: a disc holds families of circles, each one radius and one generator, a ring that sets the centers at one distance from the middle one constant angle apart until they close the turn, so the family passes through the middle and touches the rim as a rosette, or a path that walks the centers along one straight line one constant step apart into a lobe; three looks after the 1965, 1967, and 1970 paintings; over a cycle every ring family turns by one of its own steps and every path family slides one, so the picture comes back to itself. `Focos` is Vidal's straight line: an octagon with a lit corner between each pair of vertices, four faces ruled with chevrons parallel to their outer edges one step apart as far as the middle, so they meet as a pyramid seen from above, and at each corner and at a second light along each diagonal a fan of lines one constant angle apart, each line running until it leaves the two faces that meet there; three looks; the chevrons advance and the fans sweep by one step a cycle. Both export SVG that gives the rule back: every circle at its family's radius, so a ring family's centers read at one distance one angle apart with a count that closes the turn and a path family's on one line one step apart, and every line at its own two ends, so a fan reads as one point and one angle and a face's chevrons as one spacing with the count that fits.
- **A shape bent into a curved patch.** `CoonsPatch` is a four-sided region whose sides are curves, read like a rectangle with `point(u:v:)`: give four curves and each side follows its curve exactly (Coons's patch), or give four corners for the bilinear map. `Contour.warped(from:into:)` and the `Shape` form bend an outline from a rectangle into one, so lettering set in a box lands on a waving banner. The `Shapes/WavingBanner` example waves a striped banner and its word from a pole.
- **Packing rectangles.** `packRectangles(_:in:padding:allowsRotation:)` fits rectangles of given sizes into one bin without overlapping, each at its own size, and returns a `RectanglePacking` with where each one went and which did not fit, for a contact sheet, a collage, or a texture atlas. It is MaxRects with the best short side fit, choosing among every waiting rectangle at each step (Jylänki's tightest variant). The `Patterns/PackedPhotos` example lays the bundled photos out on a page that changes shape.
- **Bending outlines.** `Contour.warped(from:along:)` bends an outline so it follows one curve the way it followed another, so lettering set on a straight baseline bends along a wave; `pulled(toward:amount:radius:)` draws the part of an outline near a point toward it, a smooth bump with an open contour's ends held; `smoothed(neighbors:keepingCornersOver:)` is the moving average with the corners left where they are; `split(atCornersOver:span:)` cuts an outline into pieces at its corners; and `fitted(tolerance:)` traces a dense outline, a mouse stroke of hundreds of points, with a few smooth cubic curves returned as a `Path` (Schneider's method). Each is on `Shape` too. `Path.cubics` reads any path back as cubic Béziers, one per command, so a fit's curves can be counted and their handles drawn. Examples `Shapes/WavyLettering` and `Shapes/TraceToCurves`.
- **A small picture drawn big, cell by cell.** `imageSampling(.nearest)` keeps every pixel a hard square when a picture is drawn larger than it is (pixel art, a low-resolution canvas, a photo dithered to two levels at four canvas pixels a cell), with no enlarged copy made on the CPU; `.linear`, the default, blends as before, a picture drawn smaller is blended under either, and the choice is drawing state beside `tint` that `withState { }` restores. A web page made with `--export-web` reads the picture the same way. New example `Images/PixelArt`.
- **A still named by its moment.** `--at S` beside `--frame N` on every one-frame export (`--export`, `--export-exr`, the vector files, the sheets, the separations, the spatial file): the frame is S seconds of the sketch's clock at `--fps`, so `--at 2` is `--frame 120` at 60 fps and nobody multiplies by the frame rate.
- **A tone map that keeps a bright color's hue.** `toneMap(.neutral)`, the Khronos PBR Neutral curve: `.reinhard` and `.aces` bend each channel on its own, so a bright orange of linear (4, 1, 0.2) came out yellow; the neutral curve compresses the three channels by one factor and holds the hue, with a gentler roll-off than film. The web page's present pass carries it, and the `Rendering/ToneMapping` example cycles four.

### Changed

- **A wide frame's files keep their depth.** A `.wide` or `.extended` PNG is written at 16 bits a channel under the Display P3 curve. It was 8 bits under a linear profile, which left a dark gradient two codes. A HEIC's picture is written at 10 bits where it was 8, and it comes out a smaller file, within half a percent of the frame. A `.wide` video's frames and a recording of a wide window stay 8 bits and now take the same dither a standard frame takes, so a slow gradient that came out as 3 flat bands is 1,006 short runs.
- **A see-through still stores its color as drawn.** A PNG of a `background(.clear)` canvas keeps each pixel's color beside its coverage, the form PNG defines, and the dither goes on that color. The bytes used to hold the color already scaled by its coverage, so a fill at alpha 0.1 kept 26 of the 64 grays drawn, where it keeps all 64 now, and a translucent field stored one code everywhere instead of averaging to the shade asked for. `OllinApp.image(of:)` of such a canvas hands back an image tagged straight alpha (`CGImageAlphaInfo.first`), where it was tagged `premultipliedFirst`. Drawn through Core Graphics the two are the same picture; code that reads the bytes itself now reads the color as drawn. A frame grab for Syphon or a recording stays premultiplied, with its dither moved onto the stored value too.
- **A simulation can read a filtered layer, a frame late.** A `filtered(_:)` or `combined(with:_:)` output in a `SimField`'s `inputs` used to read as zero, since filters resolve after the sims step. It now arrives as it stood at the end of the frame before, kept for the field in a texture of its own, so a kernel can read a measurement of what the sketch drew: a wake pushed by the distance to a moving mass (`.distanceField()`), as it was a frame ago. It reads as zero on the first frame and after a frame in which the layer did not resolve.
- **A `Shape` drawn again unchanged costs a copy, and every plain stroke costs less.** A shape kept in a property and drawn every frame remembers its fill triangles and its stroke from its second draw on and copies them in each frame's colors, so 195 traced outlines filled and stroked every frame went from 124 ms to 6 ms of drawing on an M2 in a release build. An edit to its points or winding, or a stroke at a new width, join, cap, or scale, works it out again; a dashed, brushed, profiled, fading, or gradient stroke is worked out every time. Apart from that, every solid stroke of one width is laid into the frame in one pass rather than one check a vertex, which takes the same outlines drawn as new shapes every frame from 107 ms to 28 ms. Nothing drawn changes: the frames are identical to the byte.
- **Every blown tube speaks from pressure 0.3.** A tube's breath was the pressure times a constant, and where a note starts moves with how much its tube loses, so the clarinet spoke only from about 0.6 at A3, the hollow tube from 0.7, and every tube later on a low note than a high one, which left most of the dial silent. Each note's start is now worked out from its own loss and placed at 0.3, with any reed at any pitch, and full pressure blows what it always did. A sweep written for the old dial swells sooner now.
- **A `Limiter` listens ahead, and its sound leaves 5 ms later.** It used to take the gain a sample needed on that sample, which flattened every crest over the ceiling (1.2% distortion on a loud sine). It now listens `Limiter.lookahead` ahead, so the level is already coming down when a peak arrives, the wave keeps its shape, and nothing crosses the ceiling still. The cost is the lookahead itself: what leaves the limiter is 5 ms behind what went in.
- **`Delay` holds its settings to their ranges.** `time` is held to `0...2` seconds (`Delay.maxTime`, the longest the echo can wait), `feedback` to `0...0.95`, and `mix` to `0...1`, at init, as the other effects' settings are. A time of 3 s used to read back as 3 and echo at 2; it reads back as 2 now.
- **The procedural sky has its sun.** A `.sky` environment lights the scene with its own sun now: a directional light from where the sky draws its disc, as bright and as colored as the real sun's beam at that haze and height (the sky model's own solar radiance function, through the eye's color response into the sky's unit), so a white ball facing it is about eight times as bright as its far side on a clear day, a hazy day's shadows are softer, a low sun reddens, and `castShadows()` throws its shadows. While it lights the scene, the backdrop draws the disc at the sun's true size and the auto-exposure counts the beam beside the sky, so a sunlit white floor sits about where the sky alone put it and the sky reads darker beside it; under clouds the beam keeps the share of the sky the clouds leave clear. `Environment.sunlight(_:)` decides when: `.automatic` (the default) unless the sketch places a directional light of its own, which then stands in as the sun, so a sky scene with its own key light or lighting preset keeps its look; `.always` beside every light; `.off` for the sky's glow alone. A sketch with a `.sky` and no directional light changes picture. The ocean's glitter, a lens flare, and caustics follow the sky's sun like any directional light.
- **The air is the scene's light.** `aerialPerspective()` under a `.sky` takes the sky's horizon as its far light, at the backdrop's own exposure, so far ridges fade into the sky behind them and the veil scales with the sky's intensity; without a sky the first directional light's color and intensity scale it, so twice the light gives twice the glow. The haze's halo still leans the air brighter toward the sun.
- **One `drag`, read the same way in both worlds at every frame rate.** A 2D particle's `drag` was taken once a step, so the default 0.01 removed 45% of its speed in a second at 60 fps and 70% at 120, and a rigid `Body` read the same number as a loss per second, sixty times weaker. `drag` is now the share of speed lost every sixtieth of a second, scaled by the step, and a rigid body reads it the same way on its speed and its spin, so a particle and a body given the same number slow alike and a frame rate changes nothing. Every particle sketch feels as it did at 60 fps; a rigid 2D body at the default slows 45% a second where it slowed 1%, so a rigid sketch that wants its old coasting sets `world.drag = 0` (the `Physics/Burst`, `Chain`, `Joints`, and `RigidBodies` examples do). A `.kinematic` body is never dragged. `World3D` gains a `drag` of its own, the same reading, defaulting to the solver's 5% a second every 3D body already carried, so `world.drag = 0` is what lets a thrown ball land on the exact parabola.
- **A body's own `restitution` wins, and the world's reaches it when changed.** The walls of a 2D `bounds` and the 3D `ground` carried the world's `restitution`, and both engines combine a pair by the larger, so a ball given `restitution: 0` still bounced off a default floor. They now carry none of their own, so a body bounces off them with its own value. A body that named none follows `world.restitution` when it changes, in both worlds, and `drag` reaches every body the same way; `Body.restitution` is readable and settable in 2D as it was in 3D. A change to `gravity` wakes every settled body, so a tilt reaches a box asleep on the floor. The engines' quarter-turn-a-step cap on a spin is lifted: an `angularVelocity` of 100 rad/s reads back as 100.
- **`substeps` in both worlds.** A step can be cut into pieces, each integrated and relaxed on its own, for the scenes where a joint's loss shows: a bob on a rod in the 2D world gave back 70% of its height on its first swing at one piece a frame and gives back 98% at sixteen, and a bar hinged in the 3D world returns to 92% at one piece and 98% at four. The default of 1 changes nothing. A physics snapshot now carries the world's `drag` and `substeps` and which bodies named a bounce of their own (format 7; the previous three formats still read).
- **`CubicBezier` is a value of its own.** `HobbySpline.Segment` is now a typealias of the top-level `CubicBezier` (the same start, two handles, end, `point(at:)`, and `direction(at:)`), which `Path.cubics` and `Contour.fitted(tolerance:)` return too; every existing spelling compiles unchanged.
- **`setup()` may throw.** `open func setup() throws`: a loader that cannot read its file lets its `FileError` out with a bare `try`, where it used to need `try!`. A window reports the sentence on stderr and, under OllinLive and the live-coding stage, in the error strip the way a compile error is reported, and holds the sketch rather than drawing it half built; a headless export fails with the sketch's own error. An override may leave `throws` off, so every existing sketch compiles unchanged. The in-memory recorders that run `setup()` for a frame now throw with it: `OllinApp.svg(of:)`, `pdf(of:)`, `gcode(of:)`, `dxf(of:)`, `drafting(of:)`, `stitching(of:)`, `embroidery(of:)`, and `spatialScene(of:)` take a `try` (a breaking change for a caller of those; the file-writing exports already threw).
- **Timeline edits undo.** ⌘Z while the timeline panel is in front puts the last edit back, from a key placed in the inspector to a track removed from a lane, and ⇧⌘Z brings it back; a drag is one step. Clicking a key moves the playhead onto it, a scrub near a key snaps to it, and the footer names a selected key's value beside its moment and curve.
- **The keyframe diamonds make one column, and come with the panel.** Every inspector row's diamond sits at the trailing edge, after its control, where it used to sit before controls of differing widths and zigzag down the list. The diamonds show while the Timeline panel is open; with it closed, only a parameter a track or a rule already drives keeps its mark, so a sketch that never keyframes has a clean inspector.
- **A `@Param` tunes the 2D view live.** `viewControl(center:zoom:)` applied its arguments on the first call only, so a parameter passed as the zoom changed nothing in the window until `resetView()`. After the first call they are now the view's home, as the camera rig's framing is: a constant framing changes nothing, a changed one glides the view there while the viewer has not dragged or scrolled it, and once they have, a changed value moves only the home, which `resetView()` opens on.

### Fixed

- **A gain map keeps a highlight's edge.** A HEIC rebuilt a highlight's edge, and the three pixels around it, up to 38% off, because the file keeps a map's color at half resolution and a per-channel gain is that color. The map is now written at four times the frame's size, so the edge rebuilds within about 2% of the rendered frame. It drops to twice the size for a frame past about 1.5 million pixels, where a highlight's one-pixel rim can stay 4% off.
- **A black ground separates to black alone.** Once the inks already chosen print solid black, nothing laid over them shows, and the separation broke that tie toward solid ink, so a black background in `separated(into: [.black, .fluorescentPink])` laid solid pink under the black, in either ink order. A tie now goes to less ink.
- **An absolute proof shows the paper, and the paper is the stock's.** An absolute `SoftProof` of white came out cyan (195, 210, 226), and the simulated paper came out far yellower than the press profile's own stock, (238, 220, 185) against (224, 223, 216). The sRGB profile records the screen's daylight white, and the color engine's absolute intent carried that white across as a color. The proof now reads the canvas profile's white as the white the two profiles meet at, which is what the format's current version writes for every display profile, so white proofs as the stock and the stock as its profile says. An `.absolute` proof now shows the paper whether or not `simulatesPaper` is set, as its page always said. Relative and perceptual proofs are unchanged.
- **A video started before an export plays in it.** An export makes the sketch before it takes the clock, so a `VideoPlayer` told to `play()` in a property's initializer held its first frame for the whole export, and a tracker reading it was handed nothing. The play now carries over to the export's clock. `Examples/Vision/OpticalFlow` was caught by it: its published picture showed the film's first frame with no motion field at all, and it is rendered again.
- **`ObjectTracking` follows something with no camera.** With no camera it used to track a still photograph, where nothing moves. It now plays the bundled dance film and takes its first lock from a `FaceTracker` box, asking the detector again only once the tracker's confidence says the patch is lost. The Vision reference also listed a `stop()` the tracker does not have; the call is `stopTracking()`.
- **A white surface sends back the light that reaches it, from every side.** The matte color under a physically based gloss kept one minus the Fresnel term, which does not match what the gloss beside it reflects, so a white plate under an even environment kept 90% of it at a low angle at roughness 0.5 and 103% at roughness 0.2, and under lamps it sent back up to 135% of what fell on it at roughness 0.2. The color now keeps one minus what the gloss reflects toward the eye, under the environment, every lamp and panel, a caustic, the bounce, and the path-traced export, so a white plate returns the light within half a percent at every angle. Floors seen low and glancing read a little darker and glossier; head on, almost nothing moves. A panel's diffuse now takes the same weight, where it took none.
- **A panel's traced shadow is the share of its light the blocker stops.** Its visibility rays counted every point of the panel alike, so the floor under a blocker's edge kept 0.213 of its light where the share that arrives is 0.091. Each ray now counts by the light its point sends the surface, so the near part of a panel weighs more than its far part.
- **A pane of index 1 changes nothing in the path-traced export.** A ball seen through clear glass of the air's own index read 8 to 12% brighter than without it. Two things counted light twice: a continuation that crossed a pane gave up its pairing with the environment's draw, and the light strategies of a surface seen past one priced a widened lobe the continuation did not. A thin pane's crossing now carries the pairing through, the widened lobe is kept for the scene's lights alone, and a shadow ray through a pane loses what the pane reflects, as a path through it does.
- **A picture shows in a traced mirror and through traced glass.** A mesh drawn with a picture (`Mesh.textured`, from a loaded image or a layer's `image`) reached a ray-traced reflection or the traced view through glass as its plain fill, so a picture on a table under a glass drop read as a white slab where the raster view showed the picture. A traced hit now reads the surface's picture, at full size; its other maps (normal, roughness, occlusion, glow) still show only on the surface itself.
- **A plucked string rings for its `decay` at every pitch.** The loop's loss filter takes a share of the note itself, and that share was not counted, so a high note rang a fraction of the time it was given: the steel string's 3.6 s was 0.64 s at MIDI 93, nylon's 1.8 s was 0.42 s. The loop's gain now allows for it, and near the top, where the filter alone would take more than the decay allows, the filter is eased (Jaffe and Smith's decay stretching).
- **A lifted bow lets the string ring.** At pressure 0 the bow sat on the string and muted it within a tenth of a second, whatever `decay` said. The bow now leaves the string over the bottom tenth of `pressure`, where nothing sounds yet, so a string the bow leaves rings for its `decay`, and the loop's loss is taken once a period, which had made that ring half as long.
- **A blown tube holds no offset.** What a tube sent out sat below zero by up to 0.17 at every pressure under full, which ate headroom in a mix. The standing part is taken out of what it radiates.
- **The four built-in rooms are four rooms.** `Reverb(.room)`, `.hall`, `.plate`, and `.cathedral` all sounded as the same medium hall, live and in an export alike: on macOS 27 the call that loaded a room named it and left every setting at the default. Each now loads its own settings, so a hall and a cathedral have the long tails they are named for.
- **A distortion at mix 0 passes the sound unchanged.** The unit still colored what it passed at mix 0 (a bell by 7% of its height); it now steps aside, as a flat equalizer does.
- **An export no longer writes a frame the GPU threw away.** When another program's work faults the GPU, the GPU can discard a frame that was only waiting its turn; the export printed one line and wrote the frame anyway, half drawn (a long render kept a frame of green blocks that way). A frame whose picture builds on nothing from the frame before is now drawn again, up to twice, and the file holds the frame it would have held. One that does build on the frame before (a `noClear()` pile, a persistent layer, a simulation, a moment of a `--subframes` average) stops the export with an `unrendered` error carrying the GPU's own reason, since the discarded work may have left part of it behind.
- **`withMotion` keeps its drawing state inside the block.** It saves and restores the transform and style the way `withState` does, so `withMotion { translate(x, y, 0); drawMesh(box) }` in a loop places each box from the origin. The block left them in place, so each `translate` added to the last one and a 10 by 10 grid drew 2 of its 100 boxes, the rest marched off screen with nothing said; a block that already holds a `withState` inside draws as before.
- **A dense pile of 3D bodies no longer stops the sketch.** A `World3D` set aside one touching pair and one contact constraint a body, and the physics library's debug checks were compiled into every build, so a pile denser than that (a box packed among others touches up to 26 neighbors) tripped an assert and ended the process. The world now holds sixteen touching pairs a body, the checks are off, and a step that still drops contacts says so in `World3D.lastError` instead, cleared by the next step that keeps them all. `maxBodies` reads back what the world was made with.
- **A floor gathers the whole sky.** The environment's diffuse light read the middle of its up face, the direction every floor faces, through the environment's coarsest level (the whole face averaged with the ground), so floors under a sky or a loaded map with any structure came out dim: 3 to 8% under a clear procedural sky and 14% under a hazy one. Each sample of the bake now reads the level its own footprint asks for, and a floor gathers the sky's own level irradiance within 2% on a clear day.
- **Two volumes over one box composite in call order.** Volumes draw farthest first by their boxes' centers, and nothing defined the order when two centers were the same distance from the eye, so an absorbing medium drawn over the same box as a glow could land under it and dim nothing. The tie now keeps call order, the later `drawVolume` on top: a dust layer drawn after a glow dims the glow, and drawn before it dims only what lies behind both. A medium that only glows adds its light in any order, so only an absorbing one needs placing, and the reference says so.
- **The timeline's authoring loop works as written.** A parameter changed while the clock is held now shows at once (the picture follows the inspector, and the track takes the value back on the next scrub unless it is keyed); the diamond on a key re-keys a changed value, where it removed the key and lost the value; an edit made with the panel closed lands where the clock stands rather than where the playhead was when the panel closed; the ruler keeps the playhead in view when the clock runs past the last key and holds still while a key is dragged; the loop region and its button show the moment an Option-drag sets them under a held clock; and the panel takes its first click as a drag rather than as the click that brings it to the front.
- A surface that glows lights its neighbors under `globalIllumination()`: the probe trace reads a hit's emission (`Mesh.glowing`, an emissive color) and adds it to the bounce, so a glowing slab lights the floor under it as the path-traced export does, where before the field read 0 there.
- A metal bounces through its own lobe under `globalIllumination()`, not as the plaster its base color would make: a copper wall throws copper onto what faces it, and a chrome one throws the lamp back along its reflection. The probe trace shades a physically based hit through the same microfacet lobes the renderer and the path tracer use (its body over pi, its lobe beside it), so a physically based wall bounces the light it is lit with and no more, where it bounced about three times its share before; the standard finish is unchanged.
- An export's bounce reaches its depth in a light room: the headless probe field runs a burn-in before it averages, so the deep bounces that are most of a light room's light are no longer weighed down by the shallow first updates. A white room at albedo 0.8 read 0.77 of a 64-deep path trace; its bounce against a darker room's now matches the trace's own ratio within 3%. Exports and snapshots with the bounce on render brighter in light rooms.

## [0.15.0] - 2026-10-12

### Added

- **Segments, arcs, and ellipses as values.** `LineSegment`, `Arc`, and `Ellipse` join `Circle` and `Rectangle` as values a sketch holds: each knows its length or perimeter (the ellipse's exact, through the arithmetic-geometric mean, where the usual formula is a few parts in ten thousand off on a flat one), its area, its bounds, the point on it nearest to any other, and its outline, and each is drawn by the call that always drew it (`drawLine(_:)`, `drawArc(_:mode:)`, `drawEllipse(_:)`, which turns a tilted ellipse for you). All of them, with `Circle`, `Rectangle`, `Triangle`, and `Contour`, share the `PlaneCurve` protocol, so any two can be asked where they cross: `crossings(with:)` returns the meeting points sorted along the shape you asked, from the textbook closed forms where one exists (a straight piece against a circle or an ellipse, two circles, arcs keeping only their sweep) and, for an ellipse against anything round, from the two outlines polished onto the true curves. A mixed list of them can be asked pair by pair. `Circle` gains `area`, `perimeter`, `point(at:)`, `nearestPoint(to:)`, and `distance(to:)`; `Rectangle` gains `area`, `perimeter`, and `edges`; `Triangle` gains `perimeter` and `edges`. New example: [`Examples/Shapes/Crossings`](examples/shapes/crossings.md). [Where curves cross](docs/drawing/geometry.md#plane-curves)
- **A turtle you drive from code.** `Turtle` is a pen that stands on the canvas facing some way: `forward`, `back`, `left`, and `right` (in degrees) walk and turn it, `penUp` and `penDown` break the drawing into runs, `pushState` and `popState` remember where it stood and take it back there without drawing the way, which is how a branch is drawn, and `move(to:)`, `home()`, and a settable `heading` place it outright. Every run comes back as an ordinary open `Contour`, or a closed one after `close()`, so the walk strokes, fills, and plots like any outline. The L-system turtle reads a string; this one takes calls. New example: [`Examples/Patterns/TurtleWalk`](examples/patterns/turtlewalk.md). [Turtle](docs/generators/turtle.md)
- **Anwar Jalal Shemza joins the recreations.** [`Examples/Recreations/AnwarJalalShemza`](examples/recreations/anwarjalalshemza/index.md) is new, after the painter from Lahore who settled in Stafford and built his pictures, he said, from two simple forms, the square and the circle, taken from carpets, Urdu calligraphy, and the architecture of Mughal Lahore. `Meem` is the letter he painted again and again from 1964 to 1968, reduced to a disc for its head and a line for its tail: the canvas is ruled into lanes, a cluster is a few heads laid one touching the next, and every head drops a tail on the cluster's outer side, the tails one gap apart, each leaving its head as a quarter turn around it and falling to the bottom edge, the top head's turn hooking over its shoulder. The picture can be mirrored, and the look is either the oils' thin red lines over blue and green bands or the stained-glass window's wide green stems with white outlines. The letters are written right to left as the pen would write them. `Roots` is his last series, 1977 to 1985: pieces of dyed cloth with frayed edges on a board, a flower on the top piece built by joining a half-disc and three discs, and on the pieces below its roots as a word in Urdu, set from a real face's own outlines in naskh, nastaliq, or Kufi, at a size taller than the cloth so it is cut where the cloth ends. Both export SVG that gives the rule back, and the words come back as the face's glyphs point for point.
- **Lofting.** `Mesh.loft(_:along:at:smoothed:scale:twist:capped:)` and `drawLoft` build one surface through several flat shapes placed along a 3D path, so a bottle runs from a square foot through a round belly to a narrow neck, a horn rounds from a star into a circle as it turns, and a boat hull is lofted as a sheet through open ribs. Each section lands exactly where it was asked for, corners and all, between the path's own points or on them; the outlines are matched round their lengths with each seam turned to the nearest start on the section before, so the skin does not twist on its own; a corner sharp on any section keeps its edge down the loft and fades where the other sections round it off; three or more sections blend as a smooth curve, or straight from rib to rib with `smoothed: false`; and a loft of one shape is that shape's sweep, byte for byte. A sweep's and a loft's caps now meet the walls point for point (the cap's corners are the shape's own points rather than the triangulator's single-precision copies, and a run of points along one straight side no longer fills with slivers of no area), and both say whether they are closed, which the canvas depth of field reads. [Lofting](docs/3d/sweeps.md#a-surface-through-several-shapes-lofting)

- **A shader as paint.** `fill(shader)` and `stroke(shader)` take a `Shader` anywhere they take a color, and its `shade(uv, info)` runs at every pixel the shape covers, on every shape and every path: the analytic shapes, polygons and `Shape`s, strokes, curves, and outline text. `uv` runs across the shape's own bounding box and turns with it under a transform; `info.position` is the pixel in canvas points, `info.distance` the signed distance to the shape's edge (negative inside, zero on a stroke's centerline), and `info.along` the fraction along a stroke. Coverage is applied after the shader returns, so the edge stays as sharp as a flat fill's, and a shader that returns one color draws what that color draws, byte for byte. A mesh under `fill(shader)` takes the shader as its surface texture, read at its uvs, so it reaches reflections, the tracer, and an export. Shapes painted by one shader draw as one run. A vector export writes a shader-painted shape in middle gray; a web page export stops and names the call. `Paint.shader(_:)` carries one as a value. New example: [`Examples/Shaders/ShaderPaint`](examples/shaders/shaderpaint.md). [A shader as paint](docs/drawing/color.md#shader-paint)

- **Hidden-line solids.** `featureEdges(creaseAngle:)` draws a mesh the way a draughtsman draws a solid: the faces in the fill as always, and over them only the lines that describe the form, in the stroke at `strokeWeight`. Those are the creases where two faces meet past the angle, the boundary where a surface ends, and the silhouette, worked out for every copy from where it is seen. A box gives its twelve edges and no diagonals, a sphere its outline, and near solids hide the edges of far ones. The weight is in canvas points, a hairline on every copy at any distance, or in world units under `strokeWeight(_:in: .world)`. Under `drawMesh(_:instances:)` every copy's edges are one more GPU draw placed by the same matrices, so ten thousand boxes cost what their faces cost plus a few milliseconds of GPU time and no CPU time; a mesh's edges are found once and kept while it is drawn. `noFeatureEdges()` turns it off. New example: [`Examples/3D/Geometry/HiddenLines`](examples/3d/geometry/hiddenlines.md). [Hidden-line solids](docs/3d/hiddenlines.md)

- **Noise in three dimensions that loops.** `noise(x, y, z, loop:)`, with `signedNoise`, `fbm`, `signedFbm`, `ridgedFbm`, and `turbulence` in the same form, reads a volume that changes over a lap and comes back to exactly where it started, so a point cloud's drift or a cloud's density closes a seamless export. It reads a five-axis field, three for the point and two for the circle the lap tours, scaled to spread like `noise(x, y, z)` so a sketch keeps its contrast when it moves to the looping form. [Noise](docs/generators/noise.md#loop)

- **Live reload for a sketch that is a package.** The live window compiles a sketch inside a SwiftPM target with every other `.swift` file of that target, so helpers can live in files of their own and a save to any of them reloads the window, where before a helper moved into a second file stopped the live route with `cannot find ... in scope`. `ollin MySketch` runs a package's sketch by its folder, a bare `ollin` runs the package you are in, and `--target <name>` picks one sketch of several. The compile reads `Package.swift` for the target's language mode, defines, features, and resources, so a picture in a processed `Images/` folder loads through `.module` in the live window as it does under `swift run`. A sketch that imports another target of its package, or another package, still needs `swift run`, and the error says so. `OllinRun` (the `--installation` host) and `OllinLiveCoding` take a package's folder too. [Single-file sketches](docs/tools/singlefile.md#growing-into-a-package)

- **Hiroshi Kawano joins the recreations.** [`Examples/Recreations/HiroshiKawano`](examples/recreations/hiroshikawano/index.md) is new, after the philosopher who taught himself to program in 1963 to test a theory of aesthetics, and published some of the first computer-made pictures anywhere in 1964. `Design` walks a grid of black, red, blue, yellow, and white with a Markov chain of order 2, each cell chosen from what followed the last two in counts measured cell by cell off his *Design 3-1* and *Design 3-2*; the printer types a code for every cell and the gouache goes on in the order the chain decided. Read in rows, the exported squares give back the counts they were walked with. `ArtificialMondrian` is the series of 1969: a chain sets where the lines run, a second decides whether each line runs on through a crossing, and the regions the black closes are colored at random, in three looks read from *KD 55*, *KD 27*, and *KD 52*. Every band, notch, and region exports as rectangles on the raster.

- **Smoothing that keeps the points.** `smoothed(neighbors:)` on a `Contour` or a `Shape` is a moving average: each point moves to the average of itself and that many neighbors on either side, and the outline comes back with one point for each point it had, in the same order. So a traced or resampled ring loses its facets and its jitter and stays in step with anything keyed to its points, a color per point or a partner on another ring, where corner cutting doubles the count on every pass. A bell weighs the window by default, so near neighbors count most and a curve shrinks less; `weights: .box` is the plain average. A closed contour wraps around its seam, and an open one keeps its two ends exactly. [Smoothing that keeps the points](docs/drawing/curves.md#moving-average)

- **Motion blur drawn from subframes.** `--subframes N` draws each exported frame at N moments while the shutter is open and averages them in linear light, the accumulation buffer. It blurs anything a sketch draws, 2D and shaders included, at N times the render cost, and it is the reference the canvas motion blur is now measured against. `--shutter F` sets the opening, and left out it is the sketch's own `motionBlur(shutter:)`, else 0.5. Every moment is a draw: `time` steps through the opening, `deltaTime` is the time since the last draw, and `frameCount` counts each moment, so motion counted in frames runs N times as fast. A picture the GPU carries from one draw to the next (a `noClear` pile, a feedback layer, an accumulator, a simulation field) is refused by name. In code it is `OllinApp.exportSubframes` and `OllinApp.exportShutter`. See [Export](docs/output/export.md#motion-blur-from-subframes).

- **Volumes.** A `Volume` is a box of density samples, built from a function of position, from a list, or from noise filled across every core (`Volume.noise`, with a looping form for a cloud that comes home each lap). `drawVolume(_:size:medium:)` draws it in a 3D scene as smoke, cloud, fire, or ink, and the `Medium` says what it is made of: `density` (how thickly it blocks light, per world unit, like `fog`), `color` (the share of light it scatters), `glow` and `glowIntensity` (its own light), and `anisotropy`, with presets `.smoke`, `.cloud`, `.fire`, `.ink`, and `.nebula`. Each pixel's ray is marched through the box after the frame's solids are drawn and ends at the first one, so a solid inside a volume is veiled by the medium in front of it whatever order the calls came in. The medium scatters the scene's directional, point, and spot lights and its ambient and environment light, shades itself, and takes the shadows solids throw into it. Measured against the closed forms: transmittance by Beer's law, the glow's integral, and single scattering, each within half-float precision. A web page export names `drawVolume` and stops. The `3D/Effects/Volumes` example sends a column of smoke up past a hanging ball. See [Volumes](docs/3d/volumes.md).

- **Layer styles.** Six filters give a layer an edge of its own, read from its alpha: `.outline(width:color:align:)` outside, inside, or centered on the edge; `.dropShadow(offset:radius:color:)` and `.innerShadow(offset:radius:color:)`, the layer's alpha blurred and moved, laid under it or held inside it; `.outerGlow(radius:color:)` and `.innerGlow(radius:color:)`, which follow the measured distance from the edge rather than a blur, so a thin line keeps its glow; and `.bevel(width:depth:profile:angle:elevation:highlight:shadow:)`, the band in from the edge raised into a rounded or chiseled slope and lit. They chain like any filter, and the chain's order is the order they stack in. A clear color, or a width or radius of 0, hands back the layer's own bytes. Measured against the shape they were drawn from, an outline is within 0.06 pixel of its width on either side, and a drop shadow is exactly the layer's Gaussian moved. All six cross to a web page, and so does `.distanceField`, which used to stop the export. The `Effects/LayerStyles` example stacks them on a word under a moving light. See [Layer styles](docs/drawing/layerstyles.md).

- **Zdeněk Sýkora joins the recreations.** [`Examples/Recreations/ZdenekSykora`](examples/recreations/zdeneksykora/index.md) is new, after the Czech painter who, with the mathematician Jaroslav Blažek, began placing the elements of his paintings by computer in 1964, and from 1973 painted lines whose every number came from chance. `Structure` is the rule the two of them published in *Leonardo* in 1970: twenty elements of half-discs in four groups by how much of them is black, a chart with a few elements placed and plus and minus signs, and a walk left to right and back that takes each cell's group from the cells around it and keeps the elements whose color or shape continues, as one of four rules asks. The machine types its codes on the left sheet, and the canvas is painted one template at a time. Replayed against the chart and table printed with the article, this reading gives back the printed group for 182 of the 196 computed cells, and all but 8 of the printed elements pass the rule. `Lines` writes a score: a start in whole centimeters, a width, one to five of thirty paints mixed one to one the way pigments mix, and three to eight tangents, each joined to the next by an arc whose radius follows from their lengths and the angle between them, along the grid's eight directions, the clock's twelve, or every even degree. Every crossing goes over or under as a series of ones and twos decides. Both export SVG that gives the rule back: every element of the canvas and every sign of the chart, and every line's start, tangents, and width, with each settled crossing drawn again inside a clip of the place where the two lines overlap.

- **Measuring color.** `distance(to:)` says how different two colors look, as a straight line in OKLab (the scale the palette check's tolerance already uses), or by the CIE's 2000 color-difference formula with `measure: .ciede2000`, which matches all 34 of the test pairs Sharma, Wu and Dalal published for checking implementations. `CIELab` brings in the Lab numbers swatch books and color pickers give, against the D50 white they are measured under. `Palette.nearest(to:)` returns the swatch that looks most like a color and `nearestIndex(to:)` its position. `Image.histogram(of:bins:in:)` counts a picture's pixels by lightness or by one channel, and `percentile(_:)` reads its working black and white. The [`Color/Shades`](examples/color/shades.md) example uses all three. [Measuring color](docs/drawing/color.md#measuring)

- **Shades whose hue drifts.** `Palette.shades(of:count:)` and `Ramp.shades(of:)` build a ramp of one color the way pixel artists and painters shade: the lights turn toward yellow and the shadows away from it, toward blue or purple, with the chroma eased at both ends. The base color is always one of the swatches, and `hueDrift` sets how far the hue turns, a negative drift cooling the lights instead. [Shades](docs/drawing/color.md#shades)

- **The slope of a fitted field.** `RadialBasis` reports how its field changes: `gradient(at:)` for a field of numbers, the way uphill with a length that says how steep, and `derivative(at:along:)` for any field, a warp's stretch included. Both are exact and cost about one read of the value. For a function you wrote yourself, `derivative(at:function:)` and `gradient(at:field:)` measure the slope by central differences. The [`Motion/ContourWalkers`](examples/motion/contourwalkers.md) example walks the contours of both kinds. [Slopes](docs/drawing/fitting.md#slopes)

### Changed

- **A caption draws over the finished frame.** `drawCaption` now lands in the overlay, so it stays sharp and white over a depth of field, a motion blur, a bloom, or a lens flare, and never joins an accumulating canvas; inside a layer or a batch recording, where there is no overlay, it draws in place as before. [drawStatus & drawCaption](docs/drawing/text.md#notices)
- **A parameter read costs what a stored property costs.** A `@Param` holding a number, an integer, or a switch now reads in under a nanosecond, down from about 40, so a loop may read one per vertex and the advice to copy parameters into locals first applies only to a color, a vector, a palette, or a text value, which still read through a lock. [Parameters](docs/helpers/parameters.md)
- **2D torque takes points, like force.** `applyTorque` now takes a force in points times an arm in points, so the numbers that push a body also turn it; it took the engine's newton-meters before, so a sketch calling it spins about ten thousand times faster at the default scale and wants its torques divided by `unitsPerMeter` squared to keep its motion. [Rigid bodies](docs/simulation/physics.md#rigid-bodies)
- **The band-pass voice filter is unity at its cutoff.** Resonance now only narrows the band; the output used to grow with it (five times louder at resonance 0.9), so a band-pass voice at high resonance plays quieter than before. [Voice.Filter](docs/helpers/synthesis.md#voicefilter)
- **An export's soft shadows sample 64 points on the light.** Under temporal anti-aliasing an export traces one set of points on the light across its sixteen passes, and the `.detail` tier every export resolves to now takes 64 of them, four a pass, where the live tier's 16 showed a wide penumbra as sixteen steps; `shadowSamples(_:)` still passes an exact count through, and a window traces what it did. [Soft shadows](docs/3d/3d.md#soft-shadows)
- **A camera rig's framing follows its arguments.** `cameraShowcase`, `cameraControl` and `cameraMove` used to read `target`, `radius`, `elevation`, `azimuth` and `fieldOfView` on their first call only, so a `@Param` passed in tuned nothing in the window while an export framed with it. The arguments are now the shot's home. A changed one glides the shot there over about half a second, under a move or the showcase's auto-orbit, and under `cameraControl` until the viewer takes the camera. After that it moves only where `resetCamera()` and the idle return go. The same values every frame change nothing, so a sketch passing constants draws exactly what it did. The `from:` forms take the authored camera's pose the same way. [Showcase](docs/3d/camera.md#showcase)

- **A traced shadow under an export's temporal anti-aliasing costs a sixteenth of what it did.** The export draws the scene sixteen times at sub-pixel offsets, and a traced point or area shadow traced its whole ray budget in each of them, the same points on the light every time. Each pass now traces its own share of one set, so the frame traces the budget once: 1,000 spheres under a point light at 1080 by 1920 went from 25 seconds a frame to 2.3, with the same penumbra. A `shadowSamples` count is now how many points on the light the whole frame traces, so under the sixteen passes any count up to 16 costs one ray a pass. Exports of traced shadows under temporal AA change by a fraction of a level along their penumbrae. [Soft shadows](docs/3d/3d.md#soft-shadows)

- **Even spacing on a surface is about three times faster.** `surfacePoints(on:count:)` with its default `.blueNoise` thinning now searches only the cells its candidates occupy, where it walked a grid of the whole bounding box, and keeps one queue entry per candidate. 400,000 points on a terrain took 13.4 s in a release build and take 4.7 s, a second of which is drawing the candidates. The debug build that `swift run` makes gains more: there the thinning of those points went from three minutes to 20 seconds. The points are the same for every mesh, count, and seed, so a scatter laid down before looks exactly as it did. [Points on a surface](docs/generators/surfacesampling.md)

- **`MarkovChain` is in the core.** The chain the music helpers use to pick notes now lives in `Ollin` itself, since it picks colors and line lengths as readily as notes, and a drawing sketch no longer needs the audio library for it. Every seed walks the same way it did. A file that imports only `OllinAudio` and uses the chain now needs `import Ollin` beside it. It also takes a table of counts directly: `learn(_:after:times:)` records that a context was followed by an element a number of times, so a table counted off a picture or written by hand becomes a chain with no sequence behind it. [MarkovChain](docs/helpers/composition.md#markovchain), [a choice that remembers](docs/generators/random.md#markovchain)

- **A color-vision simulation shows a dichromat colors they would see as themselves.** At full severity `Color.simulated(_:)` and `Filter.colorVision(_:)` now project each color onto the surface of colors a dichromat can tell apart, the two half-planes of Brettel, Viénot and Mollon (1997), so simulating a simulated color leaves it where it is. The table they used before was not a projection: a blue moved 0.064 on a second pass, a purple 0.072 under tritanopia. A severity below 1 now moves each color that share of the way in a straight line in linear light, so turning the severity up only ever moves a color toward where the dichromat sees it; the old tritan dial passed its own end on the way. Grays and white come back exactly. A result past what the display can show is still clamped channel by channel, which changes what that viewer sees least. Pictures move, the tritan column most, and the safe set's closest pair reads 0.078 where it read 0.076. **Breaking:** `ColorVision.matrix` is removed, since every kind now has two transforms rather than one; `applied(toLinear:_:_:)` still gives the model's result for a linear color. [Seeing your colors as somebody else does](docs/helpers/accessibility.md#seeing-your-colors-as-somebody-else-does)

### Fixed

- **An overlay no longer loses the scene under a sky backdrop.** The overlay pass drew the environment's backdrop a second time, full-frame and with no depth, so a `withOverlay` block (or a caption) over a scene lit by `environment(.sky(...))` or an HDRI with its backdrop shown could cover every surface and keep only the sky and the block, where the camera's far plane let the backdrop through; the pass now starts from the finished frame and draws only the block. [withOverlay](docs/drawing/effects.md#withoverlay)
- **The probe grid keeps its planes off a scene's faces.** The global-illumination grid is now laid so a scene's bounding faces sit half a cell inside the outer probe planes, where a face on a plane put a ragged dark seam on a room's open end (a row of probes at the walls' front edges, flipping in and out of the visibility test); the seam is gone at the framework, and a room no longer has to run past the camera to avoid it. [Global illumination](docs/3d/3d.md#global-illumination)
- **A traced shadow's softness no longer follows the camera.** A traced point light's area is now sized by its distance to the scene's center, where it followed the distance to the camera's target, so the penumbra stayed put only while the camera aimed at one spot (192 levels between two aims along one line) and breathed through a camera move. [Soft shadows](docs/3d/3d.md#soft-shadows)
- **A light's reach holds in the bounce and in the trace.** The probe field and the path tracer now read a lamp's `reach:` as the raster does, so a lamp lights and bounces nothing past it anywhere, where the field used to fill a room the lamp could not reach and the tracer lit the floor past it. [Reach](docs/3d/3d.md#reach)
- **A sun brighter than a half float arrives whole.** An HDRI is now decoded at single precision and scaled into half range by one factor the bake divides back out, so a sun of 200,000 lights what its pixels say, where the decode used to clamp it at 65,504 and deliver a third of its light. [Environment lighting](docs/3d/3d.md#environment)
- **The blobby cross ends at its tips.** Its distance formula reported a fraction of the true distance past each tip, so the render drew a faint spike from every tip to the quad's edge; the diamond through the four tips now floors it, and a blobbiness below 0.35 draws to its `radius` where the tip used to fall short. [drawBlobbyCross](docs/drawing/drawing.md#drawblobbycross)
- **A traced SVG outline keeps its sharp corners.** The curved distance-field shapes trace their outline on a grid, which cut a sharp corner across by up to a cell's diagonal (2.4 px on the cut disk's corners, 1.7 on the horseshoe's arm ends); a cell whose two crossings disagree in normal now bends through the corner where the two sides meet. [Vector: SVG](docs/output/export.md#vector-svg)
- **Text along a path fills the glyph the plain run fills.** The per-glyph route (`drawText(_:along:)`, `perGlyph:`) flattened each glyph afresh at render size, so the same letter carried 4.5% more ink along a path than in a plain run; it now takes the cached outline the plain run uses, cleaned of self-overlaps, so the two agree to a level. [Per-glyph drawText](docs/drawing/text.md#perglyph)
- **A sound export finds every instrument, on time, at the right level.** Four leaks in the soundtrack an export writes, each measured in the survey below, are closed. An instrument held inside a struct, an array, an optional, a dictionary, or an object of the sketch's own is found and advanced like one held as a plain property, so the Spatial example's three chimes are back in its own file (the sketch's values are read through everything it holds, direct properties first, each object once, a list through its first element). A note asked for on a frame starts on that frame's first sample, where every note used to land one frame late (42 ms at 24 fps). A change of `hearingDistance` while the export runs reaches the file at the frame that made it, where the file used to hold the range the export began with. And `unplace()` plays the sound at the level it had before it was placed, in both ears, where it used to sit 6 dB down for the rest of the piece. The finder's rules are on the Synthesis page under *Sound in an export*. [Sound in an export](docs/helpers/synthesis.md#sound-in-an-export)

- **A struck body no longer clips, and `hardness` is the color of the strike rather than its loudness.** The strike's push is divided by its own area, so a softer strike reaches fewer tones instead of pushing the same tones harder, and a strike is as loud at 48 kHz as at 44.1. Before, every struck preset but `.wood` sat on the renderer's ceiling for most of its attack (the bell for 56% of its first second) and the clip put tones in the bell that it has no partial at. The presets had been tuned under that clip, so they strike harder now (`.bell` and `.bar` 0.9, `.drum` and `.plate` 0.85, `.glass` 0.8) and each rings the partials it declares; the default `hardness` of a `ModalBody`, `ModalBody(shape:)`, and `StruckShape.body` moves from 0.7 to 0.85 so a drawn shape's upper modes are heard an octave up; and the struck `Voice` presets (`.drum`, `.bar`, `.chime`, `.glass`) carry gain 1, which puts them within 3 dB of `.pluck` at the same velocity. A sketch that balanced a struck instrument against the old clipped level will hear it a little quieter and a good deal cleaner; the `SoundInAnExport` and `StruckShapes` examples raised their struck instruments' gain to keep their balance. [A struck body](docs/helpers/synthesis.md#a-struck-body)

- **Promises the reference no longer makes, after a survey of the sound, the color science, and the solvers.** A plucked string's `decay` is exact at low notes and shorter at high ones; a bowed string's `decay` is a loss in its loop, not a ring-out, since a bow at pressure 0 rests on the string; a blown tube needs a minimum breath before it speaks (0.6 for the clarinet, 0.4 for the reedy one, 0.7 for the hollow one), and the pressure sweep the page shows now starts there; `Delay.time` runs up to 2 s; the limiter flattens a wave that crosses its ceiling, since it has no lookahead; the mixed soundtrack bends anything above 0.7, a single loud instrument included. A `.wide` or `.extended` frame skips the dither on its way to an eight-bit file, the wide PNG is written at eight bits with a linear profile, and a gain map's one percent holds across a highlight and its field, not at its edge. A Lab color outside sRGB keeps its OKLab lightness and hue, and the system's generic CMYK profile proofs alike under perceptual, saturation, and relative. In the physics, a particle's `drag` is taken once a step (45% of its speed a second at 60 Hz at the default) where a rigid body's is a loss per second and both reach a body or the walls when they are made; a body bounces off the walls or the ground with the larger of its own restitution and the world's; a spin is capped at a quarter turn a step; every 3D body loses 5% of its speed and spin each second; and a 3D frame a hair over 1/60 s takes two collision passes. Each gap is pinned by an invariant test that fails the day it closes, and filed in the design notes with the shape of its fix.

- **A background on the canvas leaves the layers alone.** `background(_:)` outside a layer block used to drop everything the frame had recorded, the layers drawn before it included, so a `withTarget` layer, a `withField` seed, or a feedback loop drawn before the canvas background came out empty with no error, and the reference told you to call `background` first. It now clears the canvas and keeps every layer's marks, so the order no longer matters: a frame that draws a layer, then the canvas, then the background is the same picture, pixel for pixel, as one that sets the background first. A frame that sets its background first is unchanged. [withTarget](docs/drawing/effects.md#withtarget)

- **Measured and written down where the vector outline and the render part.** A test now checks, for all 34 distance-field shapes and point markers, that the outline the SVG export writes traces the shape the window draws. Three gaps stand as known issues with their numbers: the blobby cross draws a faint spike past each tip that its outline does not have, and the traced outlines of the cut disk and the horseshoe round their sharp corners off by about 2 px. A plain text run and the same text along a path or drawn glyph by glyph differ slightly at the curves (a 20 px O carries 4.5% more ink along a path); that is pinned the same way. [Vector: SVG](docs/output/export.md#vector-svg)

- **Known gaps in the photoreal tier, measured and written down.** Nineteen more physical invariants read the materials, the environment and the sky, the air, global illumination, the shadows, the path-traced export, the live temporal resolve, `LineSpray`, and the lens flare's coatings against analytic values and a deep path trace. Five hold outright, and fourteen gaps stand as known issues with their numbers (in `CAPABILITIES.md` and under *Photorealistic 3D* in `DESIGN-NOTES.md`). Among them: a rough white plastic loses about a tenth of its light at grazing under an environment, the procedural sky's sun casts almost no light of its own, and global illumination gathers no emission and ignores a lamp's `reach:`, as does the path tracer. The 3D reference no longer promises that emitters light their neighbors under global illumination (they do so in a path-traced export) or that 2D in a 3D canvas is untouched by the live temporal resolve while the camera is still.

- **A camera move starts at its start.** A `cameraMove` or `cameraShowcase` move advanced its clock before posing the camera, so its first frame showed it one step in and exported frame N was posed at (N+1)/fps while `time` read N/fps. Two exports of one frame at different frame rates disagreed. The move now poses at its clock and then advances it, so frame N shows the move at N/fps. Every exported frame of a sketch with a camera move moves by one step. [Camera moves](docs/3d/camera.md#moves)

- **A move resumed after the hands starts from the hands.** Switching from `cameraControl()` back to the same `cameraMove` it left resumed the move from where it first started, so the camera jumped away from the shot just framed by hand. It now departs from the current pose, as switching to a different move always did. [Camera control](docs/3d/camera.md#control)

- **Exact black and exact white stay exact.** The last pass adds up to a level of noise before the 8-bit encode so a smooth ramp does not band, and at the ends of the range that noise had nothing to do but harm: about one pixel in six of an exact black came out as 1, and one in eight of an exact white as 254. So a picture made only of black and white did not export as only black and white, and every export on a black ground fed a faint noise floor to the video encoder. The noise now reaches less far within a level of either end, never past it: exact black is 0 and exact white is 255 in every export and on the web page, gradients away from the ends dither as before, and values near an end average back to themselves more closely than they did. [Color output](docs/drawing/coloroutput.md#exports)

- **A motion streak keeps the light of what made it.** `motionBlur()` had kept a mover's inside at its full value while adding the smear around it, so a moving thing grew brighter: a 24-pixel box crossing at 20 pixels a frame gained 5% of its light at the default shutter and 25% at a full one, and a bloom read the excess. The gather now weighs each tap by whether a streak reaches the pixel and leaves the weights as they are, so their mean is the share of the shutter the streak spent there and the pixel's own color fills the rest. The box keeps its light within 0.2%, and region by region the streak stays within 2% of the box's value of the subframe export. On the `MotionBlur` example the error around the spheres fell by two to three times, and the sharp ghost of a sphere inside its own streak is gone. A caption drawn over the 3D scene takes the motion of the surface under it, so draw it in `withOverlay { }`. [Motion blur](docs/3d/3d.md#motion-blur)

- **A caustic gives back the light its shadow took, and no more.** A clear glass ball's caustic had been adding half again the light its shadow removed, where real glass gives back less than it blocks. Two things made it bright: on a physically based floor the splat shaded its light at pi times what a direct light of the same strength gets, and it counted the angle the light arrives at twice. A half-float layer hid part of that, capping a focused spot at a power of two and shaving a few percent off everything else; the layer is single precision now. A caustic is now shaded exactly as the floor shades a direct light, on every shading model, and a clear pane gives back 92% of its shadow's light, a clear ball about 84% (the rest is what their surfaces reflect). Caustics are dimmer than before on a physically based floor; `caustics(intensity:)` above 1 turns them up past physical. Light leaving glass now reflects at the angle it exits at, a live caustic no longer dims for its first frames while its photon plan settles, and a point or spot light's caustic carries the light the lamp lays at the end of each photon's path, so a lamp's ball gives back what its shadow took rather than a third of it, a mirror throws exactly the light of the lamp's mirror image, and a lamp's `reach` thins its caustic as it thins its light. [Caustics](docs/3d/caustics.md#casts)


## [0.14.0] - 2026-10-06

### Added

- **Kazuya Sakai joins the recreations.** [`Examples/Recreations/KazuyaSakai`](examples/recreations/kazuyasakai/index.md) is new, after the painter who was art director of Octavio Paz's *Plural* and among the first to show geometric abstraction in Mexico. `Ondulaciones` is the series of 1975 and 1976: a ribbon of hard-edge parallel bands comes into the panel straight, wraps disc after disc in concentric arcs, turning one way then the other, and runs off another side, each band the one centerline moved across by its own width so neighbors share their edge point for point, in three looks read from *Filles de Kilimanjaro III (Miles Davis)*, the 1975 screenprint and the purple canvases, the route dealt by the seed and laid down and taken back over the cycle. `RightOff` is the painting of 1973 where the circle came into his fields: a score of horizontal bands under navy suns ruled with white rings, crescent hills, and two red lobes with a valley between them, the rings traveling outward on the clock. Both export as outlines, every band and hill a closed path and every disc and ring a circle.

- **The lit and sounding tiers are pinned by physical invariants.** Thirty-two tests state what holds whatever the implementation (a lossless surface returns the furnace's field, a shadow only takes, fog follows Beer-Lambert, a tone curve is monotone, a plucked string only loses energy, a sound placed to the left is the mirror of one placed to the right) and read it off a render or rendered samples against a twin or an analytic value; each one that holds was verified red under a planted defect. Four measured gaps stand as known-issue pins and name the next deep reviews: the caustic deposits 1.5 times the light its shadow took, the motion blur gains a quarter of a mover's light at a full shutter, the color-vision simulation's dichromat ends are not projections (the tritan one farthest off), and a proof through the generic CMYK profile moves a saturated color by up to 11 levels when proofed again.

- **The regions a drawing encloses.** `regions(enclosedBy:)` reads a set of outlines as drawn, cuts every line where it meets another or itself, and hands back every patch the lines wall off as its own `Shape`: two overlapping circles give three, a grid of lines its cells, a page of scribbles every patch a pen could color without crossing a line, and the bare page ringed by several circles is a patch like any other. A region inside another is a region of its own and a hole in the one around it, a stroke whose end hangs loose walls nothing off, and the regions come back in reading order. `Circle.contour(segments:)` and `Rectangle.contour` turn the two values into outlines for it, or for a `Shape` or a boolean. New example: [`Examples/Shapes/ColoringBook`](examples/shapes/coloringbook.md). [Geometry](docs/drawing/geometry.md#regions)

- **Points in the order of a Hilbert curve.** `hilbertOrder(of:)` and `hilbertSorted(_:)` sort points by their place along a space-filling curve laid over their bounds, so one line through a stipple comes from a single sort and stays local, and a large set is put in order in one pass; `hilbertIndex(of:in:level:)` is the place of one point on a curve over any rectangle, and `level` sets how fine the curve is. New example: [`Examples/Images/HilbertLine`](examples/images/hilbertline.md). [Hilbert order](docs/generators/hilbertorder.md)

- **A traced frame on a time budget.** `--path-traced --pt-seconds 10` gives each frame ten seconds in place of a count: the tracer sizes its last dispatch to the time left, so a frame ends within about a sample's worth of its budget, and the count follows the scene under the cap (a named count is still the most a pixel traces; the bare flag's cap under a budget is the detail tier's 4096). The frame the clock leaves is a fixed render of the count it reached, byte for byte, and the recipe records the budget and that count (`"seconds":10,"reachedSamples":50`), so a frame reproduces with the count in place of the budget, under `--pt-noise` too. `--pt-min` is a floor the clock may not cut under, the first frame of a `--pt-reuse` run gets the history's worth of time, and a sequence or video export ends with one line that says the counts the frames reached and the mean time a frame took. On the example scene at 1080 by 1080, ten seconds traced 50 samples where a fixed 128 took 29.5 s, and the same ten seconds at a path depth of 2 traced 93. In code, `PathTracing.secondsPerFrame`. [Path-traced export](docs/output/pathtraced.md#a-time-budget-a-frame)

- **A traced sequence carries its earlier frames.** `--path-traced 32 --pt-reuse 8` carries up to eight frames' worth of samples into each frame of a sequence or video export: every pixel is followed back to where it was in the frame before, by the motion the sketch declared for its movers or the camera's own through the depth, the earlier frame's samples are taken there only where they belong to the same surface, and the carried light is trusted only as far as the frame's own grain allows, so a surface a mover uncovers starts over and a reflection sliding over a polished ball is followed rather than smeared. A sequence at 32 samples a frame then shows the grain of 256, and that grain holds still from frame to frame: on a field of two thousand polished beads sweeping under a lens, 32 with 8 carried ran in 42 percent of the time of 128 alone and its frames sat 20.6 dB apart where 128's sat 19.9, each frame's own fidelity between the two counts. The first frame of a run traces the history's worth itself, so the sequence starts settled. A frame is then a function of the frames before it: the same command renders the same sequence byte for byte, a still rendered alone has no history, and the recipe records the frames reused and the mean count a pixel held (`"reuse":8,"carriedSamples":231.5`). In code, `PathTracing.reusedFrames`. [Path-traced export](docs/output/pathtraced.md#earlier-frames-carried-reuse-across-a-sequence)

- **A bound on what a second bounce may carry.** `--path-traced 256 --pt-clamp 4` holds what light that has bounced twice or more may add to a sample at four times white, so a polished scene's fireflies, a lamp caught off one polished surface onto another every few hundred samples that no count smooths, are gone at a few hundred samples and the per-pixel stop can settle it. What one bounce shows, a lamp in a mirror, a window on a glossy floor, is never touched, so a scene with nothing to bounce light between renders the same bytes. It is a bias and the frame says what it cost: the recipe records the bound and the share of the frame's light it dropped (`"clamp":4,"clampDropped":0.0083`), and a sequence or video export ends with the mean over the run. A share of several percent beside grain that did not move means the grain was one-bounce light, which the bound leaves alone, and the scene wants its count instead. In code, `PathTracing.maxBounceLight`. [Path-traced export](docs/output/pathtraced.md#a-bound-on-what-one-bounce-may-carry)

- **A traced pixel stops once it has settled.** `--path-traced 512 --pt-noise 0.01` lets each pixel end early once the grain it is left with falls under the figure (the standard error of its brightness against the square root of that brightness, about one level in 255 at 0.01), so the sample count becomes the most a pixel may trace and the samples go where the grain is: the black margins and a sharp, evenly lit face end early, a blurred or glossy region runs to the cap. Every pixel takes a minimum first (the square root of the count, `--pt-min N` to raise it), the open pixels are checked every eight samples, and a pixel still sampling keeps its eight neighbors sampling. On the example scene at 1080 by 1080, a cap of 512 at 0.04 reached a mean of 217 samples a pixel for 29.2 dB against a 2048-sample render, where a fixed 224 read 26.7 and a fixed render given the same time read 28.9; the time follows the samples only loosely, since the pixels that settle first are the cheap ones. A scene that is grain from edge to edge settles only its margins, and says so in the mean. The frame stays a pure function of its index, and a traced frame's recipe now names what it was traced with and, under the stop, the mean count reached (`"pathTraced":{"samples":512,"depth":8,"noise":0.01,"minSamples":24,"meanSamples":131.4}`); a sequence or video export ends with one line that says the mean over the run. In code, `PathTracing.noiseThreshold` and `minSamplesPerPixel`. [Path-traced export](docs/output/pathtraced.md#fewer-samples-where-the-picture-has-settled)

- **Small highlights under the lens are drawn as discs of their own.** `depthOfField()` and the `.defocus` layer filter take a small bright source out of the gather and draw its disc directly, scattered rather than gathered, so a point of light opens into an even disc at every quality where before it came out grainy at `.performance` and a one-pixel lamp was lost almost whole; a sharp object in front cuts the disc at its own edge. The look of every blurred highlight moves, so a snapshot or a video made with either will differ. [3D](docs/3d/3d.md#depth-of-field)

- **A traced export no longer blurs twice.** A sketch that calls `depthOfField()` and exports with `--path-traced` had the canvas pass run over the traced frame, which had come through the lens already; the pass now stands down under a traced export. [Path-traced export](docs/output/pathtraced.md#what-the-traced-frame-adds)

- **The overlay.** `withOverlay { }` draws its block over the finished frame, after the temporal anti-aliasing, the depth of field, the motion blur, the lens flare, and the `postProcess` filters, so a caption or a readout stays sharp and untinted whatever the scene under it does. The block lands over everything else whatever the call order, a blend mode in it reads the finished picture, a clip inside it clips the overlay alone, and over an accumulating canvas (`noClear()`) it is drawn each frame without joining the pile. A vector export writes it last, and so does the web page. [Effects](docs/drawing/effects.md#withoverlay_)

- **Depth of field on the canvas.** `depthOfField()` blurs the finished 3D frame by its own depth through the camera's lens: `Camera3D.aperture`, `focusDistance`, and `apertureBlades`, the settings the path-traced export reads. A point spreads by the thin lens's own size, so the blur grows quickly toward the camera and levels off far away, a highlight opens into an even disc the shape of the iris, a subject in focus keeps its edge, and a blurred foreground turns into a veil over what stands behind it. `focusRange` holds a band sharp, `maxBlur` caps the blur, and `quality` sets how smooth it is. It runs after temporal anti-aliasing and before motion blur, so all three work together, and an export carries it byte for byte. `noDepthOfField()` turns it off. [3D](docs/3d/3d.md#depth-of-field) What a blurred foreground hides is read from a second drawing of the scene behind it (a depth peel against the frame's own depth, drawn only when something stands in front of the focus and only over the pixels it covers), so a ball's halo over the horizon carries the floor the ball hides rather than the backdrop beside it; measured against an accumulation-buffer lens, the green cast there fell by three quarters and the whole frame's error by a fifth. A raymarched field joins that second drawing (its march starts behind the first surface), and a plane, a strip, or an open tube seen from its back joins it too, where before only closed shapes' outsides did.

- **Instanced copies that move.** `drawMesh(_:instances:)` inside `withMotion` follows every copy from where it was last frame to where it is now, so temporal anti-aliasing keeps each copy's edges and motion blur streaks each copy along its own path, in one draw. Copies are matched by their place in the list; a frame whose copy count changed keeps the camera's motion alone for those copies. Copies placed by a compute buffer and a `MeshField`'s keep the camera's motion, with a note. [Instancing](docs/3d/instancing.md#motion)

- **Lines in 3D.** `drawLine` and `drawPolyline` take `Vector3` points and draw a line through the camera with the 2D stroke's weight, joins, caps, and soft edge, and `drawPolyline(_:colors:closed:)` gives every point its own color. Every point keeps its depth, so a solid in front hides the line and the line hides what lies behind it, whichever is drawn first. The weight is in canvas points, the same at every distance, unless `strokeWeight(0.05, in: .world)` asks for the scene's units, which thin the line as it recedes. `drawAxes(length:)` and `drawGrid(size:divisions:)` are built from the same lines and export with the picture. An SVG export carries the line as the 2D polyline the camera made of it. New example: [`Examples/3D/Geometry/FieldLines`](examples/3d/geometry/fieldlines.md). [Lines in 3D](docs/3d/lines.md)

- **Drawing part of an image.** `drawImage` takes a source rectangle in the image's own pixels, so one cell of a sprite sheet, a tile of a tile set, or a frame of a film strip draws with no cropping first: `drawImage(sheet, x, y, width, height, sourceX, sourceY, sourceWidth, sourceHeight)`, or `drawImage(sheet, in: box, source: cell)`. The draw reads only inside the part, so a cell drawn at its own size or larger looks exactly like the same cell cropped out first, and a magnified sprite never picks up the border of the one beside it. A web page of the sketch does the same. [Images](docs/drawing/images.md#part)

- **A precise pile.** `noClear(precision: .float32)` keeps the canvas in single precision, so a long exposure of very faint marks keeps adding where a half-float canvas levels off: a pile that takes eight marks of 0.00005 a frame from 0.25 reaches 0.37 after three hundred frames, where half float stays at 0.25. [Accumulation](docs/drawing/accumulation.md#noclear)

- **A feedback layer, an accumulator, or a simulation field as the base of a combine.** `Feedback`, `Accumulator`, and `SimField` take `combined(with:_:)` beside `filtered(_:)`, so trails can be masked or displaced by another layer without a filter that changes nothing first. [Effects](docs/drawing/effects.md#feedback)

- **Saloua Raouda Choucair joins the recreations.** [`Examples/Recreations/SalouaRaoudaChoucair`](examples/recreations/salouaraoudachoucair/index.md) is new, after her modular sculptures. `Poem` cuts a block into verses by a rule, every corner of every joint on a grid and some verses with windows through them, after the wooden *Poem*, the stone *Poem* and *Infinite Structure*, and *Poem of Nine Verses*; the verses are lifted apart until each clears the one below, turned and slid, and set back, lit in the round, and `--export-usdz` writes them as solids that make the block again. `Duals` cuts a block once into two parts with the shape booleans, along the stepped teeth of the *Repetitive Dual*, a keyed slab, or a staircase; they slide apart until a line can pass between them, lean away, and lock, lit in the round or flat as a plan that `--export-svg` writes.

- **Sweeps and strips.** `Curve3D` is a path through space, open or closed, kept as points or threaded smoothly through a few with `Curve3D(curveThrough:closed:segments:)`. It walks by length (`point(at:)`, `frame(at:)`) and carries a frame at every point that turns with the path and never twists about it, so `rotate(frame.rotation)` faces a rider the way the path runs. `Mesh.sweep(_:along:scale:twist:capped:)` (and `drawSweep`) carries a `Shape` along it, sized and turned by functions of the fraction along, with hard edges kept at sharp corners, walls facing out whichever way the outlines run, capped ends, and uvs. `Mesh.strip(between:and:colors:closed:)` (and `drawStrip`) is the ribbon between two lines, one color a rung, with normals from the strip itself. `Pace` walks a curve given as a function at one speed (`Pace(byLengthOf:period:)`), or the strip between two such curves at one rate of area (`Pace(byAreaBetween:and:period:)`), and keeps counting periods so a loop never jumps. [Sweeps and strips](docs/3d/sweeps.md)

### Changed

- **A traced pixel reads a curved polished surface at its footprint.** A pixel's rays fan out over a cone of directions on a curved mirror, and the path tracer now carries that cone with each path (the pixel's angle, the lens, and every surface's curvature under it) and reads the environment at the matching level, beside the sampled lobe's own footprint, while the light list is shaded through a lobe widened by the normals under the footprint, the live view's specular anti-aliasing; the tracer's copy of the environment weights its mip chain by latitude so a coarse read keeps the room's mean. A field of small polished beads mirroring a lit room, grain from edge to edge at any count before, renders at 128 samples with a quarter of the per-sample spread; a flat mirror reads as before, and nothing is set. [Path-traced export](docs/output/pathtraced.md#a-pixels-worth-of-a-curved-surface)

- **A scene nests four times deeper, and nothing a file nests reaches the stack.** A glTF or USD node tree reads to 256 levels, from 64, and every walk of a loaded scene (its bounds, cameras, and lights, the posing and skinning, an animation, the import, the USD writer, and the draw) keeps the nodes still to visit on a list rather than on the call stack, as both readers already built their trees. The limit is a choice checked against the one cost that stays per level, the runtime's own release of a nested value when a scene is dropped, so a scene at the limit reads, poses, writes out, and is dropped on half a background thread's stack with room to spare. A value in a USD file (a dictionary in a dictionary, a tuple in an array) keeps a limit of its own, 64 levels. [Scenes](docs/3d/scenes.md#loading)

- **`.defocus` is the canvas lens's gather.** The layer filter runs the same five passes `depthOfField()` runs, fed by its own depth map and its ramp (`focus`, `range`, `maxBlur` unchanged): each sample lands as a disc of light of its own size, and the layers are sorted by distance, so a near highlight keeps its light and opens into an even disc (it kept 27 percent and peaked at six times its mean), a thin line in focus keeps its weight (it kept 4 percent), a near object keeps all its light (half before), and an in-focus silhouette has no rim (10 percent before). A drawn depth map's anti-aliased rim joins its nearer side instead of being dilated away, which is what used to consume thin lines. The blur also grows with distance all the way to the map's ends now, reaching `maxBlur` at 0 and 1 (a scene's near and far planes); before it reached `maxBlur` one `range` past the sharp band, so a narrow band blurred every other depth by the same amount, a masked blur rather than a lens. The blade count, `irisAngle`, and `catsEye` carry over. Behind the focus both passes now composite a nearer surface over what it hides by its cover instead of averaging the two by density, so a sharp object over a much blurrier background keeps its light (98 percent measured, 93 before). The look of every defocused layer moves, so a snapshot or a video made with it will differ. [3D](docs/3d/3d.md#depth-of-field)

- **The frame recipe names the framework's commit.** Beside `git`, the sketch package's commit, an exported frame's recipe carries `ollin`, the framework's own, each `-dirty` when its tree is, so a frame made from a path dependency can be tied to the framework that drew it. [Export](docs/output/export.md#reproducibility-metadata)

- **A filter over a single-precision layer keeps single precision.** A filter or a combine over a layer made with `precision: .float32` used to write a half-float layer, so a distance field measured past 1,024 pixels came back rounded to whole pixels and its far contours were ragged, and a blur after it rounded again. Precision now follows the layer through the whole chain. A half-float layer's chain is unchanged. [Effects](docs/drawing/effects.md#rendertarget), [Measured distance fields](docs/drawing/distancefields.md#how-far)

- **An export holds a sketch that stopped its loop, the way the window does.** After `noLoop()`, every export used to call `draw()` for every frame up to the one asked for, so a draw that rolled the sketch's dice exported a picture the window never showed, and a slow still took one draw per frame (the Watercolor example, about four seconds a draw, took half an hour to reach eight seconds). Now the frame the loop stopped on is the picture from then on: a still at any later frame is that frame, drawn once, and a video, a GIF, or a PNG sequence writes it for every frame after the stop. A web page of a sketch stopped in `setup()` is one frame, held still. A replayed take still draws every frame it holds. [Sketch](docs/core/sketch.md#noLoop)

- **A closed contour's walk wraps.** `Contour.point(at:)`, `tangent(at:)`, `normal(at:)`, and `piece(from:to:)` used to clamp a fraction to `0...1` on every contour, so a clock passed to a closed one stopped at the start after one lap. On a closed contour they now wrap it, the way a closed `Curve3D` does: `point(at: 1.25)` is a quarter of the way round again, and `piece(from: 1.9, to: 2.1)` is `piece(from: 0.9, to: 0.1)`. An open contour still clamps, and fractions inside `0...1` read exactly as before. [Geometry](docs/drawing/geometry.md#contour-questions)

- **A tube's rings ride the new frames.** `Mesh.tube(along:)`, and the helix and torus knot built on it, now place their rings on `Curve3D`'s frames. They are worked out by double reflection, which is accurate to the fourth order where the old transport was second order (on a helix at 64 points a turn, its twist was 0.006 radians off after one turn, and now it is 0.000002). A closed tube spreads its leftover turn by length rather than by point. On the built-in helix and torus knot the rings turn by under two degrees about the path, and the first ring starts where it always did. [3D](docs/3d/3d.md#solid-primitives)

### Fixed

- **A tap no longer overflows its thread after minutes of use.** A video's soundtrack tap, a camera's or the screen's frame tap, a listening hub's tap, a custom effect's process, and a parameter's show rule were each kept as the bare state of a lock, which re-wrapped the closure in two thunk frames on every read, in debug and release builds alike; a tap read once per audio block or camera frame overflowed its thread's 512 KB stack within minutes (the video tap at about twenty, the screen tap sooner). Each is kept in a struct now, and the consumer sits one call deep on the three-hundredth block as on the first. [Video](docs/video/video.md)

- **A blurred ball's edge follows the lens.** `depthOfField()` and `.defocus` spread each sample of a surface by its own blur, and on a ball in front of the focus the rim is nearer than the middle and so blurred wider, which spread the ball's edge wider than a lens does: a band of too much ball outside the silhouette (a teal halo over a floor, a brighter halo over black) and too little inside. Each sample's weight now carries the Jacobian of the blur's slope across its surface, and a surface's share of its own pixel is read as that density, so to the gather a ball is what it is to a lens, a disc at its center's depth. Measured against the lens on a ball in front of the focus, the cover outside the rim went from a thirtieth over to within a hundredth, and inside from a twentieth under to within a hundredth; a flat surface, a bar, and a disc render as before. The look of a blurred curved surface moves a little, so a snapshot or a video made with either will differ. [3D](docs/3d/3d.md#depth-of-field)

- **A number the solver cannot hold no longer stops the sketch.** A body's position, velocity, or spin set to a value that is not a number, an infinity, or a magnitude past a billion units, a force or an impulse made of one, a world's gravity, ground, or unit scale, a character's walk, a joint's drive, a vehicle's controls, a soft body's pressure, or a body added at such a place used to stop a debug build inside the solver one step later and poison a release build's contacts. Each is now refused where it crosses into the solver, in 2D and 3D: a property keeps what it had, a force does nothing, a creation argument falls back to its default, and the world prints one note naming the call. The calls that already throw for what they cannot build throw for the number too. [3D physics](docs/simulation/physics3d.md), [2D physics](docs/simulation/physics.md)

- **The barrel keeps the light.** A `.defocus` with a `catsEye` above zero let a blurred disc carry only the clipped opening's share of its light, so a near disc kept its whole color inside its silhouette and covered the backdrop by that share outside, a step at the silhouette and a halo too faint, growing toward the frame's edges (on the `Effects/Defocus` example's largest disc, a fifth of its brightness across four pixels). The gather and the sprite draw now divide by the area of the opening as the barrel leaves it, so the clipped opening changes a disc's shape and never its light, as the reference said. A `catsEye` of 0 is byte for byte what it was; a picture made with one above it moves. [3D](docs/3d/3d.md#depth-of-field)

- **The traced lens focuses where `focusDistance` says.** The path tracer focused `focusDistance` past the near plane rather than from the eye, and placed its opening on the near plane, which widened the blur by `s / (s - near)`; the focus is now measured from the eye and the opening sits at the eye, as the canvas lens and the thin-lens formula have it. Every traced frame with an aperture changes. [Path-traced export](docs/output/pathtraced.md#what-the-traced-frame-adds)

- **A mover behind instanced copies no longer streaks through them.** The motion pass that temporal anti-aliasing and motion blur read drew only plain meshes in front of a mover, so a `withMotion` mover behind a field of instanced copies wrote its motion through them. Every instanced draw now hides what stands behind it there. [Instancing](docs/3d/instancing.md)

- **An EXR's coverage stays at one.** Additive marks sum their alpha along with their light, so an opaque canvas lit by two added coats wrote an alpha of 3 into `--export-exr`, which a compositor reads as a pixel covered three times over. The alpha channel now stops at 1; the light itself stays as it summed. [Export](docs/output/export.md#linear-frames-exr)

- **A sketch that reads a detector exports the same picture every run.** A tracker's reading used to land whenever its analysis finished, so which frame showed it followed how fast the export ran: seven examples exported a different picture each time, and in most of the rest the detector never answered at all (DepthCloud stayed on its loading notice, BodyPose found nobody). During an export, every frame a source offers is now analyzed before the frame is drawn, each tracker's model is loaded before its first frame, and a tracker made during the export reads the newest frame at once. A still feed publishes on the export's clock, and a playing `VideoPlayer` hands each new frame to its trackers, where before they read nothing in an export. `--settle` no longer has to wait out a model. One detector is still not exact: Vision's trajectory request fits its arcs a few millionths differently from run to run, so a long `TrajectoryTracker` export can find a different arc. [Vision](docs/vision/vision.md#frame-sources)

## [0.13.0] - 2026-10-02

### Changed

- **The inspector's number boxes step.** Holding Shift while dragging a value box used to move it ten times faster, which ran it to an end of its range in about 25 points; Shift now keeps the plain speed and lands on round marks (every 10 in a box from 10 to 375, every 0.1 from 0 to 1). A whole number drags at a speed that follows its range, so a count over 0 to 10,000 crosses it in the drag a slider takes instead of in thousands of points. While a box is being typed in, the up and down arrows step it to its next mark, with Shift and Option for bigger and smaller steps, starting from what has been typed. A whole number's minus and plus buttons repeat while held, and the line a save leaves under the button goes once a value is turned again. [Parameters](docs/helpers/parameters.md#scrubbing)

- **A color outside sRGB reads the components the system gives it.** `Color(displayP3:green:blue:)` stores a color as the sRGB components that mean the same color, and a component below zero went through the transfer curve's linear segment, so pure Display P3 red read (1.093, -0.544, -0.254). The curve is now mirrored about zero, the extended-range convention `NSColorSpace.extendedSRGB` uses, and the same red reads (1.093, -0.227, -0.150); `displayP3Components` still comes back to (1, 0, 0), and `Color(linear:)` and `linearRGB` round-trip on either side of zero. Only a component below zero changes, so a color inside sRGB is untouched, and a picture that names a color outside it and reads its components back sees the new numbers. [Color output](docs/drawing/coloroutput.md)

- **A generated project says sketch.** `ollin new`'s sample name is `MySketch` (it was `MyPiece`); a widget project's one public line is `Door.swift`, declaring `<Name>Door` (it was `Piece.swift` and `<Name>Piece`), and a project already generated keeps its own names; the generator's menu-bar kind is titled *Menu bar strip*; and the installation supervisor's log lines say "the sketch" where they said "the piece". [Project generator](docs/tools/projectgenerator.md), [Widget](docs/output/widget.md), [Installation](docs/output/installation.md)

- **Noise along one coordinate is a field of its own.** `noise(x)`, `signedNoise(x)`, `fbm(x)`, `ridgedFbm(x)` and `turbulence(x)` read the three-coordinate lattice along its x axis, where six of the sixteen gradient directions have no x component, so the curve left three eighths of its lattice points with no slope and ran dead flat across a whole cell wherever two such points met (a handheld camera's drift paused on them). They now read a one-dimensional gradient field seeded the same way, so the curve is never flat between two lattice points, and `noiseField()` and a formula's `noise` at y 0 and z 0 read the same field. The values change: a sketch reading one-coordinate noise gets a different curve of the same character under the same seed, and the two- and three-coordinate forms are untouched. [Noise](docs/generators/noise.md)

- **The extension starter's filter is `cornerShade(amount:)`.** `ollin new --kind extension --seam filter` wrote a `Filter.vignette(amount:)` beside the built-in `vignette(amount:radius:softness:)`, so the starter's own `.vignette()` call was ambiguous. It now writes `cornerShade(amount:)`, called as `.cornerShade()`; a package made from the old starter can rename its function the same way. [Extensions](docs/tools/extensions.md)

- **The Guide is forty-five chapters.** Four chapters split along their families: Automata from Simulations on a grid, Materials and surroundings from Meshes, maps, and materials (now Meshes and maps), The iPhone as a sensor from Depth and the iPhone as a sensor (now Depth), and Making it physical from Finishing a sketch. Every chapter from 24 on has a new number, so a link to a chapter file needs the new one: 24 is now 25, 25 and 26 are 26 and 27, 27 to 33 are 29 to 35, 34 to 38 are 37 to 41, and 39 to 41 are 43 to 45. One file has a new name as well as a new number, `35-Depth.md` (it was `33-DepthAndThePhone.md`), and the new chapters are `24-Automata.md`, `28-MaterialsAndSurroundings.md`, `36-ThePhoneAsASensor.md`, and `42-MakingItPhysical.md`. A link to a section that moved names its new chapter. [The Guide's contents](guide/index.md#contents) list them all.

- **The Guide's finished sketches compose more of their chapters.** Chapter 36 ends on a dancer read from the film that ships with Ollin and drawn in lit solids. Chapter 40's music box swings, follows a chord progression, and writes what it played as a MIDI file. Chapter 43's live-coded set saves looks as cues and calls one back. In eight chapters, the sections a finished sketch does not use now come after it, as families. [Guide](guide/index.md)

- **The credits carry the history the Guide tells.** `ATTRIBUTION.md` gains an entry for each chapter, naming the people, works, and dates behind its ideas with a source to read, and several chapters correct a date or a name against those sources. [Attribution](attribution.md#the-history-the-guide-tells)

- **The headless calls that hand back a picture throw an `ExportError`, where they answered `nil` and printed a line.** When the renderer would not start, most often because a shader stopped compiling, `OllinApp.image(of:)` returned `nil` and the reason went to the console, so code that rendered a frame could not tell a broken shader from a missing GPU. It now throws the same `ExportError` the exports throw, of kind `unrendered`, with the renderer's own message in its sentence and an empty `path`. **Breaking**: `image(of:)`, the four `contactSheet(of:)` forms, `plates(of:)`, and `separations(of:)` now throw and return a value that is no longer optional. A sweep over a parameter the sketch does not declare, an empty seed or value list, and a separation with no inks or no printing condition throw an `unsupported` one naming what to add, where they returned `nil`. In code, `if let frame = OllinApp.image(of: sketch)` becomes `let frame = try OllinApp.image(of: sketch)`, or `try?` to keep the old answer. `exportWidget` throws when no picture draws, where it wrote an empty folder. [Export](docs/output/export.md#when-an-export-fails)

### Added

- **The pitch wheel as a parameter.** `MIDIInput.bindPitchBend(to:channel:travel:)` drives a `@Param` from the wheel the way `bind(controlChange:to:)` drives one from a knob: at rest the parameter sits exactly at the middle of its range, and each end of the wheel reaches that end. `travel` is on the wheel's own scale, -1 fully down to 1 fully up, so `travel: 0...1` spends the whole range on an upward bend. `pitchBend(channel:)` reads the wheel on the same scale, 0 until it moves, and `unbindPitchBend(channel:)` lets go. This is the whole channel's wheel; on a per-note controller each note's bend is still in `heldNotes`. [MIDI](docs/integration/midi.md#binding-to-a-param)

- **A still sketch that answers, and knowing an export.** `redraw()` draws one more frame of a sketch that has stopped looping, so a `noLoop()` sketch can change what it shows on a click or a key and cost nothing between them. `isExporting` is true for the whole of any export (`setup()`, the frames run up to the first one written, and every written frame) and false in a window and under `--bench`, so a sketch that spreads slow work over live frames can do all of it at once when every frame is written. [Sketch](docs/core/sketch.md#redraw)

- **The pointer, the open panel, and the clipboard.** `pointerShape(_:)` gives the pointer over the canvas one of the system's shapes (`PointerShape`: crosshair, open and closed hands, and nine more), and `hidePointer()` / `showPointer()` take it away and bring it back, over the canvas only and apart from an installation's own hiding. `chooseFiles(withExtensions:allowsMultiple:)` shows the open panel without stopping the frames, and the pick arrives as `String` paths through `chosenFiles()` and the `filesChosen()` hook, the way a drop does. `copyFrame()` puts the frame the window shows on the clipboard as a PNG. [Input](docs/helpers/input.md#chooseFiles), [Recording](docs/output/recording.md#copyFrame)

- **Spherical coordinates.** `spherical(azimuth, elevation, radius, around:)` is `polar` in 3D, the angles first, in the camera's own orbit convention, and a `Vector3`'s `azimuth` and `elevation` read the angles back. [Math](docs/helpers/math.md#spherical)

- **Points inside a shape.** `randomPoints(in:count:)` scatters points evenly over a `Shape`'s fill, holes left out and every island given its share by area, drawn from the same triangles the fill is made of rather than thrown at the box around the shape; `randomPoints(along:count:)` scatters them along the outline, even by walked length; and `poissonDisk(in:radius:)` is the blue-noise form kept inside the shape, seeded from the fill's own triangles and restarted from a fresh seed when the darts run out, so a thin shape fills end to end and every island fills. All three run on the sketch's seeded `random`, and each wraps a free function that takes any random source. ([Geometry](docs/drawing/geometry.md#shape-points), [Blue noise](docs/generators/bluenoise.md#shape))

- **One mesh from several.** `Mesh.joined(_:)` lays parts end to end as one mesh with no boolean, and `placed(_:)` bakes a `MeshInstance` placement into a part first (positions by the matrix, normals by its inverse transpose, tangents with the surface, a tint into the vertex colors). The join runs the arithmetic `drawMesh` runs under the transform stack, so the result renders as its parts would drawn one by one, in one draw call; `union(_:)` stays the answer for a watertight print. ([3D](docs/3d/3d.md#join))

- **The metaball curve on its own.** `Metaballs.falloff(distanceSquared:radius:)` is the bump one ball is made of, in a sketch's terms (1 at the center, a half at the radius, 0 from twice the radius out), and `Metaballs.Ball.value(at:)` is one ball's own contribution, so a sketch shapes its own blobs, a rim that wobbles, a ball stretched along an axis, from the curve instead of a copy of its polynomial. ([Isosurfaces](docs/generators/isosurface.md#metaballs))

- **Three more filters.** `.channelMixer(_:)` runs a `ColorMatrix` over a layer, every channel of the result a weighted sum of the input's red, green, blue, and alpha plus an offset: `.gray` and `.gray(red:green:blue:)` for a monochrome mixed to a recipe, `.swapping(.red, .blue)` to trade two channels, or a matrix built row by row. `.lensDistortion(amount:quartic:center:fillsFrame:)` bends a layer into a barrel or a pincushion by the Brown-Conrady radial model, leaves what read past the edge transparent, and with `fillsFrame` scales the read so no border shows. `.cornerPin(topLeft:topRight:bottomRight:bottomLeft:)` lays a layer onto four points through the same map installation mode's projection runs, each corner defaulting to its own place so moving one is one argument. The identity of each changes no byte, and all three cross to the web page. [Effects](docs/drawing/effects.md#color-tone)

- **Motion for a mesh that changes shape.** `drawMesh(_:previous:)` takes last frame's vertex positions beside the mesh, so a ribbon rebuilt every frame, a marching-cubes surface, or a cloth is followed vertex by vertex: `temporalAntialiasing()` keeps its edges refined mid-motion and `motionBlur()` streaks it where it bent, not only where the camera moved. `withMotion { }` still covers a mesh that moves whole; inside such a block the previous positions use last frame's placement too, so a mesh that both moves and bends writes the sum. A count that does not match the mesh notes once and draws with transform-only motion. [3D](docs/3d/3d.md#drawmesh_previous)

- **A surface that glows in its own colors.** `MeshMaterial.emissiveIntensity`, set on a copy by `Mesh.glowing(_:)`, adds the surface's own color (the fill, the base color, the vertex colors, and the texture) as light of its own, so a vertex-colored mesh glows in each vertex's hue where `emissiveColor` tints the whole mesh one color. The two add. It works with no lights, traces in a path-traced export, and a spatial export writes it into the material's one emissive slot as the base color times the intensity. [3D](docs/3d/3d.md#meshglowing_-and-meshmaterialemissiveintensity)

- **The environments on a menu.** `Environment` conforms to `ParamChoices`, so `@Param var surroundings = Environment.studio` gives the inspector a pop-up menu of the twenty built-in environments. A `.sky(...)` or a built-in with `lightingOnly()` applied is off the menu and reads as the first entry; keep the parameter on the plain built-in and apply the modifier where the sketch uses it. [3D](docs/3d/3d.md#the-built-in-environments)

- **A radius per corner.** `drawRect(_:cornerRadii:)`, on every anchor `cornerRadius:` takes and on `drawRects`, rounds each corner of a rectangle by its own radius: `CornerRadii.top(20)` for a tab, `.bottom`, `.left`, and `.right` for the other pairs, or all four named. Two radii that add up past their side scale down together so the shape is kept. Four equal radii draw exactly as one radius does. The dashed outline, the hollow band, and the merged fields (`SDF.rect(width:height:cornerRadii:)`, `SDF.square(_:cornerRadii:)`) carry the four, and the SVG and PDF exports write the outline as a path with an arc at each rounded corner. [Drawing](docs/drawing/drawing.md#rect)

- **A gradient that sweeps around a point.** `Gradient.conic(center:startAngle:)` lays a ramp once around a center from an angle the sketch chooses (radians from three o'clock, clockwise, like every angle in Ollin), for a color wheel, a dial, or a pie. It paints fills and strokes on every shape, the tessellated shapes and outline text by vertex, and the merged fields by position, and it crosses to the web page. On the tessellated path a fill triangle that straddled the sweep's seam used to shade backwards through the whole ramp; it is now cut along the seam, which also mends an along-path fill. The vector exports, which have no sweep, write the ramp's midpoint color as they do for an along-path fill. [Color](docs/drawing/color.md#gradient)

- **A generator's plane as a value.** `ComplexPlane` is the framing every picture of the complex plane shares (`center` in the middle, `span` units across the shorter side, the imaginary axis up), and a generator's `plane` hands its own back, so `plane.canvasPoint(of: c, in: bounds)` marks a number on the picture and `plane.planePoint(at: mouse, in: bounds)` reads the number under the pointer without the sketch redoing the arithmetic. `ComplexPlane(center:zoom:)` takes a generator's `zoom`, where the span is 3 / zoom. [Effects](docs/drawing/effects.md#generate_-and-generator)

- **Putting a parameter back.** A parameter tuned in the inspector goes back to the value its `@Param` line declares without quitting the host: a dot after the name marks a tuned row and puts it back on a click, *Reset* is in every row's menu, a group card offers *Reset* for its rows, and *Reset all* sits beside the save button. The sketch keeps running, with no reload and no `setup()`, so the clock, the seed, and the canvas carry on, and a track on the timeline still wins its parameter. From code, `Param.reset()` and `Sketch.resetParameters(named:)`, and `declaredValue` reads what the line gave the parameter; the remote surface has a *Reset all* of its own (`RemoteReset` on the wire). A host that shows the parameters list passes a `ParamResetAction` as its new `reset:` argument (`ParametersListView(parameters:sketchName:onChange:save:reset:)`), which carries the tuned names and the call that puts values back. [Parameters](docs/helpers/parameters.md#reset)

- **Alma Thomas joins the recreations.** [`Examples/Recreations/AlmaThomas`](examples/recreations/almathomas/index.md) is new, after the paintings of 1966 to 1972 made of one stroke of a flat brush laid down and lifted, the next a little way on, the canvas showing between them. `Rings` lays the strokes around a core in rings, each color a dealt number of rows and the last running out to the corners, after *Resurrection* and *The Eclipse*; `Columns` lays them down ruled columns, in a disc of red and orange with a seam of yellow, in bands of one color after another, red on a dark ground, or blues with a patch of warm strokes at one corner, after *Snoopy Sees Earth Wrapped in Sunset*, *Earth Sermon*, *Mars Dust* and *Starry Night and the Astronauts*. Both are built on `strokeBrush`: a ring or a column is one path, the stroke is a block-shaped tip stamped along it, and the ground between strokes is the spacing, so `--export-svg` gives every stroke back as its own outline centered on its circle or column. Each is painted stroke by stroke over the first two thirds of its cycle, holds, and is taken back the way it came.

### Fixed

- **A stack of contour levels costs one march.** `isolines(at: levels, ...)` sampled the field once but marched every cell again for each level, about 0.8 ms a level on a 650-wide grid, so a contour map of a cheap field paid for every level in full. Each cell now visits only the levels that pass between its corners, and joining the crossings into contours is faster too: 28 levels of a smooth lookup went from 22 ms to 8.5 ms beside 2.4 ms of sampling, with the same contours. [Isolines](docs/generators/isolines.md)

- **A hole in a contoured field leaves the contour open.** A field that returned NaN or infinity next to a contour made `isolines` interpolate a crossing onto NaN and stop the program. A cell with a corner that is not finite now holds no contour, so the line ends at the hole's edge. [Isolines](docs/generators/isolines.md#levels)

- **Input after a live reload reaches the sketch that is drawing.** In OllinLive and OllinLiveCoding, a click, a key, a drop, or the pointer's position after an edit went to the first instance the window mounted rather than the one swapped in, so a mouse-driven sketch stopped answering after its first reload. The canvas now follows every swap.

- **A simulation's substeps cost what their kernels cost.** Every pass of a `Sim.shader` kernel, and every user shader, composed its full Metal source (the shared library spliced in, a few hundred kilobytes) and hashed it to find its compiled pipeline, on every encode: 17 ms a substep in a debug build, so a 25-substep field ran at two frames a second with the GPU idle, and `--bench` showed the time as end-to-end rather than GPU. The hash is now remembered by what the shader itself says (its text, its file, its modules, the variant and the output format), so a pass costs its draw; the same 25-substep field exports 60 frames in 2.5 s where it took 37. [Effects](docs/drawing/effects.md#simfield-shader)

- **A diffusion curve holds its far side.** Past the band of a two-sided `drawDiffusionCurve`, the region held by one side alone came back as the mean of both sides (a navy sea under a dusk sky rendered rust brown), at any `sharpness`. The solve ran a coarse-to-fine cascade of relaxation passes, which settles a picture's broad shape but, at every level where the curve's two sides share a texel, holds their mean as a wall and lets it leak across. `.diffuse` now runs multigrid V-cycles over that first guess: each level measures what it still gets wrong, the level below solves for the correction with the marks held as walls, and the correction is carried back up. Each side fills its region, and `sharpness` buys cycles, three at 0 and six at 1. [Effects](docs/drawing/effects.md)

- **A transparent pixel of a mark writes no depth.** Under `depth(at:)`, a circle, a rect, a glyph, an image, and a point-cloud splat wrote depth over their whole quad, the transparent corners and halo included, so a mark set behind another cut a straight edge out of it where nothing was drawn. A pixel with no coverage is now discarded before the depth write, so a mark hides only where its ink is. [Depth compositing](docs/3d/depthcompositing.md#depth)

- **A neighbor grid covers its bounds.** `SpatialHash`, and with it Particle Life, the Primordial Particle System, Particle Lenia, swarm chemistry, `Swarm`, `ParticleFluid` and `SoftBodies`, sized its world as a whole number of cells of the radius, so a radius that did not divide the bounds left the far edge outside the world: at a radius of 34 in a 253-wide panel the particles wrapped at 238, leaving a 15-pixel strip empty along the right and the bottom. The cells now stretch to cover the bounds exactly, each at least the radius wide so a search still reaches every neighbor, and the world is the bounds. [Compute](docs/shaders/compute.md#spatialhash)

- **A kite-and-dart tiling covers its patch.** `Penrose.tiles(.kitesAndDarts, in:tileEdge:)` split each half-kite into a piece that was no half-kite, so a third of the halves found no mirror partner and were dropped, and the tiling came back with bare patches through it (about 31% of a panel) and too few kites. The split is the classic one now, two half-kites and a half-dart, each tiling covers its patch once, the kites outnumber the darts by the golden ratio, and the arcs meet across every edge; a test that samples the patch's interior pins the coverage the edge test could not see. The rhombs were right all along. [Aperiodic tilings](docs/drawing/aperiodictilings.md)

- **A physics snapshot keeps a body's own mass and center of mass.** A body given a mass or a center of mass rather than one worked out from its collider, as a scene's `physics:mass` and `physics:centerOfMass` give them through the import, came back from `restore(_:)` at the mass its shape and density make, balanced at its middle. The snapshot now writes both, in format 6, and reads formats 4 and 5 as before. [Physics3D](docs/simulation/physics3d.md)

- **A small chance in a simulation is the chance it says.** The forest fire's lightning and growth, the Ising model's flips, and a Schelling agent's move compared a probability against a float hash that lands on exactly 0 about once in 1,800 rolls, so any chance below about 5e-4 fired at that floor instead. The forest fire's default lightning struck about 90 times too often, so the default ratio of growth to lightning was near 28 to 1 rather than 2,500 to 1, and a magnet below its critical temperature kept flipping spins it should have held. The rolls now draw on steps of 2^-24, so a lightning of 1e-4 lights 1e-4 of the forest and a cold magnet holds still. A run with these sims under a seed draws a different run from before. [Effects](docs/drawing/effects.md#simfield)

- **The bundled Hershey font's notice carries the acknowledgments its distribution requires.** The `.jhf` data is used under terms that ask two credits to travel with it, the original author's and the format's author's, and the notices called it public domain with no requirement. Both now appear as worded, in `THIRD-PARTY-NOTICES.md` and in the notice beside the font. [Notices](third-party-notices.md#hershey-fonts)

- **`--cue` finds a look saved beside a live sketch.** An OllinLive export given `--cue <name>` and no `--cues` looked for the cue in a sheet its export path never loaded, so a look saved from the inspector was reported missing. The export path now loads `Sketch.cues.json` from beside the sketch, as it loads the automation, and a `--cues` file still wins. [Cues](docs/helpers/cues.md)

- **A forest fire takes the trees you plant.** Drawing into a `.forestFire` field blended the mark in by its coverage, in linear light, so the mid-gray the reference names (`Color(white: 0.5)`) arrived a third of the way to a tree and planted nothing, and a rect's soft rim spilled trees into the cells beside it. A mark now snaps to the nearest of the three states in the gray the sketch wrote, and a rim under three-quarters coverage writes nothing, as Wireworld, Schelling's board, and the Ising model already did. A gray that planted trees before, from about 0.54 to 0.74, still does. [Effects](docs/drawing/effects.md#simfield)

- **Caustics hold still in an exported video.** An export traced a new set of photons at every frame and keeps no history to average them, so a slowly moving scene flickered where the caustic light fell, about 5% up and down from frame to frame, with the sparks changing shape each frame. An export now traces the same photons at every frame, so the caustic moves only as the glass and the light move. The live window is unchanged. A still export's caustic comes out a little different from before, since it was drawn with that frame's photons. [Caustics](docs/3d/caustics.md)

- Glass shows the scene behind it at the brightness that scene has. With `sceneThroughGlass()` on, the view through a transmissive surface was scaled by the environment's intensity and by the environment's own exposure, so clear glass over a lit floor passed about a fifth of the floor's light under an environment at 0.4 and half at 1. It now passes all but what its surface reflects away, about 96% for glass of index 1.5, whatever the environment. Sketches that use `sceneThroughGlass()` without ray tracing render brighter through their glass.

- Atlas text draws every glyph right on the frame that fills the font's atlas page. The page was cleared and refilled in the middle of that frame, so the glyphs drawn earlier in it pointed into a page that now held other glyphs, and they showed as the wrong characters for that one frame, again each time a long run of text in many scripts filled a page. A full page is now kept as it is and the atlas moves on to a fresh one, so text drawn before, a retained `Batch` that holds atlas text, and a web recording all keep the glyphs they were drawn with. A web export no longer stops when a font fills its page during the recording; each page crosses as its own picture.

- `surfacePoints` with the default `.blueNoise` scatter is about three times faster and returns the same points. Its thinning pass pushed a sample's weight into its queue a second time each time it passed over an outdated entry, so the queue filled with copies. Four hundred thousand points on a terrain took over ten minutes in a debug build, and now take three and a half.
- The Mandelbrot, Julia, and orbit-trap generators frame the plane with the imaginary axis up, as domain coloring, Newton's basins, the shader library's `complexPlane`, and the reference say; they read it down, and nothing said so. A Julia set with `c` off the real axis now draws mirrored left to right from before, and a Mandelbrot centered off the real axis top to bottom; the Guide's marked-`c` figure had marked the conjugate point.
- `ollin examples` describes every sketch in its own words: the picture grid at the top of a listing had renamed the first sketch of each grid row after its neighbors (`ImaginaryLog` read as "Meromorphic | ShaderBlend | ShaderFilter"). A search lists the sketches whose description matches beside the ones whose name does, where a name match used to hide them (`complex` now finds Meromorphic as well as ComplexPlane), and a sketch is found by the author its header credits (`turan` finds the three written after that article); the single-sketch view prints the credit.
- Command-Z and Shift-Command-Z no longer reach a sketch's `keyPressed()` when the host has nothing to undo.
- `TUIOReceiver` reads a real tracker's frame as one frame. TUIO sends each frame's alive list first, then the sets, then the frame number, and the receiver closed a frame on the first `set` after an alive list, so every frame from a tracker was taken twice: `framesReceived` counted double, a lifted finger could leave before the moves of its own frame arrived, and a late datagram's alive list got past the frame-number check, which could blink a finger out. A frame now closes on its frame number, or on the next alive list for a tracker that sends none, and a frame that sends its sets first is still read.
- The `MIDIFiles` example starts each bar's melody on the bar's downbeat. It restarted the arpeggiator on the downbeat itself, and a reset counts the step at its own position as played, so every bar's figure began an eighth late and the file ran an eighth past its eight bars. It now resets to the step before and reads each bar up to its last eighth.
- An image the GPU holds, such as a layer's `image` or a compute texture's, reads `.clear` at every pixel and ignores a write, as its documentation says. Its pixel subscript treated it as a picture in memory. A read set aside a buffer the size of the image, and a write was kept there and read back but never drawn.

## [0.12.0] - 2026-09-27

### Changed

- **The Guide is forty-one chapters in seven parts.** Nine chapters grew out of the longest ones: Curves and figures and Marks and media from Shapes as material, Pictures restyled and Pictures you solve from Layers and effects, Traced light from Sculpting with fields, Controls and signals from Sound and control (now Listening) and Pictures and data, Music by rule from Making sound, and Performing and Handing it over from Sharing and performing (now Finishing a sketch). Every chapter after 15 has a new number, so a link to a chapter file needs the new one: 16 is now 19, 17 is 18, 18 to 26 are 22 to 30, 27 is 33, 28 is 34 (`34-Listening.md`), 29 is 36, 30 is 32, 31 is 38 (`38-FinishingASketch.md`), and 32 is 41. [The Guide's contents](guide/index.md#contents) list them all.

- **Every export throws an `ExportError` when it cannot finish, where it stopped the process.** A frame the video encoder refused, a folder that was not there, or a file extension no writer takes each ended the whole program with a `fatalError`, so one refused frame in a test run took every other test in the process with it. Each export call on `OllinApp` now throws an `ExportError` naming the file, the frame it had reached, and what went wrong, and a clip stopped partway is removed rather than left short. Its `kind` says which of three things happened: the request could not be met (`unsupported`), a frame did not draw (`unrendered`), or the file would not take it (`unwritable`). Asking for zero frames, which returned quietly, is now `unsupported`. **Breaking**: `export`, `exportSequence`, `exportVideo`, `exportGIF`, `exportSpatialVideo`, `exportEXR`, `exportSVG`, `exportPDF`, `exportDXF`, `exportGCode`, `exportEmbroidery`, `exportSpatial`, `exportWeb`, `exportSeparations`, `exportPlates`, and the four `exportContactSheet` forms now throw, `exportHEIC` throws where it answered `nil` and returns a `StillExport` that is no longer optional, and `exportWidget` throws where it printed and returned what it managed. In code, `OllinApp.exportVideo(sketch, …)` becomes `try OllinApp.exportVideo(sketch, …)`. From the command line nothing changes but the message: a failed export flag prints the sentence and exits with status 1, as a refused web page already did. [Export](docs/output/export.md#when-an-export-fails)

- **`ollin api` says where a call may run.** The `API/` listings leave isolation out, so `ollin api Sketch` printed `open class Sketch` with nothing to say that a sketch lives on the main thread. A declaration its source isolates to an actor now shows it (`@MainActor open class Sketch`), read from its own attributes and never from an actor named in its parameters, and a `nonisolated` member keeps the word. [The reference offline](docs/tools/reference.md#a-name-and-what-it-takes)

- **Every loader throws when a file will not load, where it answered `nil`.** A missing file and a file in the wrong format used to come back as the same `nil`, so a sketch could not tell a typo from a broken file and nothing said which. Each loader now throws a new `FileError`: `missing` for a path or resource that is not there, `unreadable` for bytes that are not the format, with the file's path and a sentence (`photo.jpg: no such file`). **Breaking**, for every call below: `init?` is now `init … throws`, a `-> T?` is now `throws -> T`, and `Palette.palettes` and `loadPalettes` throw where they returned `[]`. The calls are `loadImage`, `loadMesh`, `loadScene`, `loadSVG`, `loadPalette`, `loadPalettes`, `loadTable`, `loadJSON`; the file, data, text, and resource initializers of `Image`, `Mesh` (with `objSource:`), `Scene`, `SVG`, `Palette`, `Table`, `JSON`, `IESProfile` (with `string:`), `ICCProfile`, `OutlineFont`, `BitmapFont` (`bdf`, `fnt`, and `resource:` forms), and `StrokeFont` (`jhf` and `resource:` forms); `ComputeKernel(entry:resource:withExtension:in:)` and `ComputeKernel(entry:contentsOf:)`; and `PhoneReference.picture(path:…)`, `picture(resource:…)`, `object(path:…)`, and `object(resource:…)`. The writers `Mesh.write(to:)`, `Scene.write(to:)`, `saveMesh`, and `saveScene` throw an `unwritable` one where they returned `false` and printed. `IESProfile` no longer prints a line on every refusal, since the sentence travels in the error. A name lookup (`OutlineFont(name:)`) still answers `nil`. In a sketch, `loadImage("photo.jpg")!` becomes `try! loadImage("photo.jpg")`, which stops naming the file and the reason, and `if let photo = loadImage(path)` becomes `if let photo = try? loadImage(path)`. [Sketch](docs/core/sketch.md#fileerror)

### Added

- **Every example is checked to draw the same picture twice.** `Scripts/check-determinism.sh` exports each example that runs on its own at the frame its still is taken at, twice under one `--seed` in two separate processes, and compares the pictures pixel for pixel; `Scripts/preflight.sh --milestone` runs it. [Why a run repeats](docs/concepts/determinism.md) lists what reaches past a seed: the system's dice, the clock on the wall, the order a `Set` walks, and live input.

- **An Ollin quick reference.** [`Docs/QuickReference.md`](docs/quickreference.md) is the framework in one read: a sketch's shape and its clock, the calls by area, a table of units and conventions (radians and which way they turn, the vertical field of view, how an orbit is measured, how colors are stored), `@Param`, the command line, a table of names from other frameworks, the mistakes that give a wrong picture with no error, and how to check your work.

- **`llms.txt` at the root of the checkout.** A map of the reference for an assistant helping somebody write a sketch: what Ollin is, the three commands that look a detail up, the quick reference to read first, then every Guide chapter and reference page in one line each, as paths in the checkout. `Scripts/llms.sh` writes it from the pages, and preflight fails when it falls behind them. [The reference offline](docs/tools/reference.md#for-an-assistant)

- **`ollin api <name>` says what a public name is.** It prints every declaration the name has as its source writes it, so a parameter's name carries its unit (`rotate(_ radians: Double)`) and a default shows its value. With each comes the comment written above it in the source and the file and line it is declared on. Then, for each type the name sits on, it lists the pages that document it and the shortest examples that use it. `ollin api Mesh.tube` picks one type's, `ollin api Param` lists every member of a type, and a name nothing is called lists the names spelled like it. A name that is not a sketch call leads with the sketch calls spelled like it, so `strokeWidth` points at `strokeWeight`. It reads the listings under `API/`, so a name it does not know is not public. See [The reference offline](docs/tools/reference.md#a-name-and-what-it-takes).

- **A second visit to Monir Shahroudy Farmanfarmaian.** [`Examples/Recreations/MonirFarmanfarmaian`](examples/recreations/monirfarmanfarmaian/index.md) gains two sketches. `Geometric` is after the felt-tip drawings on a grid of triangles (*Geometric*, 2013 and 2014): the sheet ruled with the grid in black and sparser colored lines, a hexagon at the middle with smaller ones inside it and every ring hatched parallel to its sides, closer toward the middle, and triangles filled with felt-tip in pairs that meet at a point, mirrored both ways across the sheet. Its SVG is a plotter drawing whose every corner is a point of the grid, or of the grid cut finer for the hatching. `Convertible` is after the *Convertibles* (2010 to 2016), with the modules after *Tir* (2015), whose six kites are the ones a hexagon falls into when it is cut from its center to the middle of every side. The sketch cuts each polygon of her family into its kites, searches for every way they can hang (the set turned about one point, every kite along another's edge, none overlapping), and moves the kites between those arrangements as rigid mirror pieces: off the wall and back on, or sliding along each other when a pinwheel only opens further.

- **A second visit to Vicente Rojo.** [`Examples/Recreations/VicenteRojo`](examples/recreations/vicenterojo/index.md) gains two sketches. `Senales` is after the *Señales* (1966-1972), read off *Señal sobre fondo violeta* at the Denver Art Museum, *Vieja señal # 2* at the Museo Blaisten, and the four in Memórica's exhibition: one sign after another, each painted over the last, standing on the canvas's middle line and built down it from a crown, a bar, a body, and sometimes a foot, every piece cut from the circle, the triangle, or the square. Some pieces are laid flat and some are worked (clouded, scumbled, scratched, and dripped) inside a clip of their own outline, a piece nested in one of its own kind takes the other finish, and the hand is the one thing on the canvas that is not mirrored. `PiramidesYVolcanes` is after the *Pirámides y volcanes* of the *Escenarios* and the four prints of *Prosa del Popocatépetl* (2003), with Sergio Pitol's line that Rojo "organizes an order only to knock it down later and raise it again": a stepped pyramid laid course by course and written over with rows of small signs, streams of lava breaking out of its steps as bands outlined once, twice, or three times, each allowed to rise and turn over once and after that only to fall, then the steps melting into lobes while the streams fill in, and the pyramid taken down from the top, a new print dealt every cycle. Both were checked on their SVG exports: every piece of a sign is a known cut and has its mirror image, the marks sit in clip groups that are the outlines of the worked pieces, and the PNG of the same seed reads every flat piece's color; the pyramid's courses are centered, flush, and stepped evenly, every sign sits inside its course, every strand of a stream is a closed outline around the stream's one middle line, and the colors count out their shares.

- **A second visit to Vladimir Bonačić.** [`Examples/Recreations/VladimirBonacic`](examples/recreations/vladimirbonacic/index.md) gains two sketches. `GFE164` is *Dynamic Object GF.E (16,4)* (1969 to 1971), the relief of 1,024 squares of colored glass at the ends of tubes of four lengths, built from the appendix of his 1974 article. His mask turns each square's number into one of sixteen classes: the class is the square's color and its tone, and two of its bits set the length of its tube. A square lights when a field element modulo x^16 + x^12 + x^3 + x + 1 and a four-bit plane register both say so, and a third register gates the clock. The sixty-four tones of his table sound with the lights, and the flat plan exports as SVG. `Random63` is *Random 63* (1969), his one work made from chance, beside the same sixty-three bulbs driven by a field. The places of the bulbs, read off a photograph, are exactly the powers of x modulo x^8 + x^5 + x^2 + x + 1, the only polynomial of degree eight that fits. With `sendsToWall` on, both panels go out over sACN through an `LEDMap`.

- **A second visit to Osamu Sato.** [`Examples/Recreations/OsamuSato`](examples/recreations/osamusato/index.md) gains two sketches from *The Art of Computer Designing* (1993). `StraightLines` is the eye of its "Straight Lines" chapter, captioned "An eyeball, created with nothing but straight lines", inside a word set in hatched letters, where every filled cell of a letter is three short horizontals. Every mark is one straight line and every angle a whole multiple of 3.75 degrees, the eye is mirrored both ways, and once a cycle the page is taken apart down to one line and built again. `word` sets any word you type. `TotemBuilder` is "a monster made of a circle": the pieces its "Circles" chapter takes apart (hands, feet, a tail, eyes, an antenna, a nose, a mouth, a neck, ears, a face, and a body), each in a few forms made only of disks, rings, crescents, and lenses. The seed deals one form of each, so every seed is a different monster, and `--export-grid` proofs a pile of them.

- **Waldemar Cordeiro joins the recreations.** [`Examples/Recreations/WaldemarCordeiro`](examples/recreations/waldemarcordeiro/index.md) is new: the folder's first artist from Brazil, and its first recreation that reads a photograph. `Derivadas` is after *Derivadas de uma imagem* (1969, with the physicist Giorgio Moscati), built from Moscati's own account of the program. A bundled photograph is cut into 98 by 112 blocks, and each block's mean tone becomes a level from 0 to 6. The levels are printed as characters whose ink rises with the level, the darkest struck two and three times on one place, the way the line printer struck a line again without moving the paper. Then the derivative is taken three times, each point the larger of its differences from the left and from above: four sheets, printed a line at a time and fed out of the printer. `Gente` is after *Gente* (1972-1973): a crowd read into levels and printed four times on continuous-form paper, at degrees of contrast 1, 2, 4 and 6. Every level is multiplied by the degree and held at the darkest, and the view steps in until the characters read and back out until the picture does. Both were checked against an independent reference that decodes the photograph the way Ollin does and recomputes every level. Every glyph in the SVG sits in one cell, each level is struck as one fixed combination, and the first sheet matches the reference in every cell. Every derivative and every degree of contrast matches its rule exactly, and in the PNG the ink rises with the level.

### Fixed

- **An example's page shows one piece in its clip and in its picture.** The clip and the still are two renders, and a sketch that rolls its variation dealt a new piece for each, so 85 pages played one piece under a poster of another. Every render now draws under one seed and those 85 are rendered again, with two more whose picture was paler than their clip because the page's JPEG dropped a canvas's alpha. Eight examples have media for the first time: seven that play the far end of their own wire (the DMX, MIDI, and OSC loopbacks, the TUIO stand-in, the laser preview, `Audio/Expression`, and Nick Cave's `Soundsuit`), and `Shapes/Watercolor`, drawn at its second frame since each frame takes it seconds. [Examples](examples/index.md)

- **A chaotic GPU simulation exports the same frames under the same seed.** Particle Life, the Primordial Particle System, Particle Lenia, swarm chemistry, `Swarm`, `ParticleFluid`, `SoftBodies`, and any kernel of your own over a `SpatialHash` summed each particle's neighbors in whatever order the GPU's threads reached the neighbor search. A chaotic system grows that last-bit rounding into a different picture within seconds, so two exports of one seed differed in up to 78 percent of their pixels by the eighth second. An export, or a take playing back, now lists each cell's particles in index order, so the run repeats on the same machine. A live window keeps the faster order: the extra pass costs a clumping sim about a fifth of its step, and a clock that follows the wall never repeats a run anyway. [Artificial life](docs/simulation/artificiallife.md)

- **Caustics export the same pixels every time.** The photon trace handed out places in the splat through an atomic counter, so the photons were added in a different order each run and rounded a level or two apart in a few dozen pixels. Every trace thread now writes its own place, and the splat draws them in order. [Caustics](docs/3d/caustics.md)

- **A `MeshField` exports the same pixels every time.** The cull gathered the visible copies in whatever order the GPU's threads arrived, and where two copies meet at exactly the same depth the one drawn first wins, so the field example came out three ways in four renders. An export now gathers them in the order they were placed, and a culled export matches an unculled one to the pixel. A live frame keeps the one-pass gather. [Instancing](docs/3d/instancing.md#meshfield)

- **`--photo` keeps every camera closed.** The flag reached only `Camera.orStill`, so a sketch that started a `Camera` itself opened this machine's camera even under it. `Camera.start()` now throws under `--photo` before touching the device. The four examples that started one directly (`LiveSurface`, `LiveEnvironment`, `DepthCloud`, `BarcodeReader`) take a bundled picture there instead, so their stills show the technique rather than the room; the barcode reader reads a QR code it makes for itself. [Vision](docs/vision/vision.md)

- **A glow or blur radius under a pixel is noted, since it spreads nothing.** The radius of `.bloom`, `.halation` and `.gaussianBlur` is a pixel count, and a share of the canvas written there (`radius: 0.012`) blurs by less than a pixel, so the glow a sketch asked for never leaves the mark it comes from. Four examples shipped that way (`DynamicObject`, `NamaFrieze`, `LensFlare`, `SceneExplorer`) and now give their radius in pixels. A sketch that writes one hears about it once, from `postProcess` or `filtered` alike, and the reference now says the unit. [Effects](docs/drawing/effects.md#blur-glow)

- **A feedback layer, an accumulator, or a simulation field is let go when the sketch lets go of it.** Each one held its own drawing surface, and the surface held it back, so neither was ever freed. Its storage on the GPU stayed too, since the renderer drops a layer's textures only once the layer is gone. A sketch that made one in `setup()` lost it once per live reload, and one that remade a layer while it ran lost one every time. `leaks` found the cycle on an export of the feedback example. [Effects](docs/drawing/effects.md)

- **Glyph-atlas text no longer crashes when it fills the atlas.** Atlas text (`textMode(.atlas)`) keeps its glyphs on one page and starts a fresh page when that one fills. A glyph that filled the page for a font already on it crashed with an index out of range, so text with more distinct characters than a page holds (a long run of Chinese or Japanese, or a ticker over many hours) would stop the sketch. [Text](docs/drawing/text.md#textmode)

- **An inline compute kernel no longer rebuilds its source on every dispatch.** A kernel written in a sketch is meant to be found again by its own text, but the check for that could never match, so every dispatch of every frame read the shader library, resolved its includes, and hashed the whole text before finding the pipeline it already had. An `Accumulator`, a particle system, and a simulation each paid it, about a fifth of a small frame in a long-run test. [Compute](docs/shaders/compute.md#the-kernel-snippet)

- **A sea state that changes every frame no longer grows a cache every frame.** An ocean whose wind or height follows the clock added an entry a frame to the amplitudes a sketch keeps, for as long as it ran. The cache now holds its most recent entries only. The path-traced export's environment tables are bounded the same way, for an environment that changes every frame. [The ocean](docs/3d/ocean.md)

- **A sketch window opened while the display is asleep draws once the display wakes.** A window that came up with the lid closed and the monitor asleep, as when another program or session launches a sketch while you are away, stayed blank for good: its frame timer was made with no display awake and never fired, and nothing made it again. The canvas now rebuilds the timer when the displays wake, when the display setup changes, and when its window moves to another screen or comes into view; a paused sketch stays paused. A sketch that was already running when the display slept was never affected.

- **Text set a character at a time in the atlas mode is one draw call, not one per character.** Every `drawText` under `textMode(.atlas)` opened its own run of glyph quads, so a sketch that placed each character in a cell of its own paid a draw call per character, live and again in every replay of a `Batch`. Calls in a row with the same font, blend and target, and nothing else drawn between them, now share one run; color and size are stored in each quad as before. Four sheets of 16,000 cells each went from 125 ms a frame to 11 ms, with every exported frame identical. [Text](docs/drawing/text.md#textmode)

- **`--skip` builds a picture that carries from frame to frame.** The warmup ran the sketch's `draw()` but rendered only a `noClear` canvas, so a `Feedback` pile, an `Accumulator`, a `LineSpray`'s running mean, or a simulation field started the first written frame empty while the sketch's own state was `--skip` seconds on. On a 30-second lap whose frame 1801 should match frame 1, the two came in at 8.6 dB. Warmup frames now render whenever the frame holds such a layer, in the sequence and video exports and the spatial video alike, and the first frame after a skip is the frame a run from zero reaches at that moment. [Export](docs/output/export.md)

- **Every tile of a contact sheet draws its feedback layer.** In `--export-grid` and `--export-sweep`, a sketch with a `Feedback` layer drew its first two tiles and left every tile after them black. Each tile is a fresh sketch at the same frame on one renderer, and the renderer, which keeps a layer from stepping twice when one frame is rendered twice, knew a frame by its drawer's address. A tile's drawer could be handed the address of the tile freed before it and pass for a repeat, and its layer served empty textures. Drawers now carry a serial number of their own. [Variations](docs/core/variations.md)

- **A frame with millions of marks in one pass draws them.** A `LineSpray` past about ten million points a frame, or any pass with that many particles, laid down nothing at all, with nothing said. The GPU bins a pass's geometry before it shades a tile, and a pass holding more than its binning memory takes has to be drawn in parts. Its multisample attachments lived only in tile memory, which cannot be stored between parts, so the GPU refused the whole frame. A pass past a million triangles now draws on attachments that can be stored: the same pixels, and the same GPU time below the edge. Twenty million points land where 9.8 million drew nothing. A frame the GPU refuses for any reason now says so on standard error, once for each different reason. [Depth of field from light](docs/drawing/depthoffield.md)

- **`--param` wins over `--cue`.** The cue landed after `--param`, so a value given by hand beside a cue was overwritten by the cue's, where the pages said the hand wins. The cue now lands first and `--param` on top of it, and the cue lands before `setup()` too, as `--param` does, so whatever `setup()` builds from a parameter reads the cue's value. [Cues](docs/helpers/cues.md)

- **`ollin docs`, `ollin examples` and `ollin api` answer in a tenth of a second.** Each lookup used to go through `swift run`, which spent about four seconds asking whether anything had changed and held the checkout's build lock while it asked, so a lookup waited out any build already running there. The command now builds the reader once after its own code changes and runs it straight from the build after that. If that build fails on a checkout caught mid-edit, it answers from the last build that worked and says so. [The reference offline](docs/tools/reference.md)

- **A file you are handed can no longer end the sketch that reads it.** Every reader of a file now runs under the harness the wire readers got in 0.11.0, with two more checks: a count a file declares is checked against the bytes the file holds before anything is set aside for it, and an input nested past a reader's limit is refused or read to the limit instead of running the stack out. What that fixed, by reader: a [`.cube` look](docs/drawing/looks.md) declaring a 256-node cube no longer sets aside 265 MB before its first line; an [ASE palette](docs/drawing/color.md) swatch whose numbers are not numbers is skipped (it used to end `Color(hue:)`, which now reads a hue that is not a number as red); an [SVG](docs/drawing/svg.md) with an arc or a transform past what a `Double` holds draws a line where it used to stop the flattener; [glTF](docs/3d/scenes.md) accessors with negative indices, overflowing offsets, or triangles naming vertices the mesh lacks are refused or dropped, a node listed under itself is read once, and a buffer or picture the file names is read only when it is an ordinary file; [USD](docs/3d/scenes.md) text with an integer past 64 bits, a crate or package declaring more than its bytes can hold, and a negative face count are refused; a PLY whose faces name missing vertices and a binary STL whose triangle count its size cannot hold no longer crash Model I/O; the [audio loaders](docs/helpers/audio.md) read a file a stretch at a time and refuse a sample rate no sound plays at, where a header promising billions of frames used to allocate them or trap; [SFZ](docs/helpers/synthesis.md) keys, tunings, levels and loop points are held to the format's ranges; [physics snapshots](docs/simulation/physics3d.md) refuse numbers a world does not hold, a payload declaring more than its bytes can unpack to, and indices naming nothing, and a vehicle that cannot be built no longer destroys a body the solver never made; `JSON`'s `int` reads `nil` for a number no `Int` holds; `noise()` at a coordinate past 2^63 lands on the lattice instead of trapping. **New limits**: a scene nests at most 64 levels deep in glTF and USD (every walk of a scene recurses once per level, and 64 is the depth a background thread's stack holds in a debug build), and a [formula](docs/helpers/formula.md) is at most 1,024 numbers, names, and signs, nested at most 64 deep. Loading a glTF or USD scene on a background thread no longer overflows at 40 or 26 levels.

- **A scene nested to the depth limit reads on a background thread with room to spare.** `Scene.bounds` took about 6.6 KB of stack a level in a debug build, so a glTF or USD scene 64 levels deep, the most either reader takes, left a background thread's half-megabyte stack little to spare, and less under a compiler that lays out larger frames. It and merging a USD scene into one mesh now keep the nodes still to visit on a list rather than on the call stack. [Scenes](docs/3d/scenes.md#loading)

- **An audio file that declares tens of thousands of channels is refused rather than ending the sketch.** A WAV keeps its channel count in two bytes, so a damaged one can claim 32,768 channels, and on macOS 26 the system's buffer for that many throws an exception no Swift code can catch, which stopped the process. Every audio loader now refuses a file past 1,024 channels (the most the system's reader converts) before a buffer is made, with `.couldNotDecode` from `AudioPlayer`, and reads a many-channel file in stretches of a few megabytes. [Audio](docs/helpers/audio.md)

- **An SFZ map written in Latin-1 loads.** The reader meant to fall back to Latin-1 when a map is not UTF-8, which is how older libraries spell an accented sample name, but the fallback was never reached, so `SampledInstrument(contentsOf:)` threw `.couldNotDecode` for such a map. It reads now. [Synthesis](docs/helpers/synthesis.md#sampled-instruments)

- **Typing a number into an inspector box does what you typed.** The box used to clamp each key as it arrived, so a first "2" in a box running from 10 to 375 became "10.00" and the next keys piled onto it, "300" over 50...500 came out as 500, a maximum typed through a value under the minimum moved the minimum, and Return rounded a value to two decimals (0.007 in 0...0.01 became 0.01). The save button then wrote those values into the sketch. Now the text is yours until Return, Tab or a click away commits it, clamped; a number already in range shows on the canvas as you type; Escape puts the starting value back; and a min/max end committed past the other pushes it along. Boxes show a value's own digits without trailing zeros or a thousands separator (`175`, `0.004`, `25000`), and a slider, scrub or pad drag lands on round values (`2.4`, not `2.4027931415929205`), which is also what Save writes. [Parameters](docs/helpers/parameters.md#where-the-controls-appear)

## [0.11.0] - 2026-09-23

### Added

- **Mohamed Melehi joins the recreations.** `Examples/Recreations/MohamedMelehi` is new, after the hard-edge wave paintings of the 1970s, the folder's first artist from Africa. `Waves` sends one ribbon of flat bands across the panel: it enters straight and breaks into the wave part of the way over, every band edge is one curve moved over by the band's width so neighbors share their edge point for point, the palette hands its colors out by declared shares in its own order, and the wave travels because the phase is the clock, one wavelength per cycle, which is the export loop. `Flamme` stands the wave up: a row of pink and navy bands rises from the bottom edge, each cut flat at its own height, the tops stepping up to a peak and down again so the row reads as one flame, every top breathing within the steps to its neighbors, a straight ribbon of yellow, orange and blue crossing behind. Both export every band as one closed outline. Read off the SVG: every band's two sides differ by one constant vector of exactly one band width across the run, neighbors' shared edges are written identically, each edge fits the wave on the clock's phase to the file's precision, and the fills count out the shares; read off the PNG, every pixel along a shared edge is a blend of its two fills and never the ground.

- **The handoffs run under Thread Sanitizer.** `Scripts/test.sh tsan` builds the package a second time with the sanitizer, under `.build/tsan`, and runs every target whose tests run without the GPU: the wires, the phone's codecs and readers, the audio engine, the mutation harness, and the feeds, where a producer on a thread of its own hands a sketch what it read through a lock, a convention no tool had checked before. Any report fails the run and the reports print after it; one class of report, an ordering that runs through a system framework the sanitizer cannot see into, is excused per entry in `Scripts/tsan-suppressions.txt`; `Scripts/preflight.sh --milestone` runs it. `Tests/README.md` says what is in, what stays out and why, and what a run costs. The committed tree reports nothing. [The test suite](https://github.com/eaviles/Ollin/blob/main/Tests/README.md)

- **A second visit to Gego.** `Examples/Recreations/Gego` gains two sketches. `Tejedura` is after the *Tejeduras* (1988 to 1991), the woven sheets of her last years: a print of one of her nets is pulled twice, one impression kept whole and the other cut into warp strips of uneven width and woven with strips of cigarette-packet foil, weft over warp, the plain rhythm broken where a strip floats over two or three crossings, and every cell of the weave showing whichever strip is on top there, so the print goes on reading through the foil. `Chorro` is after the *Chorros* (1970 to 1971): falls of rods hooked end to end, each rod a rigid body on a ball joint, hung from a plate the air moves, tied into rings and triangles on the way down, and let down to the floor, where whatever is left over lies out. Drag a rod to take hold of it.

- **Kuramoto oscillators couple over a lattice or a graph, and say where they have locked.** `Kuramoto(columns:rows:layout:wraps:)` lays a crowd on a square (`.square(.vonNeumann)` or `.square(.moore)`) or hex (`.hex`) lattice, each oscillator pulled by the sites within `range` rings of its own, with `wraps` for a torus; the hex lattice shares `HexGrid`'s addressing, so the cell `hexGrid(columns:rows:)[i]` draws is site `i`. `Kuramoto(neighbors:)` couples over any list of neighbor sites, and the settable `neighbors` hands any crowd a graph. `localCoherence` is the order parameter over each oscillator's own neighborhood, the map of where the crowd has locked. A crowd can hold several `layers` of oscillators per site, each layer coupled over the same neighborhoods and a site's layers pulled together by `layerCoupling`. `rates` is every oscillator's rate at the last substep, the natural pace plus the pull, for a motion blur or a trail of whatever the crowd drives. The ring and the mean field step exactly as before. `Docs/Simulation/Oscillators.md`; the `Simulation/Kuramoto` example gains a honeycomb field (issue #25). [Oscillators](docs/simulation/oscillators.md)

- **A second visit to Hassan Sharif.** `Examples/Recreations/HassanSharif` gains two sketches. `OctoberLines` is after *10th to 13th October No. 1 & No. 2* (1984), the semi-system whose rule is all on its second sheet: a table of two-digit numbers, a wavy line that cuts it, the numbers on the line's left summed digit by digit, the repeats dropped, and the sums ruled as bands of lines, straight and wavy in turn, as many lines as the sum; here chance draws the cut, the sheet writes the list and the red box, and the pen rules the bands on the sheet and on the wall beside it. `BodyAndSquares` is after the 1983 performance of that name: a grid of twenty-five squares the size of a body, five squares picked by chance for the head, hands and feet, the body relaxed onto them along fixed bone lengths, a pick it cannot reach struck through and picked again, and every square the body lies across filled in on the record beside a print of the position.

- **Nasreen Mohamedi joins the recreations.** `Examples/Recreations/NasreenMohamedi` is new, after the untitled ink and graphite drawings of the 1970s and of about 1980, the folder's first artist from South Asia. `Registers` rules a square sheet edge to edge: heavy lines cut the field into registers, each register holds a run of fine lines whose intervals are a geometric progression, so the lines gather against one heavy line and open toward the other, and a line's weight follows its interval; the upper half is doubled on the right, a few verticals rise from the bottom edge, pencil guides stand under everything, and over one cycle each register's ratio drifts so the lines tighten and relax while the heavy lines hold still. `Diagonals` floats a figure on a wide sheet: one to four chevrons along a slant, each one stroke whose width is full at the corner and thins to the tip of each arm through a stroke profile, with a wake of hairlines parallel to the long arm laid along the short one at growing intervals, and a fan of hairlines from stations on the short arm toward one point well off the sheet; over the cycle the figure slides a little along its axis, the fans sweep and the wakes breathe. Both export for a pen: every hairline as a line, and a chevron as the outline of the region its stroke covers, so the thinning survives on paper.

- **A second visit to Manuel Felguérez.** `Examples/Recreations/ManuelFelguerez` gains two sketches, after *El espacio múltiple* (1973) and the lacquered reliefs that followed the aesthetic machine. `EspacioMultiple` builds one form-idea from a square with rounded corners, a rectangle, a disk, a half disk and a right triangle, and carries it through its three states: the flat painting in a silver or a gold scheme, the relief the planes rise into under a light that rakes across the wall, and the standing piece the relief comes apart into, where the sheet becomes the floor, the band a shelf through the square and the displacement depth; a new combination of colors, displacement, rows and heights arrives with each cycle, and the cycle is the export loop. `stage` holds one state still, `unlit` shows that the relief seen straight on is the painting again, and `--export-svg` gives the plan. `RelieveLacado` composes and weighs a design exactly as `MaquinaEstetica` does, so the same seed is the same design in both, and raises it in lacquered layers, the largest element lowest and the pen's outline as a plate of aluminum, under a key light that circles slowly; `view` walks round it, sees it straight on, or shows the plan.

### Fixed

- **The accumulation page says where a long sum stops.** It promised that a faint sample on a `noClear` canvas always adds up, but the surface is half float: a sample under about one part in a thousand of what the pixel already holds stops counting, so a long exposure of faint strokes stops brightening. The page now says so and sends a long sum to `makeFeedback(precision: .float32)`. [Accumulation](docs/drawing/accumulation.md)

- **The API diff reads a conformance to the standard library.** A type that became a `Collection` and dropped its own `count`, `first` and `isEmpty` read as three removals and a rename nobody made (`count` is `endIndex`), a major and three shim violations from 1.0 on, though every call site still compiled. `Scripts/api-diff.sh` now reads such a member as moved onto the protocol that still supplies it, from a table of what `Sequence`, the `Collection` family, `Equatable`, `Hashable`, `Comparable`, `Identifiable`, `CustomStringConvertible` and the string literals give a conforming type, and its self-test compiles a call against every line of that table. The report's rename count no longer counts a type that only gained conformances. [The API listings](https://github.com/eaviles/Ollin/blob/main/API/README.md)

- **A still counts its frame at the rate `--fps` names.** `--export`, `--export-exr`, `--export-plates` and the vector exports read `--frame` and never `--fps`, so `--frame 30 --fps 10` was drawn at half a second rather than three, and a still never matched the sequence it was meant to sit in at any rate but 60. Every one-frame export now reads both flags the way the sequence does. [Export](docs/output/export.md)

- **An extension's `afterFrame` fires in an export.** The headless drives never called it, so `FrameInfo.profile` could be read from a window and never from the run that mattered. It now fires once per rendered frame, with the export clock's rate and step and the frame's own unsmoothed times, the renderer timing its encode, GPU run and wait on the headless path too. See [Profiling](docs/tools/profiling.md).

- **A `--param` value is there when `setup()` runs.** It landed only after `setup()`, so a count built there, or an extension registered behind a switch, read the declared default. It is now applied before `setup()` and again after it, so `setup()` reads the run's value and a `setup()` that assigns the parameter itself still does not undo it. The canvas follows: the value is on the sketch before `canvasSize` is read, so a `canvasSize` that picks its size by a parameter exports and opens at the run's size, where it took the declared default. [Parameters](docs/helpers/parameters.md)

- **A stranger's packet no longer ends the sketch.** Every decoder that reads a network or a cable now runs under a mutation harness in the test suite (truncations, bit flips, splices, length fields pushed to their extremes, numbers written as text), and what it found is fixed. A Firmata board sending a one-byte command, a program change, took the parser past its data. An OSC float or double too large for an `Int`, an MQTT payload reading `1e300`, a room value carrying the same number, and a stepper or menu index sent over OSCQuery or from the remote page each trapped on the narrowing, and now read as `nil` or are ignored like a value of the wrong kind. A WebSocket frame announcing a length one short of what an `Int` holds overflowed the check that read it, and a frame past 1 MiB now drops the connection instead. A Link peer announcing a beat or a time past two to the fiftieth overflowed the tempo math on joining, and the entry is now skipped. A depth, matte, saliency or flow map from the phone whose width times height overflows is refused at the wire. A table cell reading `1e300` trapped the same way through `int`. [The test suite](https://github.com/eaviles/Ollin/blob/main/Tests/README.md)

- **The aesthetic machine weighed a quarter disk off center, and drew its ring as a cut disk.** `MaquinaEstetica` placed a wedge's weight at 0.3 of its radius from the corner rather than 4 / 3π, so a design with a wedge was kept or refused against the wrong center, and it drew its quarter ring through `drawPolygon`, which fans a concave outline, so the ring's inner arc came out as a straight chord. Both are fixed, and the sheets a seed produces change wherever a wedge or a ring appears.

- **A line spray whose scene changes size no longer remakes its particles every frame.** Under `.byLength` sampling, lines that stretch or surfaces that breathe move the pass's total by a few points each frame, and the buffer all the points land in was sized to that total exactly, so it was thrown away and made again every frame: at six million points, 576 MB of new memory a frame, enough to stall the desktop under a live window. The particles are now kept and only grown, with room for the total to drift. A scene that only turns never saw it, since its lengths hold. See [Depth of field from light](docs/drawing/depthoffield.md).

- **The lens flare's ghosts were in the wrong places, at the wrong sizes, since it shipped.** The matrices that place a ghost went wrong on the leg between its two reflections: they passed *through* the inner reflecting surface before reflecting off it, and they mixed two sign conventions for a ray running back toward the front. Every ghost came out misplaced, missized, and so misweighted in brightness, and nearly all of them landed on the light's own side of the frame, which is why the flare never looked like a chain through the middle. The angle the coating was read at also had the wrong sign on a surface's lean. The only check the matrices had was the straight-through path, which has no reflection in it. They are now checked against real rays followed through the same glass, to parts in a million, for every ghost of both bundled lenses. Expect a flare that looks different: ghosts on both sides of the frame's center, in a sane spread of brightness. The ghost polygons also line up with the star's arms now, where they used to sit turned by an angle that changed with `starSize`. [Lens flare](docs/3d/lensflare.md)

### Changed

- **One shape per kind of failure, first half.** A failure a call can see for itself throws, and one that happens later is readable afterwards; several kinds took two shapes, and each is one now. `VideoPlayer(url:)` throws `VideoError.fileNotFound` for a file that is not there, as the `path:` and `resource:` doors already did, and a file that opens but is not a movie says so in the new `VideoPlayer.unavailableReason` instead of never producing a frame. `Record3DRecording.pose(at:)` throws `frameOutOfRange` outside the recording, as `frame(at:)` does, and is `nil` only for a frame with no recorded pose. `ImpulseResponse.load(_:)`, `.resource(_:withExtension:in:)` and `init(contentsOf:)`, `GrainSource(named:withExtension:in:)` and `(contentsOf:)`, and `SampledInstrument(sfz:in:)` and `(contentsOf:)` throw `AudioError` (`resourceNotFound`, `couldNotDecode`, and the new `empty`) where they returned `nil` beside a printed note. An audio engine that will not start is written to a new `unavailableReason` on `Synth`, `Tone` and `AudioPlayer`, cleared by the next start that succeeds, where `Synth.start()` printed and `Tone.play()` and `AudioPlayer.play()` swallowed it; `AudioInput.start()` still throws for what it can see and now writes `unavailableReason` for a refused, denied or restricted microphone, which used to leave it silent. `PhysicsSnapshot(data:)` and `(resource:withExtension:in:)` throw `PhysicsSnapshot.Failure` (`unreadable`, and the new `resourceNotFound`, each printing as its sentence) where they returned `nil`; `World3D.restore(_:)` throws `unreadable` where it printed, and `World3D.load(contentsOf:)` throws where it returned `false`. `World3D.addVehicle`, `addRagdoll(from:)`, `addSoftBody(from:)`, `addRope(through:)` and `addTensegrity(_:)` throw the new `PhysicsError.unbuildable`, whose text says what was needed, where they returned `nil` and printed it. A sketch that stored one of these in an optional writes `try?` at the call and keeps its shape. [Video](docs/video/video.md), [Audio](docs/helpers/audio.md), [Synthesis](docs/helpers/synthesis.md), [3D physics](docs/simulation/physics3d.md), [Record3D](docs/3d/record3d.md)

- **One shape per failure, second half: the failures that had no shape, and the errors nothing could reach.** `SerialPort` and `FirmataBoard` gain `lastError`, the sentence `open()` is waiting on (`no device at /dev/cu.usbmodem1101`, a port another program holds, `the device went away`), cleared the moment the port opens, and a device that went away keeps that sentence while the retries only confirm the absence, a retry that fails some other way replacing it; a wrong path, a missing device and a device that went away used to be one silent state behind `isOpen`. `SyphonClient` gains `unavailableReason` for a source that was not there when it looked, no Metal device, a refused connection, or a source that has since gone away. `LinkClock` gains `interfaceCount` and `unavailableReason`, so a running clock that could open no network interface says so instead of reading exactly like one alone on the network. `OSCQueryServer` and `RemoteInspector` gain `isRunning`, `unavailableReason` and a `start()` that serves again after `stop()` or tries the port again after a failed start; a port another program holds used to be a printed line and a listener that could never be started again, and `RemoteInspector.stop()` no longer leaves `boundPort` set. `VirtualCameraError` is internal, since no public call ever produced it; `VirtualCameraServer.unavailableReason` carried its sentences already. The five `Error` enums nested in `ModelTracker`, `PointSegmenter`, `ConceptTracker`, `DepthTracker` and `DepthClip` are one `VisionError` with `unavailable` and the new `failed`; a `catch` naming one of the old types names `VisionError` now. `PersonSegmenter.detect(in:quality:)` and `SaliencyTracker.detect(in:mode:)` return their value rather than an Optional and throw `VisionError.failed` when a matte or heat map cannot be converted, where a conversion failure used to come back as `nil`; `SubjectSegmenter.detect(in:)` is `nil` only when no subject is found. The depth tracker no longer drops its own model's failure on the floor: a model that answers no depth map lands in `unavailableReason`. [Serial](docs/integration/serial.md), [Syphon](docs/integration/syphon.md), [Link](docs/integration/link.md), [OSCQuery](docs/integration/oscquery.md), [Remote](docs/integration/remote.md), [Vision](docs/vision/vision.md)

- **A number that does not fit reads as `nil`.** `OSCArgument.int`, `MQTTMessage.int` and `int(named:)`, `RoomMessage.int`, and a table's `int(_:)` return `nil` for a number that is not finite or is too large for an `Int`, where they trapped before; a parameter restored from a stored `.number` of that size keeps its value. [OSC](docs/integration/osc.md), [MQTT](docs/integration/mqtt.md), [Room](docs/integration/room.md)

- **The lens flare is followed ray by ray.** A ghost used to be a flat, evenly lit copy of the iris in one color, worked out to first order. Each ghost is now a grid of real rays carried through the lens's actual spherical surfaces, so it comes out the way glass makes it: its sides stretched and leaning, more the further the light sits from the middle; cut by the round barrel wherever a ray strays past the rim of an element; brighter along a rim or in a core where neighboring rays land together, which is a caustic; and lit by a coating read at the angle each ray really met it at. A ghost bright enough for it to show is followed in seven wavelengths, each through the glass that color sees (`LensInterface.abbeNumber`, with the numbers the patents print on both bundled lenses), so its colors part smoothly at the rim. From a small source the light rings just inside each edge. `LensFlare.sourceSize` now sets how soft every ghost is, and a ghost near focus seen from a wide source becomes a small soft picture of it, round for a round source whatever the iris's shape. The star gains `LensFlare.wear`, the wear on the opening: uneven and bowed blades split and fray the arms, and specks and scratches fill the space between them with fine needles. `Lens.doubleGauss` is a second bundled lens, a 1950s six-element double Gauss whose twenty ghosts make the chain a flare is known for. Three extras are off until asked for: `LensFlare.streak` (with `streakLength`, `streakAngle`, `streakTint`) is the long thin line cylindrical glass fans a light into, the one an anamorphic lens or a streak filter throws; `LensFlare.dirt` is grime on the front element, each speck a soft picture of the iris that glows where it sits near the light; and `LensFlare.halo` (with `haloSize`) is the rainbow ring flare artwork draws around a light, which is a look and not optics, and says so. `Lens.anamorphic(squeeze:)` puts an anamorphic front group on a lens: the ghosts, the star, and the halo stretch sideways by the squeeze, while a ghost's edge stays as soft across as up and a ghost near focus stays a round picture of a round source; the streak stays a line and the dirt on the front keeps its shape. **What moves in an existing sketch:** `lensFlare()` with no lens named now uses `Lens.standard`, the double Gauss multicoated at f/8, where it used the Heliar wide open (name `.heliar` to keep it; with its ghosts in their true places it veils the frame more than it chains across it); the level is set once per lens instead of per frame off the hottest ghost; closing the iris brightens the ghosts it shrinks, by as much as it shrinks them, and dims the star's arms as it lengthens them; `multicoated()` hands its wavelengths out so that neighboring surfaces land far apart, which changes which color each ghost comes out; and `sourceSize` defaults to `0.015` (was `0.035`). It costs from about one millisecond a frame to about five at 1080 square on an M2, by how much of the frame the lens's ghosts fill. [Lens flare](docs/3d/lensflare.md)

## [0.10.0] - 2026-09-19

### Added

- **Coming from openFrameworks or OPENRNDR.** The Guide's new Appendix E covers the big moves from either one, each as a pair of code followed by what's different. From openFrameworks: the app as one class with no `update()`, fill and stroke as two inks, `withState` for both stacks, an `ofFbo` as a layer, `ofShader` as one `shade` function in Metal, the camera as a call, the C++ loop-by-reference habit, the addons as satellite modules, projectGenerator as `ollin new`, and rebuilding as live reload. From OPENRNDR: Kotlin to Swift, the program as a sketch, helpers that extend `Sketch` rather than the drawer, the value types, `rotate` taking radians where OPENRNDR takes degrees, the compositor, shade styles, and the orx modules. Appendix C stays the full call-by-call map for p5.js and Processing. [Appendix E](guide/e-comingfromopenframeworksandopenrndr.md)

- **A black hole, drawn by following light past it.** `Examples/Shaders/BlackHole` traces every pixel's ray backward past a Schwarzschild horizon with the orbit equation for light, so the shadow, the far side of the disk standing over the hole and under it, stars smeared into arcs, and a star behind the hole seen as a ring all fall out of the physics rather than being painted. The thin disk shines as a black body at the temperature Page and Thorne's flux gives each radius, shifted by the climb out of the well and by the gas's own motion: blue-white on the side turning toward the camera, red on the side turning away, as Luminet drew it in 1979. Drag to move round the hole, scroll to come closer; a grid sky shows the folding on lines of latitude and longitude. The physics is its own file, `physics.metal`, which the picture includes, and `BlackHoleLensTests` compiles that file alone and checks it against closed forms: the swept angle against the exact orbit integral, the far-field bend against 4GM/(c²b), the shadow's edge against Synge's angle, the ring against the exact orbit and, far off, the weak-field Einstein ring, and the color of heat against the CIE tables. It is the heaviest user shader in the examples and plays on the web page too.

- **An outline answers questions about itself.** A `Contour` can say which way it runs at a fraction along it (`tangent(at:)`, `normal(at:)`), where it passes nearest a point and how far along that is (`nearestPoint(to:)`, `fraction(of:)`, `distance(to:)`), and hand back the stretch between two fractions (`piece(from:to:)`). It finds where it meets another outline or crosses itself (`crossings(with:)`, `crossings()`), each crossing carrying its point and how far along both lines it sits. It thins a dense trace to the points it needs (`simplified(tolerance:)`), rounds or cuts its corners (`rounded(_:)`, `chamfered(_:)`, also on `Shape`), and walks itself backward as a `Contour` (`reversed()`). `Examples/Shapes/OverUnder` weaves a tangled loop over and under through its own crossings. [Geometry](docs/drawing/geometry.md)

- **A homage after Vladimir Bonačić.** `Examples/Recreations/VladimirBonacic` rebuilds two of the dynamic objects he made in Zagreb between 1969 and 1971, lamps driven by the arithmetic of Galois fields. `DynamicObject` is the 32 by 32 square of lamps of *GF.E 32-S*, built from the arithmetic he published in 1974: each lamp belongs to the residue class `row ^ column`, the pattern is an element of the field of 2^32 elements modulo x^32 + x^22 + x^2 + x + 1 whose bit i lights class i, and every tick multiplies it by x, so the walk passes all 4,294,967,295 patterns before one repeats. The back of the object is drawn too, 32 indicators and 32 buttons that read the element in binary or set it. `NamaFrieze` is *DIN. PR 18*, the frieze of eighteen lamp panels that ran 36 meters across a department store front on Kvaternik Square, stepping through all 262,143 states of an 18-bit field with a second, shorter register gating the rhythm. Set `sendsToWall` and the panels go out over sACN as eighteen dimmer channels, so the same universe lights a real wall.

- **The types that are a list of things say so.** A `Contour` is its points, a `Shape` is its contours, a `Grid`, `HexGrid`, or `TriangleGrid` is its cells, and a `Polyomino` is its squares, so all of them now read as collections: `for point in contour`, `grid.count`, `piece.contains(cell)`, `shape.first`, `map`, `filter`, `reversed()`, `randomElement(using:)`, and the rest of the standard library, without reaching for `points` / `contours` / `cells`. A grid works each cell out as it is asked for, so looping one builds no array at all, and `cells` is still there for when an array is what you want. `Contour` also gained a geometric `contains(_ point:)`, the same question `Shape.contains(_:)` answers; ask `contour.points.contains(p)` for the vertex-membership one. [Geometry](docs/drawing/geometry.md)

- **Timecode compares.** `Timecode` is `Comparable`, so a cue test is `<` and a cue window is a range: `if (cueIn..<cueOut).contains(now)`. The order runs on the moment a timecode names rather than on its frame number, so two counted at different rates still answer honestly about which came first. [MIDI](docs/integration/midi.md)

- **One frame, several lighting rigs.** Lamps are drawing state now, so the lights in force when a solid is drawn are the lights that shade it. `withLights([...]) { }` lights a block from its own fixtures and nothing else, `withLights(.noir) { }` does it from a preset, and `withoutLights { }` draws a body flat in its fill color while the scene around it stays shaded, which is how you get a hologram, a flat overlay, or a bulb that reads as a source rather than as one more shaded ball. `addLight(_:)` adds one `Light` value to the frame's own rig. Lamps set the ordinary way behave exactly as before: the frame's rig still reaches every solid wherever in `draw()` you set it, so no existing sketch changes. The frame's rig keeps the shadow casters, the environment, and the atmosphere; a scoped set lights what it draws and throws nothing. A frame holds its own set plus fifteen. [3D](docs/3d/3d.md)

- **The depth-of-field aperture has a shape.** `Bokeh.aperture` decides what a point too far from focus turns into: `.round` as before, `.blades(6)` for the hexagonal sparkle a real iris draws, `.blades(count:rotation:)` to turn it, or `.picture(_:)` for a mask read across the hole. That last one takes any `Image`, so an iris a sketch drew into a render target and one it loaded are the same path, and Ollin bundles no masks. An aperture changes where light lands, not how much of it there is, and `.round` draws exactly what it drew before. [Depth of field](docs/drawing/depthoffield.md)

- **Surfaces in the depth-of-field spray.** `SprayQuad` puts a face in a `LineSpray` scene beside its wires: the parallelogram two edges span from a corner, sampled over its area, set through `setQuads(_:)`. Lines and quads scatter into one accumulator and print as one picture. A quad's `light` is the whole quad's, not a brightness per unit of area, which is the rule a line already kept. `LineSpray.picture` is an image the quads read through each quad's own `pictureBounds`, so a photograph or a sheet of glyphs becomes an object made of light for the lens to throw out of focus. Purely additive: nothing in the line path changed. [Depth of field](docs/drawing/depthoffield.md)

- **A homage after Gego.** `Examples/Recreations/Gego` takes up the two bodies of work she is best known for. `Reticularea` ties a net of irregular triangles, hangs it from the ceiling at eight points and lets gravity have it: the triangles the sketch draws are the triangles the simulation holds, there is more net than the ties are spread over so it falls into folds, and the whole thing is drawn as 2D lines through `project`, thinner and paler as they go away from you. `DrawingWithoutPaper` hangs a bent wire lattice a few centimeters off a wall and crosses a lamp in front of it, and the shadow it throws is the drawing. Drag a wire in either one.

- **MIDI files in and out.** `MIDIFile` reads a Standard MIDI File into notes a sketch can play or draw, and writes one back out, so a piece written elsewhere runs in a sketch and a phrase a sketch worked out opens in a sequencer. Formats 0, 1, and 2, the tempo map read from either side with `beats(at:)` and `seconds(at:)`, plus control changes, pitch bends, programs, names, and markers. A `Synth` will also write down what it is asked to play: `startRecording(tempo:name:)` and `stopRecording()` hand back a take. It lives in `OllinAudio`, since a file arrives as notes rather than as messages. [MIDI files](docs/helpers/midifiles.md)

### Changed

- **The satellites read for meaning, and thirty names moved.** A second pass over the public API, this time satellite by satellite, renamed the calls whose names said something other than what they do. **Sound:** `Synth.hearingRange` is `hearingDistance` (it is a distance, and a hearing range is a band of frequencies), `AudioAnalyzer.process(_:)` is `analyze(_:)`, `Wavetable.length` is `samplesPerCycle`, `Patch.modulated(by:index:)` takes `amount:`, `Compressor.makeup` is `makeupGain`, `Equalizer.lowEdge` / `highEdge` are `lowFrequency` / `highFrequency` beside the `midFrequency` already there, `GrainCloud.scatter` is `timingJitter` beside its `positionJitter` and `pitchSpread` siblings, `SpeechListener.captionWords` is `maxCaptionWords`, `Sonification.levels` is `levelRange`, and `DetectedPitch.note` is `nearestPitch`, since it is the rounded twin of the `pitch` beside it and `Note` is a different type in the same module (`AudioAnalyzer.note` and `AudioSource.note` follow it). **Vision:** the `[Classification]` lists on `ImageClassifier`, `ConceptTracker`, `ModelTracker` and `ModelOutput` are `classifications`, since `labels` is a vocabulary elsewhere in the same module; `ConceptTracker.confidence(of:)` is `share(of:)`, because it is each phrase's share of a set that sums to one rather than a confidence; `PointSegmenter.Pick.score` is `confidence` like every other result in the module, and its `isWorking` is `isPicking`; `TrajectoryTracker(trajectoryLength:)` is `minObservationCount:`; `ContourDetector.contrastAdjustment` is `contrastFactor`. **Elsewhere:** `ScreenSource.window(title:)` is `window(matching:)` (it matches a substring); `BluetoothDevice(named:)` is `BluetoothDevice(matching:)` for the same reason, `BluetoothReading.isOn` is `bool` like every other value reader, and `BluetoothCharacteristic.standard(for:)` is `known(for:)` since it answers for uncatalogued ones too; `HapticEvent.strength(at:)` is `intensity(at:)`, the word the type already stores; `controllerMotion(_:)` is `controllersReportMotion(_:)`; `FirmataBoard.setSamplingInterval(_ milliseconds: Int)` is `sampleEvery(_ seconds: Double)`, so the unit is in the type rather than only in the documentation; `MQTTTopic.isValid(_:)` is `isValidFilter(_:)`, which is what it checks; `LaserSafety.stationaryLimit` is `maxStationaryPoints` and `LaserProjector.isConnected` is `isPlaying`, which is what it reports; `Timecode.advanced(by:)` is `advanced(byFrames:)`, since `advance(by:)` means seconds everywhere else; `World.pixelsPerMeter` is `unitsPerMeter`, matching `World3D`; `Rope3D`'s `thickness` is `radius`, which is what it always was, so `World3D.addRope` takes `radius:` too; `Wheel3D.spin` is `rollAngle` beside its `steerAngle`, with `spinRate` the rate; `Character3D.move(_:)` is `walk(at:)`, since it sets a standing velocity rather than carrying anything; `dragSoftGrab` / `releaseSoftGrab` are `dragSoftGrip` / `releaseSoftGrip`, after the `SoftGrip` they take; `Room.name` is `peerName`, because it named this machine and not the room; `Room.problem` is `lastError`, the spelling the rest of the framework uses; `PhoneState.status` is `statusMessage`; and `PhoneBody.isJointTracked(_:)` is `isObserved(_:)`, since the rig fills an unseen joint in from its neighbors and what the call answers is whether the camera saw it.

- **One shape per kind of failure.** A rule the framework now holds itself to: a failure the call can see for itself throws, a failure that happens later is readable afterwards as a sentence, and `nil` means a value is merely absent. Four places that broke it are fixed. `MQTTClient.lastError` is cleared when the line comes back up, so it no longer says a working client is broken forever. `EtherDreamFinder.start()` throws `StartError.badPort` where it used to return in silence one line above a `try`. `Camera` gained `unavailableReason`, so a refused permission prompt or a failure after it says so instead of leaving `frame` nil with no reason; `start()` still throws for what it can see itself. And `Record3DRecording(url:)` reports a missing file as `Record3DError.fileNotFound`, the same error its `path` twin already threw, rather than passing Foundation's through.

### Fixed

- **A physics world built while another was being torn down could end the process.** The rigid solver keeps its worlds in one process-global table, and claiming a free slot was not safe to do from two threads at once: both could walk away holding the same slot, and the first teardown then invalidated a handle the other was still using. A sketch builds its world on the main thread and never met this; several test suites running side by side did, which is what it was caught by.

- **The compose reference names its modifiers right.** `Docs/Drawing/Effects.md` listed a layer's blend and resolution modifiers as `.blend(_:)` and `.scale(_:)`. They are `.blended(_:)` and `.scaled(_:)`, as its own example already wrote them.

## [0.9.0] - 2026-09-18

### Added

- **The published release is checked the way a stranger gets it.** `Scripts/release-smoke.sh` generates the project `ollin new --remote` writes, lets SwiftPM fetch the framework from GitHub at a published tag, builds it, and renders one frame headless, so a tag nobody outside this repository could fetch, build, or draw from fails at the release rather than in somebody's terminal. It runs when a release is cut, and `--selftest` proves its ink test goes red on a blank frame.
- **A homage after Monir Shahroudy Farmanfarmaian.** `Examples/Recreations/MonirFarmanfarmaian` follows her rule that every polygon is a circle divided into equal arcs, from the triangle to the decagon, the way her *Families* run. *MirrorFamily* cuts each member into rows of small triangles, sets every one as a piece of mirror in plaster, presses every third point of the lattice up so each piece tilts and the raised points become six-sided stars, and cuts a star of those stars from glass painted on the back. The pieces go in from the center out, and then a lamp moves in front of the relief while ray-traced reflections show the room in every facet. *BehindGlass*, after her *Mazes*, paints a pane of spiral channels the way reverse-glass painting has to be done: from behind, the blue lines first, the sponged green and a red flower next, and silver over everything last, then turns the pane over so the layers read the other way and every spiral turns the other way. Both are proven from their exports. The relief's `.usda` holds `sides * rings * rings` pieces, and a turn of one side's angle maps every piece onto a piece of the same kind at the same height. The pane's two SVGs, from behind and from the front, hold the same marks in the opposite order of layers, each one reflected, with every lap a `band` inside the last.
- **The phone as the sketch's screen.** `device.show(self)` keeps a sketch running on the Mac and shows its frames full screen on Ollin Capture, which gains a **Sketch** mode for it. Each frame is compressed as HEVC on the Mac and sent down the cable on a connection of its own, and the phone decodes it on its media engine. The first finger on the glass is the sketch's pointer (`mouseX`, `mouseY`, `mouseIsPressed`, `pressure`), as it is when the sketch is installed on the phone, and every finger still arrives on `device.touches`. Nothing is built for the phone or installed on it, so an edit under the live window reaches the phone as soon as the Mac has compiled it. `PhoneScreen` reports `isShowing` and lets go of the pointer with `drivesPointer`. `Examples/3D/Phone/PhoneCanvas` paints with a finger and pours the paint the way the phone is tilted. The capture app has to be rebuilt for the new mode.
- **Every public name has a page.** `Scripts/check-api-pages.sh` reads every declaration in the public listings under `API/` and fails when nothing under `Docs/` names it, which is the reverse of the gate that reads the prose back to the tree, and the direction nothing covered before. Its first run found 1,198 names the reference never mentioned, and they are written now: the `Mesh` generators (`.klein`, `.lathe`, `.mobius`, `.superellipsoid`, the Platonic solids), `Material`'s `rimColor`, `rimSharpness`, `subsurfaceColor` and the gooch pair, `StrokeFont`'s metrics, `Installation`'s projection corners and schedule, `Take.Event`'s nine input cases, `FrameProfile`'s ten counters, `ScanGraph.Settings`, `SwarmChemistry.Recipe`, `MPEZone`, `Controller.dpad` and the rest of the pad, and every satellite's error type named at the call that throws it. A name that is deliberately bare is listed with its reason in `Scripts/check-api-pages-allow.txt`. One stale label turned up with them: the `RectangleDetector` initializer's size argument is `minSize:`, which the Vision page had been spelling `minimumSize:`.
- **A homage after Owen Schuh.** `Examples/Recreations/OwenSchuh` reads the two leaves of a 2022 notebook spread, where a digit is painted as a color on one ten-step scale. *CountingTheRationals* lays the fractions out in a table, rows the numerator and columns the denominator, and walks its diagonals in turn so that one walk reaches every cell and the fractions can be counted off; the table is woven, a number's ribbon is the color of its last digit, and the crossing shows whichever ribbon the walk let pass, which works out to the plain weave of cloth. A fraction not in lowest terms was counted already and is left as a hole in the fabric, so what stays painted is one cell for each counting number. *DiagonalArgument* writes a list of numbers as rows of colored digits, reads the diagonal one place at a time into the strip beside it, and builds the number that differs from the nth number in the nth place, which is on no line of the list.
- **A simulation of your own.** `Sim.shader(_:inject:substeps:rest:)` runs a Metal kernel you write over a `SimField` in place of a built-in rule: `shade(uv, info)` returns the cell's next state from `cell(info)`, `cell(info, dx, dy)`, `sampleRaw(info, p)`, and the field's extra `inputs` read as `input(info, i)`, stored as data with no color conversion. An optional `inject:` kernel decides how a drawn mark enters the field (`mark(info)`), so a circle or a line drawn in `withField` is that shape stepped by your rule. Every field now has an `edge` (`FieldEdge`: `.wrapping`, the torus it always was; `.clamped` walls; or one axis of each), which the twelve neighbor-reading sims honor too, and a `precision` (`.float32` for a state that counts). `SimField.snapshot()` reads the field back as a `FieldSnapshot`, and `Filter.arrows(spacing:scale:color:width:)` draws any two-channel layer as arrows. [Effects](docs/drawing/effects.md#simfield), the `Simulation/Wind` example, and Guide chapter 19.
- **The API diff reads as a version.** `Scripts/api-diff.sh` compares the public listings under `API/` with the last tag and names every line added, removed, renamed, changed, moved onto a protocol, or deprecated, the bump the next release needs, and, with `--changelog`, the skeleton of this file's entry. From 1.0 on a spelling that goes away without its deprecated twin fails preflight, and every shim lives in its module's `Deprecations.swift`. [`API/README.md`](https://github.com/eaviles/Ollin/blob/main/API/README.md)
- **A GIF is written as it is drawn.** `--export-gif` and `--export-loop` put each frame into the file the moment it is rendered, so a long loop at canvas size costs the memory of one frame; it used to hold the whole animation, about ten bytes a pixel a frame, and warn past a share of the machine. The writer is `GIFWriter`, which a sketch or an extension can drive with images of its own: `GIFWriter(path:width:height:)`, `append(_:delay:)`, `finish()`, with `palette:` and `loops:` beside them. One table of colors is chosen from the first frame and shared by every frame after it, replaced on its own when a frame's colors leave it; `--gif-palette per-frame` (`palette: .perFrame` on `exportGIF`) chooses one for every frame instead. Pixels that did not change are left alone and each frame is cropped to what moved, a picture of few colors codes in fewer bits, and a see-through canvas keeps its hard cut. The files decode through the system's reader within its own writer's tolerance and come in no larger.
- **A homage after Hassan Sharif.** `Examples/Recreations/HassanSharif` reads two of his semi-systems. *DotsLinesForms* is the 1984 sheet made again from new numbers: a cell is a square of nine points numbered like a keypad, chance picks a pair for every column and the rows add a line from the middle out to each point around the square in turn, and the table is drawn three times, as dots, as lines, and as squares with the lines inside, with the picks, the walk and the rule on a draft paper beside it. *AngularLines* fills a sheet with open lines through six numbered crossings of a fine grid, the numbers written on the draft paper as they are picked, then chooses one at random and paints it as a black band across a red canvas, the way his *Six Points Angular Lines* went from paper to canvas and wood.
- **Complex numbers, on both sides of the GPU.** `Complex` is a point of the plane that multiplies: `Complex(1, 2)`, `1 + 2 * .i`, the polar `Complex(magnitude:argument:)`, `.unit(angle)` to turn by an angle, `magnitude`/`argument`/`conjugate`/`reciprocal`, and `Complex.exp`, `.log`, `.pow`, `.sqrt`, `.sin`, `.cos`, `.tan` (and the hyperbolic three) on the principal branch, static members in the spelling the Swift numerics package uses. It is a `Numeric` value, so `let c: Complex = 3` is the real number three, `2 * z` doubles, and an array sums under `reduce(0, +)`; `Vector2(z)` and `Complex(v)` cross to the canvas and back. The shader library gains a `complex` module with the same arithmetic on a `float2` (`cmul`, `cdiv`, `cexp`, `clog`, `cpow`, `csqrt`, `csin`, `ccos`, and the rest, under C's names), `complexPlane` to read a layer as the plane with the imaginary axis up, and `domainColor` to paint a value the way the domain-coloring generator does (the direction as a hue, the size and direction as rulings), so a function of your own gets the same picture the built-in ones get. `Examples/Shaders/ComplexPlane` colors the ratio of two moving points in a shader and lays the circles that ratio is made of over it from `draw()`, worked out with `Complex`, and they land on the rulings; `ImaginaryLog` paints the imaginary part of that ratio's logarithm through a cosine palette, with the branch cut showing as a seam; `Meromorphic` builds a ratio of two cubics from six roots that move as `Complex` values and turns the palette's frequency up until the bands pile up around the poles. `Docs/Helpers/Complex.md`; the shader half on the [shader library](docs/shaders/shaderlibrary.md) page.
- **Curl noise in space.** `curlNoise(x, y, z)` and `curlNoise(_ p: Vector3)` are the three-coordinate forms of the flow field: the curl of three offset copies of the Perlin field, a flow that swirls in every direction and never gathers or drains, which is the field to bend geometry with. Both the plane and the space forms now read the field's slope in closed form rather than sampling it on either side, so a curl costs about three reads of `noise`, and the shader library has the matching `float3 curlNoise(float3)`. The `Rendering/LineSpray` example bends its sphere of rings with it. [Noise](docs/generators/noise.md#curlNoise).
- **The noise fields as a value.** `noiseFields` is the sketch's whole family of fields, Perlin, simplex and Worley with every `noise`, `fbm`, `curlNoise`, `simplexNoise`, `worley`, looping and tiling form on it, as one `NoiseFields` value seeded exactly as the sketch is. A sketch's own `noise` can only be called on the main thread; a value can be handed to every core, so `DispatchQueue.concurrentPerform` or a task group can read the same field the sketch reads, and `noiseSeed` still reaches it. `NoiseFields(seed:)` makes one anywhere. [Noise](docs/generators/noise.md#noiseFields); Guide Ch 5 § *A family of fields*.
- **A color named by its light.** `Color(linear:green:blue:)` takes linear numbers, the ones a lighting calculation ends with, and stores the sRGB color that means them; `linearRGB` reads a color back as light in `Double`. The two curves between them, `Color.srgbToLinear` and `Color.linearToSrgb`, are public now, the same curves the shader library runs per pixel. [Color](docs/drawing/color.md#color).
- **A `SprayLine` takes its light as light.** `SprayLine(from:to:light:endLight:)` carries linear RGB radiance per pass, so a scene that shades in linear units hands the numbers over unchanged instead of splitting them into a tone and a brightness and encoding the tone through the sRGB curve by hand. [Depth of field from light](docs/drawing/depthoffield.md#linespray).

### Changed

- **`SprayLine` stores light, not a color and an intensity.** Its `color`, `endColor`, `intensity` and `endIntensity` properties are gone; `light` and `endLight` hold the linear RGB radiance they used to be multiplied into. The `color:intensity:` initializer is unchanged and converts on the way in, so a line written that way builds as before and draws the same pixels.
- **`LineSpray.setLines` is built for a scene that moves every frame.** Handing it the lines it already has leaves the running average alone, so a sketch that rebuilds its scene each draw converges under `--settle` instead of restarting on every settle draw. Moved lines restart it as before, and while every line keeps its point count nothing is allocated: the point table is kept and the line records are rewritten into a ring of buffers the frames in flight are not reading, where every call used to allocate a fresh copy of both. [Depth of field from light](docs/drawing/depthoffield.md#linespray).

### Fixed

- **A write to a phone that has just gone fails the write, not the sketch.** A socket through the USB tunnel could raise the signal that ends the whole process when the phone left mid-write (the cable pulled, the app closed). Every such socket now turns that into a failed write, which the connection already recovers from.

## [0.8.0] - 2026-09-16

### Added

- **A canvas in the photographic 3:4.** `.photo1080` is 1080 by 1440, the frame a camera shoots natively stood up tall, and `.photo1080.landscape` is the 4:3 form at 1440 by 1080. It sits beside `.portrait1080`, the slightly taller 4:5 crop, and the project generator offers it as a canvas choice too. [Canvas](docs/core/canvas.md#canvasSize).
- **Your seed reaches the standard library's dice.** `randomness` is the sketch's own generator, and handing it to any call that takes a `using:` generator puts that call on your seed: `colors.randomElement(using: &randomness)`, `points.shuffled(using: &randomness)`, `Int.random(in: 1 ... 6, using: &randomness)`. Written without it, Swift's own randomness rolls the system's dice, so a seeded sketch quietly stops reproducing while every line of it still looks right. It is the same stream `random()` draws from, not a second one beside it, and it is also what the seedable generators take, so `WaveFunctionCollapse.solve`, `LSystem.expanded`, `ShapeGrammar.run` and `WangTiles.fill` now follow the sketch's `variation` instead of needing a generator of their own. Seeding still goes through `randomSeed` or `seed`, which are what record the recipe an export embeds. [Random](docs/generators/random.md#randomness); Guide Ch 4 § *Seeds: randomness you can keep*.
- **A palette reads as the collection it is.** `for color in palette`, `palette.enumerated()`, `map`, `first`, `isEmpty`, `reversed()`, `Array(palette)` and `palette.randomElement(using: &randomness)` all work now, with no `.colors` in the way. The wrapping `palette[i]` is unchanged and still takes any integer, where iteration stays inside the set; the subscript stays read-only, since `palette[-1] = color` could as easily mean the last swatch as a mistake, so a change still goes through `colors`. [Color](docs/drawing/color.md#palette); Guide Ch 2 § *Kits you carry: Palette and Ramp*.
- **`Vector2` and `Vector3` are one surface now.** Everything that means the same thing in two dimensions and three is written once, on a `Vector` protocol both types conform to: `length` and `normalized`, `distance(to:)`, `lerp(to:_:)`, `limited(to:)`, `projected(onto:)`, `with(x:)`, the arithmetic, and the constants. Two things follow. A helper you write against `Vector` takes either kind of point, so `func midpoint<V: Vector>(_ a: V, _ b: V) -> V { a.lerp(to: b, 0.5) }` is written once, and `centroid` now averages a collection of either. Your own type can conform too, and gets the whole shared surface for a handful of components and operators. What each type keeps is what does not carry over. `cross` is the one to know: a `Double` in the plane, because there is nowhere perpendicular to point, and a `Vector3` in space, which is how a surface gets its normal. `Vector2` keeps `angle`, `perpendicular`, the signed `angle(to:)` and `rotated(by:)`; `Vector3` keeps `unitZ` and `xy`. No call changed, no number changed, and nothing got slower: the shared surface is inlinable, so a sketch's own loops run at the speed they did. The reference follows the code, so the vector tour is now one shared section with a short list per type after it. [Geometry](docs/drawing/geometry.md#vector); Guide Ch 21 § *The world behind the canvas*.
- **The sketch tells the phone what to look for.** Everything Ollin Capture sends runs one way, up the cable. Now one thing runs down it. `device.use(.markers)` asks the phone for a mode, and `device.look(for:)` hands it the pictures to look for, each with the width it is printed at, so the reference file travels with the sketch instead of living in a folder on one particular phone. Both are declarations rather than commands fired once: the device remembers them and says them again on every connect, so plugging in the cable ten minutes into a run works exactly like plugging it in first. A tap on the phone still wins over an asked-for mode, and `device.latestState` says where it landed, how many things it is looking for, and what it could not use. That last part is why it answers at all: ARKit decides on the phone whether a picture has enough detail to be found, and a print it refuses would otherwise simply never arrive. A declared library replaces the app's own folder while the sketch is connected, and the folder takes over again when it goes. [Phone](docs/3d/phone.md#saying-what-to-look-for-from-the-sketch); Guide Ch 27 § *Saying it from the sketch*; `Examples/3D/Phone/PhoneMarkers`.
- **A crowd that makes room.** `Crowd` walks a crowd through itself without anyone overlapping while there is room to move: every walker takes the velocity nearest the one it wants that keeps it clear of each neighbor for the next second, trusting the neighbor to do half of the swerve. `crowd.add(at:goal:)` places a walker, `addObstacle` and `addWall` put in rooms and corridors that hold even in a jam, `preferredVelocity` replaces goals with a rule of your own, and `isJammed` marks the walkers with no safe velocity this step, the only ones that can overlap. Opposing streams sort themselves into lanes and a wide door drains faster than a narrow one, with no code for either. `drawCrowd` draws them, `Examples/Simulation/Crowd` has four scenes, and Chapter 12 teaches it.
- **An edit that does not start the piece over.** OllinLiveCoding now reads each ⌘↩ against the code the stage was built from. If you moved nothing but the inside of a method, and left `setup()` alone, the run underneath carries on through the swap: `setup()` does not run again, the canvas keeps what is piled on it, and the clock, the random and noise streams, the drawing state, and the `@Saved` properties come across. A long exposure ten minutes into a set survives the edit. Change a declaration, `setup()`, or anything `setup()` calls, and the swap starts the run over as it always did, and so does ⌘⇧↩. The green toast names which happened, and the editor's pulse now lights the block the caret was standing in rather than the whole page. Swift compiles rather than evaluates, so the whole buffer still compiles and the whole instance is still replaced; what is per-block is the decision and the pulse. For a framework caller, `SketchRunner.reload(to:keepClock:keepRun:)` is the new argument, and `@Saved` is how a sketch says what should travel.
- **A homage after Casey Reas.** `Examples/Recreations/CaseyReas` carries out two written instructions of the kind his *Process* works are made of, an element being a form plus a list of behaviors and the picture being what a surface full of them does to each other. In `Touching` the elements are circles that are never drawn, and the mark is a line between two centers while they touch, valued black to white by how far apart they are, kept on a surface that never clears. In `Planes` they are lines that turn toward what they touch and wander on their own, and the mark is the quadrilateral through a touching pair's four endpoints, opaque while they stay together and fading once they part.
- **A courtyard of lamps, not a key light.** `pointLight(..., reach: 11)` bounds how far a light carries: full strength at the source, a quarter of that halfway out, and exactly nothing at `reach` and beyond. Every light with a position takes it (`pointLight`, `spotLight`, `rectangleLight`, `diskLight`, `tubeLight` and the matching `Light` factories), `Light.reaching(_:)` sets or clears it on one you already have, and leaving it off is the unbounded model every light had before, unchanged down to the byte. It is the parameter a lamp needs: a room of lights that each carry forever is one flat wash with no night left between them. And a frame now carries **up to 256 lights** instead of eight. Past eight the renderer divides the screen into tiles and works out, once per tile, which lights can arrive there, so a lit pixel shades against the lamps standing over it rather than every lamp in the scene; the calls and the picture are the same either way, and on an M2 at 1080 by 1080 sixty-four lamps render in 16.9 ms against 42.7 shading every one everywhere. The reach is what buys that: a light with no bound can arrive anywhere and still costs what it always did. Shadows, visible air, bounced light, and the path-traced export keep reading the frame's first eight. [3D](docs/3d/3d.md#reach); Guide Ch 21 § *A courtyard of lamps*; `Examples/3D/Lighting/ManyLights`; `Scripts/benchmark.sh lights`.
- **A sound cut into pieces and piled back up.** `synth.grainSource = GrainSource(contentsOf: url)` and `Voice(granular: GrainCloud(size: 0.08, density: 40, speed: 0))` play a sound as a cloud of grains, each a few thousandths of a second long and worn with an envelope. The point is that the two things every other source moves together come apart: the grains are read at the note's pitch while the place they are cut from travels at `speed` alone, so `speed: 0` holds one moment of a recording for as long as the note lasts and `synth.grainScrub` drags that moment through the sound while it is still sounding. `size`, `density`, `positionJitter`, `pitchSpread`, `panSpread` and `scatter` are the rest of the cloud, `GrainShape` is the cut each grain is made with, and `.cloud`, `.smear` and `.rain` are three places to start. `GrainShape.level(at:)` and `Synth.grainCount` draw it. A cloud that spreads its grains needs two channels to spread into, so the renderer now fills both where it has them; a sketch that spreads nothing sounds exactly as it did. [Synthesis](docs/helpers/synthesis.md#grains); Guide Ch 29 § *A sound in pieces: grains*; `Examples/Audio/Grains`.
- **A sketch as a widget on the desktop.** `ollin new <name> --kind widget` writes an app with a WidgetKit extension packed inside it, and `./build.sh --install` puts it where the system looks. The surface changes what a sketch is, so the framework says what it is instead of hiding it: a widget is redrawn a few times an hour, never a few times a second, so the piece is one that **changes** rather than one that moves. One picture is drawn per moment, from the moment it stands for, and the sketch says how far apart the moments sit with `override var widgetTimeline: WidgetTimeline { .every(minutes: 15, count: 4) }`. The clock is the time of day: `time` is seconds since midnight of the moment being drawn, so a quarter past two draws the same picture today as tomorrow, `deltaTime` is the spacing, and `frameCount` is 1 because every picture is the first frame of a sketch of its own. `date` is new on every sketch: the moment a frame stands for, which is now at a desk and a moment still to come in a widget, so a piece about the time reads correctly in both (a day schedule's `scheduledPeriod` now reads it too). `--export-widget <dir> [--size WxH]` writes the whole run as pictures named by the moment, on any sketch, which is how a piece on this surface is judged in a second rather than a quarter of an hour. [Widget](docs/output/widget.md); Guide Ch 32 § *A widget, and what it does to a sketch*.
- **The suite runs in an order that keeps it honest, and says why.** The rule behind the phases is now written where a new test is placed rather than re-derived from a failure: a suite that measures the outside world against a clock, or that holds a device or a name only one process can have, runs on a quiet machine. [`Tests/README.md`](https://github.com/eaviles/Ollin/blob/main/Tests/README.md) carries it, with the four lists as its cases, the five things a phase does *not* fix (a suite starving inside its own target, a parked cooperative pool, a deadline read before its probe, a main-actor hop, a process global held across a suspension), and the checklist for a new test; `Scripts/test.sh` names the lists and points there. Two changes came with it. The desk's `shard` run now uses the same quiet phase the runner's recipe does, so the loopback suites and the three that cannot share the machine run alone rather than beside everything else. And the two Core MIDI loopback suites are nested under one serialized parent, because `.serialized` orders a suite's own tests and says nothing about a sibling suite sharing the same machine-global endpoints.
- **The Guide teaches every tool it lists.** OSCQuery and `ollin doctor` were the last two capabilities the coverage matrix had at *shown*, a mention and a worked example, and both are sections with figures now. Chapter 28 gains *The sketch that says what it takes*, which follows one declared parameter across three columns: the node it is served as, carrying its own type and range, and the control an app builds from that node without anybody typing an address, with the value coming back over plain OSC in the parameter's own units. Chapter 1 gains *Asking the machine what it has*, which reads the report as a list of failures you are being spared, a shader library that will not compile arriving as a window that never opens, and says what the three marks mean and which one exits nonzero. Every row of the matrix is now at *taught*. [Controls and signals](guide/38-controlsandsignals.md), [Hello Ollin](guide/01-helloollin.md).
- **A GIF export says what it will cost before it runs.** A GIF is built whole in memory and written when the last frame is in, so what it asks for rises with the frame count: about 1.6 GB for a 150-frame loop at 1080 by 1080. When that figure passes an eighth of the machine's memory the export now prints it before the first frame is drawn, along with the two ways down. `--gif-width` pays back with the square, since the height follows it, and `--export-video` has no ceiling of this kind because it encodes each frame into the file as it arrives. Nothing about the file changes, only what you are told beforehand.

### Changed

- **The GPU simulations step, because that is what the call does.** `updateSwarm`, `updateParticles`, `updateSimulation`, `updateParticleLife`, `updateParticleLenia`, `updateParticleFluid`, `updateSoftBodies`, `updatePhysarum`, `updatePrimordialParticles`, `updateSwarmChemistry`, `updateAttractorFlow` and `updateEvolution` are now `stepSwarm`, `stepParticles`, and the rest with the same suffix. Every one of them documented itself as "Step ... one frame" already, and every CPU simulation beside them (`Boids`, `ForceLayout`, `Meander`, `DifferentialGrowth`) spells it `step`. `BeatFollower.update(at:)` is `advance(to:)` for the same reason, which leaves no `update` in the public API.
- **A smoothed value jumps rather than sets.** `$x.set(v)` is `$x.jump(to: v)` on `@Eased`, `@Smoothed`, `@Sprung` and `@Param`, and on the `DampedSpring` and `OneEuroFilter` values under them. Assigning the property already sets the value; what the call does that assignment does not is arrive with no glide and drop the filter history, which is what every one of their doc comments said in its first word.
- **A random point in a ring says so.** `ring(innerRadius:outerRadius:)` is `randomVector(innerRadius:outerRadius:)`, beside `randomVector(in:)`. It returns a random point, not a ring, and the old spelling was the same one `SDF.ring(innerRadius:outerRadius:)` uses for a shape that is one.
- **An index of refraction is `ior:` everywhere.** `refractedRays(through:from:index:closed:)` and `caustic(through:from:index:closed:)` take `ior:`, which is what `Material`, `MeshMaterial`, `LensInterface` and `Filter.thinFilm` have always called it. `index` reads as a position in a list.
- **A Boolean label is a participle, not an order.** `.mask(channel:inverted:)`, `.masked(by:channel:inverted:)`, `.lumaKey(low:high:inverted:)` and `.adaptiveThreshold(window:bias:inverted:)` (was `invert:`); `.mirror(vertical:flipped:)` (was `flip:`); `.tile(count:mirrored:)` (was `mirror:`); `elementaryCA` and `totalisticCA` take `wraps:` (was `wrap:`); `map(_:_:_:_:_:clamped:)` (was `clamp:`). The surface already spelled it that way in fifty-three other places, `inverted:` and `mirrored:` among them.
- **A listing that asks the system is `available…()`.** `MIDIInput.sources` is `availableSources()` and `MIDIOutput.destinations` is `availableDestinations()`, both of which ask Core MIDI on every call; `ScreenCapture.apps()` is `availableApps()`, beside the `availableDisplays()` and `availableWindows()` it already sat next to. Going the other way, `DMXReceiver.universeNumbers()` is now the property `universeNumbers`, since it reads state the receiver already holds.
- **`detailMapped` fades by an `amount`.** `Mesh.detailMapped(_:normal:scale:amount:)` (was `strength:`) and `MeshMaterial.detailAmount` (was `detailStrength`). It is how much of an effect shows, and `strength` is the word for a force.
- **One `Easing` label, not two.** `Timeline.to(_:in:curve:)` and the `CameraMove` factories `pushIn`, `pullOut`, `tilt` and `reveal` take `curve:`, which is what `@Eased` and `ParamSmoothing.eased` already called the same value.

### Fixed

- **A long export no longer grows until the machine swaps.** Every headless drive now gives back what a frame allocated at the end of that frame. The frame's readback buffer, and anything the sketch asks the GPU for while it draws, used to be held until the process exited, because an export is one long loop with no run loop under it to hand the memory back: a 3,600-frame sequence at 1080 by 1080 reached a 15 GB footprint on an 8 GB machine and spent most of the run swapping. It now holds one frame's worth from start to finish. Warmup frames (`--skip`) leaked the same way, so rendering in chunks to work around it paid for the leak twice. The same drain covers `--export`, the video, GIF, spatial-video, SVG, PDF and web exports, a contact sheet or a parameter sweep, a scrub through a replayed take, and a Guide figure run (issue #23).

## [0.7.0] - 2026-09-14

### Added

- **Chapters 28, 30, and 32 of the Guide teach their last six tools.** Six figures, and three sections that were paragraphs. The eight quarter-frame messages that spell one timecode, with every nibble and the locate message's bytes read from the shipped encoder, so the figure cannot disagree with the wire. The notice a Mac with no camera shows, beside the bundled photograph the same `drawFrame` call draws once `Camera.orStill` stands in. A sketch's parameters beside the page they become on a phone, each control drawn from what the remote wire reports that parameter becomes. A piece running behind the icons, with the click that reaches the window and never the piece. The menu-bar strip among the status items at the size it really is, enlarged beside its own coordinates. And one rendered frame becoming an app's icon, with the two routes a signature decides. Timecode and OSC are now their own sections in Chapter 28, and the camera fallback its own in Chapter 30.
- **The message bus a building speaks.** `import OllinMQTT` and a sketch joins the network a house already talks on: `MQTTClient(host:)` connects to a broker, `subscribe(to: "home/+/temperature")` takes every room's reading, and the values are read the three ways every input in Ollin is read, the latest on a topic, everything since the last frame, or bound onto a `@Param`. Payloads are bytes and the protocol says nothing about them, so `MQTTMessage` reads the three things devices actually write: a decimal number, a switch word like `ON` or `offline`, and a JSON object's field through `number(named:)`. It publishes back the same way, which is what drives a lamp, and a retained publish means the next sketch to subscribe starts up already knowing the room. Two pieces are built for a piece left on a wall: a `MQTTWill` the broker publishes if the machine is cut off, discarded if you leave properly, and a connection that reopens itself after a backoff and puts every subscription and every unacknowledged message back up when it returns. [MQTT](docs/integration/mqtt.md); Guide Ch 32 § *What the building already says: MQTT*; `Examples/Integration/MQTTRoom`.
- **The phone as something you play.** Tap **Touch** in Ollin Capture and the phone stops watching: no camera runs at all, the screen under the modes becomes a surface, and every finger on it reaches the sketch. `PhoneDevice.touches` gives the two reads every input gives: `down` is the fingers on the glass, each with its own number, where it sits, how wide the contact is (the expressive axis every iPhone reports, since a pressure-sensing screen is not), and how long it has been down; `taps()` drains the landings, so a finger that comes and goes between two frames still rings. A finger keeps its number from landing to leaving, which is how a tap is told from a drag, and how a tap is followed into the drag it becomes. Tap **Air** as well, beside Hear, and `latestAir` carries the barometer: the pressure in kilopascals, and how far the phone has risen since it started measuring, good to about a tenth of a meter, which makes lifting the phone a continuous control that costs no camera. [Phone](docs/3d/phone.md#the-screen-as-a-control-surface); Guide Ch 27 § *Playing the glass*; `Examples/3D/Phone/PhoneTouches`.
- **A command that says what this machine can run, and a shell that knows its flags.** `ollin doctor` asks the questions a broken setup otherwise answers by failing somewhere else: macOS against the version Ollin needs, which `swiftc` is on the path, the GPU by name with ray tracing, mesh shaders, and temporal scaling each said either way, whether the shader compiler compiles (asked by compiling a shader, since Ollin builds its library from source when a sketch starts), whether the `ollin` on the path is this clone's, whether the completions are installed, and whether the camera, the microphone, and screen recording have been allowed. Every answer that is not settled carries the one line that settles it, and the command exits nonzero only for something that would stop a sketch from running. Nothing in it asks for a permission, so it is safe to run on a machine that is projecting. `ollin install` now also puts zsh completions in place: the subcommands after `ollin`, and the flags of whichever one you are typing, each with the line that says what it does. The completion file is generated from one table of all 139 flags and checked in, so a `git pull` carries a new flag to the prompt with nothing to re-run, and `ollin completions` prints it for putting somewhere yourself.
- **`--list-params` says what a sketch declares.** Every `@Param` with its name, its kind, the value it holds, and what it will accept, grouped the way the inspector groups its cards. It runs the sketch's `setup()` first and honors anything given beside it, so `--list-params --param radius=200 --cue dusk` describes that run rather than the file. Each value is printed in the spelling `--param` reads back, so a line can be pasted into a flag: exactly, for every kind but a color, which is said in hex and comes back to the nearest eight-bit step. It is the other half of `--param`, which until now could only set a value you already knew the name of.
- **A frame rate and a paper size are menus in the inspector.** `@Param var rate: FrameRate = .film` and `@Param var sheet: PaperSize = .a4` now present pop-up menus, the rates named the way a rate is said (24 fps, 29.97 fps, and the rest, the broadcast ones keeping their exact fractions) and the sheets by their own names, the US ones bare (Letter, Legal, Tabloid), which is also what `PaperSize(named:)` takes.
- **Two more fields that evolve.** `.smoothLife(radius:birth:survival:intervalSoftness:cellSoftness:)` runs Rafler's SmoothLife, the Game of Life's own birth and survival rule on a continuous field: a disc for the cell and a ring for its neighbors, two soft intervals of ring filling for born and survive, and the paper's glider parameters as the defaults, so a notched disc slides off in whatever direction its notch faces. `.predatorPrey(halfSaturation:predatorGrowth:predatorDeath:steps:)` puts prey and predators on one land, the Lotka-Volterra model in its Rosenzweig-MacArthur form: the field rests at full prey, a green mark releases predators, an invasion front runs out from it, and the wake winds into rotating spiral waves. Both on `makeSimField`, both in the `Simulation/Automata` picker (SmoothLife as its smooth rule) or the new `Simulation/PredatorPrey` example, both taught in Chapter 19 with a figure each.
- **The edit lands in two speeds.** OllinLiveCoding compiles the buffer twice at once on ⌘↩, plain and optimized: the plain build goes on stage the moment it compiles, and the optimized build replaces it when it is ready, with the clock carried both times, so the edit shows about a third sooner on a sketch of a few hundred lines while the sketch still runs at release speed. The chip reads "Optimizing…" between the two swaps. `--single-build` waits for the one optimized build instead, for a sketch that accumulates state and should not start over twice. In the engine, `SketchSession.landsPlainBuildFirst` and `isOptimizing`; on the shared chip, `InspectorStatus.optimizing`.

- **Five chapters of the Guide teach what they only named.** Five figures, one per straggler: Chapter 7 counts the corners at a vertex, six triangles, four squares, and three hexagons using up one turn exactly and four pentagons asking for 432 degrees, then the same four meeting at right angles in the Poincaré disk. Chapter 16 reads one `.extended` frame back twice, the picture clamped at white that a HEIC still carries and the gain map beside it, with the peak measured from the pixels. Chapter 19 draws a soft-edged disc as a reaction-diffusion field's modulation layer, the maze growing out into the spots across its edge, beside two dishes cut along the same circle and the seam they meet at. Chapter 20 prints the LineSpray example's ring sphere through three lenses from one probe, so `Bokeh`'s strength reads off the prints. Chapter 22 lists a scene file's node tree as the file holds it, draws the stage through its own camera and lights, and moves the lamp by name with its light following.
- **A homage after Ryszard Winiarski.** `Examples/Recreations/RyszardWiniarski` reads two of his rules. *Obszar* is the program behind his areas and his attempts at the visual representation of statistical distributions: a square ruled into a grid, a coin choosing which of two grids and the corner to start from, and every field decided in turn by a coin, a die, or a share of black rising from 0 to 100% across the area, painted at a pace with the rule written under it and the count of black against what the distribution promised, one area after another. *Losowanie dwiema kostkami* lays black and white runs along the rows whose every length is the sum of two dice, the throws written under the field as he wrote them and the dice showing the one being laid.
- **The cartoon look, and a light that follows the camera.** `outline(width:color:)` draws an ink line around every mesh that follows, `width` canvas pixels wide, the cartoon's outline over `.toon` or any finish: an inverted hull, the mesh once more pushed out along its normals with the faces toward the eye culled, so the line holds its width like a pen at any distance and a nearer shape hides a farther one's line. `Material.outlineWidth` / `outlineColor` and `outlined(_:color:)` carry it on a material value, `noOutline()` takes it off. And a light can be read in the camera's own frame: `Light.relativeTo(.camera)` (and `LightingPreset.relativeTo(.camera)` for a whole rig) keeps a light placed around the eye as the camera orbits, so a toon rig's bands stay put on the shapes, and `headlight()` is a light from the eye down the view, the fill that never lets a turned-away face go black. World space stays the default. The `Inked` example under `3D/Materials`, the Inked figure and a Chapter 21 section, both sections on the 3D page.
- **A step sequencer, and an arpeggiator.** `StepSequencer` is a bar of steps read out against the beat: one note or a rest on each, with its own velocity, a probability, and a ratchet, and `swing`, `rate`, `gate`, and `seed` on the bar. Write one out as `"36 . . 36 . . 36 ."`, or put one note on every strike of a `Rhythm`. `Arpeggiator` plays whatever is held right now one note a step, in any `Arpeggio.Pattern` over any number of octaves, with `hold(_:)` and `release(_:)` or `notes` set from a controller's held keys, and `latches` to keep the last chord playing. Both hand back `ScheduledNote`s, each with the beat it lands on, and `Synth.play(_:tempo:from:)` plays them to the sample. Underneath, `Synth.play(_:velocity:for:after:)` gained `after:`, a wait in seconds that lands a note on its own sample rather than on the next block, live and in an export. [Composition](docs/helpers/composition.md#stepsequencer); Guide Ch 29 § *Steps with a feel: the sequencer and the arpeggiator*; `Examples/Audio/Sequencer`.
- **The public surface holds its names, and the reference compiles.** `Scripts/check-api-names.sh` reads every listing under `API/` against the naming rules on each preflight (limits are `min…`/`max…`, a public `seed:` is an `Int`, a loader spells `withExtension:`, a configuration Boolean reads as an assertion, `advance(by:)` against `step(_:)`, and the rest in its header), with the exceptions and their reasons in `Scripts/check-api-names-allow.txt`; its first run named the fifty-three renames under *Changed* below. `Scripts/check-snippets.sh` now compiles every Swift block in the `Docs/` pages beside the Guide's, the fragments that cannot stand alone recorded in `Docs/Snippets/known-gaps.txt`, so a rename that breaks a reference page's code fails the gate rather than the reader.
- **How the phone's picture is moving.** Tap **Flow** in Ollin Capture and a tethered iPhone measures optical flow between consecutive frames of its rear camera, on the phone, and streams the field. `PhoneDevice.latestFlow` reads the way the Mac's `FlowTracker` field reads, `vector(at:in:)`, `samples(in:every:)`, and `averageFlow(in:)` over the same `MotionField`, and adds `interval`, the time between the two frames measured, so a vector over it is a speed; `latestFlowFrame` is the frame it was measured on. `MotionField` moved from `OllinVision` into the core and is now built from a closure, so a source of your own can produce one; a sketch that imports `Ollin` sees it unchanged. [Phone](docs/3d/phone.md#how-the-picture-is-moving); Guide Ch 27 § *How the picture is moving*.
- **Chapter 31 of the Guide teaches six more of its exports.** Six figures join *Sharing and performing*: the same set of three spheres rasterized, path-traced, and denoised through the call the flag wraps; the named frame rates read off `FrameRate` itself, with a loupe on the second mark where the thirtieth NTSC frame lands a millisecond late and the hour a rounding writer drifts; one see-through export drawn over a checkerboard, over another layer, and over black; a ring of circles as the frame the Mac renders, the records the web exporter writes instead of pixels, and the page with the controls it earned; one rose curve planned onto four named sheets with the millimeters the planner came to; and a save reaching the phone, the rings before and after at the same radii in new colors. Transparent output and the broadcast grid became their own sections under the video export. The chapter's slow-motion figure had stopped compiling when `OllinApp.image(of:frame:fps:)` took a `FrameRate`, hidden by the figure cache until a forced render; it compiles again and its image is unchanged.
- **The sketch says what it takes.** `extend(OSCQueryServer())` publishes every `@Param` as an OSCQuery namespace, the tree a control app browses: TouchOSC, Chataigne, or ossia score lists the sketch by name on the network, reads each parameter's type and range, builds its own controls, and sends values back over OSC in the parameter's own units, through the same control an inspector drag uses. One port number serves both halves, the tree over HTTP and the values over UDP, and `namespace()` hands the sketch the same tree as an `OSCQueryNode`. `Examples/Integration/OSCQuery` draws the tree it serves, and Chapter 28 shows it. [OSCQuery](docs/integration/oscquery.md)
- **A color on the OSC wire.** `OSCArgument` gained `.color`, the OSC 1.1 `r` type, four bytes read and written as a `Color`; an exhaustive `switch` over the arguments needs the new case.
- **Spins against the heat, and fireflies in step.** `.ising(temperature:field:sweeps:seed:)` runs the Ising model on a field: every cell a spin that wants to agree with its neighbors, flipped by Metropolis coins against the heat on a checkerboard, so a cold field magnetizes into growing domains, a hot one is noise, and at `Sim.isingCriticalTemperature` clusters come in every size. `IsingSpin` names the two levels for the brush, and the rule joins the `Simulation/Automata` picker. `Kuramoto` is the model of synchronization on the CPU: a crowd of oscillators each at its own pace, each pulled toward the phase of the others, with `coherence` and `meanPhase` as the order parameter, `criticalCoupling` for where a locked group forms, a `lag`, and a ring `range`. `Examples/Simulation/Kuramoto` is a meadow of fireflies falling into step, and Chapters 12 and 19 teach both. [Simulation fields](docs/drawing/effects.md#simfield), [Coupled oscillators](docs/simulation/oscillators.md)
- **A name completes under the caret.** OllinLiveCoding shows a list of what the Swift toolchain's own completion service knows at the caret: type the first letters of a call or a member dot and the list opens, Return or Tab takes the row, and a call lands with its arguments as placeholders that Tab walks. Escape opens and closes the list, and never leaves fullscreen; **View ▸ Complete as You Type** turns the automatic list off. `CodeCompleter` in OllinRuntime is the engine, a session per word that answers in milliseconds once the service is warm, over a C header of Ollin's own and nothing linked, so a toolchain without the service loses completion and nothing else.
- **Chapter 9 reads photographs.** The sunset the chapter painted in code for six figures and for its closing sketch gives way to the bundled pictures, each figure taking the one its worked example already reads: the portrait painted with type at the head of the chapter and in the listing, the profile as a grid of dots and as a stipple joined into one tour and one tree, the scarf as a glyph mosaic beside a halftone, and the alley pixel-sorted and seam-carved. Chapter 31 proofs the marigolds against the press instead of an authored poster, since more than half of that orange sits outside a four-ink gamut, which the figure now shows on a real wall. Authoring a picture pixel by pixel stays in the chapter as a pointer to the `PixelField` example.
- **A homage after Vicente Rojo.** `Examples/Recreations/VicenteRojo` reads two of his series: *Negaciones*, the letter T painted only as the shadow a T-shaped block would cast, in stepped tones with a forty-five degree cut wherever a side ends, a wall of them painted over one panel at a time so no painting is the last; and *México bajo la lluvia*, rain falling across a square as a grid of small marks, half-disks and dots above the diagonal, triangles and steps below it, blotches on both, their colors in broken streaks that slide down the slope.
- **A film look.** `.halation(threshold:radius:tint:amount:)` puts the warm fringe film shows around its brightest highlights, the light that gets through the emulsion and comes back off the base: the bright pass and blur of a bloom, colored by a tint and added back only where each channel still has room below white, so a white core stays white and gets a ring, and a frame with nothing above the threshold comes back byte for byte. `.filmGrain(amount:size:seed:)` lays a stock's grain over a layer, following the tone as a developed frame's noise does (none at black or white, most in the middle), with `amount` the spread at mid-gray, `size` the clump in pixels, the mean of a flat region kept, and a fresh pattern per seed. Both cross to the web page. `Examples/Effects/FilmLook` draws a night street through them, and Chapter 16 teaches them.
- **The note being sung.** Every audio source now reads the note it hears: `pitch` is the fundamental in hertz with the detector's confidence, the nearest note, the cents off it, and the pitch as a fractional MIDI number, nil when nothing periodic is heard; `note` is the short form; and `chroma` is the twelve pitch classes with every octave folded, the read for a chord. The tracker is YIN and the classes a peak-based pitch class profile, both worked out only when a sketch asks. The `Audio/Tuner` example follows the bundled violin, or the microphone with `--mic`, and `Audio/GuitarTuner` is a guitar tuner, six strings and a needle, with a pretend guitar tuning itself until the microphone hears one. [Pitch and pitch classes](docs/helpers/audio.md#pitch)
- **Chapters 9 and 31 of the Guide teach six more of their tools.** Six figures join *Pictures* and *Sharing and performing*: what a take writes down beside the same frames it plays back, the timeline panel with its diamonds, lanes, handles, and footer, a cue called back over a fade with what eases and what flips, one sketch rendered three times under `--param` with each file's recipe, a file dropped on the window and what the sketch is told, and one bundled picture as loaded beside its working copy and its widest 3:2 piece.

### Changed

- **Every sketch compile is one whole-module job.** The loader passes `-wmo`, so the sketch and its factory shim go through one compiler frontend instead of one each, and the framework's module loads once: a 20-line sketch compiles in 0.6 s instead of 1.0 s, a 470-line one in 1.1 s instead of 1.5 s, in every host that compiles a sketch (OllinLive, OllinLiveCoding, OllinRun, the gallery, the generator's preview, the Guide's figures).

- **Limits spell `min…`/`max…`.** `ParametricLSystem.taperedTree(minimumLength:)` is `minLength:`; `ILDAFile.maximumPointsPerFrame` is `maxPointsPerFrame`; `EtherDreamDAC.maximumBatch` is `maxBatch`; `EtherDreamDevice.maximumPointRate` is `maxPointRate`; `LaserSafety.maximumBrightness` is `maxBrightness`; `ContourDetector.detect(in:…maximumDimension:)` is `maxDimension:`; `HandTracker`'s `maximumHandCount` (the initializer, the property, and `detect(in:maximumHandCount:)`) is `maxHandCount`; `TrajectoryTracker`'s `minimumObjectRadius:`/`maximumObjectRadius:` are `minObjectRadius:`/`maxObjectRadius:`.
- **Every public `seed:` is an `Int`.** `Boids`, `AntColony`, `ContinuousPacking`, `CrackGrowth`, `DielectricBreakdown`, `DifferentialGrowth` and its `line`/`ring`, `DiffusionLimitedAggregation`, `ForceLayout`, `Kuramoto`, `Meander` and its `line`, `MeshGrowth`, `MeshReactionDiffusion`, `NBody.cluster`/`.disk`, `Vehicle`, `Population`, `Heightfield.diamondSquare`/`.eroded`, and `population(count:genes:seed:)` on `Sketch` took a `UInt64`; they take an `Int`, so `seed: variation` needs no `UInt64(...)` around it. `SplitMix64(seed:)`, the generator itself, keeps its `UInt64` state word.
- **Configuration Booleans read as assertions or state.** `Fit.Result.settled` is `isSettled`; `CloudAlignment.applied` is `isApplied`; `PathTracing.denoise` and its `denoise:` label are `denoises`; `DifferentialGrowth.fixedEnds` and its label are `hasFixedEnds`; `SchottkyPairing.fromExterior`/`toExterior` and their labels are `mapsFromExterior`/`mapsToExterior`; `Wheel3D.driven` is `isDriven` (the `wheel(at:…driven:)` label stays).
- **One spelling per kind of step.** `PingPong.advance()` and `PingPongTexture.advance()` are `swap()`, the ping-pong's own verb; `TimelineModel.step(byFrames:)` is `step(_:)`.
- **How much of an effect is `amount`.** `Generator.grainGradient(…intensity:…)` and `Generator.pulsingBorder(…intensity:…)` are `amount:`; `intensity` stays for light.
- **A loader spells `withExtension:`.** `PhysicsSnapshot(resource:extension:in:)` is `PhysicsSnapshot(resource:withExtension:in:)`.
- **The state follows the verb.** `SyphonClient.isActive` is `isConnected`, and `SyphonClient.stop()` is `disconnect()`.
- **A destination is a path.** `Automation.write(to:)`, `Take.write(to:)`, `PhysicsSnapshot.write(to:)`, `World3D.save(to:)`, and `ILDAFile.write(_:to:name:company:projector:)` take a `String` path beside the `URL` they took before.

### Fixed

- **An iPad app running on the Mac.** The framework builds and runs under Mac Catalyst, which is how an App Project previews on the Mac: the reduced-motion reader no longer calls AppKit there, and every runtime shader compile names a Metal language version of at least 4.0, since that compiler defaults to 2.2 and the built-in library needs the ray-tracing namespace. The desk and the phone compile exactly as before.

- **A cape sinking into the back it hangs on.** The back stop of a skeleton-carried surface is a sphere behind the surface's normal, and the solver reads that normal from the mesh's winding, so a sheet wound to face the figure was held off the wrong side and passed through. The faces the solver reads now cross reversed whenever the winding faces what carries the surface, decided from the rest shape and the binds alone, so behind is toward the figure whichever way the mesh is wound; the plane's winding fix in the same release had turned every written cape that way.

- **`drawPlane` faces the way its normal says.** The plane generator named +y and wound its quads to face -y, which nothing culled and so nothing showed; the ink line's hull, which culls the faces toward the eye, drew every floor as a slab of ink. Its quads now wind counter-clockwise seen from above, and the plane joins the probe that checks every generator's winding against its normals.


## [0.6.0] - 2026-09-13

### Added

- **Chapter 26 of the Guide shows its temporal tier.** Four figures join *Sculpting with fields*. Two magnify the same exported pixels with temporal anti-aliasing and specular anti-aliasing off and on, beside a sixteen-sample reference for the second. Two are diagrams: what each tier of temporal upscaling renders and how four jittered frames cover the canvas, and which frames interpolation draws, makes, and shows. [Traced light](guide/33-tracedlight.md)
- **A note under the finger.** `noteOn` hands back a `PlayingNote`, and `bend(_:semitones:)`, `press(_:_:)`, and `slide(_:_:)` reach that one note while it sounds: the pitch on every source but the struck body, the bow or the breath of a driven voice and the level of any other (`Voice.pressureAmount`), and the filter's cutoff (`Voice.Filter.slideAmount`); `pitchBend` bends the whole instrument. On the MIDI side, `MIDIInput.heldNotes` reads every held note with its bend, pressure, and slide resolved, per note on a polyphonic-expression surface (`mpeZones`, laid out from the controller's own configuration message) and per channel on a plain keyboard, and `MIDIOutput` sends bends, pressures, registered parameters, and a zone. [Expression](docs/helpers/synthesis.md#expression), [Per-note expression](docs/integration/midi.md#per-note-expression-mpe)
- **The phone's ears.** Switch **Hear** on in Ollin Capture and a tethered iPhone names the sounds around it on the phone itself, with only the labels crossing the cable. `PhoneDevice.sounds` gives the same level and trigger reads the Mac's `SoundClassifier` gives, `confidence(of:)`, `topClassification`, the draining `events()`, and `timeSinceHearing(_:)`, over the same values, with the threshold set on the Mac. `SoundClassification` and `SoundEvent` now live in the core, so `import Ollin` alone spells them. [What the phone hears](docs/3d/phone.md#what-the-phone-hears)
- **A frame rate you can name.** Every export's `fps:` takes a `FrameRate`. A number still stands in, so `fps: 60` reads as before, and `.film`, `.pal`, `.ntsc`, `.ntscFilm`, and `.ntscDouble` name the rates whose numbers are awkward, kept as exact fractions so a clip at `.ntsc` lands every frame on 1001/30000 of a second. `--fps` takes a name, a number, or a fraction. [Frame rates](docs/output/export.md#frame-rates)
- **A sheet by name for the machine exports.** `GCode(.plotter(), paper: .a4)` and `DXF(paper: .a3)` size a drawing for a sheet with a margin clear on every side, holding its height as well as its width. `PaperSize` carries the ISO A series and the US three, turns with `.landscape`, and reaches the canvas presets through `canvasSize(dpi:)`; `--gcode-paper` and `--dxf-paper` do the same from the command line. [A named sheet](docs/output/gcode.md#a-named-sheet)
- **The screen angles, said out loud.** `PrintSeparation.screenAngles(for:)` returns the angles a separation's halftone gives your inks, and `ProcessSeparation.screenAngles` is the four-color rosette in plate order, so a proof can label them or a sketch can screen its own masters at the same angles. `LaserOptimizer(pointsPerSecond:refreshRate:)` takes a projector's two numbers in one line.
- **Two more automata on a field.** `.wireworld()` is Silverman's computing automaton, a circuit typed as text with `WireworldCell` naming its four states, and `.schelling(preference:vacancy:mobility:passes:seed:)` is Schelling's board of two kinds sorting itself from a mild preference, with `SchellingCell` for the brush; both join the `Simulation/Automata` picker. [Simulation fields](docs/drawing/effects.md#simfield)
- **A rule typed where the parameter is set.** In OllinLive, right-click a number's or a switch's diamond for *Write a Rule*, and a field opens under the row. Return drives the parameter from the rule, an emptied field takes it away, and a rule that cannot be read stays in the field with a caret under the character it went wrong at. The rule lands in the same automation file as the keys. [Writing a rule in the row](docs/tools/timeline.md#writing-a-rule-in-the-row)
- **A homage after Sol LeWitt.** `Examples/Recreations/SolLeWitt` carries out two of his instructions: *Wall Drawing 118*, fifty points at random all connected by straight lines, drawn wall after wall at a drafter's pace, and the forty-five two-part combinations of his 1972 compass alphabet on one sheet, with the spacing he left to the drafter breathing.
- **A look from a `.cube` file.** `ColorLUT` reads the color table a grading tool exports, in both its forms, and `.lut(_:amount:)` applies it to a layer or the whole frame, a cube by tetrahedral interpolation. A look can also be built in code and written out as a `.cube`, and one warm print ships with the framework. [Looks](docs/drawing/looks.md)
- **The pitch moves without the length, an instant can be held, and a recording can be stretched.** `.pitchShift(PitchShift(semitones:mix:))` and `.freeze(Freeze(amount:))` join the effect chain, both a phase vocoder that keeps every partial whole, and `stretched(by:)` on a `Recording` or an `ImpulseResponse` gives it back longer at the same pitch. [The two that work in the spectrum](docs/helpers/synthesis.md#the-two-that-work-in-the-spectrum)

### Changed

- **`fps:` is a `FrameRate` rather than a `Double`** on every export entry (`OllinApp.image`, `export`, `exportSequence`, `exportVideo`, `exportGIF`, the vector, spatial, web, sheet, and separation exports). A literal compiles unchanged; a `Double` variable needs `FrameRate(rate)`.

### Fixed

- **The reference and the Guide named calls the code no longer spells.** Seventy-odd backticked names had gone stale behind a rename: `.bloom(threshold:intensity:radius:)` where the label is `amount:`, `hopfBases(spiral:)` for `spiralCount:`, `frameRendered(_:_:)` for `frameRendered(_:image:)`, `Mesh(resource:extension:in:)` for `withExtension:`, and their kin across the effects, physics, and 3D pages. A gate reads every name in backticks against the tree and the public surface now, so the next rename cannot leave a page behind.

## [0.5.0] - 2026-09-12

### Added

- **Every Guide chapter opens on its sketch running.** On the website a chapter's first picture is a clip of the sketch that chapter builds, with the committed still as its poster. The markdown is unchanged, so a clone, GitHub, and a reader who blocks video see the still they always saw. `Scripts/guide-clips.sh` records them, and the manifest moved to `Media/media.json`.
- **A frame for a compositor, in linear light.** `--export-exr <path>` writes one frame before the tone map and the eight-bit rounding, and `--export-sequence <dir> --exr` a folder of them. Red, green and blue hold the light itself, alpha holds coverage, and a 3D camera adds a `Z` channel. In code, `OllinApp.exportEXR`. [Linear frames](docs/output/export.md#linear-frames-exr)
- **Solids cut each other.** `union(_:)`, `intersection(_:)`, `subtracting(_:)`, and `symmetricDifference(_:)` on a `Mesh` are the four set operations a filled `Shape` already had, carried into space. What comes back is an ordinary closed mesh, and cutting is CPU geometry, so cut in `setup()`. [Cutting solids](docs/3d/3d.md#booleans)
- **Things break.** `fractured(into:seed:)` cuts a `Shape` or a `Mesh` into convex pieces that fit back together exactly, and a point makes them crowd around the blow. With it: `convexHull(of:)` in space, `Shape.area`/`centroid`/`bounds`/`separated()`, `Mesh.volume`/`centroid`, and `World.remove(_:)`. [Fracture](docs/generators/fracture.md)
- **The site tells a search engine what it holds.** `/sitemap.xml` lists every page the build writes, each at the address its own canonical link claims, and `/robots.txt` names the sitemap. Neither is written when the site has no address of its own.

### Fixed

- **`every(_:)` missed its beat at zero in the live window** while every export of the same sketch fired it. The window's first frame has a `deltaTime` of zero, which put the start of the clock on the crossing rather than before it; it borrows a nominal frame now, and `after(0)` fires on the first frame.
- **A 2D body built from an outline of more than eight corners had no collider at all**, so it had no shape, no mass, and hung in the air where it was born. An outline past the solver's limit of eight is now reduced to the eight corners that keep the most of it.

## [0.4.0] - 2026-09-11

### Added

- **The API reference is generated from the source as well as written by hand.** Every public symbol of the twenty-two library modules has a DocC page, with the core's four hundred and seventy top-level names grouped on one landing page. `Scripts/api-docs.sh` builds it locally and fails on a documentation warning, which is how forty stale doc comments came to light.
- **The site can be read by a machine, not just rendered for a person.** Every page has a markdown twin at the same address with `.md` in place of `.html`, and `/llms.txt` is a curated index pointing at those twins. Nothing fetches it automatically.

### Changed

- **The front page's sketches are a band you page through, and a phone gets them too.** *Hello, circle* now leads a row of sketches, each running beside the program that draws it, in place of the separate band of three. Paging is the browser's own scrolling, and only the sketch on screen is fetched.

### Fixed

- **The Syphon satellite works from a package dependency again.** Any project that pulled Ollin in by version, which is what `ollin new --remote` writes, could not use `OllinSyphon` at all: the build stopped at resolution with `the target 'CSyphon' in product 'OllinSyphon' contains unsafe build flags`. The vendored engine's prefix header is now imported per file rather than forced in with an unsafe flag, and nothing about the running code changed.
- **The site's favicon no longer sits in a white box in Safari's tabs.** Safari draws a light plate behind an icon whose own tone reads too close to the bar it sits on. The mark is drawn on nothing now, and the page picks between a near-black file and a paper one, since Safari ignores a color-scheme query inside an SVG favicon.

## [0.3.0] - 2026-09-10

### Added

- **Every example can show what it does, not just what it says.** `Scripts/media.sh` renders a clip and a still and puts them where the site reads them, so an example's page plays the clip with the still as its poster. Three hundred and fifty-three examples have theirs; the rest are written down with the reason they do not.
- **Four vision examples work with no camera plugged in.** The mirror that grows Turing patterns takes the bundled dancer, the object tracker the wrestler, the depth contours the street, and the contour tracer is told which way round its contrast runs. Two are unchanged and honest about it: the barcode reader has no bundled code to read, and the trajectory tracker draws its own ball.
- **The guide and the README open on the work.** The guide's first picture is the sketch each of its thirty-two chapters builds, one per cell, and the README's is thirty-two examples from every area of the framework. On the website both play. The markdown carries the still, which is what GitHub and a plain clone can show.
- **Every examples listing opens on a grid of its sketches.** Each picture links to the sketch and carries its name, and on the website resting the pointer on a cell swaps its still for a short clip, with nothing fetched until then. The 3D folder became ten pages on the way, since a hundred and thirty-seven sketches could not take one grid.
- **Thirteen figures stopped faking what they teach.** The Guide's optical-flow, slit-scan, contour, saliency and detector figures used to author their subject in code, a dancer made of two speckled discs or a clip made of sine stripes. They read the bundled film and photographs now, so what a reader sees is what the technique does to a real picture.
- **`VideoPlayer.snapshot()` works offline.** Under a headless export it reads the same virtual playhead the frame does, so `seek(to:)` and then `snapshot()` hands back exactly the moment asked for. A still-image `detect(in:)` over a clip now works where a live tracker reads nothing, which is what lets a figure measure optical flow on any machine.
- **A sketch that reads the world still runs when the world is not plugged in.** `Camera.orStill(_:)` gives back a running camera where the Mac has one and a still picture where it does not, and everything downstream takes either. Fourteen vision examples now fall back to a bundled photograph instead of showing a notice. `--photo` takes the picture even where a camera would have worked.
- **`StillFrames`.** One picture published as both a `VideoFeed` and a `FrameSource`, so it stands wherever a camera or a video player would. It republishes a few times a second rather than once, because an analyzer drops frames it cannot keep up with and a single frame could be the one dropped.
- **A photograph in the color-vision check.** `Color/ColorVision` and the Guide's figure show a picture beside the palettes, seen the same four ways: the woven blankets, where the two commonest kinds flatten the weave to one olive band while the blues hold.
- **A pair of hands worth tracking.** `SamplePhoto.hands` joins the bundled set, two open palms against a dark ground with every finger separated, and `Vision/HandTracking` reads it instead of the breakdancer. Both hands come back with all twenty-one joints; the breakdancer's are planted and half closed, which gives the drawing almost nothing to show.
- **A short film, for what a still cannot show.** `SampleClip.dance` joins the bundled set: thirty seconds of a dancer on a plain studio ground, 960 square, silent, 1.5 MB, with the camera locked off. `Vision/OpticalFlow` and `Vision/ContourTrace` read it where there is no camera, having shown nothing at all before.
- **The optical-flow overlay reads any source.** Its arrows were scaled against a fixed number of pixels, which suits a hand waved at a webcam and drew almost nothing for a dancer moving a few pixels a frame. Length and color now read against that frame's own fastest motion, so the field is legible whatever it is pointed at.
- **A page under uneven light, and two surfaces to wrap a form in.** `SamplePhoto.page`, `.talavera` and `.stone` join the bundled set. The page is what adaptive thresholding exists for, the talavera repeats seamlessly for `3D/Materials/Triplanar`, and the stone carries real relief, so `3D/Materials/Parallax` reads one photograph as both its color and its height map.
- **Two landscapes at dusk, and the first wide pictures in the set.** `SamplePhoto.headland` and `.boats` join the bundled photographs: a headland over the water at Loreto with cacti in silhouette, and small boats off Cozumel under a pink sky. Everything before them was a square or an upright, so a sketch that wanted a landscape had to crop one.
- **Four streets, for the techniques that want a whole scene.** `SamplePhoto.alley`, `.street`, `.textiles`, and `.city` join the bundled set: a Guanajuato alley, a San Miguel de Allende street strung with papel picado, woven blankets at Teotitlán del Valle, and rooftops running back to the hills. Pixel sorting, seam carving, the frequency domain, and the picture-fit example now run on them.
- **`Image.cropped(x:y:width:height:)` and `Image.cropped(toAspect:)`.** The companion to `resized`: that one changes how big a picture is, these change what is in it. The rectangle form is clamped to the picture, the aspect form takes the largest centered piece of a given shape, and nothing is scaled either way.
- **Two tables seen from above.** `SamplePhoto.breakfast` and `.desk` join the bundled set: an Oaxacan breakfast of plates, bowls and cups, and a desk with a laptop, a plant, a watch and a printed notebook. They finish the vision group, and object detection moved to the breakfast, where it finds five things rather than three.
- **Four more photographs, whole figures this time.** `SamplePhoto.reaching`, `.wrestler`, `.dancer`, and `.handstand` join the bundled set: a dancer against a concrete wall, a masked luchador with both arms raised, a contemporary dancer on white, and a breakdancer upside down on one hand. Each is framed upright so no limb runs off the edge, and each yields its skeleton.
- **The body-pose detector answers the first time it is asked.** Vision loads its 2D body-pose model on the first request that needs it, and requests arriving while it loads come back with nobody in the picture. `BodyTracker.detect(in:)` now asks again, up to three times, until the process has seen one pose.
- **Four photographs to work on.** `import OllinSamplePhotos` and `SamplePhoto.portrait.load()` hands a sketch a real picture, each a 1600-pixel square with its `credit` carried in code. A library of its own, so an app that never imports it ships none of them.
- **`Image.resized(width:height:)`.** A resampled copy of a picture, for the small working copy a stipple, a dither, a mosaic, or a winding wants before it reads every pixel.
- **The stylus beyond its press.** `stylus.tilt` is how far the pen leans, so a chisel drawn that way runs thick one way and thin the other; `stylus.twist` is the barrel turn, `stylus.isEraser` which end is down, and `stylus.isNearby` covers hovering. Everything reads zero under a mouse, and the values are recorded into a take. `stylus.tiltIsAvailable` answers the way `pressureIsAvailable` does.
- **A 3D scene on the plotter.** `lineDrawing(of:)` takes meshes and the camera looking at them and gives back 2D paths with everything the surfaces hide taken out: the silhouette, the creases, and the boundary where a surface ends. Being ordinary line work they reach an SVG, a PDF, G-code, or DXF, where a lit mesh reaches none of them. `creaseAngle` is the dial between a drawing and a wireframe, `Mesh.transformed(by:)` places each mesh first, and `hidden` keeps the covered stretches.
- **The three effects that hold a level.** `.compressor(Compressor())`, `.limiter(Limiter())`, and `.gate(Gate())` join the effects chain: what is over a compressor's threshold goes through at a fraction of what it does, a limiter is a ceiling nothing leaves above, and a gate turns the sound down below its threshold. `makeup` brings a compressed phrase back up, and `hold` keeps a gate from chopping a decaying note.
- **Hatching that follows the picture.** `layer.filtered(.hatching())` draws the layer as pen work whose strokes run along its own flow, so the marks bend around a form instead of running across it, and lays down as many of them as the tone is dark. `spacing`, `length`, and `directions` are the pen, the stroke, and how many layers stack.
- **A forest that keeps burning.** `Sim.forestFire(growth:lightning:)` runs the Drossel-Schwabl rule on the GPU: bare ground grows trees, a tree catches from a burning neighbor or from lightning, and a burning cell is bare ground the next step. Nothing in the rule names a density and one arrives anyway.
- **A homage after John Whitney.** `Examples/Recreations/JohnWhitney/Permutations` draws the rule he set out in *Digital Harmony*: points on concentric circles, the first turning once in a cycle, the hundred and fiftieth a hundred and fifty times. Nothing is random, so the fan falls into a star at every simple fraction of the cycle.
- **A table you put things on.** `TUIOReceiver` reads a tangible surface over the network: `cursors` are touches, `objects` printed markers with the symbol and angle read off them, and `blobs` the shapes it found but cannot name. Each report keeps its id while it stays on the surface. `touch.position(in: bounds)` puts one where it belongs, and the receiver is built on the OSC library, so `import OllinOSC` is all it takes.
- **Fog as one value.** `fog(.mist)` takes a `Fog`: a color, a `veil` (how much of a surface at the camera's target distance is fog) and a `pooling` (how fast it thins with height), so the same preset reads alike over a tabletop and a valley. Five presets, and the bare `fog(_:density:heightFalloff:)` is unchanged. `.haze`, `.mist`, `.thick`, `.groundMist` and `.night` are the presets, `tinted(_:)` takes another color, and a `@Param` of the type puts them on the inspector's menu.
- **Cues.** `saveCue("night")` keeps every parameter's value under a name and `cue("night", over: 2)` brings them back over a fade. The live hosts keep them in `Sketch.cues.json` beside the sketch, the performance host calls one from a MIDI program change or `/ollin/cue`, and `--cue night` renders an export at a saved look. `nextCue` and `previousCue` walk the sheet.
- **Transparent output.** A canvas that starts with `background(.clear)` leaves its background behind in every export: the PNG and the sequence carry an alpha channel, the GIF its one transparent index, and a clip keeps the alpha through `--codec proRes4444` or the new `--codec hevcWithAlpha`. The other codecs still composite over black and say so.
- **Files dropped on the window.** Drop a picture, a clip, a font, or a table from the Finder onto a running sketch and it arrives as a path: `droppedFiles()` returns what was dropped since the last call, `filesDropped()` is called once at each drop, and `mouseX`/`mouseY` hold where it landed. Every host takes the drop.
- **DXF, a drawing for the shop.** `--export-dxf panel.dxf` writes the frame as a DXF a CAD program opens in millimeters: strokes as lines along their centerlines, fills as closed polylines, and a circle kept as a circle. Each color goes on its own layer, so a shop tells a cut from a score by the colors you drew in. `--hatch` writes fills as line work, and `DXF(width:)` and `Drafting` are the planner behind it.
- **The four effects that move a sound.** `.chorus(Chorus())`, `.flanger(Flanger())`, `.phaser(Phaser())`, and `.tremolo(Tremolo())` join the effects chain: a copy sliding later and earlier so one voice reads as several, a copy a hair behind sweeping a comb of notches, a row of phase-turning stages, and the level breathing. Each is one slow wave and the thing it moves. `rate` and `depth` are on every one, and a `mix` of 0 is the plain sound exactly.
- **Shock, the picture flattened into regions.** `layer.filtered(.shock())` smooths the layer along its own flow and sharpens it across that flow, round after round: soft shading snaps into flat regions with crisp edges, and grain or grass is drawn out into one coherent stroke, with no color invented. `iterations` sets the level of abstraction. `radius` is how far across an edge the shock reaches, `flow` how far each round smooths along it, and `threshold` the bend under which nothing is sharpened.

### Fixed

- **A compute kernel no longer recomposes the shader library on every dispatch.** Every `compute(...)` call looked its pipeline up by a hash of the kernel's full composed source, which meant reading the shared library, resolving its includes, and hashing the whole text once per dispatch per frame. A kernel written in the sketch's own source is now found by its own text.

## [0.2.0] - 2026-09-08

### Added

- **Newton's basins.** `generate(.newton(roots:))` runs Newton's method from every pixel over the polynomial with the roots you place, up to eight, and colors each pixel by the root it lands on, shaded by how many steps it took. `relaxation` scales the step so the basins swell, spiral, and shed islands. `phase` turns the palette, and a pixel the method never brings home is painted `trapped`.
- **A homage after Frieder Nake.** `Examples/Recreations/FriederNake/HommageAPaulKlee` draws the drawing usually called *Hommage à Paul Klee* (*13/9/65 Nr. 2*, 1965) from Nake's own list of its chance decisions: horizontal bands of rolled width whose edges buckle without crossing, quadrilaterals left empty or filled, and circles thrown over the whole. A seed is a sheet. `--export-svg` gives the plotter drawing back.
- **A board with no firmware of your own.** `FirmataBoard` in `OllinSerial` speaks Firmata over a serial port, so a board running the StandardFirmata sketch every Arduino IDE ships answers pin by pin: `analog(0)` and `digital(2, pullUp: true)` read, and `write(9, 0.5)` and `write(10, angle: 90)` drive a PWM duty and a servo. `bind(analog:to:)` puts a pin on a `@Param`. `write(13, true)` drives a digital pin, and `messages()` drains every decoded message.
- **The public API, written down.** `API/` holds one generated listing per library product, every public type and member with its types, defaults, and deprecation, and `Scripts/api-surface.sh` fails preflight whenever the build and the listings disagree. A change to the public surface is recorded on purpose and reads as a diff in review. This is the ground the 1.0 deprecation discipline stands on.
- **Deriving one easing curve from another.** `Easing.reversed()` runs a curve from its other end, so an ease-in becomes its ease-out, and `mirrored()` puts the curve on the first half of a trip and its reverse on the second. Where the catalog already holds the result the helper hands back the built-in, so it still reads by name on a menu. `.easeInQuad.reversed() == .easeOutQuad`, and mirroring an ease-out gives the out-in curve the catalog lacks.
- **The performance host on a controller.** OllinLiveCoding answers OSC at fixed addresses under `/ollin/` once a port is set in the inspector's Controls card, and learns a MIDI pad, button, or fader for any of them: press Learn, move the control, done. A host preference, never the sketch's, and the map holds across launches. `OllinInspector.Palette` is public, so a host's own card can sit beside the shipped ones.
- **A room of your own: convolution reverb.** `Reverb(impulse, mix:, preDelay:)` plays an instrument through an `ImpulseResponse`: a recording, fading noise, or a room drawn from a rule, and `.reversed()` runs any of them backward. It sits in the chain like the built-in rooms, and turning `mix` never cuts a tail that is sounding. An `ImpulseResponse` comes from `.load`, `.resource`, `.decay(seconds:damping:)`, or a rule; `synth.reverb` still reaches it, and `Effect.Kind` gains `.convolution`.
- **Timecode: a sketch that chases a timeline.** `TimecodeClock(from: midi)` follows MIDI Time Code, the position a video deck, a show controller, a lighting desk, or a DAW locked to picture sends, into `timecode`, `seconds`, `frameRate`, `isPlaying`, and `isReversed`. The `Timecode` value does the drop-frame arithmetic and both wire forms, and `MIDIOutput.send(timecode:)` and `send(sysEx:)` cover the other direction. [MIDI](docs/integration/midi.md#timecode-timecodeclock)
- **Sharp features from a field: dual contouring.** `isosurface(at:in:resolution:method: .dualContouring)` meshes a field with one vertex per cube placed where the field's own normals meet, so a block's corners land on the corners and a bored hole keeps its rim, where marching cubes rounds every edge off to the size of a cell. [Isosurfaces](docs/generators/isosurface.md#sharp)
- **A homage after Josef Albers.** `Examples/Recreations/JosefAlbers/Homage` puts the *Homage to the Square* format on the canvas, squares of ten, eight, six, and four units each set half a unit lower than the one around it. It rolls a new set of colors on every few seconds from five palette rules of its own, built in OKLCH and crossfaded in OKLab. `--export-grid` prints a wall of them.
- **Search on the website.** Every page `ollin site` writes carries a search: the button in the bar, `/`, or ⌘K opens it, and it looks through the Guide, the reference, and the examples one section at a time, a page's title counting most and its prose least. The matching is [MiniSearch](https://lucaong.github.io/minisearch/) (MIT), pinned to one release by its hash.
- **Embroidery, a frame as stitches.** `--export-embroidery leaf.dst` writes the frame as the stitches an embroidery machine sews, in the Tajima `.dst` file nearly every machine reads: strokes as a running stitch whose every corner is a penetration, fills as connected rows, and each color as its own thread in draw order. `Embroidery(width:)` takes the hoop's real millimeters. `Embroidery.stitches(_:in:)` plans your own contours, so a sketch can draw the stitches before anything sews.
- **Brushwork, the picture as paint.** `layer.filtered(.brushwork())` lays the layer on as paint patches that follow the picture's own flow: each pixel takes the average of the flattest of eight sectors of a brush around it, so detail flattens while edges stay crisp. `radius` is the brush size and `stretch` how far an edge can draw it out. `sharpness` is how decisively the flattest sector wins, and the patches read as strokes along the contours rather than `.oilPaint`'s square dabs.
- **Lighter, darker, and the complement of a color.** `ink.lighter()`, `ink.darker(by: 0.25)`, and `ink.complement` derive a color from one you hold: the first two step its perceived lightness in OKLab units while hue and chroma stay put, the third is the opposite hue at the same lightness and chroma. A gray is its own complement.
- **Gabor noise, a noise whose spectrum you design.** `generate(.gaborNoise())` scatters small Gaussian blobs each carrying a cosine wave and sums them, so the field has one `wavelength` in pixels, a band `bandwidth` wide around it, and one direction or every direction. Brushed metal, wood grain, silk, and rippled water at a turn of the parameters. `impulses` is the quality and `seed` picks the field. The same field is `gaborNoise(x, y, ...)` and the `GaborNoise` type on the CPU, and `gaborNoise(p, ...)` in a shader of your own.
- **XDoG, the picture as pen and ink.** `layer.filtered(.xdog())` draws a line wherever the layer has an edge, solid ink where it is dark, and paper everywhere else, the lines following each edge's own direction so they run continuous rather than breaking into speckle. `threshold` and `softness` are the cut and its ramp. `radius` is the line scale, `sharpening` how far edges are pushed over the tone, and `foreground` / `background` the ink and the paper.
- **Hobby's spline.** `drawCurve(points, spline: .hobby)` threads a curve through points the way a practiced hand does: every gap becomes a Bézier whose control points are solved together, so the bend flows evenly instead of flattening between far-apart points and swelling between close ones. The default spline is unchanged. `.hobby(tension:curl:)` tunes it, `Contour(curveThrough:spline:)` and `Shape(curveThrough:spline:)` take the same choice, and `HobbySpline(through:)` is the typed form.
- **The drag on the performance stage.** Under OllinLiveCoding, hold Command over the stage and the shape under the pointer is outlined through the code; drag it, pull a corner, or turn the knob, and the numbers change in the code on the stage, which the host then evaluates. The file is still written only by ⌘S. `⌘]` and `⌘[` reorder the same way.
- **Dashed strokes.** `strokeDash([12, 8])` cuts every stroke into dashes, dash then gap in the path's own units, and a `phase:` marches them along. Each dash takes the caps, a corner inside one keeps its join, and SVG, PDF, and G-code carry every dash as its own subpath, so a plotter lifts the pen at each gap. `noStrokeDash()` puts the whole line back, `.dots(spacing:)` under `strokeCap(.round)` is a dotted line, and `Contour.dashed(_:)` / `Shape.dashed(_:)` make the same cut as geometry. `drawCircle`, `drawRect`, `drawTriangle`, `drawNgon`, `drawStar` and their neighbors dash too while a dash is on.
- **An app for the phone, from the generator.** `ollin new <Name> --kind ios-app` writes a sketch as an iPhone and iPad app: the sketch, a host that owns the entry point, the property list, and the `project.yml` that `xcodegen` turns into the Xcode project. `--team <id>` sets the signing team. `--team <id>` or `OLLIN_TEAM` sets the signing team; `ProjectRequest` gains `team` and `Capability` gains `staysOnTheDesk`, the refusal a Mac-only capability gets.
- **The logo.** Ollin has a mark: the day sign's crossing bands with an eye at the center and four marks at the cardinal points, in plain geometry. `Logo/` holds it as two SVG masters, black ink on nothing, and the website shows it in the bar and as the favicon, setting the color itself.
- **A tempo, and note lengths with names.** `Tempo` is beats per minute with a `beatsPerBar`: `tempo.beats(at: time)` is what a `StepCounter` wants each frame and `tempo.seconds(of: .eighth)` what a synth's `for:` wants. `NoteLength` names the lengths on a stave, and a number stands in for either. `NoteLength` runs `.whole` down to `.thirtySecond` with `.dotted` and `.triplet` deriving the others, and `seconds(beats:)`, `seconds(bars:)`, `bars(at:)`, and `secondsPerBeat` read the rest.
- **Tensegrities.** `Tensegrity` describes a structure of struts held apart by cables, with three balanced forms ready-made: `prism(struts:)`, the six-strut `icosahedron(strutLength:)`, and a `tower(levels:)`. `World3D.addTensegrity(_:at:)` gives one weight, so it falls, lands on its own cables, and stands. `imbalance` says whether an arrangement of your own can hold, `drawTensegrity(_:)` draws it where it is, and a new `.cable` joint kind is the rope beside `.distance`'s rod.
- **The weather outside.** `Weather(at:)`, `Weather(in: "Oaxaca")` and their siblings read the current conditions over a place every fifteen minutes, as `temperature`, `humidity`, `cloudCover`, the wind, `condition`, and the day's `sunrise` and `sunset`. Nothing throws, a failure keeps the last reading, and an export reads once. `Weather(latitude:longitude:)` takes a point, `precipitation`, `snowfall`, `pressure` and `isDay` come with it, and `Place.sun(at:)` puts the sun in the sky with no network. [Weather](docs/helpers/weather.md)
- **Strokes and fills as points on the web page.** `--export-web` carries a stroke as the points and the style the sketch gave, and the page expands them through the framework's own fringe expander compiled to WebAssembly. The six heaviest pattern examples, at 51 to 325 MB as vertices, cross at 386 KB to 8.7 MB.
- **Parquet deformations.** `parquetDeformation(columns:rows:from:to:sweep:)` lays a tiling whose tile changes shape as you read across it, a square at one edge becoming an interlocking key at the other, with no piece coming apart, because the geometry lives on the lattice's shared edges rather than on the tiles. An edge is a `Profile` in its own frame (`.straight`, `.tooth`, `.zigzag`, `.wave`, `.bump`, or `.custom`) and `Sweep` runs the change across, down, diagonally, or out from the center; read `tiles` for the pieces or `edges` for line work. [Parquet deformations](docs/drawing/parquetdeformation.md)
- **A weight limit on the web page.** `--export-web` refuses a page past 25 MB instead of writing it, and the message says what it would have weighed, which part is heaviest, and the `--max-page-size` that writes it anyway. A dense enough drawing is refused during the recording, so the export never holds frames a page would not get. In code, `maxBytes:` on `OllinApp.web(of:)` and `exportWeb`, and a `WebWeightRefusal` carrying the numbers.
- **Parameters as controls on the web page.** `--export-web` offers a sketch's `@Param`s on the page: each is probed at other settings, and one whose every effect on the picture moves along a line becomes a slider, a stepper, a switch, a color well, or a field per part. One that changes what is drawn stays at its recorded value, and the exporter says why. `--no-controls` skips the probe, and the inline fragment reaches them through the handle (`params`, `get`, `set`, `reset`). [Parameters as controls](docs/output/web.md#parameters-as-controls)
- **Fields on the web page.** `--export-web` carries a composed field (`drawSDF`) as its program, walked on the page over the framework's own arithmetic carried to GLSL, and a raymarched field (`drawSDF3D`) with its camera, its lights, `castShadows()`, and the four shading models. An environment, an area light, and a glass or coated finish still stop the export by name. A `raymarchResolution` below one is traced at the Mac's own fraction. [Fields on the page](docs/output/web.md#fields-on-the-page)
- **A lens as a field of view.** `.focalLength(35)` is the vertical angle a 35 mm lens sees on a full-frame camera, so `fieldOfView:` takes the lens's own number beside `.degrees(_:)`; pass `sensorHeight:` for a smaller format.
- **Ramp stops as values, and a ramp reversed.** `Ramp(stops: [Ramp.Stop])` takes typed stops beside the tuple list, and `ramp.reversed` runs a ramp the other way, a hard edge keeping its colors in mirrored order.
- **Depth of field from light.** `LineSpray` renders an array of `SprayLine`s as depth of field the way a lens makes it: every frame scatters passes of points along the lines on the GPU, pushes each into a `Bokeh` ball that grows with its distance from the plane of focus, and adds it into a running mean. Nothing is blurred afterward, and moving the camera or the lens restarts the average. `makeLineSpray(_:sampling:passesPerFrame:bokeh:)`, `drawLineSpray(_:)`, and `developed(exposure:ground:)` to print it. [Depth of field from light](docs/drawing/depthoffield.md)
- **A running mean of light.** `makeAccumulator()` / `withAccumulator(_:passes:)` / `Accumulator`: a layer that keeps the sum of every pass drawn into it in single-precision float and the number of passes beside it, and hands back the mean as `image`, so a picture built from faint samples converges instead of brightening without end. `reset()` starts it over, `sum` and `passes` are there for a shader of your own, `Filter.develop(exposure:ground:)` prints accumulated light, and `makeRenderTarget(precision:)` and `makeFeedback(precision:)` take `LayerPrecision.float32`. [Accumulation](docs/drawing/accumulation.md#accumulator)
- **Particles as light.** `drawParticles(_:style: .light)` deposits each particle's `color × alpha × area` as linear light with no perceptual remap, and draws a particle at or under one pixel as a single texel, so a million one-pixel points cost a million fragments rather than twenty-five million. The default `.marks` style is unchanged. [The light particle style](docs/drawing/depthoffield.md#light)
- **A kernel that sees the camera.** `Camera3D.viewMatrix`, `projectionMatrix(aspect:)`, `viewProjectionMatrix(aspect:)`, and `matrices(aspect:)` are public; `ComputeParams.append(camera:aspect:)` packs them, and the library's `ollin_project` projects a world point to canvas points inside a kernel. `cameraParams()` does it for the active camera, `ollin_project_eye` takes a camera-space point, and `ballSample(float3)` joins `discSample` in the library. [Projecting through the camera](docs/shaders/compute.md#camera)
- **Settled frames in an export.** `--settle N` (and `OllinApp.exportSettle`) draws each written frame N times with the clock held and writes the last, so a picture that converges over frames settles for every frame of a still, a sequence, a video, or a GIF. The recipe records it; a take and made frames refuse it. An `Accumulator`'s mean or a `LineSpray` through a turning camera settles for every written frame. [Settled frames](docs/output/export.md#settled-frames)
- **Compute binds up to ten buffers.** `compute(_:buffers:count:params:)` hands a kernel any number of `ComputeBuffer`s of any element types at indices 0…9, and `compute(_:reading:writing:)` now takes two element types, so a kernel can read a static geometry buffer and write an `OllinParticle` buffer without packing one into the other. `ComputeBindable` is public, and so is `Color.linearRGBA`, the radiance a light particle carries.
- **Fractal fields.** Three fractals join the raymarched field as leaves: `SDF3D.mandelbulb(power:iterations:radius:)`, `.mengerSponge(iterations:size:)`, and `.mandelbox(scale:iterations:size:)`. They melt, carve, mirror, and repeat like any other leaf, and `3D/Raymarching/RaymarchedFractals` turns each one's own dial.
- **The sketch on the phone.** A sketch runs on an iPhone or iPad as an app, and `ollin phone Sketch.swift` puts it there and installs it again on every save, its clock and parameters carried across, with the parameters live on the Mac. `SketchRunner.beginInstallation` is public, so an app of your own can turn the checkpoint on. [The sketch on the phone](docs/tools/onthephone.md)
- **A Web example group.** `Examples/Web/BreathingRing` draws the ring of breathing circles the site opens on as a sketch: a sixty-second loop that wraps without a seam, its ink and paper as parameters so a page can set its own theme.
- **A sketch as a web page.** `--export-web` records what a sketch draws over a duration and writes a page that plays it back in a browser, one self-contained file or, with `--inline`, a fragment for a page of your own. A sketch that declares `loopDuration` records one lap and the page wraps it without a seam. Anything that does not cross stops the export and names the call. [A sketch as a web page](docs/output/web.md)
- **Layers and shaders on the web page.** `--export-web` carries the layered effects: layers drawn with `withTarget`, every `Generator`, every `Filter` and `Combine` that is one shader pass (plus `.gaussianBlur` and `.bloom` as passes of the page's own), `postProcess`, a user `Shader` with `info.time` and `info.mouse` live on the page, compiled `Visual` chains, and a `Feedback` layer or a single-field `SimField` keeping its state. The solves, the ladders, and the simulations with pipelines of their own still leave as video.
- **Pictures, atlas text, and gradients on the web page.** `--export-web` carries a picture drawn with `drawImage`, text through the glyph atlas as its quads over the font's page, and a gradient on an analytic shape from the strip the Mac bakes it into. A live texture, such as a camera or a compute texture, still leaves as video. The page draws its first frame once the browser has decoded the pictures, and its handle gains `ready`.
- **Strokes and fills on the web page.** `--export-web` carries every stroke and every fill the triangle path tessellates, as the vertices the Mac would have drawn, a fill's edge anti-aliased through the same four samples, and a blend mode on shapes, strokes, and fills. A dense line drawing makes a heavy page for now, and the exporter prints the size. That is `drawLine`, `drawPolyline`, `drawBezier`, `drawCurve`, `drawArc`, `drawPolygon`, and `drawShape`, and a `Batch` recorded from them replays under its transform.
- **The website.** `ollin site` writes the README, the Guide, the reference, and every example with its source out as a static website, the pages rendered from the same files `ollin docs` reads, with a workflow that publishes it to GitHub Pages. [The reference offline](docs/tools/reference.md#in-a-browser)

### Changed

- **Preflight and CI, cheaper and less fragile.** `Scripts/preflight.sh` runs its gates cheapest first and the figure gate last, sizes its figure workers to the machine's memory as well as its cores, and compares each sample figure against its own last render rather than the committed image. On CI the build is cached between runs, and the Examples job skips its long build when nothing it compiles against changed.
- **The README's catalog moved to the reference index.** The capability table, the list of sixty-six generative techniques, and the 3D layer list now open [`Docs/README.md`](docs/index.md#the-catalog), above the groups that list every page. The README's *What's in it* section keeps its eight paragraphs and points at the catalog, so the front door is the pitch and the reference is the map.
- **The website's front page is a page rather than the README end to end.** It shows the README's opening and four of its sections laid out for a browser window: the code beside its paragraph, two short sections side by side, and the eight paragraphs of *What's in it* as a grid of cards. The whole README is the About page, one link away.
- **Note lengths and tempos are typed.** `Note.length` and `Sonification.noteLength` are `NoteLength`s, and `Note.seconds(at:)`, `Synth.play(_:tempo:)`, and `Synth.play(_:step:tempo:)` take a `Tempo`. A literal compiles as it did; a `Double` variable becomes `Tempo(bpm)` or `NoteLength(beats:)`.
- **The reader-facing pages in a plain register.** Every reference page, the README, the roadmap, the example lists, and the concept pages are rewritten sentence by sentence: one idea per sentence, plain words, active voice, the same facts. Along the way about a dozen stale API names, several wrong counts, and five figures whose caption disagreed with the picture are corrected.
- **The site opens on the sketch.** The front page now plays `Examples/Web/BreathingRing` as the page `--export-web` records for it, in place of a hand-written copy of the ring: its ink and paper are set to the site's own colors and follow a change of scheme, and a reader who asked for less motion sees its first frame, still.
- **The live hosts compile the sketch optimized.** `OllinLive`, `OllinLiveCoding`, `OllinRun`, and the gallery compile a sketch with `-O`, so a sketch that does real CPU work every frame runs under the live window at the speed `swift run -c release` gives it. `--no-optimize` compiles it plain when you are debugging the sketch itself: asserts fire, and a crash names every frame.
- **Recording and frame sharing no longer draw the frame twice.** A live take, a Syphon feed, the virtual camera, an LED map, and any `SketchExtension` that asks for the rendered frame now receive the frame the window shows, tone-mapped once more inside the frame's own command buffer. A lit 3D scene that recorded at 19 fps records at its unarmed 46, and the frame arrives a refresh or so after `afterFrame`. That covers `startRecording()` and `--record`, and `wantsRenderedFrame` / `wantsRenderedTexture` are read once per frame.
- **A typed box for bounds.** `Box3` replaces the `(min: Vector3, max: Vector3)` tuple everywhere it appeared: `Scene.bounds`, `Metaballs.bounds`, `ShadowArt.bounds`, `PhoneSceneMesh.bounds`, `PhonePlanes.bounds`, and the `in:` box of `isosurface` and `shadowArt`. Reading `.min` and `.max` is unchanged; a tuple literal becomes `Box3(min:max:)`, and `center`, `size`, `longestSide`, `padded(by:)`, and `union(_:)` come with it.
- The `Rendering/DepthOfField` example runs on the accumulating pipeline: a million samples a pass deposited as light into a running mean that converges, with the focus, the lens, and the passes per frame as parameters, instead of a `noClear` pile that brightened forever.

### Fixed

- **A parameter of your own type can be declared.** The parameters page said a full `ParamValue` conformance works, but the initializer it needed was internal, so a type declared outside the framework could not be wrapped at all. `@Param(constraints:)` is now public, and the source write-back keeps a wrapper such as `Opacity(0.5)` around the literal it changes. There is a bare form for a type whose constraints are `Void`.
- **A shader beside a phone sketch reached the phone as a library, not as source.** `ollin phone` listed a `.metal` sibling in the resources build phase, which Xcode compiles; the framework reads a shader's source at run time, so the file was useless there. It now goes through a copy-files phase and arrives as the text it was.
- **A test parked a worker on the environment download.** The remote-HDRI cache had only a blocking download, right on the export path's main thread and wrong from a test body, where it held one of the concurrency pool's few threads. The cache now has an `async` `download(_:)` that suspends instead, the tests use it, and preflight refuses the blocking form under Tests.
- **A picture could park the whole run.** An image's texture was built through the synchronous MetalKit loader, which decodes on a dispatch worker and waits on its own semaphore; with every worker already blocked, as a busy test run manages, the main thread waited forever. Textures are now decoded on the calling thread and their mip chains blitted on a command buffer. The pixels are unchanged.
- **A translucent PNG composites at its own alpha.** ImageIO decodes a PNG with alpha straight, and the image pipeline blends premultiplied, so a translucent pixel of a loaded PNG drew too bright. The texture is now premultiplied on upload, as an image painted in memory already was.
- The Accumulation and HDR pages described a sum on a `noClear` canvas plus a tone map and left out the divide by the number of passes; both now describe the running mean and point at `Accumulator` and `Filter.develop`.
- The Compute reference said a kernel binds its own buffers at indices 0…9 while the entry points reached two of one type; the `buffers:` form now matches the claim.
- A raymarched field under an orthographic camera drew a soft halo at its silhouette and lost gaps narrower than the halo; the silhouette anti-aliasing now sizes its pixel cone for an orthographic projection.

## [0.1.0] - 2026-09-02

The first tagged release. Ollin grew in one repository from May 2026 to this tag, and that history is not itemized here. This entry says what the release contains, area by area, with a link to the reference for each. The [README](about.md#whats-in-it) carries the fuller catalog, and the [Guide](guide/index.md) teaches it in order. Read this entry as a table of contents rather than a list of changes.

### The framework

- **Drawing.** A p5-style API over a Metal core: `setup()` and `draw()`, and bare calls like `drawCircle` and `background`. `draw()` runs at the display's refresh rate from the first line, so motion is the default. Some thirty analytic shapes, curved paths, concave and holed fills, variable-width strokes, marks that follow the hand, shape booleans and offsets, gradients, blend modes, clipping, accumulation, and retained batches. Every frame composites in linear light with HDR tone mapping and analytic anti-aliasing. [Drawing](docs/drawing/drawing.md).
- **Text and color.** Bitmap, outline, and single-line plotter fonts through one `drawText`, in any script, in columns, justified, along a path, or as vector shapes. OKLab mixing, spectral paint mixing, palettes and colormaps, color vision simulation, and wide-gamut and HDR output. [Text](docs/drawing/text.md#textmode), [Color](docs/drawing/color.md).
- **Shapes that merge.** SDF combinators in 2D and in raymarched 3D: smooth union, subtraction, morphing, domain mirror, tile, and radial repeat, and a clay-like `sculpt { }` block. [Combinators](docs/drawing/combinators.md).
- **Generative technique.** Sixty-six classic techniques as first-class helpers, from Poisson-disk scatter and L-systems to Wave Function Collapse, strange attractors, marching cubes, paper marbling, and terrain erosion. [Generators](docs/generators/index.md).
- **Layers, effects, and shaders.** Off-screen layers with fifty-plus GPU filters, two-layer combines, feedback, simulation fields, and a compose DSL. Measured distance fields, light with shadows in a flat sketch, the frequency domain, and summed-area averages. Your own `shade(uv, info)` fragment function, inline or in a hot-reloading `.metal` file, with errors reported at your own line numbers. Chainable `Visual`s when you would rather skip the Metal. [Effects](docs/drawing/effects.md), [Shaders](docs/shaders/shaders.md).
- **Motion and parameters.** Frame-rate-independent easing and springs, keyframe timelines, seeded variations, and record and replay. `@Param` properties become typed inspector controls, persist across reloads, and can follow a curve or a formula. [Sketch](docs/core/sketch.md), [Automation](docs/core/automation.md), [Variations](docs/core/variations.md).
- **GPU compute and simulation.** Compute kernels with a million particles a frame, and a neighbor-search primitive under artificial life and SPH fluids. Ping-pong texture simulations, steering crowds at GPU scale, populations that evolve, and chaotic systems. [Compute](docs/shaders/compute.md), [Simulation](docs/simulation/index.md).
- **Physics.** A stepped `World` with Verlet particles and springs, Box2D bodies and joints in 2D, and Jolt-backed 3D bodies, joints, characters, vehicles, ragdolls, cloth, ropes, and water. [Physics](docs/simulation/physics.md), [Physics in 3D](docs/simulation/physics3d.md).
- **3D, opt-in.** Cameras and imported scenes (OBJ, glTF, USD), instanced meshes and GPU-culled worlds, grass and an ocean, physically based materials with the full map set, glass with dispersion, area lights, image-based lighting and a procedural sky. Shadows, ray-traced reflections, caustics, global illumination, atmosphere, temporal anti-aliasing, motion blur, and lens flare. A 2D sketch never pays for any of it. [3D](docs/3d/index.md).
- **The world as input.** Nineteen on-device Vision trackers over the camera or any video, depth from a video model, screen and window capture, and video playback. CSV, JSON, and SVG import, live and pushed data feeds, game controllers, and an iPhone over USB as a LiDAR, motion, body, face, and hand sensor. [Vision](docs/vision/vision.md), [Data](docs/helpers/data.md), [The phone](docs/3d/phone.md).
- **Sound.** FFT analysis with beat detection, listening for words and named sounds, and a synthesizer the sketch plays. Voice presets, physical models, sampled instruments, wavetables, a patchable graph, effects chains, a place in the 3D scene, and algorithmic composition, all carried into an exported video. [Audio](docs/helpers/audio.md), [Synthesis](docs/helpers/synthesis.md).
- **Integration.** MIDI, OSC, a network tempo session, serial, Bluetooth Low Energy, DMX and LED mapping, and show lasers. Syphon out and in, a virtual camera, haptics, a remote surface for a phone, and a room several machines join to draw one piece on one clock. [Integration](docs/integration/index.md).
- **Output.** Headless stills and deterministic sequences, MP4 and GIF with slow motion, SVG and PDF with hatched fills, G-code, and print separations with press proofing. STL, OBJ, and 3MF for printing, USDZ and spatial video, a path-traced export, live recording with sound, and an accessibility layer for color vision and motion. [Export](docs/output/export.md).
- **Running in the system.** A finished piece as a signed Mac app, a screen saver, the desktop wallpaper, a menu-bar strip, or an installation that stays up for a week. [A sketch as an app](docs/output/app.md), [Installation](docs/output/installation.md).

### The tools

- **The `ollin` command.** One loose `.swift` file is a whole sketch, run from any directory. `ollin new` writes a starter, `ollin install` puts the command on PATH, and `ollin check` compiles a shader on this machine's GPU. `ollin docs` and `ollin examples` read the reference and the examples in the terminal. [Single-file sketches](docs/tools/singlefile.md), [The reference offline](docs/tools/reference.md).
- **OllinLive.** Save the file and the running window hot-swaps the sketch, with the clock carried across. `@Param` properties appear as typed inspector controls, a timeline records their automation, and Command-drag moves, resizes, or turns a shape by editing the source. `⌘]` and `⌘[` put it in front of or behind its neighbor. [The parameter timeline](docs/tools/timeline.md), [Dragging a shape](docs/tools/dragtoedit.md).
- **OllinLiveCoding.** The performance host: the sketch fills the window, the code is drawn over it as translucent text, and Command-Return recompiles the buffer mid-motion. [Live coding](docs/tools/livecoding.md).
- **The project generator.** `ollin new` and `ollin generate` write a ready-to-run project from ten templates or from any example. They also bring a GLSL fragment shader or a glTF or USD scene over as source, and scaffold an extension library other sketches import. [Project generator](docs/tools/projectgenerator.md).
- **The examples.** 494 example sketches in their own package, browsable in one gallery window, including twelve recreations after eight artists. [Examples](examples/index.md).

### The documentation

- The reference under [Docs/](docs/index.md), about two hundred pages, with a short concept page for each idea the reference assumes.
- The [Guide](guide/index.md), thirty-two chapters and four appendices: a practical introduction to creative coding taught through Ollin.
- For contributors, [CAPABILITIES.md](capabilities.md) inventories every capability's invariants, [ARCHITECTURE.md](architecture.md) explains the internals, and [ATTRIBUTION.md](attribution.md) credits the influences and the techniques.

### Requirements

- macOS 26 or later, a Metal-capable GPU, and a Swift 6 toolchain. Apple platforms only, by design: iOS and visionOS are future targets, and Linux, Windows, and the web are out of scope.

### Versioning from here

- Tags begin at 0.1.0. A breaking change or a new feature bumps the minor, a fix bumps the patch, and each release names its breaking renames in this file. Deprecation shims and a settled surface arrive at 1.0. Until then, pin `.upToNextMinor(from: "0.1.0")`.

[Unreleased]: https://github.com/eaviles/Ollin/compare/0.15.0...HEAD
[0.15.0]: https://github.com/eaviles/Ollin/compare/0.14.0...0.15.0
[0.14.0]: https://github.com/eaviles/Ollin/compare/0.13.0...0.14.0
[0.13.0]: https://github.com/eaviles/Ollin/compare/0.12.0...0.13.0
[0.12.0]: https://github.com/eaviles/Ollin/compare/0.11.0...0.12.0
[0.11.0]: https://github.com/eaviles/Ollin/compare/0.10.0...0.11.0
[0.10.0]: https://github.com/eaviles/Ollin/compare/0.9.0...0.10.0
[0.9.0]: https://github.com/eaviles/Ollin/compare/0.8.0...0.9.0
[0.8.0]: https://github.com/eaviles/Ollin/compare/0.7.0...0.8.0
[0.7.0]: https://github.com/eaviles/Ollin/compare/0.6.0...0.7.0
[0.6.0]: https://github.com/eaviles/Ollin/compare/0.5.0...0.6.0
[0.5.0]: https://github.com/eaviles/Ollin/compare/0.4.0...0.5.0
[0.4.0]: https://github.com/eaviles/Ollin/compare/0.3.0...0.4.0
[0.3.0]: https://github.com/eaviles/Ollin/compare/0.2.0...0.3.0
[0.2.0]: https://github.com/eaviles/Ollin/compare/0.1.0...0.2.0
[0.1.0]: https://github.com/eaviles/Ollin/releases/tag/0.1.0
