The modern web is ripe for drawing. Whether you’re making a swanky animation, a game, interactive art, a data visualization, or an eye-catching resume, there’s no shortage of tools to choose from. When I started to code, I wanted to build a music visualization tool to represent my music library, and was overwhelmed by the number of ways available to tackle it. I kicked the can down the road for weeks (okay... months), paralyzed by the possibilities. Should I use an impressive-looking JavaScript library like paper.js for 2D or three.js for 3D? Or should I stick to native HTML Canvas or WebGL? Would I need to code the physics by hand or could I use a library for that too? The sheer number of options can seem overwhelming at first, but you can draw and animate nearly anything on the web these days if you pick and master the right tool. In this article, we’ll go over some of the popular approaches to graphics-intensive projects on the web, compare their advantages, and look at some sample code. By the end, you’ll be able to confidently make your choice, and get drawing!
Pre-requisites:
This article assumes a beginner-to-medium understanding of JavaScript, HTML, and CSS. You definitely don’t need to be an expert, but should be comfortable building and styling a simple page with HTML and CSS and writing JavaScript. Here’s a good litmus test: how do you feel about making a web-based hangman game? If this sounds within reach, the rest of this article should be just fine.
The DOM: Quick and Efficient
Pros:
- easy to get started, since you’re working directly with the HTML elements you already have
- excellent for text and element transitions on text-based pages
- no need to add any external libraries or elements
Cons:
- not suitable for graphics-intensive applications or games
- performance issues when animating multiple elements or increasing refresh rate
The easiest way to draw things on a webpage is by manipulating DOM (Document Object Model) elements with CSS and JavaScript. (As a quick reminder, you can think of the DOM as anything that’s an HTML tag). This is a great option if you’re looking to animate small amounts of text or build in cool page transitions, where elements fly in and out a la PowerPoint.
As you might know, there are multiple ways to position an element on the screen using CSS: by giving your element a margin, padding, or applying a transformation, you can place it anywhere you need to. Native CSS transitionsandJavaScript both allow you to change CSS properties in response to events. Let’s review these:
Transitions
CSS has a property called transition, which allows you to move between its current state and the state defined by the CSS rule. For example, if your website offers a dark mode, you might define a .dark-mode class that sets a dark background and a light text color. When a user clicks the dark mode toggle, you can append this class to all the relevant elements (or a container) to trigger the switch to this color scheme. Rather than instantly switching the colors, you can transition to them by defining the new rules and adding a transitionproperty on the CSS rule:
.dark-mode {
color: “#000000”,
background-color: “#FFFFFF,
transition: color 0.5s, background-color 0.5s
}
The transition property takes another CSS property and a duration, and switches to that value over that duration. Although this example is fairly simple, its implications of this are powerful. You can transition most CSS elements. This means that by adding or removing a class with JavaScript, you can cause elements to shrink, grow, or move on-screen
Animations
Although the phrase “web animation” might bring back memories of scrolling banners on 1990s websites, they can be useful for elements like loading spinners. In an animation, you define keyframes, where each frame represents the state of an element at a given time. For example, if you wanted a <div> to change colors from green to red to blue, your three states would set background-color to each of these. The animation property then allows you to set how to transition from one keyframe to another—how long it should take and how smooth the transition should be.
JavaScript to the Rescue!
Applying animations and transitions to classes is made all the more powerful when you couple them with JavaScript, which allows you to change the styling on DOM elements by adding or removing classes or by directly changing element styles. This is incredibly useful when you need to respond to events on the page.
In the dark mode example above, we can create an event listener on the dark mode toggle, add a .dark-mode class to our page container, and watch CSS transitions take effect when the class is added. Similarly, you might show/hide a loading wheel with a keyframe animation in response to requesting/receiving data with JavaScript.
In addition to animations and transitions, there are two other DOM-based ways to draw shapes:
Clip Paths
CSS clip paths allow you to “clip” HTML elements, hiding parts of the element. This allows you to create interesting and inventive shapes, from playful curved divs to complex background elements.
SVG
SVG, which stands for Scalable Vector Graphics, is a markup language for drawing shapes. (MDN docs say, “SVG is essentially to graphics what HTML is to text.”) Just like HTML defines paragraphs, images, divs, and other elements that help you lay out a web page, SVG defines graphics elements in an <svg> tag, and uses <g> tags to group elements and basic shape tags (like <rect> and <line>) to draw shapes. You can then select and style these with CSS or dedicated inline attributes. SVGs are vector graphics, which means they are defined by mathematical equations. This, in turn, means that SVGs scale really well—the browser simply recalculates how the shape should look, rather than stretching an image with a set dimension. SVGs are often used as an alternative to Canvas graphics (which we’ll discuss next) for small animations, and as a basis for graphics and visualization libraries like D3.js. Because SVG elements are part of the DOM, they could be targeted by JavaScript and styled directly with CSS—unlike Canvas, which renders stateless pixels and needs additional code to handle user events (more on that in the next section). However, since traversing and manipulating the DOM tree is slower than pure JavaScript operations, SVG performance slows down as the number of elements grows.
Canvas: 2D Paradise
Pros:
- excellent way to write performant 2D graphics and games
- leverages the power of JavaScript to draw and control elements on screen, as well as fetch and process data
Cons:
- your graphics need to live within a Canvas DOM element
- primarily suitable for 2D
- no built-in support for charts (although there are data visualization libraries built on top of Canvas)
Canvas is a native JavaScript API that uses the dedicated HTML <canvas> element to draw on the screen. Canvas comes with rectangles and paths (which allow you to create more complex shapes) out of the box and a huge standard library of methods for manipulating them using JavaScript.
Before it draws every frame, JavaScript calculates the position of each shape on the screen (commonly stored in objects). Then it draws these elements on the screen as stateless pixels. What does that mean? Once JavaScript draws your shapes on the canvas it doesn’t retain any information about it. JavaScript doesn’t know that a circle it drew on the Canvas is a circle—it just sees it as individual pixels.
Because JavaScript is relatively fast compared to accessing and manipulating the DOM, using Canvas has a massive performance advantage over the methods we discussed in the previous section. Moving one <div> across the screen may not cause lag, but moving dozens of <div>s at the same time certainly will. Conversely, Canvas has no problem manipulating thousands of particles each frame. Generally, you’ll iterate through each element (or, using the power of JS logic, only the relevant elements) and update their state before drawing anything on screen.
So, what does using Canvas look like? To start, you’ll need to create a single <canvas> element in your HTML. Then, in your JavaScript file, you’ll need to select the element, and access its 2D context—the layer we’ll be drawing on:
See the Pen Canvas Demo by Max Pekarsky (@maximforever) on CodePen.
const canvas = document.querySelect('canvas');
const ctx = canvas.getContext('2d'); // ctx is a common convention for accessing for the Canvas context
The way Canvas draws onto the screen may not feel intuitive—that is, it may be different from animation software you’ve seen before.
You’ll need a refresh loop
As we mentioned earlier, Canvas is stateless and doesn’t retain any information about what’s drawn on it. Once processed, shapes are just pixels on the screen. So, generally, on every frame, you will clear your Canvas by drawing a full-size rectangle (effectively your background) and then drawing all your other objects on top of it. In traditional animation software, you might place an object on the screen, then move it, and then “capture” its position. In Canvas, instructions are declarative rather than procedural. So, rather than saying:
- place a circle
- move it 10px right
- scale it to twice its size
on every frame, you’ll say “clear the screen, then draw circle from our data.” This recalculate/erase/draw refresh loop can be powered by the JS setInterval method, or the better-performing requestAnimationFrame method. This browser function re-renders the screen at a rate that generally matches the monitor refresh rate (commonly 60 FPS), and pauses the animation when you switch tabs or hide the window.
“So, if I need to redraw every element every time,” you might ask, “where do I store the element data?” Fortunately, we’re working in JavaScript, so...
Store your element state in objects
A common practice when working with Canvas is to store data about everything on screen in objects. In our example above, you might save a Circle object (or whatever the circle represents—maybe your growing admiration of Canvas?) with an X, Y, and radius. On every frame draw, you’ll ask Canvas to draw you a circle using its native arcfunction, which can supply your object properties as arguments for where the circle should be placed and how big it should be. Then, you can update those properties. This object-driven workflow makes it a breeze to integrate external data, user events, or data you’re processing as the app is running. By separating the rendering/drawing functions from your data processing, you can focus on updating your objects, and trust that they’ll be updated on the screen on the next refresh.
All in all, your workflow will look roughly like this:
- Each frame, clear the screen by drawing a background rectangle over the Canvas.
- Update any objects you need based on game state, new data, and other incoming events (for example, if a bullet object has collided with a player object, you might decrease that player’s HP).
- Iterate through your JS objects, and for each object, draw a shape, text, or pictures based on the data you’re storing and updating.
As it lacks external dependencies, Canvas is a great, lightweight choice for many 2D projects such as games, art, and interactive experiences. However, if you want to move into 3D or are primarily interested in working with data visualization, there may be other tools (with more relevant standard libraries) that are easier to work with. If you’re ready to level up to 3D, a good next step might be...
Three.js: Welcome to a 3D World
Pros:
- a popular and richly-featured library for creating and animating 3D scenes
- intuitive animation concepts (data stored in objects)
- reasonable learning curve
Cons:
- you’ll need to think about light and camera placement
- performance can lag on older computers
- not geared towards creating charts or data visualizations
Three.js is a popular JavaScript library for creating, rendering, and interacting with 3D scenes. Under the hood, three.js uses a technology called WebGL to draw on the screen. If you’re curious, WebGL is a low-level native API that allows you to draw points, lines, and triangles. Three.js abstracts away much of the complicated math, and makes it easy to create scenes, place light sources and cameras, manage and apply textures and shading, and perform the complex math required for 3D. If you’re curious about what the end result looks like, David Lyons has a fantastic intro to the powerful features in three.js with some excellent code examples.
Because three.js is a JavaScript library, we can again leverage our JavaScript toolset to control our scene, supply it data, and respond to events. As a result, three.js a fantastic choice for games and interactive visualizations. Just like Canvas, you can pair three.js with a WebSocket connection (a two-way instant communication with the server) making this library an excellent tool to create 3D games. Moreover, if you already have some experience with 3D modeling software, three.js provides a host of model loaders. This means you can use your favorite graphics software to create models, then load them right into your scene, rather than creating them from basic shapes with code.
See the Pen Three.js example by Max Pekarsky (@maximforever) on CodePen.
So, what does it look like to build a basic 3D scene in three.js? Unlike Canvas, three.js isn’t stateless, so it might feel more intuitive to manipulate objects. However, since you’re now in 3D, you’ll need to start thinking about light and camera placement, as well as 3D physics. Just like the 2D Canvas we worked with before, WebGL uses the <canvas> element—and since three.js renders your scene with WebGL under the hood, that’s where we’ll be creating our three.js scene. Here is a small demo with comments to help you make sense of the code
In this example:
- We import three.js, since it’s an external JS library.
- We then set up a scene, an aspect ratio, a camera, and a renderer. Since our cube will be centered at (0,0,0) by default, we’ll set our camera Z position to 50.
- We append the renderer to the body, which automatically creates a <canvas> element. You could also pass in your own Canvas element.
- Next, we create a cube. To do this, we use a primitive box geometry, and then apply a material to it. Next, we add some wireframe geometry to better see the edges of the cube.
- We can’t see the cube (yet), so we add a light source. We’ll use AmbientLight for the sake of simplicity—it’ll illuminate every part of our scene equally. In the future, you’ll generally position your light sources and point them at your scene, resulting in more realistic lighting.
- We’ll use requestAnimationFrame to render our scene. You could, alternatively, use the same method as our Canvas example and create a setInterval loop. Generally, requestAnimationFrame will result in better performance.
- Finally, let’s add a little motion. We’ll rotate the cube a little each refresh, and we’ll also move the camera back and forth.
- Unlike Canvas, we don’t clear the screen every frame. With three.js, our data is stored in objects—for example, we can change the camera, light, and cube position by setting their position attribute.
Excited to keep going? ThreeJSFundamentals is a fantastic resource for learning this exciting library. Finally, while three.js is a very popular 3D library, it’s certainly not the only one. Babylon.js is a popular alternative, and aframe.js is a library built on top of three.js with a focus on VR. Or, if you like to do things from first principles, you can try learning WebGL (beware—it takes a while to get your scene off the ground!).
D3.js: Up And To The Right
Pros:
- large and active user community
- excellent out-of-the-box library of charts and transformations
Cons:
- moderate learning curve
- suitable primarily for data visualization
If your primary goal is to create data visualizations, D3.js is a popular, well-supported library used exactly for this purpose. Under the hood, D3 uses the HTML SVG feature we discussed earlier. D3 helps you marry data to DOM elements, which allow you to quickly and efficiently apply out-of-the-box transformations to out-of-the-box primitive shapes and charts. This might not be surprising at this point, but because D3 is a JavaScript library, it responds beautifully to evolving data—which you can fetch via an API or WebSockets, from user interaction, or even load from a number of file formats, including CSV. Getting started with D3 can feel a little tricky, but let’s take a look at a lightweight D3 bar graph to get a taste for working with this library.
See the Pen D3.js demo by Max Pekarsky (@maximforever) on CodePen.
In this example, we’re working with a small array of data—representing, if you’re curious, the average monthly temperature (in Fahrenheit), in Boulder, Colorado. Don’t be put off by the amount of code! The first half of it is just an array of objects representing our data. Notice that there’s no HTML at all—we’re dynamically appending SVG elements, and then styling them with CSS.
- Starting line 50, we append a .bar-chart SVG and then use the D3 .enter() function to append an SVG rectangle for every piece of data.
- Lines 65-75 position the rectangles and add a .rect class, which we can then use to style the rectangles.
In the remaining code, we proceed similarly to add labels to our chart:
- We select our weather data,
- We use .enter() on our data to iterate through it (this method gets fairly technical, but here is a great explanation!)
- For each data point, we append a label based on a data attribute—first the month, then the temperature.
- We give each label a class will allow us to later style it with CSS.
D3 can be tricky to get started with, but its ability to manipulate and visualize data is top notch. Excited to keep going? This compendium of tutorials should help get you up to speed!
Unity: Game On
Pros:
- the build process makes it easy to develop in 2D and 3D and export to different platforms
- offers a powerful editor with a physics engine and AI tools
Cons:
- steep learning curve
- closed platform
- charges for larger projects
- focused primarily on game development
Unity is a popular game engine for serious 2D and 3D game development. Unlike the libraries and technologies we’ve discussed so far, it’s neither native (like Canvas, WebGL, or DOM manipulation) nor open-source (like D3 and three.js)—in fact, Unity charges projects making over $100K per year. Unity is scripted in C#, a general-purpose C-style language. On the web, Unity runs within the now-deprecated Unity player or is compiled to a WebGL project during the build. In return, Unity offers some amazing advantages when it comes to game development:
- Cross-platform development: once you’re done developing, you can build and export your game to WebGL, mobile, VR, console, and many other platforms.
- The Unity Editor: an integrated UI that offers both design and developer tools, as well as additional AI tools like pathfinding, a physics engine, and animation tools.
- Physics Engine: Unity ships with an advanced physics engine that can help you create realistic, immersive experiences without dipping into complex mathematics or reaching for another third-party library.
Creating a Unity demo is beyond the scope of this article, but if you’re curious about what a finished product looks like, here is a WebGL-based demo. Incredible! If your goal is professional game development, tools like Unity—and its C++ based competitor Unreal Engine—are the way to go.
Physics Libraries: Just Like The Real World!
Before we wrap up our whirlwind tour of graphics on the web, let’s touch briefly on a closely-related topic: physics. No matter which of the above tools you choose to use, if your graphics app involves elements interacting with one another, you’ll soon find yourself writing code to simulate physics. Some popular examples of this are collision detection, travel paths through materials with different density, gravity, magnetic forces, and other behaviors meant to mirror how objects behave in the real world. For smaller projects, it’s possible (and maybe even fun!) to try and write the code for these by hand. However, as your project scales, you might want to consider leveraging a physics library—code written specifically to help simulate realistic physics. Some examples are PhysicsJS, matter.js, planck.js, and ammo.js. As always, the downside to using a library is overhead and added complexity—code abstracted away and potentially unused. The tradeoff is time saved not reinventing the wheel, the reliability of using a tried-and-true solution, and the fun of focusing on the parts of the app that are more interesting—unless, of course, you find reading physics textbooks interesting!
Wait, you forgot…
I hope that this article introduced you to the multitude of ways to draw, graph, and animate on the modern web. Of course, since I focused on some combination of the most popular tools and those I’ve personally used, I’ve undoubtedly left some great libraries out. As usual, I hope that this overview has given you a sense of the moving pieces when it comes to web graphics. Most of the libraries you’ll find are built on top of Canvas, SVG, and WebGL—native technologies built into and evolving in our browsers.
Have you found the right tool for your next project? Have you used another great library or engine I haven’t mentioned? Be sure to share in the comments, and I can’t wait to see the projects you make!