Ollin

Ollin quick reference

This page is the whole shape of Ollin in one read. It covers how a sketch runs, what the calls are called, their units, the command line, and the mistakes that fail with no error. Each part links to the page with the rest. It is written for anybody writing a sketch, and for an assistant helping somebody write one. Read it once, then look details up with the commands under Looking something up.

New to Swift? The Swift quick reference does the same for the language. Coming from p5.js or Processing, Appendix C of the Guide translates the API you know, and Appendix E does it for openFrameworks and OPENRNDR.

Contents

A sketch

A sketch is a class that builds on Sketch. setup() runs once, and draw() runs every frame at the display's rate, so anything that reads time moves.

import Ollin

@main
final class Rings: Sketch {
    override var canvasSize: CanvasSize { .vertical1080 }
    override var loopDuration: Double? { 6 }

    @Param(4...40) var count = 12
    @Param var ink: Color = .black

    override func setup() {
        seed(7)
    }

    override func draw() {
        background(.white)
        noFill()
        stroke(ink)
        strokeWeight(2 * scale)
        let t = loopProgress(over: 6)
        for i in 0..<count {
            let wobble = sin((t + Double(i) / Double(count)) * .tau) * 12
            drawCircle(width / 2, height / 2, (40 + Double(i) * 30 + wobble) * scale)
        }
    }
}
  • Configuration is overridden, not called. canvasSize, windowMode, loopDuration, title, and colorOutput are properties a sketch overrides.
  • The hooks are setup(), draw(), mousePressed(), mouseReleased(), mouseScrolled(), keyPressed(), keyReleased(), filesDropped(), and reloaded() (after a hot swap). Mouse movement has no hook; read mouseX, mouseY, and mouseIsPressed in draw().
  • Draw in draw(), not in setup(). The frame's geometry is emptied before the first draw(), and per-frame state (the camera, lights, shadows) resets each frame.
  • draw* calls emit shapes; fill, stroke, and background set state; translate, rotate, and scale move the coordinates. withState { } keeps a change of state or transform inside its block.
  • Keep state in properties. A Feedback, an Accumulator, a loaded Image, or a Mesh is made once, in setup() or as a stored property, and used every frame.
  • Loading a file throws. loadImage, loadMesh, loadScene, loadSVG, loadPalette, loadTable, loadJSON, and the initializers under them throw a FileError (missing or unreadable, with the path and a sentence). setup() may throw, so a bare try stops the sketch with that sentence (on stderr, and in a host's error strip, with the window held open), or write try? to carry on with nil.

Sketch covers the lifecycle and Canvas the canvas and window.

The clock and randomness

  • time is seconds since setup(). In a window it adds up the real frame steps, each capped at 0.25 s, so a stall pauses the clock rather than jumping it. In an export it is exactly the frame number over the frame rate.
  • frameCount is 1 during the first draw(). deltaTime is the step since the last frame: 0 on the first frame in a window, exactly 1 / fps in an export.
  • loopProgress(over:phase:) runs from 0 up to 1 over the given seconds and wraps. pingPong(over:phase:) goes 0 to 1 and back.
  • loopDuration declares the length of one lap. It does not change time. It tells --export-loop how many frames make exactly one lap, and makes a web export loop.
  • noLoop() stops the window's timer after the current frame. Exports hold that frame too, so a frame asked for past it is the frame it stopped on, drawn once. redraw() draws one more frame of a stopped sketch, from an input hook.
  • isExporting is true through the whole of an export, setup() included, and false in a window: work spread over live frames is done at once when it is true.
  • random() returns 0 up to but not including 1, random(a, b) a up to b, and randomGaussian() has mean 0 and deviation 1.
  • seed(n) fixes both random and noise and sets variation. randomSeed and noiseSeed fix one each. A sketch that never seeds gets a different variation at every launch.

Animation, Random, and Determinism have the rest.

The calls, by area

The names a sketch reaches for first in each area. ollin api <name> gives any of them in full.

AreaCallsPage
Canvas and framebackground, width, height, center, scale, noClear, makeAccumulator, withAccumulator, noLoop, redraw, isExportingSketch, Accumulation
2D shapesdrawCircle, drawEllipse, drawRect, drawLine, drawPoint, drawTriangle, drawArc, drawPolygon, drawNgon, drawStar, drawRing, drawBezier, drawArrowDrawing
Stylefill, noFill, stroke, noStroke, strokeWeight, strokeCap, strokeJoin, strokeDash, strokeAlign, blendMode, hollow, solidDrawing
Transformstranslate, rotate, scale, withState, mirrored, repeated, withClipDrawing
OutlinesdrawShape, drawCurve, drawPolyline, Path, Shape, Contour, Vector2, drawSVGGeometry
TextdrawText, textFont, textSize, textAlign, textWidth, textToShapesText
ImagesloadImage, drawImage, Image, tint, noTintImages
ColorColor, Palette, Ramp, Gradient, CosinePalette, Colormap, OKLCHColor
Math and noisemap, lerp, clamp, smoothstep, polar, spherical, random, noise, signedNoise, fbm, curlNoiseMath, Noise
MotionloopProgress, pingPong, Easing, Eased, Sprung, every, afterAnimation
Layers and effectsmakeRenderTarget, withTarget, filtered, Filter, combined, postProcess, makeFeedback, withFeedback, generateEffects
Your own shadersShader, Filter, Generator, Visual, drawVisual, computeShaders
3Dcamera, Camera3D, cameraControl, drawBox, drawSphere, drawMesh, Mesh, material, Material, directionalLight, pointLight, environment, castShadows, temporalAntialiasing, motionBlur3D
InputmouseX, mouseY, mouse, mouseIsPressed, key, keyIsPressed, isKeyDown, droppedFiles, chooseFiles, pointerShape, hidePointerInput
ParametersParam, ParamGroup, Saved, variationParameters
TechniquespoissonDisk, voronoi, delaunay, flowField, drawLSystem, drawWFC, packCircles, drawTruchet, stippleGenerators

Names you might reach for

Some names from other frameworks mean something else here, or nothing. ollin api catches the near misses and lists the sketch calls spelled like the name you typed. It cannot catch these:

You might typeIn Ollin
circle(x, y, d)drawCircle(x, y, radius), which takes a radius rather than a diameter
strokeWidth, lineWidthstrokeWeight
push() and pop()withState { }
createCanvas(w, h)override var canvasSize: CanvasSize { .size(w, h) }
saveFrame--export or --export-sequence on the command line, or OllinApp.image(of:frame:) in code; copyFrame() puts the frame on the clipboard
cursor(CROSS) and noCursor()pointerShape(.crosshair) and hidePointer(), with showPointer() to bring it back
selectInput(prompt, callback)chooseFiles(withExtensions:), with the pick read from chosenFiles() in filesChosen()
millis()time, in seconds
radians(45).degrees(45), which converts degrees to the radians every drawing call takes
beginShape() and endShape()drawShape { } with a Path, or drawPolyline for points
a Poisson fill, a Laplace or membrane fill of the space between marks.diffuse() filtered over a layer holding the marks. poissonDisk is something else: it scatters points.

Units and conventions

WhatConvention
2D coordinatesThe origin is the top-left corner and y points down. width and height are the canvas in pixels.
scale (the property)min(width, height) / 1000. Write sizes for a 1000-pixel canvas and multiply by it, and the sketch holds at any canvas size. It is separate from the scale(_:) transform.
2D anglesRadians, 0 along +x, turning clockwise on screen because y points down. rotate, drawArc, polar, and Vector2(angle:length:) all work this way.
Degrees.degrees(45) and .turns(0.125) convert to radians. The few calls that take degrees say so: an LSystem angle, Ocean.windDirection, the Physarum angles, and the latitude, longitude, and sun position of a Place.
The complex planeEvery generator that paints it and the shader library's complexPlane put center in the middle, 3 / zoom units across the shorter side, and the imaginary axis up. A generator's plane is that framing as a ComplexPlane, with canvasPoint(of:in:) and planePoint(at:in:).
HueA turn from 0 to 1 that wraps, in Color(hue:saturation:brightness:) and in OKLCH and OKHSL.
3D coordinatesRight-handed with y up, and the camera looks down its own −z. Nothing draws in 3D until a camera is set with camera, cameraControl, or the like, and the camera resets every frame.
3D transformsThree numbers or a Vector3: translate(x, y, z), scale(s, s, s), rotateY(a) or rotate(a, axis:). The two- and one-number forms, translate(x, y), scale(s), and rotate(a), are the 2D transform and leave a mesh where it was.
Field of viewVertical, in radians, .pi / 3 by default. A taller canvas keeps the height and shows less width, so a portrait render of a scene framed wide crops its sides.
OrbitCamera3D.orbiting(target:radius:azimuth:elevation:) takes radians. Elevation 0 is the horizon and positive looks down from above. Azimuth 0 puts the eye on +z looking toward −z, and π/2 puts it on +x.
Spot lightconeAngle is the full cone in radians, .pi / 6 by default. penumbra runs from 0 to 1.
ColorA Color holds sRGB components with straight alpha. Fills, strokes, lights, materials, and tints are converted to linear light for you. linearRGB and Color(linear:green:blue:alpha:) cross over by hand.
Brighter than whiteA component can pass 1: Color(linear: c.linearRGB * 3). It only shows brighter than white on screen when the sketch overrides colorOutput with .extended; otherwise the tone map clips it.
Noisenoise, simplexNoise, and fbm return 0 to 1. The signed forms (signedNoise, signedFbm) return −1 to 1. curlNoise returns a vector that is not normalized. noise(loop:radius:) loops as loop runs 0 to 1.
Export time--frame N counts from 0 and draws at N / fps, and --at S is the same moment in seconds (the frame is S × fps, rounded; it wins when both are given). In --export-sequence, file n is drawn at (n − 1) / fps. --skip is in seconds and --start only renumbers the files.
Depth-of-field lightA SprayLine's light is the whole segment's, not light per unit length. A long line and a short one at the same light put down the same amount of light.

Parameters

@Param makes a property tunable. The live hosts show a control for it, and the command line can set it.

@Param(0...200) var radius = 120.0                     // a slider
@Param("Speed", 0.1...4, step: 0.1) var rate = 1.0    // a label and a step
@Param(1...12) var rings = 5                           // an Int is a stepper
@Param var tint: Color = .purple                       // a color well
@Param(x: 0...1080, y: 0...1080) var origin = Vector2(540, 540)
@Param(group: .folded("Advanced")) var jitter = false  // a card that starts closed
  • An enum that is ParamOption (CaseIterable) becomes a menu, and style: .segmented shows every case at once. A built-in Material, Easing, Fog, or Environment is a menu too (@Param var surroundings = Environment.studio).
  • group: puts a parameter in a titled card, and parameters with no group come first. icon: takes an SF Symbol name.
  • $rate.show(when: $mode) { $0 == .fast } in setup() hides a row. The value still holds.
  • A value tuned in the inspector lives in that window until "Save parameters to Sketch.swift" writes it into the source. An export never opens the inspector, so it uses the values written in the source. The dot after a tuned parameter's name, or Reset in its row's menu, puts it back to what the line declares, and Reset all puts every parameter back, with the sketch still running.
  • ollin api Param lists every form a parameter can be declared with.

Parameters has the rest.

The command line

A packaged sketch runs with swift run <Target> <flags>, and a single file with ollin Sketch.swift <flags>. ollin <package folder> <flags> runs a package's sketch the way the single file runs, every file of its target compiled together. With no flag the sketch opens in a window. With an export flag it renders without one and exits.

FlagWhat it does
--export frame.pngOne still. --frame N picks the frame (from 0, default 0) or --at S names the moment in seconds, and --fps sets the rate (default 60). Every frame before it is drawn first.
--export-sequence dirframe-00001.png, frame-00002.png, and so on, for --seconds S or --frames N. --skip S runs S seconds before the first file is written.
--export-video out.mp4, --export-gif out.gifA movie or a GIF, with the same --seconds, --frames, --fps, and --skip.
--export-loop out.mp4Exactly one lap of loopDuration.
--seed NThe variation to render, set before setup(). A seed(n) in setup() overrides it.
--param name=valueSets a parameter, before canvasSize is read and before and after setup(). Repeat it for more. A color is #RRGGBB, a vector x,y, a menu its label.
--list-paramsPrints every parameter in the form --param takes, and stops.
--settle NDraws each written frame N times with the clock held. A LineSpray and an Accumulator need it to converge. Anti-aliasing over time and path tracing don't.
--render-qualityperformance, default, or detail: the sampling budgets for soft shadows, depth of field, and ambient occlusion. Exports default to detail.
--render-scale NDraws each frame N times larger and averages it down. It smooths fills and polygons; shapes and strokes are already smooth.
--subframes NDraws each frame at N moments across the shutter and averages them: motion blur for anything the sketch draws, 2D included. --shutter F sets the opening (0.5 when left out).
--benchTimes 600 frames and reports the cost per frame, without writing anything. --gpu adds the GPU's side. It measures the window's quality tier; an export lifts .default to .detail, so add --render-quality detail to time what an export pays.

Export has every flag.

Looking something up

Three commands answer most questions without a browser. They read the checkout you build against, so they describe the version you are running.

ollin api drawCircle           # what a name takes, what its comment says, where it is used
ollin docs Drawing/Color#ramp  # one section of one page
ollin examples ocean --source  # an example that does it, as source
  • ollin api <name> answers "what is it called, what does it take, and is it public". It reads the listings under API/, which hold every public declaration, so a name it does not know cannot be reached from a sketch. It prints each declaration as its source writes it, with the comment above it, where units and ranges are written down. ollin api Mesh.tube picks one type's member, and ollin api Param lists a whole type.
  • ollin docs <topic> opens a page by name or path, and <topic>#<heading> opens one section. ollin docs --search "long exposure" finds every line that says it.
  • ollin examples <word> lists the sketches whose name, folder, description, or header credit matches (an exact name alone, anything looser together, names first), and --source prints one.

ollin is Scripts/ollin in the checkout, and it works from any folder. ollin install puts it on your PATH. Its output is plain text under a pipe, so it composes with grep. The reference offline has the rest.

Things that fail quietly

Each of these gives a wrong or empty picture and no error.

  • A layer that is drawn into but never drawn. Nothing reaches the canvas until its image does: drawImage(layer.image, 0, 0). A LineSpray's pass count climbs all the same, and the canvas shows only the background until drawImage(spray.developed(exposure: 1).image, 0, 0).
  • Drawing in setup(). The geometry is thrown away before the first frame, and a camera, a light, or shadows set there are gone by the first draw(). Draw and set per-frame state in draw().
  • A concave outline through drawPolygon. It fills as a fan from the first point, which only works for a convex shape. A concave one goes through drawShape { } or a Shape.
  • A Feedback or Accumulator made inside draw(). A fresh one every frame never builds up. Make it once, in setup().
  • A layer that draws a filtered(_:) image. Every layer draws before any filter resolves, so a target that draws a filtered layer's image this frame comes out empty, as a picture and as data, and a SimField cannot take a filtered layer as an input either. A filter's result reaches the canvas, or the next filter in a chain, and nothing else.
  • A data texel with alpha at or below zero. drawImage writes nothing for it, so a signed field copied through drawImage into a float layer loses those texels while every other channel arrives verbatim. Keep alpha above zero (one plus the coverage, say) and carry the sign in a color channel, or read the layer where it is with sampleRaw.
  • A long, faint sum on a noClear() canvas. The canvas is half-float by default, so a light added a little at a time stops brightening. Use noClear(precision: .float32), makeFeedback(precision: .float32), or an Accumulator.
  • A far contour of a distance field measured on a half-float layer. Past 1,024 pixels a half float holds a distance only to the nearest whole pixel, so a contour cut there comes out ragged. Draw the marks into makeRenderTarget(precision: .float32); the field and every filter after it keep single precision.
  • A still at frame 0. --export draws frame 0 unless told otherwise, and nothing came before it: motion blur is off and a trail or a pile holds one frame. Pick a later frame with --frame N. A LineSpray or an Accumulator is the exception: --settle N redraws the frame with the clock held, so it converges even at frame 0.
  • Motion driven by frameCount under --settle. The clock holds for the extra draws but frameCount keeps counting, so a camera driven by it moves and restarts the converging picture. Drive motion from time.
  • Motion driven by frameCount under --subframes. Every moment is a draw, so a fixed step once per draw() runs N times as fast and does not blur. Drive it from time, or step it by deltaTime.
  • Values tuned in the inspector, in an export. The export uses the values in the source. Save them first, or pass --param.
  • Values the command line reads loosely. A --param outside the declared range is clamped, an unknown --render-quality becomes detail, and an --fps that does not parse becomes the default. None of them says so.
  • Filter.grain in the shadows. It adds noise and clips it, which lifts black. .filmGrain leaves black and white alone, though its grain still shows most in near-black tones.
  • A @Param icon that is not an SF Symbol. The row shows an empty space where the icon goes.
  • 3D on a noClear() canvas. Nothing is sorted by depth and nothing casts a shadow.
  • A volume, and the 2D drawn after it. drawVolume with no camera set draws nothing. With one, every volume composites over the frame's whole drawing once it is done, so a caption drawn later in draw() lies under the cloud. Draw the caption in withOverlay { }. See Volumes.
  • A texture on a mesh with no texture coordinates. The texture is ignored, and the mesh draws in its base color.
  • redraw(), copyFrame(), chooseFiles, and pointerShape in an export. There is no window, so each does nothing. A redraw() inside draw() asks for nothing either: that frame is the one being drawn.
  • Noise offsets 256 apart. Every Perlin field repeats every 256 units on each axis, so copies told apart by an offset of 32 * i read the same noise every eighth copy. Use an offset that is not a power of two, or a seed per copy.
  • Noise at its ends. noise is clamped after its contrast is raised, so about 2% of it sits exactly at 0 and 2% exactly at 1, and a curve read straight from it runs flat there.
  • A print during an exported sequence. It lands on the same terminal line as the progress text, and later than expected when the output is piped. Write to standard error with a newline first, or read the value after the run.

Checking your work

  • Judge speed in a release build. swift run builds the framework for debugging, which runs several times slower. Use swift run -c release, or ollin, which runs a release host and compiles the sketch optimized. --bench gives numbers.
  • A color or vector parameter reads through a lock. A number, an integer, or a switch reads in under a nanosecond, like a stored property; a Color, a Vector2, a palette, or a text value takes a lock, about 40 ns, so read one of those into a local before a loop that would read it per vertex. See Parameters.
  • Judge motion in a sequence, not a still. Anything that needs earlier frames (motion blur, trails, a pile, a settling picture) shows properly only a few frames in. A short --export-sequence shows it.
  • Compare pixels, not bytes. Every exported PNG carries its recipe in its description: the seed, every parameter, the sketch's commit and the framework's, the frame, and the frame rate. Two files with the same pixels differ when any of those do, so compare the pictures, for instance with compare -metric AE a.png b.png null: from ImageMagick.
  • Fix the seed before comparing two runs. Call seed(n) in setup() or pass --seed N, or every run is a new variation.

See also

↑↓ move ↩ open