Skip to main content

Shared line playback and style tracks

The shared map.sqs.animations.line(options, {owner: scene}) facade is available in browser main-thread and map-Worker hosts, retained native QuickJS and the desktop script service. Controllers belong to the map and survive script-session exit. Applications provide their own controls and layout. Sources and layers use MapLibre-style syntax; scene ownership and line playback are SqsMap extensions, not an implementation of MapLibre APIs.

The shared map.sqs.animations.line(options, {owner: scene}) facade is available in browser main-thread and map-Worker hosts, retained native QuickJS and the desktop script service. Controllers belong to the map and survive script-session exit. Applications provide their own controls and layout. Sources and layers use MapLibre-style syntax; scene ownership and line playback are SqsMap extensions, not an implementation of MapLibre APIs.

Development SDK 0.3.0-dev.1691a622, core 1691a622810a8bd61c7b2bfaacc2855e2522e37f, script host 0.1.0-dev.15.6b2, contract 0.5-session, ABI 1. Check sceneLinePlayback and sceneTracks; missing/false means unavailable. All six effects and tracks passed software WebGL2/Vulkan acceptance. Mobile devices, external Windows/macOS machines, hardware WebGPU and formal releases require separate acceptance.

Sources, ownership and identity

Register a loaded, static, non-clustered GeoJSON source with lineMetrics:true, then attach it using scene.attachSource(source). Source geometries must be LineString/MultiLineString, optionally wrapped in Feature/FeatureCollection. The layer must have type line and two-dimensional longitude/latitude coordinates. Absolute latitude is bounded to85.0511287798066 and absolute longitude to1000000 to allow explicit world copies. Only planar maps are supported, not globe or 3D lines. The layer option accepts a name or an {id,layerInstanceId} identity. map.getLayer returns a style specification. Script map.sqs.getLayers() and SDK map.getStyleLayers() supply state snapshots with identities. Names bind the current instance at invocation; recreated or foreign-map identities cannot reuse old controllers. Mutable settings are captured before asynchronous lookup.

The core validates source, layer and image instances and revisions. External geometry, feature-state, paint or image changes suppress old overlays and report stale/conflicting captures without overwriting user edits. Ordinary samples update material only and retain geometry caches and original style. Draft scenes prepare controllers; start the owner scene to display them.

Timing, effects and tracks

Duration and delay are milliseconds, bounded respectively to 1..86400000 and 0..86400000. Loop defaults to false. Direction accepts forward/reverse/alternate; a complete alternate round lasts twice duration. Easing accepts linear/ease-in-out. Fill accepts forwards/none and defaults to retaining the endpoint.

Effects contain 1..6 distinct types: flow (spacing, trailLength, fade, phase), dash (required on/gap), reveal (start/end), arrows (spacing/length), glow (radius/intensity), and pattern (required image). Flow/dash/arrows/pattern support progress/pixels units. Pattern resolves a caller-registered map.addImage name, not a file path or style-sprite alias; SDF images are unsupported. Cap accepts butt/round; stagger, phaseField and speedField control per-path phase/speed.

Color tracks use #RRGGBB or #RRGGBBAA, opacity is 0..1, widthScale is 0..8. Each nonempty track has 2..32 {offset,value} keyframes, strictly increasing from0 to1. Shared sampling linearly interpolates these values; the resulting material also applies the original layer color, width and opacity.

Controls, events and release

Controllers expose getState/play/pause/stop/seek/finished; generic lookup uses map.sqs.playbacks.get/list/remove. Snapshots discriminate kind=route/line. Line snapshots include frame, layer, sourceStateRevision, visibleEffect and optional pattern identity. Nested colors, identities and errors are immutable. visibleEffect describes a retained overlay; actual visibility additionally depends on the layer and owner scene.

Manual pause and owner pause are independent. Resuming an owner does not clear manual pause. Seeking a paused controller to its endpoint completes only after playback resumes. finished waits for the accepted endpoint of the current epoch. Loops require explicit cancellation. Stop, replay, removal or map destruction reject waits with CANCELLED. Conflicting external edits report CONFLICT; deleted or recreated resource instances report STALE_HANDLE. One-shot scripts cannot wait for future notices and require a retained session, while an already completed round resolves immediately. Subscribe to sqs.playbackchange for historical notifications without persistent map badges.

Budgets: 64 controllers/map, 128 notices/pump, 32 line materials/256MiB, 4096 paths/100000 points per controller. Limits are checked before allocation/commit. Removing controllers/scenes releases overlays and restores caller-owned registered layers. Sources, layers and images remain caller-owned and need separate removal. Image changes invalidate captured patterns.

Shared script

Use this code in a retained script session or desktop debugger; the browser script runner accepts the same code. A one-shot native script can omit finished and let the host rendering loop advance the retained map.

// @gis-api 0.5-session
// @permissions ["project.read","map.write"]
import {map} from 'sqsmap';
map.addSource('line-demo',{type:'geojson',lineMetrics:true,data:{type:'LineString',coordinates:[[-.006,0],[0,.003],[.006,0]]}});
map.addLayer({id:'line-demo',type:'line',source:'line-demo',paint:{'line-color':'#ffffff','line-width':8}});
await map.sqs.flush();
const scene=await map.sqs.scenes.create({id:'line-demo'});await scene.attachSource(await map.getSource('line-demo'));
const playback=await map.sqs.animations.line({id:'line-demo',layer:'line-demo',timing:{duration:6000},effects:[{type:'flow'}],tracks:{color:[{offset:0,value:'#ff0000'},{offset:1,value:'#0000ff'}]}},{owner:scene});
map.on('sqs.playbackchange',event=>console.log(event.playback));await scene.start();
console.log(await playback.finished());
// Explicit cleanup: await map.sqs.scenes.remove(scene).
// Registered sources/layers remain caller-owned; remove them separately if needed.

Complete HTML

Includes all buttons, effect selection, events, CSS and destruction. Use matching development SDK files. Public URLs require deployment; local acceptance uses ?sdk=local.

<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SqsMap line playback</title><style>body{margin:0;font:15px system-ui;color:#182435}#map{height:75vh;min-height:320px}nav{display:flex;flex-wrap:wrap;gap:8px;padding:12px}button,input,select{font:inherit}pre{padding:12px;white-space:pre-wrap}small{display:block;padding:0 12px}</style><div id="map"></div><nav><label>Effect <select id="effect"><option>flow</option><option>dash</option><option>reveal</option><option>arrows</option><option>glow</option><option>pattern</option></select></label><button id="play">Play / replay</button><button id="pause">Pause</button><button id="stop">Stop</button><label>Seek <input id="seek" type="range" min="0" max="6000" step="100" value="0"></label></nav><small>Synthetic example data · No external basemap or imagery is used.</small><pre id="log"></pre><script type="module">
const local=location.hostname==='127.0.0.1'&&new URLSearchParams(location.search).get('sdk')==='local';
const {MapView}=await import(local?'./dist/index.js':'https://www.sqsmap.com/sdk/sqsmap-core/dist/index.js');
const map=await MapView.create(document.querySelector('#map')),log=document.querySelector('#log');map.jumpTo({center:[0,0],zoom:12});
map.addSource('line',{type:'geojson',lineMetrics:true,attribution:'Synthetic example data',data:{type:'LineString',coordinates:[[-.006,0],[0,.003],[.006,0]]}}).addLayer({id:'line',type:'line',source:'line',paint:{'line-color':'#ffffff','line-width':8}});
map.addImage('pattern',{width:2,height:1,data:[255,255,255,255,255,255,255,255]});await map.flush();
const scene=await map.sqs.scenes.create({id:'line-example'});await scene.attachSource(await map.getSource('line'));
let playback,closing=false,tail=Promise.resolve();const effects={flow:{type:'flow'},dash:{type:'dash',on:.2,gap:.1},reveal:{type:'reveal'},arrows:{type:'arrows'},glow:{type:'glow'},pattern:{type:'pattern',image:'pattern'}};
const show=value=>{if(!closing)log.textContent=JSON.stringify(value,null,2);};
const wait=p=>p.finished().then(show,error=>{if(!closing)log.textContent=error.code+': '+error.message;});
const off=map.on('sqs.playbackchange',event=>{if(event.playback.playbackInstanceId===playback?.playbackInstanceId)show(event.playback);});
function action(run){tail=tail.then(async()=>{if(closing)return;try{await run();show(await playback.getState());}catch(error){if(!closing)log.textContent=error.code+': '+error.message;}});}
async function selectEffect(){if(playback)await map.sqs.playbacks.remove(playback);playback=await map.sqs.animations.line({id:'effect',layer:'line',timing:{duration:6000},effects:[effects[document.querySelector('#effect').value]],tracks:{color:[{offset:0,value:'#ff0000'},{offset:1,value:'#0000ff'}],widthScale:[{offset:0,value:1},{offset:1,value:2}]}},{owner:scene});await playback.play();wait(playback);}
document.querySelector('#effect').onchange=()=>action(selectEffect);
document.querySelector('#play').onclick=()=>action(async()=>{await playback.play();wait(playback);});
document.querySelector('#pause').onclick=()=>action(()=>playback.pause());document.querySelector('#stop').onclick=()=>action(()=>playback.stop());
document.querySelector('#seek').onchange=event=>{const time=Number(event.target.value);action(()=>playback.seek(time));};
await scene.start();await selectEffect();show(await playback.getState());
window.addEventListener('pagehide',()=>{closing=true;off();map.destroy();},{once:true});
</script></html>

HTML · Script · Types