---
title: "Animate routes with a dynamic camera | 3D JS | JavaScript maps SDK"
description: "Animate multidimensional routes with a dynamic camera that follows the journey in real-time."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# Animate routes with a dynamic camera

Animate multidimensional routes with a dynamic camera that follows the journey in real-time. Whether it’s a high-altitude flight or a ground-level drive, provide clear spatial and temporal context by syncing 3D transport models with precise path data.

This demonstration showcases how to animate multidimensional routes in a map.

[Model credit](#fn:1)

### NPM module setup

```bash
npm install --save @maptiler/sdk @maptiler/3d @turf/turf
```

#### main.js

```js
import { Map, MapStyle, config, AnimatedRouteLayer, MaptilerAnimation, LngLat, Marker } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import { Layer3D, AltitudeReference } from '@maptiler/3d';
import * as turf from '@turf/turf';

const animationDuration = 5000; // 20 seconds
const atAirportZoom = 7;
const midAirZoom = 4;

const LISBOA = [-9.166407980002864, 38.70110735141954];
const MADRID = [-3.7001908499172367, 40.41629525891401];
const RIO_DE_JANEIRO = [-43.2096, -22.9133];

config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';

const KEYFRAMES = [
  {
    delta: 0,
    easing: "QuadraticInOut",
    props: {
      lng: MADRID[0],
      lat: MADRID[1],
    },
    userData: {
      leg: 1,
    },
  },
  {
    delta: 1,
    easing: "QuadraticInOut",
    props: {
      lng: RIO_DE_JANEIRO[0],
      lat: RIO_DE_JANEIRO[1],
    },
    userData: {
      leg: 3,
    },
  },
];

const map = new Map({
  container: 'map',
  style: MapStyle.HYBRID,
  center: MADRID,
  maxPitch: 85,
  halo: true,
  space: {
    color: "#000011",
  },
  terrainControl: true,
  maptilerLogo: true,
  projectionControl: true,
  zoom: 7,
  pitch: 60,
  projection: "globe"
});

// Waiting for the map to be ready
map.on('ready', async () => {

  // Add 3D layer for the plane
  //#region Plane 3D Model
  const layer3D = new Layer3D("3d-layer");

  map.addLayer(layer3D);

  const plane = await layer3D.addMeshFromURL("plane", "https://docs-media.maptiler.com/docs/models/plane_a340.glb", {
    lngLat: MADRID,
    altitude: 0,
    scale: 1,
    altitudeReference: AltitudeReference.GROUND,
    transform: {
      rotation: {
        y: Math.PI / 2,
      },
    },
  });

  plane.setHeading(
    turf.bearing([MADRID[0], MADRID[1]], [RIO_DE_JANEIRO[0], RIO_DE_JANEIRO[1]])
  );

  //#region Boat 3D Model
  const boat = await layer3D.addMeshFromURL("boat", "https://docs-media.maptiler.com/docs/models/boat/scene.gltf", {
    lngLat: RIO_DE_JANEIRO,
    altitude: 0,
    scale: 1,
    altitudeReference: AltitudeReference.MEAN_SEA_LEVEL,
    transform: {
      rotation: {
        y: Math.PI / 2,
      },
    },
  });

  boat.setHeading(
    turf.bearing([MADRID[0], MADRID[1]], [RIO_DE_JANEIRO[0], RIO_DE_JANEIRO[1]])
  );

  //#region Car 3D Model
  const car = await layer3D.addMeshFromURL("car", "https://docs-media.maptiler.com/docs/models/car/scene.gltf", {
    lngLat: LISBOA,
    altitude: 0,
    altitudeReference: AltitudeReference.GROUND,
    scale: 1000,
  });

  car.setHeading(turf.bearing([LISBOA[0], LISBOA[1]], [MADRID[0], MADRID[1]]));
  // Add markers for the airports
  //#region Markers
  const lisboaMarker = new Marker({
    element: document.getElementById("lisboa-marker"),
    subpixelPositioning: true,
  });

  lisboaMarker.setLngLat(LISBOA);
  lisboaMarker.addTo(map);

  const rioMarker = new Marker({
    element: document.getElementById("rio-marker"),
    subpixelPositioning: true,
  });

  rioMarker.setLngLat(RIO_DE_JANEIRO);
  rioMarker.addTo(map);

  const madridMarker = new Marker({
    element: document.getElementById("madrid-marker"),
    subpixelPositioning: true,
  });

  madridMarker.setLngLat(MADRID);
  madridMarker.addTo(map);

  //#region Plane Animation
  const planeAnimation = new MaptilerAnimation({
    iterations: 1,
    keyframes: KEYFRAMES,
    duration: animationDuration,
  });

  //#region fetch data and add layers
  const boatJourneyData = await fetchGeoJSONAndAddAsLayer(
    "boat-route",
    "../data/boat.geojson",
    map
  );
  const carJourneyData = await fetchGeoJSONAndAddAsLayer(
    "car-route",
    "../data/car.geojson",
    map
  );

  //#region Boat and Car AnimatedRouteLayerSetup
  const boatAnimatedRouteLayer = new AnimatedRouteLayer({
    source: {
      id: "boat-route",
      layerID: "boat-route-layer",
    },
    pathStrokeAnimation: {
      activeColor: [0, 255, 255, 0.5],
      inactiveColor: [0, 0, 0, 0],
    },
    cameraAnimation: {
      pathSmoothing: {
        resolution: 1,
        epsilon: 3,
      },
    },
    duration: animationDuration * 4,
    iterations: 1,
    delay: 1000,
  });

  const carAnimatedRouteLayer = new AnimatedRouteLayer({
    source: {
      id: "car-route",
      layerID: "car-route-layer",
    },
    pathStrokeAnimation: {
      activeColor: [0, 255, 255, 0.5],
      inactiveColor: [0, 0, 0, 0],
    },
    cameraAnimation: {
      pathSmoothing: {
        resolution: 0.1,
        epsilon: 5,
      },
    },
    iterations: 1,
    duration: animationDuration * 2,
    delay: 1000,
  });

  let currentAnimation =
    planeAnimation;
  //#region "chaining" of animations
  planeAnimation.addEventListener("animationend", (e) => {
    planeAnimation.pause();
    map.addLayer(boatAnimatedRouteLayer);
    boatAnimatedRouteLayer.play();
    currentAnimation = boatAnimatedRouteLayer;
  });

  boatAnimatedRouteLayer.addEventListener("animationend", (e) => {
    map.removeLayer(boatAnimatedRouteLayer.id);
    boatAnimatedRouteLayer.pause();
    boatAnimatedRouteLayer.animationInstance?.reset();
    map.addLayer(carAnimatedRouteLayer);
    carAnimatedRouteLayer.play();
    currentAnimation = carAnimatedRouteLayer;
  });

  carAnimatedRouteLayer.addEventListener("animationend", (e) => {
    carAnimatedRouteLayer.pause();
    carAnimatedRouteLayer.animationInstance?.reset();
    map.removeLayer(carAnimatedRouteLayer.id);
    planeAnimation.play();
    currentAnimation = planeAnimation;
  });

  carAnimatedRouteLayer.addEventListener("timeupdate", frameCallback);
  boatAnimatedRouteLayer.addEventListener("timeupdate", frameCallback);
  planeAnimation.addEventListener("timeupdate", frameCallback);

  const carJourneyLength = turf.length(carJourneyData, { units: "kilometers" });
  const boatJourneyLength = turf.length(boatJourneyData, {
    units: "kilometers",
  });

  //#region Frame
  function frameCallback(e) {
    const isBoatAnimation =
      e.target === boatAnimatedRouteLayer.animationInstance;
    const isCarAnimation = e.target === carAnimatedRouteLayer.animationInstance;
    const isPlaneAnimation = e.target === planeAnimation;

    map.setCenter(new LngLat(e.props.lng, e.props.lat));

    const previousLngLat = [
      e.previousProps.lng,
      e.previousProps.lat,
    ];
    const nextLngLat = [e.props.lng, e.props.lat];

    const heading = turf.bearing(previousLngLat, nextLngLat);

    if (isPlaneAnimation) {
      const previousStop = [
        e.keyframe?.props.lng,
        e.keyframe?.props.lat,
      ];
      const nextStop = [
        e.nextKeyframe?.props.lng,
        e.nextKeyframe?.props.lat,
      ];

      const legDistance = turf.rhumbDistance(previousStop, nextStop);

      const currentPosition = [e.props.lng, e.props.lat];

      const percentLeftToNextStop =
        turf.rhumbDistance(currentPosition, nextStop) / legDistance;

      const scale = Math.sin(percentLeftToNextStop * Math.PI) * 500;

      const zoomAlpha = Math.sin(percentLeftToNextStop * Math.PI);
      const zoom = lerp(atAirportZoom, midAirZoom, zoomAlpha);

      map.setZoom(zoom || map.getZoom());

      map.setBearing((1 - e.currentDelta) * 360);

      plane.modify({
        heading,
        lngLat: currentPosition,
        scale,
        altitude: 1000 * scale,
      });
    }

    if (isBoatAnimation) {
      map.setCenter(new LngLat(e.props.lng, e.props.lat));
      const boatPosition = turf.along(
        boatJourneyData.features[0],
        boatJourneyLength * e.currentDelta,
        { units: "kilometers" }
      );
      const nextBoatPosition = turf.along(
        boatJourneyData.features[0],
        boatJourneyLength * (e.currentDelta + 0.005),
        { units: "kilometers" }
      );

      const boatHeading =
        turf.bearing(
          [
            boatPosition.geometry.coordinates[0],
            boatPosition.geometry.coordinates[1],
          ],
          [
            nextBoatPosition.geometry.coordinates[0],
            nextBoatPosition.geometry.coordinates[1],
          ]
        ) + 180;

      const scale = Math.sin(e.currentDelta * Math.PI) * 1000;

      boat.modify({
        heading: boatHeading,
        lngLat: [
          boatPosition.geometry.coordinates[0],
          boatPosition.geometry.coordinates[1],
        ],
        scale,
      });
    }

    if (isCarAnimation) {
      map.setCenter(new LngLat(e.props.lng, e.props.lat));

      const carPosition = turf.along(
        carJourneyData.features[0].geometry,
        carJourneyLength * e.currentDelta,
        { units: "kilometers" }
      );
      const nextCarPosition = turf.along(
        carJourneyData.features[0].geometry,
        carJourneyLength * (e.currentDelta + 0.01),
        { units: "kilometers" }
      );

      const carHeading =
        turf.bearing(
          [
            carPosition.geometry.coordinates[0],
            carPosition.geometry.coordinates[1],
          ],
          [
            nextCarPosition.geometry.coordinates[0],
            nextCarPosition.geometry.coordinates[1],
          ]
        ) + 180;

      const scale = Math.sin(e.currentDelta * Math.PI) * 1000;

      car.modify({
        heading: carHeading, // because the car model is 180
        lngLat: [
          carPosition.geometry.coordinates[0],
          carPosition.geometry.coordinates[1],
        ],
        scale,
      });
    }
  }

  let isPlaying = false;
  document.getElementById("play-button")?.addEventListener("click", () => {
    if (isPlaying) {
      currentAnimation?.pause();
      isPlaying = false;
    } else {
      currentAnimation?.play();
      isPlaying = true;
    }
  });

});

//#region Helper Functions / types
function lerp(a, b, t) {
  return a + (b - a) * t;
}

async function fetchGeoJSONAndAddAsLayer(id, url, map) {
  try {
    const data = await fetch(url).then((response) => response.json());
    addSourceAndLayer(map, id, {
      type: "geojson",
      data,
      lineMetrics: true,
    });
    return data;
  } catch (error) {
    throw error;
  }
}

function addSourceAndLayer(
  map,
  id,
  source
) {
  map.addSource(id, source);
  map.addLayer(
    {
      source: id,
      id: `${id}-layer`,
      type: "line",
      layout: {
        visibility: "visible",
        "line-sort-key": 1,
      },
      paint: {
        "line-color": "transparent",
        "line-width": 10,
      },
    },
    "3d-layer"
  );
}
```

#### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="robots" content="noindex">
  <title>MapTiler 3D | Globe Travel Example</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="map"></div>
  <div id="madrid-marker" class="marker"></div>
  <div id="rio-marker" class="marker"></div>
  <div id="lisboa-marker" class="marker"></div>
  <button id="play-button">Go!</button>
  <script type="module" src="./main.js"></script>
</body>
</html>
```

#### style.css

```css
body {
  margin: 0;
  padding: 0;
}

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

@keyframes fade {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

.marker {
  background-size: cover;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: 0.5rem solid black;
  cursor: pointer;
  position: absolute;
  top: -100px;
  left: -100px;
  z-index: 1000;
  animation: fade 0.5s ease-in-out reverse;
}

#play-button {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1000;
}

#madrid-marker::after,
#rio-marker::after,
#lisboa-marker::after {
  position: absolute;
  top: calc(100% - 0.5rem);
  left: 50%;
  transform: translateX(-50%);
  font-size: 1rem;
  font-weight: bold;
  color: white;
  text-shadow: 2px 2px 2px black, -2px -2px 2px black, 2px -2px 2px black,
    -2px 2px 2px black;
}

#madrid-marker {
  background-image: url("../img/madrid.jpg");
}

#madrid-marker::after {
  content: "Madrid";
}

#rio-marker {
  background-image: url("../img/rio.png");
}

#rio-marker::after {
  content: "Rio";
}

#lisboa-marker {
  background-image: url("../img/lisboa.jpg");
}

#lisboa-marker::after {
  content: "Lisboa";
}
```

### Basic JavaScript setup

```html
<html>

<head>
  <title>MapTiler 3D | Globe Travel Example</title>
  <style>
    body {
      margin: 0;
      padding: 0;
    }

    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }

    @keyframes fade {
      from {
        opacity: 1;
      }

      to {
        opacity: 0;
      }
    }

    .marker {
      background-size: cover;
      width: 50px;
      height: 50px;
      border-radius: 50%;
      border: 0.5rem solid black;
      cursor: pointer;
      position: absolute;
      top: -100px;
      left: -100px;
      z-index: 1000;
      animation: fade 0.5s ease-in-out reverse;
    }

    #play-button {
      position: absolute;
      top: 10px;
      left: 10px;
      z-index: 1000;
    }

    #madrid-marker::after,
    #rio-marker::after,
    #lisboa-marker::after {
      position: absolute;
      top: calc(100% - 0.5rem);
      left: 50%;
      transform: translateX(-50%);
      font-size: 1rem;
      font-weight: bold;
      color: white;
      text-shadow: 2px 2px 2px black, -2px -2px 2px black, 2px -2px 2px black,
        -2px 2px 2px black;
    }

    #madrid-marker {
      background-image: url("../img/madrid.webp");
    }

    #madrid-marker::after {
      content: "Madrid";
    }

    #rio-marker {
      background-image: url("../img/rio.png");
    }

    #rio-marker::after {
      content: "Rio";
    }

    #lisboa-marker {
      background-image: url("../img/lisboa.webp");
    }

    #lisboa-marker::after {
      content: "Lisboa";
    }
  </style>

  <script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.umd.min.js"></script>
  <link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.css" rel="stylesheet" />
  <script src="https://cdn.maptiler.com/maptiler-3d/v4.0.1/maptiler-3d.umd.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
</head>

<body>
  <div id="map"></div>
  <div id="madrid-marker" class="marker"></div>
  <div id="rio-marker" class="marker"></div>
  <div id="lisboa-marker" class="marker"></div>
  <button id="play-button">Go!</button>
  <script>
    const animationDuration = 5000; // 20 seconds
    const atAirportZoom = 7;
    const midAirZoom = 4;

    const LISBOA = [-9.166407980002864, 38.70110735141954];
    const MADRID = [-3.7001908499172367, 40.41629525891401];
    const RIO_DE_JANEIRO = [-43.2096, -22.9133];

    maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';

    const KEYFRAMES = [
      {
        delta: 0,
        easing: "QuadraticInOut",
        props: {
          lng: MADRID[0],
          lat: MADRID[1],
        },
        userData: {
          leg: 1,
        },
      },
      {
        delta: 1,
        easing: "QuadraticInOut",
        props: {
          lng: RIO_DE_JANEIRO[0],
          lat: RIO_DE_JANEIRO[1],
        },
        userData: {
          leg: 3,
        },
      },
    ];

    const map = new maptilersdk.Map({
      container: 'map',
      style: maptilersdk.MapStyle.HYBRID,
      center: MADRID,
      maxPitch: 85,
      halo: true,
      space: {
        color: "#000011",
      },
      terrainControl: true,
      maptilerLogo: true,
      projectionControl: true,
      zoom: 7,
      pitch: 60,
      projection: "globe"
    });

    // Waiting for the map to be ready
    map.on('ready', async () => {

      // Add 3D layer for the plane
      //#region Plane 3D Model
      const layer3D = new maptiler3d.Layer3D("3d-layer");

      map.addLayer(layer3D);

      const plane = await layer3D.addMeshFromURL("plane", "https://docs-media.maptiler.com/docs/models/plane_a340.glb", {
        lngLat: MADRID,
        altitude: 0,
        scale: 1,
        altitudeReference: maptiler3d.AltitudeReference.GROUND,
        transform: {
          rotation: {
            y: Math.PI / 2,
          },
        },
      });

      plane.setHeading(
        turf.bearing([MADRID[0], MADRID[1]], [RIO_DE_JANEIRO[0], RIO_DE_JANEIRO[1]])
      );

      //#region Boat 3D Model
      const boat = await layer3D.addMeshFromURL("boat", "https://docs-media.maptiler.com/docs/models/boat/scene.gltf", {
        lngLat: RIO_DE_JANEIRO,
        altitude: 0,
        scale: 1,
        altitudeReference: maptiler3d.AltitudeReference.MEAN_SEA_LEVEL,
        transform: {
          rotation: {
            y: Math.PI / 2,
          },
        },
      });

      boat.setHeading(
        turf.bearing([MADRID[0], MADRID[1]], [RIO_DE_JANEIRO[0], RIO_DE_JANEIRO[1]])
      );

      //#region Car 3D Model
      const car = await layer3D.addMeshFromURL("car", "https://docs-media.maptiler.com/docs/models/car/scene.gltf", {
        lngLat: LISBOA,
        altitude: 0,
        altitudeReference: maptiler3d.AltitudeReference.GROUND,
        scale: 1000,
      });

      car.setHeading(turf.bearing([LISBOA[0], LISBOA[1]], [MADRID[0], MADRID[1]]));
      // Add markers for the airports
      //#region Markers
      const lisboaMarker = new maptilersdk.Marker({
        element: document.getElementById("lisboa-marker"),
        subpixelPositioning: true,
      });

      lisboaMarker.setLngLat(LISBOA);
      lisboaMarker.addTo(map);

      const rioMarker = new maptilersdk.Marker({
        element: document.getElementById("rio-marker"),
        subpixelPositioning: true,
      });

      rioMarker.setLngLat(RIO_DE_JANEIRO);
      rioMarker.addTo(map);

      const madridMarker = new maptilersdk.Marker({
        element: document.getElementById("madrid-marker"),
        subpixelPositioning: true,
      });

      madridMarker.setLngLat(MADRID);
      madridMarker.addTo(map);

      //#region Plane Animation
      const planeAnimation = new maptilersdk.MaptilerAnimation({
        iterations: 1,
        keyframes: KEYFRAMES,
        duration: animationDuration,
      });

      //#region fetch data and add layers
      const boatJourneyData = await fetchGeoJSONAndAddAsLayer(
        "boat-route",
        "../data/boat.geojson",
        map
      );
      const carJourneyData = await fetchGeoJSONAndAddAsLayer(
        "car-route",
        "../data/car.geojson",
        map
      );

      //#region Boat and Car AnimatedRouteLayerSetup
      const boatAnimatedRouteLayer = new maptilersdk.AnimatedRouteLayer({
        source: {
          id: "boat-route",
          layerID: "boat-route-layer",
        },
        pathStrokeAnimation: {
          activeColor: [0, 255, 255, 0.5],
          inactiveColor: [0, 0, 0, 0],
        },
        cameraAnimation: {
          pathSmoothing: {
            resolution: 1,
            epsilon: 3,
          },
        },
        duration: animationDuration * 4,
        iterations: 1,
        delay: 1000,
      });

      const carAnimatedRouteLayer = new maptilersdk.AnimatedRouteLayer({
        source: {
          id: "car-route",
          layerID: "car-route-layer",
        },
        pathStrokeAnimation: {
          activeColor: [0, 255, 255, 0.5],
          inactiveColor: [0, 0, 0, 0],
        },
        cameraAnimation: {
          pathSmoothing: {
            resolution: 0.1,
            epsilon: 5,
          },
        },
        iterations: 1,
        duration: animationDuration * 2,
        delay: 1000,
      });

      let currentAnimation =
        planeAnimation;
      //#region "chaining" of animations
      planeAnimation.addEventListener("animationend", (e) => {
        planeAnimation.pause();
        map.addLayer(boatAnimatedRouteLayer);
        boatAnimatedRouteLayer.play();
        currentAnimation = boatAnimatedRouteLayer;
      });

      boatAnimatedRouteLayer.addEventListener("animationend", (e) => {
        map.removeLayer(boatAnimatedRouteLayer.id);
        boatAnimatedRouteLayer.pause();
        boatAnimatedRouteLayer.animationInstance?.reset();
        map.addLayer(carAnimatedRouteLayer);
        carAnimatedRouteLayer.play();
        currentAnimation = carAnimatedRouteLayer;
      });

      carAnimatedRouteLayer.addEventListener("animationend", (e) => {
        carAnimatedRouteLayer.pause();
        carAnimatedRouteLayer.animationInstance?.reset();
        map.removeLayer(carAnimatedRouteLayer.id);
        planeAnimation.play();
        currentAnimation = planeAnimation;
      });

      carAnimatedRouteLayer.addEventListener("timeupdate", frameCallback);
      boatAnimatedRouteLayer.addEventListener("timeupdate", frameCallback);
      planeAnimation.addEventListener("timeupdate", frameCallback);

      const carJourneyLength = turf.length(carJourneyData, { units: "kilometers" });
      const boatJourneyLength = turf.length(boatJourneyData, {
        units: "kilometers",
      });

      //#region Frame
      function frameCallback(e) {
        const isBoatAnimation =
          e.target === boatAnimatedRouteLayer.animationInstance;
        const isCarAnimation = e.target === carAnimatedRouteLayer.animationInstance;
        const isPlaneAnimation = e.target === planeAnimation;

        map.setCenter(new maptilersdk.LngLat(e.props.lng, e.props.lat));

        const previousLngLat = [
          e.previousProps.lng,
          e.previousProps.lat,
        ];
        const nextLngLat = [e.props.lng, e.props.lat];

        const heading = turf.bearing(previousLngLat, nextLngLat);

        if (isPlaneAnimation) {
          const previousStop = [
            e.keyframe?.props.lng,
            e.keyframe?.props.lat,
          ];
          const nextStop = [
            e.nextKeyframe?.props.lng,
            e.nextKeyframe?.props.lat,
          ];

          const legDistance = turf.rhumbDistance(previousStop, nextStop);

          const currentPosition = [e.props.lng, e.props.lat];

          const percentLeftToNextStop =
            turf.rhumbDistance(currentPosition, nextStop) / legDistance;

          const scale = Math.sin(percentLeftToNextStop * Math.PI) * 500;

          const zoomAlpha = Math.sin(percentLeftToNextStop * Math.PI);
          const zoom = lerp(atAirportZoom, midAirZoom, zoomAlpha);

          map.setZoom(zoom || map.getZoom());

          map.setBearing((1 - e.currentDelta) * 360);

          plane.modify({
            heading,
            lngLat: currentPosition,
            scale,
            altitude: 1000 * scale,
          });
        }

        if (isBoatAnimation) {
          map.setCenter(new maptilersdk.LngLat(e.props.lng, e.props.lat));
          const boatPosition = turf.along(
            boatJourneyData.features[0],
            boatJourneyLength * e.currentDelta,
            { units: "kilometers" }
          );
          const nextBoatPosition = turf.along(
            boatJourneyData.features[0],
            boatJourneyLength * (e.currentDelta + 0.005),
            { units: "kilometers" }
          );

          const boatHeading =
            turf.bearing(
              [
                boatPosition.geometry.coordinates[0],
                boatPosition.geometry.coordinates[1],
              ],
              [
                nextBoatPosition.geometry.coordinates[0],
                nextBoatPosition.geometry.coordinates[1],
              ]
            ) + 180;

          const scale = Math.sin(e.currentDelta * Math.PI) * 1000;

          boat.modify({
            heading: boatHeading,
            lngLat: [
              boatPosition.geometry.coordinates[0],
              boatPosition.geometry.coordinates[1],
            ],
            scale,
          });
        }

        if (isCarAnimation) {
          map.setCenter(new maptilersdk.LngLat(e.props.lng, e.props.lat));

          const carPosition = turf.along(
            carJourneyData.features[0].geometry,
            carJourneyLength * e.currentDelta,
            { units: "kilometers" }
          );
          const nextCarPosition = turf.along(
            carJourneyData.features[0].geometry,
            carJourneyLength * (e.currentDelta + 0.01),
            { units: "kilometers" }
          );

          const carHeading =
            turf.bearing(
              [
                carPosition.geometry.coordinates[0],
                carPosition.geometry.coordinates[1],
              ],
              [
                nextCarPosition.geometry.coordinates[0],
                nextCarPosition.geometry.coordinates[1],
              ]
            ) + 180;

          const scale = Math.sin(e.currentDelta * Math.PI) * 1000;

          car.modify({
            heading: carHeading, // because the car model is 180
            lngLat: [
              carPosition.geometry.coordinates[0],
              carPosition.geometry.coordinates[1],
            ],
            scale,
          });
        }
      }

      let isPlaying = false;
      document.getElementById("play-button")?.addEventListener("click", () => {
        if (isPlaying) {
          currentAnimation?.pause();
          isPlaying = false;
        } else {
          currentAnimation?.play();
          isPlaying = true;
        }
      });

    });

    //#region Helper Functions / types
    function lerp(a, b, t) {
      return a + (b - a) * t;
    }

    async function fetchGeoJSONAndAddAsLayer(id, url, map) {
      try {
        const data = await fetch(url).then((response) => response.json());
        addSourceAndLayer(map, id, {
          type: "geojson",
          data,
          lineMetrics: true,
        });
        return data;
      } catch (error) {
        throw error;
      }
    }

    function addSourceAndLayer(
      map,
      id,
      source
    ) {
      map.addSource(id, source);
      map.addLayer(
        {
          source: id,
          id: `${id}-layer`,
          type: "line",
          layout: {
            visibility: "visible",
            "line-sort-key": 1,
          },
          paint: {
            "line-color": "transparent",
            "line-width": 10,
          },
        },
        "3d-layer"
      );
    }
  </script>
</body>

</html>
```

[plane a340](https://skfb.ly/oJWGw) by mamont nikita is licensed under [Creative Commons Attribution](https://creativecommons.org/licenses/by/4.0/) [↩](#fnref:1)

Model by [scailman](https://sketchfab.com/scailman) for [Low Poly Small car](https://sketchfab.com/3d-models/low-poly-small-car-ebe7c5e98a7448b5abb2eaf0cb22b766) [↩](#fnref:1)

Model by [pedrosvalero](https://sketchfab.com/pedrosvalero) for [Main Boat From Over The Seas GGJ'17](https://sketchfab.com/3d-models/main-boat-from-over-the-seas-ggj17-260bd2ab5e574ce796aec5df1eb2866d) [↩](#fnref:1)

## Related examples

### [Animations and Animated Routes](https://docs.maptiler.com/sdk-js/examples/animated-route/)

Examples

Animates a path or route on the map based on keyframes or GeoJSON data.

### [Play GLTF animations](https://docs.maptiler.com/sdk-js/examples/3d-js-animated-flight/)

Examples

Play animations in GLTF models simulating a plane flight.

### [3D model events](https://docs.maptiler.com/sdk-js/examples/3d-js-events/)

Examples

Listen for mouse events on 3D models in maps.

### [Animate a 3D plane flight](https://docs.maptiler.com/sdk-js/examples/3d-js-flight/)

Examples

Simulate and animate a 3D plane flight between cities.
