Chapter 9: Graphics

Graphics: Adding wow

Tomas Reimers
Tomas Reimers
Author

Some graphics, visualizations, and other dynamic content cannot (easily) be expressed in HTML and CSS alone. For those, it might be worth switching to an alternative technology. As a general rule,

  1. Start with HTML and CSS
  2. If you can't render the visualization in HTML, try SVG, which is a richer way to declaratively express grahics and illustrations (including first-class support for animations)
  3. If SVGs are insufficient, because you need more interaction or graphical filters, canvas provides a full... well, canvas, that you can render on.

SVGs: declarative graphics

While HTML and CSS are a wonderful declarative language to describe documents, they don't work so well for complex graphics. SVG (opens in a new tab) is a declarative, XML format to describe graphics.

Like HTML, SVG provides a handful of primitive tags (opens in a new tab); however, unlike HTML, those primitives are used for drawing (for example, line, rectangle, etc.)

Because it's XML-based, SVGs can:

  1. Be embedded in HTML
  2. Rendered with React

At their core, SVGs define shapes on an infinite canvas (somewhat like HTML), and then provide a "viewbox" or rectangle in that canvas that it should render (specified as two pairs of X/Y coordinates).

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Demo app</title>
  </head>
  <body>
    <svg viewBox="0 0 220 100" xmlns="http://www.w3.org/2000/svg">
      <!-- Simple rectangle -->
      <rect width="100" height="100" />

      <!-- Rounded corner rectangle -->
      <rect x="120" width="100" height="100" rx="15" />
    </svg>
  </body>
</html>

SVGs can either be inlined (e.g. above), or they can be included as an img tag (by saving in a separate file and pointing the img tag's src attribute to that file).

Unlike HTML, where the syntax is meant to be written by hand, the syntax for SVGs can get complicated quickly. For example, here is the syntax to define a polygon (the points are pairs of X/Y coordinates):

<polygon points="5,5 195,10 185,185 10,195" fill="red" />

For that reason, many people use a library to generate SVGs (or just export them from an image editor).

CSS interop

Because SVG and HTML are so interoperable, CSS can actually effect both. For example,

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Demo app</title>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <svg viewBox="0 0 220 100" xmlns="http://www.w3.org/2000/svg">
      <!-- Simple rectangle -->
      <rect width="100" height="100" />

      <!-- Rounded corner rectangle -->
      <rect x="120" width="100" height="100" rx="15" />
    </svg>
  </body>
</html>

Embedding HTML

Not only can you embed SVGs in HTML, you can also embd HTML in SVGs using foreignObject:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Demo app</title>
  </head>
  <body>
    <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
      <polygon points="5,5 195,10 185,185 10,195" fill="red" />

      <foreignObject x="20" y="20" width="160" height="160">
        <div xmlns="http://www.w3.org/1999/xhtml">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed mollis mollis
          mi ut ultricies. Nullam magna ipsum, porta vel dui convallis, rutrum
          imperdiet eros. Aliquam erat volutpat.
        </div>
      </foreignObject>
    </svg>
  </body>
</html>

SVG animation

SVGs can be animated via the animate primitive (sometimes called SMIL (opens in a new tab)).

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Demo app</title>
  </head>
  <body>
    <svg width="300" height="100">
      <rect x="0" y="0" width="300" height="100" stroke="black" stroke-width="1" />
      <circle cx="0" cy="50" r="15" fill="blue" stroke="black" stroke-width="1">
        <animate
          attributeName="cx"
          from="0"
          to="500"
          dur="5s"
          repeatCount="indefinite" />
      </circle>
    </svg>
  </body>
</html>

Lottie

Again, while animations can be written by hand, most designers prefer to iterate on them in a graphics editor and later export the animation. Lottie (opens in a new tab) is a format developed by Airbnb to export SVG animations as JSON files.

Canvas: pixel perfect control

While SVGs are powerful, sometimes you really want pixel-perfect control. For that, there is Canvas.

Canvas (opens in a new tab) allows developers to define an area that they can programmatically draw on.

import React, { useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";

export default function App() {
  const ref = useRef();
  useEffect(() => {
    const canvas = ref.current;
    const ctx = canvas.getContext("2d");

    ctx.fillStyle = "green";
    ctx.fillRect(10, 10, 150, 100);
  }, []);
  
  return <canvas ref={ref} />
}

Libraries

Because the Canvas API is so low level, many people use a higher level library (e.g. Pixi (opens in a new tab) and Fabric (opens in a new tab)).

WebGL: GPU performance

If you start to use Canvas for realtime graphics (or games), you'll quickly run into a performance problem. This isn't specific to JavaScript, and why native apps leverage the graphics card for anything intense (a specialized piece of hardware that can quickly do mathematical operations involved in rendering 3D graphics).

WebGL (opens in a new tab) allows JavaScript to use the graphics card.

ThreeJS

The APIs behind WebGL, especially for 3D, are really low level, and as a result, most people interact with it through a library, specifically Three.js (opens in a new tab). Three handles all of the internals of 3D rendering, and provides human-readable primatives for math, shapes, cameras, etc.

import * as THREE from 'three';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

function animate() {

	cube.rotation.x += 0.01;
	cube.rotation.y += 0.01;

	renderer.render( scene, camera );

}

console.log("HI");

React three fiber

Like many Graphics libraries, Three is imperative in nature; because it is not declarative, it can be awkward to use alongside React. React-three-fiber (opens in a new tab) bridges that gap, by providing react primatives for Three concepts.

import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'

function Box(props) {
  // This reference will give us direct access to the mesh
  const meshRef = useRef()
  // Set up state for the hovered and active state
  const [hovered, setHover] = useState(false)
  const [active, setActive] = useState(false)
  // Subscribe this component to the render-loop, rotate the mesh every frame
  useFrame((state, delta) => (meshRef.current.rotation.x += delta))
  // Return view, these are regular three.js elements expressed in JSX
  return (
    <mesh
      {...props}
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={(event) => setActive(!active)}
      onPointerOver={(event) => setHover(true)}
      onPointerOut={(event) => setHover(false)}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  )
}

export default function App() {
  return <Canvas>
    <ambientLight intensity={Math.PI / 2} />
    <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
    <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
    <Box position={[-1.2, 0, 0]} />
    <Box position={[1.2, 0, 0]} />
  </Canvas>;
}

In addition, it will do all the necessary Three.js cleanup as elements are mounted and unmounted.

WebGPU

While WebGL is very powerful, it's also getting old. GPUs have really advanced in leaps and bounds over the past few years and the WebGPU (opens in a new tab) spec is a successor to WebGL currently being implemented that provides access to new GPU features, and enables developers to leverage the GPU for general-purpose compute.