For web animation, the plus side of using libraries like gsap is that they can help standardize the way animations work across projects. They provide lots of niceties around responsiveness, browser capability detection, and so on, as well as powerful, chainable timelines. And for complex animations, or carefully orchestrated motion, you absolutely should be using a production-ready library.
But… we want to play in the sunshine. We want to be free. What if we don’t want any libraries at all? Computers have been amazing for decades, and you can make them do beautiful things with a little math and wits alone. Here’s a few lovely motion options using just Math.sin()
and a frame loop.
U Got the loop
All of the following examples will be driven by a javascript frame loop. Here’s the skeleton of the kind of loop I write at least a couple times a day:
function loop(time) {
// animation logic goes here
window.requestAnimationFrame(loop);
}
Some things never change, and indeed animation has been driven by some function that gets called every frame and modifies the scene in some way since the Carter administration.
You write a function that will execute over and over. On the web, this is any function that, as its final act, schedules one more run for itself, like a kid getting off a rollercoaster and then right back in line.
Whenever this function is called, the browser passes a single value to it as an argument on the next round, which is an ever-increasing number of milliseconds since the page loaded. This is a handy number for lots of reasons. You can use it to find out how long each frame is taking, to drive a timeline, to measure performance. In our case, we’re using it like an engine, and we can yoke it to all sorts of things.
Would You Let Me Give You Some Math?
Here is the part with the trigonometry. To use these examples, you don’t really need to dig into the math behind them. But… it’s pleasant to know. And it helps you improvise.
Let’s describe the Math.sin(angle) function: its single input is a number for your angle in radians, and its output is always number between -1 and 1. Critically, any float is valid input. Impossibly large or negative… no problem. The output range will always be naturally, smoothly clamped between -1 and 1.
But what’s it doing in there? What’s this strange relationship (ship ship ship)? To illustrate how it works, imagine you had a round gear on an infinite straight-toothed rack like so. Moving the gear linearly makes it turn.
Let’s assume this gear has a 1 unit radius, and its center is at x=0, y=0. If you were to trace out just the vertical position of a dot on its left edge as it rolled, it would draw a beautiful sine wave that topped out at 1, and bottomed out at -1. It would complete one full turn after rolling a distance of 2 x π (formerly known as the circumference of your circle).
Think of the long linear gear as Math.sin()
‘s input, and the vertical position of the dot on the gear as its output.
If you wanted to know where, vertically, your dot would be after the gear rolls a certain distance horizontally, plug that distance into Math.sin()
. Officially, it takes the “angle in radians”… but for a gear or wheel of radius 1 unit, Math.sin(distance)
turns out to be the same thing. It returns how far above or below the center of your wheel that dot is, assuming it starts at the left side.
Math.sin()
does a simple but semi-cosmic thing: it represents the fundamental relationship between circles and triangles and angles in the simplest way that still has some utility. You can use this relationship to calculate angles or distances in triangles, to trace out arcs or points on a circle’s circumference, or to give objects organic motion that has some controllable variation over fields space and time.
Baby, Don’t Waste Your time
Back to our frame loop: it doesn’t return anything, and just takes a float for time as an input. Once time is but a number, you can do any math you do on numbers to adjust it. Here’s some levers you can pull to manipulate it:
- To make time go faster, multiply
time
by a number bigger than 1. - To make time go slower, divide
time
by a number bigger than 1. - To make it ramp up in speed exponentially, multiply
time
by itself (ie square it). - To make it ramp down exponentially, divide 1 by
time
. - To offset
time
, add or subtract a value.
// get any objects with the 'gentle-bob' class
let objs = window.querySelectorAll('.gentle-bob');
// multiplied by something between -1 and 1
let scale = 100;
function loop(time) {
// slow time down from milliseconds to seconds
let t = time / 1000;
// loop over each object we wish to animate
objs.forEach((o, idx) => {
// feed the current time in seconds into Math.sin
let verticalOffset = Math.sin(t);
// assign that as the scale value (fun options w/ css vars here)
o.style.transform = `translate2d(0px,${verticalOffset*scale}px`;
});
// animation logic goes here
window.requestAnimationFrame(loop);
}
With Math.sin()
, milliseconds make it run way too fast, and it’s so much better when we take our time, so we divide time
by a thousand to turn it from milliseconds into seconds. Then we can feed it into Math.sin()
, and yoke its output to the vertical position of an image.
This is an effect I have made dozens of times that I usually call gentle bob. You always need a gentle bob. I’ve implemented this on the web, on the Nintendo Entertainment System, on an Arduino, in VR. It’s always handy, and there’s a sin() function waiting for you on pretty much any platform.
In some ways, Math.sin() sort of works like a hash function in that input values always map to the same output value. This makes it great for adding these kinds of motions to many different objects. It gets particularly interesting if we added a phase offset. This the same code as above, but instead of just adding the same time
value for every object, we add time + xPosition
. This means each one is a little bit ahead of the one before it in its oscillation, and finally for the first time we can see an actual sine wave.
You can play with this in lots of interesting ways. Why not offset by x and y position, across a grid of objects? This lends it an oceanic kind of a feel.
I wanna use it baby Math.sin()
, all right
This demo uses Math.sin() to generate an SVG paths, basically using a bunch of line segments to draw out the curve of our sine waves. Each draws a certain period, and note that they each end right where they begin. The first is 2π across, the second 4π, the third 8π. Note that each wave starts and ends at the same vertical position.
As long as all of these items land on multiples of 2*π, they will become aligned, and choosing different multiples and offsets can help create these complex interlocking animations without keyframes or timelines or fiddly curves.
Being able to decide on these offsets also lets you do something people with modular synthesizers love to jabber on about: additive synthesis. This is just adding different sine waves together to create more chaotic and complex waveforms.
There's power in centering things around 0 as the origin. It makes it easy to add different frequencies together without adding an overall offset to something. In the demo above, we take our basic sine wave from earlier, and rather than drawing each separately, we add them together before we set the Y position of each point in our line segment. Because every value is between -1 and 1, the overall waveform remains vertically centered around the 0 point, no matter how many frequencies we add together to create our more complex waveform.
There's no reason not to add some randomness in here now that we know how things work. Adjusting the time multiplier and period to numbers that aren't clean multiples of π can create lovely, organic animations that have a nice mix of controllability and unpredictability, and despite their mathematical simplicity will never appear to repeat.
Forever
The cyclical nature of Math.sin()
is a good match for circular animations as well. Since it's a repeating pattern every 2*π (~6.28), we can use that to make sure beginnings and ends line up across any number of objects arranged around a circle. Join me in the 3rd dimension for this example:
Rather than offset items along the X or Y axis as we did in the ocean waves example above, we offset them by their angle around a center point. We know that the Math.sin function repeats every 6.28, so if we have 10 items, we add (1/10 * 6.28) to each one. That way, the motion of the first and last items will smoothly fade into each other in a way that continues working regardless of radius, speed, scale or count. You can play with the sliders in the control panel to adjust the strength and frequency of rotation in three directions.
Here’s the equation we’re using to actually determine each rotation:
child.rotation.x =
Math.sin(
idx * offset +
speed * time * (rotFreq[0] * Math.PI * 2)
) *
rotStrength[0];
Here we set each of the X, Y and Z rotations to the output of Math.sin(). The first line idx * offset
gives us that n/6.28 offset above to make the start and end line up. Next the speed * time
line uses the timestamp to oscillate at a certain frequency in seconds, and finally the rotStrength
multiplies our -1 to 1 Math.sin output to control the strength of the rotation. You’re essentially driving each rotation value with an LFO, if synthesizer metaphors are helpful.
Sineing Off
For the record, you probably aren’t going to run around swapping out gsap
for Math.sin()
in most situations. If gsap
is a workshop, then Math.sin()
is more of a screwdriver. But it’s a screwdriver that’s already in your pocket, so to speak, and knowing how to use it can get you out of some jams and simplify some common kinds of motion.
For situations where you are trying to really tighten load times for a hero or loading animation, the best library may be no library at all.