跳到主要内容

相机平滑移动与飞行

跨端共同动画计算、完整生命周期、停止与减少动画设置。

map.easeTo(options)、map.flyTo(options)、map.stop() 使用自研核心的同一二维相机驱动。网页主线程和 Worker、原生动态库/QuickJS、桌面应用门面复用运算与事件;移动壳仍待接入。墨卡托倾斜动画已接通;球面动画与锚点说明球面支持范围。投影、倾角与 roll 的限制以相机 API及对应宿主能力为准。

写入返回地图,可链式调用。await map.sqs.flush() 确认命令已应用或动画已启动;即使使用 render:true,也只等待下一真实帧。它不等待动画结束。应在启动前注册 once('moveend'),再检查 event.animation.status;一次性原生脚本入口没有事件等待,须使用持续会话。

// @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()};

选项沿用 MapLibre 常用名称:center、zoom、bearing、padding、duration、animate、offset、easing、essential;easeTo 还可使用 around(与 center 或 offset 互斥),flyTo 还支持 curve、speed、screenSpeed、minZoom、maxDuration。共同相机也支持已接通宿主的 pitch 动画;未知选项、无效组合和超出当前投影能力的姿态会明确拒绝。

平滑移动及 zoomTo/rotateTo/panTo/panBy/fitBounds/zoomIn/zoomOut/resetNorth 默认 300 毫秒,默认缓动为平滑三次曲线。duration 可为 0~60000 毫秒;animate:false 或 duration:0 立即应用。linear:true 使用线性进度。JavaScript 的 easing 函数在发送前采样 257 次,须返回有限的 0~1 数字,端点为 0/1;使用纯函数。C JSON 可提供 sqsEasingSamples,2~513 个同规则样本。

flyTo 采用自研实现的双曲飞行路径,会先适当拉远再移动并靠近目的地。curve 默认 1.42,speed 默认 1.2;未指定 duration 时由路径长度和速度计算,screenSpeed 覆盖 speed。minZoom 限制最远拉升缩放,不能高于任一端点;计算时长超过 maxDuration 时立即应用,超过内部 60 秒预算则明确拒绝。easeTo/flyTo 的显式 center 默认选离当前经度最近的世界副本,例如 179→-179 到达 181;sqsShortestWorldCopy:false 保留显式副本。jumpTo、panBy 保留原先明确的副本语义。

动画开始发 movestart 和对应 zoomstart/rotatestart,采样时发 move/zoom/rotate,结束各发一次 end。历史 cameraState 和 animation 均为只读快照,animation.id 在一次动画内固定;status 为 running/completed/cancelled/failed。stop 的取消原因为 stopped,替换为 replaced,手势或外部相机变化为 interrupted,切换不兼容视图为 view-changed。无变化的立即定位不制造事件;已停止时再次 stop 不制造结束事件。渲染或显示准备失败保留最后实际画面、结束活动动画并发带错误码的 error;满队列先排空再继续,不能静默丢失事件。

await map.isEasing()/isMoving()/isZooming()/isRotating() 读取共同驱动实际状态;await map.sqs.animationState() 返回状态、持续时间、进度和目标(这些额外字段仅在活动时出现)。网页 SDK 使用 view.animationState() 和 view.setReducedMotion(boolean);脚本为 map.sqs.setReducedMotion(boolean)。网页初始化默认读取系统减少动画偏好,原生默认 false;essential:true 可绕过偏好。设置只影响后续动画,不追溯改写已有动画。动画属于地图,脚本正常退出后仍可继续;stop 明确终止。

动态库继续使用 sqsm_map_camera_json 的方法/参数 JSON,不新增另一套坐标或语法;运行能力位 SQSM_MAP_CAMERA_ANIMATIONS(2048)表示支持。原生宿主必须按 state.resources.repaintAfterMs(活动时最多 16 毫秒)持续渲染或泵持续脚本会话,并排空地图/订阅事件;引擎不创建宿主计时线程。WASM SDK 自行驱动 rAF,桌面组件随 UI 刷新推进。宿主挂起时按实际单调时钟补采样;不会补发数百个过期帧。

API 名称与参数概念参考 MapLibre AnimationOptions、FlyToOptions。上述默认缓动、预算、异步权威查询和状态扩展均是 SqsMap 的明确约定,不承诺全部 MapLibre 动画兼容。Windows/macOS、移动设备、桌面 GUI 和真实浏览器硬件 WebGPU 验收另行记录。

倾斜地图的渲染确认

桌面、动态库内置 QuickJS、WASM 主线程及严格 Worker 使用同一份完整脚本。打开独立网页运行验证,也可直接复制下面代码到桌面调试板或原生脚本入口。

render:true 表示当前实际画面已经绘制;它不等待动画结束。动画运行时修改画笔,确认和截图必须使用新内容,不接受旧颜色的帧。桌面在地图绘制后结算等待,共同倾斜场景及时准备当前几何;复杂场景的性能预算与优化仍单独验收。

// @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};

本机真实桌面窗口验证 17 项及四个确认时像素快照,包含未结束动画中的绿色新画笔;共享核心生产会话 GPU 回归验证当前倾角与实际绿色像素。原生/网页验证与版本以发行清单为准。这不是 Windows/macOS 外机、移动设备、性能或正式发行验收。

网页脚本运行器按请求完成时捕获的事件序号交付收据:相关前置事件必须先应用,之后新增的动画事件不会把已经完成的请求阻塞到动画结束。无需清空整个事件流,异步事件回调也能继续发起宿主请求。