← Documentation

Observations

An observation is what one witness reports of what they saw: when, where, what it looked like, how it moved, and what the sky and the weather were. It is the event UFO@home replays. A case holds at least one, and may hold others: other witnesses of the same event, each with their own account, and alternative explanations, which are observations too. An observation can be referred to by an interview held with the witness, and refer back to it.

Technical part: the observation file#

What follows is for whoever writes these files by hand, generates them or reads them in code. In the editor none of it has to be known.

A recording is a plain JSON file. Nothing in it is a binary blob, an id into a database, or a reference to this site — you can write one by hand, generate one from your own archive, or diff two of them in a code review.

Every excerpt below is read-only, and each knows the format: put the caret inside an object and press Ctrl+Space (⌥+I on a Mac) to list every key that could go there, with what the model says of each.

The observation#

FieldMeaning
versionAlways 1
idWhich account this is, unique across every recording anywhere: the day, then who saw it ("1964-04-24-ZamoraLonnie"), or where for an anonymous observer ("1964-04-24-Socorro"). What a case names it by is not this id but its event's own (see below).
time, endTime{ year, month, day, hour, minute, second, raw }, every part optional — that is how the format states “1954” or “around 05:00”. raw is the date as written in EDTF, and it is what the date means: "1948-07-24T02:45~" (approximate), "2025-06?" (uncertain), "1965-07-01%" (both), "19XX" (a masked year), or "05:00" alone for a time of day remembered without its date. The numbers are kept in step with it for what computes (the sky, the clock). It is a subset of EDTF (level 0, these qualifiers on the whole date, masked years); @rr0/time is RR0's full EDTF model, into which UFO@home's own tooling converts a recording's dates season (spring, summer, autumn or winter) states a date known only as “in the spring of 2022” (EDTF 2022-21), where month and day are left out.
durationSecondsAn alternative to endTime, and it wins if both are given
durationTextThe length as it was stated when it was not exact: an ISO 8601 duration with a doubt (PT10M~) or a range (PT5M/PT10M). durationSeconds is then the one length played at, chosen from it
utcOffsetHoursThe LEGAL time the observer's clock was on (+1 for France in 1965). Absent means it is approximated from the longitude, which cannot know legal time or a daylight-saving switch
timeZoneThe IANA zone the offset was derived from ("Europe/Paris"): the rule, where utcOffsetHours is the number it gave at that date. Only the number is read to place the sky; the zone is what lets the offset be derived again when the date changes
place[{ lat, lng, name }] — name is the fully qualified place name the coordinates were resolved from
observer{ id, title, lastName, firstNames }, all optional; omit entirely for an anonymous observer. id is a reference to the person (on RR0, their directory: "ZamoraLonnie"); the other fields describe them when nobody has given them one
descriptionThe account in prose — one string, or one per language (see below)
image, summary, summaryTimeWhat stands for the observation where it is only linked to and never played: the card a shared link shows (see Sharing). image is a picture, an address (absolute, relative to the file, or an RR0 path from the site root); summary a sentence or two, one string or one per language. Absent, the card shows the logo and says who saw it, in which case and when, as far as the file tells. summaryTime is the second of the recording at which the picture of the card is photographed (0 by default: the first image), for a recording whose first image does not show the phenomenon; only the project's capture tool reads it, never the player
tagsA list of strings, written in English: they are technical terms, and two recordings that share one have to match on it. Each reader is shown them in their own language where a translation is known, which is the case today for landing, trace, aerial observation, project sign, paralysis, contact, occupants, close encounter, photograph, radar, electromagnetic effect. The list is not closed: anything else is shown as written, and classification codes ("RR3", "NL") or case references ("Blue Book 8729") are written as they are. One changes the replay: paralysis keeps the observer's view still
accountWho saw it and how the account travelled: observerAgeYears (at the time), observerBirth (birth date, kept in step with it), observerPerception (colour vision, alcohol, fatigue: only what differs from a typical observer), observerOccupation, source (how it reached whoever wrote the recording: on-site, interview, telephone, questionnaire, letter, social-media, press) and followedUp (whether the observer was gone back to afterwards)
sourcesWhere the account can be read as it was given, in the shape of an RR0 source: [{ type, title, authors, url, publication: { publisher, time }, index }], type being book or article, or nothing for a web page or a post, which is then simply its url. Shown in the player's ? panel, linked when it has an address; copied unchanged into an RR0 case
milestonesThe named moments of the account, shown on the seek bar: [{ t, label, note }], label being the letter or number the account itself uses ("A", "B") or a couple of words, note what happened then, in the account's words where possible (one string, or one per language)
roadsThe roads or paths the account's own plan draws, when the map does not have them: [{ id, title, surface, widthM, path, source }], surface being paved, gravel or dirt, path the centre line as [{ eastM, northM }] from the observer's place at the start, and source the plan or survey it was read off

Several observers: the case#

Each observer has a recording of their own, and a recording does not say which case it belongs to: an account stands on its own. What shows them together is the case, which names them: the case.json of an RR0 dossier, which states the case's title, date and classification, and lists everything that happened in it as events. Its events of type sighting are its accounts, each pointing at one observer's recording:

{
  "id": "ChilesWhitted",
  "title": "Chiles et Whitted",
  "time": "1948-07-24 02:45",
  "events": [
    { "type": "event", "eventType": "sighting", "url": "observer-chiles.json" },
    { "type": "event", "eventType": "sighting", "url": "observer-whitted.json" }
  ]
}
FieldMeaning
idThe case's own identifier. On rr0.org it is the dossier's directory and may be left out; a case file standing alone states it
title, timeThe case's name, and when it happened as RR0 writes a time ("1948-07-24 02:45", "1954"). The player names a case it opens by its title
eventsThe case's chronology. Only the sighting ones are replayed, each by its url, read relative to the case file's own address (so the same case works from its dossier's page and from anywhere else); the ones marked interpretationOf are readings of one of them (see below); the others (an article, a film, a confession) are RR0's An event bears an id when a reading names it, and only then: it belongs to the case, so it has to be unique among the case's events and nowhere else, and a reference is read in the file that writes it. An interpretationOf that no event bears is read, as it used to be, as the id a recording the case lists states; one that matches neither is a fault of the case, which the player reports on its console, the editor above its recordings, and the skill's validate.py when it is given the case file.
image, summaryWhat a shared link to the case shows, as for a recording (see Sharing): a picture, and a sentence or two, one string or one per language

Give it to <rr0-sighting src> or to the player, and each observer can be picked from a list. One recording can be given directly, with no case, but a case with one sighting works the same way and names what it shows. Try it with case-chiles-whitted.json (play it).

Saying it in more than one language#

A recording is handed from one reader to another, so every field an author writes can hold one string per language instead of one: description, a shape's or a decor object's title, and a milestone's label and note.

{
  "description": {
    "fr": "Tout le compte rendu de Lonnie Zamora, d'un seul tenant…",
    "en": "Lonnie Zamora's whole account, of a piece…"
  }
}

Keys are language tags as a browser gives them (fr, en, pt-BR), and none of them is required. A plain string stays perfectly valid and means “in whatever language it was written in” — which every recording made before this is. A reader whose languages are none of the ones present gets what the file DOES have rather than an empty field: a missing translation must never turn something the observer said into something they did not.

Which language a reader gets is their browser's, unless the page says otherwise: a lang on the element itself, or on anything around it, is taken first — an article that declares its own language has already stated what language its reader is reading it in. The browser's list is what follows, so declaring one forces a choice without throwing away the others.

The editor shows one language, the reader's own, and writing back touches only that one — so opening a file in the other language and typing is how a translation gets added, and one author cannot delete another's.

What was seen#

timeline.keyframes is a list of { t, shapes }, t in milliseconds from the start. Each shape carries a sourceId — several shapes can share one timeline (the phenomenon, a trailing flame, a second light) — and a shape:

{
  "kind": "oval",
  "bounds": { "x": 0, "y": 0, "width": 0, "height": 0 },
  "color": "#39ff14",
  "angle": 0,
  "transparency": 0,
  "haloScale": 1.5,
  "brightness": 0,
  "blur": 0,
  "selected": false,
  "title": "the phenomenon",
  "angular": { "widthDeg": 1.2, "heightDeg": 0.4 },
  "aim": { "azimuthDeg": 353.6, "altitudeDeg": 0.6 }
}
FieldMeaning
kindoval, or polygon, which then also takes points: [{ x, y }] in pixels from the top-left corner of bounds, spanning its width and height, so that the outline is stretched with it when the angle resizes the box. When bounds is left out (the usual case: it is derived from aim and angular), write points as FRACTIONS of the box, 0 to 1, not pixels
titleIts name, shown when the pointer is over it; one string, or one per language
colorAny CSS colour
angleIts tilt, in radians about its own centre, positive clockwise on screen
transparency0 opaque to 1 invisible
haloScaleThe glow around it; 0 is none
brightnessHow dazzling: a veil, aperture spikes, a core clipped to white
blurHow indistinct the observer said the edges looked
angularIts apparent size in degrees — see below
aimWhere it was in the observer's sky: the direction of its centre, azimuthDeg clockwise from true north and altitudeDeg above the horizon. Not the way the observer was facing, which is the pose's headingDeg and pitchDeg — see below

aim places it, angular sizes it. bounds is those two projected onto the fixed 640×360 canvas at the pose's own heading and field of view, through the recording's own instrument; it is re-derived on load, so a file survives a change of canvas, of field of view, of instrument, or of where the observer was looking. If the pixels and the angles ever disagree, the angles win: moving bounds in a file does nothing while aim is there. A file written by hand can leave bounds out entirely.

timeline.order is the back-to-front paint order, timeline.groups the grouped source ids. Both optional.

Everything around it#

FieldMeaning
observerTrack{ keyframes: [{ t, pose }] } — pose holds lat, lng, elevationM (above the local ground), headingDeg, pitchDeg, rollDeg, fovDeg, and for a camera fNumber and focusDistanceM. Played the way a person moves: exactly the stated pose at each keyframe, and between them a movement that gathers speed, carries it through the keyframes that keep moving, and slows down into a pause — two keyframes with the same place, or the same heading. A look starts and ends still; a walk or drive already under way at the first keyframe carries on. Stopping and setting off on foot add their own jolt, a nod of the head that dies within a second and a half, turned into the image as much as the instrument lets it (an eye hardly, a hand-held camera fully). And a body at rest is never quite still: standing or waiting, the view sways by millimetres and drifts by a tenth of a degree, slowly, the same way at the same instant — except for an observer the account says was paralysed (tag paralysis). The pitchDeg stated is how the observer looked in the frame of what carried them: while they travel, the player adds the grade of the road under the track (read from the relief, uphill positive, faded as the view turns off the way they go), so a climb raises the view and the Tilt chip states the sum filter is what was held in front of the instrument, a FILTERS id such as solar-visual for an eclipse. It is held between keyframes, not blended (nobody wears half a pair of glasses), and taken off by stating "none".
weatherTrack{ keyframes: [{ t, weather }] } — the sky's conditions along the recording: precipitation, wind, storm, and the clouds as layers with real heights, each able to hold individual clouds placed in metres. Every field of a weather is in the next section
weatherSource{ id, name, url } of the record the weather was looked up from. Its presence means the recording is replayed exactly as authored and never looked up again. Absent means the observer's own account
lightPollutionHow bright the night sky of the place is, towns and all: the zenith of a moonless night in magnitudes per square arcsecond, as a Sky Quality Meter reads it or as the World Atlas of the artificial night sky brightness gives it (its “SQM” figure, Falchi et al. 2016, which lightpollutionmap.info shows for any place). 22 is a natural sky, a suburb about 19, a city centre 17. Absent means a natural sky. The towns' share, what is left once the natural 22 is taken out, is added to the sky as light: brighter toward the horizon, where the low air scatters more of it back, and drawn in the warm white of a 3000 K lamp. It drowns the Milky Way and the zodiacal light, and takes stars away, by the difference in the faintest star an eye can pick out (Crumey 2014): about two magnitudes at 19. The Moon now does the same. "derived" when looked up in the atlas, "stated" when the observer measured it. The editor fills it in from the atlas for an observation from 2015 on, as "derived" with the atlas named, and never replaces a figure typed by hand; before 2015 it asks nothing, the sky having been darker than the atlas's
observerMapWhether the map of where the observer stood starts open when the recording is played or edited: true or false. Absent says nothing, and the player decides: open for an observer who went somewhere, closed for one who stayed put. A page embedding the player has the last word (its show-observer-map attribute). The editor writes it when the map is opened or closed there, so that a reader is shown what the author left
frozenWhere the data the scene would otherwise ask third parties for at every play is kept, in files beside the recording (annexes), so that a finished recording plays the same way in ten years without asking anybody: relief (the ground's height, as the elevation source answered for each patch of ground the scene builds, a zip), imagery (the photograph of the ground, as the tiles the imagery source served, a zip), roads, water, railways (the ways, rivers and tracks of each patch, JSON), upperAir (the air at the levels of the aircraft, JSON) and map (the photograph under the observer map, as the tiles the imagery source served for the ground the map is fitted to, a zip: a frozen map shows that photograph and asks for no other, larger when zoomed into, blank beyond it when zoomed out). Each is { src, takenOn, attribution }, src read relative to the recording's own address, or a data: URL for an annex kept inside the recording itself. A group read from its annex asks nobody for anything; a group without one is asked for live, as everything was before. "live": true on a group asks it live at every play and never freezes it (what picking its live source rather than File says), and on frozen itself every group; "refresh": true on one group asks it live once, and the editor takes it again at the next save. The editor takes what is missing when the recording is saved, and saves the recording and its annexes as one zip to unpack where it is published. weather, place, lightPollution and decor say the weather (weatherTrack, weatherSource), the place's name, the night sky's figure and the scenery are read from the file rather than asked again by the editor (of the weather record as the date or the place change, of the geocoder, the atlas and the survey as the observer moves): {} for those kept inside the recording, { src } for an annex holding their keys, which the players put back in the recording as they load it. Absent, they are inside the recording and the editor asks again. The time zone is not here: it is frozen in the recording itself
soundTrack{ keyframes: [{ t, sound }] } — kind (none/hum/whistle/rumble/crackle), volume, pitchHz, optional src of a real recording. volume and pitchHz glide between keyframes, kind and src change at the keyframe. A src on another site must be served to any origin (CORS)
referencesPictures of the place laid over the scene: src (an address, or a data: URL for a picture added from a disk), kind (photo/panorama), registration (headingDeg, pitchDeg, rollDeg, fovDeg), opacity, credit/creditUrl, optional t and drawing, from ({ lat, lng }, where it was taken: the player fades it out as the observer walks away from that point; absent, it is never faded), and the landmarks it was lined up on (id, label, picture as { u, v } from the top-left corner, scene as { azimuthDeg, altitudeDeg })
tracesWhat an investigator drew over the place, imported from their Google Earth file (KML or KMZ) and drawn over the scene as theirs, never as part of the reconstruction: [{ id, title, kind, points, altitude, color, source }], kind being point, line or polygon, points the vertices as [{ lat, lng, altM }] exactly as the file held them, altitude how altM is read (ground, the default, relative to the ground under each point, or absolute above sea level), color a #rrggbb and source whose it is
instrument, exposureSecondsWhat it was observed through, and how long the shutter was open. Absent means the naked eye. instrument is one of eye, rectilinear-lens (a camera of unknown make), roamer-1-120, instamatic-126, slr-35mm-50, slr-35mm-zoom, phone-landscape, phone-portrait, or a digital SLR (eos-5d-iii, nikon-d7000… the sensors of Stellarium's catalogue), binoculars (binoculars-nikon-10x42-edg…) or a telescope (telescope-celestron-c8…); exposureSeconds is one value for the whole recording, held to that device's own range
isoThe speed of the film or sensor the picture was taken on, when it is known: 400 for a 400 ISO film. Absent means the device's own (an SLR's colour negative is 100, an Instamatic's 64). A camera's picture is not answered like an eye's: a film does not adapt to the sky, it receives an exposure — the light, times the shutter, over the square of the f-number — and answers it with its own curve (a negative's soft one, a slide's steeper one, a sensor's straight line that stops at white). So a night pose comes out with a black sky and whatever was bright on it standing out
swayHow much the body holding the instrument moves the view when not walking: 1 for a person standing or sitting, 0 for a camera on a tripod, anything between or above for less or more. Absent means 1 — except for an observer the account says was paralysed (tag paralysis), for whom it is 0, and for an exposure longer than half a second, which no hand holds still: that is a tripod, and 0 too
vehicleThe vehicle the observer was in, when the decor does not draw it: { kind, windowsOpen, noise }, kind being car, van, truck, motorcycle or generic. Heard from inside: its engine follows the observer's own journey — the gear from the speed, the revs from the gear, the effort from speeding up or slowing down — with the road and the wind, muffled by a closed cabin, let in by open windows; noise is its loudness against an ordinary one of its kind (1 by default). It is also what keeps the player's sound button active when the file has no soundTrack: the button answers for everything the scene plays — this engine, an engine in the decor, rain and wind — not only for the track Its lights: headlights is a setting as for a decor vehicle (off, position, dipped, main, fog) or auto, the default: the dipped beam from the dusk on and nothing by day, which is what a driver does. headlightKeyframes is [{ t, mode }] for a change during the recording. The beam lights the road ahead of the observer, in the direction the vehicle goes.
decorScenery at a real eastM/northM from the observer: buildings (with floors, windows), trees, shrubs, streetlights, vehicles, bridges, overhead lines, other observers, aircraft — optionally with a track and lights whose pattern carries a real flash rate. See below

A decor object is stated in metres, like everything that is not the phenomenon:

FieldMeaning
eastM, northM, headingDegWhere it stands from the observer, and which way its front faces, clockwise from true north
sizeM{ widthM, lengthM, heightM } along its own axes, length being the way it faces. Each axis is optional: one nobody measured keeps the built-in shape's own proportion
modelA real 3D model standing in for the built-in shape: a catalogue entry by id, or a glTF/GLB file at url (which wins, must be readable from any origin, and then needs its credit; a relative one is read from the file that states it). It never decides the size: it is scaled, keeping its proportions, to the first measured axis among length, height and width, or to the real size the catalogue gives it. Seen from inside, and whenever the model cannot be had, the built-in shape is drawn instead. An observer or an entity that names none is never drawn as a built-in shape: it is one of the catalogue's realistic people, picked by its id (a child, or a being a statement describes, names its own model) headingOffsetDeg turns the loaded model about its vertical axis so that its nose points the way the scene expects (0 faces north at a heading of 0); a catalogue entry brings its own. credit is { title, author, license, sourceUrl }: license as its author states it ("CC BY 4.0") and sourceUrl where it came from, so that the claim can be checked.
bridgeFor a "bridge": how it is built. sizeM.lengthM is its whole length along the road, embankments included, widthM the deck's width and heightM the height of the road on the deck above the ground; spanM is the open span under the deck, the road running on a bank of earth sloping down to the ground on each side of it; deckThicknessM the slab's thickness (1.2 by default); railing { heightM, postSpacingM, rails } the railing along both edges, posts and rails whose openings are rectangles (1.05 m, 1.5 m and 2 rails by default). Drawn at those measurements, never stretched
wireFor a "wire" (an electric or telephone line): sizeM.lengthM is the span between its poles, along its headingDeg, and heightM the height of the lowest strand; strands [{ heightM, acrossM }] hangs several (stacked ones share an acrossM, a crossarm's differ); poles is both (default), start or end (the end its heading points away from or towards: the other is anchored on a building) or none; diameterM (8 mm by default) and poleHeightM (half a metre over the highest strand). Straight, without sag, and drawn at those measurements, never stretched
track[{ t, eastM, northM, altitudeM, headingDeg }] when it moves, altitudeM above the ground under it (the eye is at elevationM + 1.6 m, so a body at the eye's level has an altitudeM of that). Position blends between keyframes; the heading is held from one to the next
lightsIts lamps: id, offsetM { x, y, z } from its centre (right, up, front), color, intensity (1 is an ordinary navigation light) and a pattern: { "kind": "steady" }, or { "kind": "flash", perMinute, dutyCycle, phase }, the rate as regulations state it, the lit fraction of each cycle (about 0.5 for a filament flasher, 0.01 for a strobe) and an offset from 0 to 1 between lamps. The editor fills them from presets: airliner, helicopter, car headlights, car hazard flashers, emergency beacons, streetlamp
engineA vehicle heard running: { kind, noise }, as vehicle above. Its engine follows its own track, heard from where the observer stands, fainter and duller with distance
aircraftSoundAn aircraft (kind aircraft) heard while it flies, which it does when its track leaves the ground: the sort of noise it makes, an aircraft kind (airliner-narrow by default, airliner-wide, regional-jet, helicopter-light, light-piston…) or silent for one that makes none. What its model says applies when this is absent (the stand-ins for a phenomenon are silent). It is heard like the aircraft of a record of air traffic: fainter and duller with distance, late by the time sound takes to come (about three seconds per kilometre) so that it is heard where it was, higher in pitch as it comes and lower as it leaves, and masked by the noise of the place
occludesSourceIdsThe phenomena the observer said it stood in front of — see the rules below
facadeKind building only: { color?, plain? }. color is the wall, "#rrggbb"; unless plain is true, windows are drawn on every floor and a door on the ground floor of the front face. Drawn at the stated sizeM (all three sides must be stated), never stretched, and not for a building with an observer inside. The editor takes the colour from the material the map states for the walls (stone, brick, concrete, breeze block, wood), or a plaster where it states none. Absent is the plain box.
roofKind building only: { shape, riseM, color?, ridge? }. shape is "flat" (the box), "gable" (two slopes to a ridge along the longer side, or along the depth with ridge: "depth") or "hip" (four slopes); riseM is how far the ridge stands above the eaves, so the walls are sizeM.heightM less that; color is the roof seen from above, "#rrggbb". The editor reads them off the laser and the aerial photograph of the survey. Absent is a flat roof the colour of the walls.
originThe survey this object was taken from, by the id of its provider ("ign-lidar-topo"), when the editor took it from the survey of the place and the author did not place it by hand: a tree, a building, a bridge or a span of line. Choosing another survey, or none, or another place replaces or removes what carries an origin, and never what does not
surveyedOnThe day (YYYY-MM-DD) of the newest survey this object was drawn from, where it comes from one (see origin): the editor warns when it is later than the observation.
lit, litKeyframesWhether a streetlight's lamp or a vehicle's headlights are on. litKeyframes is [{ t, lit }], sorted by t, for one that is switched on or off during the recording; without it lit holds throughout. Ignored for a building, a tree or an observer
headlights, headlightKeyframesHow a vehicle's lights are set, since not everybody puts them on, nor as they should: off, position (the position lights, which show the vehicle and light nothing), dipped (the dipped beam), main (the main beam: far, narrow, dazzling) or fog (the fog lamps: low and wide). A beam lights the road, the verges, the trees and the walls in front of the vehicle, whatever the vehicle is drawn as. headlightKeyframes is [{ t, mode }], sorted by t, for a setting that changes during the recording. Without either, lit says it: lit is the dipped beam. Ignored for anything but a vehicle. Only four vehicles (the observer's own, then the nearest of the decor) have a real beam. The observer's own lamps cast shadows (a bank, a post or a bridge pier keeps the light from what is behind it), and the beam shows in the air in proportion to what the air scatters of it: a faint trace in clear air, a cone of light in fog
observerSide, observerFacing, observerKeyframes, occupiedFloorThe observer inside the object, for a building or a vehicle (at most one object holds them). observerSide is where they are: front, behind, left, right, front-left, front-right, behind-left or behind-right, a vehicle's occupant being at one of its four doors. observerFacing is the window they look through when it is not their seat's: a driver sits at front-left and looks through the windshield, front. observerKeyframes, [{ t, side, facing }], says where they sit over time, each held until the next, and a keyframe without side means outside from then on, as a driver who gets out. occupiedFloor is the floor of a building they are on, 0 (ground floor) up to floors
modelParametersThe settings of the model the object is drawn as, by the key the catalogue entry publishes each under: { "costumeColor": "#5a6b3a" } for the clothes of a person, foliageColor for a tree, hullColor for a craft, gearUp for an airliner in flight. A model brings its own (the editor draws a field for each of them); an object drawn as a built-in shape has none, it is painted by color
sightingUrlFor an observer: the address of that observer's own recording, when it is known, so that the scene's menu can offer to view their account

What it was: interpretations#

A recording states angles, and a body in metres is never part of what was seen. It is a claim about it, and it is tested by standing it in the scene and looking at it from where the observer stood: it casts its shadow, the ground can hide it, and its outline is measured against what the observer said at every instant. An interpretation is shown alone, as the world it claims; asked to compare (the ◌ button, or compare-account on <rr0-sighting>), the player draws everything the observer saw beside it as dashed outlines and lists how far off the direction is and how many times wider and taller each body looks, in red when an observer could not have been that far off.

The observer's own reading goes in the recording, as interpretation. Someone else's reading is not part of the account: it is another sighting, listed in the case (see below). A account whose observer said what it was is drawn in the round, as they said; one that says nothing in metres is drawn as the angles it states. The player offers it and each analyst's interpretation, one at a time.

{
  "interpretation": {
    "title": "A craft standing on its legs",
    "bodies": [{
      "id": "craft",
      "explains": ["ufo-1"],
      "model": { "id": "ellipsoid" },
      "track": [
        { "t": 52000, "eastM": -571.6, "northM": -965.5, "onGround": true,
          "sizeM": { "widthM": 3.36, "lengthM": 3.36, "heightM": 1.73 },
          "appearance": { "color": "#e8e6df", "albedo": 0.7 } },
        { "t": 83000, "azimuthDeg": 195.9, "altitudeDeg": 4.1, "distanceM": 44 }
      ]
    }]
  }
}
FieldMeaning
explainsThe sourceIds of the phenomena this body claims to be
modelA shape built here (ellipsoid, sphere, disc, cylinder, cone, box, torus, figure), a model of the catalogue by id, or a glTF file at url with its credit; a relative url is read from the file that states it, not from the page. Stretched to sizeM whichever it is headingOffsetDeg and credit ({ title, author, license, sourceUrl }) are those of a decor object's model.
trackWhere it is and what it looks like at each t. A position is stated either in the world (eastM/northM from where the observer stood at the start, like the decor, with onGround or altitudeAboveGroundM) or from the observer at that instant (azimuthDeg, altitudeDeg, distanceM). A body onGround stands on the relief; a direction with no distance then meets the ground where that line does. sizeM, attitude (headingDeg, pitchDeg, rollDeg) and appearance (color, albedo, luminanceCdM2, transparency from 0 to 1, haloScale as a multiple of the glare of its own light) hold until a later keyframe restates them, and are blended between two keyframes. present: false takes the body out of the scene from that keyframe on, and present: true brings it back. A flame (lengthM, widthM, color at the nozzle, tipColor, luminanceCdM2) is lit at the keyframe that states it, comes out of the model's node named exhaust (or the one its node names), lights what is around it, raises dust where it meets the ground when raisesDust says so, and is put out by a luminanceCdM2 of 0
motionsIn a keyframe: how far along each of its model's own movements it is, by the movement's name — the glTF file's animations. 0 is a movement's start, 1 its end, and one that repeats goes on past 1: { "legs-turn": 7 } is seven turns. The model says what moves and how; the track says when. Each movement blends between the keyframes that state it, whatever other keyframes move the body in between, holds after the last and stands at 0 before the first. Valensole's departure is written so: { "t": 246000, "motions": { "pivot-retract": 0 } }, { "t": 248000, "motions": { "pivot-retract": 1, "legs-turn": 0 } }, … { "t": 262000, "motions": { "legs-turn": 7 } }
lightsIn a keyframe: the luminance, cd/m², of each of its model's own lights, by the name of the light's material in the glTF file — so that one body carries lights that each do their own thing, a steady white at each end and a red one flashing between. The model says where each light is, how big and what colour; the track says how bright and when. Each light blends between the keyframes that name it and holds after the last, so a switch is two keyframes a millisecond apart; a light no keyframe names glows at its share of appearance.luminanceCdM2. A light too small to show is seen by its glare, and whatever stands between it and the eye — the body's own hull, a bridge — hides it. Silly-le-Long's red light: { "t": 499, "lights": { "front-red": 1500 } }, { "t": 500, "lights": { "front-red": 0 } }
outlineNodeThe node of the model that is what the observer drew ("hull" for a craft whose legs are not in the drawing): what its outline is measured by
smokeOn the interpretation itself: what it sets burning on the ground, as { eastM, northM, fromT, untilT? }, seen by its smoke carried off by the recording's wind halfLifeS is how fast it dies down, in seconds to half its thickness (20 by default): a brush fire flares and smoulders. It never quite stops before untilT.

Events of the sky#

What happened in the sky while the recording runs, as facts: a laser shot, a rocket launch, a re-entry. Each was there whether anybody saw it or not, so it is neither part of the interpretation, which reads the account, nor decor, which outlasts the observation (a house, a tree). They are stated under skyEvents, at the top of the recording; a recording that put them in its interpretation, as earlier versions did, is still read.

{
  "skyEvents": {
    "beams": [{
      "id": "meudon-paris",
      "path": [
        { "lat": 48.805055, "lng": 2.231171, "altitudeM": 172.5 },
        { "lat": 48.83632, "lng": 2.33702, "altitudeM": 88 }
      ],
      "wavelengthNm": 532,
      "powerW": 10
    }]
  }
}
FieldMeaning
reentriesThe atmospheric re-entries that happened, a satellite or rocket stage breaking up into burning pieces. Not bodies: a re-entry burns 70 to 100 km up and is seen from hundreds of kilometres, so it is placed on the Earth and seen through its curvature from wherever the observer stood. Its track gives the leading point at instants of the recording, { t, lat, lng, altitudeKm }, joined by great circles; each of its fragments follows the same path lagS seconds behind, is seen from fromT until it burns out at untilT, shines with its absoluteMagnitude (at 100 km, as for a fireball) moved to its real distance, in its color, with a tail of the last trainS seconds of its path. Its light is its own: it shows whether or not the Sun reaches it. object names what came down (name, cosparId, norad). Nothing is propagated from orbital elements, which are off by minutes in the last orbit: the path is what the sources establish, with its provenance
launchesThe rocket launches that happened, lit by a Sun that has set for the ground but not for the plume, the "space jellyfish". Placed on the Earth like a re-entry: its track gives the stage at instants of the recording, { t, lat, lng, altitudeKm } (it may begin before the recording does), and burnFromT and burnUntilT the seconds the engine burned. The plume is a string of puffs let go along that path, each spreading with age; what shows of each is the sunlight it scatters towards the observer. The Earth's shadow (where the Sun is lit or not at the puff's height, and how much air its light grazed, which reddens or blues it), the scattering angle and the spreading are computed from the real Sun at the real date, so nothing is stated of them. An optional plume overrides the defaults: exhaustKgPerS (273, a Merlin Vacuum), condensedShare (0.1, the one number nobody has measured), particleRadiusUm, expansionKmPerS, initialRadiusKm, lifetimeS, and for the collar (exhaust just let go, wide and soon gone, beside the narrow trail that stays) collarShare (0.85), collarExpansionKmPerS (2.5), collarLifetimeS (20). vehicle names what flew and, in engine, the engine whose exhaust it is: thrustKN and specificImpulseS (from which the exhaust per second and its velocity follow, hence how wide the collar spreads) and propellant (rp1-lox, methane-lox, hydrogen-lox, hypergolic or solid, which says how much of it turns into particles and how big). What plume states beats what the engine implies. The plume needs the recording's date: without one there is no Sun to light it vehicle.mission names the mission it flew ("Starlink Group 10-49").
beamsBeams of light sent through the air from the ground, a laser shot or a searchlight. Each has a path of at least two points { lat, lng, altitudeM } (altitudes above the sea): where it leaves the emitter, where a mirror turns it if one does, and where it ends. Between two points it is a straight line through space, not along the ground, which falls away under it. wavelengthNm is its colour (532 for a green laser), powerW its mean optical power, divergenceMrad how fast it widens (0.1 when absent) and apertureM how wide it leaves (0.01 m when absent); fromT and untilT say when it is on. Nothing of its brightness is stated: what is seen of each metre of it is what the air there (molecules and haze, at its wavelength) scatters of the power left in it towards the eye, at the angle it has to turn, so it is far brighter looking up it than looking along it. It is hidden by what stands in front of it and stops where it meets the ground.

Another reading: an interpretation is another sighting#

Someone else's reading of an account (an analyst's, an investigator's, a comparison made to test it) is not part of the account: it is another observation, the one that took place as its author holds it. So it is listed in the case like the original, as an event of type sighting, marked as a reading by interpretationOf, and has a recording of its own at url:

{
  "id": "McMinnville1950",
  "events": [
    { "type": "event", "eventType": "sighting", "id": "trent", "title": "Paul Trent", "url": "observer-mcminnville.json" },
    { "type": "event", "eventType": "sighting",
      "interpretationOf": "trent",
      "time": "2013-06",
      "by": [{ "people": "CousynAntoine" }, { "people": "LouangeFrancois" }],
      "title": "A model on a thread under the line",
      "url": "interpretation-mcminnville-2.json" }
  ]
}
FieldMeaning
interpretationOfThe id of the case's event it is a reading of ("trent"), or a list of ids when one reading serves several accounts (the photographs and a close-up of them). What tells it from an account: the events without it are the case's observers
timeWhen the interpretation was elaborated, as RR0 writes a time ("2013-06"): not when the sighting happened
byWho claims it: { "people": id } or { "org": id }, which name an entry that lives elsewhere (on RR0, a people or an org directory), or a person described in value. Absent for a comparison nobody claims
titleWhat the reading is called in the player's menu and the editor's picker: a few words, one string or one per language
urlIts recording, read relative to the case file's own address, like an observer's

The recording at url is a recording like any other. It shares the place, the time and the observer's pose of the account it reads, so that a body placed from the observer, or in metres from where they stood, means the same in both; it draws no shape of its own (an empty timeline); and it holds the bodies in its own interpretation, described above. A body's explains names the shapes of the account.

{
  "version": 1,
  "id": "1950-05-11-TrentPaul-interpretation-2",
  "time": { "year": 1950, "month": 5, "day": 11, "hour": 19, "minute": 45 },
  "utcOffsetHours": -7,
  "place": [{ "value": { "lat": 45.104167, "lng": -123.330556 } }],
  "observerTrack": { "keyframes": [{ "t": 0, "pose": { "lat": 45.104167, "lng": -123.330556, "headingDeg": 41.3, "pitchDeg": 14.85, "fovDeg": 33.4 } }] },
  "tags": ["interpretation"],
  "timeline": { "keyframes": [], "order": [], "groups": [] },
  "interpretation": {
    "title": "A model on a thread under the line",
    "bodies": [{
      "id": "model",
      "explains": ["ufo-1"],
      "model": { "id": "disc" },
      "track": [
        { "t": 0, "eastM": -52, "northM": 118, "altitudeAboveGroundM": 7.5,
          "sizeM": { "widthM": 0.3, "lengthM": 0.3, "heightM": 0.1 } }
      ]
    }]
  }
}

The player lists the case's observers from the events with no interpretationOf and offers each reading beside the account it names, one at a time (the ◌ button measures it against the account). A recording standing alone has no reading but its own interpretation. The older form, an event of type interpretation naming the recording in sighting with its bodies inline or in a file at url, is still read. The editor opens a case: its picker above the render lists the accounts and the readings, each opening as the recording it is, with a button to add a reading (a new recording that shares the account's place, time and pose, and an event for it), one to delete it, and one to export the case and every recording added or changed as a zip. Try it with case-mcminnville.json (play it, edit it).

The weather, and its clouds#

A weather keyframe states the sky's conditions at one moment of the recording's clock; between two keyframes every number is blended, and precipitation type and storm are held. It carries:

FieldMeaning
cloudLayersThe clouds, as a list of layers — see below. Absent means the older fields on this row's neighbours describe them, and they are adapted into one water layer and one cirrus veil; an empty list means a clear sky somebody looked at
cloudCover, lowerCloudCover, highCloudCoverFractions of sky (0–1): the total, the water decks alone, and the icy veil alone. Written by recordings made before there were layers, and still kept in step by the editor as a summary of them
cloudBaseM, cloudDarknessThe same era's one base, in metres above the reference ground, and one shade (0 white, 1 very dark)
iceCrystalAlignment0–1, how steadily the ice crystals fell — what turns a bare ring into sundogs, arcs and a pillar. No record measures it; a cirrus layer carries its own
rareIceCrystals0–1, the share of a cirrus deck's crystals that are the rare kinds: pyramid-ended columns (the odd-radius haloes at 9°, 20° and 35°) and plates free to turn about a level axis (Lowitz's arcs). Absent means 5%, an ordinary deck. Like the alignment, no record counts them, so it is stated. The simulation behind it was checked against Atmospheric Optics
relativeHumidity0–1, near the ground. It decides how milky the clear sky is: haze swells with water as the air nears saturation. A looked-up record carries it (from ERA5's temperature and dew point); absent means a typical haze
precipitationType, precipitationIntensitynone/rain/snow/hail, and 0–1. Stated at a keyframe, played the way it falls: a shower begins at the keyframe that starts it with its first drops, and reaches the stated rate no faster than real rain does (nothing to heaviest in twenty seconds); it stops the same way. A change the keyframes spread over longer is followed exactly precipitationAmount (0–1, absent meaning all of it) says how much of that precipitation is falling yet.
windDirectionDeg, windSpeedThe general wind: the bearing it blows TOWARD, clockwise from north, and metres per second. It is what carries the clouds — from time zero, so seeking and replaying give the same sky
stormLightning and thunder, at the right delay

Each layer of cloudLayers is a deck of clouds at a real height, and stays itself from one keyframe to the next:

FieldMeaning
idStable across keyframes — layers are matched by it, never by position in the list. A layer present in one keyframe and absent from the next fades out; reordering them changes nothing
typecumulus, stratus, stratocumulus, cirrus or unknown. It decides the shape of the tops and how thin the veil is; a cirrus is also the one that refracts haloes. It switches at the keyframe, it is not blended
baseM, thicknessMMetres. The base is above the recording's REFERENCE ground, not above an observer who climbs; an observer above the base is inside or over the deck, and the sky is drawn accordingly
coverage0–1, and it means what it says: the fraction of the sky this layer covers, whatever the size of its clouds
sizeMThe characteristic width of one cloud, in metres. Separate from coverage: the same fraction of sky can be many small clouds or a few large ones
density0–2, how opaque the cloud matter is; 0 is transparent. Separate from coverage too
darkness0 white to 1 very dark. Absent means the keyframe's cloudDarkness
seedWhich pattern, out of the endless ones the same numbers can draw. Absent means one derived from the id, which is why the id must not change
windDirectionDeg, windSpeedThis layer's own wind, when it differs from the general one — the high deck usually does. Absent means the general wind
iceCrystalAlignmentFor a cirrus only
instancesIndividual clouds inside this layer — see below

An individual cloud in instances is one cloud of its layer that the file places exactly, because the account did: the one the phenomenon went behind, the one that was there and nowhere else. It is drawn as one of its layer's own — the same texture, the same threshold — told apart from its neighbours by nothing but where it stands and how big it is, and it stands even when the layer's coverage is nought. It rides the layer's wind like the rest, and it hides a phenomenon it passes in front of.

FieldMeaning
idStable across keyframes, same rule as a layer's
eastM, northMWhere its centre was at time zero, in metres from the observer's starting point. The wind carries it from there
baseM, thicknessMIts own base and height, metres — a cloud can sit lower or stand taller than its deck
widthM, depthM, rotationDegIts footprint, metres, and the bearing that footprint is turned to
density, darknessIts own; darkness absent means the layer's
{
  "weather": {
    "cloudLayers": [
      {
        "id": "low", "type": "cumulus",
        "baseM": 1500, "thicknessM": 800,
        "coverage": 0.55, "sizeM": 1400, "density": 1, "darkness": 0.15,
        "instances": [
          { "id": "the-one", "eastM": 0, "northM": 4200,
            "baseM": 1500, "thicknessM": 800,
            "widthM": 1900, "depthM": 1300, "rotationDeg": 12, "density": 1 }
        ]
      },
      { "id": "high", "type": "cirrus", "baseM": 8000, "thicknessM": 400,
        "coverage": 0.2, "sizeM": 2200, "density": 0.35, "iceCrystalAlignment": 0.65 }
    ],
    "precipitationType": "none", "precipitationIntensity": 0,
    "windDirectionDeg": 90, "windSpeed": 5, "storm": false
  }
}

A recording whose weather was looked up (it has a weatherSource) holds the record's answer, not a link to it: ERA5 gives the low, middle and high bands as three layers named record-low, record-mid and record-high, the low base estimated from the spread between temperature and dew point, the other two at 3 500 m and 8 000 m. Their type is unknown (cirrus for the high one), their size and density are drawing assumptions: a reanalysis knows how much of each band was covered, not what the clouds looked like. Ask the record again from the editor and the layers are rewritten; edit a layer by hand and the recording becomes the author's, the source dropped.

Where each value comes from#

Any value in a recording can be written bare, or wrapped with where it came from:

{
  "durationSeconds": {
    "value": 15,
    "basis": "derived",
    "rationale": "13 to 18 s in the investigator's synthesis; the middle taken"
  }
}

basis is stated (the observer said it, and what a bare value means), derived (worked out from what they said plus something checkable: a road's width, a map, a measured drawing; rationale gives the working) or assumed (chosen so the reconstruction has a value at all, on nothing the observer said). The list of assumed values is the list of what to go back to the observer or the file for, which is why it is worth writing even when nothing else is.

Checking a file#

The format is also published as a JSON Schema, generated from the same types as the player: every key it knows, what each may hold, and the words a closed list accepts. A misspelt key or an unknown value fails it. It says nothing about what may be left out, which is a question of meaning this page answers. To see the result, open the file in the player, from a link, or from your disk with the pictures and models it names. The player checks it the same way on loading, and says what it could not play as written behind a ⚠ over the picture: a key nothing reads, a word outside its list, and what it had to make up because a shape's first keyframe left it out (a field held from the keyframe before is the rule above, not a problem).

A whole file#

The smallest recording that still states something — one silent oval crossing the sky over twelve seconds, on a real date at a real place. Everything else in the format is optional, and everything below is doing work:

{
  "version": 1,
  "description": "The smallest recording that still states something: one silent orange oval, a degree and a half wide, crossing the south-western sky from left to right over twelve seconds.",
  "time": { "year": 2024, "month": 8, "day": 12, "hour": 22, "minute": 30 },
  "durationSeconds": 12,
  "utcOffsetHours": 2,
  "place": [{ "lat": 45.19, "lng": 5.72, "name": "Grenoble, Isère, France" }],
  "timeline": {
    "keyframes": [
      {
        "t": 0,
        "shapes": [
          {
            "sourceId": "ufo-1",
            "shape": {
              "kind": "oval",
              "bounds": { "x": 150, "y": 150, "width": 10, "height": 3 },
              "angular": { "widthDeg": 1.6, "heightDeg": 0.5 },
              "color": "#ff8c1a",
              "angle": 0,
              "transparency": 0,
              "haloScale": 1.2,
              "selected": false
            }
          }
        ]
      },
      {
        "t": 12000,
        "shapes": [
          {
            "sourceId": "ufo-1",
            "shape": {
              "kind": "oval",
              "bounds": { "x": 480, "y": 130, "width": 10, "height": 3 },
              "angular": { "widthDeg": 1.6, "heightDeg": 0.5 },
              "color": "#ff8c1a",
              "angle": 0,
              "transparency": 0,
              "haloScale": 1.2,
              "selected": false
            }
          }
        ]
      }
    ]
  },
  "observerTrack": {
    "keyframes": [
      {
        "t": 0,
        "pose": { "lat": 45.19, "lng": 5.72, "elevationM": 0, "headingDeg": 215, "pitchDeg": 20, "fovDeg": 60 }
      }
    ]
  },
  "soundTrack": { "keyframes": [{ "t": 0, "sound": { "kind": "none", "volume": 0, "pitchHz": 100 } }] }
}

To change it and see it play, paste it into the editor, which completes on every key the format has, offers the words each one accepts, and says what the model says about it.

It is /demo-data/example-minimal.json on this site, so you can fetch it, and play it before changing anything. Note that angular and bounds both appear: the angle is what the file MEANS, and the pixels are re-derived from it on load — write the angle, and let a wrong guess at the pixels be corrected for you.

Larger ones to read#

Every demo on this site is a plain file you can open. These four are the ones worth reading to see how a real recording is put together:

FileWhat to look at in it
observer-chiles.jsonA real case: an observer, a case id shared with a second recording, ten keyframes, a looked-up weatherTrack with its weatherSource
sky-test-halos.jsonNo phenomenon at all — a sky set up by a weatherTrack whose keyframes change the crystals' alignment, the share of rare crystals, the cirrus cover and a cumulus deck, watched through a observerTrack that pans across the display and then holds
sky-test-clouds.jsonThree cloud layers with metre-based altitude, thickness, size, density and wind, evolving on the weather timeline — and in the first one an instances entry: one cloud of the field, placed and sized in metres, that grows and darkens over the two minutes
sky-test-aircraft.jsonAn instrument, an exposureSeconds and an iso, and a decor aircraft with a track and nine lights at their real flash rates, landing lights included
sky-test-reentry.jsonNo phenomenon: skyEvents holding one of reentries, the launcher of Zond 4 breaking up over Kentucky in 1968, its path derived and its timing, heights and brightness assumed, each value with its provenance
sky-test-launch-plume.jsonNo phenomenon: skyEvents holding one of launches, the exhaust of Starlink Group 10-49 (Falcon 9, 25 August 2026, 05:33 EDT) lit in the twilight and seen from Hoboken, New Jersey, the stage's path assumed with its provenance and the shadow, spreading and light computed
sky-test-laser-meudon.jsonNo phenomenon: skyEvents holding one of beams, the 10 W green laser the Paris Observatory shot from its dome of Meudon to its dome of Paris on 9 October 2026, seen from the park at Meudon, the emitter's height and the spread assumed with their provenance and the light computed
instrument-instamatic.jsonThe same sighting as observer-socorro.json, changed in one field. Diff the two

Four rules that decide what a file means#

  • Discrete fields are held, continuous ones are blended. A shape left out of a later keyframe stays as it was, and so does any field a keyframe leaves out of a shape it does restate: a keyframe that only gives a new aim moves the shape and keeps everything else (restating bounds without aim or angular is taken as moving it by its pixels); one whose first keyframe is at five seconds is already painted, in that state, from zero. To make something stop being visible, keyframe it at transparency: 1.
  • Angles only. No real size and no real distance is stored anywhere. Metres are derived, as inequalities, from what the phenomenon was stated to pass behind or in front of (decor[].occludesSourceIds).
  • Declared outranks deduced. occludesSourceIds records statements by the observer. Nothing in this format can deduce them: it describes an appearance on a field of view, not a position in space.
  • Absent is not zero. No sound track means nobody was asked; kind: "none" means the observer reported hearing nothing. The same distinction runs through the weather and the ice cloud.

This page is the reference for the format. The reasoning behind each field is in the doc comments of its type, which the excerpts above complete with, and in the source.

Components#

Three of the standard elements are about an observation: the scene it is replayed in, the player that shows it, and the editor that describes it. The editor of a case and the editor of an interview have their own sections, on the pages about those.

Standard, and that is the whole design#

These are five Web Components — the browser's own standard for a custom element, not a component of anybody's framework. What follows from that is worth spelling out, because it is why the tool can be handed to you at all:

  • They work in any page. A static site, WordPress, a React or Vue app, a wiki, a hand-written HTML file. They are elements; a page that can hold a <video> can hold these.
  • Nothing to build. No bundler, no compilation step, no configuration in your project. The module registers its element on import and the browser does the rest.
  • Nothing to keep up with. There is no framework version to match, so they cannot be made obsolete by somebody else's major release.
  • Their insides are their own. Each carries its markup and its styles in a shadow root, so your page's CSS cannot break them and they cannot break your page.

They compose in one line: both <rr0-sighting> and <rr0-sighting-editor> hold an <rr0-scene>, and the scene holds the playback layer — the timeline, the controls, the pointer's own canvas. So everything the scene answers to is available in all three — through .scene and .scene.ufoElement from the outermost, since the composition lives in a shadow root.

What each one is for#

  • <rr0-scene> — the phenomenon in the world around it. The shape an observer drew, its colour and halo and movement, standing in the real sky, horizon, weather and ground of a stated date, hour and place — hidden by what stood in front of it. Useful on its own too, for a sky with nothing in it at all.
  • <rr0-sighting> — the account, to watch. A scene plus who is testifying, the observation's own metadata, its credits, and the lines that let a reader take it elsewhere. This is what a published sighting looks like.
  • <rr0-sighting-editor> — the account, to reconstruct. Everything above plus the authoring toolbar: describe an observation, or correct one.
  • <rr0-case-editor> — a case, to assemble. Holds a <rr0-sighting-editor> and gives it one recording of a case.json at a time: the observers' accounts and the readings of them. It adds, selects and deletes them, and exports the case.
  • <rr0-date-input> — a date, with its precision. A picker for a full instant, a text for anything less (EDTF), and how sure it is. The editors use it for an observation's start and end and for a case's date; it tells what it holds by a datechange event.

How they fit together#

You never write the nesting. Each element builds the one below it inside its own shadow root, so what your page contains is a single tag:

<rr0-sighting>           who is testifying, the metadata panel, the embed lines
└─ <rr0-scene>           the real sky, horizon, weather and decor, the phenomenon, playback

<rr0-sighting-editor>    the eight authoring panels
└─ <rr0-scene>           a scene, not a sighting: an editor has its own toolbar

Which is why one script tag brings the ones underneath with it:

Loading thisregisters
/lib/rr0-scene.mjs<rr0-scene>
/lib/rr0-sighting.mjs<rr0-sighting>, <rr0-scene>
/lib/rr0-sighting-editor.mjs<rr0-sighting-editor>, <rr0-scene>

So a page showing a sighting and, further down, a bare sky of its own needs one script and two tags — the second element is already registered.

A composition can be reached into, one property at a time:

const sighting = document.querySelector("rr0-sighting")
sighting.scene                    // the <rr0-scene> it composes
sighting.scene.ufoElement         // and the playback layer under that
sighting.scene.ufoElement.play()  // so playback is two properties away

<rr0-sighting-editor> keeps its own composition to itself: what it offers a page is the recording — sightingData — and the event saying it changed.

Which one you want#

They are not variants of one bundle: each is self-contained, so load only the one you need. All three carry Three.js and a star catalogue, which is what a real sky costs.

What you are doingComponentModulegzip
Showing a UFO sighting — a case file, an article, a report<rr0-sighting>/lib/rr0-sighting.mjs249 KB
Letting somebody describe or correct one<rr0-sighting-editor>/lib/rr0-sighting-editor.mjs293 KB
Showing a sky with nothing in it — what a halo, a comet or a satellite pass looked like that night<rr0-scene>/lib/rr0-scene.mjs238 KB
Showing a sighting inside a scene of your own, with no toolbar over it<rr0-scene>/lib/rr0-scene.mjs238 KB
Not sure<rr0-sighting>/lib/rr0-sighting.mjs249 KB

Putting one on a page is two lines.

Putting one in your application#

After npm install @rr0/ufoathome:

import "@rr0/ufoathome/scene"    // registers <rr0-scene>
import "@rr0/ufoathome/sighting" // registers <rr0-sighting>
import "@rr0/ufoathome/editor"   // registers <rr0-sighting-editor>

Or copy the contents of the package's dist-embed* directories onto your own server and point the <script src> there. Each module references its own assets — the star catalogue, the weather audio — relative to itself, so it keeps working from any path; just keep each bundle's files together. Nothing then depends on this site at all.

Language#

Every label is translated by detection, with no picker: the page's own declared language first — the nearest lang attribute, so <html lang="fr"> gets French labels — then the browser's own preferences, then English. A page that declares nothing falls through to the browser exactly as before.

<rr0-sighting>#

The standard sighting view

The default for a real sighting, one observer or several. It composes an <rr0-scene> and adds the toolbar: who is testifying, and the i panel with the observation's own metadata and its credits, and a share button: the link that replays it, and its embed lines.

<rr0-sighting src="sighting.json"></rr0-sighting>
<rr0-sighting src="case.json"></rr0-sighting>  <!-- a case, and all its observers -->
MemberKindWhat it does
srcattributeA single recording, or a case whose sighting events list several — told apart by their shape. A bare array of recordings is refused: list them as the sighting events of a case
observerUrlspropertyThe recordings to show, as a plain array of URLs, instead of src. Setting it again keeps the observer on show if the new list still has them
sightingDatapropertyOne recording, set directly — for a page holding one in memory rather than at a URL
sceneproperty (read)The composed <rr0-scene>, and through scene.ufoElement the playback members
loadFromSrc(url)method (async)What the attribute triggers
show-labelsattributeThe strip of chips stating the recording under the picture, boxed by group (summary, observation, position, time, weather, sound, observer): the date said in words with its weekday, and a Description chip that shows the first words of the account and opens the whole text, scrolling past a height cap, in place of the chips. Off by default on the element — the player's own page turns it on — and a reader can open or close it from the i panel
show-observer-map / hide-milestonesattributePassed down through the composed <rr0-scene> to the player that owns them
start-timeattributeWhere the recording opens, as a length of time from its start: seconds (125), 1:30, 1m30s or an ISO 8601 duration (PT1M30S). Applied once, to the recording the element loads next; a value past the end opens at the end
start-clockattributeWhere the recording opens, as a time said during the observation, in its local time: 02:33:15, or with the day 2026-08-11T02:33:15. The recording's own start places it; it wins over start-time when it can, and is ignored for a recording that does not say its time of day
compare-accountattributeStarts with the observer's account shown beside any interpretation chosen, and measured against it

A recording that names no observer gets no “account by” line at all, which is the accurate thing to say of a sky set up to show a halo.

Every observer's recording is fetched as soon as the list is known, so picking another one is immediate. The list names each by its title, else first names and last name, else id; one that names nobody is numbered by its place in the list.

The i panel opens as a popover, which Escape or a click outside closes (a click outside only, on a browser without popovers). It states the case, the date, the place, the account and the tags; date, place and tags leave it while the show-labels strip already states them. The date is on the observer's own clock (utcOffsetHours, or one approximated from the longitude), not the reader's. The version at its foot opens this observation in the editor. The description leaves it too while the chips show: it is in their Summary box, opened with a click.

The Share button, among the player's own, opens one dialog giving the link or the embed code (the code in an editor that wraps its lines and can be edited), for the player or for the editor, with the options of the sharing page each offered as the player is. The embed code loads the element from wherever this copy of it was itself loaded, so a snippet copied from a local or staging copy points back at that copy.

What it fires

EventFiresWhere to listen
observerchangeWhen the recording on show changes: loaded, set through sightingData, or another observer picked from the list. detail.src is its address, empty for a recording set by scriptOn this element. It does not bubble: listen on the <rr0-sighting> itself, then read sightingData back off it (its description, its id)
recordingissuesOnce a loaded recording has been checked against the format: detail.issues lists what was not played as written (a key nothing reads, a value of the wrong type or outside its list, a value made up because the file left it out), each with its kind and path. Empty when there is nothing to say. The same list is shown over the picture behind a ⚠, and written to the consoleAnywhere above the element: it bubbles out of every shadow root

Putting it in your page

<script type="module" src="https://ufoathome.org/lib/rr0-sighting.mjs"></script>
<rr0-sighting src="sighting.json"></rr0-sighting>

sighting.json is a sighting file. Or, after npm install @rr0/ufoathome: import "@rr0/ufoathome/sighting". The bundle is 249 KB gzipped and registers the tag itself — nothing else to call. The overview above says what else that involves, and sharing an observation has the whole example, to try and to copy.

<rr0-scene>#

The phenomenon, the sky and the ground

The shape an observer drew, standing in the real sky, horizon, weather and ground of the recording's own date, hour and place, and hidden by whatever stood in front of it — with the playback controls under it. This is the element the two others build on.

<rr0-scene src="sighting.json"></rr0-scene>
MemberKindWhat it does
srcattributeURL of a recording, fetched on connect and whenever it changes
sightingDatapropertyThe recording as a plain object (see the format) — read it back after editing, or set it instead of using src
loadFromSrc(url)method (async)What the attribute triggers internally. Await it when you need the recording to be IN before doing anything else
ufoElementproperty (read)The playback layer it composes — the timeline, the controls, the canvas the pointer works on — and through it every playback member below
sceneRendererproperty (read)The 3D renderer, for what nothing else exposes
show-compassattributeN/NE/E/… labels around the horizon. Off by default: useful while authoring a heading, noise while watching
star-catalog-src / deep-star-catalog-srcattributeWhere to fetch the star catalogue from, when you host your own copy rather than the one beside the bundle: the base tier (to magnitude 7.5, fetched by every scene) and the deep tier (7.5 to 9, fetched only by a recording whose instrument reaches past 7.5)
max-pixel-ratioattributeThe most device pixels per CSS pixel the scene may draw at; the display's own, up to 2, when absent. The scene lowers it by itself while frames are late
show-observer-map / hide-milestonesattributePassed straight down to the playback layer below — write them on whichever tag your page actually contains

Hovering it names what is under the pointer — a star with its magnitude and height, a planet, a comet, a building, another observer — and says nothing where the ground hides what you are pointing at.

The © button in its corner lists the credits of what it shows — the imagery under the ground and on the map, the models, the pictures of the place, the sounds. Inside <rr0-sighting> the button is hidden: they are in its i panel.

Playback, on ufoElement

Everything about replaying the recording lives one property down, on the playback layer — scene.ufoElement.play() — the same layer the two other components reach through their own scene.

MemberKindWhat it does
srcattributeURL of a recording, fetched on connect and whenever it changes
sightingDatapropertyThe recording as a plain object (see the format) — read it back after editing, or set it instead of using src
sightingproperty (read)The live model: real-world time and place plus the recording's timeline
loadFromSrc(url)method (async)What the attribute triggers internally. Await it when you need the recording to be IN before doing anything else — playing before it resolves finds a zero-length timeline
play() / pause()methodSay which state you want, rather than flipping the current one
togglePlayPause()methodWhat the button, the click and the space bar do
playbackStateproperty (read)"stopped", "playing" or "paused"
currentTimepropertyThe playhead, in the timeline's own units — not real milliseconds, see positionLabel
seekableDurationproperty (read)The range currentTime can take
autoReplayEnabledpropertyLooping, off by default: a replay plays once, then fires ended. Turn it on for a loop
positionLabel / durationLabelproperty (read)The position and length already formatted by the element: clock time when the observation states a date and its length is known, elapsed time otherwise. Clicking either counter under the bar (or Enter on it) switches between the two, and fires timedisplaychange
refresh()methodRe-reads the duration and repaints — call it after mutating sighting.timeline from outside
canvasElement / rendererproperty (read)The <canvas>, and the renderer painting on it
enableClickToPlaypropertyWhether a click toggles playback and a double-click toggles fullscreen (both, or neither). The double-click also puts playback back as it was before its first click. Set false where the canvas is yours for something else
fullscreenTargetpropertyWhich element the fullscreen button expands. <rr0-scene> sets it to its own stage, so the sky goes fullscreen and not just the overlay. Where the browser offers no fullscreen (an iPhone, an iframe without allow="fullscreen"), the element fills the window instead, and Escape leaves it all the same
show-observer-mapattributeWhether the map of where the observer stood starts open: present opens it, "false" keeps it closed. Absent, the recording decides: its own observerMap when it states one, else open when the observer went somewhere (a walk across a field, a drive, a flight), closed when they stayed where they were. It decides the map's starting state, not whether it exists: the button is there for every recording that states a place
toggleObserverMap()methodWhat that button does. On the map itself the wheel zooms around the pointer, a drag moves the ground, and its own buttons zoom and go back to the whole path. Zoomed in, it recentres on the observer whenever they reach its edge, so that they never leave it
observerPlacingpropertyWhether a click on bare ground of the map fires observerplace with { lat, lng } — off by default, since where the observer stood is the recording's to say. The editor turns it on and writes the position at the playhead
hide-milestonesattributeTake the account's named moments off — the ticks along the bar, the caption naming the one being played, and the lettered points on the map. They are on wherever a recording names any, so this is the only way to say otherwise
toggleMilestones()methodWhat that button does

Those two are the only positive and negative defaults this element has, and the difference is deliberate. A recording's named moments are its own words about itself, so they show wherever they exist; the map costs tile requests to a third party the first time it opens, which is not a page's to spend on a reader's behalf without saying so. Either way the reader keeps both buttons, beside the fullscreen one — a page setting a default is not a page forbidding the opposite.

The toolbar and the corner buttons show only while the pointer is over the picture, playing or paused, or when the keyboard moves the focus onto them. A touch screen, which has no pointer to hover with, keeps them shown.

With the time bar focused, the keys work as on a video site: ← goes back 5 s, → forward 5 s, and Space plays or pauses. Playing or pausing, from a key, the button or a click on the picture, shows its sign for a moment in the middle of the picture.

What it fires

EventFiresWhere to listen
endedOnce, when playback runs off the end without looping. Not on a pause, and not on a scrub to the endAnywhere: it bubbles and is composed, so it crosses out of <rr0-scene> and <rr0-sighting> and reaches the element you put on the page. This is how you play several recordings in turn
timedisplaychangeWhen the counters switch between clock time and elapsed timeAnywhere, same as above
timeupdateEvery playback tick and every seek, with detail.timeOn this element only. It neither bubbles nor is composed, so a page holding an <rr0-scene> has to reach its ufoElement — it is meant for the elements composing this one

Putting it in your page

<script type="module" src="https://ufoathome.org/lib/rr0-scene.mjs"></script>
<rr0-scene src="sighting.json"></rr0-scene>

sighting.json is a sighting file. Or, after npm install @rr0/ufoathome: import "@rr0/ufoathome/scene". The bundle is 238 KB gzipped and registers the tag itself — nothing else to call. The overview above says what else that involves, and sharing an observation has the whole example, to try and to copy.

<rr0-sighting-editor>#

The editor

Everything above plus the authoring toolbar. Put it on a page where people should be able to describe or correct an observation themselves.

<rr0-sighting-editor></rr0-sighting-editor>
<rr0-sighting-editor src="sighting.json"></rr0-sighting-editor>

Its canvas is an editing surface, so enableClickToPlay is off there: a click selects and drags a shape rather than toggling playback. What the eight groups of its toolbar do is the editor's own page.

MemberKindWhat it does
ufoElementproperty (read)Reaches through to the canvas, the timeline and the appearance work
appearancepropertyWhat the next shape drawn will look like: { presetId, color, transparency, haloScale, blur, brightness }, presetId being "oval" or "polygon" and the others the shape fields of the same names. Setting it takes only the fields you give
sightingchangeeventFires after every edit — the single signal that the recording has changed

What it fires

EventFiresWhere to listen
sightingchangeAfter an edit: a field written, an instrument chosen, a time zone derived, a shape moved along the timelineOn this element. It does not bubble, so listen on the <rr0-sighting-editor> itself — then read sightingData back off it

Putting it in your page

<script type="module" src="https://ufoathome.org/lib/rr0-sighting-editor.mjs"></script>
<rr0-sighting-editor></rr0-sighting-editor>

Or, after npm install @rr0/ufoathome: import "@rr0/ufoathome/editor". The bundle is 293 KB gzipped and registers the tag itself — nothing else to call. The overview above says what else that involves, and sharing an observation has the whole example, to try and to copy.

Sources and choices#

Nothing the tool draws is invented. Each value comes from a named source, read at a stated moment, and where no record exists the interface says so instead of filling the gap.

The principles#

  • A source is read only when it is needed. Catalogues are built once into the repository; services are called only for a recording that needs them (a place, a date in their span, a button pressed), and never for a date before their record begins.
  • What may disappear is hosted here. Data that a third party could take down or that cannot be read from a browser (orbital elements, 3D models) is copied, reduced, and served from ufoathome.org with its credit, so any page embedding a scene gets it.
  • Measured beats modelled, and modelled beats assumed. A brightness is a published observation where one exists; a formula fills in only what nobody measured, and an object with neither is computed but not drawn.
  • Every service is swappable. Weather, elevation, imagery, place, time zone, models and drafting go through one registry: the source is a visible choice, with its credit next to it, and a second one can be added without touching the rest.
  • The limits are stated. Each source below says where it stops: before 1940 for the weather, before 1957 for satellites, before February 2021 for individual passes.

The sky · The air · Satellites · Aircraft and their trails · Weather · The ground · Place and time · Decor, pictures and sound

The sky

SourceWhat it providesWhen it is readHosting and creditChoices and limits
astronomy-engine Positions and magnitudes of the Sun, Moon and planets; the Moon's phase; sky coordinates; and, for an eclipse, how much of the Sun the Moon hides from this place at this instant. In the page, for every recording with a date. Bundled library, MIT. A full ephemeris rather than approximate formulas: the reason to reconstruct a sky is to be right about it. Its precession also carries the year-2000 catalogues (stars, the Galaxy, meteor radiants, novae) to the date observed.
HYG Database Star positions, magnitudes and colours; names of stars down to magnitude 3. Built once (build:stars). The page loads stars to magnitude 7.5, and to 9 only for an instrument that reaches that deep. Served with the scene bundle. CC BY-SA. Cut at 9 because the catalogue itself thins out beyond: star counts stop growing as they should.
JPL Horizons The orbit of each naked-eye comet since 1910, as it was at that apparition's perihelion. Built once (build:comets); the page propagates the orbit itself, no request. Generated into the code. NASA/JPL. Brightness is NOT taken from JPL: its magnitude parameters put NEOWISE 4 magnitudes too faint. Peak magnitudes observed at the time are entered by hand, with their dates. A tail is drawn only where its length was recorded.
AAVSO, via Strope, Schaefer & Henden 2010 The measured light curve of every nova since 1891 that reached magnitude 5, binned from the AAVSO's observations. Built once (build:novae) with the supernovae below; the page interpolates the curve, no request. Generated into the code. VizieR catalogue J/AJ/140/34. A recorded curve rather than a decline formula: the standard laws miss most real novae, and would draw DQ Herculis bright for a year through the weeks dust had dimmed it ten magnitudes. Nothing is drawn before the first observation or after the last. RS Oph 2021 and T CrB 1866 reuse the curve of their own previous eruption, shifted onto their recorded peak.
Historical supernovae The supernovae of 1006, 1054, 1181, 1572, 1604 and 1987A: where they stood, and how bright on each night. Built once (build:novae). Generated into the code. Positions from SIMBAD; 1572 and 1604 reduced by Ruiz-Lapuente (2004, 2017) from Tycho's, Kepler's and the Korean astronomers' own estimates; 1006, 1054 and 1181 from the chronicles (Stephenson & Green); 1987A from the Open Supernova Catalog. Each curve says which kind it is, because they differ by two orders of magnitude in quality: 1054 and 1181 are a peak and the day they were last seen, and the decline of 1006 is borrowed from 1572, the same kind of explosion. Dates in the Julian calendar are read as such. V1369 Cen (2013), whose AAVSO record is not reachable by a script and has no published table, is read out of the vector markers of a published figure (Izzo 2017, arXiv:1704.07214), within the part that figure shows.
IMO meteor shower list Radiant, activity window, peak hourly rate and speed of the annual showers; a sporadic background. Table in the code, computed for any date. In the code. International Meteor Organization. The hourly rate is a multi-year average, not a forecast for one night: outbursts and storms are not in it.
Global Meteor Network, trajectories The fireballs its cameras triangulated since December 2018: where each began and ended, at what height, for how long, and how bright at its peak. Reduced once (build:fireballs). A scene fetches the month of its recording only, and nothing for an earlier date. Served from ufoathome.org/fireballs/, credited in the player's credits. CC BY 4.0. Of three and a half million meteors, only absolute magnitude -3 and brighter are kept: the fainter are the sporadic background the scene computes. Brighter than -9 is dropped: past -8 the counts stop falling as meteors do and level into a tail reaching -45, the photometry of a saturated camera. One event solved twice is kept once. The brightness drawn is the peak all along the path, eased in and out, since the record gives the peak and not the light curve. FRIPON's detections are archived too but not used: its site asks to be contacted for any use.
Milky Way, zodiacal light, sky brightness The glow of the Galaxy and of the zodiacal dust, and the brightness of the sky by twilight and moonlight. Computed in the page, no data fetched. Models from the literature: Leinert et al. 1998 and Hong 1985 (zodiacal light), Patat et al. 2006 and Krisciunas & Schaefer 1991 (sky brightness against which the glows are judged; the light of the sky itself is under the air). No photograph of the Milky Way: a model Galaxy integrated along the line of sight, checked against three measurements rather than fitted to one.
World Atlas of the artificial night sky brightness (Falchi et al. 2016) How bright the night sky of a place is with its towns, the zenith of a moonless night in magnitudes per square arcsecond; what turns a clear sky into a polluted one. Cut once into small tiles (build:light-pollution). The editor fetches one tile, a few kilobytes, when a place has no stated value; the player reads the value the recording carries. Tiles served beside the page, then from ufoathome.org, with the credit kept in their index. The atlas is CC BY-NC 4.0, a non-commercial licence: its use on a public site is still to be settled. A measurement of 2015 stands for any date: the atlas cannot say how a town lit up or dimmed since. It is only what the editor proposes: a value the witness or the investigator states (a Sky Quality Meter reading) wins, and a recording with none is played under a natural sky.
NASA LOLA shape map of the Moon (LDEM_16) The relief of the Moon's edge, so that the limb is not a perfect circle: the mountains and craters that let the Sun's last light through as Baily's beads. Cut once (build:lunar-limb) into a file loaded with the scene, the first time an eclipse comes near totality. Served with the scene bundle, about 2 MB. NASA, Planetary Data System. 16 samples a degree, and only the band the limb can ever fall in (libration and the observer's parallax included), kept in units of 100 m: enough for the beads, a fraction of the 33 MB map. Which eclipse there is, and how much of the Sun is covered, is never listed: it is computed from the real positions of the two bodies.

The air

The light of the sky is computed, not drawn: sunlight and moonlight traced through a spherical atmosphere of air, haze and ozone at fifteen wavelengths, then shown as an adapted eye or a camera would. Every input below is a published measurement or model; the two display anchors are the only choices.

SourceWhat it providesWhen it is readHosting and creditChoices and limits
Hillaire 2020; Bruneton & Neyret 2008 The method: single scattering along each line of sight, and every higher order folded into a table of the light arriving at each height for each height of the Sun. Tables built on the graphics card once per page and per haze level (about a second of GPU, shared by every scene of the page); the sky view redrawn when the Sun, the Moon or the observer move. Implemented in the code, twice: a reference in TypeScript and its GPU twin. Chosen for being fast enough for a web page. Checked against a Monte Carlo trace of the same atmosphere: within 0.25 magnitude and 0.012 in colour from a hazy noon to a Sun at -16°, except the Earth's shadow in nautical twilight, 0.7 magnitude too dark. The table resolution was measured, not picked: coarser tables put a -10° zenith more than half a magnitude off.
Bodhaine et al. 1999 How strongly air scatters light at each wavelength. Computed in the page. Formula from the paper. Derived from the refractive index of air, measured to many figures: nothing to adjust.
Serdyuchenko-Gorshelev ozone cross-sections Ozone's absorption across the visible (the Chappuis band), which makes a twilight zenith blue. Averaged once into the code. IUP Bremen, Gorshelev et al. 2014 and Serdyuchenko et al. 2014 (Atmos. Meas. Tech. 7). Taken at 223 K, the stratosphere's temperature; the band changes by 3 % at most between 193 and 293 K. Without it, a zenith six degrees after sunset comes out nearly white (Hulburt, 1953), and a test checks that it does.
ASTM E 490-00a Sunlight above the atmosphere, wavelength by wavelength. Averaged once into the code. The standard's table 3. Averaged over 20 nm, which removes the Fraunhofer lines: right for a sky, wrong for a spectrograph. It adds up to the Sun's measured illuminance, 128 to 133 thousand lux, which a test checks. The Moon's sky uses the same spectrum, scaled by the Moon's magnitude.
Kasten 1969; Hänel 1976 How milky the clear sky is: the haze's optical depth, from the relative humidity of the weather record. With the weather: the humidity comes from ERA5's temperature and dew point (Magnus formula, Alduchov & Eskridge 1996). Growth law from the literature. Haze particles swell with water as the air nears saturation; a typical continental burden swells as (1 - humidity) to the power -0.3, giving the usual 0.1 at 50 %. How many particles there were (a town, a fire, the wind's history) is in no record, and is not guessed. Capped at 95 %: beyond, it is mist.
Patat et al. 2006 (Paranal) The measured brightness of the zenith through twilight, the check a real sky gives the model. In the tests, and in the page for whether the Milky Way and the zodiacal light could be seen. The paper's V-band fit, table 1. Never fitted to: the model meets it within half a magnitude from -6° to -16°. Reading the published fit also corrected a table in the tool that had civil twilight ten times too dark.
Krawczyk et al. 2005; Thompson et al. 2002 How an eye adapted to that sky shows it: a compressive response that follows the sky's own brightness, and the shift to a dim blue-grey as the rods take over. In the page, for every pixel of the sky. Blend of the rods from Krawczyk, Myszkowski & Seidel; night blue from Thompson, Shirley & Ferwerda, taken halfway toward white. The only choices in the chain, stated as such: a clear day zenith and a moonless night zenith are pinned where the tool showed them before, and everything between follows. A camera (a recording medium) gets no rods, and its gain over an eye brightens what it records.

Satellites

Two levels. For every date since Sputnik, what the Earth's shadow allowed and how many objects were in orbit. From February 2021, which satellites really crossed the sky, where, and how bright.

SourceWhat it providesWhen it is readHosting and creditChoices and limits
CelesTrak SATCAT Launch and re-entry date of every tracked object: the number in orbit each month, and when each class existed (Echo, Iridium flares, ISS, Starlink). Built once (build:satellites); also gives launch dates to the orbital element archive. Generated into the code. CC BY 4.0. Only the two date columns are read here (the re-entry list below also reads the inclination). Its orbit columns describe each object's LAST state: Echo 1 appears at 400 km when it flew near 1,500. Debris is not counted.
CORDS, The Aerospace Corporation Every re-entry it followed since 2000: its predicted time with the uncertainty stated for it, and for about a hundred the time it was seen coming down and where from. Reduced once (build:reentries), with the SATCAT's decay days for every year since 1957; the editor fetches the year of the recording only. Served from ufoathome.org/reentries/. Archived by hand, because the site cannot be read by a script. A re-entry is stated with its precision and nothing finer: a sighting on record within an hour and 1,500 km, a prediction whose window meets the observation, or a bare decay day. Only the first says it could have crossed this sky. An orbit whose inclination keeps it out of sight of the observer's latitude is never listed: drag lowers an orbit but leaves its plane, which is why this one SATCAT orbit column is read.
Orbital element archive, Laurent Chabin (SCEAU) The public element sets saved once or twice a day since 22 February 2021: the naked-eye list, every Starlink, and to January 2025 the full catalogue. Reduced once (build:tle). The page fetches only the two weeks around the observation, and only for a date the archive covers; the propagator itself is loaded only then. Served from ufoathome.org/tle/, credited in the player's credits. Hosted here because the original server cannot be read from a browser. 2.8 GB of snapshots are kept as 113 MB: every day for naked-eye objects and for Starlinks in their first 60 days (the trains), one set a week otherwise. Debris and objects nobody measured are dropped. A set is never used more than 7 days from its epoch, so a hole in the archive (three months in late 2025) is reported as a hole rather than filled with a stale orbit.
satellite.js (SGP4) The standard propagator for these element sets, and the Sun's position; the Earth's umbra and penumbra are computed on top. Loaded on demand, for covered dates only. Bundled library, MIT. Held on the release before the one that ships a WebAssembly build that breaks the bundle, for no gain here.
Satellite brightness: McCants, Stellarium, Mallama et al. How bright each object looks: standard magnitudes (McCants, completed by Stellarium's list for recent objects), and published means for the constellations nobody catalogued (Starlink, OneWeb, BlueBird). Merged into the archive at build time; applied in the page. Starlink 5.93 then 7.21 (arXiv:2006.08422, 2101.00374), 4.58 below 357 km while raising orbit (2405.12007); OneWeb 7.18 (2012.05100); BlueBird 3.77 then 4.32 (2608.23668). Without the orbit-raising value no Starlink train would ever be drawn. An unmeasured rocket stage takes the median of at least three identical measured stages; any other unmeasured object is computed but not drawn. Glints and flares are not modelled: they can only make a satellite brighter.

Aircraft and their trails

From 2022, the aircraft that were in the observer's sky at the hour of a recording, the air they flew in, and the trail their engines left there. What is drawn is a compatible candidate, never an identification, and an empty sky in the record excludes nothing.

SourceWhat it providesWhen it is readHosting and creditChoices and limits
adsb.lol open history Where every aircraft that broadcast its position was, from 2022: its track, and its type, registration and category. Each is drawn with its real size, lamps (from dusk, by kind of aircraft), the Sun that still lights it after the ground has lost it, the haze between, and the sound it makes if it can be heard. Reduced once per day (build:aircraft) into one packed file per hour and per 1-degree tile. The page reads only the tiles within 150 km of the observer, for the hour of the recording and the five minutes before it, and never for a date before 2022. The code that draws, labels and plays the aircraft is loaded only when a scene has some. Built from the daily releases of the adsb.lol history (ODbL 1.0), credited in the player's credits. Not hosted yet: until the archive is served from ufoathome.org/aircraft/, aircraft show only from a local copy. One feeder instance is enough: prod-0, with staging-0 as a fallback, hold the same flights to 0.02 %. Only the positions of a release's own UTC day are kept, so the result does not depend on the order days were read in; airport vehicles are dropped. What no receiver heard is absent: light aircraft, gliders, balloons and military aircraft that switch their transponder off are under-represented, and about one aircraft in seven states no type, so it is drawn as a generic airliner.
amvlab aircraft models (A320, 737, A350, A380, 787) The airframe of an Airbus A320, A350 or A380, or of a Boeing 737 or 787, for an aircraft of those types that is near enough for its shape to be seen: engines, wing tips, tail. Fetched by the page only for an aircraft of those types within about 130 times its own length of the observer (5 km for an A320, 10 km for an A380), once for the scene (170 to 250 KB each). Further off an aircraft is a few pixels, and the built-in shape and its lamps are drawn. Re-hosted on ufoathome.org/models/ with their licence, CC BY 4.0, © 2026 amvlab, and credited in the player's credits. The plain variants, without a livery, since the airline is not known. One model for the whole A320 family, one for every 737, one for the A350 (-900 and -1000), one for the A380 and one for the 787 (-8, -9 and -10), each scaled to the real length of the type (an A321 is the A320 model drawn longer). Which variant each depicts, and how faithfully, is not documented by their author. Every other type keeps the built-in shape: a model is a catalogue entry and a row of a table, not code.
Open-Meteo, historical forecast API Temperature, humidity and wind at 500, 400, 300, 250, 200 and 150 hPa, hour by hour, from 2022: the air the aircraft fly in. Fetched by the page, once per recording that has aircraft, for the observer's place and the hours of the window: never for a date before 2022, and never for a scene with no aircraft. Open-Meteo.com, CC BY 4.0, no key, credited in the player's credits. Not the reanalysis the weather uses: its archive serves no pressure levels. These are the forecasting models' own analyses, which are known to underestimate how often the air at cruise level is supersaturated over ice, so a trail that lasts in the sky may be one the record says should not. Read at the observer's place only: an aircraft a hundred kilometres off flies in air the model may state differently. The humidity is relative to water (checked against the ceiling the data make at saturation over ice, near 60 % at -47 C).
Schmidt-Appleman criterion (Schumann 1996) Whether an engine's exhaust leaves a trail in that air, and whether it lasts. The exhaust cools along a line fixed by the engine's efficiency and the pressure; a trail forms if that line crosses saturation over water, and it lasts only where the air is saturated over ice. Computed in the page for each position of each aircraft, at the pressure its barometric altitude is (a barometric altitude is a pressure), and carried by the wind at that level. Formula and constants in the code, with the saturation pressures of Murphy and Koop (2005). The engine is a typical one for the kind of aircraft, not the one of that airframe: overall efficiency 0.38 for a wide-body, 0.34 for a narrow-body, 0.30 for a business jet, 0.22 for a fighter. Propellers, rotors, gliders and balloons are given no trail. How a trail ages is a parameterisation of observed orders of magnitude, not a simulation: its width grows with the wind shear between the two levels either side of the aircraft (how much the wind changes with height) and a fixed turbulent diffusion, its depth is assumed, and it is drawn as wide across the line of sight whichever way the shear points; how it fades and how long a short one lasts are assumed too. A trail drawn is estimated, never a fact; and one left by an aircraft that had gone before the five minutes the record is asked for is missing.

Weather

SourceWhat it providesWhen it is readHosting and creditChoices and limits
Open-Meteo: ERA5 (Copernicus/ECMWF), then its forecast model for the last days Hourly cloud cover by level, precipitation, snow, weather code, wind, temperature and dew point, at the place and hour of the observation. Fetched by the editor once a date and a place are known, from 1940 only. For the last five days, which ERA5 has not caught up with, the forecast model's own analysis is read instead (the file names which one answered). Open-Meteo archive and forecast APIs, keyless. © Copernicus/ECMWF. A reanalysis on a ~28 km grid, not a station: a local shower can be missing. Cloud bases are derived (temperature and dew point for the low deck), not measured. Every field can be unlocked and replaced by the account. No vertical profile is available in archive mode, which is what radar propagation anomalies would need.

The ground

SourceWhat it providesWhen it is readHosting and creditChoices and limits
IGN LiDAR HD (bare-earth model, MNT) and RGE ALTI The relief round the observer in France, as the laser measured the bare earth, a point every metre: trees and roofs taken out. Fetched with the relief, one picture of the square at four metres to the pixel (under a megabyte), first from the laser and, where it has not flown yet, from the RGE ALTI, the older metre-grid model that covers the whole country. © IGN, Licence Ouverte 2.0, served by the Géoplateforme's WMS, which answers browsers from anywhere. Outside France, or when the service does not answer within twelve seconds, the worldwide relief below is used. The worldwide relief is a model of the surface at fifteen metres to the pixel: measured against the laser, it is a metre or two off near the observer, and five to twenty metres off under forest or beside a road on an embankment, which sinks or lifts every building stood on it. The grid is never mixed from two surveys vertex by vertex, which would draw their seam as a cliff. Where it answers the ground is a mesh with a vertex every seven metres, not 28: a road on an embankment is thirty metres across, and a coarser mesh sank the motorway of Silly-le-Long two metres and buckled it along its way. Where the laser saw the deck of a bridge, the ground is raised to the deck (the laser takes bridges out of the bare earth, so the road would otherwise dive into a trench at every overpass); a viaduct over a wide valley leaves a dam a deck wide across it. For an observer who stands still and has scenery out beyond the first 900 m (the silos on the horizon at Maffliers), the patch of real ground is widened, to 3 km at most, so that what stands there stands on the relief and not on a flat disc; a drive's patches follow it and are not widened.
AWS Terrain Tiles (SRTM, ETOPO1) Relief around the observer. Fetched when a scene has a real place, outside France or wherever the IGN's relief above does not answer. Amazon S3, keyless. © AWS Open Data. No key and no rate limit, which an embeddable component needs. The relief model is too coarse for trees and buildings: a picture of the place lined up in the scene is the way to state those.
Esri World Imagery Aerial imagery draped on the relief, and the observer map. Fetched with the relief, and when the map is opened. Esri tiles, keyless. © Esri, Maxar, Earthstar Geographics. Today's photograph of the ground, whatever the year of the account; the scene darkens it to the hour. EOX Sentinel-2 cloudless can be picked instead.

Place and time

SourceWhat it providesWhen it is readHosting and creditChoices and limits
Nominatim (OpenStreetMap) Searching a place by name, and naming a point. Only when the author presses Enter or the search button, never at each keystroke. © OpenStreetMap contributors. It knows hamlets, farms, roads and airfields, where accounts happen; its usage policy is why nothing is sent while typing.
Open-Meteo time zones and the IANA database The time zone of a place, then its legal offset on the date of the account. Once a place is known; the historical offset comes from the browser. © Open-Meteo; tz database in the browser. Today's offset is never used for an old date: France was at UTC+1 all year in 1965.

Decor, pictures and sound

SourceWhat it providesWhen it is readHosting and creditChoices and limits
IGN LiDAR HD (canopy height model and classification) and BD TOPO (map vectors) The trees, buildings, bridges and overhead lines round a place, at their measured place, height and width: from the laser, the height of everything that stands above the ground and what each point was taken to be (plant, roof), as pictures of a hundred-metre square; from the map (BD TOPO), the centre line of each bridge, the width of the road it carries, and each electric line with its voltage; the names of what it names (a town hall, a post office, the road a bridge carries). In France the roads of the scene come from the same map, with their names, instead of from OpenStreetMap, with a pavement along each side of its urban streets (the map says which are urban), and paint on the paved roads wide enough for two ways: as the road is, from what the map says of each section: a carriageway of a motorway has its edges whole (the median's, and the shoulder's where its lanes end) and a dashed line between its lanes; a two-way road has its middle dashed, whole in the bends, and its edges whole from seven metres or 80 km/h and in short dashes below; a road of four lanes has a double whole line in the middle; none on a roundabout, a lane or a track. Where the map has no lanes or direction (the roads of OpenStreetMap often have none), the road is painted as any wide road, an edge line each side and the dashed middle. The map has no barriers: one is drawn, midway, between two carriageways of a motorway that go opposite ways side by side. All of it over a mottled grain of asphalt, within 450 m of the patch of ground, and its code is loaded only for a scene that has such a road. By itself, from the survey the author chose with the selector beside the list (the laser and the map together by default, either alone, or none), when the author gives the recording a place the survey covers (France, a box that the service itself narrows by answering nothing where it has not flown), a moment after the last change of the place; never when a recording is merely loaded. © IGN, Licence Ouverte 2.0, served by the Géoplateforme's WMS, which answers browsers from anywhere. The survey date is not given by the service. The bridges and lines come from the same Géoplateforme's WFS; the pylons stand at the vertices of the line as the map draws them, 22 m tall for 90 kV where the laser did not measure them. The height model alone reads a roof as readily as an oak (291 m at the Eiffel Tower), so the classification is asked for too and only its plants and roofs are kept. It is a scan of one day, and a recording is of another: what it brings is marked as coming from a survey, replaced when the place changes, and left alone where the author has placed trees or buildings.
IGN aerial photograph (ORTHOIMAGERY.ORTHOPHOTOS) The colour of each building's roof, seen from above: the median of the photograph under its outline, without the greens, lifted a little because the photograph is already lit by the sun. With the scenery of a survey (see above), in the same request: a square of at most a thousand pixels. © IGN, Licence Ouverte 2.0, from the Géoplateforme's WMS. The photograph is of the day it was flown, not of the recording. The shape of a roof (flat, two slopes, four slopes, which way the ridge runs) is read from the laser's heights inside the outline, within 150 m only; a roof that cannot be read stays a box rather than a guess.
INSEE, Enquête Emploi du temps 2009-2010 (via Rauch, Économie et statistique 522-523, 2021) When the people of a town are up, for how many of the windows of its buildings are lit by the hour on the clock: half asleep at 22:40, half up at 7:00, nine in ten asleep from 1:00 to 6:00. Built into the lit windows of the scene; nothing is fetched. Published by INSEE; only the three figures above are used. The survey gives medians and a bound, not a curve: the spread round the medians, the two in a hundred always up, and the half of the windows lit when everybody is up are assumptions, and France's time use is taken for any place.
OpenStreetMap buildings (Overpass) The buildings round a place anywhere in the world, as OpenStreetMap's volunteers drew them: each outline as a rectangle (or rectangles), its height where it states one (height, building:levels), its roof (roof:shape, roof:height, roof:colour), the colour and material of its walls, its name. No trees. By itself, for a place outside the reach of the IGN's surveys when no survey was chosen (or when the IGN's laser turns out not to have flown there, as over Belgium), or when the author chooses it from the list of surveys. © OpenStreetMap contributors (ODbL), through the public Overpass instances, one request at a time like the roads. They are busy: a town's buildings can take a minute or more, and the spinner over the picture can be stopped by choosing "None". Most buildings carry only building=yes: they are drawn with the ordinary measures (two storeys, a flat roof, a pale plaster). Only what is written is drawn as written. The coverage is as good as the volunteers': a Belgian town counts 4,600 buildings in a square kilometre and a half, a village of the American West a handful.
OpenStreetMap street lamps (Overpass) The public lamps round a place: each lamp mapped (highway=street_lamp), and a lamp every 35 m, on alternate sides, along each street tagged lit=yes. They are switched on by the dark (the sun below the horizon), and the nearest eight light the scene. With the scenery of a survey (see above), in the same moment. © OpenStreetMap contributors (ODbL), through the public Overpass instances, one request at a time like the roads. Volunteers tag a street as lit far more than they place its lamps (three lamps and eight lit streets in the centre of Saint-Lô), so the lamps along lit streets are drawn, not surveyed. A street not tagged is not necessarily dark. Many French towns switch their lighting off at night: a recording of such a night should have its lamps removed or switched off.
3D models (Kenney, Jagobo, Jungle Jim, Nicholas-3D, amvlab, Poly by Google, MakeHuman) Cars, a house, trees, a street light, airliners (A320, 737, A350, A380, 787), people; and the craft and beings of the cases, built by the project from their accounts. When a decor object or a body names a model; the catalogue itself is read once. Re-hosted on ufoathome.org, each with its licence file: CC0 (Kenney, MakeHuman and the project's own), CC BY 4.0 (the trees, the airliners of amvlab, the A320 flight deck of davidmarton1987), CC BY 3.0 (Poly by Google). The catalogue is read from /models/index.json beside the page first, then from ufoathome.org, so a site can serve its own copy. Each credit is shown in the player's credits. Hosted here because free kits sit on mirrors of uncertain permanence. A model whose credit is unknown is not drawn; Google Earth models were refused for their licence, and the stylised broadleaf tree for not being realistic enough. People are built from the MakeHuman system assets alone, whose licence is CC0: community packs are not used, their licences differ. A craft or a being is made from the documents of its case by a script that says which of its details are assumed, and any better model replaces it by taking its id. Every model is a placeholder of the right kind, not of a particular make or year, and the picker says so. A model publishes its own settings (a costume, the leaves, a hull, retracted landing gear, which lamps and which noise suit it), which the editor offers and a recording states in modelParameters. Whenever a model cannot be had, and when the observer stands inside the object, the built-in shape is drawn instead.
Panoramax Street-level photographs and panoramas near the place, each with its position and direction. Only when the author searches for pictures in the editor. Panoramax API, credited with each picture's author and licence. Open and readable from any page. Google Street View was refused: its terms keep the pictures inside Google's viewers.
Weather sounds (OpenGameArt) Rain, wind and thunder. Loaded when the weather of the scene needs them. Bundled. Rain and wind CC0; thunder by Jerimee, CC BY 3.0, credited in the player. The phenomenon's own sound is synthesized from its description, not taken from a recording.

The detail of each choice, with the measurements that settled it, is in the code comments of each source in the repository.