Today, I’m going to try a fun exercise to demonstrate Codea’s translate, scale, and rotate.

There’s a thread on the Codea forum about drawing a sine wave and scaling it for various screen sizes. I suggested using Codea’s scaling ability but no one took the bait. I suspect it’s because folks haven’t used that capability much and don’t have a sense of how useful it might be. So, undaunted, I’m going to code up a little demo.

The plan is to start from a Codea example program that handles multi-touch, pare it down to deal with just two touches, and then draw a two-cycle sine wave, scaled, translated, and rotated to run between the two points you touch.

I’m expecting this to be easy. Let’s find out why I’m wrong.

The sample program, untouched (sorry), is this:


function setup()
    print("This example tracks multiple touches and colors them based on their ID")
    
    -- keep track of our touches in this table
    touches = {}
end

-- This function gets called whenever a touch
--  begins or changes state
function touched(touch)
    if touch.state == ENDED or touch.state == CANCELLED then
        -- When any touch ends, remove it from
        --  our table
        touches[touch.id] = nil
    else
        -- If the touch is in any other state 
        --  (such as BEGAN) we add it to our
        --  table
        touches[touch.id] = touch
    end
end

function draw()
    background(0, 0, 0, 255)
    
    for k,touch in pairs(touches) do

        -- Use the touch id as the random seed
        math.randomseed(touch.id)

        -- This ensures the same fill color is used for the same id
        fill(math.random(255),math.random(255),math.random(255))
        
        -- Draw ellipse at touch position
        ellipse(touch.pos.x, touch.pos.y, 100, 100)
    end
end

Looking at it, I see no reason to remove the ability to use more than two touches. I’ll only use the first two. As it stands, the program looks like this as you touch and drag on the iPad screen:

multi touch

So that’s nice. Now I’ll remove all the comments, which are not helping me, and get started.

My plan for the sine wave is to create a table containing the values of sin(x), for x going from zero to four pi, and not to calculate it again. This is just to show that it’s possible not to recalculate the sine when drawing it differently. In practice, one could do it either way.

At draw time, I’ll draw line segments between all the points of the wave, drawing it.

Now a sine wave at scale 1 would go from pixels -1 to 1 in however many steps and from pixel 0 to pixel 3.14. It would be tiny. So I’ll create my sine 100 times bigger, and I’ll draw it starting at 1/4 width, 1/2 height of the screen. Here goes.

function setup()
    print("This example draws a two-cycle sine wave between the first two touches on the screen.")
    touches = {}
    sine = {}
    size = 100
    step = 0.1
    for x = 0,4*math.pi,step do
        table[size*x] = size*sin(x)
    end
end

I’m not sure about step, this is probably too ragged, but it should give us a picture.

Now to draw it.

Hm, no, I don’t like that table. I want the table indexed by integers. Redo.

function setup()
    print("This example draws a two-cycle sine wave between the first two touches on the screen.")
    touches = {}
    sine = {}
    size = 100
    step = 0.1
    for x = 0,4*math.pi do
        table.insert(sine, vec2(size*x,size*math.sin(x)))
    end
end

Yes, that’s better. Now to draw:

function draw()
    background(0, 0, 0, 255)
    stroke(255)
    strokeWidth(3)
    for k,touch in pairs(touches) do
        math.randomseed(touch.id)
        fill(math.random(255),math.random(255),math.random(255))
        ellipse(touch.pos.x, touch.pos.y, 100, 100)
    end
    local start = vec2(WIDTH/4,HEIGHT/2)
    for i = 2,#sine do
        p0 = sine[i-1]+start
        p1 = sine[i]+start
        line(p0.x,p0.y, p1.x,p1.y)
    end
end

That gives me this jagged picture:

jagged

Step is too large, so is size. Try this:

function setup()
    print("This example draws a two-cycle sine wave between the first two touches on the screen.")
    touches = {}
    sine = {}
    local size = 50
    local step = 0.01
    for x = 0,4*math.pi do
        table.insert(sine, vec2(size*x,size*math.sin(x)))
    end
end

I made those variables local while I was at it. No point leaving globals lying around to be tripped over or stepped on.

It’s still jagged, but at least it’s smaller. Perhaps I should have put the step into the for statement?

function setup()
    print("This example draws a two-cycle sine wave between the first two touches on the screen.")
    touches = {}
    sine = {}
    local size = 50
    local step = 0.1
    for x = 0,4*math.pi,step do
        table.insert(sine, vec2(size*x,size*math.sin(x)))
    end
end

I went back to 0.1 for the step. Picture now:

smooth

I think we need an x-axis line just to have it.

function draw()
    background(0, 0, 0, 255)
    stroke(255)
    strokeWidth(3)
    for k,touch in pairs(touches) do
        math.randomseed(touch.id)
        fill(math.random(255),math.random(255),math.random(255))
        ellipse(touch.pos.x, touch.pos.y, 100, 100)
    end
    local start = vec2(WIDTH/4,HEIGHT/2)
    for i = 2,#sine do
        local p0 = sine[i-1]+start
        local p1 = sine[i]+start
        line(p0.x,p0.y, p1.x,p1.y)
    end
    left = vec2(-10,0)+start
    right = vec2(10+50*4*math.pi,0)+start
    line(left.x,left.y, right.x,right.y)
end

And the picture:

axis

I think that looks about right. Remember that the distance between points on the axis is pi, so don’t be fooled by the hills appearing so low.

Now for the good bits. This program still allows for touches on the screen.

We’ll proceed in phases. First phase is to pin the sine wave to the coordinate of the first touch.

We do all that we’re going to do before we draw the sine wave. In fact, let’s break that out into a function. We’re going to encounter an inconvenience or two in the math. Let’s see what happens.

function draw()
    background(0, 0, 0, 255)
    stroke(255)
    strokeWidth(3)
    for k,touch in pairs(touches) do
        math.randomseed(touch.id)
        fill(math.random(255),math.random(255),math.random(255))
        ellipse(touch.pos.x, touch.pos.y, 100, 100)
    end
    drawSine()
end

function drawSine()
    local start = vec2(WIDTH/4,HEIGHT/2)
    for i = 2,#sine do
        local p0 = sine[i-1]+start
        local p1 = sine[i]+start
        line(p0.x,p0.y, p1.x,p1.y)
    end
    left = vec2(-10,0)+start
    right = vec2(10+50*4*math.pi,0)+start
    line(left.x,left.y, right.x,right.y)
end

Our mission is to use Codea’s translate, scale, and rotate to get our sine to draw between our finger-touches. Again, first position it.

Darn. Touches isn’t an array. I can’t just look at touches[1] and [2]. I need to get an array of touches for my purpose. No problem. Here’s what I get:

function draw()
    background(0, 0, 0, 255)
    stroke(255)
    strokeWidth(3)
    for k,touch in pairs(touches) do
        math.randomseed(touch.id)
        fill(math.random(255),math.random(255),math.random(255))
        ellipse(touch.pos.x, touch.pos.y, 100, 100)
    end
    local myTouches = {}
    for k,touch in pairs(touches) do
        table.insert(myTouches,touch)
    end
    pushMatrix()
    local start = vec2(WIDTH/4,HEIGHT/2)
    if #myTouches >= 1 then
        local pos = myTouches[1].pos - start
        translate(pos.x,pos.y)
    end
    drawSine()
    popMatrix()
end

I move from the original touches table to myTouches, indexed by integers. (There may be a nicer way to do this. We’ll see.)

If there is one or more in the table, I pick the first one and translate to that touch’s position, subtracting out the bias of the original sine wave. The effect is this:

sine follows finger

So that’s good. Shall we do scaling or rotation first. I think rotation.

When there are two more more touches, we want to align our sine wave with the line between the first two. That angle will be the arctangent of delta y over delta x. (And I’m not sure of the sign with a g. We’ll see.)

I realize that I want to save the translation position. You’ll see that below.

    pushMatrix()
    local start = vec2(WIDTH/4,HEIGHT/2)
    local pos1, pos2
    if #myTouches >= 1 then
        pos1 = myTouches[1].pos - start
        translate(pos1.x,pos1.y)
    end
    if #myTouches >= 2 then
        local pos2 = myTouches[2].pos - start
        local ang = math.atan(pos2.y-pos1.y, pos2.x-pos1.x)
        rotate(math.deg(ang))
    end
    drawSine()
    popMatrix()

This nearly works. Check the video for what happens when I touch with a second finger.

shifted

The wave is shifted. That’s because the wave is not drawn with its origin at 0,0, but WIDTH/4, HEIGHT/2. As I mentioned above, that’s a problem. We resolve it by removing the whole start idea.

function draw()
    background(0, 0, 0, 255)
    stroke(255)
    strokeWidth(3)
    for k,touch in pairs(touches) do
        math.randomseed(touch.id)
        fill(math.random(255),math.random(255),math.random(255))
        ellipse(touch.pos.x, touch.pos.y, 100, 100)
    end
    local myTouches = {}
    for k,touch in pairs(touches) do
        table.insert(myTouches,touch)
    end
    pushMatrix()
    local pos1, pos2
    if #myTouches >= 1 then
        pos1 = myTouches[1].pos
        translate(pos1.x,pos1.y)
    end
    if #myTouches >= 2 then
        local pos2 = myTouches[2].pos
        local ang = math.atan(pos2.y-pos1.y, pos2.x-pos1.x)
        rotate(math.deg(ang))
    end
    drawSine()
    popMatrix()
end

function drawSine()
    for i = 2,#sine do
        local p0 = sine[i-1]
        local p1 = sine[i]
        line(p0.x,p0.y, p1.x,p1.y)
    end
    left = vec2(-10,0)
    right = vec2(10+50*4*math.pi,0)
    line(left.x,left.y, right.x,right.y)
end

That makes the sine wave start down in the corner, but we don’t mind yet, because it does align with our touches now:

aligned sine wave

Now we want to make the sine wave grow or shrink so that it just reaches our touches, neither falling short nor extending past.

To do that, we want to scale the screen before drawing the sine wave. The nominal length of our wave is 504pi. We can get the distance between our fingers using vec2:dist:

    if #myTouches >= 2 then
        local pos2 = myTouches[2].pos
        local ang = math.atan(pos2.y-pos1.y, pos2.x-pos1.x)
        rotate(math.deg(ang))
        local dist = pos2:dist(pos1)
        scale(dist/(4*50*math.pi))
    end

And here’s the result:

aligned and scaled wave

That’s just what we intended. Let’s change it so that the static picture still has the wave in mid screen, just for aesthetics.

With a bit of cleanup, we get this code:

-- Demonstrate translate, rotate, scale
-- RJ 20211010

function setup()
    print("This example draws a two-cycle sine wave between the first two touches on the screen.")
    touches = {}
    sine = {}
    size = 50
    local step = 0.1
    for x = 0,4*math.pi,step do
        table.insert(sine, vec2(size*x,size*math.sin(x)))
    end
end

function touched(touch)
    if touch.state == ENDED or touch.state == CANCELLED then
        touches[touch.id] = nil
    else
        touches[touch.id] = touch
    end
end

function draw()
    background(0, 0, 0, 255)
    stroke(255)
    strokeWidth(3)
    for k,touch in pairs(touches) do
        math.randomseed(touch.id)
        fill(math.random(255),math.random(255),math.random(255))
        ellipse(touch.pos.x, touch.pos.y, 100, 100)
    end
    local myTouches = {}
    for k,touch in pairs(touches) do
        table.insert(myTouches,touch)
    end
    pushMatrix()
    local pos1, pos2
    if #myTouches == 0 then
        translate(WIDTH/4, HEIGHT/2)
    else 
        pos1 = myTouches[1].pos
        translate(pos1.x,pos1.y)
    end
    if #myTouches >= 2 then
        local pos2 = myTouches[2].pos
        local ang = math.atan(pos2.y-pos1.y, pos2.x-pos1.x)
        rotate(math.deg(ang))
        local dist = pos2:dist(pos1)
        scale(dist/(4*size*math.pi))
    end
    drawSine()
    popMatrix()
end

function drawSine()
    for i = 2,#sine do
        local p0 = sine[i-1]
        local p1 = sine[i]
        line(p0.x,p0.y, p1.x,p1.y)
    end
    left = vec2(-10,0)
    right = vec2(10 + size*4*math.pi, 0)
    line(left.x,left.y, right.x,right.y)
end

I just added the zero case with a fixed translation to the center, and made size global so that I could use it in the handful of places where I had “50”. Running, it looks like this:

perfect

Summary

So there we are. A single creation of a sine curve, scaled, translated, and even rotated, without having to recalculate any of the sine’s values.

Tips
Generally speaking, first translate, then rotate, then scale. You can do it in other orders but once you scale or rotate you often have to compensate for it in odd places.

Pushing and popping the matrix can help isolate changes and allow you to draw parts of the screen in a fixed mode while others vary.

Using origin zero for your objects helps a lot. As we saw, rotations occur around the current 0,0, and if an object isn’t properly set with its rotation point at 0,0, it gets offset.

It takes a little while to get used to using the graphical matrix, but you don’t have to understand matrix transformations in detail to use it to your advantage.

One thing I don’t love about this little exercise is that the original sine wave was manually scaled to be 50x its normal size. I did that because in the Dung program, some of the scaling of tiny objects didn’t work as I would expect, so I’m afraid of tiny objects. It might well be that the tiny sine would work as well. That’s left to someone else to try. Adjustments would be needed, but only a few.

Questions welcome via the usual paths.

See you next time!