Cool canvas animations with trigonometric parametric functions
How are animations created? This post introduces the fundamentals behind constructing 2D animations on HTML Canvas, and how one can create cool effects using just sine functions.
February 09, 2020
Animations often look magical, but in some cases can be constructed with the simplest pieces of math. Trigonometric functions are one example.
Simple sine curve
Let’s draw the sine function on an HTML canvas
.
/**
* ctx: a CanvasRenderingContext2D,
* fetchable with canvas.getContext('2d')
* w: width of canvas
* h: height of canvas
*/
draw = function(ctx, w, h) {
ctx.strokeStyle = `#ffffff`;
ctx.lineWidth = 2;
let Y = function(x) {
return 0 - (h/4) * Math.sin(4 * Math.PI * x/ w) + (h/2);
};
for (let x = 0; x < w; x+=1) {
let y = Y(x);
ctx.fillRect(x, y, 2, 2);
}
}
The output indeed looks familiar.
But there’s a surprising amount of numbers involved in function Y
.
Why is it so difficult to draw a simple sine curve?
Well, in graphics, everything must be expressed in pixels. Each pixel has a position in a 2-D plane. The equation has a range of . Obviously, we do not want a curve spreading across just 2 vertical pixels. So we simply scale the function vertically:
where is the height of the canvas.
This ensures that the our curve has an amplitude that is large is enough to be visible. For example, if the canvas height is 400px, then the curve would fluctuate between px and px.
Similarly, we scale the function horizontally. The function has a period of . But if our curve oscillated every 6.28 pixels on the horizontal axis, we would basically get a messy jumble of zig-zags. We will thus scale our horizontal axis such that only two periods of the curve will fit within the canvas.
where is the width of the canvas. Our function now has a period of .
One additional complication is the coordinate system of canvas
.
Unlike traditional graphs of Cartesian planes, canvas
designates the origin as the top-left, and the positive Y-axis increases downwards instead of upwards.
Therefore, we negate the function.
Furthermore, to ensure that our curve is vertically centered within the canvas, we translate the function by half the height.
Our final equation for the y-coordinate is thus
This equation defines the position of the curve at any given position.
Finally, we plot a dot for each pixel for using ctx.fillRect()
.
Voila! We have our sine curve.
Animated sine curve
The next step is animating the curve. The example above shows the curve being “drawn” from left to right. How was this done? We can think of an animation as simply a sequence of frames. If we plot a dot along the sine curve every few milliseconds (without erasing the previous dots), it will look like a continuous animation.
But how fast will we plot each pixel? How long will the animation last?
Our previous equation was dependent only on the horizontal position, and cannot accommodate the notion of “fast” or “long”, which describes time.
This means that we will have to introduce a new variable, time, denoted .
We will define as a discrete “counter” that increments every time we call the draw()
method.
In other words, the first frame is drawn when , the next with , and so on.
With HTML canvas
, the frame rate is typically 60Hz, meaning draw()
will be called 60 times per second.
Now all we have to do is figure out the X and Y position of the sine curve at a given point in time . A convenient way to start is to decide how long the entire animation will last. Let’s choose 2 seconds. Then we will draw a total of 120 frames, thus .
Next, we will define our horizontal position . Play the animation again and pay attention to the horizontal movement of the curve. You can see that it moves at a even rate along the X-axis, and that it reaches the right end in approximately 2 seconds. The equation for this movement would simply be
How about the vertical position at each time step? Our previous equation for was defined in terms of :
To define in terms of , we use the fact that is now parameterized by .
And here are the equations in code.
/**
* ctx: a CanvasRenderingContext2D,
* fetchable with canvas.getContext('2d')
* t: time
* w: width of canvas
* h: height of canvas
*/
draw = function(ctx, t, w, h) {
ctx.strokeStyle = `#ffffff`;
ctx.lineWidth = 2;
let x = function(t) {
return w * t / 120*;
};
let y = function(t) {
return 0 - Math.sin(t * (4*Math.PI) / 120) * (h/4) + (h/2);
};
ctx.beginPath();
ctx.moveTo(x(t), y(t));
ctx.lineTo(x(t+1), y(t+1));
ctx.stroke();
}
You might have noticed that instead of drawing points with fillRect()
, we are drawing line segments with lineTo()
.
But that’s just a small implementation detail for making smoother canvas
animations—the logic of the code has not changed from the previous example, except that we now have two functions of position that are each dependent upon the time .
Now improvise!
What if we make a sine function as well? What if we make a product of two sine functions? What if we change the color each time step? The possibilities are endless, and you will be amazed at how aesthetically interesting some of the results can be. Here is one example, along with the code.
draw = function(ctx, t, w, h) {
let x = function(t) {
return Math.sin(11*Math.PI*t / 240) * Math.sin(10*Math.PI*t / 240) * (w/4) + (w/2);
};
let y = function(t) {
return Math.sin(5*Math.PI*t / 240) * (h/4) + (h/2);
};
let r = function(t) {
return 200 + Math.sin(t / 120) * 55;
};
let g = function(t) {
return Math.sin(t / 120) * 255;
};
let b = function(t) {
return 200 + Math.sin(t / 60) * 55;
};
ctx.strokeStyle = `rgb(
${r(t)},
${g(t)},
${b(t)}`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x(t), y(t));
ctx.lineTo(x(t+2), y(t+2));
ctx.stroke();
}
Using the periodic property of sine functions, we were able to oscillate not only within the 2D plane, but the RGB color space as well. Overall, I hope this post has showed you how the properties of certain mathematical functions can be utilized to construct cool animations. There are no limits to what you can produce. Be creative!