Scene clock
15.3 moves scene time into the shared core. Browser main, strict Worker, native QuickJS/C and desktop scripts use the same semantics. sceneClock is implemented; Shared route playback is connected in15.5: scenePlayback/sceneRoutePlayback are true; sceneLinePlayback/sceneTracks are true; see line playback. See Route playback. Mobile hosts/devices, external Windows/macOS and formal releases still require separate acceptance.
import {map} from 'sqsmap';
if (!(await map.sqs.getCapabilities()).sceneClock) throw Error('Matching scene clock version required');
const scene = await map.sqs.scenes.create({id:'clock'});
await scene.start();
const running = await scene.getClock(); // {id, sceneInstanceId, state, elapsedMs}
await scene.pause();
const paused = await scene.getClock();
await scene.resume();
await scene.stop();
const stopped = await scene.getClock();
await map.sqs.scenes.remove(scene);
elapsedMs is this scene instance's accumulated running time in milliseconds, with fractional precision. Drafts start at zero; pause and stop freeze time; resume keeps accumulated time. Repeated start/resume never resets it. Shared scene stop is terminal. Removing a scene makes its handle return STALE_HANDLE; recreating the same public ID creates a new identity and clock. Each query returns an immutable historical observation.
Queries are pure reads: no sqs.scenechange, write sequence advance, new render frame, resource wait, animation completion or ready promise. State controls still emit existing lifecycle events; failed controls leave clock state unchanged. Caller-owned UI can query time at its own refresh rate. The map owns scene state; exiting the script VM leaves a running scene clock running.
The host supplies monotonic time. Shared calculation excludes explicit pauses and includes monotonic intervals while running in the background or between queries. Whether system sleep contributes depends on the platform's monotonic clock. This is neither a date nor an absolute wall clock. The old desktop map.scenes keeps its compatibility timing rules; new code uses map.sqs.scenes. Do not subtract another map's host timestamp from elapsedMs.
Native libraries use sqsm_map_scene_json, for example {"method":"getClock","args":[{"id":"clock","sceneInstanceId":"identity returned by create"}]}. ABI1 remains compatible and sqsm_map_bytes_free releases output. The complete C example, header and native script types come from the same core commit. Source attachments are not formal binary releases.
This complete HTML includes all six button handlers, HTML/CSS, time queries, layers/credits, errors and cleanup. Run the complete page or download the shared script. Keep the example in the complete SDK distribution root, or replace its import URL with your own complete SDK URL.
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>SqsMap scene clock</title>
<style>html,body{margin:0;height:100%;font:14px system-ui}#map{height:100%}#panel{position:absolute;top:12px;left:12px;background:white;padding:12px;border-radius:8px}button{margin:3px}#credits{position:absolute;bottom:8px;right:8px;background:#fff;padding:4px}#time{font-size:20px;font-weight:bold}#error{color:#a00;max-width:300px}</style>
<div id="map"></div><div id="panel"><div id="time">Loading</div><div id="controls"></div><div id="error" role="status"></div></div><div id="credits"></div>
<script type="module">
import {MapView} from './dist/index.js';
const map=await MapView.create(document.querySelector('#map')),time=document.querySelector('#time'),error=document.querySelector('#error'),controls=document.querySelector('#controls');
map.jumpTo({center:[0,0],zoom:12});map.addSource('point',{type:'geojson',attribution:'Synthetic example data',data:{type:'FeatureCollection',features:[{type:'Feature',id:1,properties:{},geometry:{type:'Point',coordinates:[0,0]}}]}}).addLayer({id:'point',type:'circle',source:'point',layout:{visibility:'none'},paint:{'circle-color':'#f33','circle-radius':12}});await map.flush();
let scene,alive=true,busy=false,frame=0,reading=false;const buttons={};
function updateButtons(state){for(const [action,button] of Object.entries(buttons))button.disabled=busy||({create:!!scene,start:state!=='draft',pause:state!=='running',resume:state!=='paused',stop:!scene||state==='stopped',remove:!scene}[action]);}
async function create(){scene=await map.sqs.scenes.create({id:'clock'});await scene.attachSource(await map.getSource('point'));}
for(const action of ['create','start','pause','resume','stop','remove']){
const button=document.createElement('button');button.textContent=action;buttons[action]=button;controls.append(button);
button.onclick=async()=>{busy=true;error.textContent='';updateButtons('');try{if(action==='create')await create();else if(action==='remove'){await map.sqs.scenes.remove(scene);scene=undefined;}else await scene[action]();}catch(e){error.textContent=e.message;}finally{busy=false;await observe();}};
}
const creditSub=map.onAttributionsChange(e=>document.querySelector('#credits').textContent=e.attributions.text);
const sceneSub=map.on('sqs.scenechange',e=>console.log('Scene lifecycle',e));
async function observe(){if(reading||!alive)return;reading=true;try{const clock=scene?await scene.getClock():undefined;if(!alive)return;time.textContent=clock?clock.state+' '+(clock.elapsedMs/1000).toFixed(3)+' s':'No scene';updateButtons(clock?.state);document.querySelector('#credits').textContent=(await map.getAttributions()).text;}catch(e){if(alive)error.textContent=e.message;}finally{reading=false;}}
function tick(){void observe();if(alive)frame=requestAnimationFrame(tick);}
await create();map.setLayoutProperty('point','visibility','visible');await map.flush();await observe();tick();
addEventListener('pagehide',()=>{alive=false;cancelAnimationFrame(frame);creditSub();sceneSub();map.destroy();},{once:true});
</script></html>