@rr0/ufoathome 0.10.4 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- const J=`
1
+ const K=`
2
2
  <div class="stage" id="stage">
3
3
  <div class="frame" id="frame">
4
4
  <canvas id="canvas" width="640" height="360"></canvas>
@@ -13,7 +13,7 @@ const J=`
13
13
  <button id="loop" type="button" title="Auto-replay" aria-label="Auto-replay" aria-pressed="true">↻</button>
14
14
  </div>
15
15
  </div>
16
- `,K=`
16
+ `,G=`
17
17
  :host {
18
18
  display: block;
19
19
  font-family: sans-serif;
@@ -71,6 +71,51 @@ canvas {
71
71
  border: var(--ufo-canvas-border, 1px solid #333);
72
72
  box-sizing: border-box;
73
73
  }
74
+ /* Hover feedback for the editor (<rr0-ufo-recorder>): what the pointer is over is drawn INSIDE
75
+ the canvas, so only script can hit-test it — but the appearance stays here, in CSS. The
76
+ component only ever states what is under the pointer (data-cursor="move", "resize-ns", ...);
77
+ which actual cursor that means is this stylesheet's business alone. Plain <rr0-ufo> playback
78
+ never sets the attribute, so it keeps the default arrow throughout.
79
+ Directions are SCREEN axes, already accounting for the shape's own rotation (see
80
+ ShapeHandles.resizeAxisFor) — a 45-degree-rotated shape's top-left handle really does resize
81
+ along the screen's north-east/south-west diagonal, so that is the cursor it gets. */
82
+ canvas[data-cursor="record"] {
83
+ cursor: crosshair;
84
+ }
85
+ canvas[data-cursor="select"] {
86
+ cursor: pointer;
87
+ }
88
+ canvas[data-cursor="move"] {
89
+ cursor: move;
90
+ }
91
+ canvas[data-cursor="vertex"] {
92
+ cursor: cell;
93
+ }
94
+ canvas[data-cursor="pan"] {
95
+ cursor: grab;
96
+ }
97
+ canvas[data-cursor="panning"] {
98
+ cursor: grabbing;
99
+ }
100
+ canvas[data-cursor="resize-ew"] {
101
+ cursor: ew-resize;
102
+ }
103
+ canvas[data-cursor="resize-ns"] {
104
+ cursor: ns-resize;
105
+ }
106
+ canvas[data-cursor="resize-nwse"] {
107
+ cursor: nwse-resize;
108
+ }
109
+ canvas[data-cursor="resize-nesw"] {
110
+ cursor: nesw-resize;
111
+ }
112
+ /* No native CSS cursor means "rotate", so this one is drawn here: a circular arrow, white with a
113
+ black outline so it stays readable over both a bright daytime sky and a night one. 12 12 is its
114
+ hotspot (the 24x24 image's own center, i.e. the middle of the circle it draws). The grab
115
+ fallback applies if the data URI cursor is ever rejected. */
116
+ canvas[data-cursor="rotate"] {
117
+ cursor: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2224%22%20height=%2224%22%3E%3Cpath%20d=%22M12%205A7%207%200%201%201%206.1%208.5%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%224%22%20stroke-linecap=%22round%22/%3E%3Cpath%20d=%22M12%201.5%2012%208.5%2017%205Z%22%20fill=%22%23000%22%20stroke=%22%23000%22%20stroke-width=%223%22%20stroke-linejoin=%22round%22/%3E%3Cpath%20d=%22M12%205A7%207%200%201%201%206.1%208.5%22%20fill=%22none%22%20stroke=%22%23fff%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22/%3E%3Cpath%20d=%22M12%201.5%2012%208.5%2017%205Z%22%20fill=%22%23fff%22/%3E%3C/svg%3E") 12 12, grab;
118
+ }
74
119
  .toolbar {
75
120
  position: absolute;
76
121
  left: 0;
@@ -164,4 +209,4 @@ input[type=range] {
164
209
  pointer-events: none;
165
210
  white-space: nowrap;
166
211
  }
167
- `;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 p(n,e,t){return n+(e-n)*t}function B(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 G(n){return`#${n.map(e=>Math.round(e).toString(16).padStart(2,"0")).join("")}`}function V(n,e,t){const s=B(n),i=B(e);return!s||!i?t<1?n:e:G([p(s[0],i[0],t),p(s[1],i[1],t),p(s[2],i[2],t)])}function Y(n,e,t){const s={x:p(n.bounds.x,e.bounds.x,t),y:p(n.bounds.y,e.bounds.y,t),width:p(n.bounds.width,e.bounds.width,t),height:p(n.bounds.height,e.bounds.height,t)},i=p(n.angle,e.angle,t),r=p(n.transparency,e.transparency,t),a=p(n.haloScale,e.haloScale,t),o=V(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((h,c)=>({x:p(h.x,e.points[c].x,t),y:p(h.y,e.points[c].y,t)}))}:{...t<1?n:e,bounds:s,angle:i,transparency:r,haloScale:a,color:o}}class M{keyframes=[];order=[];groups=[];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]});for(const i of t)this.order.includes(i.sourceId)||this.order.push(i.sourceId)}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?Y(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}}}removeSource(e){for(let t=this.keyframes.length-1;t>=0;t--){const s=this.keyframes[t].shapes.filter(i=>i.sourceId!==e);s.length===0?this.keyframes.splice(t,1):s.length!==this.keyframes[t].shapes.length&&(this.keyframes[t]={t:this.keyframes[t].t,shapes:s})}this.order=this.order.filter(t=>t!==e),this.removeFromGroup(e)}group(e){if(!(e.length<2)){for(const t of e)this.removeFromGroup(t);this.groups.push([...e])}}ungroup(e){const t=this.groups.findIndex(s=>s.includes(e));t!==-1&&this.groups.splice(t,1)}groupMembers(e){return this.groups.find(t=>t.includes(e))}removeFromGroup(e){const t=this.groups.findIndex(i=>i.includes(e));if(t===-1)return;const s=this.groups[t].filter(i=>i!==e);s.length<2?this.groups.splice(t,1):this.groups[t]=s}bringToFront(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.push(e))}sendToBack(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.unshift(e))}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get sourceIds(){return[...this.order]}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes,order:this.order,groups:this.groups}}static fromJSON(e){const t=new M;for(const s of e.keyframes)t.addKeyframe(s.t,s.shapes);if(e.order){const s=t.order.filter(i=>!e.order.includes(i));t.order=[...e.order,...s]}return e.groups&&(t.groups=e.groups.map(s=>s.filter(i=>t.order.includes(i))).filter(s=>s.length>=2)),t}}function q(n,e,t){return Math.max(e,Math.min(t,n))}function X(n,e,t){const s=((e-n)%360+540)%360-180;return((n+s*t)%360+360)%360}function x(n,e,t){return n+(e-n)*t}function Z(n,e,t){return{lat:n.lat===void 0||e.lat===void 0?void 0:x(n.lat,e.lat,t),lng:n.lng===void 0||e.lng===void 0?void 0:x(n.lng,e.lng,t),elevationM:x(n.elevationM,e.elevationM,t),headingDeg:n.headingDeg===void 0||e.headingDeg===void 0?void 0:X(n.headingDeg,e.headingDeg,t),pitchDeg:x(n.pitchDeg,e.pitchDeg,t),fovDeg:x(n.fovDeg,e.fovDeg,t)}}class b{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}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}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?Z(s.pose,i.pose,q((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 b;for(const s of e.keyframes)t.addKeyframe(s.t,s.pose);return t}}function S(n,e,t){return n+(e-n)*t}function Q(n,e,t){const s=((e-n)%360+540)%360-180;return((n+s*t)%360+360)%360}function j(n,e,t){return Math.max(e,Math.min(t,n))}function ee(n,e,t){return{cloudCover:S(n.cloudCover,e.cloudCover,t),cloudDarkness:S(n.cloudDarkness,e.cloudDarkness,t),precipitationType:t<1?n.precipitationType:e.precipitationType,precipitationIntensity:S(n.precipitationIntensity,e.precipitationIntensity,t),windDirectionDeg:Q(n.windDirectionDeg,e.windDirectionDeg,t),windSpeed:S(n.windSpeed,e.windSpeed,t),storm:t<1?n.storm:e.storm}}class w{keyframes=[];addKeyframe(e,t){const s=this.findInsertIndex(e);this.keyframes[s]?.t===e?this.keyframes[s]={t:e,weather:t}:this.keyframes.splice(s,0,{t:e,weather:t})}clear(){this.keyframes.length=0}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}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}getLatestWeatherAt(e){let t=this.findInsertIndex(e);return this.keyframes[t]?.t!==e&&(t-=1),t>=0?this.keyframes[t].weather:void 0}getInterpolatedWeatherAt(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.weather;const i=this.keyframes[t]?.t===e?void 0:this.keyframes[t];return s?i?ee(s.weather,i.weather,j((e-s.t)/(i.t-s.t),0,1)):s.weather:i?.weather}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 w;for(const s of e.keyframes)t.addKeyframe(s.t,s.weather);return t}}function L(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 A(n){return(((n.day??0)*24+(n.hour??0))*60+(n.minute??0))*6e4+(n.second??0)*1e3}const te=["year","month","day","hour","minute"],se=["day","hour","minute"];function O(n,e){return e.filter(t=>n[t]!==void 0).join(",")}function ne(n){if(n.durationSeconds!==void 0)return n.durationSeconds*1e3;if(!n.time||!n.endTime)return;const e=n.time.year!==void 0&&n.endTime.year!==void 0,t=e?te:se,s=O(n.time,t);if(!(s===""||s!==O(n.endTime,t)))return e?L(n.endTime)-L(n.time):A(n.endTime)-A(n.time)}class D{constructor(e,t,s,i,r,a,o,h=[]){this.event=e,this.timeline=t,this.witnessTrack=s,this.weatherTrack=i,this.witness=r,this.caseId=a,this.weather=o,this.decor=h}static create(e,t,s){return new D({eventType:"sighting",time:e,place:t},new M,new b,new w,s)}}class ie{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 U=["nw","n","ne","e","se","s","sw","w"],re=24,I=8,ae=3,oe=8;function E(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 v(n){return{x:n.x+n.width/2,y:n.y+n.height/2}}const le={nw:{left:!0,top:!0},n:{top:!0},ne:{right:!0,top:!0},e:{right:!0},se:{right:!0,bottom:!0},s:{bottom:!0},sw:{left:!0,bottom:!0},w:{left:!0}};class m{static handlePointsFor(e){const{x:t,y:s,width:i,height:r}=e.bounds,a={nw:{x:t,y:s},n:{x:t+i/2,y:s},ne:{x:t+i,y:s},e:{x:t+i,y:s+r/2},se:{x:t+i,y:s+r},s:{x:t+i/2,y:s+r},sw:{x:t,y:s+r},w:{x:t,y:s+r/2},rotate:{x:t+i/2,y:s-re}},o=v(e.bounds),h={};for(const c of Object.keys(a))h[c]=E(a[c],o,e.angle);return h}static hitTestHandle(e,t,s=8,i=[...U,"rotate"]){const r=m.handlePointsFor(e);for(const a of i)if(Math.hypot(t.x-r[a].x,t.y-r[a].y)<=s)return a}static resizeBounds(e,t,s,i){const r=v(e),a=E(i,r,-t),{x:o,y:h,width:c,height:d}=e,l=le[s];let u=o,y=h,f=o+c,g=h+d;return l.left&&(u=Math.min(a.x,f-I)),l.right&&(f=Math.max(a.x,u+I)),l.top&&(y=Math.min(a.y,g-I)),l.bottom&&(g=Math.max(a.y,y+I)),{x:u,y,width:f-u,height:g-y}}static resizeShape(e,t,s){if(t==="rotate")throw new Error("resizeShape does not accept the rotate handle");const{width:i,height:r}=e.bounds,a=m.resizeBounds(e.bounds,e.angle,t,s);if(e.kind==="oval")return{...e,bounds:a};const o=i===0?1:a.width/i,h=r===0?1:a.height/r;return{...e,bounds:a,points:e.points.map(c=>({x:c.x*o,y:c.y*h}))}}static rotateShape(e,t){const s=v(e.bounds),i=Math.atan2(t.y-s.y,t.x-s.x)+Math.PI/2;return{...e,angle:i}}static groupBoundsFor(e){const t=Math.min(...e.map(a=>a.x)),s=Math.min(...e.map(a=>a.y)),i=Math.max(...e.map(a=>a.x+a.width)),r=Math.max(...e.map(a=>a.y+a.height));return{x:t,y:s,width:i-t,height:r-s}}static vertexPointsFor(e){const t=v(e.bounds);return e.points.map(s=>E({x:e.bounds.x+s.x,y:e.bounds.y+s.y},t,e.angle))}static hitTestVertex(e,t,s=oe){const i=m.vertexPointsFor(e);let r,a=s;return i.forEach((o,h)=>{const c=Math.hypot(t.x-o.x,t.y-o.y);c<=a&&(a=c,r=h)}),r}static toLocalPoint(e,t){const s=v(e.bounds),i=E(t,s,-e.angle);return{x:i.x-e.bounds.x,y:i.y-e.bounds.y}}static moveVertex(e,t,s){const i=e.points.map((l,u)=>u===t?m.toLocalPoint(e,s):l),r=Math.min(...i.map(l=>l.x)),a=Math.min(...i.map(l=>l.y)),o=Math.max(...i.map(l=>l.x)),h=Math.max(...i.map(l=>l.y)),c={x:e.bounds.x+r,y:e.bounds.y+a,width:o-r,height:h-a},d=i.map(l=>({x:l.x-r,y:l.y-a}));return{...e,bounds:c,points:d}}static insertVertexNear(e,t){const s=m.toLocalPoint(e,t),{points:i}=e;let r=i.length-1,a=1/0;for(let h=0;h<i.length;h++){const c=i[h],d=i[(h+1)%i.length],l=m.distanceToSegment(s,c,d);l<a&&(a=l,r=h)}const o=[...i.slice(0,r+1),s,...i.slice(r+1)];return{...e,points:o}}static distanceToSegment(e,t,s){const i=s.x-t.x,r=s.y-t.y,a=i*i+r*r,o=a===0?0:Math.max(0,Math.min(1,((e.x-t.x)*i+(e.y-t.y)*r)/a)),h=t.x+o*i,c=t.y+o*r;return Math.hypot(e.x-h,e.y-c)}static deleteVertex(e,t){return e.points.length<=ae?e:{...e,points:e.points.filter((s,i)=>i!==t)}}}const F=6,T=F/2,he=20,C=4;class ce{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=he*e.haloScale,this.paintBase(e),this.ctx.restore()}paintSelectionHandles(e){this.paintHandleFrame(m.handlePointsFor(e),{includeRotate:!0}),e.kind==="polygon"&&this.paintVertexHandles(e)}paintVertexHandles(e){this.ctx.fillStyle="#39f";for(const t of m.vertexPointsFor(e))this.ctx.beginPath(),this.ctx.ellipse(t.x,t.y,C,C,0,0,2*Math.PI),this.ctx.fill()}paintMemberOutline(e){this.paintOutline(m.handlePointsFor(e))}paintGroupHandles(e){this.paintHandleFrame(m.handlePointsFor({bounds:e,angle:0}),{includeRotate:!0})}paintOutline(e){this.ctx.strokeStyle="lightgray",this.ctx.beginPath(),this.ctx.moveTo(e.nw.x,e.nw.y),this.ctx.lineTo(e.ne.x,e.ne.y),this.ctx.lineTo(e.se.x,e.se.y),this.ctx.lineTo(e.sw.x,e.sw.y),this.ctx.closePath(),this.ctx.stroke()}paintHandleFrame(e,{includeRotate:t}){this.paintOutline(e),this.ctx.fillStyle="lightgray";for(const s of U){const i=e[s];this.ctx.fillRect(i.x-T,i.y-T,F,F)}t&&(this.ctx.beginPath(),this.ctx.moveTo(e.n.x,e.n.y),this.ctx.lineTo(e.rotate.x,e.rotate.y),this.ctx.stroke(),this.ctx.beginPath(),this.ctx.ellipse(e.rotate.x,e.rotate.y,T+1,T+1,0,0,2*Math.PI),this.ctx.fill())}}function de(n){return{version:1,time:n.event.time,endTime:n.event.endTime,durationSeconds:n.event.durationSeconds,place:n.event.place,witness:n.witness,caseId:n.caseId,description:n.event.description,tags:n.event.tags,timeline:n.timeline.toJSON(),witnessTrack:n.witnessTrack.toJSON(),weatherTrack:n.weatherTrack.toJSON(),weather:n.weather,decor:n.decor}}function ue(n){return new D({eventType:"sighting",time:n.time,endTime:n.endTime,durationSeconds:n.durationSeconds,place:n.place,description:n.description,tags:n.tags},M.fromJSON(n.timeline),n.witnessTrack?b.fromJSON(n.witnessTrack):new b,n.weatherTrack?w.fromJSON(n.weatherTrack):new w,n.witness,n.caseId,n.weather,n.decor??[])}function fe(n,e){for(const t of n){const s=t.toLowerCase().split("-")[0];if(e.includes(s))return s}return"en"}const pe="modulepreload",me=function(n,e){return new URL(n,e).href},R={},N=function(e,t,s){let i=Promise.resolve();if(t&&t.length>0){let a=function(d){return Promise.all(d.map(l=>Promise.resolve(l).then(u=>({status:"fulfilled",value:u}),u=>({status:"rejected",reason:u}))))};const o=document.getElementsByTagName("link"),h=document.querySelector("meta[property=csp-nonce]"),c=h?.nonce||h?.getAttribute("nonce");i=a(t.map(d=>{if(d=me(d,s),d in R)return;R[d]=!0;const l=d.endsWith(".css"),u=l?'[rel="stylesheet"]':"";if(!!s)for(let g=o.length-1;g>=0;g--){const k=o[g];if(k.href===d&&(!l||k.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${d}"]${u}`))return;const f=document.createElement("link");if(f.rel=l?"stylesheet":pe,l||(f.as="script"),f.crossOrigin="",f.href=d,c&&f.setAttribute("nonce",c),document.head.appendChild(f),l)return new Promise((g,k)=>{f.addEventListener("load",g),f.addEventListener("error",()=>k(new Error(`Unable to preload CSS for ${d}`)))})}))}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)})},ge=["en","fr"],ye={en:()=>N(()=>Promise.resolve().then(()=>ve),void 0,import.meta.url).then(n=>n.ufoMessages_en),fr:()=>N(()=>import("./UfoMessages_fr-NZrwNFCQ.js"),[],import.meta.url).then(n=>n.ufoMessages_fr)};function xe(n){return ye[n]()}const $={play:"Play",pause:"Pause",noDuration:"No observation duration",autoReplay:"Auto-replay",currentPosition:"Current position",duration:"Duration",fullscreen:"Fullscreen",exitFullscreen:"Exit fullscreen"},ve=Object.freeze(Object.defineProperty({__proto__:null,ufoMessages_en:$},Symbol.toStringTag,{value:"Module"})),be=new Set;class we extends HTMLElement{static get observedAttributes(){return["src"]}shadow;stageElement;canvas;canvasRenderer;tooltip;toolbar;playPauseButton;loopButton;fullscreenButton;seekInput;timeStartLabel;timeEndLabel;currentSighting=D.create();player;loopEnabled=!0;highlightedSourceIds=new Set;enableClickToPlay=!0;fullscreenTarget;messages=$;realDurationMs;realStartMs;handleFullscreenChange=()=>this.updateFullscreenButton();handlePointerMove=e=>{const t=this.canvasPointFromEvent(e),s=t&&this.currentSighting.timeline.hitTest(this.currentTime,t.x,t.y);if(!s?.shape.title){this.tooltip.hidden=!0;return}this.tooltip.textContent=s.shape.title,this.tooltip.hidden=!1;const i=this.stageElement.getBoundingClientRect();this.tooltip.style.left=`${e.clientX-i.left+12}px`,this.tooltip.style.top=`${e.clientY-i.top+12}px`};handlePointerLeave=()=>{this.tooltip.hidden=!0};constructor(){super(),this.shadow=this.attachShadow({mode:"open"});const e=document.createElement("template");e.innerHTML=`<style>${K}</style>${J}`,this.shadow.appendChild(e.content.cloneNode(!0)),this.stageElement=this.shadow.getElementById("stage"),this.canvas=this.shadow.getElementById("canvas"),this.canvasRenderer=new ce(this.canvas.getContext("2d")),this.tooltip=this.shadow.getElementById("tooltip"),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()}),this.canvas.addEventListener("pointermove",this.handlePointerMove),this.canvas.addEventListener("pointerleave",this.handlePointerLeave),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 de(this.currentSighting)}set sightingData(e){this.player.stop(),this.currentSighting=ue(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}set currentTime(e){this.player.seek(e)}get seekableDuration(){return this.player.seekableDuration}get autoReplayEnabled(){return this.loopEnabled}get positionLabel(){return this.timeStartLabel.textContent??""}get durationLabel(){return this.timeEndLabel.textContent??""}set showToolbar(e){this.toolbar.classList.toggle("hidden",!e)}get playbackState(){return this.player.playbackState}get selectedSourceIds(){return this.highlightedSourceIds}get durationSeconds(){return this.currentSighting.event.durationSeconds}set durationSeconds(e){this.currentSighting.event.durationSeconds=e,this.updateTimeLabels(),this.refresh(),this.updatePlayPauseButton()}set selectedSourceIds(e){const t=new Set(e);t.size===this.highlightedSourceIds.size&&[...t].every(i=>this.highlightedSourceIds.has(i))||(this.highlightedSourceIds=t,this.refresh())}refresh(){this.seekInput.max=String(this.player.seekableDuration),this.player.seek(this.player.time)}canvasPointFromEvent(e){const t=this.canvas.getBoundingClientRect();if(!(t.width===0||t.height===0))return{x:(e.clientX-t.left)/t.width*this.canvas.width,y:(e.clientY-t.top)/t.height*this.canvas.height}}onFrame(e,t){this.canvasRenderer.clear(this.canvas.width,this.canvas.height);const s=this.playbackState!=="playing"?this.highlightedSourceIds:be;for(const[i,r]of t){const a=s.has(i);a&&s.size===1?this.canvasRenderer.paintShape({...r,selected:!0}):(this.canvasRenderer.paintShape(r),a&&this.canvasRenderer.paintMemberOutline(r))}if(s.size>1){const i=m.groupBoundsFor([...t].filter(([r])=>s.has(r)).map(([,r])=>r.bounds));this.canvasRenderer.paintGroupHandles(i)}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 ie(this.currentSighting.timeline,(t,s)=>this.onFrame(t,s));return e.loop=this.loopEnabled,e}togglePlayPause(){this.player.seekableDuration<=0||(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?"⏸":"▶";const t=this.player.seekableDuration>0;this.playPauseButton.disabled=!t;const s=t?e?this.messages.pause:this.messages.play:this.messages.noDuration;this.playPauseButton.title=s,this.playPauseButton.setAttribute("aria-label",s),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=fe(navigator.languages,ge);e!=="en"&&this.applyMessages(await xe(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=ne(e);this.realDurationMs=t!==void 0&&t>0?t:void 0,this.realStartMs=e.time?L(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 P(e);const t=this.currentSighting.timeline.duration,s=t>0&&e<=t?e/t*this.realDurationMs:e;return this.realStartMs!==void 0?H(_(this.realStartMs+s)):P(s)}formatEndOfTimeline(){return this.realDurationMs===void 0?P(this.currentSighting.timeline.duration):this.realStartMs!==void 0?H(_(this.realStartMs+this.realDurationMs)):P(this.realDurationMs)}}function _(n){const e=new Date(n);return{hour:e.getUTCHours(),minute:e.getUTCMinutes(),second:e.getUTCSeconds()}}function H(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 P(n){const e=Math.round(n/1e3),t=Math.floor(e/60),s=e%60;return`${t}:${String(s).padStart(2,"0")}`}const z="rr0-ufo";function ke(){customElements.get(z)||customElements.define(z,we)}ke();
212
+ `;function V(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 m(n,e,t){return n+(e-n)*t}function F(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 W(n){return`#${n.map(e=>Math.round(e).toString(16).padStart(2,"0")).join("")}`}function Y(n,e,t){const s=F(n),i=F(e);return!s||!i?t<1?n:e:W([m(s[0],i[0],t),m(s[1],i[1],t),m(s[2],i[2],t)])}function q(n,e,t){const s={x:m(n.bounds.x,e.bounds.x,t),y:m(n.bounds.y,e.bounds.y,t),width:m(n.bounds.width,e.bounds.width,t),height:m(n.bounds.height,e.bounds.height,t)},i=m(n.angle,e.angle,t),r=m(n.transparency,e.transparency,t),a=m(n.haloScale,e.haloScale,t),o=Y(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((h,c)=>({x:m(h.x,e.points[c].x,t),y:m(h.y,e.points[c].y,t)}))}:{...t<1?n:e,bounds:s,angle:i,transparency:r,haloScale:a,color:o}}class M{keyframes=[];order=[];groups=[];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]});for(const i of t)this.order.includes(i.sourceId)||this.order.push(i.sourceId)}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?q(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,i){const r=this.sourceIds;for(let a=r.length-1;a>=0;a--){if(i?.has(r[a]))continue;const o=this.getInterpolatedShapeAt(e,r[a]);if(o&&V(o,t,s))return{sourceId:r[a],shape:o}}}removeSource(e){for(let t=this.keyframes.length-1;t>=0;t--){const s=this.keyframes[t].shapes.filter(i=>i.sourceId!==e);s.length===0?this.keyframes.splice(t,1):s.length!==this.keyframes[t].shapes.length&&(this.keyframes[t]={t:this.keyframes[t].t,shapes:s})}this.order=this.order.filter(t=>t!==e),this.removeFromGroup(e)}group(e){if(!(e.length<2)){for(const t of e)this.removeFromGroup(t);this.groups.push([...e])}}ungroup(e){const t=this.groups.findIndex(s=>s.includes(e));t!==-1&&this.groups.splice(t,1)}groupMembers(e){return this.groups.find(t=>t.includes(e))}removeFromGroup(e){const t=this.groups.findIndex(i=>i.includes(e));if(t===-1)return;const s=this.groups[t].filter(i=>i!==e);s.length<2?this.groups.splice(t,1):this.groups[t]=s}bringToFront(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.push(e))}sendToBack(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.unshift(e))}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get sourceIds(){return[...this.order]}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes,order:this.order,groups:this.groups}}static fromJSON(e){const t=new M;for(const s of e.keyframes)t.addKeyframe(s.t,s.shapes);if(e.order){const s=t.order.filter(i=>!e.order.includes(i));t.order=[...e.order,...s]}return e.groups&&(t.groups=e.groups.map(s=>s.filter(i=>t.order.includes(i))).filter(s=>s.length>=2)),t}}function X(n,e,t){return Math.max(e,Math.min(t,n))}function Z(n,e,t){const s=((e-n)%360+540)%360-180;return((n+s*t)%360+360)%360}function v(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:v(n.lat,e.lat,t),lng:n.lng===void 0||e.lng===void 0?void 0:v(n.lng,e.lng,t),elevationM:v(n.elevationM,e.elevationM,t),headingDeg:n.headingDeg===void 0||e.headingDeg===void 0?void 0:Z(n.headingDeg,e.headingDeg,t),pitchDeg:v(n.pitchDeg,e.pitchDeg,t),fovDeg:v(n.fovDeg,e.fovDeg,t)}}class b{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}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}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,X((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 b;for(const s of e.keyframes)t.addKeyframe(s.t,s.pose);return t}}function S(n,e,t){return n+(e-n)*t}function j(n,e,t){const s=((e-n)%360+540)%360-180;return((n+s*t)%360+360)%360}function ee(n,e,t){return Math.max(e,Math.min(t,n))}function te(n,e,t){return{cloudCover:S(n.cloudCover,e.cloudCover,t),cloudDarkness:S(n.cloudDarkness,e.cloudDarkness,t),precipitationType:t<1?n.precipitationType:e.precipitationType,precipitationIntensity:S(n.precipitationIntensity,e.precipitationIntensity,t),windDirectionDeg:j(n.windDirectionDeg,e.windDirectionDeg,t),windSpeed:S(n.windSpeed,e.windSpeed,t),storm:t<1?n.storm:e.storm}}class w{keyframes=[];addKeyframe(e,t){const s=this.findInsertIndex(e);this.keyframes[s]?.t===e?this.keyframes[s]={t:e,weather:t}:this.keyframes.splice(s,0,{t:e,weather:t})}clear(){this.keyframes.length=0}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}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}getLatestWeatherAt(e){let t=this.findInsertIndex(e);return this.keyframes[t]?.t!==e&&(t-=1),t>=0?this.keyframes[t].weather:void 0}getInterpolatedWeatherAt(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.weather;const i=this.keyframes[t]?.t===e?void 0:this.keyframes[t];return s?i?te(s.weather,i.weather,ee((e-s.t)/(i.t-s.t),0,1)):s.weather:i?.weather}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 w;for(const s of e.keyframes)t.addKeyframe(s.t,s.weather);return t}}function A(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 B(n){return(((n.day??0)*24+(n.hour??0))*60+(n.minute??0))*6e4+(n.second??0)*1e3}const se=["year","month","day","hour","minute"],ne=["day","hour","minute"];function C(n,e){return e.filter(t=>n[t]!==void 0).join(",")}function ie(n){if(n.durationSeconds!==void 0)return n.durationSeconds*1e3;if(!n.time||!n.endTime)return;const e=n.time.year!==void 0&&n.endTime.year!==void 0,t=e?se:ne,s=C(n.time,t);if(!(s===""||s!==C(n.endTime,t)))return e?A(n.endTime)-A(n.time):B(n.endTime)-B(n.time)}class D{constructor(e,t,s,i,r,a,o,h=[]){this.event=e,this.timeline=t,this.witnessTrack=s,this.weatherTrack=i,this.witness=r,this.caseId=a,this.weather=o,this.decor=h}static create(e,t,s){return new D({eventType:"sighting",time:e,place:t},new M,new b,new w,s)}}class re{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 $=["nw","n","ne","e","se","s","sw","w"],ae=24,I=8,oe=3,le=8;function E(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}}const he={nw:{left:!0,top:!0},n:{top:!0},ne:{right:!0,top:!0},e:{right:!0},se:{right:!0,bottom:!0},s:{bottom:!0},sw:{left:!0,bottom:!0},w:{left:!0}};class u{static handlePointsFor(e){const{x:t,y:s,width:i,height:r}=e.bounds,a={nw:{x:t,y:s},n:{x:t+i/2,y:s},ne:{x:t+i,y:s},e:{x:t+i,y:s+r/2},se:{x:t+i,y:s+r},s:{x:t+i/2,y:s+r},sw:{x:t,y:s+r},w:{x:t,y:s+r/2},rotate:{x:t+i/2,y:s-ae}},o=x(e.bounds),h={};for(const c of Object.keys(a))h[c]=E(a[c],o,e.angle);return h}static hitTestHandle(e,t,s=8,i=[...$,"rotate"]){const r=u.handlePointsFor(e);for(const a of i)if(Math.hypot(t.x-r[a].x,t.y-r[a].y)<=s)return a}static HANDLE_DIRECTION_DEG={e:0,se:45,s:90,sw:135,w:180,nw:225,n:270,ne:315};static resizeAxisFor(e,t){const i=((u.HANDLE_DIRECTION_DEG[e]+t*180/Math.PI)%180+180)%180/45,r=["ew","nwse","ns","nesw"];return r[Math.round(i)%r.length]}static resizeBounds(e,t,s,i){const r=x(e),a=E(i,r,-t),{x:o,y:h,width:c,height:d}=e,l=he[s];let f=o,y=h,p=o+c,g=h+d;return l.left&&(f=Math.min(a.x,p-I)),l.right&&(p=Math.max(a.x,f+I)),l.top&&(y=Math.min(a.y,g-I)),l.bottom&&(g=Math.max(a.y,y+I)),{x:f,y,width:p-f,height:g-y}}static resizeShape(e,t,s){if(t==="rotate")throw new Error("resizeShape does not accept the rotate handle");const{width:i,height:r}=e.bounds,a=u.resizeBounds(e.bounds,e.angle,t,s);if(e.kind==="oval")return{...e,bounds:a};const o=i===0?1:a.width/i,h=r===0?1:a.height/r;return{...e,bounds:a,points:e.points.map(c=>({x:c.x*o,y:c.y*h}))}}static rotateShape(e,t){const s=x(e.bounds),i=Math.atan2(t.y-s.y,t.x-s.x)+Math.PI/2;return{...e,angle:i}}static groupBoundsFor(e){const t=Math.min(...e.map(a=>a.x)),s=Math.min(...e.map(a=>a.y)),i=Math.max(...e.map(a=>a.x+a.width)),r=Math.max(...e.map(a=>a.y+a.height));return{x:t,y:s,width:i-t,height:r-s}}static vertexPointsFor(e){const t=x(e.bounds);return e.points.map(s=>E({x:e.bounds.x+s.x,y:e.bounds.y+s.y},t,e.angle))}static hitTestVertex(e,t,s=le){const i=u.vertexPointsFor(e);let r,a=s;return i.forEach((o,h)=>{const c=Math.hypot(t.x-o.x,t.y-o.y);c<=a&&(a=c,r=h)}),r}static toLocalPoint(e,t){const s=x(e.bounds),i=E(t,s,-e.angle);return{x:i.x-e.bounds.x,y:i.y-e.bounds.y}}static moveVertex(e,t,s){const i=e.points.map((l,f)=>f===t?u.toLocalPoint(e,s):l),r=Math.min(...i.map(l=>l.x)),a=Math.min(...i.map(l=>l.y)),o=Math.max(...i.map(l=>l.x)),h=Math.max(...i.map(l=>l.y)),c={x:e.bounds.x+r,y:e.bounds.y+a,width:o-r,height:h-a},d=i.map(l=>({x:l.x-r,y:l.y-a}));return{...e,bounds:c,points:d}}static insertVertexNear(e,t){const s=u.toLocalPoint(e,t),{points:i}=e;let r=i.length-1,a=1/0;for(let h=0;h<i.length;h++){const c=i[h],d=i[(h+1)%i.length],l=u.distanceToSegment(s,c,d);l<a&&(a=l,r=h)}const o=[...i.slice(0,r+1),s,...i.slice(r+1)];return{...e,points:o}}static distanceToSegment(e,t,s){const i=s.x-t.x,r=s.y-t.y,a=i*i+r*r,o=a===0?0:Math.max(0,Math.min(1,((e.x-t.x)*i+(e.y-t.y)*r)/a)),h=t.x+o*i,c=t.y+o*r;return Math.hypot(e.x-h,e.y-c)}static deleteVertex(e,t){return e.points.length<=oe?e:{...e,points:e.points.filter((s,i)=>i!==t)}}}const L=6,T=L/2,ce=20,O=4;class de{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=ce*e.haloScale,this.paintBase(e),this.ctx.restore()}paintSelectionHandles(e){this.paintHandleFrame(u.handlePointsFor(e),{includeRotate:!0}),e.kind==="polygon"&&this.paintVertexHandles(e)}paintVertexHandles(e){this.ctx.fillStyle="#39f";for(const t of u.vertexPointsFor(e))this.ctx.beginPath(),this.ctx.ellipse(t.x,t.y,O,O,0,0,2*Math.PI),this.ctx.fill()}paintMemberOutline(e){this.paintOutline(u.handlePointsFor(e))}paintGroupHandles(e){this.paintHandleFrame(u.handlePointsFor({bounds:e,angle:0}),{includeRotate:!0})}paintOutline(e){this.ctx.strokeStyle="lightgray",this.ctx.beginPath(),this.ctx.moveTo(e.nw.x,e.nw.y),this.ctx.lineTo(e.ne.x,e.ne.y),this.ctx.lineTo(e.se.x,e.se.y),this.ctx.lineTo(e.sw.x,e.sw.y),this.ctx.closePath(),this.ctx.stroke()}paintHandleFrame(e,{includeRotate:t}){this.paintOutline(e),this.ctx.fillStyle="lightgray";for(const s of $){const i=e[s];this.ctx.fillRect(i.x-T,i.y-T,L,L)}t&&(this.ctx.beginPath(),this.ctx.moveTo(e.n.x,e.n.y),this.ctx.lineTo(e.rotate.x,e.rotate.y),this.ctx.stroke(),this.ctx.beginPath(),this.ctx.ellipse(e.rotate.x,e.rotate.y,T+1,T+1,0,0,2*Math.PI),this.ctx.fill())}}function ue(n){return{version:1,time:n.event.time,endTime:n.event.endTime,durationSeconds:n.event.durationSeconds,place:n.event.place,witness:n.witness,caseId:n.caseId,description:n.event.description,tags:n.event.tags,timeline:n.timeline.toJSON(),witnessTrack:n.witnessTrack.toJSON(),weatherTrack:n.weatherTrack.toJSON(),weather:n.weather,decor:n.decor}}function fe(n){return new D({eventType:"sighting",time:n.time,endTime:n.endTime,durationSeconds:n.durationSeconds,place:n.place,description:n.description,tags:n.tags},M.fromJSON(n.timeline),n.witnessTrack?b.fromJSON(n.witnessTrack):new b,n.weatherTrack?w.fromJSON(n.weatherTrack):new w,n.witness,n.caseId,n.weather,n.decor??[])}function pe(n,e){for(const t of n){const s=t.toLowerCase().split("-")[0];if(e.includes(s))return s}return"en"}const me="modulepreload",ge=function(n,e){return new URL(n,e).href},R={},N=function(e,t,s){let i=Promise.resolve();if(t&&t.length>0){let a=function(d){return Promise.all(d.map(l=>Promise.resolve(l).then(f=>({status:"fulfilled",value:f}),f=>({status:"rejected",reason:f}))))};const o=document.getElementsByTagName("link"),h=document.querySelector("meta[property=csp-nonce]"),c=h?.nonce||h?.getAttribute("nonce");i=a(t.map(d=>{if(d=ge(d,s),d in R)return;R[d]=!0;const l=d.endsWith(".css"),f=l?'[rel="stylesheet"]':"";if(!!s)for(let g=o.length-1;g>=0;g--){const k=o[g];if(k.href===d&&(!l||k.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${d}"]${f}`))return;const p=document.createElement("link");if(p.rel=l?"stylesheet":me,l||(p.as="script"),p.crossOrigin="",p.href=d,c&&p.setAttribute("nonce",c),document.head.appendChild(p),l)return new Promise((g,k)=>{p.addEventListener("load",g),p.addEventListener("error",()=>k(new Error(`Unable to preload CSS for ${d}`)))})}))}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)})},ye=["en","fr"],ve={en:()=>N(()=>Promise.resolve().then(()=>be),void 0,import.meta.url).then(n=>n.ufoMessages_en),fr:()=>N(()=>import("./UfoMessages_fr-NZrwNFCQ.js"),[],import.meta.url).then(n=>n.ufoMessages_fr)};function xe(n){return ve[n]()}const J={play:"Play",pause:"Pause",noDuration:"No observation duration",autoReplay:"Auto-replay",currentPosition:"Current position",duration:"Duration",fullscreen:"Fullscreen",exitFullscreen:"Exit fullscreen"},be=Object.freeze(Object.defineProperty({__proto__:null,ufoMessages_en:J},Symbol.toStringTag,{value:"Module"})),_=new Set;class we extends HTMLElement{static get observedAttributes(){return["src"]}shadow;stageElement;canvas;canvasRenderer;tooltip;toolbar;playPauseButton;loopButton;fullscreenButton;seekInput;timeStartLabel;timeEndLabel;currentSighting=D.create();player;loopEnabled=!0;highlightedSourceIds=new Set;occludedSourceIds=_;enableClickToPlay=!0;fullscreenTarget;messages=J;realDurationMs;realStartMs;handleFullscreenChange=()=>this.updateFullscreenButton();handlePointerMove=e=>{const t=this.canvasPointFromEvent(e),s=t&&this.currentSighting.timeline.hitTest(this.currentTime,t.x,t.y,this.occludedSourceIds);if(!s?.shape.title){this.tooltip.hidden=!0;return}this.tooltip.textContent=s.shape.title,this.tooltip.hidden=!1;const i=this.stageElement.getBoundingClientRect();this.tooltip.style.left=`${e.clientX-i.left+12}px`,this.tooltip.style.top=`${e.clientY-i.top+12}px`};handlePointerLeave=()=>{this.tooltip.hidden=!0};constructor(){super(),this.shadow=this.attachShadow({mode:"open"});const e=document.createElement("template");e.innerHTML=`<style>${G}</style>${K}`,this.shadow.appendChild(e.content.cloneNode(!0)),this.stageElement=this.shadow.getElementById("stage"),this.canvas=this.shadow.getElementById("canvas"),this.canvasRenderer=new de(this.canvas.getContext("2d")),this.tooltip=this.shadow.getElementById("tooltip"),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()}),this.canvas.addEventListener("pointermove",this.handlePointerMove),this.canvas.addEventListener("pointerleave",this.handlePointerLeave),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 ue(this.currentSighting)}set sightingData(e){this.player.stop(),this.currentSighting=fe(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}set currentTime(e){this.player.seek(e)}get seekableDuration(){return this.player.seekableDuration}get autoReplayEnabled(){return this.loopEnabled}get positionLabel(){return this.timeStartLabel.textContent??""}get durationLabel(){return this.timeEndLabel.textContent??""}set showToolbar(e){this.toolbar.classList.toggle("hidden",!e)}get playbackState(){return this.player.playbackState}get selectedSourceIds(){return this.highlightedSourceIds}get durationSeconds(){return this.currentSighting.event.durationSeconds}set durationSeconds(e){this.currentSighting.event.durationSeconds=e,this.updateTimeLabels(),this.refresh(),this.updatePlayPauseButton()}set selectedSourceIds(e){const t=new Set(e);t.size===this.highlightedSourceIds.size&&[...t].every(i=>this.highlightedSourceIds.has(i))||(this.highlightedSourceIds=t,this.refresh())}setOccludedSourceIds(e){e.size===this.occludedSourceIds.size&&[...e].every(s=>this.occludedSourceIds.has(s))||(this.occludedSourceIds=e,this.refresh())}hasVisibleShapeAt(e,t){return this.currentSighting.timeline.hitTest(this.currentTime,e,t,this.occludedSourceIds)!==void 0}refresh(){this.seekInput.max=String(this.player.seekableDuration),this.player.seek(this.player.time)}canvasPointFromEvent(e){const t=this.canvas.getBoundingClientRect();if(!(t.width===0||t.height===0))return{x:(e.clientX-t.left)/t.width*this.canvas.width,y:(e.clientY-t.top)/t.height*this.canvas.height}}onFrame(e,t){this.canvasRenderer.clear(this.canvas.width,this.canvas.height);const s=this.playbackState!=="playing"?this.highlightedSourceIds:_;for(const[i,r]of t){if(this.occludedSourceIds.has(i))continue;const a=s.has(i);a&&s.size===1?this.canvasRenderer.paintShape({...r,selected:!0}):(this.canvasRenderer.paintShape(r),a&&this.canvasRenderer.paintMemberOutline(r))}if(s.size>1){const i=u.groupBoundsFor([...t].filter(([r])=>s.has(r)).map(([,r])=>r.bounds));this.canvasRenderer.paintGroupHandles(i)}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 re(this.currentSighting.timeline,(t,s)=>this.onFrame(t,s));return e.loop=this.loopEnabled,e}togglePlayPause(){this.player.seekableDuration<=0||(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?"⏸":"▶";const t=this.player.seekableDuration>0;this.playPauseButton.disabled=!t;const s=t?e?this.messages.pause:this.messages.play:this.messages.noDuration;this.playPauseButton.title=s,this.playPauseButton.setAttribute("aria-label",s),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=pe(navigator.languages,ye);e!=="en"&&this.applyMessages(await xe(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=ie(e);this.realDurationMs=t!==void 0&&t>0?t:void 0,this.realStartMs=e.time?A(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 P(e);const t=this.currentSighting.timeline.duration,s=t>0&&e<=t?e/t*this.realDurationMs:e;return this.realStartMs!==void 0?H(z(this.realStartMs+s)):P(s)}formatEndOfTimeline(){return this.realDurationMs===void 0?P(this.currentSighting.timeline.duration):this.realStartMs!==void 0?H(z(this.realStartMs+this.realDurationMs)):P(this.realDurationMs)}}function z(n){const e=new Date(n);return{hour:e.getUTCHours(),minute:e.getUTCMinutes(),second:e.getUTCSeconds()}}function H(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 P(n){const e=Math.round(n/1e3),t=Math.floor(e/60),s=e%60;return`${t}:${String(s).padStart(2,"0")}`}const U="rr0-ufo";function ke(){customElements.get(U)||customElements.define(U,we)}ke();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rr0/ufoathome",
3
3
  "type": "module",
4
- "version": "0.10.4",
4
+ "version": "0.10.6",
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",