Skip to main content
Threebox supports two types of animations: path-based animations where objects follow routes, and embedded animations from GLB/GLTF models.

Path-Based Animations

Animate objects along geographic paths using followPath().

Basic Path Animation

map.on('style.load', function() {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function(map, mbxContext) {
      window.tb = new Threebox(
        map,
        mbxContext,
        { defaultLights: true }
      );

      // Load a truck model
      var options = {
        type: 'gltf',
        obj: 'models/vehicles/truck.glb',
        scale: 40,
        units: 'meters',
        anchor: 'bottom',
        rotation: { x: 90, y: 90, z: 0 }
      };

      tb.loadObj(options, function(model) {
        var truck = model.setCoords([-122.4340, 37.7353, 0]);
        tb.add(truck);

        // Define a path
        var path = [
          [-122.4340, 37.7353, 0],
          [-122.4335, 37.7358, 0],
          [-122.4330, 37.7360, 0],
          [-122.4325, 37.7355, 0]
        ];

        // Animate along path
        truck.followPath({
          path: path,
          duration: 10000  // 10 seconds
        });
      });
    },

    render: function(gl, matrix) {
      tb.update();
    }
  });
});

Animation Options

OptionTypeDefaultDescription
patharrayrequiredArray of [lng, lat, alt] coordinates
durationnumber1000Animation duration in milliseconds
animationnumber0Embedded animation index to play
callbackfunctionnullFunction called when animation completes

Interactive Path Animation

Create click-to-move functionality with the Mapbox Directions API:
var origin = [-122.4340, 37.7353];
var truck;

map.on('style.load', function() {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function(map, mbxContext) {
      window.tb = new Threebox(
        map,
        mbxContext,
        { defaultLights: true }
      );

      var options = {
        type: 'gltf',
        obj: 'models/vehicles/truck.glb',
        scale: 40,
        units: 'meters',
        anchor: 'bottom',
        rotation: { x: 90, y: 90, z: 0 }
      };

      tb.loadObj(options, function(model) {
        truck = model.setCoords(origin);
        truck.addEventListener('ObjectChanged', onObjectChanged, false);
        tb.add(truck);
      });
    },

    render: function(gl, matrix) {
      tb.update();
    }
  });
});

// Click to navigate
map.on('click', function(e) {
  var destination = [e.lngLat.lng, e.lngLat.lat];
  travelPath(destination);
});

function travelPath(destination) {
  // Request directions from Mapbox API
  var url = "https://api.mapbox.com/directions/v5/mapbox/driving/" +
    [origin, destination].join(';') +
    "?geometries=geojson&access_token=" + mapboxgl.accessToken;

  fetch(url)
    .then(response => response.json())
    .then(data => {
      // Extract path from API response
      var path = data.routes[0].geometry.coordinates;

      // Animate truck along path
      truck.followPath(
        {
          path: path,
          duration: 10000
        },
        function() {
          // Animation complete callback
          console.log('Arrived at destination');
          if (line) tb.remove(line);
        }
      );

      // Add visual line showing path
      var lineGeometry = path.map(coord => coord.concat([15]));
      line = tb.line({
        geometry: lineGeometry,
        width: 5,
        color: 'steelblue'
      });
      tb.add(line);

      // Update origin for next trip
      origin = destination;
    });
}

function onObjectChanged(e) {
  var model = e.detail.object;
  var action = e.detail.action;
  console.log('Object changed:', action);
}

Embedded Model Animations

Play animations that are embedded in GLB/GLTF files:
var options = {
  obj: 'models/soldier.glb',
  type: 'gltf',
  scale: 20,
  units: 'meters',
  rotation: { x: 90, y: 0, z: 0 },
  anchor: 'center'
};

tb.loadObj(options, function(model) {
  var soldier = model.setCoords([-122.4340, 37.7353, 0]);
  tb.add(soldier);

  // Play animation index 1 for 10 seconds
  soldier.playAnimation({
    animation: 1,
    duration: 10000
  });
});

Animation Controls

tb.loadObj(options, function(model) {
  model.setCoords(origin);
  tb.add(model);

  // Play animation
  model.playAnimation({ animation: 1, duration: 10000 });

  // Stop animation
  model.stop();

  // Check if playing
  if (model.isPlaying) {
    console.log('Animation is running');
  }

  // Listen to animation state changes
  model.addEventListener('IsPlayingChanged', function(e) {
    console.log('Is playing:', e.detail.isPlaying);
  }, false);
});

Combined Animations

Play both path-based and embedded animations simultaneously:
var soldier;

map.on('click', function(e) {
  var destination = [e.lngLat.lng, e.lngLat.lat];
  navigateSoldier(destination);
});

function navigateSoldier(destination) {
  var url = "https://api.mapbox.com/directions/v5/mapbox/walking/" +
    [origin, destination].join(';') +
    "?geometries=geojson&access_token=" + mapboxgl.accessToken;

  fetch(url)
    .then(response => response.json())
    .then(data => {
      var path = data.routes[0].geometry.coordinates;
      var duration = 10000;

      // Start path animation
      soldier.followPath(
        {
          path: path,
          duration: duration
        },
        function() {
          // Stop running animation when arrived
          soldier.stop();
          tb.remove(line);
        }
      );

      // Play running animation
      soldier.playAnimation({
        animation: 1,  // Running animation
        duration: duration
      });

      // Add path line
      var lineGeometry = path.map(coord => coord.concat([15]));
      line = tb.line({
        geometry: lineGeometry,
        width: 5,
        color: 'steelblue'
      });
      tb.add(line);

      origin = destination;
    });
}

Rotation Animations

Animate object rotation:
// Create a tube
var tube = tb.tube({
  geometry: spiralPath,
  radius: 0.8,
  sides: 8,
  color: '#00ffff',
  material: 'MeshPhysicalMaterial'
});
tube.setCoords(origin);
tb.add(tube);

// Animate rotation over 20 seconds
tube.set({
  rotation: { x: 0, y: 0, z: 11520 },  // Degrees
  duration: 20000
});

Animation Events

Listen to animation-related events:
tb.loadObj(options, function(model) {
  model.setCoords(origin);
  
  // Object changed (position, rotation, scale)
  model.addEventListener('ObjectChanged', function(e) {
    console.log('Action:', e.detail.action);
    console.log('Object:', e.detail.object);
  }, false);
  
  // Animation state changed
  model.addEventListener('IsPlayingChanged', function(e) {
    console.log('Is playing:', e.detail.isPlaying);
  }, false);
  
  tb.add(model);
});

Custom Animation Loop

Create custom animations using the render loop:
var angle = 0;
var sphere;

map.addLayer({
  id: 'custom_layer',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function(map, mbxContext) {
    window.tb = new Threebox(
      map,
      mbxContext,
      { defaultLights: true }
    );

    sphere = tb.sphere({
      radius: 30,
      color: 'red',
      material: 'MeshToonMaterial',
      units: 'meters'
    }).setCoords([-122.4340, 37.7353, 0]);
    
    tb.add(sphere);
  },

  render: function(gl, matrix) {
    // Custom animation in render loop
    angle += 0.01;
    
    // Orbit animation
    var radius = 50;
    var x = Math.cos(angle) * radius;
    var y = Math.sin(angle) * radius;
    
    // Update position
    sphere.setCoords([
      -122.4340 + x * 0.0001,
      37.7353 + y * 0.0001,
      Math.abs(Math.sin(angle)) * 50
    ]);
    
    tb.update();
  }
});

Animation with UI Controls

Add interactive animation controls:
<button id="playButton">Play</button>
<button id="pauseButton">Pause</button>
<button id="wireButton">Wireframe</button>

<script>
var selectedObject;

// Play/pause animation
document.getElementById('playButton').addEventListener('click', function() {
  if (selectedObject) {
    if (selectedObject.isPlaying) {
      selectedObject.stop();
    } else {
      selectedObject.playAnimation({
        animation: 1,
        duration: 10000
      });
    }
  }
});

// Toggle wireframe
document.getElementById('wireButton').addEventListener('click', function() {
  if (selectedObject) {
    selectedObject.wireframe = !selectedObject.wireframe;
  }
});

// Track selected object
model.addEventListener('SelectedChange', function(e) {
  if (e.detail.selected) {
    selectedObject = e.detail;
  } else {
    selectedObject = null;
  }
}, false);
</script>

Camera Following

Make the camera follow an animated object:
var truck;

truck.addEventListener('ObjectChanged', function(e) {
  var model = e.detail.object;
  var coords = model.coordinates;
  
  // Pan map to follow object
  map.panTo([coords[0], coords[1]], {
    duration: 100,
    easing: t => t
  });
}, false);

// Alternative: Smooth camera following
function followObject(object) {
  map.on('render', function() {
    if (object && object.coordinates) {
      var coords = object.coordinates;
      map.setCenter([coords[0], coords[1]]);
    }
  });
}

Fixed Zoom Animation

Animate with fixed visual scale:
var options = {
  obj: 'model.glb',
  type: 'gltf',
  scale: 20,
  units: 'meters',
  fixedZoom: 18  // Preserve size below this zoom
};

tb.loadObj(options, function(model) {
  model.setCoords(origin);
  
  // Object maintains visual size when zoom < 18
  model.addEventListener('ObjectChanged', function(e) {
    var currentZoom = map.getZoom();
    if (currentZoom < 18) {
      // Object scale is adjusted automatically
    }
  }, false);
  
  tb.add(model);
});

Performance Optimization

Limit Animation Updates

var lastUpdate = 0;
var updateInterval = 16; // ~60fps

render: function(gl, matrix) {
  var now = Date.now();
  if (now - lastUpdate > updateInterval) {
    tb.update();
    lastUpdate = now;
  }
}

Pause Animations When Not Visible

// Pause animations when layer is not visible
map.on('zoom', function() {
  var zoom = map.getZoom();
  var layer = map.getLayer('custom_layer');
  
  if (zoom < 14) {
    // Pause all animations
    objects.forEach(obj => {
      if (obj.isPlaying) obj.stop();
    });
  }
});

Complete Example: Animated Soldier

Here’s a complete example with click-to-move animation:
<!doctype html>
<head>
  <title>Animated Soldier</title>
  <link href="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.css" rel="stylesheet">
  <script src="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.js"></script>
  <script src="../dist/threebox.js"></script>
  <link href="../dist/threebox.css" rel="stylesheet" />
  <style>
    body, html { width: 100%; height: 100%; margin: 0; }
    #map { width: 100%; height: 100%; }
    #info {
      position: absolute;
      top: 10px;
      left: 10px;
      background: rgba(0,0,0,0.7);
      color: white;
      padding: 10px;
      border-radius: 5px;
    }
  </style>
</head>
<body>
  <div id='map'></div>
  <div id='info'>Click on the map to move the soldier</div>

  <script>
    mapboxgl.accessToken = 'YOUR_TOKEN';

    var origin = [-122.4340, 37.7353];
    var soldier;
    var line;

    var map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/outdoors-v11',
      center: origin,
      zoom: 18,
      pitch: 60,
      antialias: true
    });

    map.on('style.load', function() {
      map.addLayer({
        id: 'custom_layer',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function(map, mbxContext) {
          window.tb = new Threebox(
            map,
            mbxContext,
            {
              defaultLights: true,
              enableSelectingObjects: true,
              enableDraggingObjects: true,
              enableRotatingObjects: true,
              enableTooltips: true
            }
          );

          var options = {
            obj: 'models/soldier.glb',
            type: 'gltf',
            scale: 20,
            units: 'meters',
            rotation: { x: 90, y: 0, z: 0 },
            anchor: 'center'
          };

          tb.loadObj(options, function(model) {
            soldier = model.setCoords(origin);
            soldier.addEventListener('ObjectChanged', onObjectChanged, false);
            tb.add(soldier);
          });
        },

        render: function(gl, matrix) {
          tb.update();
        }
      });
    });

    map.on('click', function(e) {
      var destination = [e.lngLat.lng, e.lngLat.lat];
      travelPath(destination);
    });

    function travelPath(destination) {
      var url = "https://api.mapbox.com/directions/v5/mapbox/walking/" +
        [origin, destination].join(';') +
        "?geometries=geojson&access_token=" + mapboxgl.accessToken;

      fetch(url)
        .then(response => response.json())
        .then(data => {
          var duration = 10000;
          var path = data.routes[0].geometry.coordinates;

          // Animate along path
          soldier.followPath(
            { path: path, duration: duration },
            function() {
              soldier.stop();
              tb.remove(line);
            }
          );

          // Play running animation
          soldier.playAnimation({ animation: 1, duration: duration });

          // Show path
          var lineGeometry = path.map(coord => coord.concat([15]));
          line = tb.line({
            geometry: lineGeometry,
            width: 5,
            color: 'steelblue'
          });
          tb.add(line);

          origin = destination;
        });
    }

    function onObjectChanged(e) {
      console.log('Action:', e.detail.action);
    }
  </script>
</body>

Next Steps

Interactions

Add selection, dragging, and rotation

3D Models

Learn more about model loading

Examples

Browse all examples

API Reference

Animation API documentation

Build docs developers (and LLMs) love