@jjlmoya/utils-astronomy 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/category/i18n/de.ts +57 -57
- package/src/category/i18n/en.ts +1 -1
- package/src/category/i18n/es.ts +1 -1
- package/src/category/i18n/fr.ts +58 -58
- package/src/category/i18n/it.ts +1 -1
- package/src/category/i18n/nl.ts +57 -57
- package/src/category/i18n/pl.ts +57 -57
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/i18n/ru.ts +57 -57
- package/src/category/i18n/sv.ts +57 -57
- package/src/category/i18n/zh.ts +57 -57
- package/src/category/index.ts +30 -30
- package/src/category/seo.astro +15 -15
- package/src/components/PreviewNavSidebar.astro +116 -116
- package/src/components/PreviewToolbar.astro +143 -143
- package/src/data.ts +16 -16
- package/src/env.d.ts +5 -5
- package/src/index.ts +22 -22
- package/src/layouts/PreviewLayout.astro +122 -122
- package/src/pages/[locale].astro +251 -251
- package/src/pages/index.astro +4 -4
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +74 -0
- package/src/tests/faq_count.test.ts +1 -1
- package/src/tests/mocks/astro_mock.js +2 -2
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/seo_length.test.ts +1 -1
- package/src/tests/seo_parity.test.ts +2 -2
- package/src/tests/seo_translation_completeness.test.ts +75 -0
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/tool_structure.test.ts +71 -71
- package/src/tests/tool_validation.test.ts +146 -146
- package/src/tool/bortleVisualizer/bibliography.astro +14 -14
- package/src/tool/bortleVisualizer/bibliography.ts +8 -8
- package/src/tool/bortleVisualizer/i18n/pl.ts +159 -159
- package/src/tool/bortleVisualizer/i18n/ru.ts +159 -159
- package/src/tool/deepSpaceScope/bibliography.astro +14 -14
- package/src/tool/deepSpaceScope/component.astro +382 -382
- package/src/tool/deepSpaceScope/i18n/de.ts +172 -172
- package/src/tool/deepSpaceScope/i18n/ko.ts +172 -172
- package/src/tool/deepSpaceScope/i18n/pl.ts +172 -172
- package/src/tool/deepSpaceScope/i18n/ru.ts +172 -172
- package/src/tool/deepSpaceScope/i18n/zh.ts +172 -172
- package/src/tool/smartEyepieceCalculator/component.astro +125 -125
- package/src/tool/smartEyepieceCalculator/i18n/fr.ts +165 -165
- package/src/tool/smartEyepieceCalculator/i18n/ru.ts +165 -165
- package/src/tool/smartEyepieceCalculator/i18n/zh.ts +165 -165
- package/src/tool/starExposureCalculator/bibliography.astro +14 -14
- package/src/tool/starExposureCalculator/bibliography.ts +7 -7
- package/src/tool/starExposureCalculator/component.astro +249 -249
- package/src/tool/starExposureCalculator/i18n/pl.ts +156 -156
- package/src/tool/starExposureCalculator/i18n/ru.ts +156 -156
- package/src/tool/telescopeResolution/bibliography.astro +14 -14
- package/src/tool/telescopeResolution/component.astro +222 -222
- package/src/tool/telescopeResolution/i18n/de.ts +169 -169
- package/src/tool/telescopeResolution/i18n/en.ts +169 -169
- package/src/tool/telescopeResolution/i18n/es.ts +169 -169
- package/src/tool/telescopeResolution/i18n/fr.ts +169 -169
- package/src/tool/telescopeResolution/i18n/id.ts +169 -169
- package/src/tool/telescopeResolution/i18n/it.ts +169 -169
- package/src/tool/telescopeResolution/i18n/ja.ts +169 -169
- package/src/tool/telescopeResolution/i18n/ko.ts +169 -169
- package/src/tool/telescopeResolution/i18n/nl.ts +169 -169
- package/src/tool/telescopeResolution/i18n/pl.ts +169 -169
- package/src/tool/telescopeResolution/i18n/pt.ts +169 -169
- package/src/tool/telescopeResolution/i18n/ru.ts +169 -169
- package/src/tool/telescopeResolution/i18n/sv.ts +169 -169
- package/src/tool/telescopeResolution/i18n/tr.ts +169 -169
- package/src/tool/telescopeResolution/i18n/zh.ts +169 -169
|
@@ -1,382 +1,382 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { DeepSpaceScopeUI } from "./index";
|
|
3
|
-
import type { KnownLocale } from "../../types";
|
|
4
|
-
import { Icon } from "astro-icon/components";
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
ui: DeepSpaceScopeUI;
|
|
8
|
-
locale?: KnownLocale;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const { ui } = Astro.props;
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
<div class="scope-container" id="scope-container" aria-label={ui.toolTitle}>
|
|
15
|
-
<div id="world-layer" class="world-layer">
|
|
16
|
-
<canvas id="sky-canvas" class="sky-canvas"></canvas>
|
|
17
|
-
<div id="objects-layer" class="objects-layer"></div>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<div class="scope-vignette"></div>
|
|
21
|
-
<div id="pollution-layer" class="scope-pollution-layer"></div>
|
|
22
|
-
|
|
23
|
-
<div class="scope-drag-hint">
|
|
24
|
-
<span class="hint-mobile">{ui.dragHintMobile}</span>
|
|
25
|
-
<span class="hint-desktop">{ui.dragHint}</span>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div id="info-modal" class="scope-modal modal-hidden" aria-hidden="true">
|
|
29
|
-
<div class="scope-modal-content" id="modal-content">
|
|
30
|
-
<div class="modal-header">
|
|
31
|
-
<div class="modal-header-info">
|
|
32
|
-
<div id="modal-icon" class="modal-icon"></div>
|
|
33
|
-
<div>
|
|
34
|
-
<h3 id="modal-title" class="modal-title">Object Name</h3>
|
|
35
|
-
<p id="modal-type" class="modal-type">Type</p>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
<button id="close-modal" class="modal-close" aria-label={ui.closeLabel}>
|
|
39
|
-
<Icon name="mdi:close" />
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
42
|
-
<div class="modal-body">
|
|
43
|
-
<div class="modal-stat">
|
|
44
|
-
<span class="modal-stat-label">{ui.magnitudeLabel}</span>
|
|
45
|
-
<span id="modal-mag" class="modal-stat-value">1.5</span>
|
|
46
|
-
</div>
|
|
47
|
-
<div class="modal-stat">
|
|
48
|
-
<span class="modal-stat-label">{ui.coordinatesLabel}</span>
|
|
49
|
-
<div class="modal-coords">
|
|
50
|
-
<div id="modal-az" class="modal-coord">Az: 120°</div>
|
|
51
|
-
<div id="modal-alt" class="modal-coord modal-coord-secondary">
|
|
52
|
-
Alt: 45°
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
<p id="modal-desc" class="modal-desc">Description...</p>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<div class="scope-controls-wrapper">
|
|
62
|
-
<div class="scope-controls">
|
|
63
|
-
<div class="scope-control-grid">
|
|
64
|
-
<div class="scope-control-group">
|
|
65
|
-
<div class="scope-control-header">
|
|
66
|
-
<label for="aperture" class="scope-control-label scope-label-cyan">
|
|
67
|
-
<Icon name="mdi:telescope" />
|
|
68
|
-
{ui.apertureLabel}
|
|
69
|
-
</label>
|
|
70
|
-
<span id="aperture-display" class="scope-control-value">200mm</span>
|
|
71
|
-
</div>
|
|
72
|
-
<input
|
|
73
|
-
type="range"
|
|
74
|
-
id="aperture"
|
|
75
|
-
min="0"
|
|
76
|
-
max="100"
|
|
77
|
-
step="1"
|
|
78
|
-
value="50"
|
|
79
|
-
class="scope-range scope-range-cyan"
|
|
80
|
-
aria-label={ui.apertureLabel}
|
|
81
|
-
/>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<div class="scope-limit-display">
|
|
85
|
-
<div class="scope-limit-label">{ui.limitMagLabel}</div>
|
|
86
|
-
<div id="limit-mag" class="scope-limit-value">12.5</div>
|
|
87
|
-
<div class="scope-azimuth" id="azimuth-display">
|
|
88
|
-
{ui.azimuthLabel}: 0°
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<div class="scope-control-group">
|
|
93
|
-
<div class="scope-control-header">
|
|
94
|
-
<label for="bortle" class="scope-control-label scope-label-amber">
|
|
95
|
-
<Icon name="mdi:weather-night" />
|
|
96
|
-
{ui.bortleLabel}
|
|
97
|
-
</label>
|
|
98
|
-
<span id="bortle-display" class="scope-control-value">Class 5</span>
|
|
99
|
-
</div>
|
|
100
|
-
<input
|
|
101
|
-
type="range"
|
|
102
|
-
id="bortle"
|
|
103
|
-
min="1"
|
|
104
|
-
max="9"
|
|
105
|
-
step="1"
|
|
106
|
-
value="5"
|
|
107
|
-
class="scope-range scope-range-amber"
|
|
108
|
-
aria-label={ui.bortleLabel}
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<div class="scope-legend">
|
|
115
|
-
<span class="legend-item legend-planet">
|
|
116
|
-
<Icon name="mdi:circle" />
|
|
117
|
-
{ui.planetLabel}
|
|
118
|
-
</span>
|
|
119
|
-
<span class="legend-item legend-star">
|
|
120
|
-
<Icon name="mdi:star-four-points" />
|
|
121
|
-
{ui.starLabel}
|
|
122
|
-
</span>
|
|
123
|
-
<span class="legend-item legend-deep">
|
|
124
|
-
<Icon name="mdi:creation" />
|
|
125
|
-
{ui.deepSpaceLabel}
|
|
126
|
-
</span>
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<script define:vars={{ ui }} is:inline>
|
|
132
|
-
window.__deepSpaceUI = ui;
|
|
133
|
-
</script>
|
|
134
|
-
|
|
135
|
-
<script>
|
|
136
|
-
import type { DeepSpaceObject } from "./index";
|
|
137
|
-
import {
|
|
138
|
-
calculateLimitingMagnitude,
|
|
139
|
-
apertureSliderToMm,
|
|
140
|
-
} from "./logic";
|
|
141
|
-
|
|
142
|
-
const container = document.getElementById("scope-container");
|
|
143
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
|
-
const uiData = (window as any).__deepSpaceUI || {};
|
|
145
|
-
const canvas = document.getElementById("sky-canvas") as HTMLCanvasElement;
|
|
146
|
-
const getCanvasWidth = () => canvas ? canvas.offsetWidth : 0;
|
|
147
|
-
const getCanvasHeight = () => canvas ? canvas.offsetHeight : 0;
|
|
148
|
-
const getContainerWidth = () => container ? container.offsetWidth : 0;
|
|
149
|
-
const ctx = canvas?.getContext("2d");
|
|
150
|
-
const objectsLayer = document.getElementById("objects-layer");
|
|
151
|
-
const apertureInput = document.getElementById("aperture") as HTMLInputElement;
|
|
152
|
-
const bortleInput = document.getElementById("bortle") as HTMLInputElement;
|
|
153
|
-
const pollutionLayer = document.getElementById("pollution-layer");
|
|
154
|
-
const limitMagDisplay = document.getElementById("limit-mag");
|
|
155
|
-
const azimuthDisplay = document.getElementById("azimuth-display");
|
|
156
|
-
|
|
157
|
-
let panAngle = 0;
|
|
158
|
-
let isDragging = false;
|
|
159
|
-
let startX = 0;
|
|
160
|
-
let startPan = 0;
|
|
161
|
-
let dragDistance = 0;
|
|
162
|
-
|
|
163
|
-
const FOV = 100;
|
|
164
|
-
|
|
165
|
-
let stars: { az: number; alt: number; mag: number }[] = [];
|
|
166
|
-
function initStars() {
|
|
167
|
-
stars = [];
|
|
168
|
-
for (let i = 0; i < 3000; i++) {
|
|
169
|
-
stars.push({
|
|
170
|
-
az: Math.random() * 360,
|
|
171
|
-
alt: Math.random() * 100,
|
|
172
|
-
mag: Math.pow(Math.random(), 0.8) * 15,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function initObjectsDOM() {
|
|
178
|
-
if (!objectsLayer) return;
|
|
179
|
-
objectsLayer.innerHTML = "";
|
|
180
|
-
const objects = uiData.deepSpaceObjects || [];
|
|
181
|
-
objects.forEach((obj: DeepSpaceObject, idx: number) => {
|
|
182
|
-
const el = document.createElement("div");
|
|
183
|
-
el.id = `obj-${idx}`;
|
|
184
|
-
el.className = "scope-object";
|
|
185
|
-
el.dataset.mag = obj.mag.toString();
|
|
186
|
-
el.innerHTML = `
|
|
187
|
-
<div class="scope-obj-icon" style="color:${obj.color}">
|
|
188
|
-
<span class="iconify" data-icon="${obj.icon}"></span>
|
|
189
|
-
</div>
|
|
190
|
-
<div class="scope-obj-label">
|
|
191
|
-
<span class="scope-obj-name">${obj.name}</span>
|
|
192
|
-
<span class="scope-obj-mag">Mag ${obj.mag}</span>
|
|
193
|
-
</div>
|
|
194
|
-
`;
|
|
195
|
-
el.addEventListener("click", (e) => {
|
|
196
|
-
e.stopPropagation();
|
|
197
|
-
if (dragDistance < 5) openModal(obj);
|
|
198
|
-
});
|
|
199
|
-
objectsLayer.appendChild(el);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const modal = document.getElementById("info-modal");
|
|
204
|
-
const modalContent = document.getElementById("modal-content");
|
|
205
|
-
const closeModalBtn = document.getElementById("close-modal");
|
|
206
|
-
|
|
207
|
-
function openModal(obj: DeepSpaceObject) {
|
|
208
|
-
if (!modal || !modalContent) return;
|
|
209
|
-
document.getElementById("modal-title")!.innerText = obj.name;
|
|
210
|
-
document.getElementById("modal-type")!.innerText = obj.type;
|
|
211
|
-
document.getElementById("modal-mag")!.innerText = obj.mag.toString();
|
|
212
|
-
document.getElementById("modal-az")!.innerText = `${uiData.azimuthLabel}: ${obj.az}°`;
|
|
213
|
-
document.getElementById("modal-alt")!.innerText = `${uiData.altitudeLabel}: ${obj.alt}°`;
|
|
214
|
-
const iconEl = document.getElementById("modal-icon");
|
|
215
|
-
if (iconEl) {
|
|
216
|
-
iconEl.innerHTML = `<span class="iconify" data-icon="${obj.icon}" style="font-size:2.5rem;color:${obj.color}"></span>`;
|
|
217
|
-
}
|
|
218
|
-
const descEl = document.getElementById("modal-desc");
|
|
219
|
-
if (descEl) {
|
|
220
|
-
descEl.innerText =
|
|
221
|
-
obj.desc ||
|
|
222
|
-
`${uiData.defaultObjDesc} ${obj.mag}.`;
|
|
223
|
-
}
|
|
224
|
-
modal.classList.remove("modal-hidden");
|
|
225
|
-
modalContent.classList.remove("modal-scale-out");
|
|
226
|
-
modalContent.classList.add("modal-scale-in");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function closeModal() {
|
|
230
|
-
if (!modal || !modalContent) return;
|
|
231
|
-
modal.classList.add("modal-hidden");
|
|
232
|
-
modalContent.classList.remove("modal-scale-in");
|
|
233
|
-
modalContent.classList.add("modal-scale-out");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
closeModalBtn?.addEventListener("click", closeModal);
|
|
237
|
-
modal?.addEventListener("click", (e) => {
|
|
238
|
-
if (e.target === modal) closeModal();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
function getShortestDelta(target: number, current: number) {
|
|
242
|
-
const delta = target - current;
|
|
243
|
-
return ((delta + 540) % 360) - 180;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function updateDisplays(limit: number, apertureMM: number, bortle: number) {
|
|
247
|
-
document.getElementById("aperture-display")!.innerText = `${apertureMM}mm`;
|
|
248
|
-
document.getElementById("bortle-display")!.innerText = `${uiData.bortleClassPrefix} ${bortle}`;
|
|
249
|
-
if (limitMagDisplay) limitMagDisplay.innerText = limit.toFixed(1);
|
|
250
|
-
if (azimuthDisplay)
|
|
251
|
-
azimuthDisplay.innerText = `${uiData.azimuthLabel}: ${Math.round(panAngle)}°`;
|
|
252
|
-
if (pollutionLayer) {
|
|
253
|
-
const opacity = (bortle - 1) * 0.08;
|
|
254
|
-
pollutionLayer.style.backgroundColor = `rgba(217, 119, 6, ${opacity})`;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function renderStars(w: number, h: number, limit: number) {
|
|
259
|
-
if (!ctx) return;
|
|
260
|
-
const ppd = w / FOV;
|
|
261
|
-
stars.forEach((s) => {
|
|
262
|
-
if (s.mag > limit) return;
|
|
263
|
-
const delta = getShortestDelta(s.az, panAngle);
|
|
264
|
-
if (Math.abs(delta) >= FOV / 2 + 10) return;
|
|
265
|
-
const x = w / 2 + delta * ppd;
|
|
266
|
-
const y = h - (s.alt / 100) * h;
|
|
267
|
-
const margin = limit - s.mag;
|
|
268
|
-
const alpha = Math.min(1, margin * 0.4);
|
|
269
|
-
const size = Math.max(0.6, 3 - s.mag / 4);
|
|
270
|
-
ctx.fillStyle = `rgba(255,255,255, ${alpha})`;
|
|
271
|
-
ctx.beginPath();
|
|
272
|
-
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
273
|
-
ctx.fill();
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function updateObjectElement(
|
|
278
|
-
el: HTMLElement,
|
|
279
|
-
obj: DeepSpaceObject,
|
|
280
|
-
limit: number,
|
|
281
|
-
) {
|
|
282
|
-
const delta = getShortestDelta(obj.az, panAngle);
|
|
283
|
-
if (Math.abs(delta) >= FOV / 2 + 20) {
|
|
284
|
-
el.style.display = "none";
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
el.style.display = "flex";
|
|
288
|
-
el.style.left = `${50 + (delta / (FOV / 2)) * 50}%`;
|
|
289
|
-
el.style.top = `${100 - obj.alt}%`;
|
|
290
|
-
const label = el.querySelector(".scope-obj-label") as HTMLElement | null;
|
|
291
|
-
label?.style.setProperty("opacity", limit - obj.mag > 2.0 ? "1" : "0");
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function renderObjects(limit: number) {
|
|
295
|
-
const objects = uiData.deepSpaceObjects || [];
|
|
296
|
-
objects.forEach((obj: DeepSpaceObject, idx: number) => {
|
|
297
|
-
const el = document.getElementById(`obj-${idx}`);
|
|
298
|
-
if (!el) return;
|
|
299
|
-
if (obj.mag > limit) {
|
|
300
|
-
el.style.display = "none";
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
updateObjectElement(el, obj, limit);
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function render() {
|
|
308
|
-
if (!ctx || !canvas) return;
|
|
309
|
-
const w = (canvas.width = getCanvasWidth());
|
|
310
|
-
const h = (canvas.height = getCanvasHeight());
|
|
311
|
-
ctx.clearRect(0, 0, w, h);
|
|
312
|
-
const sliderVal = parseInt(apertureInput.value);
|
|
313
|
-
const bortle = parseInt(bortleInput.value);
|
|
314
|
-
const limit = calculateLimitingMagnitude(sliderVal, bortle);
|
|
315
|
-
const apertureMM = apertureSliderToMm(sliderVal);
|
|
316
|
-
updateDisplays(limit, apertureMM, bortle);
|
|
317
|
-
renderStars(w, h, limit);
|
|
318
|
-
renderObjects(limit);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
container?.addEventListener("mousedown", (e) => {
|
|
322
|
-
const target = e.target as HTMLElement;
|
|
323
|
-
if (target.tagName === "INPUT" || target.tagName === "BUTTON" || target.closest(".scope-controls-wrapper")) return;
|
|
324
|
-
isDragging = true;
|
|
325
|
-
startX = e.clientX;
|
|
326
|
-
startPan = panAngle;
|
|
327
|
-
dragDistance = 0;
|
|
328
|
-
container.style.cursor = "grabbing";
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
window.addEventListener("mousemove", (e) => {
|
|
332
|
-
if (!isDragging || !container) return;
|
|
333
|
-
const dx = e.clientX - startX;
|
|
334
|
-
dragDistance += Math.abs(e.movementX);
|
|
335
|
-
const degreesPerPixel = FOV / getContainerWidth();
|
|
336
|
-
let newPan = startPan - dx * degreesPerPixel;
|
|
337
|
-
if (newPan < 0) newPan += 360;
|
|
338
|
-
if (newPan >= 360) newPan -= 360;
|
|
339
|
-
panAngle = newPan;
|
|
340
|
-
requestAnimationFrame(render);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
window.addEventListener("mouseup", () => {
|
|
344
|
-
isDragging = false;
|
|
345
|
-
if (container) container.style.cursor = "grab";
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
container?.addEventListener("touchstart", (e) => {
|
|
349
|
-
const target = e.target as HTMLElement;
|
|
350
|
-
if (target.tagName === "INPUT" || target.closest(".scope-controls-wrapper")) return;
|
|
351
|
-
isDragging = true;
|
|
352
|
-
startX = e.touches[0]!.clientX;
|
|
353
|
-
startPan = panAngle;
|
|
354
|
-
dragDistance = 0;
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
window.addEventListener("touchmove", (e) => {
|
|
358
|
-
if (!isDragging || !container) return;
|
|
359
|
-
const dx = e.touches[0].clientX - startX;
|
|
360
|
-
dragDistance += Math.abs(dx);
|
|
361
|
-
const degreesPerPixel = FOV / getContainerWidth();
|
|
362
|
-
let newPan = startPan - dx * degreesPerPixel;
|
|
363
|
-
if (newPan < 0) newPan += 360;
|
|
364
|
-
if (newPan >= 360) newPan -= 360;
|
|
365
|
-
panAngle = newPan;
|
|
366
|
-
requestAnimationFrame(render);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
window.addEventListener("touchend", () => (isDragging = false));
|
|
370
|
-
|
|
371
|
-
const iconifyScript = document.createElement("script");
|
|
372
|
-
iconifyScript.src = "https://code.iconify.design/3/3.1.0/iconify.min.js";
|
|
373
|
-
document.head.appendChild(iconifyScript);
|
|
374
|
-
|
|
375
|
-
initStars();
|
|
376
|
-
initObjectsDOM();
|
|
377
|
-
apertureInput.addEventListener("input", () => requestAnimationFrame(render));
|
|
378
|
-
bortleInput.addEventListener("input", () => requestAnimationFrame(render));
|
|
379
|
-
window.addEventListener("resize", () => requestAnimationFrame(render));
|
|
380
|
-
setTimeout(render, 50);
|
|
381
|
-
</script>
|
|
382
|
-
|
|
1
|
+
---
|
|
2
|
+
import type { DeepSpaceScopeUI } from "./index";
|
|
3
|
+
import type { KnownLocale } from "../../types";
|
|
4
|
+
import { Icon } from "astro-icon/components";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
ui: DeepSpaceScopeUI;
|
|
8
|
+
locale?: KnownLocale;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="scope-container" id="scope-container" aria-label={ui.toolTitle}>
|
|
15
|
+
<div id="world-layer" class="world-layer">
|
|
16
|
+
<canvas id="sky-canvas" class="sky-canvas"></canvas>
|
|
17
|
+
<div id="objects-layer" class="objects-layer"></div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="scope-vignette"></div>
|
|
21
|
+
<div id="pollution-layer" class="scope-pollution-layer"></div>
|
|
22
|
+
|
|
23
|
+
<div class="scope-drag-hint">
|
|
24
|
+
<span class="hint-mobile">{ui.dragHintMobile}</span>
|
|
25
|
+
<span class="hint-desktop">{ui.dragHint}</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div id="info-modal" class="scope-modal modal-hidden" aria-hidden="true">
|
|
29
|
+
<div class="scope-modal-content" id="modal-content">
|
|
30
|
+
<div class="modal-header">
|
|
31
|
+
<div class="modal-header-info">
|
|
32
|
+
<div id="modal-icon" class="modal-icon"></div>
|
|
33
|
+
<div>
|
|
34
|
+
<h3 id="modal-title" class="modal-title">Object Name</h3>
|
|
35
|
+
<p id="modal-type" class="modal-type">Type</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<button id="close-modal" class="modal-close" aria-label={ui.closeLabel}>
|
|
39
|
+
<Icon name="mdi:close" />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="modal-body">
|
|
43
|
+
<div class="modal-stat">
|
|
44
|
+
<span class="modal-stat-label">{ui.magnitudeLabel}</span>
|
|
45
|
+
<span id="modal-mag" class="modal-stat-value">1.5</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="modal-stat">
|
|
48
|
+
<span class="modal-stat-label">{ui.coordinatesLabel}</span>
|
|
49
|
+
<div class="modal-coords">
|
|
50
|
+
<div id="modal-az" class="modal-coord">Az: 120°</div>
|
|
51
|
+
<div id="modal-alt" class="modal-coord modal-coord-secondary">
|
|
52
|
+
Alt: 45°
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<p id="modal-desc" class="modal-desc">Description...</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="scope-controls-wrapper">
|
|
62
|
+
<div class="scope-controls">
|
|
63
|
+
<div class="scope-control-grid">
|
|
64
|
+
<div class="scope-control-group">
|
|
65
|
+
<div class="scope-control-header">
|
|
66
|
+
<label for="aperture" class="scope-control-label scope-label-cyan">
|
|
67
|
+
<Icon name="mdi:telescope" />
|
|
68
|
+
{ui.apertureLabel}
|
|
69
|
+
</label>
|
|
70
|
+
<span id="aperture-display" class="scope-control-value">200mm</span>
|
|
71
|
+
</div>
|
|
72
|
+
<input
|
|
73
|
+
type="range"
|
|
74
|
+
id="aperture"
|
|
75
|
+
min="0"
|
|
76
|
+
max="100"
|
|
77
|
+
step="1"
|
|
78
|
+
value="50"
|
|
79
|
+
class="scope-range scope-range-cyan"
|
|
80
|
+
aria-label={ui.apertureLabel}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="scope-limit-display">
|
|
85
|
+
<div class="scope-limit-label">{ui.limitMagLabel}</div>
|
|
86
|
+
<div id="limit-mag" class="scope-limit-value">12.5</div>
|
|
87
|
+
<div class="scope-azimuth" id="azimuth-display">
|
|
88
|
+
{ui.azimuthLabel}: 0°
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="scope-control-group">
|
|
93
|
+
<div class="scope-control-header">
|
|
94
|
+
<label for="bortle" class="scope-control-label scope-label-amber">
|
|
95
|
+
<Icon name="mdi:weather-night" />
|
|
96
|
+
{ui.bortleLabel}
|
|
97
|
+
</label>
|
|
98
|
+
<span id="bortle-display" class="scope-control-value">Class 5</span>
|
|
99
|
+
</div>
|
|
100
|
+
<input
|
|
101
|
+
type="range"
|
|
102
|
+
id="bortle"
|
|
103
|
+
min="1"
|
|
104
|
+
max="9"
|
|
105
|
+
step="1"
|
|
106
|
+
value="5"
|
|
107
|
+
class="scope-range scope-range-amber"
|
|
108
|
+
aria-label={ui.bortleLabel}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="scope-legend">
|
|
115
|
+
<span class="legend-item legend-planet">
|
|
116
|
+
<Icon name="mdi:circle" />
|
|
117
|
+
{ui.planetLabel}
|
|
118
|
+
</span>
|
|
119
|
+
<span class="legend-item legend-star">
|
|
120
|
+
<Icon name="mdi:star-four-points" />
|
|
121
|
+
{ui.starLabel}
|
|
122
|
+
</span>
|
|
123
|
+
<span class="legend-item legend-deep">
|
|
124
|
+
<Icon name="mdi:creation" />
|
|
125
|
+
{ui.deepSpaceLabel}
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<script define:vars={{ ui }} is:inline>
|
|
132
|
+
window.__deepSpaceUI = ui;
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<script>
|
|
136
|
+
import type { DeepSpaceObject } from "./index";
|
|
137
|
+
import {
|
|
138
|
+
calculateLimitingMagnitude,
|
|
139
|
+
apertureSliderToMm,
|
|
140
|
+
} from "./logic";
|
|
141
|
+
|
|
142
|
+
const container = document.getElementById("scope-container");
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
|
+
const uiData = (window as any).__deepSpaceUI || {};
|
|
145
|
+
const canvas = document.getElementById("sky-canvas") as HTMLCanvasElement;
|
|
146
|
+
const getCanvasWidth = () => canvas ? canvas.offsetWidth : 0;
|
|
147
|
+
const getCanvasHeight = () => canvas ? canvas.offsetHeight : 0;
|
|
148
|
+
const getContainerWidth = () => container ? container.offsetWidth : 0;
|
|
149
|
+
const ctx = canvas?.getContext("2d");
|
|
150
|
+
const objectsLayer = document.getElementById("objects-layer");
|
|
151
|
+
const apertureInput = document.getElementById("aperture") as HTMLInputElement;
|
|
152
|
+
const bortleInput = document.getElementById("bortle") as HTMLInputElement;
|
|
153
|
+
const pollutionLayer = document.getElementById("pollution-layer");
|
|
154
|
+
const limitMagDisplay = document.getElementById("limit-mag");
|
|
155
|
+
const azimuthDisplay = document.getElementById("azimuth-display");
|
|
156
|
+
|
|
157
|
+
let panAngle = 0;
|
|
158
|
+
let isDragging = false;
|
|
159
|
+
let startX = 0;
|
|
160
|
+
let startPan = 0;
|
|
161
|
+
let dragDistance = 0;
|
|
162
|
+
|
|
163
|
+
const FOV = 100;
|
|
164
|
+
|
|
165
|
+
let stars: { az: number; alt: number; mag: number }[] = [];
|
|
166
|
+
function initStars() {
|
|
167
|
+
stars = [];
|
|
168
|
+
for (let i = 0; i < 3000; i++) {
|
|
169
|
+
stars.push({
|
|
170
|
+
az: Math.random() * 360,
|
|
171
|
+
alt: Math.random() * 100,
|
|
172
|
+
mag: Math.pow(Math.random(), 0.8) * 15,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function initObjectsDOM() {
|
|
178
|
+
if (!objectsLayer) return;
|
|
179
|
+
objectsLayer.innerHTML = "";
|
|
180
|
+
const objects = uiData.deepSpaceObjects || [];
|
|
181
|
+
objects.forEach((obj: DeepSpaceObject, idx: number) => {
|
|
182
|
+
const el = document.createElement("div");
|
|
183
|
+
el.id = `obj-${idx}`;
|
|
184
|
+
el.className = "scope-object";
|
|
185
|
+
el.dataset.mag = obj.mag.toString();
|
|
186
|
+
el.innerHTML = `
|
|
187
|
+
<div class="scope-obj-icon" style="color:${obj.color}">
|
|
188
|
+
<span class="iconify" data-icon="${obj.icon}"></span>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="scope-obj-label">
|
|
191
|
+
<span class="scope-obj-name">${obj.name}</span>
|
|
192
|
+
<span class="scope-obj-mag">Mag ${obj.mag}</span>
|
|
193
|
+
</div>
|
|
194
|
+
`;
|
|
195
|
+
el.addEventListener("click", (e) => {
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
if (dragDistance < 5) openModal(obj);
|
|
198
|
+
});
|
|
199
|
+
objectsLayer.appendChild(el);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const modal = document.getElementById("info-modal");
|
|
204
|
+
const modalContent = document.getElementById("modal-content");
|
|
205
|
+
const closeModalBtn = document.getElementById("close-modal");
|
|
206
|
+
|
|
207
|
+
function openModal(obj: DeepSpaceObject) {
|
|
208
|
+
if (!modal || !modalContent) return;
|
|
209
|
+
document.getElementById("modal-title")!.innerText = obj.name;
|
|
210
|
+
document.getElementById("modal-type")!.innerText = obj.type;
|
|
211
|
+
document.getElementById("modal-mag")!.innerText = obj.mag.toString();
|
|
212
|
+
document.getElementById("modal-az")!.innerText = `${uiData.azimuthLabel}: ${obj.az}°`;
|
|
213
|
+
document.getElementById("modal-alt")!.innerText = `${uiData.altitudeLabel}: ${obj.alt}°`;
|
|
214
|
+
const iconEl = document.getElementById("modal-icon");
|
|
215
|
+
if (iconEl) {
|
|
216
|
+
iconEl.innerHTML = `<span class="iconify" data-icon="${obj.icon}" style="font-size:2.5rem;color:${obj.color}"></span>`;
|
|
217
|
+
}
|
|
218
|
+
const descEl = document.getElementById("modal-desc");
|
|
219
|
+
if (descEl) {
|
|
220
|
+
descEl.innerText =
|
|
221
|
+
obj.desc ||
|
|
222
|
+
`${uiData.defaultObjDesc} ${obj.mag}.`;
|
|
223
|
+
}
|
|
224
|
+
modal.classList.remove("modal-hidden");
|
|
225
|
+
modalContent.classList.remove("modal-scale-out");
|
|
226
|
+
modalContent.classList.add("modal-scale-in");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function closeModal() {
|
|
230
|
+
if (!modal || !modalContent) return;
|
|
231
|
+
modal.classList.add("modal-hidden");
|
|
232
|
+
modalContent.classList.remove("modal-scale-in");
|
|
233
|
+
modalContent.classList.add("modal-scale-out");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
closeModalBtn?.addEventListener("click", closeModal);
|
|
237
|
+
modal?.addEventListener("click", (e) => {
|
|
238
|
+
if (e.target === modal) closeModal();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
function getShortestDelta(target: number, current: number) {
|
|
242
|
+
const delta = target - current;
|
|
243
|
+
return ((delta + 540) % 360) - 180;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function updateDisplays(limit: number, apertureMM: number, bortle: number) {
|
|
247
|
+
document.getElementById("aperture-display")!.innerText = `${apertureMM}mm`;
|
|
248
|
+
document.getElementById("bortle-display")!.innerText = `${uiData.bortleClassPrefix} ${bortle}`;
|
|
249
|
+
if (limitMagDisplay) limitMagDisplay.innerText = limit.toFixed(1);
|
|
250
|
+
if (azimuthDisplay)
|
|
251
|
+
azimuthDisplay.innerText = `${uiData.azimuthLabel}: ${Math.round(panAngle)}°`;
|
|
252
|
+
if (pollutionLayer) {
|
|
253
|
+
const opacity = (bortle - 1) * 0.08;
|
|
254
|
+
pollutionLayer.style.backgroundColor = `rgba(217, 119, 6, ${opacity})`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function renderStars(w: number, h: number, limit: number) {
|
|
259
|
+
if (!ctx) return;
|
|
260
|
+
const ppd = w / FOV;
|
|
261
|
+
stars.forEach((s) => {
|
|
262
|
+
if (s.mag > limit) return;
|
|
263
|
+
const delta = getShortestDelta(s.az, panAngle);
|
|
264
|
+
if (Math.abs(delta) >= FOV / 2 + 10) return;
|
|
265
|
+
const x = w / 2 + delta * ppd;
|
|
266
|
+
const y = h - (s.alt / 100) * h;
|
|
267
|
+
const margin = limit - s.mag;
|
|
268
|
+
const alpha = Math.min(1, margin * 0.4);
|
|
269
|
+
const size = Math.max(0.6, 3 - s.mag / 4);
|
|
270
|
+
ctx.fillStyle = `rgba(255,255,255, ${alpha})`;
|
|
271
|
+
ctx.beginPath();
|
|
272
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
273
|
+
ctx.fill();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function updateObjectElement(
|
|
278
|
+
el: HTMLElement,
|
|
279
|
+
obj: DeepSpaceObject,
|
|
280
|
+
limit: number,
|
|
281
|
+
) {
|
|
282
|
+
const delta = getShortestDelta(obj.az, panAngle);
|
|
283
|
+
if (Math.abs(delta) >= FOV / 2 + 20) {
|
|
284
|
+
el.style.display = "none";
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
el.style.display = "flex";
|
|
288
|
+
el.style.left = `${50 + (delta / (FOV / 2)) * 50}%`;
|
|
289
|
+
el.style.top = `${100 - obj.alt}%`;
|
|
290
|
+
const label = el.querySelector(".scope-obj-label") as HTMLElement | null;
|
|
291
|
+
label?.style.setProperty("opacity", limit - obj.mag > 2.0 ? "1" : "0");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function renderObjects(limit: number) {
|
|
295
|
+
const objects = uiData.deepSpaceObjects || [];
|
|
296
|
+
objects.forEach((obj: DeepSpaceObject, idx: number) => {
|
|
297
|
+
const el = document.getElementById(`obj-${idx}`);
|
|
298
|
+
if (!el) return;
|
|
299
|
+
if (obj.mag > limit) {
|
|
300
|
+
el.style.display = "none";
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
updateObjectElement(el, obj, limit);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function render() {
|
|
308
|
+
if (!ctx || !canvas) return;
|
|
309
|
+
const w = (canvas.width = getCanvasWidth());
|
|
310
|
+
const h = (canvas.height = getCanvasHeight());
|
|
311
|
+
ctx.clearRect(0, 0, w, h);
|
|
312
|
+
const sliderVal = parseInt(apertureInput.value);
|
|
313
|
+
const bortle = parseInt(bortleInput.value);
|
|
314
|
+
const limit = calculateLimitingMagnitude(sliderVal, bortle);
|
|
315
|
+
const apertureMM = apertureSliderToMm(sliderVal);
|
|
316
|
+
updateDisplays(limit, apertureMM, bortle);
|
|
317
|
+
renderStars(w, h, limit);
|
|
318
|
+
renderObjects(limit);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
container?.addEventListener("mousedown", (e) => {
|
|
322
|
+
const target = e.target as HTMLElement;
|
|
323
|
+
if (target.tagName === "INPUT" || target.tagName === "BUTTON" || target.closest(".scope-controls-wrapper")) return;
|
|
324
|
+
isDragging = true;
|
|
325
|
+
startX = e.clientX;
|
|
326
|
+
startPan = panAngle;
|
|
327
|
+
dragDistance = 0;
|
|
328
|
+
container.style.cursor = "grabbing";
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
window.addEventListener("mousemove", (e) => {
|
|
332
|
+
if (!isDragging || !container) return;
|
|
333
|
+
const dx = e.clientX - startX;
|
|
334
|
+
dragDistance += Math.abs(e.movementX);
|
|
335
|
+
const degreesPerPixel = FOV / getContainerWidth();
|
|
336
|
+
let newPan = startPan - dx * degreesPerPixel;
|
|
337
|
+
if (newPan < 0) newPan += 360;
|
|
338
|
+
if (newPan >= 360) newPan -= 360;
|
|
339
|
+
panAngle = newPan;
|
|
340
|
+
requestAnimationFrame(render);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
window.addEventListener("mouseup", () => {
|
|
344
|
+
isDragging = false;
|
|
345
|
+
if (container) container.style.cursor = "grab";
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
container?.addEventListener("touchstart", (e) => {
|
|
349
|
+
const target = e.target as HTMLElement;
|
|
350
|
+
if (target.tagName === "INPUT" || target.closest(".scope-controls-wrapper")) return;
|
|
351
|
+
isDragging = true;
|
|
352
|
+
startX = e.touches[0]!.clientX;
|
|
353
|
+
startPan = panAngle;
|
|
354
|
+
dragDistance = 0;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
window.addEventListener("touchmove", (e) => {
|
|
358
|
+
if (!isDragging || !container) return;
|
|
359
|
+
const dx = e.touches[0].clientX - startX;
|
|
360
|
+
dragDistance += Math.abs(dx);
|
|
361
|
+
const degreesPerPixel = FOV / getContainerWidth();
|
|
362
|
+
let newPan = startPan - dx * degreesPerPixel;
|
|
363
|
+
if (newPan < 0) newPan += 360;
|
|
364
|
+
if (newPan >= 360) newPan -= 360;
|
|
365
|
+
panAngle = newPan;
|
|
366
|
+
requestAnimationFrame(render);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
window.addEventListener("touchend", () => (isDragging = false));
|
|
370
|
+
|
|
371
|
+
const iconifyScript = document.createElement("script");
|
|
372
|
+
iconifyScript.src = "https://code.iconify.design/3/3.1.0/iconify.min.js";
|
|
373
|
+
document.head.appendChild(iconifyScript);
|
|
374
|
+
|
|
375
|
+
initStars();
|
|
376
|
+
initObjectsDOM();
|
|
377
|
+
apertureInput.addEventListener("input", () => requestAnimationFrame(render));
|
|
378
|
+
bortleInput.addEventListener("input", () => requestAnimationFrame(render));
|
|
379
|
+
window.addEventListener("resize", () => requestAnimationFrame(render));
|
|
380
|
+
setTimeout(render, 50);
|
|
381
|
+
</script>
|
|
382
|
+
|