Changelog
Notable changes to Ollin, newest first. The format follows Keep a Changelog. 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, withcomposingTextholding 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 thatcaption.append(typing: typedText())reads as taking a character away.caretPositiontells the input method where the line is, so its candidates open under it.keyandkeyPressed()still report the keys themselves, unchanged. A take records the text and a replay repeats it.Examples/Input/TypedTextputs 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, andcount:bounds how many it holds.ResponseCurveruns 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.easinghands it to a call that takes anEasing. 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@1or--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/Parametershas one of each. - Pinch and turn. Two fingers on the trackpad reach a sketch as plain values in
draw():pinchScaleis how much the pinch changed the size this frame, as a factor (1while nothing pinches), andturnAngleis how far the fingers turned, in radians clockwise on the screen (0while nothing turns), the wayrotateturns. Sosize *= pinchScaleandspin += turnAnglekeep a shape under the fingers.fingersPinched()andfingersTurned()fire once per event for a response in steps.viewControl()zooms with a pinch about the pointer andcameraControl()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/PinchAndTurnsizes and spins a rose and clicks its petal count with the turn; Input has the details, and Guide Chapter 38 teaches it. - Dóra Maurer joins the recreations.
Examples/Recreations/DoraMaureris 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.Displacementsruns 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.QuasiImageis 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 closedShape(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), andshapeMemory(a disc dropped from its own height squashes by a third at0, a tenth at the default0.3, and is rigid at1), 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 whenparticlesCollideis on. TheSoftBodyit returns reads itsshape,area,center, andvelocity, takespush,applyForce, andmove(by:), and inflates when itsrestAreais set. Rigid bodies pass through soft ones and nothing rubs yet. ThePhysics/Blobsexample 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/MacEntyreVidalis 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.Circunferenciasis 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.Focosis 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.
CoonsPatchis a four-sided region whose sides are curves, read like a rectangle withpoint(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 theShapeform bend an outline from a rectangle into one, so lettering set in a box lands on a waving banner. TheShapes/WavingBannerexample 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 aRectanglePackingwith 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). ThePatterns/PackedPhotosexample 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; andfitted(tolerance:)traces a dense outline, a mouse stroke of hundreds of points, with a few smooth cubic curves returned as aPath(Schneider's method). Each is onShapetoo.Path.cubicsreads any path back as cubic Béziers, one per command, so a fit's curves can be counted and their handles drawn. ExamplesShapes/WavyLetteringandShapes/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 besidetintthatwithState { }restores. A web page made with--export-webreads the picture the same way. New exampleImages/PixelArt. - A still named by its moment.
--at Sbeside--frame Non 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 2is--frame 120at 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:.reinhardand.acesbend 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 theRendering/ToneMappingexample cycles four.
Changed
- A wide frame's files keep their depth. A
.wideor.extendedPNG 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.widevideo'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 taggedpremultipliedFirst. 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(_:)orcombined(with:_:)output in aSimField'sinputsused 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
Shapedrawn 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
Limiterlistens 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 listensLimiter.lookaheadahead, 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. Delayholds its settings to their ranges.timeis held to0...2seconds (Delay.maxTime, the longest the echo can wait),feedbackto0...0.95, andmixto0...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
.skyenvironment 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, andcastShadows()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;.alwaysbeside every light;.offfor the sky's glow alone. A sketch with a.skyand 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.skytakes 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'sdragwas 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 rigidBodyread the same number as a loss per second, sixty times weaker.dragis 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 setsworld.drag = 0(thePhysics/Burst,Chain,Joints, andRigidBodiesexamples do). A.kinematicbody is never dragged.World3Dgains adragof its own, the same reading, defaulting to the solver's 5% a second every 3D body already carried, soworld.drag = 0is what lets a thrown ball land on the exact parabola. - A body's own
restitutionwins, and the world's reaches it when changed. The walls of a 2Dboundsand the 3Dgroundcarried the world'srestitution, and both engines combine a pair by the larger, so a ball givenrestitution: 0still 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 followsworld.restitutionwhen it changes, in both worlds, anddragreaches every body the same way;Body.restitutionis readable and settable in 2D as it was in 3D. A change togravitywakes 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: anangularVelocityof 100 rad/s reads back as 100. substepsin 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'sdragandsubstepsand which bodies named a bounce of their own (format 7; the previous three formats still read).CubicBezieris a value of its own.HobbySpline.Segmentis now a typealias of the top-levelCubicBezier(the same start, two handles, end,point(at:), anddirection(at:)), whichPath.cubicsandContour.fitted(tolerance:)return too; every existing spelling compiles unchanged.setup()may throw.open func setup() throws: a loader that cannot read its file lets itsFileErrorout with a baretry, where it used to needtry!. 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 leavethrowsoff, so every existing sketch compiles unchanged. The in-memory recorders that runsetup()for a frame now throw with it:OllinApp.svg(of:),pdf(of:),gcode(of:),dxf(of:),drafting(of:),stitching(of:),embroidery(of:), andspatialScene(of:)take atry(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
@Paramtunes 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 untilresetView(). 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, whichresetView()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
SoftProofof 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.absoluteproof now shows the paper whether or notsimulatesPaperis 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
VideoPlayertold toplay()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/OpticalFlowwas caught by it: its published picture showed the film's first frame with no motion field at all, and it is rendered again. ObjectTrackingfollows 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 aFaceTrackerbox, asking the detector again only once the tracker's confidence says the patch is lost. The Vision reference also listed astop()the tracker does not have; the call isstopTracking().- 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'simage) 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
decayat 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
decaysaid. The bow now leaves the string over the bottom tenth ofpressure, where nothing sounds yet, so a string the bow leaves rings for itsdecay, 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.cathedralall 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--subframesaverage) stops the export with anunrenderederror carrying the GPU's own reason, since the discarded work may have left part of it behind. withMotionkeeps its drawing state inside the block. It saves and restores the transform and style the waywithStatedoes, sowithMotion { translate(x, y, 0); drawMesh(box) }in a loop places each box from the origin. The block left them in place, so eachtranslateadded 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 awithStateinside draws as before.- A dense pile of 3D bodies no longer stops the sketch. A
World3Dset 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 inWorld3D.lastErrorinstead, cleared by the next step that keeps them all.maxBodiesreads 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
drawVolumeon 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, andEllipsejoinCircleandRectangleas 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, withCircle,Rectangle,Triangle, andContour, share thePlaneCurveprotocol, 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.Circlegainsarea,perimeter,point(at:),nearestPoint(to:), anddistance(to:);Rectanglegainsarea,perimeter, andedges;Trianglegainsperimeterandedges. New example:Examples/Shapes/Crossings. Where curves cross - A turtle you drive from code.
Turtleis a pen that stands on the canvas facing some way:forward,back,left, andright(in degrees) walk and turn it,penUpandpenDownbreak the drawing into runs,pushStateandpopStateremember where it stood and take it back there without drawing the way, which is how a branch is drawn, andmove(to:),home(), and a settableheadingplace it outright. Every run comes back as an ordinary openContour, or a closed one afterclose(), 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. Turtle - Anwar Jalal Shemza joins the recreations.
Examples/Recreations/AnwarJalalShemzais 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.Meemis 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.Rootsis 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:)anddrawLoftbuild 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 withsmoothed: 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
- A shader as paint.
fill(shader)andstroke(shader)take aShaderanywhere they take a color, and itsshade(uv, info)runs at every pixel the shape covers, on every shape and every path: the analytic shapes, polygons andShapes, strokes, curves, and outline text.uvruns across the shape's own bounding box and turns with it under a transform;info.positionis the pixel in canvas points,info.distancethe signed distance to the shape's edge (negative inside, zero on a stroke's centerline), andinfo.alongthe 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 underfill(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. A shader as 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 atstrokeWeight. 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 understrokeWeight(_:in: .world). UnderdrawMesh(_: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. Hidden-line solids
- Noise in three dimensions that loops.
noise(x, y, z, loop:), withsignedNoise,fbm,signedFbm,ridgedFbm, andturbulencein 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 likenoise(x, y, z)so a sketch keeps its contrast when it moves to the looping form. Noise
- Live reload for a sketch that is a package. The live window compiles a sketch inside a SwiftPM target with every other
.swiftfile 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 withcannot find ... in scope.ollin MySketchruns a package's sketch by its folder, a bareollinruns the package you are in, and--target <name>picks one sketch of several. The compile readsPackage.swiftfor the target's language mode, defines, features, and resources, so a picture in a processedImages/folder loads through.modulein the live window as it does underswift run. A sketch that imports another target of its package, or another package, still needsswift run, and the error says so.OllinRun(the--installationhost) andOllinLiveCodingtake a package's folder too. Single-file sketches
- Hiroshi Kawano joins the recreations.
Examples/Recreations/HiroshiKawanois 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.Designwalks 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.ArtificialMondrianis 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 aContouror aShapeis 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: .boxis the plain average. A closed contour wraps around its seam, and an open one keeps its two ends exactly. Smoothing that keeps the points
- Motion blur drawn from subframes.
--subframes Ndraws 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 Fsets the opening, and left out it is the sketch's ownmotionBlur(shutter:), else 0.5. Every moment is a draw:timesteps through the opening,deltaTimeis the time since the last draw, andframeCountcounts each moment, so motion counted in frames runs N times as fast. A picture the GPU carries from one draw to the next (anoClearpile, a feedback layer, an accumulator, a simulation field) is refused by name. In code it isOllinApp.exportSubframesandOllinApp.exportShutter. See Export.
- Volumes. A
Volumeis 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 theMediumsays what it is made of:density(how thickly it blocks light, per world unit, likefog),color(the share of light it scatters),glowandglowIntensity(its own light), andanisotropy, 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 namesdrawVolumeand stops. The3D/Effects/Volumesexample sends a column of smoke up past a hanging ball. See Volumes.
- 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. TheEffects/LayerStylesexample stacks them on a word under a moving light. See Layer styles.
- Zdeněk Sýkora joins the recreations.
Examples/Recreations/ZdenekSykorais 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.Structureis 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.Lineswrites 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 withmeasure: .ciede2000, which matches all 34 of the test pairs Sharma, Wu and Dalal published for checking implementations.CIELabbrings 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 andnearestIndex(to:)its position.Image.histogram(of:bins:in:)counts a picture's pixels by lightness or by one channel, andpercentile(_:)reads its working black and white. TheColor/Shadesexample uses all three. Measuring color
- Shades whose hue drifts.
Palette.shades(of:count:)andRamp.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, andhueDriftsets how far the hue turns, a negative drift cooling the lights instead. Shades
- The slope of a fitted field.
RadialBasisreports how its field changes:gradient(at:)for a field of numbers, the way uphill with a length that says how steep, andderivative(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:)andgradient(at:field:)measure the slope by central differences. TheMotion/ContourWalkersexample walks the contours of both kinds. Slopes
Changed
- A caption draws over the finished frame.
drawCaptionnow 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 - A parameter read costs what a stored property costs. A
@Paramholding 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 - 2D torque takes points, like force.
applyTorquenow 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 byunitsPerMetersquared to keep its motion. 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
- 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
.detailtier 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 - A camera rig's framing follows its arguments.
cameraShowcase,cameraControlandcameraMoveused to readtarget,radius,elevation,azimuthandfieldOfViewon their first call only, so a@Parampassed 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 undercameraControluntil the viewer takes the camera. After that it moves only whereresetCamera()and the idle return go. The same values every frame change nothing, so a sketch passing constants draws exactly what it did. Thefrom:forms take the authored camera's pose the same way. 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
shadowSamplescount 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
- Even spacing on a surface is about three times faster.
surfacePoints(on:count:)with its default.blueNoisethinning 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 thatswift runmakes 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
MarkovChainis in the core. The chain the music helpers use to pick notes now lives inOllinitself, 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 onlyOllinAudioand uses the chain now needsimport Ollinbeside 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, a choice that remembers
- A color-vision simulation shows a dichromat colors they would see as themselves. At full severity
Color.simulated(_:)andFilter.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.matrixis 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
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
withOverlayblock (or a caption) over a scene lit byenvironment(.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 - 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
- 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
- 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 - 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
- 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
radiuswhere the tip used to fall short. 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
- 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 - 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
hearingDistancewhile the export runs reaches the file at the frame that made it, where the file used to hold the range the export began with. Andunplace()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
- A struck body no longer clips, and
hardnessis 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.woodsat 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 (.belland.bar0.9,.drumand.plate0.85,.glass0.8) and each rings the partials it declares; the defaulthardnessof aModalBody,ModalBody(shape:), andStruckShape.bodymoves from 0.7 to 0.85 so a drawn shape's upper modes are heard an octave up; and the struckVoicepresets (.drum,.bar,.chime,.glass) carry gain 1, which puts them within 3 dB of.pluckat 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; theSoundInAnExportandStruckShapesexamples raised their struck instruments' gain to keep their balance. 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
decayis exact at low notes and shorter at high ones; a bowed string'sdecayis 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.timeruns 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.wideor.extendedframe 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'sdragis 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 awithTargetlayer, awithFieldseed, or a feedback loop drawn before the canvas background came out empty with no error, and the reference told you to callbackgroundfirst. 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
- 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
- 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 (inCAPABILITIES.mdand under Photorealistic 3D inDESIGN-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'sreach:, 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
cameraMoveorcameraShowcasemove 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 whiletimeread 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
- A move resumed after the hands starts from the hands. Switching from
cameraControl()back to the samecameraMoveit 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
- 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
- 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 theMotionBlurexample 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 inwithOverlay { }. 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'sreachthins its caustic as it thins its light. Caustics
[0.14.0] - 2026-10-06
Added
- Kazuya Sakai joins the recreations.
Examples/Recreations/KazuyaSakaiis new, after the painter who was art director of Octavio Paz's Plural and among the first to show geometric abstraction in Mexico.Ondulacionesis 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.RightOffis 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 ownShape: 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:)andRectangle.contourturn the two values into outlines for it, or for aShapeor a boolean. New example:Examples/Shapes/ColoringBook. Geometry
- Points in the order of a Hilbert curve.
hilbertOrder(of:)andhilbertSorted(_:)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, andlevelsets how fine the curve is. New example:Examples/Images/HilbertLine. Hilbert order
- A traced frame on a time budget.
--path-traced --pt-seconds 10gives 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-noisetoo.--pt-minis a floor the clock may not cut under, the first frame of a--pt-reuserun 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
- A traced sequence carries its earlier frames.
--path-traced 32 --pt-reuse 8carries 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
- A bound on what a second bounce may carry.
--path-traced 256 --pt-clamp 4holds 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
- A traced pixel stops once it has settled.
--path-traced 512 --pt-noise 0.01lets 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 Nto 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.noiseThresholdandminSamplesPerPixel. Path-traced export
- Small highlights under the lens are drawn as discs of their own.
depthOfField()and the.defocuslayer 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.performanceand 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
- A traced export no longer blurs twice. A sketch that calls
depthOfField()and exports with--path-tracedhad 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
- 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 thepostProcessfilters, 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
- Depth of field on the canvas.
depthOfField()blurs the finished 3D frame by its own depth through the camera's lens:Camera3D.aperture,focusDistance, andapertureBlades, 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.focusRangeholds a band sharp,maxBlurcaps the blur, andqualitysets 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 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:)insidewithMotionfollows 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 aMeshField's keep the camera's motion, with a note. Instancing
- Lines in 3D.
drawLineanddrawPolylinetakeVector3points and draw a line through the camera with the 2D stroke's weight, joins, caps, and soft edge, anddrawPolyline(_: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, unlessstrokeWeight(0.05, in: .world)asks for the scene's units, which thin the line as it recedes.drawAxes(length:)anddrawGrid(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. Lines in 3D
- Drawing part of an image.
drawImagetakes 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), ordrawImage(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
- 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
- A feedback layer, an accumulator, or a simulation field as the base of a combine.
Feedback,Accumulator, andSimFieldtakecombined(with:_:)besidefiltered(_:), so trails can be masked or displaced by another layer without a filter that changes nothing first. Effects
- Saloua Raouda Choucair joins the recreations.
Examples/Recreations/SalouaRaoudaChoucairis new, after her modular sculptures.Poemcuts 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-usdzwrites them as solids that make the block again.Dualscuts 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-svgwrites.
- Sweeps and strips.
Curve3Dis a path through space, open or closed, kept as points or threaded smoothly through a few withCurve3D(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, sorotate(frame.rotation)faces a rider the way the path runs.Mesh.sweep(_:along:scale:twist:capped:)(anddrawSweep) carries aShapealong 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:)(anddrawStrip) is the ribbon between two lines, one color a rung, with normals from the strip itself.Pacewalks 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
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
- 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
.defocusis the canvas lens's gather. The layer filter runs the same five passesdepthOfField()runs, fed by its own depth map and its ramp (focus,range,maxBlurunchanged): 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, reachingmaxBlurat 0 and 1 (a scene's near and far planes); before it reachedmaxBluronerangepast 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, andcatsEyecarry 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
- The frame recipe names the framework's commit. Beside
git, the sketch package's commit, an exported frame's recipe carriesollin, the framework's own, each-dirtywhen its tree is, so a frame made from a path dependency can be tied to the framework that drew it. Export
- A filter over a single-precision layer keeps single precision. A filter or a combine over a layer made with
precision: .float32used 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, Measured distance fields
- An export holds a sketch that stopped its loop, the way the window does. After
noLoop(), every export used to calldraw()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 insetup()is one frame, held still. A replayed take still draws every frame it holds. Sketch
- A closed contour's walk wraps.
Contour.point(at:),tangent(at:),normal(at:), andpiece(from:to:)used to clamp a fraction to0...1on 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 closedCurve3Ddoes:point(at: 1.25)is a quarter of the way round again, andpiece(from: 1.9, to: 2.1)ispiece(from: 0.9, to: 0.1). An open contour still clamps, and fractions inside0...1read exactly as before. Geometry
- A tube's rings ride the new frames.
Mesh.tube(along:), and the helix and torus knot built on it, now place their rings onCurve3D'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
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
- A blurred ball's edge follows the lens.
depthOfField()and.defocusspread 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
- 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, 2D physics
- The barrel keeps the light. A
.defocuswith acatsEyeabove 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 theEffects/Defocusexample'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. AcatsEyeof 0 is byte for byte what it was; a picture made with one above it moves. 3D
- The traced lens focuses where
focusDistancesays. The path tracer focusedfocusDistancepast the near plane rather than from the eye, and placed its opening on the near plane, which widened the blur bys / (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
- 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
withMotionmover behind a field of instanced copies wrote its motion through them. Every instanced draw now hides what stands behind it there. Instancing
- 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
- 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
VideoPlayerhands each new frame to its trackers, where before they read nothing in an export.--settleno 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 longTrajectoryTrackerexport can find a different arc. Vision
[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
- 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 conventionNSColorSpace.extendedSRGBuses, and the same red reads (1.093, -0.227, -0.150);displayP3Componentsstill comes back to (1, 0, 0), andColor(linear:)andlinearRGBround-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
- A generated project says sketch.
ollin new's sample name isMySketch(it wasMyPiece); a widget project's one public line isDoor.swift, declaring<Name>Door(it wasPiece.swiftand<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, Widget, Installation
- Noise along one coordinate is a field of its own.
noise(x),signedNoise(x),fbm(x),ridgedFbm(x)andturbulence(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, andnoiseField()and a formula'snoiseat 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
- The extension starter's filter is
cornerShade(amount:).ollin new --kind extension --seam filterwrote aFilter.vignette(amount:)beside the built-invignette(amount:radius:softness:), so the starter's own.vignette()call was ambiguous. It now writescornerShade(amount:), called as.cornerShade(); a package made from the old starter can rename its function the same way. Extensions
- 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 was33-DepthAndThePhone.md), and the new chapters are24-Automata.md,28-MaterialsAndSurroundings.md,36-ThePhoneAsASensor.md, and42-MakingItPhysical.md. A link to a section that moved names its new chapter. The Guide's 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
- The credits carry the history the Guide tells.
ATTRIBUTION.mdgains 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
- The headless calls that hand back a picture throw an
ExportError, where they answeredniland printed a line. When the renderer would not start, most often because a shader stopped compiling,OllinApp.image(of:)returnedniland 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 sameExportErrorthe exports throw, of kindunrendered, with the renderer's own message in its sentence and an emptypath. Breaking:image(of:), the fourcontactSheet(of:)forms,plates(of:), andseparations(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 anunsupportedone naming what to add, where they returnednil. In code,if let frame = OllinApp.image(of: sketch)becomeslet frame = try OllinApp.image(of: sketch), ortry?to keep the old answer.exportWidgetthrows when no picture draws, where it wrote an empty folder. Export
Added
- The pitch wheel as a parameter.
MIDIInput.bindPitchBend(to:channel:travel:)drives a@Paramfrom the wheel the waybind(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.travelis on the wheel's own scale, -1 fully down to 1 fully up, sotravel: 0...1spends the whole range on an upward bend.pitchBend(channel:)reads the wheel on the same scale, 0 until it moves, andunbindPitchBend(channel:)lets go. This is the whole channel's wheel; on a per-note controller each note's bend is still inheldNotes. MIDI
- A still sketch that answers, and knowing an export.
redraw()draws one more frame of a sketch that has stopped looping, so anoLoop()sketch can change what it shows on a click or a key and cost nothing between them.isExportingis 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
- 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), andhidePointer()/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 asStringpaths throughchosenFiles()and thefilesChosen()hook, the way a drop does.copyFrame()puts the frame the window shows on the clipboard as a PNG. Input, Recording
- Spherical coordinates.
spherical(azimuth, elevation, radius, around:)ispolarin 3D, the angles first, in the camera's own orbit convention, and aVector3'sazimuthandelevationread the angles back. Math
- Points inside a shape.
randomPoints(in:count:)scatters points evenly over aShape'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; andpoissonDisk(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 seededrandom, and each wraps a free function that takes any random source. (Geometry, Blue noise)
- One mesh from several.
Mesh.joined(_:)lays parts end to end as one mesh with no boolean, andplaced(_:)bakes aMeshInstanceplacement 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 arithmeticdrawMeshruns 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)
- 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), andMetaballs.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)
- Three more filters.
.channelMixer(_:)runs aColorMatrixover a layer, every channel of the result a weighted sum of the input's red, green, blue, and alpha plus an offset:.grayand.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 withfillsFramescales 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
- 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 andmotionBlur()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
- A surface that glows in its own colors.
MeshMaterial.emissiveIntensity, set on a copy byMesh.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 whereemissiveColortints 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
- The environments on a menu.
Environmentconforms toParamChoices, so@Param var surroundings = Environment.studiogives the inspector a pop-up menu of the twenty built-in environments. A.sky(...)or a built-in withlightingOnly()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
- A radius per corner.
drawRect(_:cornerRadii:), on every anchorcornerRadius:takes and ondrawRects, rounds each corner of a rectangle by its own radius:CornerRadii.top(20)for a tab,.bottom,.left, and.rightfor 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
- 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
- A generator's plane as a value.
ComplexPlaneis the framing every picture of the complex plane shares (centerin the middle,spanunits across the shorter side, the imaginary axis up), and a generator'splanehands its own back, soplane.canvasPoint(of: c, in: bounds)marks a number on the picture andplane.planePoint(at: mouse, in: bounds)reads the number under the pointer without the sketch redoing the arithmetic.ComplexPlane(center:zoom:)takes a generator'szoom, where the span is 3 / zoom. Effects
- Putting a parameter back. A parameter tuned in the inspector goes back to the value its
@Paramline 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 nosetup(), so the clock, the seed, and the canvas carry on, and a track on the timeline still wins its parameter. From code,Param.reset()andSketch.resetParameters(named:), anddeclaredValuereads what the line gave the parameter; the remote surface has a Reset all of its own (RemoteReseton the wire). A host that shows the parameters list passes aParamResetActionas its newreset:argument (ParametersListView(parameters:sketchName:onChange:save:reset:)), which carries the tuned names and the call that puts values back. Parameters
- Alma Thomas joins the recreations.
Examples/Recreations/AlmaThomasis 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.Ringslays 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;Columnslays 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 onstrokeBrush: 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-svggives 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
- A hole in a contoured field leaves the contour open. A field that returned NaN or infinity next to a contour made
isolinesinterpolate 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
- 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.shaderkernel, 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--benchshowed 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
- 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 anysharpness. 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..diffusenow 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, andsharpnessbuys cycles, three at 0 and six at 1. Effects
- 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
- A neighbor grid covers its bounds.
SpatialHash, and with it Particle Life, the Primordial Particle System, Particle Lenia, swarm chemistry,Swarm,ParticleFluidandSoftBodies, 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
- 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
- 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:massandphysics:centerOfMassgive them through the import, came back fromrestore(_:)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
- 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
- The bundled Hershey font's notice carries the acknowledgments its distribution requires. The
.jhfdata 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, inTHIRD-PARTY-NOTICES.mdand in the notice beside the font. Notices
--cuefinds a look saved beside a live sketch. An OllinLive export given--cue <name>and no--cueslooked 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 loadsSketch.cues.jsonfrom beside the sketch, as it loads the automation, and a--cuesfile still wins. Cues
- A forest fire takes the trees you plant. Drawing into a
.forestFirefield 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
- 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
- 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 usesceneThroughGlass()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
Batchthat 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.
surfacePointswith the default.blueNoisescatter 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 withcoff 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-cfigure had marked the conjugate point. ollin examplesdescribes 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 (ImaginaryLogread 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 (complexnow finds Meromorphic as well as ComplexPlane), and a sketch is found by the author its header credits (turanfinds 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. TUIOReceiverreads 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 firstsetafter an alive list, so every frame from a tracker was taken twice:framesReceivedcounted 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
MIDIFilesexample 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
imageor a compute texture's, reads.clearat 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 list them all.
- Every export throws an
ExportErrorwhen 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 afatalError, so one refused frame in a test run took every other test in the process with it. Each export call onOllinAppnow throws anExportErrornaming the file, the frame it had reached, and what went wrong, and a clip stopped partway is removed rather than left short. Itskindsays 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 nowunsupported. Breaking:export,exportSequence,exportVideo,exportGIF,exportSpatialVideo,exportEXR,exportSVG,exportPDF,exportDXF,exportGCode,exportEmbroidery,exportSpatial,exportWeb,exportSeparations,exportPlates, and the fourexportContactSheetforms now throw,exportHEICthrows where it answeredniland returns aStillExportthat is no longer optional, andexportWidgetthrows where it printed and returned what it managed. In code,OllinApp.exportVideo(sketch, …)becomestry 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
ollin apisays where a call may run. TheAPI/listings leave isolation out, soollin api Sketchprintedopen class Sketchwith 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 anonisolatedmember keeps the word. The reference offline
- 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 samenil, so a sketch could not tell a typo from a broken file and nothing said which. Each loader now throws a newFileError:missingfor a path or resource that is not there,unreadablefor 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 nowinit … throws, a-> T?is nowthrows -> T, andPalette.palettesandloadPalettesthrow where they returned[]. The calls areloadImage,loadMesh,loadScene,loadSVG,loadPalette,loadPalettes,loadTable,loadJSON; the file, data, text, and resource initializers ofImage,Mesh(withobjSource:),Scene,SVG,Palette,Table,JSON,IESProfile(withstring:),ICCProfile,OutlineFont,BitmapFont(bdf,fnt, andresource:forms), andStrokeFont(jhfandresource:forms);ComputeKernel(entry:resource:withExtension:in:)andComputeKernel(entry:contentsOf:); andPhoneReference.picture(path:…),picture(resource:…),object(path:…), andobject(resource:…). The writersMesh.write(to:),Scene.write(to:),saveMesh, andsaveScenethrow anunwritableone where they returnedfalseand printed.IESProfileno longer prints a line on every refusal, since the sentence travels in the error. A name lookup (OutlineFont(name:)) still answersnil. In a sketch,loadImage("photo.jpg")!becomestry! loadImage("photo.jpg"), which stops naming the file and the reason, andif let photo = loadImage(path)becomesif let photo = try? loadImage(path). Sketch
Added
- Every example is checked to draw the same picture twice.
Scripts/check-determinism.shexports each example that runs on its own at the frame its still is taken at, twice under one--seedin two separate processes, and compares the pictures pixel for pixel;Scripts/preflight.sh --milestoneruns it. Why a run repeats lists what reaches past a seed: the system's dice, the clock on the wall, the order aSetwalks, and live input.
- An Ollin quick reference.
Docs/QuickReference.mdis 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.txtat 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.shwrites it from the pages, and preflight fails when it falls behind them. The reference offline
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.tubepicks one type's,ollin api Paramlists 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, sostrokeWidthpoints atstrokeWeight. It reads the listings underAPI/, so a name it does not know is not public. See The reference offline.
- A second visit to Monir Shahroudy Farmanfarmaian.
Examples/Recreations/MonirFarmanfarmaiangains two sketches.Geometricis 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.Convertibleis 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/VicenteRojogains two sketches.Senalesis 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.PiramidesYVolcanesis 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/VladimirBonacicgains two sketches.GFE164is 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.Random63is 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. WithsendsToWallon, both panels go out over sACN through anLEDMap.
- A second visit to Osamu Sato.
Examples/Recreations/OsamuSatogains two sketches from The Art of Computer Designing (1993).StraightLinesis 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.wordsets any word you type.TotemBuilderis "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-gridproofs a pile of them.
- Waldemar Cordeiro joins the recreations.
Examples/Recreations/WaldemarCordeirois new: the folder's first artist from Brazil, and its first recreation that reads a photograph.Derivadasis 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.Genteis 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'sSoundsuit), andShapes/Watercolor, drawn at its second frame since each frame takes it seconds. Examples
- 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 aSpatialHashsummed 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
- 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
- A
MeshFieldexports 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
--photokeeps every camera closed. The flag reached onlyCamera.orStill, so a sketch that started aCameraitself opened this machine's camera even under it.Camera.start()now throws under--photobefore 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
- A glow or blur radius under a pixel is noted, since it spreads nothing. The radius of
.bloom,.halationand.gaussianBluris 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, frompostProcessorfilteredalike, and the reference now says the unit. Effects
- 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.leaksfound the cycle on an export of the feedback example. Effects
- 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
- 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
- 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
- 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
drawTextundertextMode(.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 aBatch. 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
--skipbuilds a picture that carries from frame to frame. The warmup ran the sketch'sdraw()but rendered only anoClearcanvas, so aFeedbackpile, anAccumulator, aLineSpray's running mean, or a simulation field started the first written frame empty while the sketch's own state was--skipseconds 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
- Every tile of a contact sheet draws its feedback layer. In
--export-gridand--export-sweep, a sketch with aFeedbacklayer 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
- A frame with millions of marks in one pass draws them. A
LineSpraypast 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
--paramwins 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--paramon top of it, and the cue lands beforesetup()too, as--paramdoes, so whateversetup()builds from a parameter reads the cue's value. Cues
ollin docs,ollin examplesandollin apianswer in a tenth of a second. Each lookup used to go throughswift 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
- 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
.cubelook declaring a 256-node cube no longer sets aside 265 MB before its first line; an ASE palette swatch whose numbers are not numbers is skipped (it used to endColor(hue:), which now reads a hue that is not a number as red); an SVG with an arc or a transform past what aDoubleholds draws a line where it used to stop the flattener; glTF 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 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 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 keys, tunings, levels and loop points are held to the format's ranges; physics snapshots 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'sintreadsnilfor a number noIntholds;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 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.boundstook 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
- 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
.couldNotDecodefromAudioPlayer, and reads a many-channel file in stretches of a few megabytes. Audio
- 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.couldNotDecodefor such a map. It reads now. Synthesis
- 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, not2.4027931415929205), which is also what Save writes. Parameters
[0.11.0] - 2026-09-23
Added
- Mohamed Melehi joins the recreations.
Examples/Recreations/MohamedMelehiis new, after the hard-edge wave paintings of the 1970s, the folder's first artist from Africa.Wavessends 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.Flammestands 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 tsanbuilds 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 inScripts/tsan-suppressions.txt;Scripts/preflight.sh --milestoneruns it.Tests/README.mdsays what is in, what stays out and why, and what a run costs. The committed tree reports nothing. The test suite
- A second visit to Gego.
Examples/Recreations/Gegogains two sketches.Tejedurais 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.Chorrois 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 withinrangerings of its own, withwrapsfor a torus; the hex lattice sharesHexGrid's addressing, so the cellhexGrid(columns:rows:)[i]draws is sitei.Kuramoto(neighbors:)couples over any list of neighbor sites, and the settableneighborshands any crowd a graph.localCoherenceis the order parameter over each oscillator's own neighborhood, the map of where the crowd has locked. A crowd can hold severallayersof oscillators per site, each layer coupled over the same neighborhoods and a site's layers pulled together bylayerCoupling.ratesis 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; theSimulation/Kuramotoexample gains a honeycomb field (issue #25). Oscillators
- A second visit to Hassan Sharif.
Examples/Recreations/HassanSharifgains two sketches.OctoberLinesis 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.BodyAndSquaresis 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/NasreenMohamediis new, after the untitled ink and graphite drawings of the 1970s and of about 1980, the folder's first artist from South Asia.Registersrules 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.Diagonalsfloats 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/ManuelFelguerezgains two sketches, after El espacio múltiple (1973) and the lacquered reliefs that followed the aesthetic machine.EspacioMultiplebuilds 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.stageholds one state still,unlitshows that the relief seen straight on is the painting again, and--export-svggives the plan.RelieveLacadocomposes and weighs a design exactly asMaquinaEsteticadoes, 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;viewwalks 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
noClearcanvas 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 tomakeFeedback(precision: .float32). Accumulation
- The API diff reads a conformance to the standard library. A type that became a
Collectionand dropped its owncount,firstandisEmptyread as three removals and a rename nobody made (countisendIndex), a major and three shim violations from 1.0 on, though every call site still compiled.Scripts/api-diff.shnow reads such a member as moved onto the protocol that still supplies it, from a table of whatSequence, theCollectionfamily,Equatable,Hashable,Comparable,Identifiable,CustomStringConvertibleand 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
- A still counts its frame at the rate
--fpsnames.--export,--export-exr,--export-platesand the vector exports read--frameand never--fps, so--frame 30 --fps 10was 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
- An extension's
afterFramefires in an export. The headless drives never called it, soFrameInfo.profilecould 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.
- A
--paramvalue is there whensetup()runs. It landed only aftersetup(), so a count built there, or an extension registered behind a switch, read the declared default. It is now applied beforesetup()and again after it, sosetup()reads the run's value and asetup()that assigns the parameter itself still does not undo it. The canvas follows: the value is on the sketch beforecanvasSizeis read, so acanvasSizethat picks its size by a parameter exports and opens at the run's size, where it took the declared default. Parameters
- 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 reading1e300, 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 asnilor are ignored like a value of the wrong kind. A WebSocket frame announcing a length one short of what anIntholds 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 reading1e300trapped the same way throughint. The test suite
- The aesthetic machine weighed a quarter disk off center, and drew its ring as a cut disk.
MaquinaEsteticaplaced 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 throughdrawPolygon, 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
.byLengthsampling, 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.
- 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
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:)throwsVideoError.fileNotFoundfor a file that is not there, as thepath:andresource:doors already did, and a file that opens but is not a movie says so in the newVideoPlayer.unavailableReasoninstead of never producing a frame.Record3DRecording.pose(at:)throwsframeOutOfRangeoutside the recording, asframe(at:)does, and isnilonly for a frame with no recorded pose.ImpulseResponse.load(_:),.resource(_:withExtension:in:)andinit(contentsOf:),GrainSource(named:withExtension:in:)and(contentsOf:), andSampledInstrument(sfz:in:)and(contentsOf:)throwAudioError(resourceNotFound,couldNotDecode, and the newempty) where they returnednilbeside a printed note. An audio engine that will not start is written to a newunavailableReasononSynth,ToneandAudioPlayer, cleared by the next start that succeeds, whereSynth.start()printed andTone.play()andAudioPlayer.play()swallowed it;AudioInput.start()still throws for what it can see and now writesunavailableReasonfor a refused, denied or restricted microphone, which used to leave it silent.PhysicsSnapshot(data:)and(resource:withExtension:in:)throwPhysicsSnapshot.Failure(unreadable, and the newresourceNotFound, each printing as its sentence) where they returnednil;World3D.restore(_:)throwsunreadablewhere it printed, andWorld3D.load(contentsOf:)throws where it returnedfalse.World3D.addVehicle,addRagdoll(from:),addSoftBody(from:),addRope(through:)andaddTensegrity(_:)throw the newPhysicsError.unbuildable, whose text says what was needed, where they returnedniland printed it. A sketch that stored one of these in an optional writestry?at the call and keeps its shape. Video, Audio, Synthesis, 3D physics, Record3D
- One shape per failure, second half: the failures that had no shape, and the errors nothing could reach.
SerialPortandFirmataBoardgainlastError, the sentenceopen()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 behindisOpen.SyphonClientgainsunavailableReasonfor a source that was not there when it looked, no Metal device, a refused connection, or a source that has since gone away.LinkClockgainsinterfaceCountandunavailableReason, so a running clock that could open no network interface says so instead of reading exactly like one alone on the network.OSCQueryServerandRemoteInspectorgainisRunning,unavailableReasonand astart()that serves again afterstop()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, andRemoteInspector.stop()no longer leavesboundPortset.VirtualCameraErroris internal, since no public call ever produced it;VirtualCameraServer.unavailableReasoncarried its sentences already. The fiveErrorenums nested inModelTracker,PointSegmenter,ConceptTracker,DepthTrackerandDepthClipare oneVisionErrorwithunavailableand the newfailed; acatchnaming one of the old types namesVisionErrornow.PersonSegmenter.detect(in:quality:)andSaliencyTracker.detect(in:mode:)return their value rather than an Optional and throwVisionError.failedwhen a matte or heat map cannot be converted, where a conversion failure used to come back asnil;SubjectSegmenter.detect(in:)isnilonly 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 inunavailableReason. Serial, Syphon, Link, OSCQuery, Remote, Vision
- A number that does not fit reads as
nil.OSCArgument.int,MQTTMessage.intandint(named:),RoomMessage.int, and a table'sint(_:)returnnilfor a number that is not finite or is too large for anInt, where they trapped before; a parameter restored from a stored.numberof that size keeps its value. OSC, MQTT, Room
- 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.sourceSizenow 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 gainsLensFlare.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.doubleGaussis 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(withstreakLength,streakAngle,streakTint) is the long thin line cylindrical glass fans a light into, the one an anamorphic lens or a streak filter throws;LensFlare.dirtis grime on the front element, each speck a soft picture of the iris that glows where it sits near the light; andLensFlare.halo(withhaloSize) 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 usesLens.standard, the double Gauss multicoated at f/8, where it used the Heliar wide open (name.heliarto 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; andsourceSizedefaults to0.015(was0.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
[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,withStatefor both stacks, anofFboas a layer,ofShaderas oneshadefunction in Metal, the camera as a call, the C++ loop-by-reference habit, the addons as satellite modules, projectGenerator asollin new, and rebuilding as live reload. From OPENRNDR: Kotlin to Swift, the program as a sketch, helpers that extendSketchrather than the drawer, the value types,rotatetaking 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
- A black hole, drawn by following light past it.
Examples/Shaders/BlackHoletraces 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, andBlackHoleLensTestscompiles 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
Contourcan 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 onShape), and walks itself backward as aContour(reversed()).Examples/Shapes/OverUnderweaves a tangled loop over and under through its own crossings. Geometry
- A homage after Vladimir Bonačić.
Examples/Recreations/VladimirBonacicrebuilds two of the dynamic objects he made in Zagreb between 1969 and 1971, lamps driven by the arithmetic of Galois fields.DynamicObjectis 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 classrow ^ 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.NamaFriezeis 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. SetsendsToWalland 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
Contouris its points, aShapeis its contours, aGrid,HexGrid, orTriangleGridis its cells, and aPolyominois 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 forpoints/contours/cells. A grid works each cell out as it is asked for, so looping one builds no array at all, andcellsis still there for when an array is what you want.Contouralso gained a geometriccontains(_ point:), the same questionShape.contains(_:)answers; askcontour.points.contains(p)for the vertex-membership one. Geometry
- Timecode compares.
TimecodeisComparable, 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
- 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, andwithoutLights { }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 oneLightvalue to the frame's own rig. Lamps set the ordinary way behave exactly as before: the frame's rig still reaches every solid wherever indraw()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
- The depth-of-field aperture has a shape.
Bokeh.aperturedecides what a point too far from focus turns into:.roundas 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 anyImage, 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.rounddraws exactly what it drew before. Depth of field
- Surfaces in the depth-of-field spray.
SprayQuadputs a face in aLineSprayscene beside its wires: the parallelogram two edges span from a corner, sampled over its area, set throughsetQuads(_:). Lines and quads scatter into one accumulator and print as one picture. A quad'slightis the whole quad's, not a brightness per unit of area, which is the rule a line already kept.LineSpray.pictureis an image the quads read through each quad's ownpictureBounds, 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
- A homage after Gego.
Examples/Recreations/Gegotakes up the two bodies of work she is best known for.Reticulareaties 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 throughproject, thinner and paler as they go away from you.DrawingWithoutPaperhangs 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.
MIDIFilereads 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 withbeats(at:)andseconds(at:), plus control changes, pitch bends, programs, names, and markers. ASynthwill also write down what it is asked to play:startRecording(tempo:name:)andstopRecording()hand back a take. It lives inOllinAudio, since a file arrives as notes rather than as messages. MIDI files
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.hearingRangeishearingDistance(it is a distance, and a hearing range is a band of frequencies),AudioAnalyzer.process(_:)isanalyze(_:),Wavetable.lengthissamplesPerCycle,Patch.modulated(by:index:)takesamount:,Compressor.makeupismakeupGain,Equalizer.lowEdge/highEdgearelowFrequency/highFrequencybeside themidFrequencyalready there,GrainCloud.scatteristimingJitterbeside itspositionJitterandpitchSpreadsiblings,SpeechListener.captionWordsismaxCaptionWords,Sonification.levelsislevelRange, andDetectedPitch.noteisnearestPitch, since it is the rounded twin of thepitchbeside it andNoteis a different type in the same module (AudioAnalyzer.noteandAudioSource.notefollow it). Vision: the[Classification]lists onImageClassifier,ConceptTracker,ModelTrackerandModelOutputareclassifications, sincelabelsis a vocabulary elsewhere in the same module;ConceptTracker.confidence(of:)isshare(of:), because it is each phrase's share of a set that sums to one rather than a confidence;PointSegmenter.Pick.scoreisconfidencelike every other result in the module, and itsisWorkingisisPicking;TrajectoryTracker(trajectoryLength:)isminObservationCount:;ContourDetector.contrastAdjustmentiscontrastFactor. Elsewhere:ScreenSource.window(title:)iswindow(matching:)(it matches a substring);BluetoothDevice(named:)isBluetoothDevice(matching:)for the same reason,BluetoothReading.isOnisboollike every other value reader, andBluetoothCharacteristic.standard(for:)isknown(for:)since it answers for uncatalogued ones too;HapticEvent.strength(at:)isintensity(at:), the word the type already stores;controllerMotion(_:)iscontrollersReportMotion(_:);FirmataBoard.setSamplingInterval(_ milliseconds: Int)issampleEvery(_ seconds: Double), so the unit is in the type rather than only in the documentation;MQTTTopic.isValid(_:)isisValidFilter(_:), which is what it checks;LaserSafety.stationaryLimitismaxStationaryPointsandLaserProjector.isConnectedisisPlaying, which is what it reports;Timecode.advanced(by:)isadvanced(byFrames:), sinceadvance(by:)means seconds everywhere else;World.pixelsPerMeterisunitsPerMeter, matchingWorld3D;Rope3D'sthicknessisradius, which is what it always was, soWorld3D.addRopetakesradius:too;Wheel3D.spinisrollAnglebeside itssteerAngle, withspinRatethe rate;Character3D.move(_:)iswalk(at:), since it sets a standing velocity rather than carrying anything;dragSoftGrab/releaseSoftGrabaredragSoftGrip/releaseSoftGrip, after theSoftGripthey take;Room.nameispeerName, because it named this machine and not the room;Room.problemislastError, the spelling the rest of the framework uses;PhoneState.statusisstatusMessage; andPhoneBody.isJointTracked(_:)isisObserved(_:), 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
nilmeans a value is merely absent. Four places that broke it are fixed.MQTTClient.lastErroris cleared when the line comes back up, so it no longer says a working client is broken forever.EtherDreamFinder.start()throwsStartError.badPortwhere it used to return in silence one line above atry.CameragainedunavailableReason, so a refused permission prompt or a failure after it says so instead of leavingframenil with no reason;start()still throws for what it can see itself. AndRecord3DRecording(url:)reports a missing file asRecord3DError.fileNotFound, the same error itspathtwin 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.mdlisted 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.shgenerates the projectollin new --remotewrites, 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--selftestproves its ink test goes red on a blank frame. - A homage after Monir Shahroudy Farmanfarmaian.
Examples/Recreations/MonirFarmanfarmaianfollows 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.usdaholdssides * rings * ringspieces, 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 abandinside 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 ondevice.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.PhoneScreenreportsisShowingand lets go of the pointer withdrivesPointer.Examples/3D/Phone/PhoneCanvaspaints 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.shreads every declaration in the public listings underAPI/and fails when nothing underDocs/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: theMeshgenerators (.klein,.lathe,.mobius,.superellipsoid, the Platonic solids),Material'srimColor,rimSharpness,subsurfaceColorand 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.dpadand 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 inScripts/check-api-pages-allow.txt. One stale label turned up with them: theRectangleDetectorinitializer's size argument isminSize:, which the Vision page had been spellingminimumSize:. - A homage after Owen Schuh.
Examples/Recreations/OwenSchuhreads 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 aSimFieldin place of a built-in rule:shade(uv, info)returns the cell's next state fromcell(info),cell(info, dx, dy),sampleRaw(info, p), and the field's extrainputsread asinput(info, i), stored as data with no color conversion. An optionalinject:kernel decides how a drawn mark enters the field (mark(info)), so a circle or a line drawn inwithFieldis that shape stepped by your rule. Every field now has anedge(FieldEdge:.wrapping, the torus it always was;.clampedwalls; or one axis of each), which the twelve neighbor-reading sims honor too, and aprecision(.float32for a state that counts).SimField.snapshot()reads the field back as aFieldSnapshot, andFilter.arrows(spacing:scale:color:width:)draws any two-channel layer as arrows. Effects, theSimulation/Windexample, and Guide chapter 19. - The API diff reads as a version.
Scripts/api-diff.shcompares the public listings underAPI/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'sDeprecations.swift.API/README.md - A GIF is written as it is drawn.
--export-gifand--export-loopput 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 isGIFWriter, which a sketch or an extension can drive with images of its own:GIFWriter(path:width:height:),append(_:delay:),finish(), withpalette:andloops: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: .perFrameonexportGIF) 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/HassanSharifreads 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.
Complexis a point of the plane that multiplies:Complex(1, 2),1 + 2 * .i, the polarComplex(magnitude:argument:),.unit(angle)to turn by an angle,magnitude/argument/conjugate/reciprocal, andComplex.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 aNumericvalue, solet c: Complex = 3is the real number three,2 * zdoubles, and an array sums underreduce(0, +);Vector2(z)andComplex(v)cross to the canvas and back. The shader library gains acomplexmodule with the same arithmetic on afloat2(cmul,cdiv,cexp,clog,cpow,csqrt,csin,ccos, and the rest, under C's names),complexPlaneto read a layer as the plane with the imaginary axis up, anddomainColorto 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/ComplexPlanecolors the ratio of two moving points in a shader and lays the circles that ratio is made of over it fromdraw(), worked out withComplex, and they land on the rulings;ImaginaryLogpaints the imaginary part of that ratio's logarithm through a cosine palette, with the branch cut showing as a seam;Meromorphicbuilds a ratio of two cubics from six roots that move asComplexvalues 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 page. - Curl noise in space.
curlNoise(x, y, z)andcurlNoise(_ 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 ofnoise, and the shader library has the matchingfloat3 curlNoise(float3). TheRendering/LineSprayexample bends its sphere of rings with it. Noise. - The noise fields as a value.
noiseFieldsis the sketch's whole family of fields, Perlin, simplex and Worley with everynoise,fbm,curlNoise,simplexNoise,worley, looping and tiling form on it, as oneNoiseFieldsvalue seeded exactly as the sketch is. A sketch's ownnoisecan only be called on the main thread; a value can be handed to every core, soDispatchQueue.concurrentPerformor a task group can read the same field the sketch reads, andnoiseSeedstill reaches it.NoiseFields(seed:)makes one anywhere. Noise; 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;linearRGBreads a color back as light inDouble. The two curves between them,Color.srgbToLinearandColor.linearToSrgb, are public now, the same curves the shader library runs per pixel. Color. - A
SprayLinetakes 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.
Changed
SprayLinestores light, not a color and an intensity. Itscolor,endColor,intensityandendIntensityproperties are gone;lightandendLighthold the linear RGB radiance they used to be multiplied into. Thecolor: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.setLinesis 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--settleinstead 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.
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.
.photo1080is 1080 by 1440, the frame a camera shoots natively stood up tall, and.photo1080.landscapeis 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. - Your seed reaches the standard library's dice.
randomnessis the sketch's own generator, and handing it to any call that takes ausing: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 streamrandom()draws from, not a second one beside it, and it is also what the seedable generators take, soWaveFunctionCollapse.solve,LSystem.expanded,ShapeGrammar.runandWangTiles.fillnow follow the sketch'svariationinstead of needing a generator of their own. Seeding still goes throughrandomSeedorseed, which are what record the recipe an export embeds. Random; 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)andpalette.randomElement(using: &randomness)all work now, with no.colorsin the way. The wrappingpalette[i]is unchanged and still takes any integer, where iteration stays inside the set; the subscript stays read-only, sincepalette[-1] = colorcould as easily mean the last swatch as a mistake, so a change still goes throughcolors. Color; Guide Ch 2 § Kits you carry: Palette and Ramp. Vector2andVector3are one surface now. Everything that means the same thing in two dimensions and three is written once, on aVectorprotocol both types conform to:lengthandnormalized,distance(to:),lerp(to:_:),limited(to:),projected(onto:),with(x:), the arithmetic, and the constants. Two things follow. A helper you write againstVectortakes either kind of point, sofunc midpoint<V: Vector>(_ a: V, _ b: V) -> V { a.lerp(to: b, 0.5) }is written once, andcentroidnow 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.crossis the one to know: aDoublein the plane, because there is nowhere perpendicular to point, and aVector3in space, which is how a surface gets its normal.Vector2keepsangle,perpendicular, the signedangle(to:)androtated(by:);Vector3keepsunitZandxy. 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; 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, anddevice.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, anddevice.latestStatesays 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; Guide Ch 27 § Saying it from the sketch;Examples/3D/Phone/PhoneMarkers. - A crowd that makes room.
Crowdwalks 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,addObstacleandaddWallput in rooms and corridors that hold even in a jam,preferredVelocityreplaces goals with a rule of your own, andisJammedmarks 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.drawCrowddraws them,Examples/Simulation/Crowdhas 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@Savedproperties come across. A long exposure ten minutes into a set survives the edit. Change a declaration,setup(), or anythingsetup()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@Savedis how a sketch says what should travel. - A homage after Casey Reas.
Examples/Recreations/CaseyReascarries 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. InTouchingthe 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. InPlanesthey 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 atreachand beyond. Every light with a position takes it (pointLight,spotLight,rectangleLight,diskLight,tubeLightand the matchingLightfactories),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; 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)andVoice(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 atspeedalone, sospeed: 0holds one moment of a recording for as long as the note lasts andsynth.grainScrubdrags that moment through the sound while it is still sounding.size,density,positionJitter,pitchSpread,panSpreadandscatterare the rest of the cloud,GrainShapeis the cut each grain is made with, and.cloud,.smearand.rainare three places to start.GrainShape.level(at:)andSynth.grainCountdraw 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; Guide Ch 29 § A sound in pieces: grains;Examples/Audio/Grains. - A sketch as a widget on the desktop.
ollin new <name> --kind widgetwrites an app with a WidgetKit extension packed inside it, and./build.sh --installputs 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 withoverride var widgetTimeline: WidgetTimeline { .every(minutes: 15, count: 4) }. The clock is the time of day:timeis seconds since midnight of the moment being drawn, so a quarter past two draws the same picture today as tomorrow,deltaTimeis the spacing, andframeCountis 1 because every picture is the first frame of a sketch of its own.dateis 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'sscheduledPeriodnow 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; 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.mdcarries 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.shnames the lists and points there. Two changes came with it. The desk'sshardrun 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.serializedorders 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 doctorwere 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, Hello Ollin. - 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-widthpays back with the square, since the height follows it, and--export-videohas 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,updateAttractorFlowandupdateEvolutionare nowstepSwarm,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 itstep.BeatFollower.update(at:)isadvance(to:)for the same reason, which leaves noupdatein the public API. - A smoothed value jumps rather than sets.
$x.set(v)is$x.jump(to: v)on@Eased,@Smoothed,@Sprungand@Param, and on theDampedSpringandOneEuroFiltervalues 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:)israndomVector(innerRadius:outerRadius:), besiderandomVector(in:). It returns a random point, not a ring, and the old spelling was the same oneSDF.ring(innerRadius:outerRadius:)uses for a shape that is one. - An index of refraction is
ior:everywhere.refractedRays(through:from:index:closed:)andcaustic(through:from:index:closed:)takeior:, which is whatMaterial,MeshMaterial,LensInterfaceandFilter.thinFilmhave always called it.indexreads 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:)(wasinvert:);.mirror(vertical:flipped:)(wasflip:);.tile(count:mirrored:)(wasmirror:);elementaryCAandtotalisticCAtakewraps:(waswrap:);map(_:_:_:_:_:clamped:)(wasclamp:). The surface already spelled it that way in fifty-three other places,inverted:andmirrored:among them. - A listing that asks the system is
available…().MIDIInput.sourcesisavailableSources()andMIDIOutput.destinationsisavailableDestinations(), both of which ask Core MIDI on every call;ScreenCapture.apps()isavailableApps(), beside theavailableDisplays()andavailableWindows()it already sat next to. Going the other way,DMXReceiver.universeNumbers()is now the propertyuniverseNumbers, since it reads state the receiver already holds. detailMappedfades by anamount.Mesh.detailMapped(_:normal:scale:amount:)(wasstrength:) andMeshMaterial.detailAmount(wasdetailStrength). It is how much of an effect shows, andstrengthis the word for a force.- One
Easinglabel, not two.Timeline.to(_:in:curve:)and theCameraMovefactoriespushIn,pullOut,tiltandrevealtakecurve:, which is what@EasedandParamSmoothing.easedalready 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
drawFramecall draws onceCamera.orStillstands 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 OllinMQTTand 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, soMQTTMessagereads the three things devices actually write: a decimal number, a switch word likeONoroffline, and a JSON object's field throughnumber(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: aMQTTWillthe 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; 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.touchesgives the two reads every input gives:downis 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, andlatestAircarries 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; 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 doctorasks the questions a broken setup otherwise answers by failing somewhere else: macOS against the version Ollin needs, whichswiftcis 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 theollinon 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 installnow also puts zsh completions in place: the subcommands afterollin, 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 agit pullcarries a new flag to the prompt with nothing to re-run, andollin completionsprints it for putting somewhere yourself. --list-paramssays what a sketch declares. Every@Paramwith 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'ssetup()first and honors anything given beside it, so--list-params --param radius=200 --cue duskdescribes that run rather than the file. Each value is printed in the spelling--paramreads 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 = .filmand@Param var sheet: PaperSize = .a4now 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 whatPaperSize(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 onmakeSimField, both in theSimulation/Automatapicker (SmoothLife as its smooth rule) or the newSimulation/PredatorPreyexample, 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-buildwaits for the one optimized build instead, for a sketch that accumulates state and should not start over twice. In the engine,SketchSession.landsPlainBuildFirstandisOptimizing; 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
.extendedframe 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, soBokeh'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/RyszardWiniarskireads 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,widthcanvas pixels wide, the cartoon's outline over.toonor 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/outlineColorandoutlined(_: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)(andLightingPreset.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, andheadlight()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. TheInkedexample under3D/Materials, the Inked figure and a Chapter 21 section, both sections on the 3D page. - A step sequencer, and an arpeggiator.
StepSequenceris 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, andswing,rate,gate, andseedon the bar. Write one out as"36 . . 36 . . 36 .", or put one note on every strike of aRhythm.Arpeggiatorplays whatever is held right now one note a step, in anyArpeggio.Patternover any number of octaves, withhold(_:)andrelease(_:)ornotesset from a controller's held keys, andlatchesto keep the last chord playing. Both hand backScheduledNotes, each with the beat it lands on, andSynth.play(_:tempo:from:)plays them to the sample. Underneath,Synth.play(_:velocity:for:after:)gainedafter:, a wait in seconds that lands a note on its own sample rather than on the next block, live and in an export. Composition; 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.shreads every listing underAPI/against the naming rules on each preflight (limits aremin…/max…, a publicseed:is anInt, a loader spellswithExtension:, a configuration Boolean reads as an assertion,advance(by:)againststep(_:), and the rest in its header), with the exceptions and their reasons inScripts/check-api-names-allow.txt; its first run named the fifty-three renames under Changed below.Scripts/check-snippets.shnow compiles every Swift block in theDocs/pages beside the Guide's, the fragments that cannot stand alone recorded inDocs/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.latestFlowreads the way the Mac'sFlowTrackerfield reads,vector(at:in:),samples(in:every:), andaverageFlow(in:)over the sameMotionField, and addsinterval, the time between the two frames measured, so a vector over it is a speed;latestFlowFrameis the frame it was measured on.MotionFieldmoved fromOllinVisioninto the core and is now built from a closure, so a source of your own can produce one; a sketch that importsOllinsees it unchanged. Phone; 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
FrameRateitself, 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 whenOllinApp.image(of:frame:fps:)took aFrameRate, 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@Paramas 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, andnamespace()hands the sketch the same tree as anOSCQueryNode.Examples/Integration/OSCQuerydraws the tree it serves, and Chapter 28 shows it. OSCQuery - A color on the OSC wire.
OSCArgumentgained.color, the OSC 1.1rtype, four bytes read and written as aColor; an exhaustiveswitchover 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 atSim.isingCriticalTemperatureclusters come in every size.IsingSpinnames the two levels for the brush, and the rule joins theSimulation/Automatapicker.Kuramotois the model of synchronization on the CPU: a crowd of oscillators each at its own pace, each pulled toward the phase of the others, withcoherenceandmeanPhaseas the order parameter,criticalCouplingfor where a locked group forms, alag, and a ringrange.Examples/Simulation/Kuramotois a meadow of fireflies falling into step, and Chapters 12 and 19 teach both. Simulation fields, Coupled oscillators - 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.
CodeCompleterin 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
PixelFieldexample. - A homage after Vicente Rojo.
Examples/Recreations/VicenteRojoreads 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), withamountthe spread at mid-gray,sizethe clump in pixels, the mean of a flat region kept, and a fresh pattern per seed. Both cross to the web page.Examples/Effects/FilmLookdraws a night street through them, and Chapter 16 teaches them. - The note being sung. Every audio source now reads the note it hears:
pitchis 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;noteis the short form; andchromais 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. TheAudio/Tunerexample follows the bundled violin, or the microphone with--mic, andAudio/GuitarTuneris a guitar tuner, six strings and a needle, with a pretend guitar tuning itself until the microphone hears one. Pitch and pitch classes - 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
--paramwith 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:)isminLength:;ILDAFile.maximumPointsPerFrameismaxPointsPerFrame;EtherDreamDAC.maximumBatchismaxBatch;EtherDreamDevice.maximumPointRateismaxPointRate;LaserSafety.maximumBrightnessismaxBrightness;ContourDetector.detect(in:…maximumDimension:)ismaxDimension:;HandTracker'smaximumHandCount(the initializer, the property, anddetect(in:maximumHandCount:)) ismaxHandCount;TrajectoryTracker'sminimumObjectRadius:/maximumObjectRadius:areminObjectRadius:/maxObjectRadius:. - Every public
seed:is anInt.Boids,AntColony,ContinuousPacking,CrackGrowth,DielectricBreakdown,DifferentialGrowthand itsline/ring,DiffusionLimitedAggregation,ForceLayout,Kuramoto,Meanderand itsline,MeshGrowth,MeshReactionDiffusion,NBody.cluster/.disk,Vehicle,Population,Heightfield.diamondSquare/.eroded, andpopulation(count:genes:seed:)onSketchtook aUInt64; they take anInt, soseed: variationneeds noUInt64(...)around it.SplitMix64(seed:), the generator itself, keeps itsUInt64state word. - Configuration Booleans read as assertions or state.
Fit.Result.settledisisSettled;CloudAlignment.appliedisisApplied;PathTracing.denoiseand itsdenoise:label aredenoises;DifferentialGrowth.fixedEndsand its label arehasFixedEnds;SchottkyPairing.fromExterior/toExteriorand their labels aremapsFromExterior/mapsToExterior;Wheel3D.drivenisisDriven(thewheel(at:…driven:)label stays). - One spelling per kind of step.
PingPong.advance()andPingPongTexture.advance()areswap(), the ping-pong's own verb;TimelineModel.step(byFrames:)isstep(_:). - How much of an effect is
amount.Generator.grainGradient(…intensity:…)andGenerator.pulsingBorder(…intensity:…)areamount:;intensitystays for light. - A loader spells
withExtension:.PhysicsSnapshot(resource:extension:in:)isPhysicsSnapshot(resource:withExtension:in:). - The state follows the verb.
SyphonClient.isActiveisisConnected, andSyphonClient.stop()isdisconnect(). - A destination is a path.
Automation.write(to:),Take.write(to:),PhysicsSnapshot.write(to:),World3D.save(to:), andILDAFile.write(_:to:name:company:projector:)take aStringpath beside theURLthey 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.
drawPlanefaces 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
- A note under the finger.
noteOnhands back aPlayingNote, andbend(_:semitones:),press(_:_:), andslide(_:_:)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);pitchBendbends the whole instrument. On the MIDI side,MIDIInput.heldNotesreads 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, andMIDIOutputsends bends, pressures, registered parameters, and a zone. Expression, Per-note expression - 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.soundsgives the same level and trigger reads the Mac'sSoundClassifiergives,confidence(of:),topClassification, the drainingevents(), andtimeSinceHearing(_:), over the same values, with the threshold set on the Mac.SoundClassificationandSoundEventnow live in the core, soimport Ollinalone spells them. What the phone hears - A frame rate you can name. Every export's
fps:takes aFrameRate. A number still stands in, sofps: 60reads as before, and.film,.pal,.ntsc,.ntscFilm, and.ntscDoublename the rates whose numbers are awkward, kept as exact fractions so a clip at.ntsclands every frame on 1001/30000 of a second.--fpstakes a name, a number, or a fraction. Frame rates - A sheet by name for the machine exports.
GCode(.plotter(), paper: .a4)andDXF(paper: .a3)size a drawing for a sheet with a margin clear on every side, holding its height as well as its width.PaperSizecarries the ISO A series and the US three, turns with.landscape, and reaches the canvas presets throughcanvasSize(dpi:);--gcode-paperand--dxf-paperdo the same from the command line. A named sheet - The screen angles, said out loud.
PrintSeparation.screenAngles(for:)returns the angles a separation's halftone gives your inks, andProcessSeparation.screenAnglesis 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 withWireworldCellnaming its four states, and.schelling(preference:vacancy:mobility:passes:seed:)is Schelling's board of two kinds sorting itself from a mild preference, withSchellingCellfor the brush; both join theSimulation/Automatapicker. Simulation fields - 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
- A homage after Sol LeWitt.
Examples/Recreations/SolLeWittcarries 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
.cubefile.ColorLUTreads 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 - 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, andstretched(by:)on aRecordingor anImpulseResponsegives it back longer at the same pitch. The two that work in the spectrum
Changed
fps:is aFrameRaterather than aDoubleon every export entry (OllinApp.image,export,exportSequence,exportVideo,exportGIF, the vector, spatial, web, sheet, and separation exports). A literal compiles unchanged; aDoublevariable needsFrameRate(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 isamount:,hopfBases(spiral:)forspiralCount:,frameRendered(_:_:)forframeRendered(_:image:),Mesh(resource:extension:in:)forwithExtension:, 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.shrecords them, and the manifest moved toMedia/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> --exra folder of them. Red, green and blue hold the light itself, alpha holds coverage, and a 3D camera adds aZchannel. In code,OllinApp.exportEXR. Linear frames - Solids cut each other.
union(_:),intersection(_:),subtracting(_:), andsymmetricDifference(_:)on aMeshare the four set operations a filledShapealready had, carried into space. What comes back is an ordinary closed mesh, and cutting is CPU geometry, so cut insetup(). Cutting solids - Things break.
fractured(into:seed:)cuts aShapeor aMeshinto 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, andWorld.remove(_:). Fracture - The site tells a search engine what it holds.
/sitemap.xmllists every page the build writes, each at the address its own canonical link claims, and/robots.txtnames 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 adeltaTimeof zero, which put the start of the clock on the crossing rather than before it; it borrows a nominal frame now, andafter(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.shbuilds 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
.mdin place of.html, and/llms.txtis 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 --remotewrites, could not useOllinSyphonat all: the build stopped at resolution withthe 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.shrenders 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, soseek(to:)and thensnapshot()hands back exactly the moment asked for. A still-imagedetect(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.--phototakes the picture even where a camera would have worked. StillFrames. One picture published as both aVideoFeedand aFrameSource, 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/ColorVisionand 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.handsjoins the bundled set, two open palms against a dark ground with every finger separated, andVision/HandTrackingreads 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.dancejoins 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/OpticalFlowandVision/ContourTraceread 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,.talaveraand.stonejoin the bundled set. The page is what adaptive thresholding exists for, the talavera repeats seamlessly for3D/Materials/Triplanar, and the stone carries real relief, so3D/Materials/Parallaxreads one photograph as both its color and its height map. - Two landscapes at dusk, and the first wide pictures in the set.
SamplePhoto.headlandand.boatsjoin 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.cityjoin 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:)andImage.cropped(toAspect:). The companion toresized: 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.breakfastand.deskjoin 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.handstandjoin 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 OllinSamplePhotosandSamplePhoto.portrait.load()hands a sketch a real picture, each a 1600-pixel square with itscreditcarried 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.tiltis how far the pen leans, so a chisel drawn that way runs thick one way and thin the other;stylus.twistis the barrel turn,stylus.isEraserwhich end is down, andstylus.isNearbycovers hovering. Everything reads zero under a mouse, and the values are recorded into a take.stylus.tiltIsAvailableanswers the waypressureIsAvailabledoes. - 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.creaseAngleis the dial between a drawing and a wireframe,Mesh.transformed(by:)places each mesh first, andhiddenkeeps 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.makeupbrings a compressed phrase back up, andholdkeeps 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, anddirectionsare 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/Permutationsdraws 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.
TUIOReceiverreads a tangible surface over the network:cursorsare touches,objectsprinted markers with the symbol and angle read off them, andblobsthe 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, soimport OllinOSCis all it takes. - Fog as one value.
fog(.mist)takes aFog: a color, aveil(how much of a surface at the camera's target distance is fog) and apooling(how fast it thins with height), so the same preset reads alike over a tabletop and a valley. Five presets, and the barefog(_:density:heightFalloff:)is unchanged..haze,.mist,.thick,.groundMistand.nightare the presets,tinted(_:)takes another color, and a@Paramof the type puts them on the inspector's menu. - Cues.
saveCue("night")keeps every parameter's value under a name andcue("night", over: 2)brings them back over a fade. The live hosts keep them inSketch.cues.jsonbeside the sketch, the performance host calls one from a MIDI program change or/ollin/cue, and--cue nightrenders an export at a saved look.nextCueandpreviousCuewalk 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 proRes4444or 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, andmouseX/mouseYhold where it landed. Every host takes the drop. - DXF, a drawing for the shop.
--export-dxf panel.dxfwrites 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.--hatchwrites fills as line work, andDXF(width:)andDraftingare 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.rateanddepthare on every one, and amixof 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.iterationssets the level of abstraction.radiusis how far across an edge the shock reaches,flowhow far each round smooths along it, andthresholdthe 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.relaxationscales the step so the basins swell, spiral, and shed islands.phaseturns the palette, and a pixel the method never brings home is paintedtrapped. - A homage after Frieder Nake.
Examples/Recreations/FriederNake/HommageAPaulKleedraws 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-svggives the plotter drawing back. - A board with no firmware of your own.
FirmataBoardinOllinSerialspeaks Firmata over a serial port, so a board running the StandardFirmata sketch every Arduino IDE ships answers pin by pin:analog(0)anddigital(2, pullUp: true)read, andwrite(9, 0.5)andwrite(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, andmessages()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, andScripts/api-surface.shfails 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, andmirrored()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.Paletteis 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 anImpulseResponse: 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 turningmixnever cuts a tail that is sounding. AnImpulseResponsecomes from.load,.resource,.decay(seconds:damping:), or a rule;synth.reverbstill reaches it, andEffect.Kindgains.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, intotimecode,seconds,frameRate,isPlaying, andisReversed. TheTimecodevalue does the drop-frame arithmetic and both wire forms, andMIDIOutput.send(timecode:)andsend(sysEx:)cover the other direction. MIDI - 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 - A homage after Josef Albers.
Examples/Recreations/JosefAlbers/Homageputs 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-gridprints a wall of them. - Search on the website. Every page
ollin sitewrites 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 (MIT), pinned to one release by its hash. - Embroidery, a frame as stitches.
--export-embroidery leaf.dstwrites the frame as the stitches an embroidery machine sews, in the Tajima.dstfile 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.radiusis the brush size andstretchhow far an edge can draw it out.sharpnessis 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), andink.complementderive 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 onewavelengthin pixels, a bandbandwidthwide around it, and one direction or every direction. Brushed metal, wood grain, silk, and rippled water at a turn of the parameters.impulsesis the quality andseedpicks the field. The same field isgaborNoise(x, y, ...)and theGaborNoisetype on the CPU, andgaborNoise(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.thresholdandsoftnessare the cut and its ramp.radiusis the line scale,sharpeninghow far edges are pushed over the tone, andforeground/backgroundthe 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:)andShape(curveThrough:spline:)take the same choice, andHobbySpline(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 aphase: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:)understrokeCap(.round)is a dotted line, andContour.dashed(_:)/Shape.dashed(_:)make the same cut as geometry.drawCircle,drawRect,drawTriangle,drawNgon,drawStarand their neighbors dash too while a dash is on. - An app for the phone, from the generator.
ollin new <Name> --kind ios-appwrites a sketch as an iPhone and iPad app: the sketch, a host that owns the entry point, the property list, and theproject.ymlthatxcodegenturns into the Xcode project.--team <id>sets the signing team.--team <id>orOLLIN_TEAMsets the signing team;ProjectRequestgainsteamandCapabilitygainsstaysOnTheDesk, 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.
Tempois beats per minute with abeatsPerBar:tempo.beats(at: time)is what aStepCounterwants each frame andtempo.seconds(of: .eighth)what a synth'sfor:wants.NoteLengthnames the lengths on a stave, and a number stands in for either.NoteLengthruns.wholedown to.thirtySecondwith.dottedand.tripletderiving the others, andseconds(beats:),seconds(bars:),bars(at:), andsecondsPerBeatread the rest. - Tensegrities.
Tensegritydescribes a structure of struts held apart by cables, with three balanced forms ready-made:prism(struts:), the six-struticosahedron(strutLength:), and atower(levels:).World3D.addTensegrity(_:at:)gives one weight, so it falls, lands on its own cables, and stands.imbalancesays whether an arrangement of your own can hold,drawTensegrity(_:)draws it where it is, and a new.cablejoint 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, astemperature,humidity,cloudCover, the wind,condition, and the day'ssunriseandsunset. Nothing throws, a failure keeps the last reading, and an export reads once.Weather(latitude:longitude:)takes a point,precipitation,snowfall,pressureandisDaycome with it, andPlace.sun(at:)puts the sun in the sky with no network. Weather - Strokes and fills as points on the web page.
--export-webcarries 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 aProfilein its own frame (.straight,.tooth,.zigzag,.wave,.bump, or.custom) andSweepruns the change across, down, diagonally, or out from the center; readtilesfor the pieces oredgesfor line work. Parquet deformations - A weight limit on the web page.
--export-webrefuses 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-sizethat 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:onOllinApp.web(of:)andexportWeb, and aWebWeightRefusalcarrying the numbers. - Parameters as controls on the web page.
--export-weboffers a sketch's@Params 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-controlsskips the probe, and the inline fragment reaches them through the handle (params,get,set,reset). Parameters as controls - Fields on the web page.
--export-webcarries 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. AraymarchResolutionbelow one is traced at the Mac's own fraction. 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, sofieldOfView:takes the lens's own number beside.degrees(_:); passsensorHeight:for a smaller format. - Ramp stops as values, and a ramp reversed.
Ramp(stops: [Ramp.Stop])takes typed stops beside the tuple list, andramp.reversedruns a ramp the other way, a hard edge keeping its colors in mirrored order. - Depth of field from light.
LineSprayrenders an array ofSprayLines 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 aBokehball 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(_:), anddeveloped(exposure:ground:)to print it. Depth of field from light - 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 asimage, so a picture built from faint samples converges instead of brightening without end.reset()starts it over,sumandpassesare there for a shader of your own,Filter.develop(exposure:ground:)prints accumulated light, andmakeRenderTarget(precision:)andmakeFeedback(precision:)takeLayerPrecision.float32. Accumulation - Particles as light.
drawParticles(_:style: .light)deposits each particle'scolor × alpha × areaas 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.marksstyle is unchanged. The light particle style - A kernel that sees the camera.
Camera3D.viewMatrix,projectionMatrix(aspect:),viewProjectionMatrix(aspect:), andmatrices(aspect:)are public;ComputeParams.append(camera:aspect:)packs them, and the library'sollin_projectprojects a world point to canvas points inside a kernel.cameraParams()does it for the active camera,ollin_project_eyetakes a camera-space point, andballSample(float3)joinsdiscSamplein the library. Projecting through the camera - Settled frames in an export.
--settle N(andOllinApp.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. AnAccumulator's mean or aLineSpraythrough a turning camera settles for every written frame. Settled frames - Compute binds up to ten buffers.
compute(_:buffers:count:params:)hands a kernel any number ofComputeBuffers of any element types at indices 0…9, andcompute(_:reading:writing:)now takes two element types, so a kernel can read a static geometry buffer and write anOllinParticlebuffer without packing one into the other.ComputeBindableis public, and so isColor.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, and3D/Raymarching/RaymarchedFractalsturns 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.swiftputs it there and installs it again on every save, its clock and parameters carried across, with the parameters live on the Mac.SketchRunner.beginInstallationis public, so an app of your own can turn the checkpoint on. The sketch on the phone - A Web example group.
Examples/Web/BreathingRingdraws 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-webrecords 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 declaresloopDurationrecords 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 - Layers and shaders on the web page.
--export-webcarries the layered effects: layers drawn withwithTarget, everyGenerator, everyFilterandCombinethat is one shader pass (plus.gaussianBlurand.bloomas passes of the page's own),postProcess, a userShaderwithinfo.timeandinfo.mouselive on the page, compiledVisualchains, and aFeedbacklayer or a single-fieldSimFieldkeeping 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-webcarries a picture drawn withdrawImage, 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 gainsready. - Strokes and fills on the web page.
--export-webcarries 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 isdrawLine,drawPolyline,drawBezier,drawCurve,drawArc,drawPolygon, anddrawShape, and aBatchrecorded from them replays under its transform. - The website.
ollin sitewrites the README, the Guide, the reference, and every example with its source out as a static website, the pages rendered from the same filesollin docsreads, with a workflow that publishes it to GitHub Pages. The reference offline
Changed
- Preflight and CI, cheaper and less fragile.
Scripts/preflight.shruns 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, 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.lengthandSonification.noteLengthareNoteLengths, andNote.seconds(at:),Synth.play(_:tempo:), andSynth.play(_:step:tempo:)take aTempo. A literal compiles as it did; aDoublevariable becomesTempo(bpm)orNoteLength(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/BreathingRingas the page--export-webrecords 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 speedswift run -c releasegives it.--no-optimizecompiles 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
SketchExtensionthat 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 afterafterFrame. That coversstartRecording()and--record, andwantsRenderedFrame/wantsRenderedTextureare read once per frame. - A typed box for bounds.
Box3replaces the(min: Vector3, max: Vector3)tuple everywhere it appeared:Scene.bounds,Metaballs.bounds,ShadowArt.bounds,PhoneSceneMesh.bounds,PhonePlanes.bounds, and thein:box ofisosurfaceandshadowArt. Reading.minand.maxis unchanged; a tuple literal becomesBox3(min:max:), andcenter,size,longestSide,padded(by:), andunion(_:)come with it. - The
Rendering/DepthOfFieldexample 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 anoClearpile that brightened forever.
Fixed
- A parameter of your own type can be declared. The parameters page said a full
ParamValueconformance 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 asOpacity(0.5)around the literal it changes. There is a bare form for a type whose constraints areVoid. - A shader beside a phone sketch reached the phone as a library, not as source.
ollin phonelisted a.metalsibling 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
asyncdownload(_:)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
noClearcanvas plus a tone map and left out the divide by the number of passes; both now describe the running mean and point atAccumulatorandFilter.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 carries the fuller catalog, and the Guide 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()anddraw(), and bare calls likedrawCircleandbackground.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. - 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, Color. - 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. - 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.
- 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.metalfile, with errors reported at your own line numbers. ChainableVisuals when you would rather skip the Metal. Effects, Shaders. - Motion and parameters. Frame-rate-independent easing and springs, keyframe timelines, seeded variations, and record and replay.
@Paramproperties become typed inspector controls, persist across reloads, and can follow a curve or a formula. Sketch, Automation, Variations. - 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, Simulation.
- Physics. A stepped
Worldwith Verlet particles and springs, Box2D bodies and joints in 2D, and Jolt-backed 3D bodies, joints, characters, vehicles, ragdolls, cloth, ropes, and water. Physics, Physics in 3D. - 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.
- 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, Data, The phone.
- 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, Synthesis.
- 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.
- 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.
- 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, Installation.
The tools
- The
ollincommand. One loose.swiftfile is a whole sketch, run from any directory.ollin newwrites a starter,ollin installputs the command on PATH, andollin checkcompiles a shader on this machine's GPU.ollin docsandollin examplesread the reference and the examples in the terminal. Single-file sketches, The reference offline. - OllinLive. Save the file and the running window hot-swaps the sketch, with the clock carried across.
@Paramproperties 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, Dragging a shape. - 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.
- The project generator.
ollin newandollin generatewrite 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. - The examples. 494 example sketches in their own package, browsable in one gallery window, including twelve recreations after eight artists. Examples.
The documentation
- The reference under Docs/, about two hundred pages, with a short concept page for each idea the reference assumes.
- The Guide, thirty-two chapters and four appendices: a practical introduction to creative coding taught through Ollin.
- For contributors, CAPABILITIES.md inventories every capability's invariants, ARCHITECTURE.md explains the internals, and 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