@rr0/ufoathome 0.2.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.
@@ -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};
@@ -1,6 +1,9 @@
1
- const k = `
2
- <div class="stage">
3
- <canvas id="canvas" width="640" height="360"></canvas>
1
+ const L=`
2
+ <div class="stage" id="stage">
3
+ <div class="frame" id="frame">
4
+ <canvas id="canvas" width="640" height="360"></canvas>
5
+ </div>
6
+ <button id="fullscreen" class="fullscreen-btn" type="button" title="Fullscreen" aria-label="Fullscreen">⛶</button>
4
7
  <div class="toolbar" id="toolbar">
5
8
  <button id="play-pause" type="button" title="Play" aria-label="Play">▶</button>
6
9
  <span id="time-start" class="time-label" title="Current position">0:00</span>
@@ -9,20 +12,51 @@ const k = `
9
12
  <button id="loop" type="button" title="Auto-replay" aria-label="Auto-replay" aria-pressed="true">↻</button>
10
13
  </div>
11
14
  </div>
12
- `, x = `
15
+ `,F=`
13
16
  :host {
14
17
  display: block;
15
18
  font-family: sans-serif;
16
19
  }
20
+ /* height:100% is a no-op fallback (resolves to auto) whenever .stage's own parent/host has no
21
+ definite height of its own (the normal, standalone case — .stage's height stays driven by
22
+ .frame's content, unchanged) — but it matters when this element is embedded with a definite
23
+ host size from outside (e.g. <rr0-scene>'s .ufo-overlay sizing this element to fill its own
24
+ #stage while THAT is fullscreen): it lets .toolbar/.fullscreen-btn, anchored to .stage below,
25
+ actually reach that outer element's true edges instead of only .frame's letterboxed ones. */
17
26
  .stage {
18
27
  position: relative;
19
28
  width: 100%;
29
+ height: 100%;
30
+ }
31
+ /* The browser's own fullscreen UA styles force the fullscreened element (.stage) to fill the
32
+ whole viewport (100vw/100vh) regardless of its content's aspect ratio. .toolbar/.fullscreen-btn
33
+ are anchored to .stage itself (not .frame) specifically so they stay pinned to the true screen
34
+ edges, full width, like a normal video player's controls — not stuck to the letterboxed
35
+ content's own (possibly smaller, centered) box above/around them. */
36
+ .stage:fullscreen {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ width: 100vw;
41
+ height: 100vh;
42
+ background: #000;
43
+ }
44
+ /* max-width/max-height are unconditional (not just a :fullscreen override): percentages resolve
45
+ against .stage's height, which is only definite when .stage itself has one (fullscreen, or the
46
+ nested-in-<rr0-scene> case above) — otherwise they're inert, so this is always safe. When
47
+ definite, the browser's aspect-ratio/min-max interplay algorithm correctly derives whichever
48
+ of width/height is the tighter constraint from the other — a real "contain, centered" fit,
49
+ not just a single-axis cap that can let the other axis overflow and crop instead of shrink. */
50
+ .frame {
51
+ width: 100%;
52
+ max-width: 100%;
53
+ aspect-ratio: 640 / 360;
54
+ max-height: 100%;
20
55
  }
21
56
  canvas {
22
57
  display: block;
23
58
  width: 100%;
24
- height: auto;
25
- aspect-ratio: 640 / 360;
59
+ height: 100%;
26
60
  background: var(--ufo-canvas-background, #050510);
27
61
  border: var(--ufo-canvas-border, 1px solid #333);
28
62
  box-sizing: border-box;
@@ -39,18 +73,38 @@ canvas {
39
73
  background: rgba(0, 0, 0, 0.55);
40
74
  transition: opacity 0.15s ease;
41
75
  }
42
- /* While playing, the toolbar auto-hides and only reappears on hover — kept always visible while
43
- paused/stopped, since that's when the user is most likely to want it. Deliberately hover-only,
44
- not :focus-within: a clicked button/range input keeps keyboard focus after the pointer moves
45
- away, which would otherwise keep the toolbar stuck visible indefinitely after any interaction. */
46
- .toolbar.auto-hide {
76
+ /* While playing, the toolbar and fullscreen button auto-hide and only reappear on hover — kept
77
+ always visible while paused/stopped, since that's when the user is most likely to want them.
78
+ Deliberately hover-only, not :focus-within: a clicked button/range input keeps keyboard focus
79
+ after the pointer moves away, which would otherwise keep them stuck visible indefinitely after
80
+ any interaction. */
81
+ .auto-hide {
47
82
  opacity: 0;
48
83
  pointer-events: none;
49
84
  }
50
- .stage:hover .toolbar.auto-hide {
85
+ .stage:hover .auto-hide {
51
86
  opacity: 1;
52
87
  pointer-events: auto;
53
88
  }
89
+ .fullscreen-btn {
90
+ position: absolute;
91
+ top: 0.4em;
92
+ right: 0.4em;
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ width: 1.8em;
97
+ height: 1.8em;
98
+ padding: 0;
99
+ border: none;
100
+ border-radius: 3px;
101
+ cursor: pointer;
102
+ font-size: 1em;
103
+ line-height: 1;
104
+ background: rgba(0, 0, 0, 0.55);
105
+ color: #fff;
106
+ transition: opacity 0.15s ease;
107
+ }
54
108
  input[type=range] {
55
109
  flex: 1;
56
110
  }
@@ -76,396 +130,4 @@ input[type=range] {
76
130
  min-width: 3em;
77
131
  text-align: center;
78
132
  }
79
- `;
80
- function w(i, t, e) {
81
- const { bounds: s } = i;
82
- return t >= s.x && t <= s.x + s.width && e >= s.y && e <= s.y + s.height;
83
- }
84
- class h {
85
- keyframes = [];
86
- addKeyframe(t, e) {
87
- const s = this.findInsertIndex(t);
88
- this.keyframes[s]?.t === t ? this.keyframes[s] = { t, shapes: e } : this.keyframes.splice(s, 0, { t, shapes: e });
89
- }
90
- findInsertIndex(t) {
91
- let e = 0, s = this.keyframes.length;
92
- for (; e < s; ) {
93
- const a = e + s >>> 1;
94
- this.keyframes[a].t < t ? e = a + 1 : s = a;
95
- }
96
- return e;
97
- }
98
- getKeyframeAt(t) {
99
- const e = this.findInsertIndex(t), s = this.keyframes[e];
100
- return s?.t === t ? s : void 0;
101
- }
102
- getShapeAt(t, e) {
103
- return this.getKeyframeAt(t)?.shapes.find((s) => s.sourceId === e)?.shape;
104
- }
105
- /**
106
- * Finds the most recent shape recorded at-or-before t for that source (hold-last-value),
107
- * which is what playback needs since not every source has a keyframe at every sampled tick.
108
- */
109
- getLatestShapeAt(t, e) {
110
- let s = this.findInsertIndex(t);
111
- for (this.keyframes[s]?.t !== t && (s -= 1); s >= 0; s--) {
112
- const a = this.keyframes[s].shapes.find((n) => n.sourceId === e);
113
- if (a) return a.shape;
114
- }
115
- }
116
- hitTest(t, e, s) {
117
- const a = this.getKeyframeAt(t);
118
- if (a) {
119
- for (let n = a.shapes.length - 1; n >= 0; n--)
120
- if (w(a.shapes[n].shape, e, s))
121
- return a.shapes[n];
122
- }
123
- }
124
- get duration() {
125
- return this.keyframes.length === 0 ? 0 : this.keyframes[this.keyframes.length - 1].t;
126
- }
127
- get sourceIds() {
128
- const t = /* @__PURE__ */ new Set();
129
- for (const e of this.keyframes)
130
- for (const s of e.shapes)
131
- t.add(s.sourceId);
132
- return [...t];
133
- }
134
- get allKeyframes() {
135
- return this.keyframes;
136
- }
137
- toJSON() {
138
- return { keyframes: this.keyframes };
139
- }
140
- static fromJSON(t) {
141
- const e = new h();
142
- for (const s of t.keyframes)
143
- e.addKeyframe(s.t, s.shapes);
144
- return e;
145
- }
146
- }
147
- function c(i) {
148
- if (i.year !== void 0)
149
- return Date.UTC(i.year, (i.month ?? 1) - 1, i.day ?? 1, i.hour ?? 0, i.minute ?? 0, i.second ?? 0);
150
- }
151
- function T(i) {
152
- if (i.durationSeconds !== void 0) return i.durationSeconds * 1e3;
153
- const t = i.time ? c(i.time) : void 0, e = i.endTime ? c(i.endTime) : void 0;
154
- return t !== void 0 && e !== void 0 ? e - t : void 0;
155
- }
156
- class u {
157
- constructor(t, e, s) {
158
- this.event = t, this.timeline = e, this.witnessId = s;
159
- }
160
- static create(t, e, s) {
161
- return new u({ eventType: "sighting", time: t, place: e }, new h(), s);
162
- }
163
- }
164
- class P {
165
- constructor(t, e) {
166
- this.timeline = t, this.onFrame = e;
167
- }
168
- rafId = null;
169
- state = "stopped";
170
- currentT = 0;
171
- lastWallTime = 0;
172
- /**
173
- * Multiplies elapsed wall-clock time before advancing currentT. 1 (default) means playback
174
- * takes as long as `timeline.duration` itself; UfoElement sets this to
175
- * `timeline.duration / sightingDurationMs(event)` when the sighting's real declared duration
176
- * is known, so watching it takes as long as the observation was actually reported to last,
177
- * rather than however long the recording itself took to author (e.g. a quick mouse drag).
178
- * Manually dragging the seek bar always jumps directly regardless of this rate.
179
- */
180
- playbackRate = 1;
181
- /** When true, playback restarts from 0 instead of stopping once it reaches the end. */
182
- loop = !1;
183
- play() {
184
- if (this.state === "playing") return;
185
- this.currentT >= this.timeline.duration && (this.currentT = 0, this.resolveFrame(0)), this.state = "playing", this.lastWallTime = performance.now();
186
- const t = () => {
187
- if (this.state !== "playing") return;
188
- const e = performance.now();
189
- if (this.currentT += (e - this.lastWallTime) * this.playbackRate, this.lastWallTime = e, this.currentT >= this.timeline.duration) {
190
- if (this.loop && this.timeline.duration > 0) {
191
- this.currentT %= this.timeline.duration, this.resolveFrame(this.currentT), this.rafId = requestAnimationFrame(t);
192
- return;
193
- }
194
- this.currentT = this.timeline.duration, this.stop(), this.resolveFrame(this.currentT);
195
- return;
196
- }
197
- this.resolveFrame(this.currentT), this.rafId = requestAnimationFrame(t);
198
- };
199
- this.rafId = requestAnimationFrame(t);
200
- }
201
- pause() {
202
- this.state === "playing" && (this.state = "paused", this.rafId !== null && (cancelAnimationFrame(this.rafId), this.rafId = null));
203
- }
204
- stop() {
205
- this.state = "stopped", this.rafId !== null && (cancelAnimationFrame(this.rafId), this.rafId = null);
206
- }
207
- seek(t) {
208
- this.currentT = Math.max(0, Math.min(t, this.timeline.duration)), this.resolveFrame(this.currentT);
209
- }
210
- get playbackState() {
211
- return this.state;
212
- }
213
- get time() {
214
- return this.currentT;
215
- }
216
- resolveFrame(t) {
217
- const e = /* @__PURE__ */ new Map();
218
- for (const s of this.timeline.sourceIds) {
219
- const a = this.timeline.getLatestShapeAt(t, s);
220
- a && e.set(s, a);
221
- }
222
- this.onFrame(t, e);
223
- }
224
- }
225
- const d = 6, p = d / 2, I = 20;
226
- class E {
227
- constructor(t) {
228
- this.ctx = t;
229
- }
230
- clear(t, e) {
231
- this.ctx.clearRect(0, 0, t, e);
232
- }
233
- paintShape(t) {
234
- this.ctx.save(), this.ctx.globalAlpha = 1 - t.transparency, t.haloScale > 0 && this.paintHalo(t), this.paintBase(t), t.selected && this.paintSelectionHandles(t), this.ctx.restore();
235
- }
236
- paintBase(t) {
237
- if (this.ctx.fillStyle = t.color, this.ctx.beginPath(), t.kind === "oval") {
238
- const { x: e, y: s, width: a, height: n } = t.bounds, r = a / 2, o = n / 2;
239
- this.ctx.ellipse(e + r, s + o, r, o, t.angle, 0, 2 * Math.PI);
240
- } else {
241
- const { x: e, y: s } = t.bounds;
242
- this.ctx.save(), this.ctx.translate(e, s), this.ctx.rotate(t.angle), t.points.forEach((a, n) => {
243
- n === 0 ? this.ctx.moveTo(a.x, a.y) : this.ctx.lineTo(a.x, a.y);
244
- }), this.ctx.closePath(), this.ctx.restore();
245
- }
246
- this.ctx.fill();
247
- }
248
- paintHalo(t) {
249
- this.ctx.save(), this.ctx.shadowColor = t.color, this.ctx.shadowBlur = I * t.haloScale, this.paintBase(t), this.ctx.restore();
250
- }
251
- paintSelectionHandles(t) {
252
- const { x: e, y: s, width: a, height: n } = t.bounds, r = a / 2, o = n / 2;
253
- this.ctx.strokeStyle = "lightgray", this.ctx.strokeRect(e, s, a, n), this.ctx.fillStyle = "lightgray";
254
- const b = [
255
- [e, s],
256
- [e + r, s],
257
- [e + a, s],
258
- [e + a, s + o],
259
- [e + a, s + n],
260
- [e + r, s + n],
261
- [e, s + n],
262
- [e, s + o]
263
- ];
264
- for (const [v, S] of b)
265
- this.ctx.fillRect(v - p, S - p, d, d);
266
- }
267
- }
268
- function M(i) {
269
- return {
270
- version: 1,
271
- time: i.event.time,
272
- endTime: i.event.endTime,
273
- durationSeconds: i.event.durationSeconds,
274
- place: i.event.place,
275
- witnessId: i.witnessId,
276
- timeline: i.timeline.toJSON()
277
- };
278
- }
279
- function L(i) {
280
- return new u(
281
- {
282
- eventType: "sighting",
283
- time: i.time,
284
- endTime: i.endTime,
285
- durationSeconds: i.durationSeconds,
286
- place: i.place
287
- },
288
- h.fromJSON(i.timeline),
289
- i.witnessId
290
- );
291
- }
292
- function B(i, t) {
293
- for (const e of i) {
294
- const s = e.toLowerCase().split("-")[0];
295
- if (t.includes(s)) return s;
296
- }
297
- return "en";
298
- }
299
- const A = ["en", "fr"], C = {
300
- en: () => Promise.resolve().then(() => F).then((i) => i.ufoMessages_en),
301
- fr: () => import("./UfoMessages_fr-DKnWFOyl.js").then((i) => i.ufoMessages_fr)
302
- };
303
- function D(i) {
304
- return C[i]();
305
- }
306
- const g = {
307
- play: "Play",
308
- pause: "Pause",
309
- autoReplay: "Auto-replay",
310
- currentPosition: "Current position",
311
- duration: "Duration"
312
- }, F = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
313
- __proto__: null,
314
- ufoMessages_en: g
315
- }, Symbol.toStringTag, { value: "Module" }));
316
- class R extends HTMLElement {
317
- static get observedAttributes() {
318
- return ["src"];
319
- }
320
- shadow;
321
- canvas;
322
- canvasRenderer;
323
- toolbar;
324
- playPauseButton;
325
- loopButton;
326
- seekInput;
327
- timeStartLabel;
328
- timeEndLabel;
329
- currentSighting = u.create();
330
- player;
331
- loopEnabled = !0;
332
- /** Set to false by composing elements that need the canvas's own click for something else
333
- * instead of toggling playback — see UfoRecorderElement, which uses pointerdown/pointermove on
334
- * this same canvas to place shapes while recording. */
335
- enableClickToPlay = !0;
336
- /** Matches the template's baked-in English defaults until (if ever) loadLocaleMessages()
337
- * resolves a better match — see its doc comment. */
338
- messages = g;
339
- /** The sighting's real reported duration/start, cached by updateTimeLabels() so onFrame doesn't
340
- * recompute them every animation frame — see formatPosition, which turns a `Timeline` position
341
- * (ms since recording start) into what's actually displayed. */
342
- realDurationMs;
343
- realStartMs;
344
- constructor() {
345
- super(), this.shadow = this.attachShadow({ mode: "open" });
346
- const t = document.createElement("template");
347
- t.innerHTML = `<style>${x}</style>${k}`, this.shadow.appendChild(t.content.cloneNode(!0)), this.canvas = this.shadow.getElementById("canvas"), this.canvasRenderer = new E(this.canvas.getContext("2d")), this.toolbar = this.shadow.getElementById("toolbar"), this.playPauseButton = this.shadow.getElementById("play-pause"), this.loopButton = this.shadow.getElementById("loop"), this.seekInput = this.shadow.getElementById("seek"), this.timeStartLabel = this.shadow.getElementById("time-start"), this.timeEndLabel = this.shadow.getElementById("time-end"), this.playPauseButton.addEventListener("click", () => this.togglePlayPause()), this.loopButton.addEventListener("click", () => this.toggleLoop()), this.seekInput.addEventListener("input", () => this.player.seek(Number(this.seekInput.value))), this.canvas.addEventListener("click", () => {
348
- this.enableClickToPlay && this.togglePlayPause();
349
- }), this.player = this.createPlayer(), this.updateTimeLabels(), this.updatePlayPauseButton(), this.refresh(), this.loadLocaleMessages();
350
- }
351
- connectedCallback() {
352
- const t = this.getAttribute("src");
353
- t && this.loadFromSrc(t);
354
- }
355
- attributeChangedCallback(t, e, s) {
356
- t === "src" && s && s !== e && this.isConnected && this.loadFromSrc(s);
357
- }
358
- /** Fetches a SightingRecordingJson from `url` and loads it — what the `src` attribute uses. */
359
- async loadFromSrc(t) {
360
- const e = await fetch(t);
361
- this.sightingData = await e.json();
362
- }
363
- get sightingData() {
364
- return M(this.currentSighting);
365
- }
366
- set sightingData(t) {
367
- this.currentSighting = L(t), this.player = this.createPlayer(), this.updateTimeLabels(), this.updatePlayPauseButton(), this.refresh();
368
- }
369
- /**
370
- * The live Sighting/Timeline, exposed so UfoRecorderElement/SceneElement
371
- * (which compose this element) can add keyframes to it directly as it
372
- * records, or read its time/place for lighting.
373
- */
374
- get sighting() {
375
- return this.currentSighting;
376
- }
377
- /** Exposed so UfoRecorderElement can paint a live drag preview on the same canvas. */
378
- get canvasElement() {
379
- return this.canvas;
380
- }
381
- get renderer() {
382
- return this.canvasRenderer;
383
- }
384
- /**
385
- * Re-reads the timeline's duration into the seek slider and repaints the
386
- * current frame — call after externally mutating `sighting.timeline`
387
- * (e.g. UfoRecorderElement adding keyframes while recording).
388
- */
389
- refresh() {
390
- this.seekInput.max = String(this.currentSighting.timeline.duration), this.player.seek(this.player.time);
391
- }
392
- onFrame(t, e) {
393
- this.canvasRenderer.clear(this.canvas.width, this.canvas.height);
394
- for (const s of e.values())
395
- this.canvasRenderer.paintShape(s);
396
- this.seekInput.value = String(t), this.timeStartLabel.textContent = this.formatPosition(t), this.updatePlayPauseButton();
397
- }
398
- createPlayer() {
399
- const t = new P(this.currentSighting.timeline, (e, s) => this.onFrame(e, s));
400
- return t.loop = this.loopEnabled, t;
401
- }
402
- togglePlayPause() {
403
- this.player.playbackState === "playing" ? this.player.pause() : this.player.play(), this.updatePlayPauseButton();
404
- }
405
- updatePlayPauseButton() {
406
- const t = this.player.playbackState === "playing";
407
- this.playPauseButton.textContent = t ? "⏸" : "▶", this.playPauseButton.title = t ? this.messages.pause : this.messages.play, this.playPauseButton.setAttribute("aria-label", t ? this.messages.pause : this.messages.play), this.toolbar.classList.toggle("auto-hide", t);
408
- }
409
- toggleLoop() {
410
- this.loopEnabled = !this.loopEnabled, this.loopButton.setAttribute("aria-pressed", String(this.loopEnabled)), this.player.loop = this.loopEnabled;
411
- }
412
- /**
413
- * Auto-detects the visitor's preferred UI language from `navigator.languages`, falling back to
414
- * English (already baked into the template) when none of their preferences are supported —
415
- * see selectLocale. There is deliberately no language-picker UI: this is the only mechanism.
416
- */
417
- async loadLocaleMessages() {
418
- const t = B(navigator.languages, A);
419
- t !== "en" && this.applyMessages(await D(t));
420
- }
421
- applyMessages(t) {
422
- this.messages = t, this.timeStartLabel.title = t.currentPosition, this.timeEndLabel.title = t.duration, this.loopButton.title = t.autoReplay, this.loopButton.setAttribute("aria-label", t.autoReplay), this.updatePlayPauseButton();
423
- }
424
- /**
425
- * Caches the sighting's real-world reported duration/start (see sightingDurationMs) and sets
426
- * the player's playback rate and the seek bar's end label from them, rather than from
427
- * `timeline.duration` (how long the recording itself took to author).
428
- */
429
- updateTimeLabels() {
430
- const t = this.currentSighting.event, e = T(t);
431
- this.realDurationMs = e !== void 0 && e > 0 ? e : void 0, this.realStartMs = t.time ? c(t.time) : void 0, this.player.playbackRate = this.realDurationMs !== void 0 ? this.currentSighting.timeline.duration / this.realDurationMs : 1, this.timeEndLabel.textContent = this.formatEndOfTimeline(), this.timeStartLabel.textContent = this.formatPosition(this.player.time);
432
- }
433
- /**
434
- * Turns a `Timeline` position (ms since recording start, i.e. what Player deals in) into what's
435
- * actually displayed: a real clock time (e.g. "02:47") when a real start/duration are both
436
- * known, an elapsed real duration ("0:00" based) when only the duration is known, or the
437
- * recording's own elapsed time when neither is known — see updateTimeLabels.
438
- */
439
- formatPosition(t) {
440
- if (this.realDurationMs === void 0) return l(t);
441
- const e = this.currentSighting.timeline.duration, s = e > 0 ? t / e * this.realDurationMs : 0;
442
- return this.realStartMs !== void 0 ? f(m(this.realStartMs + s)) : l(s);
443
- }
444
- /**
445
- * The fixed end-of-timeline label — always the *full* real declared duration (clock time or
446
- * elapsed), or the recording's own length when no real duration is known. Unlike
447
- * formatPosition, doesn't scale by `timeline.duration`: a single-keyframe/static recording
448
- * (timeline.duration === 0) still has a full declared real duration to show as its end.
449
- */
450
- formatEndOfTimeline() {
451
- return this.realDurationMs === void 0 ? l(this.currentSighting.timeline.duration) : this.realStartMs !== void 0 ? f(m(this.realStartMs + this.realDurationMs)) : l(this.realDurationMs);
452
- }
453
- }
454
- function m(i) {
455
- const t = new Date(i);
456
- return { hour: t.getUTCHours(), minute: t.getUTCMinutes(), second: t.getUTCSeconds() };
457
- }
458
- function f(i) {
459
- if (i.hour === void 0) return "0:00";
460
- const t = (e) => String(e).padStart(2, "0");
461
- return i.second ? `${t(i.hour)}:${t(i.minute ?? 0)}:${t(i.second)}` : `${t(i.hour)}:${t(i.minute ?? 0)}`;
462
- }
463
- function l(i) {
464
- const t = Math.round(i / 1e3), e = Math.floor(t / 60), s = t % 60;
465
- return `${e}:${String(s).padStart(2, "0")}`;
466
- }
467
- const y = "rr0-ufo";
468
- function O() {
469
- customElements.get(y) || customElements.define(y, R);
470
- }
471
- O();
133
+ `;function C(n,e,t){const{bounds:s}=n;return e>=s.x&&e<=s.x+s.width&&t>=s.y&&t<=s.y+s.height}function l(n,e,t){return n+(e-n)*t}function T(n){const e=/^#([0-9a-f]{6})$/i.exec(n);if(!e)return;const t=parseInt(e[1],16);return[t>>16&255,t>>8&255,t&255]}function O(n){return`#${n.map(e=>Math.round(e).toString(16).padStart(2,"0")).join("")}`}function _(n,e,t){const s=T(n),i=T(e);return!s||!i?t<1?n:e:O([l(s[0],i[0],t),l(s[1],i[1],t),l(s[2],i[2],t)])}function R(n,e,t){const s={x:l(n.bounds.x,e.bounds.x,t),y:l(n.bounds.y,e.bounds.y,t),width:l(n.bounds.width,e.bounds.width,t),height:l(n.bounds.height,e.bounds.height,t)},i=l(n.angle,e.angle,t),r=l(n.transparency,e.transparency,t),a=l(n.haloScale,e.haloScale,t),o=_(n.color,e.color,t);return n.kind==="polygon"&&e.kind==="polygon"&&n.points.length===e.points.length?{...n,bounds:s,angle:i,transparency:r,haloScale:a,color:o,points:n.points.map((c,f)=>({x:l(c.x,e.points[f].x,t),y:l(c.y,e.points[f].y,t)}))}:{...t<1?n:e,bounds:s,angle:i,transparency:r,haloScale:a,color:o}}class k{keyframes=[];addKeyframe(e,t){const s=this.findInsertIndex(e);if(this.keyframes[s]?.t===e){const i=new Set(t.map(r=>r.sourceId));this.keyframes[s]={t:e,shapes:[...this.keyframes[s].shapes.filter(r=>!i.has(r.sourceId)),...t]}}else this.keyframes.splice(s,0,{t:e,shapes:[...t]})}findInsertIndex(e){let t=0,s=this.keyframes.length;for(;t<s;){const i=t+s>>>1;this.keyframes[i].t<e?t=i+1:s=i}return t}getKeyframeAt(e){const t=this.findInsertIndex(e),s=this.keyframes[t];return s?.t===e?s:void 0}getShapeAt(e,t){return this.getKeyframeAt(e)?.shapes.find(s=>s.sourceId===t)?.shape}getLatestShapeAt(e,t){let s=this.findInsertIndex(e);for(this.keyframes[s]?.t!==e&&(s-=1);s>=0;s--){const i=this.keyframes[s].shapes.find(r=>r.sourceId===t);if(i)return i.shape}}getInterpolatedShapeAt(e,t){const s=this.findShapeAtOrBefore(e,t);if(s?.t===e)return s.shape;const i=this.findShapeAtOrAfter(e,t);return s?i?R(s.shape,i.shape,(e-s.t)/(i.t-s.t)):s.shape:i?.shape}findShapeAtOrBefore(e,t){let s=this.findInsertIndex(e);for(this.keyframes[s]?.t!==e&&(s-=1);s>=0;s--){const i=this.keyframes[s].shapes.find(r=>r.sourceId===t);if(i)return{t:this.keyframes[s].t,shape:i.shape}}}findShapeAtOrAfter(e,t){for(let s=this.findInsertIndex(e);s<this.keyframes.length;s++){const i=this.keyframes[s].shapes.find(r=>r.sourceId===t);if(i)return{t:this.keyframes[s].t,shape:i.shape}}}hitTest(e,t,s){const i=this.sourceIds;for(let r=i.length-1;r>=0;r--){const a=this.getInterpolatedShapeAt(e,i[r]);if(a&&C(a,t,s))return{sourceId:i[r],shape:a}}}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get sourceIds(){const e=new Set;for(const t of this.keyframes)for(const s of t.shapes)e.add(s.sourceId);return[...e]}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes}}static fromJSON(e){const t=new k;for(const s of e.keyframes)t.addKeyframe(s.t,s.shapes);return t}}function N(n,e,t){return Math.max(e,Math.min(t,n))}function U(n,e,t){const s=((e-n)%360+540)%360-180;return((n+s*t)%360+360)%360}function m(n,e,t){return n+(e-n)*t}function H(n,e,t){return{lat:n.lat===void 0||e.lat===void 0?void 0:m(n.lat,e.lat,t),lng:n.lng===void 0||e.lng===void 0?void 0:m(n.lng,e.lng,t),elevationM:m(n.elevationM,e.elevationM,t),headingDeg:n.headingDeg===void 0||e.headingDeg===void 0?void 0:U(n.headingDeg,e.headingDeg,t),pitchDeg:m(n.pitchDeg,e.pitchDeg,t),fovDeg:m(n.fovDeg,e.fovDeg,t)}}class y{keyframes=[];addKeyframe(e,t){const s=this.findInsertIndex(e);this.keyframes[s]?.t===e?this.keyframes[s]={t:e,pose:t}:this.keyframes.splice(s,0,{t:e,pose:t})}clear(){this.keyframes.length=0}findInsertIndex(e){let t=0,s=this.keyframes.length;for(;t<s;){const i=t+s>>>1;this.keyframes[i].t<e?t=i+1:s=i}return t}getLatestPoseAt(e){let t=this.findInsertIndex(e);return this.keyframes[t]?.t!==e&&(t-=1),t>=0?this.keyframes[t].pose:void 0}getInterpolatedPoseAt(e){const t=this.findInsertIndex(e),s=this.keyframes[t]?.t===e?this.keyframes[t]:this.keyframes[t-1];if(s?.t===e)return s.pose;const i=this.keyframes[t]?.t===e?void 0:this.keyframes[t];return s?i?H(s.pose,i.pose,N((e-s.t)/(i.t-s.t),0,1)):s.pose:i?.pose}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes}}static fromJSON(e){const t=new y;for(const s of e.keyframes)t.addKeyframe(s.t,s.pose);return t}}function S(n){if(n.year!==void 0)return Date.UTC(n.year,(n.month??1)-1,n.day??1,n.hour??0,n.minute??0,n.second??0)}function $(n){if(n.durationSeconds!==void 0)return n.durationSeconds*1e3;const e=n.time?S(n.time):void 0,t=n.endTime?S(n.endTime):void 0;return e!==void 0&&t!==void 0?t-e:void 0}class w{constructor(e,t,s,i,r,a){this.event=e,this.timeline=t,this.observerTrack=s,this.witnessId=i,this.witnessName=r,this.caseId=a}static create(e,t,s){return new w({eventType:"sighting",time:e,place:t},new k,new y,s)}}class J{constructor(e,t){this.timeline=e,this.onFrame=t}rafId=null;state="stopped";currentT=0;lastWallTime=0;playbackRate=1;loop=!1;durationOverrideMs=0;get seekableDuration(){return Math.max(this.timeline.duration,this.durationOverrideMs)}play(){if(this.state==="playing")return;this.currentT>=this.seekableDuration&&(this.currentT=0,this.resolveFrame(0)),this.state="playing",this.lastWallTime=performance.now();const e=()=>{if(this.state!=="playing")return;const t=performance.now();if(this.currentT+=(t-this.lastWallTime)*this.playbackRate,this.lastWallTime=t,this.currentT>=this.seekableDuration){if(this.loop&&this.seekableDuration>0){this.currentT%=this.seekableDuration,this.resolveFrame(this.currentT),this.rafId=requestAnimationFrame(e);return}this.currentT=this.seekableDuration,this.stop(),this.resolveFrame(this.currentT);return}this.resolveFrame(this.currentT),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}pause(){this.state==="playing"&&(this.state="paused",this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null))}stop(){this.state="stopped",this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}seek(e){this.currentT=Math.max(0,Math.min(e,this.seekableDuration)),this.resolveFrame(this.currentT)}get playbackState(){return this.state}get time(){return this.currentT}resolveFrame(e){const t=new Map;for(const s of this.timeline.sourceIds){const i=this.timeline.getInterpolatedShapeAt(e,s);i&&t.set(s,i)}this.onFrame(e,t)}}const K=["nw","n","ne","e","se","s","sw","w"],q=24;function z(n,e,t){const s=n.x-e.x,i=n.y-e.y,r=Math.cos(t),a=Math.sin(t);return{x:e.x+s*r-i*a,y:e.y+s*a+i*r}}function W(n){return{x:n.x+n.width/2,y:n.y+n.height/2}}function j(n){const{x:e,y:t,width:s,height:i}=n.bounds,r={nw:{x:e,y:t},n:{x:e+s/2,y:t},ne:{x:e+s,y:t},e:{x:e+s,y:t+i/2},se:{x:e+s,y:t+i},s:{x:e+s/2,y:t+i},sw:{x:e,y:t+i},w:{x:e,y:t+i/2},rotate:{x:e+s/2,y:t-q}},a=W(n.bounds),o={};for(const c of Object.keys(r))o[c]=z(r[c],a,n.angle);return o}const I=6,b=I/2,Z=20;class G{constructor(e){this.ctx=e}clear(e,t){this.ctx.clearRect(0,0,e,t)}paintShape(e){this.ctx.save(),this.ctx.globalAlpha=1-e.transparency,e.haloScale>0&&this.paintHalo(e),this.paintBase(e),this.ctx.restore(),e.selected&&this.paintSelectionHandles(e)}paintBase(e){if(this.ctx.fillStyle=e.color,this.ctx.beginPath(),e.kind==="oval"){const{x:t,y:s,width:i,height:r}=e.bounds,a=i/2,o=r/2;this.ctx.ellipse(t+a,s+o,a,o,e.angle,0,2*Math.PI)}else{const{x:t,y:s,width:i,height:r}=e.bounds;this.ctx.save(),this.ctx.translate(t+i/2,s+r/2),this.ctx.rotate(e.angle),this.ctx.translate(-i/2,-r/2),e.points.forEach((a,o)=>{o===0?this.ctx.moveTo(a.x,a.y):this.ctx.lineTo(a.x,a.y)}),this.ctx.closePath(),this.ctx.restore()}this.ctx.fill()}paintHalo(e){this.ctx.save(),this.ctx.shadowColor=e.color,this.ctx.shadowBlur=Z*e.haloScale,this.paintBase(e),this.ctx.restore()}paintSelectionHandles(e){const t=j(e);this.ctx.strokeStyle="lightgray",this.ctx.beginPath(),this.ctx.moveTo(t.nw.x,t.nw.y),this.ctx.lineTo(t.ne.x,t.ne.y),this.ctx.lineTo(t.se.x,t.se.y),this.ctx.lineTo(t.sw.x,t.sw.y),this.ctx.closePath(),this.ctx.stroke(),this.ctx.fillStyle="lightgray";for(const s of K){const i=t[s];this.ctx.fillRect(i.x-b,i.y-b,I,I)}this.ctx.beginPath(),this.ctx.moveTo(t.n.x,t.n.y),this.ctx.lineTo(t.rotate.x,t.rotate.y),this.ctx.stroke(),this.ctx.beginPath(),this.ctx.ellipse(t.rotate.x,t.rotate.y,b+1,b+1,0,0,2*Math.PI),this.ctx.fill()}}function Q(n){return{version:1,time:n.event.time,endTime:n.event.endTime,durationSeconds:n.event.durationSeconds,place:n.event.place,witnessId:n.witnessId,witnessName:n.witnessName,caseId:n.caseId,timeline:n.timeline.toJSON(),observerTrack:n.observerTrack.toJSON()}}function V(n){return new w({eventType:"sighting",time:n.time,endTime:n.endTime,durationSeconds:n.durationSeconds,place:n.place},k.fromJSON(n.timeline),n.observerTrack?y.fromJSON(n.observerTrack):new y,n.witnessId,n.witnessName,n.caseId)}function X(n,e){for(const t of n){const s=t.toLowerCase().split("-")[0];if(e.includes(s))return s}return"en"}const Y="modulepreload",ee=function(n,e){return new URL(n,e).href},E={},P=function(e,t,s){let i=Promise.resolve();if(t&&t.length>0){let a=function(h){return Promise.all(h.map(d=>Promise.resolve(d).then(p=>({status:"fulfilled",value:p}),p=>({status:"rejected",reason:p}))))};const o=document.getElementsByTagName("link"),c=document.querySelector("meta[property=csp-nonce]"),f=c?.nonce||c?.getAttribute("nonce");i=a(t.map(h=>{if(h=ee(h,s),h in E)return;E[h]=!0;const d=h.endsWith(".css"),p=d?'[rel="stylesheet"]':"";if(!!s)for(let g=o.length-1;g>=0;g--){const v=o[g];if(v.href===h&&(!d||v.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${h}"]${p}`))return;const u=document.createElement("link");if(u.rel=d?"stylesheet":Y,d||(u.as="script"),u.crossOrigin="",u.href=h,f&&u.setAttribute("nonce",f),document.head.appendChild(u),d)return new Promise((g,v)=>{u.addEventListener("load",g),u.addEventListener("error",()=>v(new Error(`Unable to preload CSS for ${h}`)))})}))}function r(a){const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=a,window.dispatchEvent(o),!o.defaultPrevented)throw a}return i.then(a=>{for(const o of a||[])o.status==="rejected"&&r(o.reason);return e().catch(r)})},te=["en","fr"],se={en:()=>P(()=>Promise.resolve().then(()=>ie),void 0,import.meta.url).then(n=>n.ufoMessages_en),fr:()=>P(()=>import("./assets/UfoMessages_fr-COCxf8Q_.js"),[],import.meta.url).then(n=>n.ufoMessages_fr)};function ne(n){return se[n]()}const A={play:"Play",pause:"Pause",autoReplay:"Auto-replay",currentPosition:"Current position",duration:"Duration",fullscreen:"Fullscreen",exitFullscreen:"Exit fullscreen"},ie=Object.freeze(Object.defineProperty({__proto__:null,ufoMessages_en:A},Symbol.toStringTag,{value:"Module"}));class re extends HTMLElement{static get observedAttributes(){return["src"]}shadow;stageElement;canvas;canvasRenderer;toolbar;playPauseButton;loopButton;fullscreenButton;seekInput;timeStartLabel;timeEndLabel;currentSighting=w.create();player;loopEnabled=!0;highlightedSourceId;enableClickToPlay=!0;fullscreenTarget;messages=A;realDurationMs;realStartMs;handleFullscreenChange=()=>this.updateFullscreenButton();constructor(){super(),this.shadow=this.attachShadow({mode:"open"});const e=document.createElement("template");e.innerHTML=`<style>${F}</style>${L}`,this.shadow.appendChild(e.content.cloneNode(!0)),this.stageElement=this.shadow.getElementById("stage"),this.canvas=this.shadow.getElementById("canvas"),this.canvasRenderer=new G(this.canvas.getContext("2d")),this.toolbar=this.shadow.getElementById("toolbar"),this.playPauseButton=this.shadow.getElementById("play-pause"),this.loopButton=this.shadow.getElementById("loop"),this.fullscreenButton=this.shadow.getElementById("fullscreen"),this.seekInput=this.shadow.getElementById("seek"),this.timeStartLabel=this.shadow.getElementById("time-start"),this.timeEndLabel=this.shadow.getElementById("time-end"),this.fullscreenTarget=this.stageElement,this.playPauseButton.addEventListener("click",()=>this.togglePlayPause()),this.loopButton.addEventListener("click",()=>this.toggleLoop()),this.fullscreenButton.addEventListener("click",()=>this.toggleFullscreen()),this.seekInput.addEventListener("input",()=>this.player.seek(Number(this.seekInput.value))),this.canvas.addEventListener("click",()=>{this.enableClickToPlay&&this.togglePlayPause()}),document.addEventListener("fullscreenchange",this.handleFullscreenChange),this.player=this.createPlayer(),this.updateTimeLabels(),this.updatePlayPauseButton(),this.updateFullscreenButton(),this.refresh(),this.loadLocaleMessages()}connectedCallback(){const e=this.getAttribute("src");e&&this.loadFromSrc(e)}disconnectedCallback(){document.removeEventListener("fullscreenchange",this.handleFullscreenChange)}attributeChangedCallback(e,t,s){e==="src"&&s&&s!==t&&this.isConnected&&this.loadFromSrc(s)}async loadFromSrc(e){const t=await fetch(e);this.sightingData=await t.json()}get sightingData(){return Q(this.currentSighting)}set sightingData(e){this.currentSighting=V(e),this.player=this.createPlayer(),this.updateTimeLabels(),this.updatePlayPauseButton(),this.refresh()}get sighting(){return this.currentSighting}get canvasElement(){return this.canvas}get renderer(){return this.canvasRenderer}get currentTime(){return this.player.time}get playbackState(){return this.player.playbackState}get selectedSourceId(){return this.highlightedSourceId}get durationSeconds(){return this.currentSighting.event.durationSeconds}set durationSeconds(e){this.currentSighting.event.durationSeconds=e,this.updateTimeLabels(),this.refresh()}set selectedSourceId(e){e!==this.highlightedSourceId&&(this.highlightedSourceId=e,this.refresh())}refresh(){this.seekInput.max=String(this.player.seekableDuration),this.player.seek(this.player.time)}onFrame(e,t){this.canvasRenderer.clear(this.canvas.width,this.canvas.height);const s=this.playbackState!=="playing";for(const[i,r]of t){const a=s&&i===this.highlightedSourceId;this.canvasRenderer.paintShape(a?{...r,selected:!0}:r)}this.seekInput.value=String(e),this.timeStartLabel.textContent=this.formatPosition(e),this.updatePlayPauseButton(),this.dispatchEvent(new CustomEvent("timeupdate",{detail:{time:e}}))}createPlayer(){const e=new J(this.currentSighting.timeline,(t,s)=>this.onFrame(t,s));return e.loop=this.loopEnabled,e}togglePlayPause(){this.player.playbackState==="playing"?(this.player.pause(),this.refresh()):this.player.play(),this.updatePlayPauseButton()}updatePlayPauseButton(){const e=this.player.playbackState==="playing";this.playPauseButton.textContent=e?"⏸":"▶",this.playPauseButton.title=e?this.messages.pause:this.messages.play,this.playPauseButton.setAttribute("aria-label",e?this.messages.pause:this.messages.play),this.toolbar.classList.toggle("auto-hide",e),this.fullscreenButton.classList.toggle("auto-hide",e)}toggleLoop(){this.loopEnabled=!this.loopEnabled,this.loopButton.setAttribute("aria-pressed",String(this.loopEnabled)),this.player.loop=this.loopEnabled}toggleFullscreen(){document.fullscreenElement?document.exitFullscreen():this.fullscreenTarget.requestFullscreen().catch(e=>{console.error("<rr0-ufo>: requestFullscreen() failed —",e)})}updateFullscreenButton(){const e=document.fullscreenElement===this.fullscreenTarget;this.fullscreenButton.title=e?this.messages.exitFullscreen:this.messages.fullscreen,this.fullscreenButton.setAttribute("aria-label",this.fullscreenButton.title)}async loadLocaleMessages(){const e=X(navigator.languages,te);e!=="en"&&this.applyMessages(await ne(e))}applyMessages(e){this.messages=e,this.timeStartLabel.title=e.currentPosition,this.timeEndLabel.title=e.duration,this.loopButton.title=e.autoReplay,this.loopButton.setAttribute("aria-label",e.autoReplay),this.updatePlayPauseButton(),this.updateFullscreenButton()}updateTimeLabels(){const e=this.currentSighting.event,t=$(e);this.realDurationMs=t!==void 0&&t>0?t:void 0,this.realStartMs=e.time?S(e.time):void 0;const s=this.currentSighting.timeline.duration;this.player.playbackRate=this.realDurationMs!==void 0&&s>0?s/this.realDurationMs:1,this.player.durationOverrideMs=this.realDurationMs??0,this.timeEndLabel.textContent=this.formatEndOfTimeline(),this.timeStartLabel.textContent=this.formatPosition(this.player.time)}formatPosition(e){if(this.realDurationMs===void 0)return x(e);const t=this.currentSighting.timeline.duration,s=t>0&&e<=t?e/t*this.realDurationMs:e;return this.realStartMs!==void 0?B(M(this.realStartMs+s)):x(s)}formatEndOfTimeline(){return this.realDurationMs===void 0?x(this.currentSighting.timeline.duration):this.realStartMs!==void 0?B(M(this.realStartMs+this.realDurationMs)):x(this.realDurationMs)}}function M(n){const e=new Date(n);return{hour:e.getUTCHours(),minute:e.getUTCMinutes(),second:e.getUTCSeconds()}}function B(n){if(n.hour===void 0)return"0:00";const e=t=>String(t).padStart(2,"0");return n.second?`${e(n.hour)}:${e(n.minute??0)}:${e(n.second)}`:`${e(n.hour)}:${e(n.minute??0)}`}function x(n){const e=Math.round(n/1e3),t=Math.floor(e/60),s=e%60;return`${t}:${String(s).padStart(2,"0")}`}const D="rr0-ufo";function ae(){customElements.get(D)||customElements.define(D,re)}ae();
@@ -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 s={witness:"Witness"};export{s as witnessSelectorMessages_en};
@@ -0,0 +1 @@
1
+ const s={witness:"Témoin"};export{s as witnessSelectorMessages_fr};