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
| openFrameworks | Ollin | Notes |
|---|---|---|
ofGetElapsedTimef() | time | seconds, as a Double |
ofGetLastFrameTime() | deltaTime | |
ofGetFrameNum() | frameCount | |
ofGetWidth(), ofGetHeight() | width, height | the 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, ofColor | Vector2, Color | values; 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:
| openFrameworks | In Ollin | Reference |
|---|---|---|
| ofxGui | @Param properties, shown in the inspector | Parameters; Chapter 1 |
| ofxOsc | OSCReceiver and OSCSender, in OllinOSC | OSC; Chapter 38 |
| ofxMidi | MIDIInput and MIDIOutput, in OllinMIDI | MIDI; Chapter 38 |
| ofxOpenCv, ofxCv | OllinVision, on Apple's Vision framework: faces, hands, bodies, and ContourDetector for outlines | Vision; Chapter 34 |
| ofxBox2d | World, in OllinPhysics, with Box2D bundled | Physics; Chapter 11 |
| ofxSyphon | SyphonServer and SyphonClient, in OllinSyphon | Syphon; Chapter 43 |
| ofxFX | the Filter catalog, on layers | Effects; Chapter 19 |
| ofxAssimpModelLoader | loadMesh and loadScene | Scenes; Chapter 27 |
| ofVideoGrabber, ofVideoPlayer | Camera in OllinVision, VideoPlayer in OllinVideo | Vision, Video; Chapter 34 |
| ofSoundPlayer, ofSoundStream | AudioPlayer to play a file, AudioInput and AudioAnalyzer to listen, and Synth to make sound, in OllinAudio | Audio, Synthesis; Chapter 37, Chapter 39 |
| ofxDmx | DMXSender and DMXReceiver, in OllinDMX | DMX; 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
| OPENRNDR | Ollin | Notes |
|---|---|---|
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), Vector3 | Vector2(3, 4), Vector3 | the same names; Ollin has no * or / between two vectors, and no + or - with a number |
a.distanceTo(b) | a.distance(to: b) | |
Rectangle | Rectangle | |
ShapeContour, Shape | Contour, Shape | a Contour holds points, so curves are flattened into it |
contour { moveTo(…); lineTo(…) } | Path { $0.move(to: …); $0.line(to: …) }.contour | drawShape { } 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).position | c.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 module | In Ollin | Reference |
|---|---|---|
| orx-fx | the Filter catalog | Effects; Chapter 19 |
| orx-compositor | compose { } | Effects; Chapter 19 |
| orx-gui, orx-parameters | @Param properties, shown in the inspector | Parameters; Chapter 1 |
| orx-olive | the live host, OllinLive | Chapter 1 |
| orx-camera | cameraControl() and cameraShowcase() | Camera; Chapter 26 |
| orx-noise | noise, simplexNoise, fbm, and the signed… forms | Noise; Chapter 5 |
| orx-shapes | drawCurve(_:closed:spline:) with .hobby, convexHull(of:), offset(by:join:), simplified(tolerance:) | Geometry; Chapter 15, Chapter 16 |
| orx-osc, orx-midi, orx-syphon | OllinOSC, OllinMIDI, OllinSyphon | Integration; 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
- Appendix C: the full call-by-call dictionary, written for p5.js and Processing. Its rows for the everyday calls work for openFrameworks and OPENRNDR too.
- Appendix A: the Swift you need, including values and references.
- Chapter 19, Layers and effects: layers, filters, feedback, and
compose. - Chapter 18, Your first shader: the
shadecontract, the shader library, and bringing GLSL over. - Chapter 26, 3D, gently: the camera, lights, and the built-in solids.
- Chapter 27, Meshes and maps: meshes from files and from other meshes, and the maps on their surfaces.
- Chapter 28, Materials and surroundings: surfaces set by measured numbers, the environments that light them, and the sky and air around a scene.
- Appendix D: everything Ollin ships, one line each.
Contents · Previous: Appendix D, The complete toolbox