@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.
- package/README.md +192 -2
- package/dist-embed/UfoMessages_fr-DKnWFOyl.js +10 -0
- package/dist-embed/rr0-ufo-recorder.mjs +298 -102
- package/dist-embed-scene/UfoMessages_fr-DKnWFOyl.js +10 -0
- package/dist-embed-scene/rr0-scene.mjs +21178 -0
- package/dist-embed-ufo/UfoMessages_fr-DKnWFOyl.js +10 -0
- package/dist-embed-ufo/rr0-ufo.mjs +471 -0
- package/package.json +13 -8
- package/dist-embed-player/rr0-ufo-player.mjs +0 -275
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
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="
|
|
17
|
-
`,
|
|
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
|
-
`,
|
|
41
|
-
<div class="
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
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
|
-
|
|
77
|
+
padding: 0.4em 0.6em;
|
|
78
|
+
background: rgba(0, 0, 0, 0.55);
|
|
79
|
+
transition: opacity 0.15s ease;
|
|
57
80
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
136
|
+
return E(i, n, t);
|
|
84
137
|
}
|
|
85
|
-
function
|
|
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
|
|
144
|
+
return E(i, n, t);
|
|
92
145
|
}
|
|
93
|
-
const
|
|
94
|
-
oval:
|
|
95
|
-
saucer:
|
|
96
|
-
triangle:
|
|
146
|
+
const D = {
|
|
147
|
+
oval: R,
|
|
148
|
+
saucer: L,
|
|
149
|
+
triangle: C
|
|
97
150
|
};
|
|
98
|
-
function
|
|
151
|
+
function F(i, t) {
|
|
99
152
|
return {
|
|
100
|
-
...
|
|
153
|
+
...D[t.presetId](i, t.color),
|
|
101
154
|
transparency: t.transparency,
|
|
102
155
|
haloScale: t.haloScale
|
|
103
156
|
};
|
|
104
157
|
}
|
|
105
|
-
function
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
152
|
-
if (
|
|
153
|
-
return n.shapes[
|
|
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
|
|
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
|
-
|
|
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
|
|
246
|
+
return new u({ eventType: "sighting", time: t, place: e }, new c(), s);
|
|
185
247
|
}
|
|
186
248
|
}
|
|
187
|
-
class
|
|
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.
|
|
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
|
|
234
|
-
class
|
|
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:
|
|
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,
|
|
251
|
-
|
|
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 =
|
|
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:
|
|
261
|
-
this.ctx.strokeStyle = "lightgray", this.ctx.strokeRect(e, s, n,
|
|
262
|
-
const
|
|
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 +
|
|
268
|
-
[e + o, s +
|
|
269
|
-
[e, s +
|
|
344
|
+
[e + n, s + a],
|
|
345
|
+
[e + o, s + a],
|
|
346
|
+
[e, s + a],
|
|
270
347
|
[e, s + l]
|
|
271
348
|
];
|
|
272
|
-
for (const [
|
|
273
|
-
this.ctx.fillRect(
|
|
349
|
+
for (const [x, k] of P)
|
|
350
|
+
this.ctx.fillRect(x - f, k - f, p, p);
|
|
274
351
|
}
|
|
275
352
|
}
|
|
276
|
-
function
|
|
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
|
|
286
|
-
return new
|
|
287
|
-
{
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
408
|
+
toolbar;
|
|
409
|
+
playPauseButton;
|
|
410
|
+
loopButton;
|
|
301
411
|
seekInput;
|
|
302
|
-
|
|
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>${
|
|
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
|
|
449
|
+
return $(this.currentSighting);
|
|
323
450
|
}
|
|
324
451
|
set sightingData(t) {
|
|
325
|
-
this.currentSighting =
|
|
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
|
|
329
|
-
* this element) can add keyframes to it directly as it
|
|
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
|
|
357
|
-
function
|
|
358
|
-
customElements.get(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
402
|
-
const
|
|
403
|
-
class
|
|
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
|
-
|
|
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 = { ...
|
|
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>${
|
|
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.
|
|
423
|
-
for (const e of
|
|
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.
|
|
630
|
+
return this.ufoElement.sightingData;
|
|
435
631
|
}
|
|
436
632
|
set sightingData(t) {
|
|
437
|
-
this.
|
|
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
|
|
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
|
|
656
|
+
return F(t, this.currentAppearance);
|
|
461
657
|
}
|
|
462
658
|
defaultBounds() {
|
|
463
|
-
const t = this.
|
|
659
|
+
const t = this.ufoElement.canvasElement;
|
|
464
660
|
return {
|
|
465
|
-
x: t.width / 2 -
|
|
466
|
-
y: t.height / 2 -
|
|
467
|
-
width:
|
|
468
|
-
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.
|
|
473
|
-
this.
|
|
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.
|
|
477
|
-
this.isRecording ? (this.recorder?.stop(), this.isRecording = !1, this.recordButton.textContent = "Record", t.style.cursor = "", t.style.touchAction = "", this.
|
|
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.
|
|
485
|
-
this.recorder?.onPointerMove(n,
|
|
486
|
-
|
|
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
|
|
491
|
-
function
|
|
492
|
-
|
|
686
|
+
const b = "rr0-ufo-recorder";
|
|
687
|
+
function tt() {
|
|
688
|
+
I(), customElements.get(b) || customElements.define(b, V);
|
|
493
689
|
}
|
|
494
|
-
|
|
690
|
+
tt();
|