As we were exploring options on how to best illustrate a brand story recently, we came up with a pretty interesting prototype: a multi-step animation that starts with a rotating globe full of particles.
From a technical perspective, that first step was definitely the most interesting one. Because as all the following animation steps were plain 2D, I couldn’t use a 3D renderer such as Three.js. And so I had to figure out how to render a 3D shape using only the Canvas 2D API.
For the CSS, we need to remove the default margins on the body and prevent scrollbars from appearing by using ‘overflow: hidden;’. Because we want our canvas to be full screen, we define its width and height at 100% of the viewport.
The first part of a class is the constructor method and we use it to store the custom properties of each particle. We also create two methods for our dots: project() and draw(). The project method is were the magic happens: we convert the 3D coordinates of our particle to a 2D world. Finally, the draw method is where we draw the particle on our canvas after we calculate the projected values.
You may have noticed we are using a PERSPECTIVE constant. This value will define the behavior of the ‘camera’ we are simulating in the project method. If you increase its value, you will notice that the perspective is getting less and less visible, everything will look flat. If you do the opposite and lower the value, the perspective will be much more intense.
Now that we have all our particles ready to be rendered on the screen, we need to create a small function that loops through all the dots and renders them on the canvas.
If you try the code as is, you will get something very static because we are not moving the dots in our 3D scene.
To make everything move there are many options and there is no perfect solution. In my case I’m going to use the TweenMax plugin made by GreenSock because it doesn’t require a lot of setup and is pretty straightforward.
You may think it is not that interesting to write so much for so little. We could get away with simulating the complex math. But, now that the hard part is done, we can go crazy!
To create a globe out of particles, we need to calculate their coordinates on its surface. The coordinates of a point along a sphere don’t use the classic Cartesian Coordinates (x, y, z) but three different values from the Polar Coordinate System:
- Radius: The radius of the sphere
- Theta: The polar angle (between 0 and 360°)
- Phi: The azimuth angle (between -90° and 90°)
In our case we want random values for every dot, so we will define the Theta and Phi values randomly when we create a new one. The radius being the same for every dot we can store in into a global variable.
You may be wondering why we are not using a more basic way to generate a random value for Phi. If we were to do so, the distribution along the sphere wouldn’t be uniform and would display more particles around the poles. This is why we are using using an Arc Cosine (acos) function to fix this issue.
Finally we need to convert the coordinates from Polar to Cartesian to match our current 3D world every time we want to project the dot on the 2D canvas. We are also drawing it using a circle this time, instead of a rectangle.
So far we have only used a basic monochrome shape from the Canvas API. If we want to use a custom texture for our particles, we face a problem.
In the render function we are drawing the particles based on their order in the dots array. When we use textures it will become noticeable that we are drawing particles on top of others, without taking their depth value into account. This will create a weird result with further particles being displayed over closer ones.
To fix this issue, we need to sort the array of dots based on their Z position right before we draw them so the furthest dots will be drawn first and the closest ones last. We call this method Depth Sorting!
Let’s get really crazy and see if we can render more than just flat shapes with our script! (Spoiler alert: we can) If we think of a cube as data, we can decompose its construction in two parts: 8 vertices and 12 lines.
With that information, we can set two variables in our script to store this data.
- The first variable contains the coordinates of every vertex of cube.
- The second variable is an array of 12 pairs of vertices stored by their index.
We will use this second variable to draw the lines based on those pairs. For example the first pair [0, 1] means that we connect the vertex to the vertex.
From those two variables we can generate random cubes in our scene and start animating them by moving the vertices in our 3D world.
You could go even further and draw the faces of the cubes instead of their edges. To do so you will need to know what vertices must be connected for every face and check what faces must be drawn first.
Do not hesitate to fork any demo and explore more crazy ideas! If you have any question or remark about this article, do not hesitate to ping me on Twitter @Mamboleoo.