Now for Something Completely Different: A Codea Lua experiment for Hill.

The curiously symmetrical GeePaw Hill wants to do an animation for a class he’s working on, and although he is making fine progress on his own, he has Tom Sawyered me into working on the problem. The basic notion is to set up a system kind of like the old Spirograph toy. Wheels within wheels, identified points on the wheels, tracing through space. We’ll leave his plans to him, but what I want to do in the next few articles is to show how I might do a Spirograph kind of thing in Codea Lua. Just what it’ll do I’ve not decided, but basically wheels rolling in wheels and dot tracing interesting paths.

Rough Idea

The basic idea is that there will be fixed points, lines, circles, and target points, names to be determined. The base of things will be a fixed point or perhaps a fixed line, TBD.

The objects all have a collection of children which are defined relative to their parent, so as to make up the starting arrangement of stuff. Each object will have a value that defines the amount of motion it should have in one second, to be scaled by deltaTime as one does.

The “game cycle” will be to move the base item, which will be fixed. After it moves (not), it will move its children. Children move by determining the distance they move, generally the angle they rotate times their radius. They then ask their parent to for the position they should move to, given their current coordinates and the distance. The parent, say, a circle, translates that distance to a central angle in itself, and computes where the child should now be. A line, of course, just adjusts the child’s x coordinate.

I’m not clear at this instant just what information will be needed. I figure circles will be defined at their center, offset from their parent’s line/circumference by their radius. The distance moved will not at be the center, but at the point where the circle touches its parent, so there will be some fiddling. I am guessing that the circle will pass in its center position and we’ll have to do some fiddling with the radii and so on.

Let’s Get Started

I’m start with my usual base template, CUBase, the Codea Unit starting template. So far I have no useful tests, but I have this code from yesterday.

-- CUBase

function setup()
    --CodeaUnit_Detailed = false
    CodeaUnit_Lock = false
    if CodeaUnit then 
        _.execute()
    end
    center = Point(WIDTH/2, HEIGHT/2)
    local outer = Circle(250, center)
    center:add(outer)
    local inner = Circle(100, outer)
    outer:add(inner)
end

function draw()
    background(40)
    if CodeaUnit and CodeaUnit_Lock then 
        _.showTests()
    else
        pushMatrix()
        background(128)
        stroke(0)
        strokeWidth(3)
        noFill()
        --text("Gear?", WIDTH/2,HEIGHT/2)
        center:move()
        center:draw()
        popMatrix()
    end
end

function touched()
    CodeaUnit_Lock = false
end

This is mostly boilerplate to run the tests, but I’ve defined three of my spirograph objects, a Point at the center of the screen, and two Circle instances, a large one that I intend to be the track for the smaller one and, well, a smaller one. My intention is that the big circle should appear with the Point as its center, and the small one should appear ab the bottom of the large one, so as to be running around the inside of it.

Other than the boilerplate, the draw function just moves and draws the center point. Before we read the code, let me show you what happens, because we’d like to fix it:

big circle too high

The big circle is positioned with its bottom at center, not its center. Let’s review the code as it stands now, just the initial stuff I’ve scribbled in over the past few days:

Point = class()

function Point:init(x,y)
    self.x = x
    self.y = y
    self.radius = 0
    self.children = {}
end

function Point:add(child)
    table.insert(self.children, child)
end

function Point:draw()
    translate(self.x, self.y)
    stroke(0,255,0)
    line(0,-3, 0,3)
    line(-3,0,3,0)
    for i,c in ipairs(self.children) do
        c:draw()
    end
end

function Point:move()
    
end

Circle = class()

function Circle:init(radius, owner)
    -- you can accept and set parameters here
    self.owner = owner
    self.radius = radius
    self.angle = 0
    self.x = 0
    self.y = 0
    self.children = {}
end

function Circle:add(child)
    table.insert(self.children, child)
end

function Circle:draw( ownerX, ownerY)
    --pushMatrix()
    --pushStyle()
    stroke(255,0,0)
    translate(0, -self.owner.radius + self.radius)
    ellipse(0,0, 2*self.radius)
    for i,c in ipairs(self.children) do
        c:draw()
    end
    --popStyle()
    --popMatrix()
end

You can tell from that draw method that Circle is just something scratched down earlier. It’s actually coded to position itself at that odd Y coordinate you see in the translate, which is what positioned the small circle correctly, and the big one incorrectly.

We have too many member variables and too many assumptions. Let’s improve the assumptions.

We’ll declare that every Circle (and probably every Point and Line too) considers its center to be at 0,0. When they get positioned, their parent will figure out what to do. How this will happen … well, that’s got to evolve, which is another way of saying that I sure don’t know right now.

Let’s trim down the Circle:draw to the bare minimum. We won’t do a translate at all: I’m not sure yet who should do those, if we have them at all.

Two Ways
There are basically two ways to position and rotate objects in a program like this. One is to treat the screen as a big array of points and to position things at the actual coordinate where they should be, and draw everything in place. In this scheme, the objects know their and rotation in “real” space.

The other way is for all the objects to think of themselves as positioned at 0,0 and rotated right side up, and to use the graphical system’s translate and rotate to draw them at the right place on the screen with the right rotation.

When we use the graphical system’s operations, the math we have to do in our objects is usually simpler. Naturally, this isn’t always true, and there are ways to make things worse, especially if you mix graphical and real operations. At this writing, I don’t know which way we’ll want to go, so the best thing to do is probably to back away until I get a better feeling for how this works.

I tweak the colors, remove the translate and get this:

function Circle:draw( ownerX, ownerY)
    --pushMatrix()
    --pushStyle()
    stroke(255)
    ellipse(0,0, 2*self.radius)
    for i,c in ipairs(self.children) do
        c:draw()
    end
    --popStyle()
    --popMatrix()
end

And this:

centered circles

Here, everyone is drawing at their natural location. I think this is a better starting point. Let’s start filling in move, back in Point:

function Point:move()
    for i,c in ipairs(self.children) do
        c:move()
    end
end

The Point doesn’t move, so it just tells its children to move. Circle doesn’t know how. This is our big chance.

A Bit of Speculative Planning

A circle thinks of itself as being at 0,0. It does have an angle, and I’m not sure yet how we’ll manage that. It starts at zero, at least for now. So I think that the Circle wants to ask its owner to compute the circle’s position, relative to the owner.

Or … another possibility would be for the move command to compute that position before even inviting the child to move.

And … I’m even starting to question whether we should do move move move draw draw draw, or move draw move draw move draw.

We’ll leave that question open, and there is a cost to doing so. The cost is that the objects will have to remember their new position and rotation. (We could of course remember them for them all but that would complicate the children collection a lot.) For now, we’ll say that the Circles will provide their current x,y to the parent and the parent will return the new x,y. This call may need more parameters later. I don’t know. We’ll find out and deal with whatever happens.

So, Circle move:

function Circle:move()
    self:getNewPosition()
    for i,c in ipairs(self.children) do
        c: move()
    end
end
By Intention
I was taught to call the trick shown above “Programming by Intention”. When you don’t know yet how to do a thing, you give the thing a name and call a function of that name. So we’ll get the new position. Working that way let us get the basic shape of the program a bit further along. Now we need to code getNewPosition, but the idea is nicely isolated and we don’t have to think about other matters.

Now I’ve done a bit of thinking about this and I am guessing that what will happen is that each circle will have a small angle that it uses to roll a bit, along the path of its parent. A Circle knows that if it rolls by and angle a, it will roll a distance of r times a, where r is its radius. That’s how they roll. Angles in radians by the way.

So I’m thinking the Circle will ask its parent to compute its new position given its old position and the angle it wants to roll by.

Let’s just try it:

function Circle:getNewPosition()
    self.x, self.y = parent:moveMe(self.x, self.y, 0)
end
Wrong
The code above is wrong. There is no thing called parent. Fixed below.

Yes, we can do that in Lua. Also, yes, we should convert x,y to vectors but not yet, I’m workin’ here.

And in Point …

function Point:moveMe(x,y,d)
    return self.x, self.y
end

That should position the big circle center at the Point’s enter, i.e. middle of the screen. Now let’s do Circle’s move me.

I realize immediately that I need to know the child circle’s radius in order to position it. We’ll have to pass in the object. Let’s change that all the way around:

function Point:moveMe(child,d)
    return self.x, self.y
end


function Circle:moveMe(child,d)
    local x = self.x
    local y = self.y - self.radius + child.radius
end

function Circle:getNewPosition()
    self.x, self.y = self.owner:moveMe(self, 0)
end

Let me say right now that this code is assuming that the child has a radius and while that’s true for now, it seems wrong. We’ll see. Make it work, then make it right. Let’s run it.

The picture comes out the same. Two concentric circles about a green dot. What have I done wrong?

I haven’t returned my result. Kind of surprised we didn’t blow up trying to draw the inner circle.

function Circle:moveMe(child,d)
    local x = self.x
    local y = self.y - self.radius + child.radius
    return x,y
end

Oh and I’m drawing the Circle at 0,0. Might want to use self x and y.

function Circle:draw( ownerX, ownerY)
    --pushMatrix()
    --pushStyle()
    stroke(255)
    ellipse(self.x, self.y, 2*self.radius)
    for i,c in ipairs(self.children) do
        c:draw()
    end
    --popStyle()
    --popMatrix()
end

Now then.

circles at top right

The circles are now being drawn at top right. But the little one is where it should be relative to the big one.

Is my Point thinking it’s at WIDTH,HEIGHT rather than those over 2?

No! Remember when I said that you can get in trouble when you mix graphical translations and rotations with explicit ones? Well, I did that. There’s a translate in Point:draw, and it’s leaving the screen translated, so the other guys are drawn in the wrong place. Fix that thus:

function Point:draw()
    pushMatrix()
    translate(self.x, self.y)
    stroke(0,255,0)
    line(0,-3, 0,3)
    line(-3,0,3,0)
    popMatrix()
    for i,c in ipairs(self.children) do
        c:draw()
    end
end

And we get the picture I intended:

big circle tangent inner circle

OK, we have something to work from. And we have 300 lines of article, which is plenty. Let’s sum up and come back to this later.

Summary

I think we have something that constitutes a bit more than a spike and certainly a lot less than a working program. I’ll dump all the code below the line here, in case you want to browse it.

No surprise, this is pretty ragged, but it’s got approximately the right structure, with objects owning objects, objects moving and drawing their children, and the children asking for advice from their parents. I’ve called them owner. Probably parent will be a better name.

Right now the rules are that you can use translate and rotate if you want to, inside your own draw, but you have to pop them back to whatever was provided you before you process your children. I do think it would be quite interesting if we could do all the math with translate and rotate in the graphical system, but I’m not sure if we can. I’m also not sure whether it will clear to my eventual readers. All three of them.

I’ve found this to be a bit of a struggle, for at least two reasons.

First, I don’t quite see how to do this. I have the general idea, and I’ve written some design notes, that I’ll show you next time, but I don’t quite see it.

Second, I am very clumsy with Codea Lua, coming right off the very powerful Kotlin and IDEA. The limited Codea editor slows me more than it used to, and the syntax differences cause me to type in the wrong language a high percentage of the time.

But I think it’s starting to come together. I’m pretty sure we won’t need all the digits of the article file name this time.

See you next time!


--# Main
-- CUBase

function setup()
    --CodeaUnit_Detailed = false
    CodeaUnit_Lock = false
    if CodeaUnit then 
        _.execute()
    end
    center = Point(WIDTH/2, HEIGHT/2)
    local outer = Circle(250, center)
    center:add(outer)
    local inner = Circle(100, outer)
    outer:add(inner)
end

function draw()
    background(40)
    if CodeaUnit and CodeaUnit_Lock then 
        _.showTests()
    else
        ellipseMode(RADIUS)
        pushMatrix()
        pushStyle()
        background(0)
        stroke(255)
        strokeWidth(3)
        noFill()
        center:move()
        center:draw()
        popStyle()
        popMatrix()
    end
end

function touched()
    CodeaUnit_Lock = false
end


--# Tests
-- RJ 20220608
-- DUPLICATE these then edit.

function test_RENAME()

    _:describe("RENAME", function()

        _:before(function()
        end)

        _:after(function()
        end)

        _:test("HOOKUP", function()
            _:expect("Foo", "testing fooness").is("Foo")
        end)
        
        _:test("table insert", function()
            local t = {}
            table.insert(t,3)
            _:expect(t[1]).is(3)
        end)
        
    end)
end

--# Circle
Circle = class()

function Circle:init(radius, owner)
    -- you can accept and set parameters here
    self.owner = owner
    self.radius = radius
    self.angle = 0
    self.x = 0
    self.y = 0
    self.children = {}
end

function Circle:add(child)
    table.insert(self.children, child)
end

function Circle:draw( ownerX, ownerY)
    --pushMatrix()
    --pushStyle()
    stroke(255)
    ellipse(self.x, self.y, self.radius)
    for i,c in ipairs(self.children) do
        c:draw()
    end
    --popStyle()
    --popMatrix()
end

function Circle:getNewPosition()
    self.x, self.y = self.owner:moveMe(self, 0)
end

function Circle:move()
    self:getNewPosition()
    for i,c in ipairs(self.children) do
        c: move()
    end
end

function Circle:moveMe(child,d)
    local x = self.x
    local y = self.y - self.radius + child.radius
    return x,y
end

--# Point
Point = class()

function Point:init(x,y)
    self.x = x
    self.y = y
    self.radius = 0
    self.children = {}
end

function Point:add(child)
    table.insert(self.children, child)
end

function Point:draw()
    pushMatrix()
    translate(self.x, self.y)
    stroke(0,255,0)
    line(0,-3, 0,3)
    line(-3,0,3,0)
    popMatrix()
    for i,c in ipairs(self.children) do
        c:draw()
    end
end

function Point:move()
    for i,c in ipairs(self.children) do
        c:move()
    end
end

function Point:moveMe(child,d)
    return self.x, self.y
end