Three Js example

How it works

The smallest Three.js setup in the framework: a scene, a camera, a renderer and a cube that spins.

The class in src/js/handler/three is the starting point for any WebGL component built with Three.js (three on npm). It creates a scene, a perspective camera and a WebGLRenderer sized to the element, appends the renderer's canvas to the element and adds one mesh: a 1×1×1 box with a MeshNormalMaterial, which colours every face by the direction it points to and needs no lights. A requestAnimationFrame loop turns the box by 0.01 radians per frame on x and y and renders.

Everything lives in ThreeGL.js; the handler only names the hook. The renderer is created with antialiasing and a transparent background, so the page shows through around the cube, and the pixel ratio is capped at 2 so a retina screen does not quadruple the pixels drawn. On window resize the camera aspect and the renderer size follow the element.

The markup

One element with the js--three-webgl hook. It must have a height: the canvas takes the element's clientWidth and clientHeight, so an element with no height draws nothing. The example sizes it with an inline style to keep the snippet short; in a project give the element a component class and set the size in SCSS.

<!-- The element is the frame; the class appends a canvas that fills it -->
<div class="js--three-webgl" style="width: 100%; height: 400px;"></div>

The handler

Handler.js has nothing to pass on: the config is the element and nothing else, since the class has no options.

this.config = ({ element }) => ({
    element,
});

This is the whole setup, in ThreeGL.js. The numbers to change are here: the field of view and distance of the camera, the geometry and the material:

init() {
  this._setSize();

  this.scene = new THREE.Scene();

  this.camera = new THREE.PerspectiveCamera(75, this.width / this.height, 0.1, 1000);
  this.camera.position.z = 3;

  this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
  this.renderer.setSize(this.width, this.height);
  this.DOM.element.appendChild(this.renderer.domElement);

  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const material = new THREE.MeshNormalMaterial();
  this.mesh = new THREE.Mesh(geometry, material);
  this.scene.add(this.mesh);

  this.animate();
}

What the framework does for you

  • Lazy loading. An element is only built when it gets within 30px of the viewport (Boostify scroll). ThreeGL.js, and with it three, is imported through the Manager on first use, so a page without the hook never downloads the library.
  • Page transitions. Before swup replaces the content every instance is destroyed: the frame loop is cancelled, the resize listener removed, the geometry and material disposed, the scene cleared, the renderer disposed and its canvas taken out of the DOM. The new page builds its own.
  • One renderer per instance. Every element gets its own WebGLRenderer, which is its own WebGL context. Browsers allow a limited number of live contexts, so keep the number of hooks on a page small, and rely on the destroy above to free them on every transition.
if (!this.DOM?.element) return;

cancelAnimationFrame(this.animationId);
window.removeEventListener("resize", this.onResize);

// dispose geometry and material explicitly (scene.clear() does not dispose)
if (this.mesh) {
  if (this.mesh.geometry) this.mesh.geometry.dispose();

  if (this.mesh.material) {
    if (Array.isArray(this.mesh.material)) {
      this.mesh.material.forEach((m) => m.dispose && m.dispose());
    } else {
      this.mesh.material.dispose && this.mesh.material.dispose();
    }
  }
}

if (this.scene) this.scene.clear();

if (this.renderer) {
  this.renderer.dispose();
  this.renderer.domElement?.remove();
}

Cube

A box with MeshNormalMaterial, turning on two axes.

Default

The only example: the hook on an element 400px high. The cube sits 3 units in front of a 75° camera and never stops turning; there is no interaction, no lights and no texture to load, which is why the canvas is drawn on the first frame.

<!-- The element is the frame; the class appends a canvas that fills it -->
<div class="js--three-webgl" style="width: 100%; height: 400px;"></div>
Live example

Options reference

No attributes: the hook is the only thing the HTML carries. What can change is in the class.

Reference

There are no data-* attributes and the handler passes no options. The size comes from the element, and everything else is a value in ThreeGL.js:

In the classValueWhat it does
PerspectiveCamera(75, …, 0.1, 1000)fov 75, near 0.1, far 1000The field of view and the clipping planes of the camera
camera.position.z3How far the camera is from the cube; higher means a smaller cube
BoxGeometry(1, 1, 1)1×1×1 unitsThe shape. Any Three.js geometry works in its place
MeshNormalMaterialno optionsColours by face normal, needs no lights. Another material needs lights in the scene
rotation.x += 0.01, rotation.y += 0.01radians per frameThe turning speed on each axis, in animate
setPixelRatio(min(devicePixelRatio, 2))cap 2The sharpness on high-density screens, and the cost of each frame

The size of the canvas is the size of the element: set the width and height, or an aspect ratio, in SCSS on a component class of your own; js--three-webgl is a hook and carries no styles.