@jjlmoya/utils-chrono 1.9.0 → 1.11.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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -0
  3. package/src/entries.ts +7 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tool/gear-train-explorer/bibliography.astro +16 -0
  7. package/src/tool/gear-train-explorer/bibliography.ts +12 -0
  8. package/src/tool/gear-train-explorer/client.ts +146 -0
  9. package/src/tool/gear-train-explorer/component.astro +17 -0
  10. package/src/tool/gear-train-explorer/components/GearPanel.astro +102 -0
  11. package/src/tool/gear-train-explorer/entry.ts +53 -0
  12. package/src/tool/gear-train-explorer/gear-train-explorer.css +172 -0
  13. package/src/tool/gear-train-explorer/gears.ts +148 -0
  14. package/src/tool/gear-train-explorer/helpers.ts +49 -0
  15. package/src/tool/gear-train-explorer/i18n/de.ts +99 -0
  16. package/src/tool/gear-train-explorer/i18n/en.ts +98 -0
  17. package/src/tool/gear-train-explorer/i18n/es.ts +99 -0
  18. package/src/tool/gear-train-explorer/i18n/fr.ts +99 -0
  19. package/src/tool/gear-train-explorer/i18n/id.ts +98 -0
  20. package/src/tool/gear-train-explorer/i18n/it.ts +99 -0
  21. package/src/tool/gear-train-explorer/i18n/ja.ts +98 -0
  22. package/src/tool/gear-train-explorer/i18n/ko.ts +98 -0
  23. package/src/tool/gear-train-explorer/i18n/nl.ts +99 -0
  24. package/src/tool/gear-train-explorer/i18n/pl.ts +99 -0
  25. package/src/tool/gear-train-explorer/i18n/pt.ts +99 -0
  26. package/src/tool/gear-train-explorer/i18n/ru.ts +99 -0
  27. package/src/tool/gear-train-explorer/i18n/sv.ts +99 -0
  28. package/src/tool/gear-train-explorer/i18n/tr.ts +98 -0
  29. package/src/tool/gear-train-explorer/i18n/zh.ts +98 -0
  30. package/src/tool/gear-train-explorer/index.ts +11 -0
  31. package/src/tool/gear-train-explorer/movements.ts +61 -0
  32. package/src/tool/gear-train-explorer/scene.ts +120 -0
  33. package/src/tool/gear-train-explorer/seo.astro +16 -0
  34. package/src/tool/gear-train-explorer/state.ts +30 -0
  35. package/src/tool/sidereal-time-tracker/bibliography.astro +16 -0
  36. package/src/tool/sidereal-time-tracker/bibliography.ts +16 -0
  37. package/src/tool/sidereal-time-tracker/client.ts +278 -0
  38. package/src/tool/sidereal-time-tracker/component.astro +15 -0
  39. package/src/tool/sidereal-time-tracker/components/SiderealPanel.astro +197 -0
  40. package/src/tool/sidereal-time-tracker/entry.ts +51 -0
  41. package/src/tool/sidereal-time-tracker/helpers.ts +80 -0
  42. package/src/tool/sidereal-time-tracker/i18n/de.ts +93 -0
  43. package/src/tool/sidereal-time-tracker/i18n/en.ts +93 -0
  44. package/src/tool/sidereal-time-tracker/i18n/es.ts +93 -0
  45. package/src/tool/sidereal-time-tracker/i18n/fr.ts +93 -0
  46. package/src/tool/sidereal-time-tracker/i18n/id.ts +93 -0
  47. package/src/tool/sidereal-time-tracker/i18n/it.ts +93 -0
  48. package/src/tool/sidereal-time-tracker/i18n/ja.ts +93 -0
  49. package/src/tool/sidereal-time-tracker/i18n/ko.ts +93 -0
  50. package/src/tool/sidereal-time-tracker/i18n/nl.ts +93 -0
  51. package/src/tool/sidereal-time-tracker/i18n/pl.ts +93 -0
  52. package/src/tool/sidereal-time-tracker/i18n/pt.ts +93 -0
  53. package/src/tool/sidereal-time-tracker/i18n/ru.ts +93 -0
  54. package/src/tool/sidereal-time-tracker/i18n/sv.ts +93 -0
  55. package/src/tool/sidereal-time-tracker/i18n/tr.ts +93 -0
  56. package/src/tool/sidereal-time-tracker/i18n/zh.ts +93 -0
  57. package/src/tool/sidereal-time-tracker/index.ts +11 -0
  58. package/src/tool/sidereal-time-tracker/seo.astro +16 -0
  59. package/src/tool/sidereal-time-tracker/sidereal-time-tracker.css +257 -0
  60. package/src/tools.ts +4 -0
@@ -0,0 +1,120 @@
1
+ import type { MovementDef } from './movements';
2
+ import { getCtx, getMov, getHovered, getFontFam, setMov, setHovered, detectTheme, c, W, H } from './state';
3
+ import { drawGear, drawPallet, drawBalance, drawConnection } from './gears';
4
+
5
+ export function drawBackground() {
6
+ const ctx = getCtx();
7
+ const bg0 = c('#1e1e3a', '#f5f0e8');
8
+ const bg1 = c('#16162e', '#eae4d8');
9
+ const bg2 = c('#0c0c18', '#ddd6c8');
10
+ const grad = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, 400);
11
+ grad.addColorStop(0, bg0);
12
+ grad.addColorStop(0.5, bg1);
13
+ grad.addColorStop(1, bg2);
14
+ ctx.fillStyle = grad;
15
+ ctx.fillRect(0, 0, W, H);
16
+ }
17
+
18
+ export function drawTitleBar(label: string) {
19
+ const ctx = getCtx();
20
+ ctx.fillStyle = c('rgba(212,175,55,0.7)', 'rgba(120,80,10,0.85)');
21
+ ctx.font = '600 14px ' + getFontFam();
22
+ ctx.textAlign = 'left';
23
+ ctx.fillText(label + ' — Gear Train', 16, 24);
24
+ ctx.fillStyle = c('rgba(160,160,184,0.55)', 'rgba(60,50,30,0.7)');
25
+ ctx.font = '11px ' + getFontFam();
26
+ ctx.fillText('Mainspring to Balance Wheel', 16, 40);
27
+ }
28
+
29
+ export function drawLabels() {
30
+ const ctx = getCtx();
31
+ const mov = getMov();
32
+ const hover = getHovered();
33
+ ctx.font = '10px ' + getFontFam();
34
+ ctx.textAlign = 'center';
35
+ for (let i = 0; i < mov.gears.length; i++) {
36
+ const g = mov.gears[i];
37
+ ctx.fillStyle = hover === i ? c('#ffd700', '#7a5a00') : c('rgba(200,200,220,0.8)', 'rgba(40,30,15,0.85)');
38
+ ctx.fillText(g.label, g.x, g.y + g.r + 14);
39
+ const rpmTxt = g.rpm < 1 ? (g.rpm * 60).toFixed(1) + '/h' : g.rpm.toFixed(1) + ' rpm';
40
+ ctx.fillStyle = hover === i ? c('rgba(255,215,0,0.6)', 'rgba(100,70,10,0.7)') : c('rgba(160,160,184,0.5)', 'rgba(60,50,30,0.65)');
41
+ ctx.font = '9px ' + getFontFam();
42
+ ctx.fillText(rpmTxt, g.x, g.y + g.r + 24);
43
+ ctx.font = '10px ' + getFontFam();
44
+ }
45
+ }
46
+
47
+ export function drawExtraLabels() {
48
+ const ctx = getCtx();
49
+ const mov = getMov();
50
+ const p = mov.pallet;
51
+ const b = mov.balance;
52
+ ctx.font = '10px ' + getFontFam();
53
+ ctx.textAlign = 'center';
54
+ ctx.fillStyle = c('rgba(200,200,220,0.8)', 'rgba(40,30,15,0.85)');
55
+ ctx.fillText('Pallet Fork', p.x, p.y + 22);
56
+ ctx.fillStyle = c('rgba(160,160,184,0.5)', 'rgba(60,50,30,0.65)');
57
+ ctx.font = '9px ' + getFontFam();
58
+ ctx.fillText(p.bph + ' bph', p.x, p.y + 32);
59
+ ctx.font = '10px ' + getFontFam();
60
+ ctx.fillStyle = c('rgba(200,200,220,0.8)', 'rgba(40,30,15,0.85)');
61
+ ctx.fillText('Balance Wheel', b.x, b.y + b.r + 16);
62
+ ctx.fillStyle = c('rgba(160,160,184,0.5)', 'rgba(60,50,30,0.65)');
63
+ ctx.font = '9px ' + getFontFam();
64
+ ctx.fillText(b.hz + ' Hz / ' + b.vph + ' vph', b.x, b.y + b.r + 26);
65
+ }
66
+
67
+ export function drawPowerFlowLine() {
68
+ const ctx = getCtx();
69
+ const mov = getMov();
70
+ const gears = mov.gears;
71
+ ctx.beginPath();
72
+ ctx.moveTo(gears[0].x + gears[0].r + 5, gears[0].y);
73
+ for (let i = 1; i < gears.length; i++) {
74
+ ctx.lineTo(gears[i].x - gears[i].r - 5, gears[i].y);
75
+ }
76
+ const eg = gears[gears.length - 1];
77
+ ctx.lineTo(eg.x + eg.r + 15, eg.y);
78
+ ctx.lineTo(mov.pallet.x - 15, mov.pallet.y);
79
+ ctx.lineTo(mov.pallet.x, mov.pallet.y);
80
+ ctx.strokeStyle = c('rgba(212,175,55,0.08)', 'rgba(139,105,20,0.12)');
81
+ ctx.lineWidth = 60; ctx.stroke();
82
+ ctx.strokeStyle = c('rgba(212,175,55,0.12)', 'rgba(139,105,20,0.15)');
83
+ ctx.lineWidth = 2;
84
+ ctx.setLineDash([4, 6]); ctx.stroke();
85
+ ctx.setLineDash([]);
86
+ }
87
+
88
+ function drawConnections(mov: MovementDef, highlight: number | null) {
89
+ const gears = mov.gears;
90
+ for (let i = 0; i < gears.length; i++) {
91
+ if (i < gears.length - 1) {
92
+ const g1 = gears[i], g2 = gears[i + 1];
93
+ drawConnection({ x1: g1.x + g1.r * 0.7, y1: g1.y, x2: g2.x - g2.r * 0.7, y2: g2.y, active: highlight === i || highlight === i + 1 });
94
+ }
95
+ }
96
+ const lg = gears[gears.length - 1];
97
+ drawConnection({ x1: lg.x + lg.r * 0.7, y1: lg.y, x2: mov.pallet.x - 10, y2: mov.pallet.y, active: highlight === gears.length - 1 });
98
+ drawConnection({ x1: mov.pallet.x, y1: mov.pallet.y, x2: mov.balance.x, y2: mov.balance.y, active: highlight === gears.length - 1 });
99
+ }
100
+
101
+ export function drawScene(mov: MovementDef, opts: { angles: number[]; palletPhase: number; balancePhase: number; highlight: number | null; hover: number | null }) {
102
+ const ctx = getCtx();
103
+ setMov(mov);
104
+ setHovered(opts.hover);
105
+ detectTheme();
106
+ ctx.save();
107
+ ctx.clearRect(0, 0, W, H);
108
+ drawBackground();
109
+ drawTitleBar(mov.label);
110
+ drawConnections(mov, opts.highlight);
111
+ for (let i = 0; i < mov.gears.length; i++) {
112
+ const g = mov.gears[i];
113
+ drawGear({ x: g.x, y: g.y, r: g.r, teeth: g.teeth, angle: opts.angles[i], color: g.color, highlight: opts.hover === i });
114
+ }
115
+ drawPallet(mov.pallet.x, mov.pallet.y, opts.palletPhase);
116
+ drawBalance(mov.balance.x, mov.balance.y, mov.balance.r, opts.balancePhase);
117
+ drawLabels();
118
+ drawExtraLabels();
119
+ ctx.restore();
120
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { gearTrainExplorer } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = gearTrainExplorer.i18n[locale] || gearTrainExplorer.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,30 @@
1
+ import type { MovementDef } from './movements';
2
+
3
+ let _ctx: CanvasRenderingContext2D;
4
+ let _mov: MovementDef;
5
+ let _hovered: number | null = null;
6
+ let _isDark = true;
7
+ let _fontFam = 'system-ui, sans-serif';
8
+
9
+ export function getCtx() { return _ctx; }
10
+ export function getMov() { return _mov; }
11
+ export function getHovered() { return _hovered; }
12
+ export function isDark() { return _isDark; }
13
+ export function getFontFam() { return _fontFam; }
14
+ export function setMov(m: MovementDef) { _mov = m; }
15
+ export function setHovered(h: number | null) { _hovered = h; }
16
+
17
+ export function setCtx(ctx: CanvasRenderingContext2D) {
18
+ _ctx = ctx;
19
+ _fontFam = getComputedStyle(document.body).fontFamily || _fontFam;
20
+ }
21
+
22
+ export function detectTheme() {
23
+ const bg = window.getComputedStyle(document.body).backgroundColor;
24
+ _isDark = !bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent' ? true : parseInt(bg.replace(/[^\d,]/g, '').split(',')[0]) < 128;
25
+ }
26
+
27
+ export function c(h: string, l: string): string { return _isDark ? h : l; }
28
+
29
+ export const W = 900;
30
+ export const H = 520;
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { siderealTimeTracker } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props as Props;
11
+ const loader = siderealTimeTracker.i18n[locale] || siderealTimeTracker.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Sidereal Time - Wikipedia',
6
+ url: 'https://en.wikipedia.org/wiki/Sidereal_time',
7
+ },
8
+ {
9
+ name: 'Greenwich Mean Sidereal Time - US Naval Observatory',
10
+ url: 'https://aa.usno.navy.mil/data/siderealtime',
11
+ },
12
+ {
13
+ name: 'Patek Philippe Celestial - Astronomical Watches complication',
14
+ url: 'https://www.patek.com/en/service/taking-care-of-your-watch/settings/celestial',
15
+ },
16
+ ];
@@ -0,0 +1,278 @@
1
+ import { getJulianDate, getLST, formatHours } from './helpers';
2
+
3
+ let simulatedTime = Date.now();
4
+ let lastFrameTime = Date.now();
5
+ let speedMultiplier = 1;
6
+ let longitude = 0;
7
+ let longitudeFormat: 'decimal' | 'dms' = 'decimal';
8
+ let hemisphere: 'N' | 'S' = 'N';
9
+ let audioEnabled = false;
10
+ let audioCtx: AudioContext | null = null;
11
+ let lastTickSecond = -1;
12
+
13
+ try {
14
+ const storedLon = localStorage.getItem('sid-longitude');
15
+ if (storedLon) longitude = parseFloat(storedLon);
16
+ const storedFormat = localStorage.getItem('sid-longitude-format');
17
+ if (storedFormat === 'decimal' || storedFormat === 'dms') {
18
+ longitudeFormat = storedFormat;
19
+ }
20
+ const storedHem = localStorage.getItem('sid-hemisphere');
21
+ if (storedHem === 'N' || storedHem === 'S') {
22
+ hemisphere = storedHem;
23
+ }
24
+ } catch {
25
+ }
26
+
27
+ function saveSettings(): void {
28
+ try {
29
+ localStorage.setItem('sid-longitude', longitude.toString());
30
+ localStorage.setItem('sid-longitude-format', longitudeFormat);
31
+ localStorage.setItem('sid-hemisphere', hemisphere);
32
+ } catch {
33
+ }
34
+ }
35
+
36
+ function formatDMS(deg: number): string {
37
+ const direction = deg >= 0 ? 'E' : 'W';
38
+ const absDeg = Math.abs(deg);
39
+ const degrees = Math.floor(absDeg);
40
+ const minutes = Math.round((absDeg - degrees) * 60);
41
+ return `${degrees}° ${minutes.toString().padStart(2, '0')}' ${direction}`;
42
+ }
43
+
44
+ function updateLongitudeLabel(): void {
45
+ const label = document.getElementById('sid-label-longitude-val');
46
+ if (!label) return;
47
+ if (longitudeFormat === 'decimal') {
48
+ label.textContent = `${longitude.toFixed(1)}°`;
49
+ } else {
50
+ label.textContent = formatDMS(longitude);
51
+ }
52
+ }
53
+
54
+ function updateHemisphere(): void {
55
+ const northGroup = document.getElementById('sid-constellations-north');
56
+ const southGroup = document.getElementById('sid-constellations-south');
57
+ if (northGroup && southGroup) {
58
+ if (hemisphere === 'N') {
59
+ northGroup.style.display = 'block';
60
+ southGroup.style.display = 'none';
61
+ } else {
62
+ northGroup.style.display = 'none';
63
+ southGroup.style.display = 'block';
64
+ }
65
+ }
66
+ }
67
+
68
+ function initAudio(): void {
69
+ if (!audioCtx) {
70
+ const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
71
+ audioCtx = new AudioContextClass();
72
+ }
73
+ if (audioCtx.state === 'suspended') {
74
+ audioCtx.resume();
75
+ }
76
+ }
77
+
78
+ function playTick(freq: number): void {
79
+ if (!audioCtx || !audioEnabled) return;
80
+ const osc = audioCtx.createOscillator();
81
+ const gain = audioCtx.createGain();
82
+ osc.type = 'sine';
83
+ osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
84
+ gain.gain.setValueAtTime(0.015, audioCtx.currentTime);
85
+ gain.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.08);
86
+ osc.connect(gain);
87
+ gain.connect(audioCtx.destination);
88
+ osc.start();
89
+ osc.stop(audioCtx.currentTime + 0.08);
90
+ }
91
+
92
+ function updateHands(date: Date, lst: number): void {
93
+ const sec = date.getUTCSeconds() + date.getUTCMilliseconds() / 1000;
94
+ const min = date.getUTCMinutes() + sec / 60;
95
+ const hour = date.getUTCHours() + min / 60;
96
+
97
+ const elSec = document.getElementById('sid-hand-sec');
98
+ const elMin = document.getElementById('sid-hand-solar-min');
99
+ const elHour = document.getElementById('sid-hand-solar-hour');
100
+ const elSidHour = document.getElementById('sid-hand-sid-hour');
101
+ const elSphere = document.getElementById('sid-celestial-sphere');
102
+
103
+ if (elSec) elSec.style.transform = `rotate(${sec * 6}deg)`;
104
+ if (elMin) elMin.style.transform = `rotate(${min * 6}deg)`;
105
+ if (elHour) elHour.style.transform = `rotate(${(hour % 12) * 30}deg)`;
106
+ if (elSidHour) elSidHour.style.transform = `rotate(${lst * 15}deg)`;
107
+ if (elSphere) elSphere.style.transform = `rotate(${-lst * 15}deg)`;
108
+ }
109
+
110
+ function handleTickSound(date: Date): void {
111
+ const solarSec = date.getUTCSeconds();
112
+ if (solarSec !== lastTickSecond) {
113
+ lastTickSecond = solarSec;
114
+ const isHourCross = date.getUTCMinutes() === 0 && solarSec === 0;
115
+ playTick(isHourCross ? 1400 : 900);
116
+ }
117
+ }
118
+
119
+ function setText(id: string, text: string): void {
120
+ const el = document.getElementById(id);
121
+ if (el) {
122
+ el.textContent = text;
123
+ }
124
+ }
125
+
126
+ function updateDisplay(): void {
127
+ const date = new Date(simulatedTime);
128
+ const lst = getLST(date, longitude);
129
+ const jd = getJulianDate(date);
130
+
131
+ handleTickSound(date);
132
+ updateHands(date, lst);
133
+
134
+ const utcHours = date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600;
135
+ let diff = Math.abs(utcHours - lst);
136
+ if (diff > 12) {
137
+ diff = 24 - diff;
138
+ }
139
+
140
+ setText('sid-val-solar', formatHours(utcHours));
141
+ setText('sid-val-sidereal', formatHours(lst));
142
+ setText('sid-val-drift', `${Math.floor(diff * 60)}m ${Math.floor((diff * 3600) % 60)}s`);
143
+ setText('sid-val-julian', jd.toFixed(5));
144
+ }
145
+
146
+ function tick(): void {
147
+ const now = Date.now();
148
+ const delta = now - lastFrameTime;
149
+ lastFrameTime = now;
150
+ simulatedTime += delta * speedMultiplier;
151
+
152
+ updateDisplay();
153
+ requestAnimationFrame(tick);
154
+ }
155
+
156
+ function setupSpeedButtons(): void {
157
+ const btns = ['sid-btn-speed-1', 'sid-btn-speed-100', 'sid-btn-speed-10000'];
158
+ btns.forEach((id) => {
159
+ const el = document.getElementById(id);
160
+ if (!el) return;
161
+ el.addEventListener('click', () => {
162
+ btns.forEach((b) => document.getElementById(b)?.classList.remove('active'));
163
+ el.classList.add('active');
164
+ speedMultiplier = parseFloat(el.getAttribute('data-speed') || '1');
165
+ initAudio();
166
+ });
167
+ });
168
+ }
169
+
170
+ function setupFormatButtons(): void {
171
+ const decimalBtn = document.getElementById('sid-btn-format-decimal');
172
+ const dmsBtn = document.getElementById('sid-btn-format-dms');
173
+ if (decimalBtn && dmsBtn) {
174
+ decimalBtn.addEventListener('click', () => {
175
+ decimalBtn.classList.add('active');
176
+ dmsBtn.classList.remove('active');
177
+ longitudeFormat = 'decimal';
178
+ updateLongitudeLabel();
179
+ saveSettings();
180
+ });
181
+ dmsBtn.addEventListener('click', () => {
182
+ dmsBtn.classList.add('active');
183
+ decimalBtn.classList.remove('active');
184
+ longitudeFormat = 'dms';
185
+ updateLongitudeLabel();
186
+ saveSettings();
187
+ });
188
+ }
189
+ }
190
+
191
+ function setupHemisphereButtons(): void {
192
+ const northBtn = document.getElementById('sid-btn-hem-north');
193
+ const southBtn = document.getElementById('sid-btn-hem-south');
194
+ if (northBtn && southBtn) {
195
+ northBtn.addEventListener('click', () => {
196
+ northBtn.classList.add('active');
197
+ southBtn.classList.remove('active');
198
+ hemisphere = 'N';
199
+ updateHemisphere();
200
+ saveSettings();
201
+ });
202
+ southBtn.addEventListener('click', () => {
203
+ southBtn.classList.add('active');
204
+ northBtn.classList.remove('active');
205
+ hemisphere = 'S';
206
+ updateHemisphere();
207
+ saveSettings();
208
+ });
209
+ }
210
+ }
211
+
212
+ function setupLocationButton(slider: HTMLInputElement): void {
213
+ const locateBtn = document.getElementById('sid-btn-locate');
214
+ if (!locateBtn) return;
215
+ locateBtn.addEventListener('click', () => {
216
+ if (navigator.geolocation) {
217
+ navigator.geolocation.getCurrentPosition((pos) => {
218
+ const lon = pos.coords.longitude;
219
+ slider.value = lon.toFixed(1);
220
+ longitude = lon;
221
+ updateLongitudeLabel();
222
+ saveSettings();
223
+ }, () => {});
224
+ }
225
+ });
226
+ }
227
+
228
+ function setupAudioButton(): void {
229
+ const audioBtn = document.getElementById('sid-btn-audio');
230
+ if (!audioBtn) return;
231
+ audioBtn.addEventListener('click', () => {
232
+ initAudio();
233
+ audioEnabled = !audioEnabled;
234
+ audioBtn.textContent = audioEnabled ? 'ON' : 'OFF';
235
+ audioBtn.classList.toggle('active', audioEnabled);
236
+ });
237
+ }
238
+
239
+ function setupEventListeners(): void {
240
+ const slider = document.getElementById('sid-input-longitude') as HTMLInputElement;
241
+ if (slider) {
242
+ slider.value = longitude.toString();
243
+ slider.addEventListener('input', () => {
244
+ longitude = parseFloat(slider.value);
245
+ updateLongitudeLabel();
246
+ saveSettings();
247
+ });
248
+ setupLocationButton(slider);
249
+ }
250
+ setupAudioButton();
251
+ setupSpeedButtons();
252
+ setupFormatButtons();
253
+ setupHemisphereButtons();
254
+ }
255
+
256
+ let initiated = false;
257
+ function init(): void {
258
+ if (initiated) return;
259
+ initiated = true;
260
+ lastFrameTime = Date.now();
261
+ simulatedTime = Date.now();
262
+ setupEventListeners();
263
+
264
+ updateLongitudeLabel();
265
+ updateHemisphere();
266
+
267
+ document.getElementById('sid-btn-format-decimal')?.classList.toggle('active', longitudeFormat === 'decimal');
268
+ document.getElementById('sid-btn-format-dms')?.classList.toggle('active', longitudeFormat === 'dms');
269
+ document.getElementById('sid-btn-hem-north')?.classList.toggle('active', hemisphere === 'N');
270
+ document.getElementById('sid-btn-hem-south')?.classList.toggle('active', hemisphere === 'S');
271
+
272
+ requestAnimationFrame(tick);
273
+ }
274
+
275
+ document.addEventListener('DOMContentLoaded', init);
276
+ if (document.readyState !== 'loading') {
277
+ init();
278
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import SiderealPanel from './components/SiderealPanel.astro';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="tool-main-card" data-ui={JSON.stringify(ui)}>
12
+ <SiderealPanel labels={ui} />
13
+ </div>
14
+
15
+ <script src="./client.ts"></script>