@rr0/ufoathome 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,304 @@
1
1
  <img src="doc/web/ufoathome/UFOAtHome.png" align=right alt="UFO@home logo">
2
2
 
3
- The UFO@home project aims to provide a standardized and efficient way to report and replay UFO sightings.
3
+ # UFO@home
4
4
 
5
- See its [Wiki](https://github.com/RR0/UfoAtHome/wiki).
5
+ **UFO@home** lets a UFO witness record the shape, appearance and movement of what they saw — and replay it like a VCR —
6
+ instead of relying only on a written or spoken account. The approach follows [Roger Shepard's
7
+ recommendation](https://rr0.org/time/1/9/6/8/07/29/Symposium/Shepard/index_fr.html) that a visual reconstruction of a
8
+ testimony is more faithful than an oral or written one.
9
+
10
+ Originally a Java applet (2003), the project has been rewritten from scratch in TypeScript: a small,
11
+ dependency-light engine (keyframe timeline, recording, playback, Canvas2D rendering) wrapped in four vanilla
12
+ [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) — no UI framework, no build step
13
+ required by the consuming page. One of the four (`<rr0-scene>`) does pull in [Three.js](https://threejs.org/)
14
+ for an optional 3D backdrop — see [`<rr0-scene>`](#rr0-scene--3d-decor) below for why that's an isolated,
15
+ opt-in bundle rather than a project-wide dependency.
16
+
17
+ ### Naming
18
+
19
+ `<rr0-ufo>` is the UFO's own 2D shape/appearance/movement layer — no "player" suffix, since read-only playback is
20
+ its default behavior and `<rr0-ufo-recorder>` is the one that needs a qualifier (it *adds* recording on top).
21
+ `<rr0-scene>` is named without "ufo" on purpose: it only renders a generic 3D decor (sky/horizon/stars) from a
22
+ real-world time and place, with no UFO-specific logic of its own — today it composes a nested `<rr0-ufo>` for the
23
+ common case (see its section below), but the decor itself could back other kinds of reconstructions later. A fully
24
+ generic version (accepting arbitrary overlay content instead of always creating its own `<rr0-ufo>`) is a natural
25
+ follow-up, not implemented yet. `<rr0-ufo-witnesses>` follows the same "adds a qualifier" logic as the recorder:
26
+ it *adds* a multi-witness selector on top of `<rr0-ufo>`, for cases that have more than one witness's own
27
+ recording of the same sighting.
28
+
29
+ See the [Wiki](https://github.com/RR0/UfoAtHome/wiki) for the project's history, and a live example embedded in
30
+ [rr0.org's UFO@home page](https://rr0.org/science/crypto/ufo/enquete/projet/UfoAtHome.html) and in its
31
+ [Chiles-Whitted case reconstruction](https://rr0.org/science/crypto/ufo/enquete/dossier/ChilesWhitted/index.html).
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install @rr0/ufoathome
37
+ ```
38
+
39
+ Four self-contained, pre-built ES modules are published — each self-registers its custom element as soon as it's
40
+ imported, no explicit setup call needed:
41
+
42
+ ```html
43
+ <script type="module" src="/node_modules/@rr0/ufoathome/dist-embed-ufo/rr0-ufo.mjs"></script>
44
+ <script type="module" src="/node_modules/@rr0/ufoathome/dist-embed/rr0-ufo-recorder.mjs"></script>
45
+ <script type="module" src="/node_modules/@rr0/ufoathome/dist-embed-scene/rr0-scene.mjs"></script>
46
+ <script type="module" src="/node_modules/@rr0/ufoathome/dist-embed-witnesses/rr0-ufo-witnesses.mjs"></script>
47
+ ```
48
+
49
+ or, from a bundler:
50
+
51
+ ```ts
52
+ import "@rr0/ufoathome/ufo" // registers <rr0-ufo>
53
+ import "@rr0/ufoathome/recorder" // registers <rr0-ufo-recorder> (and <rr0-ufo>, which it composes)
54
+ import "@rr0/ufoathome/scene" // registers <rr0-scene> (and <rr0-ufo>, which it composes)
55
+ import "@rr0/ufoathome/witnesses" // registers <rr0-ufo-witnesses> (and <rr0-ufo>, which it composes)
56
+ ```
57
+
58
+ Only load the one(s) a given page actually needs — `rr0-scene.mjs` in particular pulls in Three.js and is far
59
+ heavier than the others (see its section below), so pages that just need playback should stick to `rr0-ufo.mjs`.
60
+
61
+ ## `<rr0-ufo>` — read-only playback
62
+
63
+ The lightweight component (~9KB): a canvas plus Play/Pause/Loop/seek controls. Use it wherever a page only needs to
64
+ *replay* an already-recorded sighting — this is the one to embed in content pages.
65
+
66
+ ```html
67
+ <rr0-ufo src="sighting.json"></rr0-ufo>
68
+ ```
69
+
70
+ | Member | Kind | Description |
71
+ |---|---|---|
72
+ | `src` | attribute | URL of a [`SightingRecordingJson`](#data-format) file, fetched automatically on connect and whenever the attribute changes |
73
+ | `sightingData` | property (get/set) | The current recording as a plain [`SightingRecordingJson`](#data-format) object |
74
+ | `sighting` | property (readonly) | The live `Sighting` model (real-world time/place + the recording's `Timeline`) |
75
+ | `canvasElement` | property (readonly) | The underlying `<canvas>` element |
76
+ | `renderer` | property (readonly) | The `CanvasRenderer` instance painting onto that canvas |
77
+ | `refresh()` | method | Re-reads the timeline's duration into the seek slider and repaints the current frame — call after externally mutating `sighting.timeline` |
78
+ | `loadFromSrc(url)` | method (async) | What the `src` attribute triggers internally; can be called directly too |
79
+ | `enableClickToPlay` | property (get/set, default `true`) | Whether clicking the canvas toggles Play/Pause (see below). Composing elements that need the canvas's own click for something else set this to `false` — see `<rr0-ufo-recorder>`. |
80
+ | `fullscreenTarget` | property (get/set, default: the component's own stage) | The element the fullscreen button requests fullscreen on. Composing elements that need a *different* element fullscreened set this — see `<rr0-scene>`. |
81
+
82
+ Playback matches the observation's *real reported duration* when it's known: set `time`/`endTime`, or `time`/
83
+ `durationSeconds`, in the [data format](#data-format) (`durationSeconds` takes precedence over `endTime` if both are
84
+ given). Watching a 5-minute sighting then takes 5 real minutes, not however long the recording itself took to
85
+ author (e.g. a quick mouse drag) — drag the seek bar directly to skip ahead. The start/end labels around the seek
86
+ bar show real clock times when `time` has an hour (e.g. `02:45` → `02:50`); otherwise they show `0:00` → the
87
+ duration actually available (the declared one if known, else the recording's own length). Playback loops by
88
+ default — click the loop button (pressed = looping) to play once and stop instead.
89
+
90
+ Clicking anywhere on the canvas also toggles Play/Pause (not just the button), matching common video-player UX.
91
+ While playing, the toolbar and the fullscreen button (top-right, semi-transparent over the content) auto-hide and
92
+ only reappear on hover — always shown while paused/stopped. The fullscreen button uses the standard Fullscreen API
93
+ (`requestFullscreen`/`exitFullscreen`); exiting with Escape is native browser behavior, nothing custom.
94
+
95
+ Labels (Play/Pause, Auto-replay, Current position, Duration, Fullscreen) are translated (English/French) based on
96
+ the visitor's `navigator.languages`, falling back to English — there's no language-picker UI, this is the only
97
+ mechanism.
98
+
99
+ ## `<rr0-ufo-recorder>` — full editor
100
+
101
+ The authoring component (~540KB gzip — see below for why): everything `<rr0-ufo>` has, plus a shape/appearance
102
+ toolbar (oval/saucer/triangle presets, color, transparency, halo) and drag-to-record. It composes a nested
103
+ `<rr0-scene>` internally — not a bare `<rr0-ufo>` — so the shape being drawn is always seen against the
104
+ sighting's own real sky, computed live from whatever latitude/longitude/heading/orientation/observation-time
105
+ fields the toolbar currently holds (see [Architecture](#architecture)). This absorbs `<rr0-scene>`'s own
106
+ Three.js/`astronomy-engine` weight on top of the authoring-only code this element already carried (Recorder
107
+ engine, SamplingClock, appearance toolbar) — a page that only needs to *play* a sighting (the common case: an
108
+ rr0.org case dossier) should still embed the much lighter `<rr0-ufo>` (or `<rr0-scene>` alone) directly, never
109
+ this heavier authoring component.
110
+
111
+ ```html
112
+ <rr0-ufo-recorder></rr0-ufo-recorder>
113
+ ```
114
+
115
+ Usage: click **Record**, move the pointer over the canvas to draw the UFO's path, click **Stop**, then **Play** to
116
+ replay it. The nested `<rr0-ufo>`'s `enableClickToPlay` is set to `false` here — a completed recording drag also
117
+ fires a native "click" on the canvas, which would otherwise spuriously toggle playback right after recording.
118
+
119
+ All of the toolbar's own labels (shape presets, Color/Transparency/Halo, Add shape, Record/Stop, Export JSON,
120
+ Duration) are translated (English/French) the same way `<rr0-ufo>`'s own labels are — based on
121
+ `navigator.languages`, no picker UI.
122
+
123
+ | Member | Kind | Description |
124
+ |---|---|---|
125
+ | `sightingData` | property (get/set) | Delegates to the nested `<rr0-ufo>`'s `sightingData` |
126
+ | `appearance` | property (get/set, accepts a partial object on set) | `{ presetId: "oval" \| "saucer" \| "triangle", color: string, transparency: number, haloScale: number }` — the UFO's appearance used for the next recording |
127
+
128
+ ## `<rr0-scene>` — 3D decor
129
+
130
+ The environmental variant (~530KB gzip — [Three.js](https://threejs.org/) plus
131
+ [`astronomy-engine`](https://github.com/cosinekitty/astronomy)'s planetary/lunar position tables, which don't
132
+ tree-shake since they're one shared data table used internally for every body — this is by far the heaviest
133
+ of the four bundles, load it only on pages that want it): everything `<rr0-ufo>` has, composited over a 3D
134
+ sky/horizon/starfield backdrop instead of a plain background. Same markup and members as `<rr0-ufo>` (`src`,
135
+ `sightingData`, `loadFromSrc`, `enableClickToPlay`) — it's a drop-in upgrade, including click-to-play/pause
136
+ anywhere on the scene (the nested `<rr0-ufo>`'s transparent canvas covers the whole stage). The fullscreen button
137
+ fullscreens the *whole* scene (3D backdrop included), not just the nested `<rr0-ufo>`'s own overlay — it sets the
138
+ nested element's `fullscreenTarget` to its own outer stage for this.
139
+
140
+ ```html
141
+ <rr0-scene src="sighting.json"></rr0-scene>
142
+ ```
143
+
144
+ **Real astronomy for misidentification spotting.** A recurring cause of UFO reports is a mundane astronomical
145
+ object or atmospheric optical effect — Venus (by far the most commonly misreported "UFO"), other planets, the
146
+ Moon, lens flare, or halo phenomena like sun dogs/moon dogs. `<rr0-scene>` renders the sky astronomically: real
147
+ Sun/Moon/Venus/Mars/Jupiter/Saturn positions and the Moon's phase via
148
+ [`astronomy-engine`](https://github.com/cosinekitty/astronomy) (see `src/engine/astronomy/CelestialPositions.ts`),
149
+ and a real star catalog (see below) instead of a randomized field, filtered to naked-eye visibility
150
+ (magnitude ≤ 7.5) since these are human eyewitness observations, not instrument-assisted ones. The sky's
151
+ darkness/color follows the sun's altitude (day/twilight bands/night), and its dawn/dusk glow is anchored on the
152
+ sun's real compass direction, not spread uniformly around the horizon — see `src/render3d/skyColors.ts`.
153
+
154
+ The witness's own pose — geographic position, elevation, and viewing heading/pitch/field of view — can vary over
155
+ the sighting's timeline via `observerTrack` in the sighting JSON (a keyframe array alongside `timeline`, same
156
+ hold-last/interpolated-lookup shape — see `src/engine/model/ObserverTrack.ts`), driving both the camera's own
157
+ orientation and which real-world instant the astronomy is computed for as playback advances. Older recordings
158
+ with no `observerTrack` fall back to the legacy static `place[0]` (see `resolveObserverPoseAt` in
159
+ `src/engine/model/Sighting.ts`) — usable for sky darkness/color and camera pitch/fov, but with no compass heading
160
+ to orient the camera by.
161
+
162
+ `src/engine/astronomy/SunPosition.ts` (the original vanilla, dependency-free NOAA/Spencer solar position
163
+ approximation) stays in the repo, tested, and still backs `skyBrightness()`'s twilight-band classification — but
164
+ the live rendering path now uses `astronomy-engine` for the Sun too, for a single source of truth and to get the
165
+ Sun's azimuth from the same call used for the sky's directional glow.
166
+
167
+ `<rr0-ufo-recorder>` has editor fields for the witness's latitude/longitude/heading and the observation's start
168
+ date/time (all optional) — filling in lat+lng writes both the legacy `place` and a single t=0 `observerTrack`
169
+ keyframe (elevation/pitch/field of view stay at neutral defaults; there's no UI yet for authoring the observer
170
+ *moving* over time, only a single static pose per recording).
171
+
172
+ Not yet done: sun dog/moon dog/halo rendering, a real (non-flat) ground/terrain, precipitation and other optical
173
+ effects (lens flare, mirage), and a multi-keyframe `observerTrack` authoring UI (today the recorder can only set
174
+ one static pose; an observer that moves/re-orients mid-recording still needs hand-authored or scripted JSON). The
175
+ Moon's phase currently only dims/brightens its disc's overall
176
+ color rather than rendering a geometrically accurate crescent shape — a natural follow-up.
177
+
178
+ **Regenerating the star catalog.** `src/assets/stars-mag7.5.bin` (a compact binary asset, four concatenated
179
+ `Float32Array` sections: ra/dec/mag/ci — see `src/render3d/StarCatalog.ts` for the exact layout) is generated
180
+ from the [HYG Database v4.1](https://github.com/astronexus/HYG-Database) (CC BY-SA), filtered to magnitude ≤ 7.5.
181
+ To regenerate it: download `hyg/CURRENT/hygdata_v41.csv` from that repo into `scripts/data/hygdata_v41.csv`
182
+ (gitignored — not checked in, ~34MB), then run `npm run build:stars`. The generated `.bin`/`.json` pair *is*
183
+ checked in (~400KB) since it's small and doesn't need regenerating on every install.
184
+
185
+ The UFO shape itself deliberately stays a 2D overlay on top of the 3D decor, never "upgraded" to a 3D object: it's
186
+ what the witness reported — possibly a misidentification or optical effect — not something to interpret as a real
187
+ 3D shape. Only the surrounding environment, independently computable from real astronomy, is rendered in 3D.
188
+
189
+ ## `<rr0-ufo-witnesses>` — multi-witness selector
190
+
191
+ A case can have more than one witness, each with their own recording (their own `sighting.json`) of the same
192
+ event. This component lets a page switch between them instead of only ever embedding one fixed `<rr0-ufo>` — it
193
+ composes a nested `<rr0-ufo>` the same way `<rr0-scene>` does, and is otherwise just as lightweight.
194
+
195
+ ```html
196
+ <rr0-ufo-witnesses src="witnesses.json"></rr0-ufo-witnesses>
197
+ ```
198
+
199
+ `src` points not at a `sighting.json` directly, but at a small manifest — a plain JSON array of each witness's own
200
+ `SightingRecordingJson` URL (typically relative to the case's own page, same as `<rr0-ufo>`'s own `src`):
201
+
202
+ ```json
203
+ ["chiles-sighting.json", "whitted-sighting.json"]
204
+ ```
205
+
206
+ No labels or ids are duplicated in the manifest itself — each witness's display name and the shared case id
207
+ grouping them together are read from that witness's *own* file (`witnessName`/`caseId`, see
208
+ [Data format](#data-format)), so there's a single source of truth and nothing to drift out of sync. This means
209
+ every listed witness's recording is fetched upfront (to read its name), not lazily on selection — fine at the
210
+ scale a case's witness list actually has. If a witness has no `witnessName`, its `witnessId` is shown instead, or
211
+ the URL itself as a last resort. A mismatched `caseId` across the listed witnesses logs a console warning (doesn't
212
+ block) — likely means unrelated recordings got listed together by mistake.
213
+
214
+ | Member | Kind | Description |
215
+ |---|---|---|
216
+ | `src` | attribute | URL of a witness manifest (above), fetched automatically on connect and whenever the attribute changes |
217
+ | `witnessUrls` | property (get/set) | The manifest as a plain array of URLs, for programmatic use instead of `src` |
218
+ | `loadFromSrc(url)` | method (async) | What the `src` attribute triggers internally; can be called directly too |
219
+
220
+ The selector (a plain `<select>` of witness labels) is hidden entirely when there are 0 or 1 witnesses — no point
221
+ showing a dropdown with nothing to actually choose between. The first witness loads automatically once the list is
222
+ known; switching the selector loads that witness's already-fetched recording into the nested `<rr0-ufo>` (no
223
+ re-fetch). Setting `witnessUrls` again (e.g. a manifest refresh) keeps the current selection if that witness is
224
+ still present, instead of resetting back to the first.
225
+
226
+ The "Witness" label itself is translated (English/French) the same way as `<rr0-ufo>`'s own labels.
227
+
228
+ ## Data format
229
+
230
+ Both components read/write a plain, JSON-serializable `SightingRecordingJson`:
231
+
232
+ ```ts
233
+ interface SightingRecordingJson {
234
+ version: 1
235
+ time?: { year?: number, month?: number, day?: number, hour?: number, minute?: number, second?: number }
236
+ endTime?: { year?: number, month?: number, day?: number, hour?: number, minute?: number, second?: number } // alternative to durationSeconds
237
+ durationSeconds?: number // alternative to endTime; takes precedence if both are set
238
+ place?: { lat: number, lng: number }[]
239
+ witnessId?: string // opaque internal reference — no PII beyond a display name (see witnessName)
240
+ witnessName?: string // for cases where the witness is already publicly named in the published material (e.g. Chiles-Whitted) — omit for anonymous witnesses
241
+ caseId?: string // shared by every witness's own sighting.json for the same case — see <rr0-ufo-witnesses>
242
+ timeline: {
243
+ keyframes: Array<{
244
+ t: number // milliseconds since recording start
245
+ shapes: Array<{
246
+ sourceId: string // e.g. "ufo-1" — lets several shapes (a UFO, a landmark, a trailing flame...) share one timeline
247
+ shape: {
248
+ kind: "oval" | "polygon"
249
+ bounds: { x: number, y: number, width: number, height: number }
250
+ color: string // CSS color
251
+ angle: number // radians
252
+ transparency: number // 0 = opaque, 1 = fully transparent
253
+ haloScale: number // 0 = no glow
254
+ selected: boolean
255
+ points?: { x: number, y: number }[] // "polygon" shapes only
256
+ }
257
+ }>
258
+ }>
259
+ }
260
+ }
261
+ ```
262
+
263
+ This format is deliberately independent of [`@rr0/data`](https://github.com/RR0/data)'s `RR0Event`/`@rr0/time`'s
264
+ `Level2Date`/`@rr0/place`'s `Place` classes, even though its `time`/`place` fields are structurally aligned with
265
+ them — importing those classes into browser-bundled code pulls in Node-only file-scanning dependencies that break a
266
+ `vite build`. `src/engine/interop/rr0Data.ts` converts between the two for Node-side tooling (e.g. generating a
267
+ case's `sighting.json` from its `RR0Event`).
268
+
269
+ ## Architecture
270
+
271
+ - `src/engine/` — framework-agnostic core: `model/` (`Shape`, `Timeline`, `Sighting`), `record/` (`Recorder`,
272
+ `SamplingClock`), `playback/` (`Player`), `persistence/` (JSON (de)serialization), `astronomy/` (vanilla solar
273
+ position), `interop/` (real `@rr0/data` conversion, Node-only).
274
+ - `src/render/CanvasRenderer.ts` — paints shapes onto a `<canvas>` 2D context.
275
+ - `src/render3d/` — the Three.js decor renderer (`SceneRenderer`) and its pure, dependency-free color logic
276
+ (`skyColors.ts`), kept separate so the latter is unit-testable without a WebGL context.
277
+ - `src/component/` — the four Web Components. `UfoElement` (`<rr0-ufo>`) owns the canvas/playback; `SceneElement`
278
+ (`<rr0-scene>`) and `WitnessSelectorElement` (`<rr0-ufo-witnesses>`) compose it directly (via
279
+ `document.createElement`, not an inline template tag — see the comment at that call site) rather than duplicating
280
+ it, adding the 3D decor or the multi-witness selector on top, respectively. `UfoRecorderElement` composes a
281
+ `SceneElement` (not `UfoElement` directly) and reaches through to its public `ufoElement` property for the
282
+ actual canvas/timeline/appearance work — the toolbar edits the exact same `Sighting` instance the nested scene
283
+ renders from, so an observer/time/appearance change needs no separate sync step to reach the sky.
284
+ - Playback linearly interpolates shapes between a source's surrounding keyframes for smooth motion
285
+ (`Timeline.getInterpolatedShapeAt`/`Shape.lerpShape`), holding at the ends of its recorded range.
286
+ - Recording samples the pointer position at a configurable rate via `requestAnimationFrame`, not on every
287
+ `pointermove` event.
288
+
289
+ ## Development
290
+
291
+ ```bash
292
+ npm install
293
+ npm run dev # local demo (record + play), Vite dev server
294
+ npm test # vitest
295
+ npm run build # type-check + build the demo
296
+ npm run build:embed # build dist-embed/rr0-ufo-recorder.mjs
297
+ npm run build:embed-ufo # build dist-embed-ufo/rr0-ufo.mjs
298
+ npm run build:embed-scene # build dist-embed-scene/rr0-scene.mjs
299
+ npm run build:all # all four
300
+ ```
301
+
302
+ ## License
303
+
304
+ MIT
@@ -0,0 +1 @@
1
+ const e={play:"Lecture",pause:"Pause",autoReplay:"Lecture automatique",currentPosition:"Position actuelle",duration:"Durée",fullscreen:"Plein écran",exitFullscreen:"Quitter le plein écran"};export{e as ufoMessages_fr};
@@ -0,0 +1 @@
1
+ const e={oval:"Ovale",saucer:"Soucoupe",triangle:"Triangle",color:"Couleur",transparency:"Transparence",halo:"Halo",shape:"Forme",addShape:"Ajouter une forme",samplingRate:"Fréquence d'échantillonnage (ms)",duration:"Durée (s)",durationPlaceholder:"durée de l'enregistrement",export:"Exporter le JSON",record:"Enregistrer",stop:"Arrêter",latitude:"Latitude",longitude:"Longitude",heading:"Orientation (°)",headingPlaceholder:"inconnu",pitch:"Inclinaison (°)",observationTime:"Début de l'observation (optionnel)",yearPlaceholder:"année",monthPlaceholder:"mois",dayPlaceholder:"jour",hourPlaceholder:"heure",minutePlaceholder:"min"};export{e as ufoRecorderMessages_fr};