Kotlin: OPENRNDR
Yesterday, the OPENRNDR starting project ran on my machine. Today, before studying the system, let’s take a look at the code and tweak it a bit.
I’ve disconnected my project from the origin. It seems that their instructions, while simple, lave your project set with origin as the template. I don’t know whether GitHub would let me commit to it, but I’m sure that now, even if I do hit the push button (haha) it won’t mess up OPENRNDR for the world.
As one does, I plan to review the code they’ve provided and tweak it a bit, just to get a sense of how it all works. Because that’s the kind of person I am, I’ll read the documents before doing much. Here’s one of the two demos their starting template provides:
fun main() = application {
configure {
width = 768
height = 576
}
program {
val image = loadImage("data/images/pm5544.png")
val font = loadFont("data/fonts/default.otf", 64.0)
extend {
drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2))
drawer.image(image)
drawer.fill = ColorRGBa.PINK
drawer.circle(cos(seconds) * width / 2.0 + width / 2.0, sin(0.5 * seconds) * height / 2.0 + height / 2.0, 140.0)
drawer.fontMap = font
drawer.fill = ColorRGBa.WHITE
drawer.text("OPENRNDR", width / 2.0, height / 2.0)
}
}
}
My past experience with drawing programs is such as to tell me these things:
-
They’ve done a Kotlin-style DSL thing here, with the top level function being
application
, with at least two subordinates,configure
andprogram
. -
It seems quite likely that
configure
sets up configuration matters, including the width and height of a window that’s going to be created1. -
I’d bet a few Linden dollars that the
program
bit is called repeatedly, perhaps at some fixed rate like 30 or 60 times per second. -
The
drawer
is some kind of object that does drawing2, sort of an object that draws on a panel or pane or chunk of paper, and it clearly has a cubic bunload of convenient functions one can call.
The app displays a window that looks like this:
The pink circle is moving in a sinusoidal fashion from top left to middle right to bottom left. What is most interesting about this fact is that the program
bit does not include any obvious “move this thing” code. It does, however, include this:
drawer.circle(
cos(seconds) * width / 2.0 + width / 2.0,
sin(0.5 * seconds) * height / 2.0 + height / 2.0,
140.0)
From this we surmise that every time program
is executed, the value seconds
includes a time value, probably an accumulating value. (An alternative would be delta-seconds since last call, but if that were the case, the circle wouldn’t move as it does. Also seconds
would be a uniquely bad name for deltaSeconds
.)
So as time increases, the x and y of the circle follow, yep, sinusoidal curves. since sin and cos both go from -1 to 1, out of phase with each other, x goes smoothly from 0 to width, y from 0 to height. All the rigmarole with cos*width/2.0 + width/2.0 is just coordinate transforms to keep us inside the window.
It’s time to mess with this.
drawer.circle(
cos(seconds) * width / 4.0 + width / 2.0,
sin(seconds) * height / 4.0 + height / 2.0,
140.0)
Now the pink disc rotates in a circle around the center of the screen. Why? Well, because x=cos(theta), y=sin(theta) is the way you express a circle in polar coordinates. One of the things one learns in a lifetime of learning useless things.
There’s not a lot more to learn hear. One interesting thing is the display of the background pattern:
drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2))
drawer.image(image)
If we comment out that colorMatrix line, we see the background for what it truly is:
I hope that didn’t hurt your eyes. The basic pattern is, um vivid. For the details of that colorMatrix stuff, we’ll have to read the documents, I’m afraid. Well, I’m not afraid of reading the documents, I just mean there is a limit to what we can best learn by experimentation.
I am minded to try one little thing, though. In a real graphics program, I like to have objects that know how to draw themselves. (My colleagues often add in another layer, winding up with objects that know where they are and have a view that draws them. We may explore this alternative if and when our joint project starts coming together.)
So let’s create a Disc class and use it. A bit of fiddling and we have this:
fun main() = application {
val disc = Disc(100.0)
configure {
width = 768
height = 576
}
program {
val image = loadImage("data/images/pm5544.png")
val font = loadFont("data/fonts/default.otf", 64.0)
extend {
drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2))
drawer.image(image)
disc.draw(drawer,seconds)
drawer.fontMap = font
drawer.fill = ColorRGBa.WHITE
drawer.text("OPENRNDR", width / 2.0, height / 2.0)
}
}
}
class Disc(private val radius: Double) {
fun draw(drawer: Drawer, seconds: Double) {
val xMul = drawer.width/4.0
val yMul = drawer.height/4.0
val xCenter = drawer.width/2.0
val yCenter = drawer.height/2.0
val x = cos(seconds)*xMul + xCenter
val y = sin(seconds)*yMul + yCenter
val rad = radius*cos(seconds)
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.circle(x,y,rad)
}
}
It took me a tiny bit of guessing to find out that drawer
knows width and height It makes sense that one can find out all the interesting facts from it. So now the program draws a slate blue disc that appears to approach and recede as it circles around. It looks like this:
Summary
That should be close to what we can learn here. One more thing comes to mind, making sure that my tests are hooked up correctly. That will require a bit more configuration knowledge than I have just now, so I’ll do that offline.
For now, we’ve managed to understand a basic OPENRNDR program, and get a bit of comfort from using it. To me, that’s one of the early things one does. And giving it a better design, by pulling out a class? Chef’s kiss.
See you next time!