Three.js Animation Explained — Complete Guide to requestAnimationFrame, Clocks, Easing & Tweening
Three.js animation is what brings a Three.js scene to life. Without it, you have a static snapshot — a 3D screenshot. With it, you get moving cameras, spinning objects, pulsing lights, and living experiences.
What You’ll Learn
By the end of this tutorial, you’ll understand the requestAnimationFrame loop, use THREE.Clock for frame-rate-independent motion, apply easing functions for natural movement, integrate TWEEN.js and GSAP, animate along 3D paths, and build an interactive Animation Explorer with pause/play and speed controls.
Why Animation Matters
Animation transforms raw geometry into perceived life. A rotating logo, a pulsing button, a floating notification — these cues tell users the application is alive and responsive. In Durga Antivirus Pro, the 3D threat dashboard uses sine-based oscillation for threat indicators: low-severity threats pulse slowly (green), critical threats pulse rapidly (red). This real-time animation gives analysts instant threat level awareness without reading numbers.
flowchart LR
A[Animation Loop] --> B[requestAnimationFrame]
B --> C[THREE.Clock]
C --> D{Delta or Elapsed?}
D --> E[getDelta: Frame-rate independence]
D --> F[getElapsedTime: Sine/cosine input]
E --> G[Object Updates]
F --> G
G --> H[Easing Functions]
H --> I[Tweening Libraries]
I --> J[TWEEN.js]
I --> K[GSAP]
J --> L[Animation Explorer Project]
K --> L
style C fill:#44aa88,color:#fff,stroke:none
The Animation Loop — Why requestAnimationFrame?
Three.js doesn’t have a built-in “play” button. You drive animation manually:
function animate() {
requestAnimationFrame(animate); // Browser calls this before next paint (~60fps)
renderer.render(scene, camera);
}
animate();Why requestAnimationFrame and not setInterval?
rAFpauses when the tab is hidden — saving battery and CPUrAFsyncs with the monitor’s refresh rate — no unnecessary framesrAFwon’t queue overlapping calls — if a frame takes too long, the next one waits
Think of requestAnimationFrame as raising your hand and saying “call me when you’re ready to paint.” setInterval is like shouting “PAINT NOW!” every 16ms regardless of whether the browser is ready.
THREE.Clock & Delta Time — Frame-Rate Independence
The naive approach — adding a fixed value each frame — breaks on different devices:
// ❌ Bad: rotates at different speeds on 30fps vs 120fps
mesh.rotation.x += 0.01;
// ✅ Good: rotates at 1 radian per second on any device
var clock = new THREE.Clock();
function animate() {
var delta = clock.getDelta(); // Seconds since last frame (~0.016 at 60fps)
mesh.rotation.x += delta; // Speed is now frame-rate independent
renderer.render(scene, camera);
requestAnimationFrame(animate);
}Why delta time is critical: At 60fps, each frame is ~0.016s. At 30fps, each frame is ~0.033s. Without delta, the 30fps machine rotates the object half as much in the same real time. With delta, both machines move the object 1 unit per second.
var clock = new THREE.Clock();
function animate() {
var delta = clock.getDelta(); // Frame time (seconds)
var elapsed = clock.getElapsedTime(); // Total time since clock started
// Delta-based: smooth, frame-rate independent
mesh.rotation.y += delta * 2; // Rotate 2 radians per second
// Elapsed-based: perfect for loops (sine, cosine)
mesh.position.y = Math.sin(elapsed * 2) * 0.5; // Bob 0.5 units at 2Hz
}getDelta() once per frame at the top of the loop. Each call resets the internal timer. Calling it twice gives 0 on the second call.Delta Time vs Fixed Timestep
Delta time is simple but non-deterministic — a large delta after tab-switch can cause physics objects to tunnel through walls.
A fixed timestep runs logic at a constant interval:
var FIXED_DT = 1 / 60;
var accumulator = 0;
clock.start();
function animate() {
accumulator += clock.getDelta();
while (accumulator >= FIXED_DT) {
fixedUpdate(FIXED_DT); // Physics/state at exactly 60 Hz
accumulator -= FIXED_DT;
}
renderer.render(scene, camera);
requestAnimationFrame(animate);
}Rule of thumb: Use delta time for presentation (rotation, oscillation, camera movement). Use fixed timestep for simulation (physics, character controllers).
Animating Position, Rotation & Scale
// Rotation — additive, delta-based
mesh.rotation.x += delta * 2;
mesh.rotation.y += delta * 1.5;
// Position — absolute, elapsed-based (circular orbit)
var t = clock.getElapsedTime();
mesh.position.x = Math.cos(t * 0.8) * 3; // Radius = 3
mesh.position.z = Math.sin(t * 0.8) * 3;
// Scale — pulsing
var pulse = 0.8 + 0.2 * Math.sin(t * 2);
mesh.scale.set(pulse, pulse, pulse);Oscillation with Math.sin / Math.cos
Sine and cosine produce smooth, looping values between -1 and 1 — perfect for natural motion:
// Floating / bobbing
mesh.position.y = Math.sin(elapsed * 2) * 0.5; // Frequency=2, Amplitude=0.5
// Circular orbit
mesh.position.x = Math.cos(elapsed * 0.8) * radius;
mesh.position.z = Math.sin(elapsed * 0.8) * radius;
// Color pulsing
var r = 0.5 + 0.5 * Math.sin(elapsed * 2);
material.color.setRGB(r, 0.5, 0.5);
// Opacity blinking
material.opacity = 0.3 + 0.7 * Math.abs(Math.sin(elapsed * 2));| Pattern | Formula | Effect |
|---|---|---|
| Float | baseY + sin(t * freq) * amp | Gentle up/down bobbing |
| Orbit | cos(t) * r, sin(t) * r | Circular motion around center |
| Pulse | 0.8 + 0.2 * sin(t * 2) | Breathing scale effect |
| Blink | 0.3 + 0.7 * abs(sin(t * 2)) | On/off opacity blink |
Easing Functions — Making Motion Feel Natural
Linear motion feels mechanical. Easing controls the rate of change over time:
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function animate() {
var t = Math.min((clock.getElapsedTime() - startTime) / duration, 1);
var easedT = easeInOutCubic(t);
mesh.position.x = lerp(startX, endX, easedT); // lerp = linear interpolate
requestAnimationFrame(animate);
renderer.render(scene, camera);
}Easing families:
- easeIn — slow start, fast end (like a car accelerating)
- easeOut — fast start, slow end (like a ball rolling to a stop)
- easeInOut — slow start & end, fast middle (most natural)
TWEEN.js — Lightweight Tweening
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
<script>
new TWEEN.Tween(mesh.position)
.to({ x: 5, y: 3, z: 0 }, 2000) // 2-second tween
.easing(TWEEN.Easing.Quadratic.Out)
.delay(500)
.repeat(Infinity)
.yoyo(true)
.start();
function animate() {
TWEEN.update(); // Drive all tweens
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
</script>GSAP — Professional Animation
GSAP is more powerful and handles requestAnimationFrame internally:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script>
gsap.to(mesh.position, {
x: 5, y: 3,
duration: 2,
ease: "power2.out",
repeat: -1, yoyo: true
});
// GSAP runs rAF internally, but you still need your own render loop
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
</script>Common Mistakes
1. Not using delta time
Animation runs fast on 144Hz monitors and slow on 30Hz. Always multiply by delta for frame-rate independence.
2. Calling getDelta() multiple times per frame
Each call resets the internal oldTime. Call once at the top of the loop and store in a variable.
3. Modifying geometry.vertices directly (r125+)
In modern Three.js, vertex data lives in geometry.attributes.position.array, not geometry.vertices.
4. Mutating position without .copy()
mesh.position = otherMesh.position shares the reference. Use mesh.position.copy(otherMesh.position) instead.
5. Heavy work inside the animation loop
Expensive calculations cause frame drops. Defer to setTimeout or a Web Worker.
6. TWEEN/GSAP not updated
TWEEN.js needs TWEEN.update() each frame or tweens won’t progress. GSAP runs independently but needs the render loop to show changes.
7. Not clamping delta after tab switch
After switching back to a background tab, the first getDelta() might return seconds. Clamp it: Math.min(clock.getDelta(), 0.1).
Practice Questions
Q1: What is delta time and why does it matter? A: Delta time is the seconds elapsed between frames. Multiplying animation values by delta ensures consistent speed across different frame rates.
Q2: What’s the difference between getDelta() and getElapsedTime()?
A: getDelta() returns time since the last frame (for incremental updates). getElapsedTime() returns total seconds since the clock started (for sine/cosine inputs).
Q3: Why does the animation speed up when returning from a background tab?
A: requestAnimationFrame pauses when the tab is hidden. On return, the first getDelta() returns a large value (the hidden duration). Clamp with Math.min(delta, 0.1).
Q4: When would you use a fixed timestep instead of delta time? A: For physics simulations where deterministic behavior matters. A fixed timestep runs game logic at a constant rate regardless of frame rate.
Q5: How do you pause and resume animation? A: Use a boolean flag. In the animation loop, skip updates when paused but still render the scene.
Challenge: Create an animation where 5 different objects each use a different animation technique: one rotates (delta-based), one orbits (elapsed-based), one pulses (scale with sin), one fades (opacity with sin), and one follows a CatmullRomCurve3 path. Add a speed slider and pause button.
FAQ
Try It Yourself
Copy this complete HTML file. It demonstrates spinning cubes, orbiting spheres, pulsing colors, pause/play, speed control, and multiple animation modes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Three.js Animation Explorer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; overflow: hidden; background: #111; }
canvas { display: block; }
#ui {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
display: flex; gap: 12px; align-items: center; flex-wrap: wrap; justify-content: center;
background: rgba(0,0,0,0.7); backdrop-filter: blur(8px);
padding: 12px 20px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);
color: #fff; font-size: 14px; z-index: 10;
}
#ui button {
background: #3b82f6; color: #fff; border: none; padding: 6px 16px;
border-radius: 8px; cursor: pointer; font-size: 14px;
}
#ui button:hover { background: #2563eb; }
#ui input[type="range"] { width: 100px; accent-color: #3b82f6; }
#mode-label { min-width: 100px; text-align: center; font-weight: 600; color: #93c5fd; }
</style>
</head>
<body>
<div id="ui">
<button id="btn-toggle">⏸ Pause</button>
<button id="btn-mode">Switch Mode</button>
<label>Speed <input type="range" id="speed-slider" min="0" max="3" step="0.1" value="1" /></label>
<span id="mode-label">Spin</span>
</div>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(6, 4, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.prepend(renderer.domElement);
new OrbitControls(camera, renderer.domElement);
const ambient = new THREE.AmbientLight(0x404060);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 2);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
const objects = [];
const geo1 = new THREE.BoxGeometry(1, 1, 1);
const mat1 = new THREE.MeshStandardMaterial({ color: 0x3b82f6, roughness: 0.3, metalness: 0.6 });
const cube = new THREE.Mesh(geo1, mat1);
cube.position.set(-2, 0, 0);
scene.add(cube);
objects.push(cube);
const geo2 = new THREE.SphereGeometry(0.6, 32, 32);
const mat2 = new THREE.MeshStandardMaterial({ color: 0xf59e0b, roughness: 0.2, metalness: 0.8 });
const sphere = new THREE.Mesh(geo2, mat2);
sphere.position.set(2, 0, 0);
scene.add(sphere);
objects.push(sphere);
const clock = new THREE.Clock();
let isPaused = false;
let speed = 1;
let mode = 0;
const modes = ['Spin', 'Orbit', 'Wave', 'All'];
const basePositions = objects.map(o => o.position.clone());
document.getElementById('btn-toggle').addEventListener('click', () => {
isPaused = !isPaused;
document.getElementById('btn-toggle').textContent = isPaused ? '▶ Play' : '⏸ Pause';
});
document.getElementById('btn-mode').addEventListener('click', () => {
mode = (mode + 1) % modes.length;
document.getElementById('mode-label').textContent = modes[mode];
});
document.getElementById('speed-slider').addEventListener('input', () => {
speed = parseFloat(document.getElementById('speed-slider').value);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
function animate() {
requestAnimationFrame(animate);
const rawDelta = clock.getDelta();
const dt = isPaused ? 0 : rawDelta * speed;
const t = clock.getElapsedTime() * speed;
if (mode === 0 || mode === 3) {
cube.rotation.x += dt * 1.2;
cube.rotation.y += dt * 0.8;
}
if (mode === 1 || mode === 3) {
sphere.position.x = basePositions[1].x + Math.cos(t * 0.8) * 2.5;
sphere.position.z = basePositions[1].z + Math.sin(t * 0.8) * 2.5;
}
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>What’s Next
| Topic | Description | Link |
|---|---|---|
| Particles | Animated particle systems for effects | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-particles/ |
| Cameras & Controls | Animating camera transitions | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-cameras-controls/ |
| Models & Loading | Playing imported model animations | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-models-loading/ |
| Post-Processing | Animated post-processing effects | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-post-processing/ |
| Three.js Basics | Scene setup fundamentals | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-basics/ |
| GSAP | Professional animation library | GSAP |
| JavaScript | Language fundamentals | JavaScript |
You now understand Three.js animation from the ground up — from the requestAnimationFrame loop to delta-time correction, easing, and tweening libraries. The same sine-based oscillation technique is used in Durga Antivirus Pro to create threat-level-aware pulsing indicators that help analysts identify critical security events at a glance.
What’s Next
Congratulations on completing this Threejs Animation tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro