Skip to content

Customize camera animations

Replicates the MapLibre example Customize camera animations. The target marker is YAML: one named source drawn by a circle layer and a label layer. The control panel is page chrome in <ml-map>’s top-left slot, and its JS turns the choices into flyTo AnimationOptions, then moves the target through updateLayerData().

Pick an easing and duration, set an offset, and press Test animation
customize-camera-animations.yaml
# yaml-language-server: $schema=https://docs.maplibre-yaml.org/schema/latest/map.schema.json
# Gallery twin of https://maplibre.org/maplibre-gl-js/docs/examples/customize-camera-animations/
# The YAML half: the map and the "target" marker (one named source, two
# layers; the label reads a `label` property). The page's JS turns the
# panel's choices into flyTo AnimationOptions and moves the target —
# see customize-camera-animations.js.
type: map
id: customize-camera-animations
config:
center: [-95, 40]
zoom: 3
mapStyle: "https://demotiles.maplibre.org/style.json"
sources:
target:
type: geojson
data:
type: Feature
properties: { label: "Center: [-94, 40]" }
geometry: { type: Point, coordinates: [-94, 40] }
layers:
- id: target-circle
type: circle
source: target
paint:
circle-radius: 6
circle-color: "#007cbf"
- id: target-label
type: symbol
source: target
layout:
text-field: ["get", "label"]
text-font: ["Noto Sans Regular"]
text-offset: [0, 0.6]
text-anchor: top

This markup goes inside <ml-map>; children with a slot ride its corner system.

customize-camera-animations.html
<div slot="top-left" class="gallery-panel" data-camera-panel>
<fieldset>
<label>Easing
<select 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>
</label>
<label>Duration
<input type="range" name="duration" min="0" max="10000" step="500" value="1000" />
<span data-duration-value></span>
</label>
<label><input type="checkbox" name="animate" checked /> Animate camera motion</label>
<label>Offset X <input type="number" name="offset-x" min="-200" max="200" step="50" value="0" /></label>
<label>Offset Y <input type="number" name="offset-y" min="-200" max="200" step="50" value="0" /></label>
<button type="button" data-animate>Test animation</button>
</fieldset>
</div>
customize-camera-animations.js
// The JavaScript half: flyTo's AnimationOptions — duration, easing, offset,
// animate — built from the panel's controls. The YAML's "target" layers
// show where the camera was asked to centre; with an offset, compare that
// point with where the camera actually ends up.
const mapEl = document.querySelector("ml-map");
const panel = document.querySelector("[data-camera-panel]");
const field = (name) => panel.querySelector(`[name="${name}"]`);
// Easing functions map animation progress t (0..1) to output progress.
const easingFunctions = {
easeInCubic: (t) => t * t * t, // slow start, then speeds up
easeOutQuint: (t) => 1 - Math.pow(1 - t, 5), // fast start, long wind-down
easeInOutCirc: (t) => // slow start and finish, fast middle
t < 0.5
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
easeOutBounce(t) { // fast start with a bounce at the end
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) return n1 * t * t;
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
return n1 * (t -= 2.625 / d1) * t + 0.984375;
},
};
const durationLabel = panel.querySelector("[data-duration-value]");
const showDuration = () => (durationLabel.textContent = `${field("duration").value / 1000} s`);
field("duration").addEventListener("input", showDuration);
showDuration();
panel.querySelector("[data-animate]").addEventListener("click", async () => {
const map = await mapEl.mapReady();
// A random target up to 10 degrees from the starting centre.
const center = [-95 + (Math.random() - 0.5) * 20, 40 + (Math.random() - 0.5) * 20];
map.flyTo({
center,
duration: Number(field("duration").value),
easing: easingFunctions[field("easing").value],
offset: [Number(field("offset-x").value), Number(field("offset-y").value)],
animate: field("animate").checked,
essential: true,
});
mapEl.updateLayerData("target-circle", {
type: "Feature",
properties: { label: `Center: [${center[0].toFixed(1)}, ${center[1].toFixed(1)}]` },
geometry: { type: "Point", coordinates: center },
});
});
  • With an offset, the camera centres the target that many pixels away from the viewport centre — compare where the marker lands with the middle of the map.
  • Both layers read one named source, so updating it through either layer id moves the circle and its label together.
  • Unchecking Animate camera motion sets animate: false: the same call becomes an instant jump.