This sample demonstrates how to animate a 3D object along a path in an Scene and synchronize the camera with its movement. To bring this visualization to life, we use the 3D paraglider model of Aaron Durogati (ITA), winner of the Red Bull X-Alps 2025.
The segmented control at the top of the view allows you to switch between different camera perspectives relative to the model, including a "Free view" mode for manual navigation. The time slider at the bottom lets you scrub through the animation or adjust the playback speed. The play/pause button toggles an is
state, which controls the animation loop, and updates its icon to provide visual feedback.
function toggleAnimation() {
if (isAnimating) {
stopAnimation();
} else {
startAnimation();
}
}
function startAnimation() {
if (isAnimating) return;
isAnimating = true;
playAnimationButton.icon = "pause-f";
previousTimestamp = null; // Reset timestamp for the first frame
requestAnimationFrame(animationLoop);
}
function stopAnimation() {
isAnimating = false;
playAnimationButton.icon = "play-f";
}
The animation
function drives the animation. It uses request
to execute on every frame as long as the is
state is true
. Inside the loop, it calculates the time elapsed since the last frame (delta
) to ensure smooth playback regardless of the frame rate. It updates the time slider's value, which in turn calls update
to position the model and camera. The point layer (the small spheres) updates its visibility automatically because its timeInfo property is configured, showing only the points up to the current Scene time.
function animationLoop(timestamp) {
if (!isAnimating) return;
if (previousTimestamp === null) {
previousTimestamp = timestamp; // Initialize on the first frame
}
const deltaTime = timestamp - previousTimestamp;
previousTimestamp = timestamp;
let currentTime = timeSlider.value + (deltaTime * playRateControl.value);
if (currentTime >= timeSlider.max) {
currentTime = timeSlider.min; // Loop animation
}
timeSlider.value = currentTime;
updateScene(currentTime);
requestAnimationFrame(animationLoop);
}
The update
function is the core of the visualization. For performance, all path coordinates are pre-fetched from the layer into a features
array, which is sorted by timestamp. To find the model's current position, we locate the segment of the path corresponding to the current animation time. Once the two data points bracketing the current time are found, we use linear interpolation (lerp
) to calculate the paraglider's exact position and heading between those points. This ensures the model moves smoothly along its path, even when the data points are unevenly spaced in time.
while (index <= features.length - 1 && time > features[index + 1].attributes.timedate) {
index++;
}
// Get surrounding points for interpolation
// p0 - previous point, p1 - current point, p2 - next point
const p0 = features[index > 0 ? index - 1 : index].geometry;
const p1 = features[index].geometry;
const p2 = features[index < features.length - 1 ? index + 1 : index].geometry;
const time1 = features[index].attributes.timedate;
const time2 = features[index < features.length - 1 ? index + 1 : index].attributes.timedate;
// Calculate interpolation factor (t)
const t = time2 - time1 === 0 ? 0 : (time - time1) / (time2 - time1);
// Interpolate position and heading
const point = interpolatePoint(p1, p2, t);
const heading = calculateHeading(p0, p1, p2, t);
Finally, the update
and update
functions apply the calculated values. The paraglider mesh is moved to the newly interpolated point and rotated based on the calculated heading. If a camera follow-mode is active, a new Camera object is set on the view for each frame. Using trigonometry, it positions the camera at a fixed offset from the paraglider and adjusts the heading
and tilt
to keep the model centered in the view.
function updateMeshPosition(point, heading) {
const paragliderMesh = animationTarget.geometry;
paragliderMesh.centerAt(point);
paragliderMesh.transform = initialTransform?.clone();
paragliderMesh.rotate(0, 0, -heading);
}
function updateCameraPosition(point, heading) {
const cameraDirection = cameraModeControl.value === "back" ? -1 : 1;
const paragliderOrigin = webMercatorUtils.geographicToWebMercator(point);
viewElement.camera = new Camera({
position: new Point({
spatialReference: paragliderOrigin.spatialReference,
x: paragliderOrigin.x - cameraDirection * 40 * Math.sin(heading * Math.PI / 180),
y: paragliderOrigin.y - cameraDirection * 40 * Math.cos(heading * Math.PI / 180),
z: paragliderOrigin.z + 10, // Height offset from the model
}),
heading: heading,
tilt: cameraDirection * 80, // Camera tilt angle
fov: 105, // Field of view
});
}