Skip to example

Three.js sequences and stagger

An example of sequencing Three.js meshes alongside DOM elements and staggering an array of meshes with animate() and threeEffect in Motion.

JavaScript

Source code

<div class="stage">
  <h2 class="title">Sequenced with the DOM</h2>
  <canvas></canvas>
</div>

<script type="module">
  import { animate, frame, stagger } from "motion"
  import { threeEffect } from "motion/three"
  import * as THREE from "three"

  animate.addEffect(threeEffect)

  const token = (name) =>
    getComputedStyle(document.body).getPropertyValue(name).trim()

  const canvas = document.querySelector("canvas")

  function runSequenceMotion(cubes) {
    /**
     * Because threeEffect makes meshes first-class animate() subjects, they
     * can sit in the same sequence as DOM elements, and an array of meshes
     * can be staggered like an array of elements.
     */
    animate([
      [".title", { opacity: [0, 1], y: [16, 0] }, { duration: 0.5 }],
      [
        cubes,
        { y: [-2.5, 0], rotateY: [0, 360], scale: [0.6, 1] },
        {
          at: "-0.2",
          delay: stagger(0.08),
          type: "spring",
          stiffness: 180,
          damping: 20,
          mass: 1,
        },
      ],
    ])
  }

  const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
    alpha: true,
  })
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
  renderer.setSize(canvas.clientWidth, canvas.clientHeight, false)

  const scene = new THREE.Scene()
  const camera = new THREE.PerspectiveCamera(
    30,
    canvas.clientWidth / canvas.clientHeight,
    0.1,
    100
  )
  camera.position.set(0, 1.2, 7)
  camera.lookAt(0, 0, 0)

  scene.add(new THREE.HemisphereLight(token("--white"), token("--layer"), 2))
  const keyLight = new THREE.DirectionalLight(token("--white"), 3)
  keyLight.position.set(3, 4, 5)
  scene.add(keyLight)

  /**
   * A row of cubes, each with its own hue from the page's CSS tokens
   */
  const cubes = [1, 2, 3, 4, 5].map((hue, i) => {
    const cube = new THREE.Mesh(
      new THREE.BoxGeometry(0.8, 0.8, 0.8),
      new THREE.MeshStandardMaterial({
        color: token(`--hue-${hue}`),
        roughness: 0.5,
      })
    )
    cube.position.x = (i - 2) * 1.1
    scene.add(cube)
    return cube
  })

  frame.render(() => renderer.render(scene, camera), true)
  runSequenceMotion(cubes)
</script>

<style>
  .stage {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    width: 480px;
    max-width: 100vw;
  }

  .title {
    margin: 0;
    font-size: 28px;
    letter-spacing: -0.02em;
    text-wrap: balance;
    opacity: 0;
  }

  .stage canvas {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 2;
  }
</style>