Skip to main content

Camera easing and flight

Shared planar animation, lifecycle, stopping and reduced-motion policy.

map.easeTo(options), map.flyTo(options) and map.stop() use one independently developed planar camera driver. Browser main/Worker, native C/QuickJS and the desktop facade share calculations and events. Mobile shells still need integration. Pitched Mercator animation is connected; globe motion and anchors documents spherical support. Projection, pitch and roll limits follow the Camera API and the host capabilities.

Writes return the map. await map.sqs.flush() confirms application or animation start; render:true waits for a real future frame, not animation completion. Register once('moveend') before starting, then inspect animation.status. Native one-shot scripts cannot await events; use retained sessions.

// @gis-api 0.5-session
// @permissions ["project.read", "map.write"]
import {map} from 'sqsmap';
const end = map.once('moveend');
map.flyTo({center:[116.4,39.9],zoom:7,duration:1200,essential:true});
await map.sqs.flush({render:false});
const event = await end;
export default {status:event.animation?.status,center:await map.getCenter()};

Options use familiar MapLibre names: center, zoom, bearing, padding, duration, animate, offset, easing and essential. easeTo also accepts around, mutually exclusive with center/offset; flyTo adds curve, speed, screenSpeed, minZoom and maxDuration. Connected hosts also support pitch animation. Unknown options, invalid combinations and poses beyond the current projection capabilities reject explicitly.

Ease and zoomTo/rotateTo/panTo/panBy/fitBounds/zoomIn/zoomOut/resetNorth default to 300 ms and smooth cubic progress. duration is 0–60000 ms; animate:false or duration:0 applies immediately. linear:true uses linear progress. JavaScript easing functions are sampled 257 times before transport: use a pure function returning finite values within 0,1, with exact 0/1 endpoints. C JSON accepts sqsEasingSamples with 2–513 samples under the same constraints.

flyTo follows an independently implemented hyperbolic flight, zooming out during travel and back in near its destination. curve defaults to 1.42 and speed to 1.2. Without duration, path length and speed determine timing; screenSpeed overrides speed. minZoom caps the farthest zoom-out and cannot exceed either endpoint. A computed duration above maxDuration applies immediately; exceeding the internal 60-second budget rejects explicitly. Explicit easeTo/flyTo centers use the nearest longitude copy by default: 179→-179 ends at 181. sqsShortestWorldCopy:false preserves the explicit copy. jumpTo and panBy retain their existing explicit-copy semantics.

Start channels fire once, move/zoom/rotate update during actual samples, and corresponding end channels fire once. Historical cameraState and animation snapshots are immutable. animation.id remains stable; status is running/completed/cancelled/failed. Cancellation reasons include stopped, replaced, interrupted and view-changed. Unchanged immediate writes and repeated stop calls emit no artificial lifecycle. Display/render failure preserves the last applied frame, stops the timeline and emits typed error. Queue pressure requires draining and retrying rather than silently discarding events.

Authoritative await map.isEasing()/isMoving()/isZooming()/isRotating() queries report the shared driver. await map.sqs.animationState() includes status and, while active, elapsed time, duration and target. Standalone SDK calls are view.animationState()/view.setReducedMotion(boolean); scripts use map.sqs.setReducedMotion(boolean). Browser initialization reads the OS reduced-motion preference, native defaults to false, and essential:true bypasses it. Policy changes affect subsequent animations. Motion belongs to the map and can continue after normal script exit; stop explicitly terminates it.

C hosts use the existing sqsm_map_camera_json method/argument JSON; SQSM_MAP_CAMERA_ANIMATIONS (2048) advertises support. Hosts must keep rendering or pumping retained sessions according to state.resources.repaintAfterMs (at most 16 ms while active), and drain both map/subscription events. The engine creates no host timer thread. The WASM SDK schedules rAF; desktop components advance with UI refresh. After suspension, a monotonic-clock sample catches up without replaying stale frames.

Names and option concepts reference MapLibre AnimationOptions and FlyToOptions. SqsMap explicitly defines its easing, budgets, asynchronous authoritative reads and status extensions; complete MapLibre animation compatibility is not claimed. External Windows/macOS, mobile, desktop GUI and hardware browser WebGPU acceptance remain separate.

Rendering receipts in pitched maps

The desktop console, embedded native QuickJS, WASM main-thread maps and strict rendering Workers run the same complete script. Open the standalone browser verification, or copy the code below into a desktop/native script entry.

render:true confirms actual drawing of the current view; it does not wait for animation completion. A paint update during motion must be drawn before its receipt or screenshot is accepted. The desktop settles pending receipts after drawing. Checked shared pitched scenes prepare current geometry without deferring indefinitely during motion. Complex-scene performance budgets and optimizations still need separate acceptance.

// @gis-api 0.5-session
// @permissions ["project.read", "map.write"]
import {map} from 'sqsmap';
const checks=[];function check(name,value){if(!value)throw Error(name);checks.push(name);}
const caps=await map.sqs.getCapabilities();
check('real pitched host',caps.nativePitchedCamera===true||caps.webPitchedCamera===true||caps.desktopPitchedCamera===true);
map.sqs.setReducedMotion(false);map.setProjection({type:'mercator'});
map.jumpTo({center:[181,40],zoom:19,pitch:65,bearing:33,padding:{left:30,right:2,top:15,bottom:4}});
map.addSource('render-points',{type:'geojson',data:{type:'Feature',id:7,properties:{color:'#ff0000',radius:40},geometry:{type:'Point',coordinates:[181,40]}}});
map.addLayer({id:'render-dots',type:'circle',source:'render-points',paint:{'circle-color':['get','color'],'circle-radius':['get','radius']}});
const initial=await map.sqs.flush({render:true});check('initial actual frame',initial.stage==='rendered');
check('ease chain',map.easeTo({pitch:30,bearing:83,duration:10000})===map);
const applied=await map.sqs.flush({render:false});check('application separate from drawing',applied.stage==='applied');
const first=await map.sqs.flush({render:true});check('rendered receipt',first.stage==='rendered');
check('frame advances',first.frameSequence>initial.frameSequence);
const during=await map.sqs.animationState();check('render completes before animation',during.active===true&&during.animation.status==='running');
check('receipt pitch is actual sample',first.cameraState.camera.pitch>=30&&first.cameraState.camera.pitch<=65);
check('live pitch is actual sample',(await map.getPitch())>=30&&(await map.getPitch())<=65);
map.setPaintProperty('render-dots','circle-color','#00ff00');
const painted=await map.sqs.flush({render:true});check('paint receipt is rendered',painted.stage==='rendered');
check('paint does not wait for motion end',await map.isMoving()===true);
check('paint gets newer frame',painted.frameSequence>first.frameSequence);
const point=await map.project([181,40]);const hits=await map.queryRenderedFeatures(point);
check('original source identity',hits.length===1&&hits[0].id===7&&hits[0].source==='render-points');
check('paint does not rewrite properties',hits[0].properties.color==='#ff0000'&&hits[0].properties.radius===40);
map.stop();const stopped=await map.sqs.flush({render:true});check('stop is rendered',stopped.stage==='rendered');
check('motion stopped',await map.isMoving()===false);
const after=await map.sqs.animationState();check('historical stop reason',after.animation.status==='cancelled'&&after.animation.reason==='stopped');
export default {checks,initial,applied,first,painted,stopped,during,after,point};

Local real-window desktop acceptance covers seventeen assertions and four receipt-time pixel snapshots, including new green paint while animation remains active. Production core GPU regression verifies the current pitch and actual green pixels. Native/browser validation and source versions are identified by the distribution manifest. This is not external Windows/macOS, mobile-device, performance or formal-release acceptance.

The web script runner delivers each reply after acknowledging the event watermark captured when its host request completed. Related preceding events are applied first; animation events emitted later do not hold an already completed reply until motion stops. The stream need not become empty, so asynchronous callbacks can continue making host requests.