The Sines of the Fathers
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:
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:
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:
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:
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:
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.
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:
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:
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:
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!