Skip to main content

Shared camera calculations

Shared coordinate, pixel-ratio, bounds fitting, and animation-sampling semantics.

The core repository provides sqs-map-camera, also re-exported as sqs_map::camera in Rust. It depends only on the existing serialization library and compiles independently to WASM. It contains no GPU, window, downloader, or script engine. Existing core Mercator and zoom helpers now delegate to this module.

This is the 06.1 planar calculation implementation. Host bindings for MapLibre-style getCenter, project, jumpTo, and fitBounds continue in subsequent increments; this page does not claim that an older npm package includes them. Syntax references: Map API, bounds-fitting options.

Coordinates and pixels

Type / fieldMeaning
CameraPose.centerWGS84 longitude/latitude; longitude can explicitly identify a world copy, such as 181 versus -179
CameraPose.zoomContinuous zoom using a 512-pixel world baseline
CameraPose.bearingDegrees normalized to [-180,180)
Viewport.width / heightLogical viewport pixels; CSS pixels on the web
Viewport.pixel_ratioPhysical/logical pixel ratio, from 0.125 to 16
RawCamera.scalePhysical pixels per world unit; not resolution
resolution()World units per logical pixel
paddingNonnegative top, right, bottom, and left logical pixels

Mercator uses metres; the Geographic planar extension uses degrees. Callers identify the projection explicitly rather than inferring it from coordinate values. This planar implementation accepts only zero pitch/roll. Globe and pitched views reject explicitly until connected to the existing 3D camera.

The range for constructing or changing zoom is -32 to 32, including negative native world-fit views. Raw renderer ingress preserves larger actual zoom values produced by legacy point fitting and permits center/bearing updates that retain that zoom. It never presents a clipped value as authoritative. Bounds fitting defaults to limits of 0 to 24; hosts can pass their publicly configured limits. The computation range does not replace host or tile-level limits. Mercator input latitude clips to ±85.0511287798066. Geographic-plane pixels outside the Earth's latitude domain have no valid ground point. An out-of-domain raw Mercator centre rejects rather than producing a falsely authoritative clipped snapshot.

Fitting and conversion

use sqs_map::camera::{CameraPose, FitBoundsOptions, Padding, PlanarCamera, Projection, Viewport};
let camera = PlanarCamera::new(
    Projection::Mercator,
    Viewport::new(1000.0, 700.0, 2.0)?,
    CameraPose { center: [116.4, 39.9], zoom: 8.0, ..Default::default() },
)?;
let pose = camera.camera_for_bounds(
    [116.0, 39.0, 117.0, 40.0],
    FitBoundsOptions {
        padding: Some(Padding { top: 45.0, right: 25.0, bottom: 20.0, left: 280.0 }),
        offset: [15.0, -8.0],
        bearing: 37.0,
        ..Default::default()
    },
)?;
let fitted = camera.with_pose(pose)?;
let pixel = fitted.project([116.4, 39.9])?;
let coordinate = fitted.unproject(pixel)?;
let renderer_snapshot = fitted.raw()?;

Bounds use [west,south,east,north]. East less than west denotes an antimeridian crossing with a forward span of at most 360 degrees. For example, [170,-10,-170,10] centres on 180 degrees. project preserves the specified world copy; it does not silently replace -170 with 190. Point bounds use the configured maximum zoom.

Fitting accounts for rotation, asymmetric padding, and centre offset. An offset consumes available margin so all four corners remain inside the requested content area. A box that cannot fit within viewport and zoom limits returns CannotFit. Invalid inputs return InvalidArgument; unimplemented camera modes return Unsupported. Bindings handle integer GPU-viewport rounding and f32 renderer scales. Ideal calculation precision is not device-image acceptance.

Animation sampling

CameraTransition accepts a starting camera, target, duration in milliseconds, and Linear / Smoothstep; the host supplies elapsed monotonic time. It creates no timer and does not equate sample completion with rendering or idle. Bearing follows the shortest path. Longitude selects the nearest world copy by default; with_world_copy(..., false) retains an explicitly unwrapped path. Zero duration immediately returns the target, and the final sample lands exactly on it. This is the shared interpolation foundation; full easeTo / flyTo parameters and events remain subsequent binding work.

Acceptance covers rotation and pixel ratio, asymmetric bounds fitting, the antimeridian, high latitudes, negative native zoom, invalid parameters, and animation samples, plus an executable Rust documentation example. These checks establish calculation semantics; actual GPU, shared-script, and mobile-device images remain their respective acceptance stages.