Building a 3D Boat in Desmos

The graphing calculator Desmos recently held a competition called the Global Math Art Contest. People aged 13-18 submitted artwork they created with the graphing calculator using combinations of math curves. The judging is based on creativity, originality, visual appeal, and the math used to create the art. As a person with little artistic talent, I had to rely on the use of math category to get my edge. So I decided to create a rotatable 3D boat that moves and tilts according to the waves in the water below it. Here I’ll explain how I went about doing this. Finished project:

“Modifying” the in-browser calculator

By default, Desmos only supports 6 different colors and none of them are really optimal for a boat. I wanted to use brown for the boat and light blue for the water beneath it. I opened developer tools in Chrome to see how I could add additional colors. As it turns out, Desmos defines an instance of the calculator called Calc that can be changed from the client side. After looking at the Desmos API documentation for a little bit I was able to figure out how to modify the available colors. I used a Chrome extension called Tampermonkey to automatically execute my color adding script after the calculator loads. Here’s the script:

(function() {
    // Calc will be defined when on desmos.
    let expressions = Calc.getExpressions();
    if (expressions.length == 1 && expressions[0].latex == "") {
        Calc.removeExpressions(expressions); // this deletes the first expression, because it will already be loaded with the old colors

    Calc.updateSettings({"colors": {
        "RED": "#ff0000",
        "GREEN": "#00ff00",
        "BLUE": "#0000ff",
        "BLACK": "#000000",
        "ORANGE": "#ff8800",
        "YELLOW": "#ffff00",
        "LIGHTBLUE": "#8888ff",
        "PURPLE": "#aa00ff",
        "PINK": "#ff00bb",
        "BROWN": "#D2691E",
    }}); // This defines a new list of ten colors

Doing this might actually disqualify me from the contest.. but I checked the rules and there was no mention of using the Desmos API. It just said that the art had to be done in the online calculator, which it was. I won’t know for sure if it’s allowed until the results are announced in the second half of May.

Preparing to draw in 3D

Since Desmos is strictly a 2D graphing calculator, I had to implement my own algorithms for drawing in three dimensions. I’ve already worked out how to project from 3D world space to a 2D canvas on a screen because I needed to do that for MathGraph3D. The algorithm I implemented in Desmos is actually the OLD version of my 3D to 2D algorithm. Here is the derivation.

The basic idea is to figure out the screen coordinates of each 3D unit vector. First, we must have a way to define orientation. Call $\alpha$ the angle formed between the positive $x$ axis and a fixed vertical axis, and call $\beta$ the angle between the positive $x$ axis and a fixed horizontal axis. If $\vec{i}$, $\vec{j}$, and $\vec{k}$ are the standard unit vectors for $\mathbb{R}^3$, their projections are

$$ \vec{i}_{\text{proj}} = \left(\cos\left(\alpha\right),\sin\left(\alpha\right)\sin\left(\beta\right)\right)$$

$$ \vec{j}_{\text{proj}} = \left(-\sin\left(\alpha\right),\cos\left(\alpha\right)\sin\left(\beta\right)\right) $$

$$\vec{k}_{\text{proj}} = \left(0,\cos\left(\beta\right)\right) $$

Now, the boundaries of the 3D space must be defined. To keep things simple I decided to make the space a cube. So in the graph the value $s_{tart}$ is necessarily negative and defines the lower bound for the space in each dimension. Then $s_{top} = -s_{tart}$ defines the upper bound and $u_{nits} = s_{top} – s_{tart}$ is the number of units in each direction. I disabled the gridlines, axis numbers, and axes so the graph is just a blank white page by this point.

The water

The top of the water is the surface of a function of two variables. I wanted it to look quite realistic, so I experimented with some functions to create ripples and waves. I took advantage of the wavy nature of sinusoid functions to do this. But that comes with a different challenge: sines and cosines are periodic, and I didn’t want the waves to look like a simple pattern that repeats itself over and over. To avoid this, I messed with the period and amplitude of a sine wave and a cosine wave, then added a constant to the argument of the cosine to translate it in and out of phase with the sine. When the constant’s value is animated, the function looks like this:

(This function is defined by $w_{1}\left(x\right)=\frac{1}{4}\left(\sin\left(x\right)+\frac{1}{2}\cos\left(2x-b_{0}\right)\right)$ where $b_0$ is the constant)

Next, I defined another function $w_{2}\left(x\right)$ that is exactly the same as $w_{1}\left(x\right)$ except the constant $b_0$ is subtracted from the argument of cosine, not added. Finally, to make an occasional really big wave, I defined another function $w_{3}\left(x\right)=2e^{-\frac{1}{16}\left(x-a_{0}\right)^{2}}$. This is a scaled normal distribution with mean $a_0$. When $a_0$ is animated, the function looks like this:

These are all the tools needed to create the final water function $W\left(x,y\right)$. Here is its definition:

$$|x+y|+|x-y|\le u_{nits}:w_{1}\left(x\right)+w_{2}\left(y\right)+w_{3}\left(x+y\right)$$

The condition on that function tests if the point $(x,y)$ is in the portion of the $xy$-plane being drawn to the screen. This is needed for drawing the polygons at the side of the water to shade from the surface of the water to the bottom of the space. Notice that $w_1$ acts in the $x$ direction, $w_2$ acts in the $y$ direction, and $w_3$ acts in the $x+y$ direction, or diagonally. This creates more wave interference and thus further removes the feeling that the ripples are just a repeated pattern. Here’s what it looks like so far:

Change of basis

In reality, when I was making this project I created the boat first then added change of basis later. However, it makes more sense to explain it this way. Change of basis in this context means defining a new linearly independent set of basis unit vectors for $\mathbb{R}^3$ such that the $z$ unit vector is normal to the surface of the water under the boat. Let the new basis be $\{e_1,e_2,e_3\}$. Then $e_1$ is calculated by taking the vector difference between $(-l, 0, W(-l,0))$ and $(l, 0, W(l,0))$ where $l$ is the length of the boat. $e_2$ is the vector difference between $(0, -w, W(0, -w))$ and $(0, w, W(0, w))$ where $w$ is the width of the boat. $e_3$ is the cross product $e_1 \times e_2$. Finally, these vectors are all normalized.

Now, by multiplying the coordinates of each point on the boat by the corresponding vector in the new basis, the boat will tilt in both directions according to the shape of the water beneath it. It’s a bit hard to tell that it’s working until the big wave comes by and the boat dramatically tilts forward.

The boat

This was by far the hardest part because I’m not an artist and ruining the project with a poorly done boat would have been disappointing. But I managed to put together a pretty decent boat that, honestly, turned out better than I expected. I defined three functions $g_1$, $g_2$, and $g_3$. $g_1$ defines the shape of the boat along the front to back direction, $g_2$ defines the shape along the side to side direction, and $g_3$ is used to make the boat have curved (or “flared-out”) edges instead of straight sides. $g_1$ is a parabolic segment that is divided by the square of the boat length, so that it’s slightly curved upward but not too much. It looks like this:

$g_2$ is a hyperbolic cosine function divided by the square of the boat width. I tried a parabola at first but it was too round near the center.

$g_3$ is another normal distribution curve that depends on the width of the boat:

Putting these together, and using Desmos list magic to draw many of these curves, the boat looks like this:

Finally, I used a grid of sample $W$ values to determine the maximum height of the water near the boat. I translated the boat upwards by this amount divided by two so it stays near the surface but looks like it is displacing water.

The link to the final graph is near the top of this post. Thanks for reading!