@rr0/ufoathome 0.1.0 → 0.2.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.
@@ -1,4 +1,4 @@
1
- const S = `
1
+ const T = `
2
2
  <div class="appearance">
3
3
  <div class="presets" role="group" aria-label="UFO shape">
4
4
  <button class="preset" id="preset-oval" type="button" data-preset="oval">Oval</button>
@@ -13,8 +13,8 @@ const S = `
13
13
  <button id="record" type="button">Record</button>
14
14
  <label>Sampling rate (ms) <input id="samplingRate" type="number" min="16" step="16" value="100"/></label>
15
15
  </div>
16
- <div id="player-slot"></div>
17
- `, b = `
16
+ <div id="ufo-slot"></div>
17
+ `, B = `
18
18
  :host {
19
19
  display: block;
20
20
  font-family: sans-serif;
@@ -37,39 +37,92 @@ button.preset[aria-pressed="true"] {
37
37
  outline: 2px solid #39f;
38
38
  font-weight: bold;
39
39
  }
40
- `, k = `
41
- <div class="toolbar">
42
- <button id="play" type="button">Play</button>
43
- <button id="pause" type="button">Pause</button>
44
- <input id="seek" type="range" min="0" max="0" value="0" step="1"/>
40
+ `, M = `
41
+ <div class="stage">
42
+ <canvas id="canvas" width="640" height="360"></canvas>
43
+ <div class="toolbar" id="toolbar">
44
+ <button id="play-pause" type="button" title="Play" aria-label="Play">▶</button>
45
+ <span id="time-start" class="time-label" title="Current position">0:00</span>
46
+ <input id="seek" type="range" min="0" max="0" value="0" step="1"/>
47
+ <span id="time-end" class="time-label" title="Duration">0:00</span>
48
+ <button id="loop" type="button" title="Auto-replay" aria-label="Auto-replay" aria-pressed="true">↻</button>
49
+ </div>
45
50
  </div>
46
- <canvas id="canvas" width="640" height="360"></canvas>
47
51
  `, A = `
48
52
  :host {
49
53
  display: block;
50
54
  font-family: sans-serif;
51
55
  }
56
+ .stage {
57
+ position: relative;
58
+ width: 100%;
59
+ }
60
+ canvas {
61
+ display: block;
62
+ width: 100%;
63
+ height: auto;
64
+ aspect-ratio: 640 / 360;
65
+ background: var(--ufo-canvas-background, #050510);
66
+ border: var(--ufo-canvas-border, 1px solid #333);
67
+ box-sizing: border-box;
68
+ }
52
69
  .toolbar {
70
+ position: absolute;
71
+ left: 0;
72
+ right: 0;
73
+ bottom: 0;
53
74
  display: flex;
54
75
  align-items: center;
55
76
  gap: 0.5em;
56
- margin-bottom: 0.5em;
77
+ padding: 0.4em 0.6em;
78
+ background: rgba(0, 0, 0, 0.55);
79
+ transition: opacity 0.15s ease;
57
80
  }
58
- canvas {
59
- background: #050510;
60
- border: 1px solid #333;
81
+ /* While playing, the toolbar auto-hides and only reappears on hover — kept always visible while
82
+ paused/stopped, since that's when the user is most likely to want it. Deliberately hover-only,
83
+ not :focus-within: a clicked button/range input keeps keyboard focus after the pointer moves
84
+ away, which would otherwise keep the toolbar stuck visible indefinitely after any interaction. */
85
+ .toolbar.auto-hide {
86
+ opacity: 0;
87
+ pointer-events: none;
88
+ }
89
+ .stage:hover .toolbar.auto-hide {
90
+ opacity: 1;
91
+ pointer-events: auto;
61
92
  }
62
93
  input[type=range] {
63
94
  flex: 1;
64
95
  }
96
+ .toolbar button {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ width: 1.8em;
101
+ height: 1.8em;
102
+ padding: 0;
103
+ border-radius: 3px;
104
+ cursor: pointer;
105
+ font-size: 1em;
106
+ line-height: 1;
107
+ }
108
+ .toolbar button[aria-pressed="true"] {
109
+ outline: 2px solid #39f;
110
+ }
111
+ .time-label {
112
+ color: #fff;
113
+ font-variant-numeric: tabular-nums;
114
+ font-size: 0.85em;
115
+ min-width: 3em;
116
+ text-align: center;
117
+ }
65
118
  `;
66
- function P(i, t = "#39ff14") {
119
+ function R(i, t = "#39ff14") {
67
120
  return { kind: "oval", bounds: i, color: t, angle: 0, transparency: 0, haloScale: 0, selected: !1 };
68
121
  }
69
- function g(i, t, e = "#39ff14") {
122
+ function E(i, t, e = "#39ff14") {
70
123
  return { kind: "polygon", bounds: i, points: t, color: e, angle: 0, transparency: 0, haloScale: 0, selected: !1 };
71
124
  }
72
- function R(i, t = "#39ff14") {
125
+ function L(i, t = "#39ff14") {
73
126
  const { width: e, height: s } = i, n = [
74
127
  { x: 0.5 * e, y: 0 },
75
128
  { x: 0.8 * e, y: 0.25 * s },
@@ -80,40 +133,40 @@ function R(i, t = "#39ff14") {
80
133
  { x: 0, y: 0.5 * s },
81
134
  { x: 0.2 * e, y: 0.25 * s }
82
135
  ];
83
- return g(i, n, t);
136
+ return E(i, n, t);
84
137
  }
85
- function T(i, t = "#39ff14") {
138
+ function C(i, t = "#39ff14") {
86
139
  const { width: e, height: s } = i, n = [
87
140
  { x: 0, y: 0 },
88
141
  { x: e, y: s / 2 },
89
142
  { x: 0, y: s }
90
143
  ];
91
- return g(i, n, t);
144
+ return E(i, n, t);
92
145
  }
93
- const B = {
94
- oval: P,
95
- saucer: R,
96
- triangle: T
146
+ const D = {
147
+ oval: R,
148
+ saucer: L,
149
+ triangle: C
97
150
  };
98
- function L(i, t) {
151
+ function F(i, t) {
99
152
  return {
100
- ...B[t.presetId](i, t.color),
153
+ ...D[t.presetId](i, t.color),
101
154
  transparency: t.transparency,
102
155
  haloScale: t.haloScale
103
156
  };
104
157
  }
105
- function F(i, t, e) {
158
+ function _(i, t, e) {
106
159
  const { bounds: s } = i;
107
160
  return t >= s.x && t <= s.x + s.width && e >= s.y && e <= s.y + s.height;
108
161
  }
109
- function v(i, t, e) {
162
+ function S(i, t, e) {
110
163
  const s = i.bounds.width, n = i.bounds.height;
111
164
  return {
112
165
  ...i,
113
166
  bounds: { x: t - s / 2, y: e - n / 2, width: s, height: n }
114
167
  };
115
168
  }
116
- class h {
169
+ class c {
117
170
  keyframes = [];
118
171
  addKeyframe(t, e) {
119
172
  const s = this.findInsertIndex(t);
@@ -141,16 +194,16 @@ class h {
141
194
  getLatestShapeAt(t, e) {
142
195
  let s = this.findInsertIndex(t);
143
196
  for (this.keyframes[s]?.t !== t && (s -= 1); s >= 0; s--) {
144
- const n = this.keyframes[s].shapes.find((r) => r.sourceId === e);
197
+ const n = this.keyframes[s].shapes.find((a) => a.sourceId === e);
145
198
  if (n) return n.shape;
146
199
  }
147
200
  }
148
201
  hitTest(t, e, s) {
149
202
  const n = this.getKeyframeAt(t);
150
203
  if (n) {
151
- for (let r = n.shapes.length - 1; r >= 0; r--)
152
- if (F(n.shapes[r].shape, e, s))
153
- return n.shapes[r];
204
+ for (let a = n.shapes.length - 1; a >= 0; a--)
205
+ if (_(n.shapes[a].shape, e, s))
206
+ return n.shapes[a];
154
207
  }
155
208
  }
156
209
  get duration() {
@@ -170,21 +223,30 @@ class h {
170
223
  return { keyframes: this.keyframes };
171
224
  }
172
225
  static fromJSON(t) {
173
- const e = new h();
226
+ const e = new c();
174
227
  for (const s of t.keyframes)
175
228
  e.addKeyframe(s.t, s.shapes);
176
229
  return e;
177
230
  }
178
231
  }
179
- class c {
232
+ function d(i) {
233
+ if (i.year !== void 0)
234
+ return Date.UTC(i.year, (i.month ?? 1) - 1, i.day ?? 1, i.hour ?? 0, i.minute ?? 0, i.second ?? 0);
235
+ }
236
+ function U(i) {
237
+ if (i.durationSeconds !== void 0) return i.durationSeconds * 1e3;
238
+ const t = i.time ? d(i.time) : void 0, e = i.endTime ? d(i.endTime) : void 0;
239
+ return t !== void 0 && e !== void 0 ? e - t : void 0;
240
+ }
241
+ class u {
180
242
  constructor(t, e, s) {
181
243
  this.event = t, this.timeline = e, this.witnessId = s;
182
244
  }
183
245
  static create(t, e, s) {
184
- return new c({ eventType: "sighting", time: t, place: e }, new h(), s);
246
+ return new u({ eventType: "sighting", time: t, place: e }, new c(), s);
185
247
  }
186
248
  }
187
- class u {
249
+ class N {
188
250
  constructor(t, e) {
189
251
  this.timeline = t, this.onFrame = e;
190
252
  }
@@ -192,14 +254,29 @@ class u {
192
254
  state = "stopped";
193
255
  currentT = 0;
194
256
  lastWallTime = 0;
257
+ /**
258
+ * Multiplies elapsed wall-clock time before advancing currentT. 1 (default) means playback
259
+ * takes as long as `timeline.duration` itself; UfoElement sets this to
260
+ * `timeline.duration / sightingDurationMs(event)` when the sighting's real declared duration
261
+ * is known, so watching it takes as long as the observation was actually reported to last,
262
+ * rather than however long the recording itself took to author (e.g. a quick mouse drag).
263
+ * Manually dragging the seek bar always jumps directly regardless of this rate.
264
+ */
265
+ playbackRate = 1;
266
+ /** When true, playback restarts from 0 instead of stopping once it reaches the end. */
267
+ loop = !1;
195
268
  play() {
196
269
  if (this.state === "playing") return;
197
- this.state = "playing", this.lastWallTime = performance.now();
270
+ this.currentT >= this.timeline.duration && (this.currentT = 0, this.resolveFrame(0)), this.state = "playing", this.lastWallTime = performance.now();
198
271
  const t = () => {
199
272
  if (this.state !== "playing") return;
200
273
  const e = performance.now();
201
- if (this.currentT += e - this.lastWallTime, this.lastWallTime = e, this.currentT >= this.timeline.duration) {
202
- this.currentT = this.timeline.duration, this.resolveFrame(this.currentT), this.stop();
274
+ if (this.currentT += (e - this.lastWallTime) * this.playbackRate, this.lastWallTime = e, this.currentT >= this.timeline.duration) {
275
+ if (this.loop && this.timeline.duration > 0) {
276
+ this.currentT %= this.timeline.duration, this.resolveFrame(this.currentT), this.rafId = requestAnimationFrame(t);
277
+ return;
278
+ }
279
+ this.currentT = this.timeline.duration, this.stop(), this.resolveFrame(this.currentT);
203
280
  return;
204
281
  }
205
282
  this.resolveFrame(this.currentT), this.rafId = requestAnimationFrame(t);
@@ -230,8 +307,8 @@ class u {
230
307
  this.onFrame(t, e);
231
308
  }
232
309
  }
233
- const d = 6, m = d / 2, C = 20;
234
- class M {
310
+ const p = 6, f = p / 2, O = 20;
311
+ class H {
235
312
  constructor(t) {
236
313
  this.ctx = t;
237
314
  }
@@ -243,68 +320,118 @@ class M {
243
320
  }
244
321
  paintBase(t) {
245
322
  if (this.ctx.fillStyle = t.color, this.ctx.beginPath(), t.kind === "oval") {
246
- const { x: e, y: s, width: n, height: r } = t.bounds, o = n / 2, l = r / 2;
323
+ const { x: e, y: s, width: n, height: a } = t.bounds, o = n / 2, l = a / 2;
247
324
  this.ctx.ellipse(e + o, s + l, o, l, t.angle, 0, 2 * Math.PI);
248
325
  } else {
249
326
  const { x: e, y: s } = t.bounds;
250
- this.ctx.save(), this.ctx.translate(e, s), this.ctx.rotate(t.angle), t.points.forEach((n, r) => {
251
- r === 0 ? this.ctx.moveTo(n.x, n.y) : this.ctx.lineTo(n.x, n.y);
327
+ this.ctx.save(), this.ctx.translate(e, s), this.ctx.rotate(t.angle), t.points.forEach((n, a) => {
328
+ a === 0 ? this.ctx.moveTo(n.x, n.y) : this.ctx.lineTo(n.x, n.y);
252
329
  }), this.ctx.closePath(), this.ctx.restore();
253
330
  }
254
331
  this.ctx.fill();
255
332
  }
256
333
  paintHalo(t) {
257
- this.ctx.save(), this.ctx.shadowColor = t.color, this.ctx.shadowBlur = C * t.haloScale, this.paintBase(t), this.ctx.restore();
334
+ this.ctx.save(), this.ctx.shadowColor = t.color, this.ctx.shadowBlur = O * t.haloScale, this.paintBase(t), this.ctx.restore();
258
335
  }
259
336
  paintSelectionHandles(t) {
260
- const { x: e, y: s, width: n, height: r } = t.bounds, o = n / 2, l = r / 2;
261
- this.ctx.strokeStyle = "lightgray", this.ctx.strokeRect(e, s, n, r), this.ctx.fillStyle = "lightgray";
262
- const I = [
337
+ const { x: e, y: s, width: n, height: a } = t.bounds, o = n / 2, l = a / 2;
338
+ this.ctx.strokeStyle = "lightgray", this.ctx.strokeRect(e, s, n, a), this.ctx.fillStyle = "lightgray";
339
+ const P = [
263
340
  [e, s],
264
341
  [e + o, s],
265
342
  [e + n, s],
266
343
  [e + n, s + l],
267
- [e + n, s + r],
268
- [e + o, s + r],
269
- [e, s + r],
344
+ [e + n, s + a],
345
+ [e + o, s + a],
346
+ [e, s + a],
270
347
  [e, s + l]
271
348
  ];
272
- for (const [w, x] of I)
273
- this.ctx.fillRect(w - m, x - m, d, d);
349
+ for (const [x, k] of P)
350
+ this.ctx.fillRect(x - f, k - f, p, p);
274
351
  }
275
352
  }
276
- function N(i) {
353
+ function $(i) {
277
354
  return {
278
355
  version: 1,
279
356
  time: i.event.time,
357
+ endTime: i.event.endTime,
358
+ durationSeconds: i.event.durationSeconds,
280
359
  place: i.event.place,
281
360
  witnessId: i.witnessId,
282
361
  timeline: i.timeline.toJSON()
283
362
  };
284
363
  }
285
- function D(i) {
286
- return new c(
287
- { eventType: "sighting", time: i.time, place: i.place },
288
- h.fromJSON(i.timeline),
364
+ function K(i) {
365
+ return new u(
366
+ {
367
+ eventType: "sighting",
368
+ time: i.time,
369
+ endTime: i.endTime,
370
+ durationSeconds: i.durationSeconds,
371
+ place: i.place
372
+ },
373
+ c.fromJSON(i.timeline),
289
374
  i.witnessId
290
375
  );
291
376
  }
292
- class H extends HTMLElement {
377
+ function W(i, t) {
378
+ for (const e of i) {
379
+ const s = e.toLowerCase().split("-")[0];
380
+ if (t.includes(s)) return s;
381
+ }
382
+ return "en";
383
+ }
384
+ const J = ["en", "fr"], q = {
385
+ en: () => Promise.resolve().then(() => Z).then((i) => i.ufoMessages_en),
386
+ fr: () => import("./UfoMessages_fr-DKnWFOyl.js").then((i) => i.ufoMessages_fr)
387
+ };
388
+ function z(i) {
389
+ return q[i]();
390
+ }
391
+ const w = {
392
+ play: "Play",
393
+ pause: "Pause",
394
+ autoReplay: "Auto-replay",
395
+ currentPosition: "Current position",
396
+ duration: "Duration"
397
+ }, Z = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
398
+ __proto__: null,
399
+ ufoMessages_en: w
400
+ }, Symbol.toStringTag, { value: "Module" }));
401
+ class G extends HTMLElement {
293
402
  static get observedAttributes() {
294
403
  return ["src"];
295
404
  }
296
405
  shadow;
297
406
  canvas;
298
407
  canvasRenderer;
299
- playButton;
300
- pauseButton;
408
+ toolbar;
409
+ playPauseButton;
410
+ loopButton;
301
411
  seekInput;
302
- currentSighting = c.create();
412
+ timeStartLabel;
413
+ timeEndLabel;
414
+ currentSighting = u.create();
303
415
  player;
416
+ loopEnabled = !0;
417
+ /** Set to false by composing elements that need the canvas's own click for something else
418
+ * instead of toggling playback — see UfoRecorderElement, which uses pointerdown/pointermove on
419
+ * this same canvas to place shapes while recording. */
420
+ enableClickToPlay = !0;
421
+ /** Matches the template's baked-in English defaults until (if ever) loadLocaleMessages()
422
+ * resolves a better match — see its doc comment. */
423
+ messages = w;
424
+ /** The sighting's real reported duration/start, cached by updateTimeLabels() so onFrame doesn't
425
+ * recompute them every animation frame — see formatPosition, which turns a `Timeline` position
426
+ * (ms since recording start) into what's actually displayed. */
427
+ realDurationMs;
428
+ realStartMs;
304
429
  constructor() {
305
430
  super(), this.shadow = this.attachShadow({ mode: "open" });
306
431
  const t = document.createElement("template");
307
- t.innerHTML = `<style>${A}</style>${k}`, this.shadow.appendChild(t.content.cloneNode(!0)), this.canvas = this.shadow.getElementById("canvas"), this.canvasRenderer = new M(this.canvas.getContext("2d")), this.playButton = this.shadow.getElementById("play"), this.pauseButton = this.shadow.getElementById("pause"), this.seekInput = this.shadow.getElementById("seek"), this.playButton.addEventListener("click", () => this.player.play()), this.pauseButton.addEventListener("click", () => this.player.pause()), this.seekInput.addEventListener("input", () => this.player.seek(Number(this.seekInput.value))), this.player = new u(this.currentSighting.timeline, (e, s) => this.onFrame(e, s)), this.refresh();
432
+ t.innerHTML = `<style>${A}</style>${M}`, this.shadow.appendChild(t.content.cloneNode(!0)), this.canvas = this.shadow.getElementById("canvas"), this.canvasRenderer = new H(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", () => {
433
+ this.enableClickToPlay && this.togglePlayPause();
434
+ }), this.player = this.createPlayer(), this.updateTimeLabels(), this.updatePlayPauseButton(), this.refresh(), this.loadLocaleMessages();
308
435
  }
309
436
  connectedCallback() {
310
437
  const t = this.getAttribute("src");
@@ -319,14 +446,15 @@ class H extends HTMLElement {
319
446
  this.sightingData = await e.json();
320
447
  }
321
448
  get sightingData() {
322
- return N(this.currentSighting);
449
+ return $(this.currentSighting);
323
450
  }
324
451
  set sightingData(t) {
325
- this.currentSighting = D(t), this.player = new u(this.currentSighting.timeline, (e, s) => this.onFrame(e, s)), this.refresh();
452
+ this.currentSighting = K(t), this.player = this.createPlayer(), this.updateTimeLabels(), this.updatePlayPauseButton(), this.refresh();
326
453
  }
327
454
  /**
328
- * The live Sighting/Timeline, exposed so UfoRecorderElement (which composes
329
- * this element) can add keyframes to it directly as it records.
455
+ * The live Sighting/Timeline, exposed so UfoRecorderElement/SceneElement
456
+ * (which compose this element) can add keyframes to it directly as it
457
+ * records, or read its time/place for lighting.
330
458
  */
331
459
  get sighting() {
332
460
  return this.currentSighting;
@@ -350,14 +478,82 @@ class H extends HTMLElement {
350
478
  this.canvasRenderer.clear(this.canvas.width, this.canvas.height);
351
479
  for (const s of e.values())
352
480
  this.canvasRenderer.paintShape(s);
353
- this.seekInput.value = String(t);
481
+ this.seekInput.value = String(t), this.timeStartLabel.textContent = this.formatPosition(t), this.updatePlayPauseButton();
482
+ }
483
+ createPlayer() {
484
+ const t = new N(this.currentSighting.timeline, (e, s) => this.onFrame(e, s));
485
+ return t.loop = this.loopEnabled, t;
486
+ }
487
+ togglePlayPause() {
488
+ this.player.playbackState === "playing" ? this.player.pause() : this.player.play(), this.updatePlayPauseButton();
489
+ }
490
+ updatePlayPauseButton() {
491
+ const t = this.player.playbackState === "playing";
492
+ 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);
493
+ }
494
+ toggleLoop() {
495
+ this.loopEnabled = !this.loopEnabled, this.loopButton.setAttribute("aria-pressed", String(this.loopEnabled)), this.player.loop = this.loopEnabled;
496
+ }
497
+ /**
498
+ * Auto-detects the visitor's preferred UI language from `navigator.languages`, falling back to
499
+ * English (already baked into the template) when none of their preferences are supported —
500
+ * see selectLocale. There is deliberately no language-picker UI: this is the only mechanism.
501
+ */
502
+ async loadLocaleMessages() {
503
+ const t = W(navigator.languages, J);
504
+ t !== "en" && this.applyMessages(await z(t));
505
+ }
506
+ applyMessages(t) {
507
+ 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();
354
508
  }
509
+ /**
510
+ * Caches the sighting's real-world reported duration/start (see sightingDurationMs) and sets
511
+ * the player's playback rate and the seek bar's end label from them, rather than from
512
+ * `timeline.duration` (how long the recording itself took to author).
513
+ */
514
+ updateTimeLabels() {
515
+ const t = this.currentSighting.event, e = U(t);
516
+ this.realDurationMs = e !== void 0 && e > 0 ? e : void 0, this.realStartMs = t.time ? d(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);
517
+ }
518
+ /**
519
+ * Turns a `Timeline` position (ms since recording start, i.e. what Player deals in) into what's
520
+ * actually displayed: a real clock time (e.g. "02:47") when a real start/duration are both
521
+ * known, an elapsed real duration ("0:00" based) when only the duration is known, or the
522
+ * recording's own elapsed time when neither is known — see updateTimeLabels.
523
+ */
524
+ formatPosition(t) {
525
+ if (this.realDurationMs === void 0) return h(t);
526
+ const e = this.currentSighting.timeline.duration, s = e > 0 ? t / e * this.realDurationMs : 0;
527
+ return this.realStartMs !== void 0 ? y(g(this.realStartMs + s)) : h(s);
528
+ }
529
+ /**
530
+ * The fixed end-of-timeline label — always the *full* real declared duration (clock time or
531
+ * elapsed), or the recording's own length when no real duration is known. Unlike
532
+ * formatPosition, doesn't scale by `timeline.duration`: a single-keyframe/static recording
533
+ * (timeline.duration === 0) still has a full declared real duration to show as its end.
534
+ */
535
+ formatEndOfTimeline() {
536
+ return this.realDurationMs === void 0 ? h(this.currentSighting.timeline.duration) : this.realStartMs !== void 0 ? y(g(this.realStartMs + this.realDurationMs)) : h(this.realDurationMs);
537
+ }
538
+ }
539
+ function g(i) {
540
+ const t = new Date(i);
541
+ return { hour: t.getUTCHours(), minute: t.getUTCMinutes(), second: t.getUTCSeconds() };
542
+ }
543
+ function y(i) {
544
+ if (i.hour === void 0) return "0:00";
545
+ const t = (e) => String(e).padStart(2, "0");
546
+ return i.second ? `${t(i.hour)}:${t(i.minute ?? 0)}:${t(i.second)}` : `${t(i.hour)}:${t(i.minute ?? 0)}`;
547
+ }
548
+ function h(i) {
549
+ const t = Math.round(i / 1e3), e = Math.floor(t / 60), s = t % 60;
550
+ return `${e}:${String(s).padStart(2, "0")}`;
355
551
  }
356
- const p = "rr0-ufo-player";
357
- function E() {
358
- customElements.get(p) || customElements.define(p, H);
552
+ const m = "rr0-ufo";
553
+ function I() {
554
+ customElements.get(m) || customElements.define(m, G);
359
555
  }
360
- class _ {
556
+ class X {
361
557
  constructor(t, e) {
362
558
  this.timeline = t, this.clock = e;
363
559
  }
@@ -375,11 +571,11 @@ class _ {
375
571
  }
376
572
  recordSample(t) {
377
573
  if (!this.currentPointer || !this.shapePrototype) return;
378
- const e = v(this.shapePrototype, this.currentPointer.x, this.currentPointer.y);
574
+ const e = S(this.shapePrototype, this.currentPointer.x, this.currentPointer.y);
379
575
  this.timeline.addKeyframe(t, [{ sourceId: this.sourceId, shape: e }]);
380
576
  }
381
577
  }
382
- class O {
578
+ class Y {
383
579
  constructor(t) {
384
580
  this.intervalMs = t;
385
581
  }
@@ -398,11 +594,11 @@ class O {
398
594
  this.rafId !== null && (cancelAnimationFrame(this.rafId), this.rafId = null);
399
595
  }
400
596
  }
401
- E();
402
- const a = { width: 48, height: 28 }, U = "ufo-1", y = ["oval", "saucer", "triangle"], K = { presetId: "oval", color: "#39ff14", transparency: 0, haloScale: 1.5 };
403
- class J extends HTMLElement {
597
+ I();
598
+ const r = { width: 48, height: 28 }, Q = "ufo-1", v = ["oval", "saucer", "triangle"], j = { presetId: "oval", color: "#39ff14", transparency: 0, haloScale: 1.5 };
599
+ class V extends HTMLElement {
404
600
  shadow;
405
- playerElement;
601
+ ufoElement;
406
602
  recordButton;
407
603
  samplingRateInput;
408
604
  presetButtons;
@@ -411,16 +607,16 @@ class J extends HTMLElement {
411
607
  haloScaleInput;
412
608
  recorder;
413
609
  isRecording = !1;
414
- currentAppearance = { ...K };
610
+ currentAppearance = { ...j };
415
611
  constructor() {
416
612
  super(), this.shadow = this.attachShadow({ mode: "open" });
417
613
  const t = document.createElement("template");
418
- t.innerHTML = `<style>${b}</style>${S}`, this.shadow.appendChild(t.content.cloneNode(!0)), this.playerElement = document.createElement(p), this.shadow.getElementById("player-slot").replaceWith(this.playerElement), this.recordButton = this.shadow.getElementById("record"), this.samplingRateInput = this.shadow.getElementById("samplingRate"), this.presetButtons = {
614
+ t.innerHTML = `<style>${B}</style>${T}`, this.shadow.appendChild(t.content.cloneNode(!0)), this.ufoElement = document.createElement(m), this.ufoElement.enableClickToPlay = !1, this.shadow.getElementById("ufo-slot").replaceWith(this.ufoElement), this.recordButton = this.shadow.getElementById("record"), this.samplingRateInput = this.shadow.getElementById("samplingRate"), this.presetButtons = {
419
615
  oval: this.shadow.getElementById("preset-oval"),
420
616
  saucer: this.shadow.getElementById("preset-saucer"),
421
617
  triangle: this.shadow.getElementById("preset-triangle")
422
- }, this.colorInput = this.shadow.getElementById("color"), this.transparencyInput = this.shadow.getElementById("transparency"), this.haloScaleInput = this.shadow.getElementById("haloScale"), this.playerElement.canvasElement.addEventListener("pointerdown", (e) => this.onPointerDown(e)), this.playerElement.canvasElement.addEventListener("pointermove", (e) => this.onPointerMove(e)), this.recordButton.addEventListener("click", () => this.toggleRecording());
423
- for (const e of y)
618
+ }, this.colorInput = this.shadow.getElementById("color"), this.transparencyInput = this.shadow.getElementById("transparency"), this.haloScaleInput = this.shadow.getElementById("haloScale"), this.ufoElement.canvasElement.addEventListener("pointerdown", (e) => this.onPointerDown(e)), this.ufoElement.canvasElement.addEventListener("pointermove", (e) => this.onPointerMove(e)), this.recordButton.addEventListener("click", () => this.toggleRecording());
619
+ for (const e of v)
424
620
  this.presetButtons[e].addEventListener("click", () => this.setAppearance({ presetId: e }));
425
621
  this.colorInput.addEventListener("input", () => this.setAppearance({ color: this.colorInput.value })), this.transparencyInput.addEventListener(
426
622
  "input",
@@ -431,10 +627,10 @@ class J extends HTMLElement {
431
627
  ), this.updatePresetButtons(), this.renderPreview();
432
628
  }
433
629
  get sightingData() {
434
- return this.playerElement.sightingData;
630
+ return this.ufoElement.sightingData;
435
631
  }
436
632
  set sightingData(t) {
437
- this.playerElement.sightingData = t;
633
+ this.ufoElement.sightingData = t;
438
634
  }
439
635
  /** The UFO's appearance (shape preset, color, transparency, halo) used for the next recording. */
440
636
  get appearance() {
@@ -447,7 +643,7 @@ class J extends HTMLElement {
447
643
  this.currentAppearance = { ...this.currentAppearance, ...t }, this.updatePresetButtons(), this.isRecording || this.renderPreview();
448
644
  }
449
645
  updatePresetButtons() {
450
- for (const t of y)
646
+ for (const t of v)
451
647
  this.presetButtons[t]?.setAttribute(
452
648
  "aria-pressed",
453
649
  String(t === this.currentAppearance.presetId)
@@ -457,38 +653,38 @@ class J extends HTMLElement {
457
653
  return Number(this.samplingRateInput.value);
458
654
  }
459
655
  buildPrototype(t = this.defaultBounds()) {
460
- return L(t, this.currentAppearance);
656
+ return F(t, this.currentAppearance);
461
657
  }
462
658
  defaultBounds() {
463
- const t = this.playerElement.canvasElement;
659
+ const t = this.ufoElement.canvasElement;
464
660
  return {
465
- x: t.width / 2 - a.width / 2,
466
- y: t.height / 2 - a.height / 2,
467
- width: a.width,
468
- height: a.height
661
+ x: t.width / 2 - r.width / 2,
662
+ y: t.height / 2 - r.height / 2,
663
+ width: r.width,
664
+ height: r.height
469
665
  };
470
666
  }
471
667
  renderPreview() {
472
- const t = this.playerElement.canvasElement;
473
- this.playerElement.renderer.clear(t.width, t.height), this.playerElement.renderer.paintShape(this.buildPrototype());
668
+ const t = this.ufoElement.canvasElement;
669
+ this.ufoElement.renderer.clear(t.width, t.height), this.ufoElement.renderer.paintShape(this.buildPrototype());
474
670
  }
475
671
  toggleRecording() {
476
- const t = this.playerElement.canvasElement;
477
- this.isRecording ? (this.recorder?.stop(), this.isRecording = !1, this.recordButton.textContent = "Record", t.style.cursor = "", t.style.touchAction = "", this.playerElement.refresh()) : (this.recorder = new _(this.playerElement.sighting.timeline, new O(this.samplingRate)), this.recorder.start(U, this.buildPrototype()), this.isRecording = !0, this.recordButton.textContent = "Stop", t.style.cursor = "crosshair", t.style.touchAction = "none");
672
+ const t = this.ufoElement.canvasElement;
673
+ this.isRecording ? (this.recorder?.stop(), this.isRecording = !1, this.recordButton.textContent = "Record", t.style.cursor = "", t.style.touchAction = "", this.ufoElement.refresh()) : (this.recorder = new X(this.ufoElement.sighting.timeline, new Y(this.samplingRate)), this.recorder.start(Q, this.buildPrototype()), this.isRecording = !0, this.recordButton.textContent = "Stop", t.style.cursor = "crosshair", t.style.touchAction = "none");
478
674
  }
479
675
  onPointerDown(t) {
480
676
  this.isRecording && this.onPointerMove(t);
481
677
  }
482
678
  onPointerMove(t) {
483
679
  if (!this.isRecording) return;
484
- const e = this.playerElement.canvasElement, s = e.getBoundingClientRect(), n = t.clientX - s.left, r = t.clientY - s.top;
485
- this.recorder?.onPointerMove(n, r), this.playerElement.renderer.clear(e.width, e.height), this.playerElement.renderer.paintShape(
486
- v(this.buildPrototype({ x: 0, y: 0, width: a.width, height: a.height }), n, r)
680
+ const e = this.ufoElement.canvasElement, s = e.getBoundingClientRect(), n = t.clientX - s.left, a = t.clientY - s.top;
681
+ this.recorder?.onPointerMove(n, a), this.ufoElement.renderer.clear(e.width, e.height), this.ufoElement.renderer.paintShape(
682
+ S(this.buildPrototype({ x: 0, y: 0, width: r.width, height: r.height }), n, a)
487
683
  );
488
684
  }
489
685
  }
490
- const f = "rr0-ufo-recorder";
491
- function W() {
492
- E(), customElements.get(f) || customElements.define(f, J);
686
+ const b = "rr0-ufo-recorder";
687
+ function tt() {
688
+ I(), customElements.get(b) || customElements.define(b, V);
493
689
  }
494
- W();
690
+ tt();
@@ -0,0 +1,10 @@
1
+ const e = {
2
+ play: "Lecture",
3
+ pause: "Pause",
4
+ autoReplay: "Lecture automatique",
5
+ currentPosition: "Position actuelle",
6
+ duration: "Durée"
7
+ };
8
+ export {
9
+ e as ufoMessages_fr
10
+ };