Customize Camera Animations in Maptoolkit Maps JS
Camera animations in Maptoolkit Maps JS accept an AnimationOptions object that controls duration, easing function, and whether the animation can be interrupted. You can pass a custom easing function to create spring-like, bouncy, or decelerated motion instead of the default ease-in-out curve. Use this to match the feel of your application, create dramatic fly-in effects, or implement consistent animation timing across all map interactions.
const API_KEY = 'YOUR_API_KEY';
// Declare various easing functions.
// Easing functions mathematically describe
// how fast a value changes during an animation.
// Each function takes a parameter t that represents
// the progress of the animation.
// t is in a range of 0 to 1 where 0 is the initial
// state and 1 is the completed state.
const easingFunctions = {
// Start slow and gradually increase speed
easeInCubic(t) {
return t * t * t;
},
// Start fast with a long, slow wind-down
easeOutQuint(t) {
return 1 - Math.pow(1 - t, 5);
},
// Slow start and finish with fast middle
easeInOutCirc(t) {
return t < 0.5
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
},
// Fast start with a "bounce" at the end
easeOutBounce(t) {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
};
// Set up some helpful UX on the form
const durationValueSpan = document.getElementById('durationValue');
const durationInput = document.getElementById('duration');
durationValueSpan.innerHTML = `${durationInput.value / 1000} seconds`;
durationInput.addEventListener('change', (e) => {
durationValueSpan.innerHTML = `${e.target.value / 1000} seconds`;
});
const animateLabel = document.getElementById('animateLabel');
const animateValue = document.getElementById('animate');
animateValue.addEventListener('change', (e) => {
animateLabel.innerHTML = e.target.checked ? 'Yes' : 'No';
});
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [-95, 40],
zoom: 3,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
map.on('load', () => {
// Add a layer to display the map's center point
map.addSource('center', {
type: 'geojson',
data: {
type: 'Point',
coordinates: [-94, 40]
}
});
map.addLayer({
id: 'center',
type: 'symbol',
source: 'center',
layout: {
'text-field': 'Center: [-94, 40]',
'text-offset': [0, 0.6],
'text-anchor': 'top'
}
});
map.addLayer({
id: 'center-circle',
type: 'circle',
source: 'center',
paint: {
'circle-radius': 6,
'circle-color': '#007cbf'
}
});
const animateButton = document.getElementById('animateButton');
animateButton.addEventListener('click', () => {
const easingInput = document.getElementById('easing');
const easingFn = easingFunctions[
easingInput.options[easingInput.selectedIndex].value
];
const duration = parseInt(durationInput.value, 10);
const animate = animateValue.checked;
const offsetX = parseInt(document.getElementById('offset-x').value, 10);
const offsetY = parseInt(document.getElementById('offset-y').value, 10);
const animationOptions = {
duration,
easing: easingFn,
offset: [offsetX, offsetY],
animate,
essential: true // Animation will happen even if user has `prefers-reduced-motion` setting on
};
// Create a random location to fly to by offsetting the map's
// initial center point by up to 10 degrees.
const center = [
-95 + (Math.random() - 0.5) * 20,
40 + (Math.random() - 0.5) * 20
];
// Merge animationOptions with other flyTo options
animationOptions.center = center;
map.flyTo(animationOptions);
// Update 'center' source and layer to show our new map center.
// Compare this center point to where the camera ends up when an offset is applied.
map.getSource('center').setData({
type: 'Point',
coordinates: center
});
map.setLayoutProperty(
'center',
'text-field',
`Center: [${center[0].toFixed(1)}, ${center[1].toFixed(1)}]`
);
});
});<!DOCTYPE html>
<html lang="en">
<head>
<title>Customize Camera Animations - Maptoolkit Maps JS</title>
<meta property="og:description" content="Customize camera animations using AnimationOptions." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
.map-overlay {
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
position: absolute;
width: 200px;
top: 0;
left: 0;
padding: 10px;
}
.map-overlay .map-overlay-inner {
background-color: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
border-radius: 3px;
padding: 10px;
margin-bottom: 10px;
}
.map-overlay-inner fieldset {
border: none;
padding: 0;
margin: 0 0 10px;
}
.map-overlay-inner fieldset:last-child { margin: 0; }
.map-overlay-inner select { width: 100%; }
.map-overlay-inner p { margin: 0; }
.map-overlay-inner label { display: block; font-weight: bold; }
.map-overlay-inner button {
background-color: cornflowerblue;
color: white;
border-radius: 5px;
display: inline-block;
height: 20px;
border: none;
cursor: pointer;
}
.map-overlay-inner button:focus { outline: none; }
.map-overlay-inner button:hover {
background-color: blue;
box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.1);
-webkit-transition: background-color 500ms linear;
-ms-transition: background-color 500ms linear;
transition: background-color 500ms linear;
}
.offset > label, .offset > input { display: inline; }
#animateLabel { display: inline-block; min-width: 20px; }
</style>
</head>
<body>
<div id="map"></div>
<div class="map-overlay top">
<div class="map-overlay-inner">
<fieldset>
<label for="easing">Select easing function</label>
<select id="easing" name="easing">
<option value="easeInCubic">Ease In Cubic</option>
<option value="easeOutQuint">Ease Out Quint</option>
<option value="easeInOutCirc">Ease In/Out Circ</option>
<option value="easeOutBounce">Ease Out Bounce</option>
</select>
</fieldset>
<fieldset>
<label for="duration">Set animation duration</label>
<p id="durationValue"></p>
<input type="range" id="duration" name="duration" min="0" max="10000" step="500" value="1000" />
</fieldset>
<fieldset>
<label>Animate camera motion</label>
<label for="animate" id="animateLabel">Yes</label>
<input type="checkbox" id="animate" name="animate" checked />
</fieldset>
<fieldset class="offset">
<label for="offset-x">Offset-X</label>
<input type="number" id="offset-x" name="offset-x" min="-200" max="200" step="50" value="0" />
</fieldset>
<fieldset class="offset">
<label for="offset-y">Offset-Y</label>
<input type="number" id="offset-y" name="offset-y" min="-200" max="200" step="50" value="0" />
<p>Offsets can be negative</p>
</fieldset>
<button type="button" id="animateButton" name="test-animation">
Test Animation
</button>
</div>
</div>
<script>
const API_KEY = 'YOUR_API_KEY';
// Declare various easing functions.
// Easing functions mathematically describe
// how fast a value changes during an animation.
// Each function takes a parameter t that represents
// the progress of the animation.
// t is in a range of 0 to 1 where 0 is the initial
// state and 1 is the completed state.
const easingFunctions = {
// Start slow and gradually increase speed
easeInCubic(t) {
return t * t * t;
},
// Start fast with a long, slow wind-down
easeOutQuint(t) {
return 1 - Math.pow(1 - t, 5);
},
// Slow start and finish with fast middle
easeInOutCirc(t) {
return t < 0.5
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
},
// Fast start with a "bounce" at the end
easeOutBounce(t) {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
};
// Set up some helpful UX on the form
const durationValueSpan = document.getElementById('durationValue');
const durationInput = document.getElementById('duration');
durationValueSpan.innerHTML = `${durationInput.value / 1000} seconds`;
durationInput.addEventListener('change', (e) => {
durationValueSpan.innerHTML = `${e.target.value / 1000} seconds`;
});
const animateLabel = document.getElementById('animateLabel');
const animateValue = document.getElementById('animate');
animateValue.addEventListener('change', (e) => {
animateLabel.innerHTML = e.target.checked ? 'Yes' : 'No';
});
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [-95, 40],
zoom: 3,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
map.on('load', () => {
// Add a layer to display the map's center point
map.addSource('center', {
type: 'geojson',
data: {
type: 'Point',
coordinates: [-94, 40]
}
});
map.addLayer({
id: 'center',
type: 'symbol',
source: 'center',
layout: {
'text-field': 'Center: [-94, 40]',
'text-offset': [0, 0.6],
'text-anchor': 'top'
}
});
map.addLayer({
id: 'center-circle',
type: 'circle',
source: 'center',
paint: {
'circle-radius': 6,
'circle-color': '#007cbf'
}
});
const animateButton = document.getElementById('animateButton');
animateButton.addEventListener('click', () => {
const easingInput = document.getElementById('easing');
const easingFn = easingFunctions[
easingInput.options[easingInput.selectedIndex].value
];
const duration = parseInt(durationInput.value, 10);
const animate = animateValue.checked;
const offsetX = parseInt(document.getElementById('offset-x').value, 10);
const offsetY = parseInt(document.getElementById('offset-y').value, 10);
const animationOptions = {
duration,
easing: easingFn,
offset: [offsetX, offsetY],
animate,
essential: true // Animation will happen even if user has `prefers-reduced-motion` setting on
};
// Create a random location to fly to by offsetting the map's
// initial center point by up to 10 degrees.
const center = [
-95 + (Math.random() - 0.5) * 20,
40 + (Math.random() - 0.5) * 20
];
// Merge animationOptions with other flyTo options
animationOptions.center = center;
map.flyTo(animationOptions);
// Update 'center' source and layer to show our new map center.
// Compare this center point to where the camera ends up when an offset is applied.
map.getSource('center').setData({
type: 'Point',
coordinates: center
});
map.setLayoutProperty(
'center',
'text-field',
`Center: [${center[0].toFixed(1)}, ${center[1].toFixed(1)}]`
);
});
});
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
How it works
Every camera method takes an options object, and three fields matter most.
duration is in milliseconds and overrides the distance-based default. easing is a function
from 0 to 1 that shapes progress, so a linear function feels mechanical while an ease-out
feels like the map settling. essential: true marks the animation as meaningful, which both
keeps it from being cancelled and makes it run for users who have asked for reduced motion.
offset shifts the target away from centre, which is what you want when a panel or sidebar
covers part of the map: without it the destination ends up behind the panel.
animate: false turns any of these calls into the equivalent of jumpTo, which is a
convenient single switch when a preference decides whether to animate.
Next steps
Once you are setting options explicitly, the useful step is being consistent about it. Most applications end up with two or three named movements, a quick nudge, a considered travel, an instant jump, rather than tuning each call separately.
offset is the one to reach for as soon as anything overlays the map, since a destination
centred behind a sidebar has effectively not been shown at all.