Playing with Animations¶
Welcome back! 
In this tutorial we'll bring your 3D models to life by controlling animations with the API.
We'll add playback controls and even a scrubber to step through frames manually — like a tiny timeline editor.
This tutorial builds off the Basic implementation tutorial.
Let's go!
1. Set Up the Viewer¶
We'll start with a basic viewer that loads an animated model. Add a few buttons and a range slider underneath for our controls:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animation Controls</title>
<style>
body {
font-family: sans-serif;
}
#viewer-container {
width: 500px;
height: 500px;
}
.controls {
display: flex;
gap: .75rem;
align-items: center;
margin-top: 1rem;
}
.controls button {
padding: .4rem .8rem;
}
.scrubber {
margin-top: .75rem;
}
.scrubber input[type="range"] {
width: 300px;
}
</style>
</head>
<body>
<main>
<h1>Animation Controls</h1>
<div id="viewer-container">
<div class="js-270viewer"
data-270-model="270logo"
data-270-background="#eeeeee">
</div>
</div>
<div class="controls">
<button id="play">▶ Play</button>
<button id="pause">⏸ Pause</button>
<button id="stop">⏹ Stop</button>
</div>
<div class="scrubber">
<label>Frame <input type="range" id="frame-slider" min="0" value="0" /></label>
<span id="frame-display">0</span>
</div>
</main>
<script>window.TSDAPIKEY = 'rest-of-the-owl';</script>
<script src="https://api.270degrees.nl/api/script/latest/viewer.js"></script>
<script>
// We'll use this in the next step
</script>
</body>
</html>
Nothing too fancy — just a viewer with some buttons and a slider below it.
2. Wait for the Model to Load¶
Before we can play animations, the model needs to be fully loaded — otherwise methods like getAnimationNames() won't have anything to return yet. We'll use onLoadComplete via TSDViewer.create() to make sure everything is ready before we wire up our controls:
<script>
const $viewer = document.querySelector('.js-270viewer');
TSDViewer.create($viewer, {
onLoadComplete: () => {
// The model is ready — all methods are now available.
// We'll add our controls here.
}
});
</script>
Everything from here on goes inside that onLoadComplete callback.
3. Wire Up Playback¶
Now let's bring the buttons to life. We'll use startAnimation, pauseAnimation and stopAnimation to control playback:
document.querySelector('#play').addEventListener('click', () => {
$viewer.startAnimation({ loop: true });
});
document.querySelector('#pause').addEventListener('click', () => {
$viewer.pauseAnimation();
});
document.querySelector('#stop').addEventListener('click', () => {
$viewer.stopAnimation();
});
Click Play and watch the model animate! Pause freezes it in place, and Stop rewinds back to the beginning.
Passing loop: true keeps the animation looping. You can also pass reverse: true to play it backwards.
4. Add Frame Scrubbing¶
This is where it gets fun. We'll use getAnimationFrames to find out how many frames the animation has, and setAnimationFrame to scrub to a specific frame with the slider.
const $slider = document.querySelector('#frame-slider');
const $display = document.querySelector('#frame-display');
// Set up the scrubber
const animationName = $viewer.getAnimationNames()[0];
const totalFrames = $viewer.getAnimationFrames(animationName);
$slider.max = totalFrames;
$slider.addEventListener('input', (ev) => {
const frame = parseInt(ev.target.value);
$viewer.setAnimationFrame({ name: animationName, frame: frame });
$display.textContent = frame;
});
Also update the Stop button to reset the slider when the animation rewinds:
document.querySelector('#stop').addEventListener('click', () => {
$viewer.stopAnimation();
$slider.value = 0;
$display.textContent = '0';
});
Drag the slider to scrub through the animation frame by frame. The frame counter next to it updates in real time.
If your model has multiple animations, use getAnimationNames() to list them all, then pass a specific name to target one.
5. End Result¶
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animation Controls</title>
<style>
body {
font-family: sans-serif;
}
#viewer-container {
width: 500px;
height: 500px;
}
.controls {
display: flex;
gap: .75rem;
align-items: center;
margin-top: 1rem;
}
.controls button {
padding: .4rem .8rem;
}
.scrubber {
margin-top: .75rem;
}
.scrubber input[type="range"] {
width: 300px;
}
</style>
</head>
<body>
<main>
<h1>Animation Controls</h1>
<div id="viewer-container">
<div class="js-270viewer"
data-270-model="270logo"
data-270-background="#eeeeee">
</div>
</div>
<div class="controls">
<button id="play">▶ Play</button>
<button id="pause">⏸ Pause</button>
<button id="stop">⏹ Stop</button>
</div>
<div class="scrubber">
<label>Frame <input type="range" id="frame-slider" min="0" value="0" /></label>
<span id="frame-display">0</span>
</div>
</main>
<script>window.TSDAPIKEY = 'rest-of-the-owl';</script>
<script src="https://api.270degrees.nl/api/script/latest/viewer.js"></script>
<script>
const $viewer = document.querySelector('.js-270viewer');
TSDViewer.create($viewer, {
onLoadComplete: () => {
const $slider = document.querySelector('#frame-slider');
const $display = document.querySelector('#frame-display');
document.querySelector('#play').addEventListener('click', () => {
$viewer.startAnimation({ loop: true });
});
document.querySelector('#pause').addEventListener('click', () => {
$viewer.pauseAnimation();
});
document.querySelector('#stop').addEventListener('click', () => {
$viewer.stopAnimation();
$slider.value = 0;
$display.textContent = '0';
});
const animationName = $viewer.getAnimationNames()[0];
const totalFrames = $viewer.getAnimationFrames(animationName);
$slider.max = totalFrames;
$slider.addEventListener('input', (ev) => {
const frame = parseInt(ev.target.value);
$viewer.setAnimationFrame({ name: animationName, frame: frame });
$display.textContent = frame;
});
}
});
</script>
</body>
</html>
What's Next?¶
You've got full playback and frame-level control over animations now!
Here are some ideas to keep going:
- Combine animations with camera positions for cinematic sequences
- Check out the Photo Studio tutorial to capture perfect product shots
- Browse all available methods for more possibilities