Русский · English · Español · 中文 · Português

«Space Now» — agent API

This page is for AI agents (and their humans). It explains how an agent connects to a live «Space Now» page (known.html) in a person's browser and what it can do there: everything the interface can do — open any object, fly the camera, scrub time, switch layers and filters, astronaut mode, find eclipses and planet parades, tell a story with voice and captions, show its own images, record video, read errors. Rendering happens in the person's browser (WebGL on their GPU); the agent sends commands and gets answers and screenshots.

1. Pairing

  1. The person opens the site and presses the round 🤖 button (bottom-right, above the astronaut) — the agent bar opens; «Turn the agent on» shows a code (20 characters, e.g. r4fxjbjsxdy6ex3pf4hd). The code is the pass: whoever has it controls this tab. 🤖 only hides/shows the bar, the connection stays; «Disconnect» in the bar turns it off. The bar always has 📂 (open a story file or save the shown one — without an agent too) and «MCP ↗» (this page).
  2. The person gives the code to the agent. The agent calls connect with it.
  3. Permanent connection (optional): on the page — 📌 «Remember» in the agent bar (the code stays in this browser, the site connects by itself every time it opens); the agent — connect with save: true (code in ~/.orbit-agent.json, MCP connects on start). After that, just open the site. To forget: unpin 📌 and call forget.
  4. «Disconnect» on the page (or the agent leaving) ends the session; the page then returns layers and interface to how they were before the agent, stops music, voice, captions, images and scenarios, and removes landing sites (sites), drawings and marks (draw, mark, flybys), media windows and own bodies (spawn) — regardless of onstop. The camera, time and selection come back too (by default; onstop {restore: false} — keep what the agent showed). Exception: play / story run with detach: true — the show plays to the end, then rolls back.

Sound (voice, music) starts only after the person has clicked the page at least once — browser rule. The voice is off by default: the person turns it on with 🔊 in the agent bar (remembered); captions are always shown.

Show checklist — always do this

[
  {"cmd":"onstop","args":{"restore":true}},        // first step: on end, Esc or ✕ everything comes back
  {"cmd":"reset"},
  {"cmd":"view","args":{"on":["oplanets"]}},        // only the layers you need: satellites, craft, orbits, constellations off
  {"cmd":"hideUi","args":{"on":true,"labels":false}},
  {"cmd":"time","args":{"at":"<moment>","rate":0}}, // set the time yourself; rate 0 — the lighting won't drift
  … flight (fly) …
  … the show …
]

2. Transport

Both sides connect to a relay room on the same site: wss://<site>/agent/<code>?r=agent (the page uses ?r=page). The room forwards messages between the two; nothing is stored. JSON messages:

agent → page   { "id": 1, "cmd": "go", "args": { "search": "?o=Mars" } }
page  → agent  { "id": 1, "ok": true, "result": … }   |   { "id": 1, "ok": false, "error": "…" }
room  → both   { "sys": "peer", "on": true|false }      (the other side joined / left)
keepalive      send { "sys": "ping" } every 30 s, the room answers { "sys": "pong" }

One connection per role (a new agent with the same code replaces the old one, close code 4000 — do not reconnect then). Messages up to 4 MB. Sites: https://space.bl2.ru, https://space.bl2.net (same site, ru/en by default).

3. MCP server

Ready-made MCP server (stdio): scripts/agent-mcp.mjs in the orbit-proto repository (Node ≥ 20, dependency ws). Every command below is a tool of the same name (hide_ui → page command hideUi), plus connect, disconnect, help (this page as text; lang: en). Files: image takes path, record saves to path, screenshot returns an image.

{ "mcpServers": { "orbit-space": { "command": "node", "args": ["scripts/agent-mcp.mjs"] } } }

Env: AGENT_SITE — default site, AGENT_CODE — connect at start.

Connecting without the MCP server (cloud agents)

// «start and return» — from the console of a tab on space.bl2.ru (or Node ≥ 22)
const code = '<code>', ws = new WebSocket(`wss://space.bl2.ru/agent/${code}?r=agent`);
let id = 0; const wait = new Map();
const cmd = (name, args = {}, ms = 8000) => new Promise((res, rej) => {
  const i = ++id, t = setTimeout(() => (wait.delete(i), rej(new Error('timeout ' + name))), ms);
  wait.set(i, (m) => (clearTimeout(t), m.ok ? res(m.result) : rej(new Error(m.error))));
  ws.send(JSON.stringify({ id: i, cmd: name, args }));
});
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (!m.sys) wait.get(m.id)?.(m), wait.delete(m.id); };
ws.onopen = async () => {
  await cmd('onstop', { restore: true });
  await cmd('play', { detach: true, onError: 'skip', steps: [
    { cmd: 'reset' },
    { cmd: 'view', args: { on: ['oplanets'] } },
    { cmd: 'hideUi', args: { on: true, labels: false } },
    { cmd: 'fly', args: { name: 'Saturn', size: 0.5, lit: true }, wait: 6500 },
    { cmd: 'caption', args: { title: 'Saturn', sub: '1.4 billion km from the Sun' }, wait: 8000 },
  ] });
  ws.close(); // the show plays on by itself, then puts everything back
};

4. Commands

Look and state

cmdargswhat
statetime, rate, selected object, view centre, camera (yaw, pitch, dist km, fov), star nearby, keys of visible layers, canvas size px
screenshotwidth, quality, fullJPEG of the 3D scene. full: true — whole tab with interface (the person allows tab capture once via «📷» in the agent bar)
bodynamefor framing: radius km, kind, parent, moons, camera angles, sunYaw/sunPitch (camera direction from which the body is fully lit), fov, distForSize {0.25, 0.5, 0.66, 0.8 → km}: distance at which the disc fills that share of screen height (= radius / tan(fov/2) / k)
wherename, atposition at a moment: km and AU from Sun and Earth, RA/Dec from Earth (J2000)
logslevel, last, clearpage console errors/warnings/info (ring buffer 300)
snapshotfull state (time, rate, camera, centre, selection, all layer checkboxes, interface hidden, astronaut link) — keep it to reproduce a bug
restorestatereturn to a snapshot

View, camera, time, layers

cmdargswhat
searchq, limitsearch the object base like the search field: bodies, moons, spacecraft, nebulae and galaxies (when their layer is on), Earth satellites (name or NORAD), asteroids (name or number; the catalogue loads via the load-asteroids row). Returns rows with n
opennopen a search result: select, card, flight — like clicking the row
gosearchopen a view like a share link: ?o= body (Mars, Марс, ISS…), ?a= asteroid (number or name), ?s= satellite NORAD, ?g= galaxy, ?n= nebula, ?st= constellation star; extras d= camera km, v=yaw,pitch, t= ISO time
flyname, size, lit, face, secondssmooth flight to a body (like a click in the tree). size — share of screen height for the body (0.66 — two thirds), lit: true — from the lit side; framing takes ~2.5 s more after the flight (~6 s total, use wait ≥ 6500). face: "spot" — Jupiter: time rewinds until the Great Red Spot is on the day side
camerayaw, pitch, dist, mscamera around the view centre (rad, km). ms > 0 — move smoothly over ms. Example «fill 2/3 of the screen, lit side»: body → camera {yaw: sunYaw+0.4, pitch: sunPitch, dist: distForSize[0.66], ms: 3000}
timeat, rate, live, from, tofrom, to, ms, ease — smooth scrub from date to date; at — Unix seconds or ISO moment (BC: -000500-01-01), speed (1 real, 3600 = 1 h/s, negative = back, 0 pause), live: true
viewon, set, orbitsview — standard keys for every toggle. No args — list {key, on}; on: [...] — switch everything off and only the listed on; set: {key: true|false} — switch only these; orbits: false — all orbit lines at once. Keys (every group also has <key>:orbits — its orbit lines): oplanets — Sun, planets and moons; odwarfs — dwarf planets and the largest asteroids; ohelio — heliopause; oshock — termination shock; osandbox — own bodies (spawn); oknown-small — known small ones (Apophis, Bennu…); ocomets — comets; ointerstellar — interstellar objects (1I, 2I, 3I); okuiper — Kuiper belt; ooort — Oort cloud; a0…a9 — asteroids by size (a0 — over 100 km); ocraft, ocraft-orbit, ocraft-l — spacecraft in flight, in orbit around bodies, at Lagrange points; s0…s30 — Earth satellite groups, sats — all at once; ogalaxy — Milky Way model; ouniverse — galaxies; stars — constellation stars with names; conlines — constellation figure lines (with stars; not listed in view {on} — no lines); galmarks — Galactic centre and apex labels; galsun — «Sun — you are here»; scenelabels — labels only on what the scene needs (labels {only}). A single interstellar path: switch ointerstellar:orbits off, select {name: '3I/ATLAS'} — its path bright. Also: earth:ph|cities|seas|mountains|volcanoes, weather, aurora (aurora), sw (space weather), sk:0…6, sk:neb. Stories and slides take an on field (applied before the steps): list only what is needed — new layers that are on by default will not show up in old stories
layersall layer groups: key, name, visible, orbits, count
starname | hip | gaia | index, flya star from the known-stars database (Gaia + Hipparcos): a famous one by name (Sirius, Proxima…), by Hipparcos number (hip) or Gaia DR3 id (gaia, as a string). Flies there by default (fly: false — card only); up close it is a sphere, the card shows the spectrum. The first call downloads the database (a few MB). The dots of the other stars are the ostarsdb layer
layerkey, visible, orbitsswitch a layer. Keys: oplanets, odwarfs, ocomets, ocraft*, ogalaxy, ouniverse, ostarsdb — known-stars database (Gaia + Hipparcos, ~470k; the first switch-on downloads the data and the reply comes once it is loaded; warm it up with warm {code:"starsdb:"}), a0…a9 asteroids by size, s0… satellite groups…
galaxyfarview of the Milky Way (far — with other galaxies). For scenes: dist (0 — model at the Sun, > 0 — fly out), pitch, center: true — orbit around the Galactic centre, off: true; Galaxy time — galtime {rate} (years/s)
galaxiescount, dwellfly through N famous galaxies by distance, dwell seconds each
cleardeselect, close card

Events (computed by the page, no formulas needed)

cmdargswhat
findkind, from, to, total, count, minlunar (total: true — «blood moon»), solar, parade (min planets 3…7) from any date; returns mid time, total?, lat/lon/region where visible; lunar — contact (umbra touch)
showkind, at, keepViewshow an event like «📅 Events»: time — half an hour before mid, speed ×100 (set time {rate: 0} or your own), selection cleared, view centre and camera on the Earth (solar: above the shadow point) or the Moon (lunar); a parade — first-person view from the Earth, planets only. Layers set with view stay (keepView: false — the event's). After it: hideUi, camera if needed

Whole interface

cmdargswhat
uitext, withinevery visible button, checkbox, slider, field, list/tree row with ref, text, id, data-attributes, state. Filter by text or CSS area
clickref | selectorpress it
setref | selector, valuecheckbox true/false, slider, field, select
keycode, ms, shift, ctrlkey press (KeyW… hold ms to fly, Space pause, KeyH hide panels, Escape…)
pointeraction, x, y, to, dymouse on the 3D scene: click (select), dblclick (fly to), move (hover), drag (rotate) to [x,y], wheel (dy zoom)
hideUion, labelshide everything: panels, corner buttons, the agent bar (labels: false — body labels too); only the scene, caption, images, media windows, marks (mark) and drawings of the agent stay. on: false — back
astroaction, nameastronaut: enter (where the camera is), goto name (fly there, orbit), land (stand on the ground), exit, link, state. Walk and look — key/pointer
evalcodedev server only (npm run dev): run a JS expression in the page; window.__known holds state. Not available on the public site

Telling a story

cmdargswhat
captiontitle, sub, text, secondscaption bottom-left like the screensaver (empty title — hide)
saytext, title, sub, audio, keep, captionshow the text as a caption; audio — an mp3 voice-over (link or a file name on the computer; no browser voice — make the mp3 beforehand, e.g. edge-tts); 🔇 — silent
musicon, theme, volumebackground music generated in the browser: theme 0 «Nebula», 1 «Depth», 2 «Crystals», null — rotate
imageid, src | path, at, x, y, dx, dy, w, label, remove, clearyour picture over the scene: attached to a body (follows it) or at a screen point
playsteps | stop | pause | seek | stepa whole scenario played by the page: [{cmd, args, wait}], wait = ms or "speech". Like a player: pause, seek: n — jump to step n (earlier steps run at once, without waits or voice), step: ±1. The person gets a player at the bottom: ⏮ ⏪ ▶/⏸ ⏹ ⏩ ⏭, «slide · step», 💾 save on the site (password), ⬇ download, 🗑 delete, ✕. Stops on disconnect — except with detach: true (plays to the end, then rolls back). onError: 'skip' (default — go on) | 'stop'; a failing step goes to logs, to play without args (lastError) and to the player. A presentation moves to the next slide by itself; ✕ or Esc — exit (the interface comes back)
deckslides, start, go, closepresentation: [{title, steps}]; the person gets «◀ n/N ▶» at the bottom (and PageUp/PageDown), moving plays that slide’s steps. Stays visible with hideUi
buttonsitemsyour own buttons at the bottom: [{label, title, steps}], a click plays the steps; [] removes
storyaction, story, id, password, namea story = what the agent composed (flight plan, narration, visualisation — e.g. for NASA Space Apps): {title, summary, lang, slides:[{title, steps}] | steps}. run — show; publish — save on the site with an edit password (password; none — the page asks the person) → link known.html?story=<uuid>, anyone views it without an agent (with «◀ n/N ▶»); up to 256 KB, kept 30 days after the last opening; update — edit (id + the same password); delete — delete from the site (id + the same password); download — a file for the person (any size); open — the person picks a file (or drops a .json on the page; «📂» menu in the agent bar); load — by id
stepswhere we are in a story or presentation: slide and slide list, step and step list (n, label), playing, paused. Jump — play {seek: n} / {step: ±1}, deck {go: n}. The person has ☰ in the player — the list of slides and steps, click to jump
scenecodean ambient-mode scene inside a story — its camera flight, caption and time (~20–30 s, waits for the end). Codes — scenes: body:Сатурн, event:<id>, mission:…, astro:…, lapse:…, galaxy, pullback. The ambient mode («∞») itself is the same scene generator in the common player: ⏮ ▶/⏸ ⏭, ☰ played and all scenes, 💾/⬇ — what was played as a story of scene steps
scenesall ambient scenes by section, with codes
recordaction, fps, mbps / pathvideo of the scene: start, stop (the MCP saves WebM to path)

One entry point: WS, MCP, console, story

Commands and the language are the same wherever they come from: WebSocket from any language, MCP, the page console (the >_ button in the agent bar) or a story. Write a whole scenario and give it to story, or call commands one by one. New: exec {steps} — run language steps now (variables persist between calls), expr {e} — evaluate an expression, js {code, timeout} — JavaScript in a sandbox (isolated iframe + Web Worker: no DOM, cookies or network; inside — await cmd(name, args), await expr('…'), log(), sleep(), return).

Console: fly Mars size=0.6 (a command, the first word is name), a JSON command, steps {…} / […], an expression = …, code js ….

Base script — Node.js:

// node agent.mjs <код>   (Node ≥ 22 — WebSocket встроен)
const code = process.argv[2], ws = new WebSocket(`wss://space.bl2.ru/agent/${code}?r=agent`);
let id = 0; const wait = new Map();
const cmd = (name, args = {}) => new Promise((res, rej) => {
  const i = ++id; wait.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, cmd: name, args }));
});
ws.onmessage = (e) => {
  const m = JSON.parse(e.data);
  if (m.sys) return;                                   // peer / pong
  const w = wait.get(m.id); wait.delete(m.id);
  m.ok ? w.res(m.result) : w.rej(new Error(m.error));
};
setInterval(() => ws.send('{"sys":"ping"}'), 30000);
ws.onopen = async () => {
  await cmd('fly', { name: 'Mars', size: 0.6, lit: true });
  console.log(await cmd('expr', { e: "round(au(get('Mars','sun')),3)" }));
  await cmd('exec', { steps: [{ wait: "dist('camera','Mars') < 1e5 km" }, { cmd: 'caption', args: { title: 'Mars' } }] });
  process.exit(0);
};

Python:

# python agent.py <код>    (pip install websockets)
import asyncio, json, sys, websockets
async def main(code):
    async with websockets.connect(f"wss://space.bl2.ru/agent/{code}?r=agent") as ws:
        n = 0
        async def cmd(name, **args):
            nonlocal n; n += 1
            await ws.send(json.dumps({"id": n, "cmd": name, "args": args}))
            while True:
                m = json.loads(await ws.recv())
                if m.get("id") == n:
                    if not m["ok"]: raise RuntimeError(m["error"])
                    return m["result"]
        await cmd("fly", name="Saturn", size=0.6, lit=True)
        print(await cmd("expr", e="get('Saturn','moons')"))
asyncio.run(main(sys.argv[1]))

Scenario language v2

Versions: a story without v (or v: 1) — steps and slides as above; v: 2 — a language with conditions. A new handler version never changes old ones.

{ "v": 2, "title": "…", "on": ["oplanets"], "vars": { "n": 0 }, "steps": [
  { "cmd": "fly", "args": { "name": "Mars", "size": 0.5, "lit": true } },
  { "wait": "dist('camera', 'Mars') < 50000 km", "timeout": 20 },
  { "set": { "n": "n + 1" } },
  { "if": "visible('Mars')", "then": [ { "cmd": "caption", "args": { "title": "Mars", "sub": "{round(au(dist('camera','Sun')),3)} AU from the Sun" } } ] },
  { "repeat": 2, "do": [ { "wait": 1 } ] }
] }

Steps: {cmd, args, wait} (wait — ms or an expression), {wait: expression | seconds, timeout}, {if, then, else}, {repeat: n | expression, do}, {set: {name: expression}}, {on: [...]}. Expressions — no eval: numbers with units km au ly m, strings '…', variables, + - * / %, < > <= >= == !=, && || !; functions dist(a, b) ('camera' or a body, km), visible(body), elapsed(), time(), camdist(), au(x), ly(x), round(x, d), abs min max. Properties on the fly: get(body | 'camera', 'prop') — a body has name, kind, radius, parent, moons, x y z, sun (km from the Sun), earth, camera (km from the camera), center, visible; the camera has dist, yaw, pitch, fov, x y z, sun, center; without a prop — the whole object. A command result into a variable: {cmd, args, as: 'r'}, a field — field(r, 'rows.0.text'), size — count(x). In args strings — {expression}. Errors go to logs. Next: triggers (when/once/every) and a sandbox (spawn bodies and 3D models). Own functions: {def: name, params, e} — in expressions, {def, params, steps} — as a step {call, args, as}, {local}, {return}; in a story — funcs. More functions: angle(a, b) — degrees between directions from the camera, edge(a, b) — how many degrees a shows past the limb of b, sin cos (degrees), pow sqrt. wait: true on a command — until it finishes. For scenes: fitdist(r[, margin]) — the distance from which a circle of radius r km fits the screen (narrow side; a body also has day — rotation period, s); satcount('all' | 'active' | 'debris' | 'rb' | 'dead') — catalogue objects around Earth; mstat(craft, 'launch' | 'km' | 'turns' | 'rmin' | 'rnow' | 'flybys') — mission numbers from its trajectory, as of today. Spacecraft trajectories and flybys come from the import (scripts/fetch-missions.mjs, JPL Horizons, daily): a new craft in the base gets its flybys and numbers automatically; flybys {craft} draws the flyby points on its path. Media in scenes: media {url | file, title, x, y, w, h, volume, loop, start, id} — a window with audio or video: an mp3/video link or YouTube; file — a file from the person's computer (the site stores no files: the person picks it in the window, then it plays offline); close: true closes the windows. State: remember {name} — save the camera, time, view and interface; recall {name} — bring it back; onstop {restore: true} (in a story file — the field "onstop": "restore") — on end, Esc or ✕ put everything back as before the start. Tell the person: stories and the screensaver can be driven by a remote — a presentation clicker (PageDown/PageUp, «.» or B — black screen), media keys on keyboards, headphones and TV boxes (next, previous, pause), a gamepad (D-pad ←/→, A — pause, B — black screen), keyboard arrows, Space, Esc. Direct links: ?ambient — the screensaver, ?ambient=<scene code from ☰> — starting from that scene (e.g. ?ambient=landings:Moon:apollo), ?story=<id>&step=N or &slide=N — a published story from that point; the 🔗 button in the player copies a link to what is playing. The ambient {play: code} command does the same from an agent; reset resets camera, time and view before a scene.

Camera and motion

cmdargswhat
flyname, ms, ease, size, alt, lat, lon, rel, litOne-motion flight straight into the framing: size — share of screen height for the disk, alt — km above the surface, lat/lon — above a surface point, rel: {to, deg, elev} — side relative to another object (0° — it is behind the camera, 180° — behind the body in frame; elev — raise). Returns ms.
cameradist, yaw, pitch, rel, ms, ease, targetCamera around the view centre; rel as in fly. target: {between: [a, b], k} — look at a point between bodies; {sat: 25544} — orbit around a satellite (NORAD); {sat, space: {tilt, spin, from, ms}} — view from space (Earth behind the satellite): tilt° from vertical, spin °/s, approach from from km to dist over ms; null — back to the body.
mediaurl | file, kind, title, x, y, w, h, volume, loop, start, id, anchor, dx, dy, header, close, forgetDirA window with audio or video: a link (mp3/video/YouTube) or file — a file on the computer (the person picks it). anchor: {sat: NORAD} | {body: name} — the window follows an object (dx, dy — offset, px); header: false — no title bar (default when anchored). Returns ms for wait: true. close: true | id; forgetDir: true — forget the media folder. Before a story starts — the «Preparing the scene» window (files, link preload).
ambientplayScreensaver «∞» from a given scene: code as in ☰ (hover|Mars, landings:Moon:apollo, event:eclSun, mission:Parker, satellites…); without play — the list of codes.
langsetInterface language: without set — the current one and the list (ru, en, es, zh, pt); set — switch. Switches live, without a reload: the agent stays connected. The screensaver replays the current scene in the new language; the player, the card, the layer tree and the windows are redrawn. The page address becomes /<lang>/ (for example /es/; the domain’s own language is just /) — the language is set by the address only, there is no ?lang=; a link to the site in a given language is https://space.bl2.net/<lang>/ (Russian — https://space.bl2.ru/).
reset—Before a scene: stop orbit, look and time ramps; Galaxy off; marks, flyby points and media windows removed (screensaver scenes do it themselves).
remember / recallnameSave the state (camera, time, view, interface) / bring it back.
onstoprestoreOn by default: on end, Esc, ✕ or the agent leaving, put everything back as before the agent's first command: layers and view toggles, panels and hidden interface, camera (angle, distance, centre, tracking), selection, time and speed (or «live»); music and voice stop. The agent's own things (sites, draw, mark, flybys, image, media, spawn) are always removed. restore: false (in a story file — "onstop": "keep") — keep the camera, time and selection as the agent left them. exec rolls back at its end only with an explicit onstop {restore: true}.
consoleopenOpen / close the >_ console.
sitesbody | code, group, ids, light, onLanding sites: cards at surface points (in the scene). body may be omitted — the body comes from the group (apollo, luna, rovers, new, farside, first, china — Moon; mars, chryse — Mars; venera — Venus) or from ids. Example: sites {group: 'apollo', light: true} → {lat, lon, alt, at, ids}; then time {at, rate: 0} and fly {name: 'Moon', lat, lon, alt}. Without flying to lat/lon the camera may face the far side — no cards visible. light: true — a time when the sites are in daylight (reply at).
flybyscraft, onGravity assist and orbit insertion points on a craft's path (from the mission trajectory).
orbitspeed (°/s), turns | ms, pitch, dist, end, blend, ease, stopOrbit around the body. end: {to, deg, elev} — finish exactly on that framing (one direction, no going back). Ending at speed — it coasts, the next fly takes the speed over.
lookto, ms, ease, clearLook at an object: the eye stays (or keeps orbiting), the view turns smoothly and tracks it. clear: true — back to the centre.
select, shadow, camera targetname; name, by, center, ms, dist; targetFor screensaver scenes: select {name} — highlight a body's path ('' — clear); shadow {name, by, center, ms, dist} — camera on the eclipse shadow spot; camera {target: {between: [a, b], k}, ms} — look-at point between bodies (orbit circles it), target: null — back; look {to: 'galcenter'}; fly {frame: {path, from, to, with}, elev, deg} — frame a craft's whole path.
followname, ms, dist, easeFollow another object: smooth hand-over keeping the framing.

Showing a small body (a comet, an interstellar object — invisible from tens of millions of km): follow {name} or fly {name}, then mark {name, label: true} — a blinking multicoloured marker at the body (visible with hideUi too; also color, speed), select {name} — its path bright. Remove: mark {remove: name} or mark {clear: true}, select {name: ''}; the onstop rollback and the agent leaving remove marks by themselves.

Easing ease — like a CSS timing function: linear, ease, ease-in, ease-out, ease-in-out, smooth, cubic-bezier(x1,y1,x2,y2) or a formula of t (0…1): 1-pow(1-t,2), t*t*t*(t*(6*t-15)+10).

Overlap: {cmd, args, then: [steps], before_end: ms | after_begin: ms | when: "condition"} — nested steps run alongside the command. Example: orbit the Earth, turn to Io, fly to it:

[ {"cmd": "fly", "args": {"name": "Earth", "size": 0.4, "rel": {"to": "sun", "deg": 0, "elev": 10}, "ms": 3000, "ease": "ease-in-out"}, "wait": true},
  {"cmd": "orbit", "args": {"speed": 60, "ms": 6000, "ease": "ease-in"},
   "when": "edge('Io','Earth') < 15", "then": [{"cmd": "look", "args": {"to": "Io", "ms": 1500}, "wait": true}], "wait": true},
  {"cmd": "fly", "args": {"name": "Io", "size": 0.15, "rel": {"to": "Jupiter", "deg": 180, "elev": 15}, "ms": 8000, "ease": "ease-out"}, "wait": true} ]

Sandbox, drawing, astronaut

cmdargswhat
spawnid, like, diameter, color, at: {near, dist, dir} | pos, velYour own object: a copy of a body or a rock; it takes a circular orbit around the dominant body (physics, not straight lines)
moveid, vel | impact: {target, at}New velocity or «hit a body at a moment»
despawn / objectsid | allRemove / list
drawitems | clear | removeGraphics in space for debugging and lessons: arrow (vel, sun, north, {sum}), axes (roll/pitch/yaw), line, dash, path, angle (arc and degrees), project (projection onto a body, altitude), point, text; label — text along the line
flybyscraft, onGravity-assist points on a spacecraft's path: «Body · date» and what the assist gave; on: false — remove
sitesbody | code, group, ids, light, onLanding sites (Moon, Mars, Venus, Titan): points and in-scene cards — photo, name, year, description; group — combined view (apollo, luna, rovers, new, farside, first, china, mars, chryse, venera; the body follows from the group, body optional); light: true — sites on the day side; returns lat, lon, alt for fly; on: false — remove
astroaction: goto | view | shield | land | exit, name, tpd, yaw, pitch, on, textAstronaut: anchor to anything (body, ISS, NORAD number, asteroid); view — look at the figure; shield — remove/set a shield, own message

Page console — the backquote key (ё on a Russian layout).

How to work

  1. Read this page (help), connect with the code.
  2. Ask the page for data: search / open (objects), body (sizes, lit side), where (positions), find (events), layers, state.
  3. Prepare the actions from that data: one play scenario, or a deck of slides the person steps through, or buttons.
  4. Run it; check with screenshot and logs; adjust.

5. Example: «tell me about the planets»

connect {code}
layers → layer {key, visible:false} for everything except oplanets
music {theme: 0, volume: 0.4};  hide_ui {on: true}
play {steps: [
  {cmd:"fly", args:{name:"Mars"}, wait: 3800},
  {cmd:"camera", args:{yaw: …, pitch: …, dist: …, ms: 3000}, wait: 3200},   // values from body {name:"Mars"}
  {cmd:"say", args:{title:"Mars", sub:"228 million km · light takes 13 min", text:"…", audio:"mars.mp3"}, wait: "speech"}, // voice is Piper in the browser (ru ruslan, en amy); audio — a ready mp3 instead
  …
]}

6. Resources

Rules: the page accepts only these commands; no navigation to other sites, no personal data. Keep the person in control — they can press «Disconnect» any time.