@rr0/ufoathome 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ const O=`
2
+ <div class="witness-selector" id="witness-selector" hidden>
3
+ <label><span id="label-witness">Witness</span> <select id="witness"></select></label>
4
+ </div>
5
+ <div id="ufo-slot"></div>
6
+ `,R=`
7
+ :host {
8
+ display: block;
9
+ font-family: sans-serif;
10
+ }
11
+ .witness-selector {
12
+ margin-bottom: 0.5em;
13
+ }
14
+ `,N=`
15
+ <div class="stage" id="stage">
16
+ <div class="frame" id="frame">
17
+ <canvas id="canvas" width="640" height="360"></canvas>
18
+ </div>
19
+ <button id="fullscreen" class="fullscreen-btn" type="button" title="Fullscreen" aria-label="Fullscreen">⛶</button>
20
+ <div class="toolbar" id="toolbar">
21
+ <button id="play-pause" type="button" title="Play" aria-label="Play">▶</button>
22
+ <span id="time-start" class="time-label" title="Current position">0:00</span>
23
+ <input id="seek" type="range" min="0" max="0" value="0" step="1"/>
24
+ <span id="time-end" class="time-label" title="Duration">0:00</span>
25
+ <button id="loop" type="button" title="Auto-replay" aria-label="Auto-replay" aria-pressed="true">↻</button>
26
+ </div>
27
+ </div>
28
+ `,U=`
29
+ :host {
30
+ display: block;
31
+ font-family: sans-serif;
32
+ }
33
+ /* height:100% is a no-op fallback (resolves to auto) whenever .stage's own parent/host has no
34
+ definite height of its own (the normal, standalone case — .stage's height stays driven by
35
+ .frame's content, unchanged) — but it matters when this element is embedded with a definite
36
+ host size from outside (e.g. <rr0-scene>'s .ufo-overlay sizing this element to fill its own
37
+ #stage while THAT is fullscreen): it lets .toolbar/.fullscreen-btn, anchored to .stage below,
38
+ actually reach that outer element's true edges instead of only .frame's letterboxed ones. */
39
+ .stage {
40
+ position: relative;
41
+ width: 100%;
42
+ height: 100%;
43
+ }
44
+ /* The browser's own fullscreen UA styles force the fullscreened element (.stage) to fill the
45
+ whole viewport (100vw/100vh) regardless of its content's aspect ratio. .toolbar/.fullscreen-btn
46
+ are anchored to .stage itself (not .frame) specifically so they stay pinned to the true screen
47
+ edges, full width, like a normal video player's controls — not stuck to the letterboxed
48
+ content's own (possibly smaller, centered) box above/around them. */
49
+ .stage:fullscreen {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ width: 100vw;
54
+ height: 100vh;
55
+ background: #000;
56
+ }
57
+ /* max-width/max-height are unconditional (not just a :fullscreen override): percentages resolve
58
+ against .stage's height, which is only definite when .stage itself has one (fullscreen, or the
59
+ nested-in-<rr0-scene> case above) — otherwise they're inert, so this is always safe. When
60
+ definite, the browser's aspect-ratio/min-max interplay algorithm correctly derives whichever
61
+ of width/height is the tighter constraint from the other — a real "contain, centered" fit,
62
+ not just a single-axis cap that can let the other axis overflow and crop instead of shrink. */
63
+ .frame {
64
+ width: 100%;
65
+ max-width: 100%;
66
+ aspect-ratio: 640 / 360;
67
+ max-height: 100%;
68
+ }
69
+ canvas {
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ background: var(--ufo-canvas-background, #050510);
74
+ border: var(--ufo-canvas-border, 1px solid #333);
75
+ box-sizing: border-box;
76
+ }
77
+ .toolbar {
78
+ position: absolute;
79
+ left: 0;
80
+ right: 0;
81
+ bottom: 0;
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 0.5em;
85
+ padding: 0.4em 0.6em;
86
+ background: rgba(0, 0, 0, 0.55);
87
+ transition: opacity 0.15s ease;
88
+ }
89
+ /* While playing, the toolbar and fullscreen button auto-hide and only reappear on hover — kept
90
+ always visible while paused/stopped, since that's when the user is most likely to want them.
91
+ Deliberately hover-only, not :focus-within: a clicked button/range input keeps keyboard focus
92
+ after the pointer moves away, which would otherwise keep them stuck visible indefinitely after
93
+ any interaction. */
94
+ .auto-hide {
95
+ opacity: 0;
96
+ pointer-events: none;
97
+ }
98
+ .stage:hover .auto-hide {
99
+ opacity: 1;
100
+ pointer-events: auto;
101
+ }
102
+ .fullscreen-btn {
103
+ position: absolute;
104
+ top: 0.4em;
105
+ right: 0.4em;
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ width: 1.8em;
110
+ height: 1.8em;
111
+ padding: 0;
112
+ border: none;
113
+ border-radius: 3px;
114
+ cursor: pointer;
115
+ font-size: 1em;
116
+ line-height: 1;
117
+ background: rgba(0, 0, 0, 0.55);
118
+ color: #fff;
119
+ transition: opacity 0.15s ease;
120
+ }
121
+ input[type=range] {
122
+ flex: 1;
123
+ }
124
+ .toolbar button {
125
+ display: inline-flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ width: 1.8em;
129
+ height: 1.8em;
130
+ padding: 0;
131
+ border-radius: 3px;
132
+ cursor: pointer;
133
+ font-size: 1em;
134
+ line-height: 1;
135
+ }
136
+ .toolbar button[aria-pressed="true"] {
137
+ outline: 2px solid #39f;
138
+ }
139
+ .time-label {
140
+ color: #fff;
141
+ font-variant-numeric: tabular-nums;
142
+ font-size: 0.85em;
143
+ min-width: 3em;
144
+ text-align: center;
145
+ }
146
+ `;function W(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 M(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 $(n){return`#${n.map(e=>Math.round(e).toString(16).padStart(2,"0")).join("")}`}function H(n,e,t){const s=M(n),i=M(e);return!s||!i?t<1?n:e:$([l(s[0],i[0],t),l(s[1],i[1],t),l(s[2],i[2],t)])}function J(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=H(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 S{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?J(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&&W(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 S;for(const s of e.keyframes)t.addKeyframe(s.t,s.shapes);return t}}function z(n,e,t){return Math.max(e,Math.min(t,n))}function K(n,e,t){const s=((e-n)%360+540)%360-180;return((n+s*t)%360+360)%360}function p(n,e,t){return n+(e-n)*t}function q(n,e,t){return{lat:n.lat===void 0||e.lat===void 0?void 0:p(n.lat,e.lat,t),lng:n.lng===void 0||e.lng===void 0?void 0:p(n.lng,e.lng,t),elevationM:p(n.elevationM,e.elevationM,t),headingDeg:n.headingDeg===void 0||e.headingDeg===void 0?void 0:K(n.headingDeg,e.headingDeg,t),pitchDeg:p(n.pitchDeg,e.pitchDeg,t),fovDeg:p(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?q(s.pose,i.pose,z((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 E(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 j(n){if(n.durationSeconds!==void 0)return n.durationSeconds*1e3;const e=n.time?E(n.time):void 0,t=n.endTime?E(n.endTime):void 0;return e!==void 0&&t!==void 0?t-e:void 0}class k{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 k({eventType:"sighting",time:e,place:t},new S,new y,s)}}class Z{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 G=["nw","n","ne","e","se","s","sw","w"],Q=24;function V(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 X(n){return{x:n.x+n.width/2,y:n.y+n.height/2}}function Y(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=X(n.bounds),o={};for(const c of Object.keys(r))o[c]=V(r[c],a,n.angle);return o}const I=6,b=I/2,ee=20;class te{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=ee*e.haloScale,this.paintBase(e),this.ctx.restore()}paintSelectionHandles(e){const t=Y(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 G){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 se(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 ne(n){return new k({eventType:"sighting",time:n.time,endTime:n.endTime,durationSeconds:n.durationSeconds,place:n.place},S.fromJSON(n.timeline),n.observerTrack?y.fromJSON(n.observerTrack):new y,n.witnessId,n.witnessName,n.caseId)}function C(n,e){for(const t of n){const s=t.toLowerCase().split("-")[0];if(e.includes(s))return s}return"en"}const ie="modulepreload",re=function(n,e){return new URL(n,e).href},P={},x=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(m=>({status:"fulfilled",value:m}),m=>({status:"rejected",reason:m}))))};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=re(h,s),h in P)return;P[h]=!0;const d=h.endsWith(".css"),m=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}"]${m}`))return;const u=document.createElement("link");if(u.rel=d?"stylesheet":ie,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)})},D=["en","fr"],ae={en:()=>x(()=>Promise.resolve().then(()=>ce),void 0,import.meta.url).then(n=>n.ufoMessages_en),fr:()=>x(()=>import("./assets/UfoMessages_fr-COCxf8Q_.js"),[],import.meta.url).then(n=>n.ufoMessages_fr)};function oe(n){return ae[n]()}const le={en:()=>x(()=>import("./assets/WitnessSelectorMessages_en-D8xgZDfY.js"),[],import.meta.url).then(n=>n.witnessSelectorMessages_en),fr:()=>x(()=>import("./assets/WitnessSelectorMessages_fr-C0GHqC_J.js"),[],import.meta.url).then(n=>n.witnessSelectorMessages_fr)};function he(n){return le[n]()}const F={play:"Play",pause:"Pause",autoReplay:"Auto-replay",currentPosition:"Current position",duration:"Duration",fullscreen:"Fullscreen",exitFullscreen:"Exit fullscreen"},ce=Object.freeze(Object.defineProperty({__proto__:null,ufoMessages_en:F},Symbol.toStringTag,{value:"Module"}));class ue extends HTMLElement{static get observedAttributes(){return["src"]}shadow;stageElement;canvas;canvasRenderer;toolbar;playPauseButton;loopButton;fullscreenButton;seekInput;timeStartLabel;timeEndLabel;currentSighting=k.create();player;loopEnabled=!0;highlightedSourceId;enableClickToPlay=!0;fullscreenTarget;messages=F;realDurationMs;realStartMs;handleFullscreenChange=()=>this.updateFullscreenButton();constructor(){super(),this.shadow=this.attachShadow({mode:"open"});const e=document.createElement("template");e.innerHTML=`<style>${U}</style>${N}`,this.shadow.appendChild(e.content.cloneNode(!0)),this.stageElement=this.shadow.getElementById("stage"),this.canvas=this.shadow.getElementById("canvas"),this.canvasRenderer=new te(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 se(this.currentSighting)}set sightingData(e){this.currentSighting=ne(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 Z(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=C(navigator.languages,D);e!=="en"&&this.applyMessages(await oe(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=j(e);this.realDurationMs=t!==void 0&&t>0?t:void 0,this.realStartMs=e.time?E(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 w(e);const t=this.currentSighting.timeline.duration,s=t>0&&e<=t?e/t*this.realDurationMs:e;return this.realStartMs!==void 0?B(L(this.realStartMs+s)):w(s)}formatEndOfTimeline(){return this.realDurationMs===void 0?w(this.currentSighting.timeline.duration):this.realStartMs!==void 0?B(L(this.realStartMs+this.realDurationMs)):w(this.realDurationMs)}}function L(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 w(n){const e=Math.round(n/1e3),t=Math.floor(e/60),s=e%60;return`${t}:${String(s).padStart(2,"0")}`}const T="rr0-ufo";function _(){customElements.get(T)||customElements.define(T,ue)}_();class de extends HTMLElement{static get observedAttributes(){return["src"]}shadow;ufoElement;selectorContainer;witnessSelect;labelWitness;entries=[];currentSrc;constructor(){super(),this.shadow=this.attachShadow({mode:"open"});const e=document.createElement("template");e.innerHTML=`<style>${R}</style>${O}`,this.shadow.appendChild(e.content.cloneNode(!0)),this.ufoElement=document.createElement(T),this.shadow.getElementById("ufo-slot").replaceWith(this.ufoElement),this.selectorContainer=this.shadow.getElementById("witness-selector"),this.witnessSelect=this.shadow.getElementById("witness"),this.labelWitness=this.shadow.getElementById("label-witness"),this.witnessSelect.addEventListener("change",()=>this.selectWitness(this.witnessSelect.value)),this.loadLocaleMessages()}async loadLocaleMessages(){const e=C(navigator.languages,D);if(e==="en")return;const t=await he(e);this.labelWitness.textContent=t.witness}connectedCallback(){const e=this.getAttribute("src");e&&this.loadFromSrc(e)}attributeChangedCallback(e,t,s){e==="src"&&s&&s!==t&&this.isConnected&&this.loadFromSrc(s)}async loadFromSrc(e){const s=await(await fetch(e)).json();await this.loadWitnessUrls(s)}get witnessUrls(){return this.entries.map(e=>e.src)}set witnessUrls(e){this.loadWitnessUrls(e)}async loadWitnessUrls(e){const t=await Promise.all(e.map(async s=>{const i=await fetch(s);return{src:s,sighting:await i.json()}}));this.setEntries(t)}setEntries(e){this.entries=e,this.warnOnMismatchedCaseIds(e),this.selectorContainer.hidden=e.length<=1,this.witnessSelect.innerHTML="";for(const s of e){const i=document.createElement("option");i.value=s.src,i.textContent=s.sighting.witnessName??s.sighting.witnessId??s.src,this.witnessSelect.appendChild(i)}const t=e.find(s=>s.src===this.currentSrc)??e[0];t&&this.selectWitness(t.src)}warnOnMismatchedCaseIds(e){const t=new Set(e.map(s=>s.sighting.caseId).filter(s=>s!==void 0));t.size>1&&console.warn(`<rr0-ufo-witnesses>: witnesses declare different case ids (${[...t].join(", ")}) — they may not belong to the same case.`)}selectWitness(e){const t=this.entries.find(s=>s.src===e);t&&(this.currentSrc=e,this.witnessSelect.value=e,this.ufoElement.sightingData=t.sighting)}}const A="rr0-ufo-witnesses";function fe(){_(),customElements.get(A)||customElements.define(A,de)}fe();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rr0/ufoathome",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "UFO@home — record and replay a UFO sighting's shape, movement and appearance",
6
6
  "author": "Jérôme Beau <rr0@rr0.org> (https://rr0.org)",
7
7
  "license": "MIT",
@@ -16,12 +16,14 @@
16
16
  "exports": {
17
17
  "./ufo": "./dist-embed-ufo/rr0-ufo.mjs",
18
18
  "./recorder": "./dist-embed/rr0-ufo-recorder.mjs",
19
- "./scene": "./dist-embed-scene/rr0-scene.mjs"
19
+ "./scene": "./dist-embed-scene/rr0-scene.mjs",
20
+ "./witnesses": "./dist-embed-witnesses/rr0-ufo-witnesses.mjs"
20
21
  },
21
22
  "files": [
22
23
  "dist-embed",
23
24
  "dist-embed-ufo",
24
- "dist-embed-scene"
25
+ "dist-embed-scene",
26
+ "dist-embed-witnesses"
25
27
  ],
26
28
  "scripts": {
27
29
  "dev": "vite",
@@ -29,21 +31,25 @@
29
31
  "build:embed": "tsc --noEmit && vite build --config vite.embed.config.ts",
30
32
  "build:embed-ufo": "tsc --noEmit && vite build --config vite.embed-ufo.config.ts",
31
33
  "build:embed-scene": "tsc --noEmit && vite build --config vite.embed-scene.config.ts",
32
- "build:all": "npm run build && npm run build:embed && npm run build:embed-ufo && npm run build:embed-scene",
33
- "prepublishOnly": "npm run build:embed && npm run build:embed-ufo && npm run build:embed-scene && npm test",
34
+ "build:embed-witnesses": "tsc --noEmit && vite build --config vite.embed-witnesses.config.ts",
35
+ "build:all": "npm run build && npm run build:embed && npm run build:embed-ufo && npm run build:embed-scene && npm run build:embed-witnesses",
36
+ "prepublishOnly": "npm run build:embed && npm run build:embed-ufo && npm run build:embed-scene && npm run build:embed-witnesses && npm test",
34
37
  "preview": "vite preview",
35
38
  "test": "vitest run",
36
- "test:watch": "vitest"
39
+ "test:watch": "vitest",
40
+ "build:stars": "tsx scripts/build-star-catalog.ts"
37
41
  },
38
42
  "dependencies": {
39
43
  "@rr0/data": "^0.3.40",
40
44
  "@rr0/place": "^0.5.3",
41
45
  "@rr0/time": "^0.11.2",
46
+ "astronomy-engine": "^2.1.19",
42
47
  "three": "^0.185.1"
43
48
  },
44
49
  "devDependencies": {
45
50
  "@types/three": "^0.185.3",
46
51
  "jsdom": "^25.0.0",
52
+ "tsx": "^4.23.5",
47
53
  "typescript": "^5.9.2",
48
54
  "vite": "^6.3.5",
49
55
  "vitest": "^3.0.0"
@@ -1,10 +0,0 @@
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
- };
@@ -1,10 +0,0 @@
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
- };
@@ -1,10 +0,0 @@
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
- };