A KeyframeTrack animates one property: its name is a binding path ('.position', '.rotation[x]', '.material.opacity', 'Spine.quaternion' for a named child), followed by a Float32Array of times in seconds and one of values. Subclasses fix the value type: NumberKeyframeTrack, VectorKeyframeTrack, QuaternionKeyframeTrack (slerped, the right choice for rotations), ColorKeyframeTrack, BooleanKeyframeTrack and StringKeyframeTrack. An AnimationClip groups tracks under a name. Here is a clip that pulls a book off the shelf and tips it toward the reader, sampled in Node:
import * as THREE from 'three';
const book = new THREE.Object3D(); // stands in for a book mesh
const pull = new THREE.VectorKeyframeTrack('.position', [0, 0.6, 1.2], // times in seconds
[0, 0, 0, 0, 0.05, 0.25, 0, 0.12, 0.35]); // x, y, z per key
const tilt = new THREE.NumberKeyframeTrack('.rotation[x]', [0, 1.2], [0, -0.3]);
const clip = new THREE.AnimationClip('pull-out', -1, [pull, tilt]); // -1: duration from keys
console.log(clip.name, clip.duration, clip.tracks.map((track) => track.name), clip.validate());
const mixer = new THREE.AnimationMixer(book);
mixer.clipAction(clip).setLoop(THREE.LoopOnce).play();
for (const time of [0, 0.3, 0.9, 1.2]) {
mixer.setTime(time);
const p = book.position.toArray().map((c) => +c.toFixed(3));
console.log(time, p, book.rotation.x.toFixed(3));
}pull-out 1.2000000476837158 [ '.position', '.rotation[x]' ] true 0 [ 0, 0, 0 ] 0.000 0.3 [ 0, 0.025, 0.125 ] -0.075 0.9 [ 0, 0.085, 0.3 ] -0.225 1.2 [ 0, 0.12, 0.35 ] -0.300
Values between keys are interpolated linearly (InterpolateDiscrete steps, InterpolateSmooth eases). The duration shows 1.2000000476837158 because times are 32-bit floats. AnimationClip.findByName(clips, 'Walk') picks a clip out of a glTF file's list.
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 12px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
pre { margin: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<pre id="out"></pre>
<script type="module">
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 260);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 260, 0.1, 50);
camera.position.set(2.6, 1.0, 2.4);
camera.lookAt(0, 0.05, 0.15);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.2));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);
sun.position.set(2, 3, 4);
scene.add(sun);
const plank = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.04, 0.4), new THREE.MeshStandardMaterial({ color: '#8d6e63' }));
plank.position.y = -0.47;
scene.add(plank);
// Tracks: a binding path, times in seconds, and a flat array of values
const pull = new THREE.VectorKeyframeTrack('.position', [0, 0.6, 1.2],
[0, 0, 0, 0, 0.05, 0.25, 0, 0.12, 0.35]); // x, y, z per key
const tilt = new THREE.NumberKeyframeTrack('.rotation[x]', [0, 1.2], [0, -0.3]);
const clip = new THREE.AnimationClip('pull-out', -1, [pull, tilt]); // -1: duration from the keys
const makeBook = (opacity) => new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.9, 0.62),
new THREE.MeshStandardMaterial({ color: '#e09a10', transparent: opacity < 1, opacity }));
// Ghosts: one mixer per copy, jumped to a fixed time with setTime()
for (const time of [0, 0.3, 0.6, 0.9, 1.2]) {
const ghost = makeBook(0.18);
const mixer = new THREE.AnimationMixer(ghost);
mixer.clipAction(clip).setLoop(THREE.LoopOnce).play();
mixer.setTime(time);
scene.add(ghost);
}
// The live book plays the clip back and forth
const book = makeBook(1);
scene.add(book);
const mixer = new THREE.AnimationMixer(book);
mixer.clipAction(clip).setLoop(THREE.LoopPingPong, Infinity).play();
document.getElementById('out').textContent =
`${clip.name}: duration ${clip.duration} s (32-bit float times), tracks ${clip.tracks.map((t) => t.name).join(', ')}, ` +
`validate() ${clip.validate()}`;
const timer = new THREE.Timer();
renderer.setAnimationLoop(() => {
timer.update();
mixer.update(timer.getDelta());
renderer.render(scene, camera);
});
</script>