I want to play further with OPENRNDR. I’ve skimmed the docs. I’m impressed, and probably dangerous.

My ignorance of project setup is pushing me in a direction I ought not go. It’s pushing me to just do things with the graphics and see what happens. I’d like to be pushed in the direction of creating tested objects that just happen to draw themselves. I have a query out to my pals, but for now … I’m just going to play with OPENRNDR a bit.

The Disc

I have an object, Disc, that can draw itself. It goes like this:

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)
    }
}

The draw should at least be split into two parts, the updating of the position and look in general, and the actual drawing. I’ll just make some changes here, with the help of extract method if IDEA will cooperate. I think we need some member variables in our class.

class Disc(private val radius: Double) {
    var position: Vector2 = Vector2(0.0,0.0)
    var angle: Double = 0.0
    
    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
        position = Vector2(x,y)
        val rad = radius*cos(seconds)
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.circle(position,rad)
    }
}

Nearly good. I’m not sure how I want to handle the weird radius trick that I’m doing. And I put in an angle that isn’t used yet, just a placeholder for my thoughts. This should still work, and it does. Now extract an update method:

class Disc(private val radius: Double) {
    var position: Vector2 = Vector2(0.0,0.0)
    var angle: Double = 0.0

    fun draw(drawer: Drawer, seconds: Double) {
        update(drawer, seconds)
        val rad = radius*cos(seconds)
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.circle(position,rad)
    }

    private fun update(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
        position = Vector2(x, y)
    }
}

Better. Not great. I’d really like to use the word draw here in the method that does the actual drawing, the fill and circle. This should be the method that all our objects implement in order to be plugged into our “game loop”. It’s more than draw. update wouldn’t be a bad word for the outer method: we could rename if need be. I think I’ll try the word cycle just to see if I like it. Early days. Rename draw to cycle. Extract the drawing bit? No, I need to update to set rad, since I have the thing. Move that to update. Remove angle as speculative.

class Disc(private val radius: Double) {
    var position: Vector2 = Vector2(0.0,0.0)
    var rad: Double = 0.0

    fun cycle(drawer: Drawer, seconds: Double) {
        update(drawer, seconds)
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.circle(position,rad)
    }

    private fun update(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
        position = Vector2(x, y)
        rad = radius*cos(seconds)
    }
}

Extract the draw method:

class Disc(private val radius: Double) {
    var position: Vector2 = Vector2(0.0,0.0)
    var rad: Double = 0.0

    fun cycle(drawer: Drawer, seconds: Double) {
        update(drawer, seconds)
        draw(drawer)
    }

    private fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.circle(position, rad)
    }

    private fun update(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
        position = Vector2(x, y)
        rad = radius*cos(seconds)
    }
}

Much of what is in update is constant. We should do those once and for all. But we need the drawer to do that, or to do some other trick. I don’t have a good idea right now. Probably we can create the Disc with a drawer. But … if the window changes, maybe we want these “constants” to update. OK, we’ll leave that alone.

Now I want to change how the drawing itself is done. In particular, I want to use the OPENRNDR transform capability. I’m not sure I like what I’m about to do. I’m learning, trying to see how the bits of OPENRNDR fit together best with my style.

    private fun update(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
        position = Vector2(x, y)
        rad = radius*cos(seconds)
        drawer.translate(position)
    }

Here I’ve done the translate operation, which sets the drawing center. If we draw the circle at 0,0 now, it’ll be in the right place. But we need to push and pop the context. Too long a search finally comes up with the answer:

class Disc(private val radius: Double) {
    var position: Vector2 = Vector2(0.0,0.0)
    var rad: Double = 0.0

    fun cycle(drawer: Drawer, seconds: Double) {
        drawer.isolated {
            update(drawer, seconds)
            draw(drawer)
        }
    }

    private fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.circle(Vector2.ZERO, rad)
    }

    private fun update(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
        position = Vector2(x, y)
        rad = radius*cos(seconds)
        drawer.translate(position)
    }
}

The call to drawer.isolated pushes the style and transform information, runs the block, then restores them.

The circle draw has been changed to draw always at 0,0. This isn’t a great deal of help now, but my experience suggests that if we create all our objects to think they’re drawing at 0,0, things will go more easily.

We can fix that radius thing while we’re at it:

    private fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.circle(Vector2.ZERO, radius)
    }

Now the disc always draws itself at zero, with its full radius. So we need to scale in the update:

    private fun update(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
        position = Vector2(x, y)
        drawer.translate(position)
        val s = cos(seconds)
        drawer.scale(s,s)
    }
}

After we translate to our desired position, we scale by the cosine of time. We used to say rad = radius*cos(seconds). The scale factor is therefore cos(seconds).

So far this gives me the same picture as before:

video

Let’s draw a rectangle instead.

    private fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.rectangle(-radius, -radius, radius, radius)
    }

I believe that rectangle origins are at their corner, so if we want the center of a rectangle of “radius”, we draw from -radius to radius. Now I see a square rotating around, getting larger and smaller. But I did this for a reason: I want the square to rotate as it goes around.

I found that I didn’t like how things were moving, especially since the orbit we’re following was scaled differently in X and Y (i.e. by width and height), so I have tweaked the background and used the circle you’ll see to ensure that I have the right values.

Here’s the code that does that. It’s getting close to how I’d like things to go. We’ll review below.

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)

            drawer.fill = null
            drawer.stroke = ColorRGBa.WHITE
            drawer.circle(width/2.0,height/2.0, width/4.0)

            disc.cycle(drawer,seconds)
        }
    }
}

class Disc(private val radius: Double) {
    var position: Vector2 = Vector2(0.0,0.0)

    fun cycle(drawer: Drawer, seconds: Double) {
        drawer.isolated {
            update(drawer, seconds)
            draw(drawer)
        }
    }

    private fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.rectangle(-radius/2.0,-radius/2.0, radius, radius)
    }

    private fun update(drawer: Drawer, seconds: Double) {
        val xMul = drawer.width / 4.0
        val yMul = drawer.width / 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
        position = Vector2(x, y)
        drawer.translate(position)
        val s = cos(seconds)
        drawer.scale(s,s)
        drawer.rotate(45.0+Math.toDegrees(seconds))
    }
}

Review

I drew the white circle, and changed our rectangle to orbit in a circle, so that I could make sure that I knew how rectangles draw. They do in fact draw from the corner, not the center. That was hard to see without the circle present. I wanted the rectangle to lead with a corner, thus the 45 in the rotate. (rotate uses degrees. sin and cos use radians. consistency for the win. oh well.)

Naturally, if this were a real program that for some reason needed that circle, we’d pull it out into an object. Since it’s just there as a debugging guide, I just patched it into the draw.

I’ve learned a bit about what’s visible in the code, and where, and so I think I can rearrange the objects a bit to our benefit.

Imagine a space game like Spacewar or Asteroids. In such a thing, we’d like to have the dimensions expressed not in pixels, but in some pace units, probably not light years, which would get scaled into the screen separately. I note without understanding that OPENRNDR has a three-stage pipeline of transformations, model, view, and projection (camera viewpoint). We’ll probably want to consider that pipeline in the game. Although the use of the various transforms gets a bit mathematical, it lets us do things with simpler code. We’ll see that happen in the future, I believe.

I am not clear at this point what OPENRNDr considers model, view, and projection. We’ll have to try to learn that as we go.

I think we can simplify the code in the Disc. Let’s do that.

Disc

Using some vectors and such, how about this:

    private fun update(drawer: Drawer, seconds: Double) {
        val sunPosition = Vector2(drawer.width/2.0, drawer.height/2.0)
        val orbitRadius = drawer.width/4.0
        val orbitPosition = Vector2(cos(seconds), sin(seconds))*orbitRadius
        realPosition = orbitPosition+sunPosition
        drawer.translate(realPosition)
        val size = cos(seconds)
        drawer.scale(size,size)
        drawer.rotate(45.0+Math.toDegrees(seconds))
    }

I think this is better. At least we’re doing some vector arithmetic instead of spelling everything out. I’m not really satisfied with the separation of graphics from world. We’d like to update the Disc’s position according to world rules, gravity, thrust, whatever, and then translate, no pun intended, that into the drawing. We’re not quite there yet.

One thing we can do is move our object creation into a better location. And I’ll rename Disc to Ship, since I’m beginning to think about space here.

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)
        val ship = Ship(width/8.0)

        extend {
            drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2))
            drawer.image(image)

            drawer.fill = null
            drawer.stroke = ColorRGBa.WHITE
            drawer.circle(width/2.0,height/2.0, width/4.0)
            ship.cycle(drawer,seconds)
        }
    }
}

class Ship(private val radius: Double) {
    var realPosition: Vector2 = Vector2(0.0,0.0)

    fun cycle(drawer: Drawer, seconds: Double) {
        drawer.isolated {
            update(drawer, seconds)
            draw(drawer)
        }
    }

    private fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.rectangle(-radius/2.0,-radius/2.0, radius, radius)
    }

    private fun update(drawer: Drawer, seconds: Double) {
        val sunPosition = Vector2(drawer.width/2.0, drawer.height/2.0)
        val orbitRadius = drawer.width/4.0
        val orbitPosition = Vector2(cos(seconds), sin(seconds))*orbitRadius
        realPosition = orbitPosition+sunPosition
        drawer.translate(realPosition)
        val size = cos(seconds)
        drawer.scale(size,size)
        drawer.rotate(45.0+Math.toDegrees(seconds))
    }
}

Here’s the program running:

square orbiting on a circle

Summary

I think that’ll do for today. I’m beginning to get comfortable in this new drawing environment. It takes a while to get the rhythm of how things want to be done when we move to a new system, so I’ll be shaking that out over some time.

I need to learn how to get tests running inside this thing. I’m sure I’m going to come up against Gradle. Perhaps not with this program, but soon, I suppose I should get it up on GitHub so that people can browse it if they wish.

But small steps. Bits of learning. It’s all good.