Move the camera
The camera belongs to the map. A camera call needs a live map and the map’s owner thread, and it works before any render session exists.
Every command takes the same camera options. Set the fields that you want to change, and MapLibre leaves every other field at its current value.
mln_camera_options camera = mln_camera_options_default();camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM | MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH;camera.latitude = 37.7749;camera.longitude = -122.4194;camera.zoom = 13.0;camera.bearing = 12.0;camera.pitch = 30.0;Choose how the camera moves
Section titled “Choose how the camera moves”Three commands apply that camera, and they differ only in timing.
mln_map_jump_to applies the camera immediately. Use it for the first position
of a session, a position restored from saved state, or a camera that the host
recomputes every frame during a gesture.
mln_status jump_downtown(mln_map map) { const mln_camera_options camera = downtown(); return mln_map_jump_to(map, &camera);}mln_map_ease_to interpolates every field that you set along a cubic easing
curve. Use it for a short move that remains easy to follow. A duration of zero,
the default, applies the camera immediately.
mln_animation_options animation = mln_animation_options_default();animation.fields = MLN_ANIMATION_OPTION_DURATION | MLN_ANIMATION_OPTION_EASING | MLN_ANIMATION_OPTION_TRANSITION_ID;animation.duration_ms = 800.0;// Control points of a cubic bezier that runs from (0, 0) to (1, 1).animation.easing = (mln_unit_bezier){.x1 = 0.25, .y1 = 0.1, .x2 = 0.25, .y2 = 1.0};animation.transition_id = transition_id;return mln_map_ease_to(map, &camera, &animation);mln_map_fly_to takes the same animation options and follows a curved path. Use
it for a long move. The zoom decreases through the middle of the transition to
cross the distance without loading every high-zoom tile between the positions.
When the duration is absent, MapLibre derives one from the distance and a
velocity of 1.2 screenfuls per second.
Ease
Zoom runs straight from the start value to the end value.
Fly
Zoom pulls back across the middle, then settles on the end value.
An animated transition advances while the host pumps the runtime and draws frames. Its duration uses wall-clock time, but pausing the pump also pauses the transition. A host that pumps on demand continues until the transition ends. The repaint flag in Run the render loop signals that more frames are required.
Move by a gesture
Section titled “Move by a gesture”The three commands above take a destination. Gestures instead provide relative input: a drag provides a distance, and a pinch provides a scale factor and a screen position. Relative moves apply those values to the current camera.
Set the gesture flag when the interaction starts, and clear it when the interaction ends. While the flag is set, a drag across the antimeridian continues in the same direction. Cancel a running transition when the gesture starts to give the gesture sole control of the camera.
mln_map_cancel_transitions(map);mln_map_set_gesture_in_progress(map, true);A drag applies screen-space deltas in logical pixels. Send the movement since the last frame rather than a running total, because each call composes with the camera it finds.
// Screen-space deltas in logical pixels, measured since the last call rather// than from the start of the gesture.mln_map_move_by(map, delta_x, delta_y);A pinch scales about an anchor. The anchor stays under the fingers while the map moves around it.
// The focus point stays fixed on screen while the map scales around it.mln_map_scale_by(map, scale, &focus);Clear the flag for both completed and cancelled gestures. The animated variants continue the gesture’s momentum and take the same animation options as an ease.
mln_map_set_gesture_in_progress(map, false);
mln_animation_options animation = mln_animation_options_default();animation.fields |= MLN_ANIMATION_OPTION_DURATION;animation.duration_ms = 250.0;mln_map_scale_by_animated(map, residual_scale, &focus, &animation);Call each camera operation on the map’s owner thread. When another thread reads gestures, send relative deltas to the owner thread. An absolute camera computed from an earlier frame can be stale when the owner thread applies it.
Fit the camera to bounds
Section titled “Fit the camera to bounds”To frame a region rather than a point, compute the camera for it first. A bounds query reports the camera that fits those bounds inside the current viewport, inset by the padding that you pass in logical pixels.
mln_camera_fit_options fit = mln_camera_fit_options_default();fit.fields = MLN_CAMERA_FIT_OPTION_PADDING;fit.padding = (mln_edge_insets){.top = 24, .left = 24, .bottom = 24, .right = 24};The query reports a camera and leaves the map unchanged. Apply the result with the command that provides the required timing.
mln_camera_options fitted = mln_camera_options_default();const mln_status status = mln_map_camera_for_lat_lng_bounds(map, bounds, &fit, &fitted);if (status != MLN_STATUS_OK) { return status;}
return mln_map_fly_to(map, &fitted, NULL);Know when a transition ends
Section titled “Know when a transition ends”Set a transition ID on the animation options to identify its completion event. The runtime queues one transition-finished event with that value while the map’s subscription selects that type. The event arrives after completion, replacement by another camera command, or cancellation. Drain it from the runtime that owns the map, as described in Handle events.
if (event->type != MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED) { return false;}const mln_runtime_event_camera_transition_finished* finished = &event->payload.camera_transition_finished;return finished->transition_id == transition_id;The event identifies the transition but has no completion reason. To distinguish completion from cancellation, compare the resulting camera with the requested camera.