Ollin

E. Coming from openFrameworks and OPENRNDR

Ollin learned from both of these. openFrameworks showed how plain a creative-coding app can be, and OPENRNDR showed what a typed core with layers you compose looks like. So if you've worked in either one, the ideas here won't be new. What you need is where each thing went.

This appendix covers the big moves only. Each one says what you reach for there and how Ollin does it, usually in two short listings, and then what's different. The everyday calls for shapes, ink, and transforms read much the same in every framework, so Appendix C's tables serve you too. The Swift itself is in Appendix A and the Swift quick reference.

From openFrameworks

The app becomes one class

An openFrameworks app is three files. main.cpp opens the window, while ofApp.h and ofApp.cpp declare and define setup, update, and draw:

// main.cpp
int main() {
    ofGLWindowSettings settings;
    settings.setSize(1080, 1080);
    auto window = ofCreateWindow(settings);
    ofRunApp(window, std::make_shared<ofApp>());
    ofRunMainLoop();
}

// ofApp.cpp, with float angle = 0 declared in ofApp.h
void ofApp::update() {
    angle += 0.02;
}

void ofApp::draw() {
    ofBackground(0);
    ofTranslate(ofGetWidth() / 2, ofGetHeight() / 2);
    ofRotateRad(angle);
    ofSetColor(255, 180, 0);
    ofDrawRectangle(-100, -100, 200, 200);
}

In Ollin the same program is one class in one file:

import Ollin

final class Spinner: Sketch {
    override func draw() {
        background(.black)
        translate(width / 2, height / 2)
        rotate(time * 1.2)
        fill(Color(red: 1, green: 0.7, blue: 0))
        drawRect(center: .zero, width: 200, height: 200)
    }
}

There's no header and no main. The canvas is 1080 by 1080 unless the sketch declares another canvasSize, and swift run OllinLive Spinner.swift opens the window.

There's no update() either. draw() is the one call per frame, so anything that changes goes at its top. Often nothing needs storing, because the state can be worked out from the clock. Here the angle is time * 1.2, where time is the seconds since the sketch started. When something does build up over time, like a particle's position, step it by deltaTime at the top of draw().

The frame rate follows the display, and there's no call to set it. A Mac display may refresh 60 or 120 times a second. Motion written against time and deltaTime keeps its speed on both. Ported as it is, the 0.02 a frame above would spin twice as fast on the faster display.

The names you type every day

openFrameworksOllinNotes
ofGetElapsedTimef()timeseconds, as a Double
ofGetLastFrameTime()deltaTime
ofGetFrameNum()frameCount
ofGetWidth(), ofGetHeight()width, heightthe canvas, not the window
ofBackground(0)background(.black)the canvas clears every frame on its own
ofSetBackgroundAuto(false)noClear()for marks that pile up
ofSetColor(255, 0, 102)fill(Color(red: 1, green: 0, blue: 0.4))channels run 0...1, as in ofFloatColor
ofSetLineWidth(4)strokeWeight(4)
ofDrawCircle(x, y, r)drawCircle(x, y, r)a radius in both
ofDrawRectangle(x, y, w, h)drawRect(x, y, w, h)
ofRotateDeg(45)rotate(.pi / 4)radians, and rotate(.degrees(45)) converts
ofRandom(10), ofNoise(x, y), ofSignedNoise(x)random(10), noise(x, y), signedNoise(x)
ofMap(v, a, b, c, d, true)map(v, a, b, c, d, clamped: true)
glm::vec2, ofColorVector2, Colorvalues; Vector2 adds and scales with operators, and a Color mixes with mixed(with:_:)

Fill and stroke are separate inks

In openFrameworks, ofSetColor sets one current color, and ofFill or ofNoFill decides whether a shape comes out filled or outlined. A disc with an outline takes two draws:

ofSetColor(255, 180, 0);
ofFill();
ofDrawCircle(540, 540, 200);
ofSetColor(0);
ofNoFill();
ofSetLineWidth(6);
ofDrawCircle(540, 540, 200);

Ollin keeps two inks at once, so one call draws both:

fill(Color(red: 1, green: 0.7, blue: 0))
stroke(.black)
strokeWeight(6)
drawCircle(540, 540, 200)

Ollin also strokes every shape by default, with a thin black line. A shape drawn under ofFill has no outline, so call noStroke() when you want the fill alone.

Three openFrameworks settings have no counterpart in Ollin. Antialiasing is always on, and so is alpha blending. There's no circle resolution either, because a circle isn't built from straight sides. Its edge is worked out at every pixel, so it stays round at any size.

One scope for the matrix and the style

ofPushMatrix and ofPushStyle are two stacks, and each needs its matching pop:

ofPushMatrix();
ofPushStyle();
ofTranslate(300, 200);
ofRotateDeg(30);
ofSetColor(255, 0, 0);
ofDrawRectangle(-40, -40, 80, 80);
ofPopStyle();
ofPopMatrix();

withState { } saves both at once and restores them when the block ends, so a pop can't be forgotten:

withState {
    translate(300, 200)
    rotate(.pi / 6)
    fill(.red)
    drawRect(-40, -40, 80, 80)
}

pushState() and popState() are there for the rare case a block can't express. Chapter 6 uses the scoped form throughout.

An ofFbo is a layer

You allocate an ofFbo once, draw into it between begin and end, and then draw it to the screen. Its pixels stay until you clear them:

// setup()
fbo.allocate(1080, 1080, GL_RGBA);

// draw()
fbo.begin();
ofClear(0, 0, 0, 0);
ofSetColor(255, 120, 0);
ofDrawCircle(540, 540, 200);
fbo.end();
fbo.draw(0, 0);

An Ollin layer is made inside draw() and drawn into with withTarget. Filters run on it directly, and its image composites back:

override func draw() {
    background(.black)
    let layer = makeRenderTarget()
    withTarget(layer) {
        fill(.orange)
        drawCircle(540, 540, 200)
    }
    drawImage(layer.filtered(.gaussianBlur(radius: 20)).image, 0, 0)
}

Making the layer every frame is cheap, because Ollin reuses the GPU textures behind layers from frame to frame instead of allocating new ones. A new layer starts transparent, so there's no ofClear to call.

The two differ in what they keep between frames, since a layer keeps nothing from the frame before. For a buffer that keeps the last frame's picture, as a trails buffer does, make a Feedback once in setup() with makeFeedback(). Then withFeedback hands you that picture to draw back in, faded or moved, before the new marks go on top:

var trails: Feedback!

override func setup() {
    trails = makeFeedback()
}

override func draw() {
    background(.black)
    withFeedback(trails) { previous in
        tint(Color(white: 1, alpha: 0.92))   // last frame, a little fainter
        drawImage(previous, 0, 0)
        noTint()
        fill(.orange)
        drawCircle(mouseX, mouseY, 16)       // the new mark on top
    }
    drawImage(trails.image, 0, 0)
}

Chapter 19 builds both. The effect chains ofxFX gives you are the Filter catalog there: blurs, bloom, color grading, and many more, each one a value you name.

Shaders: one function, in Metal

ofShader loads a vertex and fragment pair written in GLSL, and a rectangle drawn between begin and end runs it:

// setup()
shader.load("shader");   // shader.vert and shader.frag

// draw()
shader.begin();
shader.setUniform1f("time", ofGetElapsedTimef());
ofDrawRectangle(0, 0, ofGetWidth(), ofGetHeight());
shader.end();

An Ollin shader is one function, shade, written in the Metal Shading Language. Ollin writes the vertex stage and the pass around it:

let rings = Shader("""
float4 shade(float2 uv, ShaderInfo info) {
    float d = length(uv - 0.5);
    float v = 0.5 + 0.5 * sin(d * 60.0 - info.time * 3.0);
    return float4(v, v * 0.6, 1.0 - v, 1.0);
}
""")

override func draw() {
    drawImage(generate(rings).image, 0, 0)
}

The time, the resolution, and the mouse arrive in info without being set. Numbers of your own go in as params, and the shader reads them with param(info, i). How many layers the function reads decides what it is. With none it's a generator, with one it's a filter, and with two it combines them. Metal and GLSL both take their grammar from C, so most lines change only their type names, such as vec2 to float2.

For a shader you already have, ollin new --from-shader translates it into Metal and writes a project around it. It takes a GLSL fragment shader built around a mainImage function, the common form on the web, or an older one built around main. Chapter 18 teaches the shader contract, and Bringing a GLSL shader over lists what the translation carries and what it leaves as a note.

3D: the camera is a call

In openFrameworks, 3D is an ofEasyCam you begin and end, with depth testing and lights switched on by hand:

// ofApp.h declares ofEasyCam cam and ofLight light, and setup() calls light.setPosition(300, 400, 500)
void ofApp::draw() {
    ofEnableDepthTest();
    cam.begin();
    light.enable();
    ofSetColor(240, 90, 90);
    ofDrawBox(200);
    cam.end();
}

In Ollin, calling a camera is what makes a sketch 3D, and depth testing comes with it:

override func draw() {
    background(Color(hex: 0x10141B))
    cameraControl(radius: 5)
    pointLight(.white, at: Vector3(3, 4, 5))
    fill(Color(hex: 0xF05A5A))
    drawBox(size: 1.5)
}

cameraControl orbits with the mouse the way ofEasyCam does. With no lights of your own, a default rig shades what you draw. Inside the camera, y points up and distances are world units, not pixels. An ofMesh becomes a Mesh value, built from positions and indices or from a generator such as Mesh.box, and drawn with drawMesh. loadMesh reads glTF, OBJ, STL, and PLY files, as ofxAssimpModelLoader does, and USD files too. It doesn't read FBX, Collada, or 3DS. Chapter 27 starts there.

C++ habits that change

Several C++ chores go away in Swift. There are no headers and no pointers. There's no new or delete either, because memory is counted for you. std::vector<T> becomes an array, [T]. auto becomes let for a constant and var for something that changes.

One habit changes meaning, and it's the loop over your particles. In C++, auto& hands you each particle by reference, so the loop changes the vector:

for (auto& p : particles) {
    p.position += p.velocity;
}

In Swift a struct is a value, and for p in particles hands you a copy. The compiler won't let you change it, and for var p changes only the copy. Loop over the indices and write through the array instead:

for i in particles.indices {
    particles[i].position += particles[i].velocity
}

Or make Particle a class, whose instances are shared rather than copied. Appendix A covers values and references.

Addons become satellite modules

Most of the addons that sketches reach for, and a few core classes, have counterparts that ship with Ollin. Each one is a Swift module you import, or a part of the core:

openFrameworksIn OllinReference
ofxGui@Param properties, shown in the inspectorParameters; Chapter 1
ofxOscOSCReceiver and OSCSender, in OllinOSCOSC; Chapter 38
ofxMidiMIDIInput and MIDIOutput, in OllinMIDIMIDI; Chapter 38
ofxOpenCv, ofxCvOllinVision, on Apple's Vision framework: faces, hands, bodies, and ContourDetector for outlinesVision; Chapter 34
ofxBox2dWorld, in OllinPhysics, with Box2D bundledPhysics; Chapter 11
ofxSyphonSyphonServer and SyphonClient, in OllinSyphonSyphon; Chapter 43
ofxFXthe Filter catalog, on layersEffects; Chapter 19
ofxAssimpModelLoaderloadMesh and loadSceneScenes; Chapter 27
ofVideoGrabber, ofVideoPlayerCamera in OllinVision, VideoPlayer in OllinVideoVision, Video; Chapter 34
ofSoundPlayer, ofSoundStreamAudioPlayer to play a file, AudioInput and AudioAnalyzer to listen, and Synth to make sound, in OllinAudioAudio, Synthesis; Chapter 37, Chapter 39
ofxDmxDMXSender and DMXReceiver, in OllinDMXDMX; Chapter 45

Here is ofxGui beside its Ollin form, which has no panel code at all:

// ofApp.h declares ofxPanel gui and ofxFloatSlider radius
void ofApp::setup() {
    gui.setup();
    gui.add(radius.setup("radius", 140, 10, 300));
}

void ofApp::draw() {
    ofDrawCircle(ofGetWidth() / 2, ofGetHeight() / 2, radius);
    gui.draw();
}
@Param(10...300) var radius = 140.0

override func draw() {
    background(.black)
    drawCircle(width / 2, height / 2, radius)
}

The inspector beside the canvas draws the slider, so it never lands in an export. The control follows the property's type. A Bool becomes a toggle, a Color becomes a color well, and a Vector2 becomes a pair of fields. Values survive a live reload, and the inspector can write the ones you like back into the source file.

projectGenerator becomes ollin new

The projectGenerator writes an IDE project with your addons in it. The command ollin new writes a Swift package with what you list after --with already wired in. A name that ends in .swift gets a single loose file instead:

ollin new Spinner --with osc,params    # a project folder
ollin new Spinner.swift                # one file
ollin Spinner.swift                    # run it live

The bigger change is the working loop. In openFrameworks you rebuild, relaunch, and start again from the first frame. Ollin's live host keeps the window open instead. Save the file and it recompiles just the sketch, then swaps it into the running window. A compile error leaves the last good version running. --keep-clock carries time across the swap, so an animation doesn't jump back to its start. Chapter 1 sets the host up. Chapter 43 carries the clock across a swap on stage, and Chapter 44 carries it across a reinstall on a phone. The project generator lists every kind of project it writes.

From OPENRNDR

From Kotlin to Swift

OPENRNDR runs on the Java virtual machine, but its programs are written in Kotlin. Kotlin and Swift share many ideas. val is Swift's let, and var is var. A trailing lambda reads like a trailing closure, it becomes $0, and ? marks a value that may be missing in both. An extension function is a Swift extension.

Two differences show up often. Swift reads 140 as a Double wherever one is expected, so the .0 endings go. And a Kotlin data class is a reference, while a Swift struct is a value, copied when you assign it.

The program becomes a sketch

fun main() = application {
    configure {
        width = 1080
        height = 1080
    }
    program {
        var radius = 200.0
        mouse.buttonDown.listen { radius = 120.0 }
        extend {
            drawer.clear(ColorRGBa.BLACK)
            drawer.fill = ColorRGBa.PINK
            drawer.stroke = null
            drawer.circle(drawer.bounds.center, radius + sin(seconds) * 80.0)
        }
    }
}
import Ollin

final class Pulse: Sketch {
    var radius = 200.0

    override func mousePressed() {
        radius = 120
    }

    override func draw() {
        background(.black)
        fill(.pink)
        noStroke()
        drawCircle(center: center, radius: radius + sin(time) * 80)
    }
}

Code in program { } that comes before extend { } runs once, so it moves to setup() or into properties. The extend { } block runs every frame, which makes it draw(). In the Swift, seconds becomes time, and drawer.bounds.center becomes center. The configure block becomes a canvasSize declaration, which Pulse leaves out because 1080 by 1080 is the default.

Events are overrides rather than listeners. Where OPENRNDR listens on mouse.buttonDown, a sketch overrides mousePressed() and reads mouse inside it. Keys work the same way, through keyPressed().

No drawer to pass around

OPENRNDR hands every program a drawer, and you set its state by assigning to it. Ollin's calls live on the sketch, so drawer.fill = ColorRGBa.PINK is fill(.pink). There is a drawer underneath, and the bare calls forward to it, but it is not public. Whether to make it public is an open question for 1.0.

So a helper that draws extends Sketch rather than the drawer:

fun Drawer.target(position: Vector2) {
    circle(position, 40.0)
    circle(position, 20.0)
}
extension Sketch {
    func drawTarget(at position: Vector2) {
        drawCircle(center: position, radius: 40)
        drawCircle(center: position, radius: 20)
    }
}

The draw prefix is the house rule for any call that puts geometry on the canvas. What a helper passes around is still public: Color, Vector2, Contour, Shape, and Mesh are all values. Writing an extension covers helpers you hand to other people.

The value types are close by design

OPENRNDROllinNotes
ColorRGBa(1.0, 0.4, 0.0)Color(linear: 1, green: 0.4, blue: 0)channels run 0...1, read as linear light in both; Color(red:green:blue:) reads them as sRGB
ColorRGBa.fromHex("#ff6600")Color(hex: "#ff6600")optional, nil for a bad string; Color(hex: 0xFF6600) is not
Vector2(3.0, 4.0), Vector3Vector2(3, 4), Vector3the same names; Ollin has no * or / between two vectors, and no + or - with a number
a.distanceTo(b)a.distance(to: b)
RectangleRectangle
ShapeContour, ShapeContour, Shapea Contour holds points, so curves are flattened into it
contour { moveTo(…); lineTo(…) }Path { $0.move(to: …); $0.line(to: …) }.contourdrawShape { } takes the same builder and draws at once
drawer.contour(c)drawShape(Shape(contours: [c]))
c.position(t)c.point(at: t)measured by length along the whole outline
c.normal(t), c.nearest(p).positionc.normal(at: t), c.nearestPoint(to: p)
c.sub(t0, t1)c.piece(from: t0, to: t1)
intersections(a, b)a.crossings(with: b)

position(t) and point(at:) measure differently. OPENRNDR splits t evenly across the contour's segments, so on an outline of uneven segments 0.5 can land well away from halfway. Ollin's point(at:) always measures along the length, and so do normal(at:) and piece(from:to:). So c.point(at: t) matches OPENRNDR's c.pointAtLength(t * c.length). The contour questions are in Geometry and Chapter 15.

isolated, and the degrees trap

drawer.isolated { } is withState { }, the same block shown in the openFrameworks half:

drawer.isolated {
    translate(300.0, 200.0)
    rotate(30.0)
    fill = ColorRGBa.RED
    rectangle(-40.0, -40.0, 80.0, 80.0)
}

Ported line by line, one call keeps compiling and changes meaning. OPENRNDR's rotate takes degrees. Ollin's rotate takes radians, so rotate(30) turns almost five times around and gives no error. Write rotate(.pi / 6) or rotate(.degrees(30)), or think in turns, where .tau is one.

Render targets and the compositor

OPENRNDR builds a composite once, before the frame loop, and draws it every frame. A filter is an object you configure:

val composite = compose {
    layer {
        draw {
            drawer.fill = ColorRGBa.PINK
            drawer.circle(drawer.bounds.center, 200.0)
        }
        post(GaussianBlur()) {
            window = 25
            sigma = 5.0
        }
    }
}
extend {
    drawer.clear(ColorRGBa.BLACK)
    composite.draw(drawer)
}

Ollin's compose { } goes inside draw() and is declared again every frame. A filter is a value named with its settings:

override func draw() {
    background(.black)
    compose {
        layer {
            fill(.pink)
            drawCircle(center: center, radius: 200)
        }
        .post(.gaussianBlur(radius: 25))
    }
}

Declaring it every frame makes no new textures, since Ollin keeps the ones underneath and reuses them. To animate a setting, you pass a new value each frame rather than changing a property on the filter. A layer's blend is a modifier, .blended(.add). A helper layer that feeds another layer's effect is aside { }, as in the compositor.

The calls under compose have matches too. A renderTarget made once and drawn into with isolatedWithTarget becomes makeRenderTarget() and withTarget inside draw(), as in the openFrameworks half. A target that keeps its pixels between frames becomes a Feedback, made once in setup() with makeFeedback(). It keeps them as the last frame's picture, which withFeedback hands you to draw back in. And extend(NoClear()) becomes noClear(). Chapter 19 teaches the whole stack.

Shade styles

A shade style splices GLSL into the shader of every shape drawn while it's set:

drawer.shadeStyle = shadeStyle {
    fragmentTransform = "x_fill.rgb *= c_boundsPosition.x;"
}
drawer.circle(drawer.bounds.center, 200.0)

Ollin has no shader per shape. The common uses divide in two. A gradient across a shape is paint, which fill and stroke take anywhere they take a color:

fill(.linear(from: Vector2(340, 0), to: Vector2(740, 0), [.black, .pink]))
drawCircle(center: center, radius: 200)

The gradient's two ends are canvas points, so you place them where the shape is. .radial(center:radius:_:), .conic(center:startAngle:_:), and stroke(.alongPath(_:)) are the other three geometries.

Anything else is a Shader over a layer, kept inside the shape by a mask. In a compose block that's .masked(by: aside { … }), with the shape drawn white in the aside. Layered effects shows the mask, and Chapter 18 the shader.

The orx modules

OPENRNDR moduleIn OllinReference
orx-fxthe Filter catalogEffects; Chapter 19
orx-compositorcompose { }Effects; Chapter 19
orx-gui, orx-parameters@Param properties, shown in the inspectorParameters; Chapter 1
orx-olivethe live host, OllinLiveChapter 1
orx-cameracameraControl() and cameraShowcase()Camera; Chapter 26
orx-noisenoise, simplexNoise, fbm, and the signed… formsNoise; Chapter 5
orx-shapesdrawCurve(_:closed:spline:) with .hobby, convexHull(of:), offset(by:join:), simplified(tolerance:)Geometry; Chapter 15, Chapter 16
orx-osc, orx-midi, orx-syphonOllinOSC, OllinMIDI, OllinSyphonIntegration; Chapter 38, Chapter 43

Parameters take one line rather than an annotated object registered with a panel. @DoubleParameter("radius", 10.0, 300.0) on a settings object, added with gui.add, is the same @Param(10...300) shown in the openFrameworks half.

orx-noise passes the seed with every call, as in simplex(seed, x, y), and its simplex is centered on zero. In Ollin you set the seed once with noiseSeed(_:), and the match is signedSimplexNoise(x, y). The unsigned forms run 0...1.

orx-olive and OllinLive both keep the window open while you edit. Olive evaluates a Kotlin script inside the running program. OllinLive compiles the saved Swift file and swaps the result in, and nothing in the sketch changes to allow it:

swift run OllinLive Pulse.swift

extend(…) is here too. A SketchExtension hooks setup and before and after the draw, as an OPENRNDR extension does, and it can also receive each finished frame. Chapter 44 teaches it. It has no hook for when the sketch shuts down. Screenshots and screen recording are run flags, --export and --export-video, rather than extensions. Syphon reads almost the same in both:

extend(SyphonServer("Pulse"))
import OllinSyphon

override func setup() {
    extend(SyphonServer(name: "Pulse"))
}

The OPENRNDR template is a Gradle build, and it's where you name the orx modules you want. Here the build is SwiftPM. ollin new writes the Package.swift with the satellites you name, the same command as in the openFrameworks half.

What stays behind

Both frameworks run on Windows and Linux, and both draw through OpenGL. They can also run in a browser, openFrameworks through Emscripten and OPENRNDR through its experimental Kotlin/JS target. Ollin draws through Metal and runs on Apple platforms only, by design. The README gives the reasons. In short, it lets Ollin use Metal and Apple's own frameworks for vision, audio, and the iPhone's sensors directly. That choice has a cost. A sketch here won't build on a Linux box or a Raspberry Pi, and it won't run in a browser. For sharing, --export-web records what the sketch draws and writes a page that plays it back, with its shaders still running live. On a sketch with 3D meshes in it, the export stops and points you to --export-video instead.

The other thing that stays behind is years of community work. The ofx and orx ecosystems hold addons for hardware and techniques that Ollin doesn't have yet. When you miss one, check Appendix D first, since it may go by another name. If it isn't there, the roadmap says what's planned, and Writing an extension shows how to build it yourself.

Go deeper


Contents · Previous: Appendix D, The complete toolbox

↑↓ move ↩ open