Skip to example

Three.js motion values

An example of binding three Motion values to a Three.js torus knot mesh and ShaderMaterial uniform with threeEffect.

JavaScript

Source code

<div class="stage">
  <canvas aria-label="A Three.js torus knot controlled by Motion values"></canvas>
</div>

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

  const canvas = document.querySelector("canvas")

  function runMotionValues(mesh, material) {
    /**
     * One spring value drives the mesh position. Two derived values fan it
     * out to the rotation and a shader uniform, so everything moves together.
     */
    const x = springValue(0, { stiffness: 160, damping: 20 })
    const rotateY = transformValue(() => x.get() * 40)
    const progress = transformValue(() => (x.get() + 1.5) / 3)

    threeEffect(mesh, { x, rotateY })
    threeEffect(material.uniforms, { progress })

    canvas.addEventListener("pointermove", (event) => {
      const rect = canvas.getBoundingClientRect()
      x.set(((event.clientX - rect.left) / rect.width - 0.5) * 3)
    })
  }

  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(
    35,
    canvas.clientWidth / canvas.clientHeight,
    0.1,
    100
  )
  camera.position.z = 6

  const material = new THREE.ShaderMaterial({
    uniforms: { progress: { value: 0.5 } },
    vertexShader: `
      varying vec3 vNormal;

      void main() {
        vNormal = normalize(normalMatrix * normal);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      varying vec3 vNormal;
      uniform float progress;

      void main() {
        vec3 pink = vec3(1.0, 0.0, 0.53);
        vec3 cyan = vec3(0.05, 0.86, 0.97);
        float light = 0.55 + 0.45 * dot(vNormal, normalize(vec3(0.4, 0.8, 1.0)));
        vec3 color = mix(pink, cyan, progress) * light;
        gl_FragColor = vec4(color, 1.0);

        #include <colorspace_fragment>
      }
    `,
  })
  const mesh = new THREE.Mesh(
    new THREE.TorusKnotGeometry(0.8, 0.26, 160, 24),
    material
  )
  scene.add(mesh)

  runMotionValues(mesh, material)
  frame.render(() => renderer.render(scene, camera), true)
</script>

<style>
  .stage {
    width: 480px;
    max-width: 100vw;
  }

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