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.

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 y=sin(x)y = \sin(x) 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 (x,y)(x, y) position in a 2-D plane. The equation y=sin(x)y = \sin(x) has a range of [1,1][-1, 1]. Obviously, we do not want a curve spreading across just 2 vertical pixels. So we simply scale the function vertically:

y(x)=h4sin(x),y(x) = \frac{h}{4} \sin(x),

where hh 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 h/4=100-h/4=-100px and h/4=100h/4=100px.

Similarly, we scale the function horizontally. The function sin(x)\sin(x) has a period of 2π6.282\pi \approx 6.28. 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.

y(x)=h4sin(4πxw),y(x) = \frac{h}{4} \sin(\frac{4 \pi x}{w}),

where ww is the width of the canvas. Our function now has a period of w/2w/2.

One additional complication is the coordinate system of canvas. Unlike traditional graphs of Cartesian planes, canvas designates the origin (0,0)(0, 0) 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

y(x)=h4sin(4πxw)+h2.y(x) = - \frac{h}{4} \sin(\frac{4 \pi x}{w}) + \frac{h}{2}.

This equation defines the yy position of the curve at any given xx position.

Finally, we plot a dot for each pixel (x,y(x))(x, y(x)) for x=0,,wx=0, \ldots, w 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 y(x)y(x) 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 tt. We will define tt as a discrete “counter” that increments every time we call the draw() method. In other words, the first frame is drawn when t=0t=0, the next with t=1t=1, 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 tt. 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 t=1,120t = 1, \ldots 120.

Next, we will define our horizontal position xx. 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

x(t)=wt/120.x(t) = w t / 120.

How about the vertical yy position at each time step? Our previous equation for yy was defined in terms of xx:

y(x)=h4sin(4πxw)+h2.y(x) = - \frac{h}{4} \sin(\frac{4 \pi x}{w}) + \frac{h}{2}.

To define yy in terms of tt, we use the fact that xx is now parameterized by tt.

y(t)=y(x(t))=h4sin(4πx(t)w)+h2=h4sin(4π(wt/120)w)+h2=h4sin(4πt120)+h2\begin{aligned} y(t) = y(x(t)) &= - \frac{h}{4} \sin(\frac{4 \pi x(t)}{w}) + \frac{h}{2}\\[3ex] &= - \frac{h}{4} \sin(\frac{4 \pi (w t / 120)}{w}) + \frac{h}{2}\\[3ex] &= - \frac{h}{4} \sin(\frac{4 \pi t}{120}) + \frac{h}{2} \end{aligned}

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 tt.

Now improvise!

What if we make xx a sine function as well? What if we make yy 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!