qr code

BIT-101

Bill Gates touched my MacBook Pro

FOTD: loopsin


[ fotd ]

This function, loopsin, is one of my favorite functions ever, and one I am quite proud of. Though I’m sure I’m not the first to think of it, I came up with it on my own and have not seen it used or written about anywhere else. It’s not rocket science, but it’s immensely useful in animating perfect loops. I’ll also go a bit into the history of how I evolved this function over time.

LoopSin

Background

A popular (with me anyway) animation technique is to use a sine wave to oscillate some property of an object - make it go back and forth or up and down or get big and small, or whatever. You just feed the sine function an ever-increasing t value, which gives you values that go back and forth between -1 and 1. Multiply that by some offset and add it to a center position, and you have oscillation.

// t initialized to 0
offset = 10
center = 100
speed = 0.01

x = center + sin(t) * offset
t += speed

Sometimes you don’t really know what the center or offset are though. You just know you want this object to go back and forth between two values, say, 117 and 236. Well, our old friend map is useful here.

// t initialized to 0
speed = 0.01
min = 117
max = 236

s = sin(t)
x = map(s, -1, 1, min, max)
t += speed

Evolving the function

When I started using Go for my creative coding, I moved away from interactive JavaScript pieces that ran forever and started creating animated gifs or videos that had a finite number of frames. One thing that really annoys me is gifs that run for a few seconds and then jump back to the start. It’s jarring and kills any positive aesthetic value the animation set out to create. So I wanted to create perfect loops.

Well, I knew how many frames I had, and I knew the current frame. By dividing those two, I could get a t value that went from 0 to just under 1 (see below). I could then map that to an angle from 0 to 2 * PI, and then run that through sine and map it to my range. This would form one full cycle through the range, which should then loop perfectly. So my function sort of looked like this:

t = frame / frameCount
min = 117
max = 236

x = loopsin(t, min, max)
circle(x, 50, 5)

function loopsin(t, min, max) {
    a = t * 2 * PI
    s = sin(a)
    return map(s, -1, 1, min, max)
}

And here’s what that looks like, using x as the position of a drawn circle:

A perfect loop!

I used something almost just like this for years. But when I started doing animations that had multipe scenes, I had to improve this somewhat. To show the problem, I added a second scene to the above. I’m expecting the first scene to animate the circle from 117 to 236 and back. In the second scene, I want to keep the x position at 117, and animate the y position from 50 to 100 with this code:

// scene 2
t = frame / frameCount
min = 50
max = 100

x = 117
y = loopsin(t, min, max)
circle(x, y, 5)

So again, I’m expecting it to go right, left, then down, up. But here’s what I get:

Ouch.

The problem is that I’m using sine, and mapping -1, 1 to min, max. The result of sine with an angle of zero will be zero. As the angle increases, it will go up to 1 and back to 0 then -1 and back to 0.

So the animation will start and end at the midpoint of min and max. Meaning it will end at the midpoint of the ranges in scene 1 and jump to the midpoint of the ranges in scene 2. Aha!

I can change that by using cosine rather than sine. The cosine of 0 is 1, it will then go down to 0 as the angle increases, then down more to -1 and back to 0 and 1. Better. But I’m mapping -1, 1 to min, max. This will have the animation start at max and animate to min. So I need to swap those so it goes from min to max instead. Here’s the new function:

function loopsin(t, min, max) {
    a = t * 2 * PI
    s = cos(a)
    return map(s, 1, -1, min, max)
}

Now maybe you’re thinking that I should change the function name to loopcos. Whatever. Name it what you want. It’s still doing a sinusoidal curve of the values, so I’m not going to get hung up on it! :)

Making that change, things now work as expected:

Now that we’ve got it working just right, we can simplify it a bit…

function loopsin(t, min, max) {
    return map(cos(t * 2 * PI), 1, -1, min, max)
}

You can apply this to more than just position. Here I’m using it to animate three different properties of objects:

In the first one, I’m calculating a radius with loopsin. In the second, I’m using it to calculate a color value. And in the final one, a rotation. Perfect loops in every case. Rough pseudocode just to give you the idea:

t = frame / frameCount

r = loopsin(t, 10, 60)
circle(80, 75, r)

v = loopsin(t, 0.0, 1.0)
rgb(v, 0, 1 - v) // rgb values go from 0.0 to 1.0 here
circle(200, 75, 50)

a = loopsin(t, -PI/2, PI/2)
translate(320, 75)
rotate(a)
rect(-40, -40, 80, 80)

If you see an animation made by me, there’s a 90% chance that it uses this function.

Advanced implementation details

Earlier I said the by dividing the current frame number by total frames, will give us a number from zero to just under one. This is because I zero-index my frames. If I have 60 frames, the frame numbers are 0 to 59. But I divide by the frame count, 60. So frame 0 gets a t of 0.0. But frame 59 will get a t of around 0.98333…, which seems like a flaw. But in fact, it’s exactly what we want.

When we convert t to an angle, we’ll get angles from 0.0 to something just under 2 * PI. And when we pass that to the cosine function, the values we’ll get back will start at exactly 1.0, go down to -1.0 and back up to just shy of 1.0.

When we map that to our min/max range, we’ll start with exactly min, move through to max and then back to just shy of min. That’s perfect. Because on the next loop, it will start of at exactly min again.

If we “fixed” things so that t went all the way to 1.0, then we’d start and end each loop at exactly min. Meaning we’d have one duplicate frame where it looped. You might not notice it, but in some cases, it could cause a tiny stutter.

Alternately, you could have your frames start at 1, rather than 0. For 60 frames, this would mean the last frame would get a t of 60/60 or 1.0, but the first frame would be 1/60, or slightly over 0.0. It should work either way, as long as you’re avoiding that duplicated frame. For me it makes more sense to start at 0.

« Previous Post
Next Post »

Comments? Best way to shout at me is on Mastodon

Or share this post directly on Mastodon