@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.
- package/README.md +137 -28
- package/dist-embed/assets/UfoMessages_fr-COCxf8Q_.js +1 -0
- package/dist-embed/assets/UfoRecorderMessages_fr-DRl6tTYJ.js +1 -0
- package/dist-embed/assets/stars-mag7.5-DDr9QasD.bin +0 -0
- package/dist-embed/rr0-ufo-recorder.mjs +4331 -590
- package/dist-embed-scene/assets/UfoMessages_fr-COCxf8Q_.js +1 -0
- package/dist-embed-scene/assets/stars-mag7.5-DDr9QasD.bin +0 -0
- package/dist-embed-scene/rr0-scene.mjs +340 -17164
- package/dist-embed-ufo/assets/UfoMessages_fr-COCxf8Q_.js +1 -0
- package/dist-embed-ufo/rr0-ufo.mjs +67 -405
- package/dist-embed-witnesses/assets/UfoMessages_fr-COCxf8Q_.js +1 -0
- package/dist-embed-witnesses/assets/WitnessSelectorMessages_en-D8xgZDfY.js +1 -0
- package/dist-embed-witnesses/assets/WitnessSelectorMessages_fr-C0GHqC_J.js +1 -0
- package/dist-embed-witnesses/rr0-ufo-witnesses.mjs +146 -0
- package/package.json +12 -6
- package/dist-embed/UfoMessages_fr-DKnWFOyl.js +0 -10
- package/dist-embed-scene/UfoMessages_fr-DKnWFOyl.js +0 -10
- package/dist-embed-ufo/UfoMessages_fr-DKnWFOyl.js +0 -10
|
@@ -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
|
|
2
|
-
<div class="stage">
|
|
3
|
-
<
|
|
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
|
-
`,
|
|
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:
|
|
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-
|
|
43
|
-
paused/stopped, since that's when the user is most likely to want
|
|
44
|
-
not :focus-within: a clicked button/range input keeps keyboard focus
|
|
45
|
-
away, which would otherwise keep
|
|
46
|
-
.
|
|
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 .
|
|
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};
|