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>