@jjlmoya/utils-chrono 1.8.0 → 1.10.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 +1 -1
- package/src/category/index.ts +4 -0
- package/src/entries.ts +7 -1
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/sidereal-time-tracker/bibliography.astro +16 -0
- package/src/tool/sidereal-time-tracker/bibliography.ts +16 -0
- package/src/tool/sidereal-time-tracker/client.ts +278 -0
- package/src/tool/sidereal-time-tracker/component.astro +15 -0
- package/src/tool/sidereal-time-tracker/components/SiderealPanel.astro +197 -0
- package/src/tool/sidereal-time-tracker/entry.ts +51 -0
- package/src/tool/sidereal-time-tracker/helpers.ts +80 -0
- package/src/tool/sidereal-time-tracker/i18n/de.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/en.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/es.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/fr.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/id.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/it.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/ja.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/ko.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/nl.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/pl.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/pt.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/ru.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/sv.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/tr.ts +93 -0
- package/src/tool/sidereal-time-tracker/i18n/zh.ts +93 -0
- package/src/tool/sidereal-time-tracker/index.ts +11 -0
- package/src/tool/sidereal-time-tracker/seo.astro +16 -0
- package/src/tool/sidereal-time-tracker/sidereal-time-tracker.css +257 -0
- package/src/tool/telemeter-calculator/audio.ts +59 -0
- package/src/tool/telemeter-calculator/bibliography.astro +16 -0
- package/src/tool/telemeter-calculator/bibliography.ts +16 -0
- package/src/tool/telemeter-calculator/client.ts +269 -0
- package/src/tool/telemeter-calculator/component.astro +15 -0
- package/src/tool/telemeter-calculator/components/TelemeterPanel.astro +224 -0
- package/src/tool/telemeter-calculator/entry.ts +53 -0
- package/src/tool/telemeter-calculator/helpers.ts +80 -0
- package/src/tool/telemeter-calculator/i18n/de.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/en.ts +114 -0
- package/src/tool/telemeter-calculator/i18n/es.ts +114 -0
- package/src/tool/telemeter-calculator/i18n/fr.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/id.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/it.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/ja.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/ko.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/nl.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/pl.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/pt.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/ru.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/sv.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/tr.ts +57 -0
- package/src/tool/telemeter-calculator/i18n/zh.ts +57 -0
- package/src/tool/telemeter-calculator/index.ts +11 -0
- package/src/tool/telemeter-calculator/seo.astro +16 -0
- package/src/tool/telemeter-calculator/telemeter-calculator.css +856 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
let audioCtx: AudioContext | null = null;
|
|
2
|
+
|
|
3
|
+
function initAudio() {
|
|
4
|
+
if (!audioCtx) {
|
|
5
|
+
audioCtx = new (window.AudioContext || (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext)();
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function playPusherSound() {
|
|
10
|
+
initAudio();
|
|
11
|
+
if (!audioCtx) return;
|
|
12
|
+
|
|
13
|
+
const osc = audioCtx.createOscillator();
|
|
14
|
+
const gainNode = audioCtx.createGain();
|
|
15
|
+
|
|
16
|
+
osc.connect(gainNode);
|
|
17
|
+
gainNode.connect(audioCtx.destination);
|
|
18
|
+
|
|
19
|
+
osc.type = 'sine';
|
|
20
|
+
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
|
|
21
|
+
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.05);
|
|
22
|
+
|
|
23
|
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
|
24
|
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.05);
|
|
25
|
+
|
|
26
|
+
osc.start();
|
|
27
|
+
osc.stop(audioCtx.currentTime + 0.05);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function playThunderSound() {
|
|
31
|
+
initAudio();
|
|
32
|
+
if (!audioCtx) return;
|
|
33
|
+
|
|
34
|
+
const bufferSize = audioCtx.sampleRate * 1.5;
|
|
35
|
+
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
36
|
+
const data = buffer.getChannelData(0);
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < bufferSize; i++) {
|
|
39
|
+
data[i] = Math.random() * 2 - 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const noiseNode = audioCtx.createBufferSource();
|
|
43
|
+
noiseNode.buffer = buffer;
|
|
44
|
+
|
|
45
|
+
const lowpass = audioCtx.createBiquadFilter();
|
|
46
|
+
lowpass.type = 'lowpass';
|
|
47
|
+
lowpass.frequency.setValueAtTime(80, audioCtx.currentTime);
|
|
48
|
+
lowpass.frequency.linearRampToValueAtTime(30, audioCtx.currentTime + 1.2);
|
|
49
|
+
|
|
50
|
+
const gainNode = audioCtx.createGain();
|
|
51
|
+
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
|
52
|
+
gainNode.gain.linearRampToValueAtTime(0.001, audioCtx.currentTime + 1.2);
|
|
53
|
+
|
|
54
|
+
noiseNode.connect(lowpass);
|
|
55
|
+
lowpass.connect(gainNode);
|
|
56
|
+
gainNode.connect(audioCtx.destination);
|
|
57
|
+
|
|
58
|
+
noiseNode.start();
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { telemeterCalculator } 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 = telemeterCalculator.i18n[locale] || telemeterCalculator.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: 'Speed of Sound in Air - Physics LibreTexts',
|
|
6
|
+
url: 'https://phys.libretexts.org/Bookshelves/University_Physics/University_Physics_(OpenStax)/Book%3A_University_Physics_I_-_Mechanics_Sound_Oscillations_and_Waves_(OpenStax)/17%3A_Sound/17.03%3A_Speed_of_Sound',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'A Deep Dive into Your Watch\'s Bezel',
|
|
10
|
+
url: 'https://www.brigadewatches.com/blogs/news/part-1-the-bezel-of-the-watch',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'How to Use a Tachymeter, Telemeter, and Pulsations Scale - Craft + Tailored',
|
|
14
|
+
url: 'https://journal.craftandtailored.com/how-to-use-a-tachymeter-telemeter-and-pulsations-scale/',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { playPusherSound, playThunderSound } from './audio';
|
|
2
|
+
import { getSpeedOfSound, formatDistance } from './helpers';
|
|
3
|
+
|
|
4
|
+
const pusherStart = document.getElementById('pusher-start') as HTMLElement;
|
|
5
|
+
const pusherReset = document.getElementById('pusher-reset') as HTMLElement;
|
|
6
|
+
const btnFlash = document.getElementById('btn-flash') as HTMLButtonElement;
|
|
7
|
+
const btnSound = document.getElementById('btn-sound') as HTMLButtonElement;
|
|
8
|
+
const btnReset = document.getElementById('btn-reset') as HTMLButtonElement;
|
|
9
|
+
|
|
10
|
+
const tempSlider = document.getElementById('temp-slider') as HTMLInputElement;
|
|
11
|
+
const tempDisplay = document.getElementById('temp-display') as HTMLElement;
|
|
12
|
+
const speedDisplay = document.getElementById('speed-display') as HTMLElement;
|
|
13
|
+
|
|
14
|
+
const resultPrimary = document.getElementById('result-primary') as HTMLElement;
|
|
15
|
+
const resultSecondary = document.getElementById('result-secondary') as HTMLElement;
|
|
16
|
+
const resultTime = document.getElementById('result-time') as HTMLElement;
|
|
17
|
+
|
|
18
|
+
const dialDigitalTime = document.getElementById('dial-digital-time') as SVGTextElement;
|
|
19
|
+
const dialAmbientIndicator = document.getElementById('dial-ambient-indicator') as SVGTextElement;
|
|
20
|
+
const secondsHandGroup = document.getElementById('seconds-hand-group') as SVGGElement;
|
|
21
|
+
|
|
22
|
+
const lightningOverlay = document.getElementById('lightning-overlay') as HTMLElement;
|
|
23
|
+
const soundRipple = document.getElementById('sound-ripple') as HTMLElement;
|
|
24
|
+
|
|
25
|
+
const historyListContainer = document.getElementById('history-list') as HTMLElement;
|
|
26
|
+
const emptyHistoryEl = document.getElementById('empty-history') as HTMLElement;
|
|
27
|
+
const ticksG = document.getElementById('tel-ticks') as SVGElement;
|
|
28
|
+
|
|
29
|
+
const unitButtons = document.querySelectorAll('.tel-chip-btn');
|
|
30
|
+
|
|
31
|
+
let isRunning = false;
|
|
32
|
+
let startTime = 0;
|
|
33
|
+
let elapsedTime = 0;
|
|
34
|
+
let rafId = 0;
|
|
35
|
+
let tempCelsius = 20;
|
|
36
|
+
let unitSystem = 'metric';
|
|
37
|
+
|
|
38
|
+
interface HistoryItem {
|
|
39
|
+
timestamp: string;
|
|
40
|
+
distance: string;
|
|
41
|
+
time: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildTicks() {
|
|
45
|
+
if (!ticksG) return;
|
|
46
|
+
ticksG.innerHTML = '';
|
|
47
|
+
const CX = 135;
|
|
48
|
+
const CY = 135;
|
|
49
|
+
const R_IN = 88;
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < 60; i++) {
|
|
52
|
+
const angle = (i * 6) * Math.PI / 180;
|
|
53
|
+
const isMajor = i % 5 === 0;
|
|
54
|
+
const length = isMajor ? 6 : 3;
|
|
55
|
+
|
|
56
|
+
const x1 = CX + R_IN * Math.sin(angle);
|
|
57
|
+
const y1 = CY - R_IN * Math.cos(angle);
|
|
58
|
+
const x2 = CX + (R_IN + length) * Math.sin(angle);
|
|
59
|
+
const y2 = CY - (R_IN + length) * Math.cos(angle);
|
|
60
|
+
|
|
61
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
62
|
+
line.setAttribute('x1', String(x1));
|
|
63
|
+
line.setAttribute('y1', String(y1));
|
|
64
|
+
line.setAttribute('x2', String(x2));
|
|
65
|
+
line.setAttribute('y2', String(y2));
|
|
66
|
+
line.classList.add(isMajor ? 'tel-tick-major' : 'tel-tick-minor');
|
|
67
|
+
ticksG.appendChild(line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function updateEnvironmentalReadouts() {
|
|
72
|
+
const speed = getSpeedOfSound(tempCelsius);
|
|
73
|
+
if (unitSystem === 'metric') {
|
|
74
|
+
speedDisplay.textContent = `${speed.toFixed(1)} m/s`;
|
|
75
|
+
dialAmbientIndicator.textContent = `${tempCelsius.toFixed(1)}°C`;
|
|
76
|
+
} else {
|
|
77
|
+
const speedFt = speed * 3.28084;
|
|
78
|
+
speedDisplay.textContent = `${speedFt.toFixed(1)} ft/s`;
|
|
79
|
+
const tempFahr = (tempCelsius * 9/5) + 32;
|
|
80
|
+
dialAmbientIndicator.textContent = `${tempFahr.toFixed(1)}°F`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updateCalculations() {
|
|
85
|
+
const result = formatDistance(elapsedTime, tempCelsius, unitSystem);
|
|
86
|
+
resultTime.textContent = result.timeText;
|
|
87
|
+
dialDigitalTime.textContent = `${elapsedTime.toFixed(1)}s`;
|
|
88
|
+
resultPrimary.textContent = result.primary;
|
|
89
|
+
resultSecondary.textContent = result.secondary;
|
|
90
|
+
|
|
91
|
+
const handAngle = (elapsedTime * 6) % 360;
|
|
92
|
+
secondsHandGroup.style.transform = `rotate(${handAngle}deg)`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tick(timestamp: number) {
|
|
96
|
+
if (!startTime) startTime = timestamp;
|
|
97
|
+
elapsedTime = (timestamp - startTime) / 1000;
|
|
98
|
+
updateCalculations();
|
|
99
|
+
rafId = requestAnimationFrame(tick);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function startTimer() {
|
|
103
|
+
playPusherSound();
|
|
104
|
+
if (isRunning) return;
|
|
105
|
+
isRunning = true;
|
|
106
|
+
startTime = 0;
|
|
107
|
+
elapsedTime = 0;
|
|
108
|
+
|
|
109
|
+
btnFlash.disabled = true;
|
|
110
|
+
btnSound.disabled = false;
|
|
111
|
+
btnReset.disabled = true;
|
|
112
|
+
pusherReset.classList.add('disabled');
|
|
113
|
+
|
|
114
|
+
lightningOverlay.classList.add('flash-active');
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
lightningOverlay.classList.remove('flash-active');
|
|
117
|
+
}, 400);
|
|
118
|
+
|
|
119
|
+
rafId = requestAnimationFrame(tick);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stopTimer() {
|
|
123
|
+
playPusherSound();
|
|
124
|
+
playThunderSound();
|
|
125
|
+
if (!isRunning) return;
|
|
126
|
+
isRunning = false;
|
|
127
|
+
cancelAnimationFrame(rafId);
|
|
128
|
+
|
|
129
|
+
btnFlash.disabled = false;
|
|
130
|
+
btnSound.disabled = true;
|
|
131
|
+
btnReset.disabled = false;
|
|
132
|
+
pusherReset.classList.remove('disabled');
|
|
133
|
+
|
|
134
|
+
soundRipple.classList.add('ripple-active');
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
soundRipple.classList.remove('ripple-active');
|
|
137
|
+
}, 800);
|
|
138
|
+
|
|
139
|
+
addHistoryItem();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resetTimer() {
|
|
143
|
+
playPusherSound();
|
|
144
|
+
if (isRunning) {
|
|
145
|
+
isRunning = false;
|
|
146
|
+
cancelAnimationFrame(rafId);
|
|
147
|
+
}
|
|
148
|
+
elapsedTime = 0;
|
|
149
|
+
startTime = 0;
|
|
150
|
+
updateCalculations();
|
|
151
|
+
|
|
152
|
+
btnFlash.disabled = false;
|
|
153
|
+
btnSound.disabled = true;
|
|
154
|
+
btnReset.disabled = true;
|
|
155
|
+
pusherReset.classList.add('disabled');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function addHistoryItem() {
|
|
159
|
+
const item: HistoryItem = {
|
|
160
|
+
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
|
|
161
|
+
distance: resultPrimary.textContent || '0.00 km',
|
|
162
|
+
time: `${elapsedTime.toFixed(2)}s`
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const list = getHistory();
|
|
166
|
+
list.unshift(item);
|
|
167
|
+
if (list.length > 5) {
|
|
168
|
+
list.pop();
|
|
169
|
+
}
|
|
170
|
+
localStorage.setItem('telemeter_history', JSON.stringify(list));
|
|
171
|
+
renderHistory();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getHistory(): HistoryItem[] {
|
|
175
|
+
const data = localStorage.getItem('telemeter_history');
|
|
176
|
+
return data ? JSON.parse(data) : [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderHistory() {
|
|
180
|
+
const items = getHistory();
|
|
181
|
+
if (items.length === 0) {
|
|
182
|
+
emptyHistoryEl.style.display = 'block';
|
|
183
|
+
const oldItems = historyListContainer.querySelectorAll('.tel-history-item');
|
|
184
|
+
oldItems.forEach(el => el.remove());
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
emptyHistoryEl.style.display = 'none';
|
|
189
|
+
const oldItems = historyListContainer.querySelectorAll('.tel-history-item');
|
|
190
|
+
oldItems.forEach(el => el.remove());
|
|
191
|
+
|
|
192
|
+
items.forEach(item => {
|
|
193
|
+
const row = document.createElement('div');
|
|
194
|
+
row.className = 'tel-history-item';
|
|
195
|
+
|
|
196
|
+
const timeSpan = document.createElement('span');
|
|
197
|
+
timeSpan.className = 'tel-history-item-time';
|
|
198
|
+
timeSpan.textContent = item.timestamp;
|
|
199
|
+
|
|
200
|
+
const elapsedSpan = document.createElement('span');
|
|
201
|
+
elapsedSpan.className = 'tel-history-item-elapsed';
|
|
202
|
+
elapsedSpan.textContent = item.time;
|
|
203
|
+
|
|
204
|
+
const distSpan = document.createElement('span');
|
|
205
|
+
distSpan.className = 'tel-history-item-dist';
|
|
206
|
+
distSpan.textContent = item.distance;
|
|
207
|
+
|
|
208
|
+
row.appendChild(timeSpan);
|
|
209
|
+
row.appendChild(elapsedSpan);
|
|
210
|
+
row.appendChild(distSpan);
|
|
211
|
+
|
|
212
|
+
historyListContainer.appendChild(row);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pusherStart.addEventListener('click', () => {
|
|
217
|
+
if (pusherStart.classList.contains('disabled')) return;
|
|
218
|
+
if (isRunning) {
|
|
219
|
+
stopTimer();
|
|
220
|
+
} else {
|
|
221
|
+
startTimer();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
pusherReset.addEventListener('click', () => {
|
|
226
|
+
if (pusherReset.classList.contains('disabled')) return;
|
|
227
|
+
resetTimer();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
btnFlash.addEventListener('click', startTimer);
|
|
231
|
+
btnSound.addEventListener('click', stopTimer);
|
|
232
|
+
btnReset.addEventListener('click', resetTimer);
|
|
233
|
+
|
|
234
|
+
tempSlider.addEventListener('input', (e) => {
|
|
235
|
+
tempCelsius = parseFloat((e.target as HTMLInputElement).value);
|
|
236
|
+
if (unitSystem === 'metric') {
|
|
237
|
+
tempDisplay.textContent = `${tempCelsius}°C`;
|
|
238
|
+
} else {
|
|
239
|
+
const tempFahr = (tempCelsius * 9/5) + 32;
|
|
240
|
+
tempDisplay.textContent = `${tempFahr.toFixed(0)}°F`;
|
|
241
|
+
}
|
|
242
|
+
updateEnvironmentalReadouts();
|
|
243
|
+
updateCalculations();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
unitButtons.forEach(btn => {
|
|
247
|
+
btn.addEventListener('click', () => {
|
|
248
|
+
unitButtons.forEach(b => b.classList.remove('active'));
|
|
249
|
+
btn.classList.add('active');
|
|
250
|
+
unitSystem = btn.getAttribute('data-unit') || 'metric';
|
|
251
|
+
|
|
252
|
+
const tempVal = parseFloat(tempSlider.value);
|
|
253
|
+
if (unitSystem === 'metric') {
|
|
254
|
+
tempDisplay.textContent = `${tempVal}°C`;
|
|
255
|
+
} else {
|
|
256
|
+
const tempFahr = (tempVal * 9/5) + 32;
|
|
257
|
+
tempDisplay.textContent = `${tempFahr.toFixed(0)}°F`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
updateEnvironmentalReadouts();
|
|
261
|
+
updateCalculations();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
buildTicks();
|
|
266
|
+
updateEnvironmentalReadouts();
|
|
267
|
+
updateCalculations();
|
|
268
|
+
renderHistory();
|
|
269
|
+
pusherReset.classList.add('disabled');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import TelemeterPanel from './components/TelemeterPanel.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
|
+
<TelemeterPanel labels={ui} />
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<script src="./client.ts"></script>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
labels: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { labels } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="tel-card">
|
|
10
|
+
<div class="tel-top">
|
|
11
|
+
<h3 class="tel-h">{labels.title}</h3>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="tel-grid">
|
|
15
|
+
<div class="tel-visualizer-section">
|
|
16
|
+
<div class="tel-lightning-overlay" id="lightning-overlay"></div>
|
|
17
|
+
<div class="tel-ripple" id="sound-ripple"></div>
|
|
18
|
+
|
|
19
|
+
<div class="tel-watch-container">
|
|
20
|
+
<svg class="tel-svg-dial" viewBox="0 0 270 270" id="tel-svg">
|
|
21
|
+
<defs>
|
|
22
|
+
<linearGradient id="tel-case-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
23
|
+
<stop offset="0%" stop-color="#9ca3af" />
|
|
24
|
+
<stop offset="50%" stop-color="#4b5563" />
|
|
25
|
+
<stop offset="100%" stop-color="#1f2937" />
|
|
26
|
+
</linearGradient>
|
|
27
|
+
<radialGradient id="tel-dial-grad" cx="50%" cy="50%" r="50%">
|
|
28
|
+
<stop offset="0%" stop-color="#1e293b" />
|
|
29
|
+
<stop offset="70%" stop-color="#0f172a" />
|
|
30
|
+
<stop offset="100%" stop-color="#020617" />
|
|
31
|
+
</radialGradient>
|
|
32
|
+
<linearGradient id="tel-metal-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
33
|
+
<stop offset="0%" stop-color="#f3f4f6" />
|
|
34
|
+
<stop offset="30%" stop-color="#d1d5db" />
|
|
35
|
+
<stop offset="70%" stop-color="#9ca3af" />
|
|
36
|
+
<stop offset="100%" stop-color="#4b5563" />
|
|
37
|
+
</linearGradient>
|
|
38
|
+
<linearGradient id="tel-metal-grad-dark" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
39
|
+
<stop offset="0%" stop-color="#9ca3af" />
|
|
40
|
+
<stop offset="100%" stop-color="#374151" />
|
|
41
|
+
</linearGradient>
|
|
42
|
+
<marker id="tel-arrow" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
43
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--accent)" />
|
|
44
|
+
</marker>
|
|
45
|
+
<marker id="tel-arrow-gray" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
46
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--text-base)" fill-opacity="0.4" />
|
|
47
|
+
</marker>
|
|
48
|
+
</defs>
|
|
49
|
+
|
|
50
|
+
<circle class="tel-case-bg" cx="135" cy="135" r="112" />
|
|
51
|
+
<circle class="tel-bezel-bg" cx="135" cy="135" r="102" />
|
|
52
|
+
<circle class="tel-bezel-inner" cx="135" cy="135" r="82" />
|
|
53
|
+
<circle class="tel-dial-face" cx="135" cy="135" r="80" />
|
|
54
|
+
|
|
55
|
+
<g id="pusher-start" class="tel-svg-pusher" transform="translate(135, 135) rotate(-33)">
|
|
56
|
+
<rect x="110" y="-5" width="12" height="10" fill="url(#tel-metal-grad-dark)" stroke="#374151" stroke-width="0.5" />
|
|
57
|
+
<rect x="122" y="-7" width="6" height="14" rx="1.5" fill="url(#tel-metal-grad)" stroke="#1f2937" stroke-width="0.5" />
|
|
58
|
+
<rect x="100" y="-20" width="35" height="40" fill="transparent" style="cursor: pointer;" />
|
|
59
|
+
</g>
|
|
60
|
+
|
|
61
|
+
<g id="pusher-reset" class="tel-svg-pusher" transform="translate(135, 135) rotate(33)">
|
|
62
|
+
<rect x="110" y="-5" width="12" height="10" fill="url(#tel-metal-grad-dark)" stroke="#374151" stroke-width="0.5" />
|
|
63
|
+
<rect x="122" y="-7" width="6" height="14" rx="1.5" fill="url(#tel-metal-grad)" stroke="#1f2937" stroke-width="0.5" />
|
|
64
|
+
<rect x="100" y="-20" width="35" height="40" fill="transparent" style="cursor: pointer;" />
|
|
65
|
+
</g>
|
|
66
|
+
|
|
67
|
+
<g class="tel-svg-crown" transform="translate(135, 135)">
|
|
68
|
+
<rect x="110" y="-6" width="8" height="12" fill="url(#tel-metal-grad-dark)" stroke="#374151" stroke-width="0.5" />
|
|
69
|
+
<rect x="118" y="-10" width="8" height="20" rx="1.5" fill="url(#tel-metal-grad)" stroke="#1f2937" stroke-width="0.5" />
|
|
70
|
+
<line x1="120" y1="-8" x2="120" y2="8" stroke="#4b5563" stroke-width="0.5" />
|
|
71
|
+
<line x1="122" y1="-8" x2="122" y2="8" stroke="#4b5563" stroke-width="0.5" />
|
|
72
|
+
<line x1="124" y1="-8" x2="124" y2="8" stroke="#4b5563" stroke-width="0.5" />
|
|
73
|
+
</g>
|
|
74
|
+
|
|
75
|
+
<text class="tel-dial-brand" x="135" y="105" text-anchor="middle">TELEMETER</text>
|
|
76
|
+
<text class="tel-dial-subbrand" x="135" y="114" text-anchor="middle">KM (OUTER) & MI (INNER)</text>
|
|
77
|
+
|
|
78
|
+
<g id="tel-ticks"></g>
|
|
79
|
+
|
|
80
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(17.5, 135, 135)">1k</text>
|
|
81
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(35, 135, 135)">2k</text>
|
|
82
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(52.4, 135, 135)">3k</text>
|
|
83
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(70, 135, 135)">4k</text>
|
|
84
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(87.4, 135, 135)">5k</text>
|
|
85
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(174.9, 135, 135)">10k</text>
|
|
86
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(262, 135, 135)">15k</text>
|
|
87
|
+
<text class="tel-bezel-label-km" x="135" y="44" transform="rotate(349.5, 135, 135)">20k</text>
|
|
88
|
+
|
|
89
|
+
<text class="tel-bezel-label-mi" x="135" y="60" transform="rotate(28.1, 135, 135)">1m</text>
|
|
90
|
+
<text class="tel-bezel-label-mi" x="135" y="60" transform="rotate(56.3, 135, 135)">2m</text>
|
|
91
|
+
<text class="tel-bezel-label-mi" x="135" y="60" transform="rotate(84.4, 135, 135)">3m</text>
|
|
92
|
+
<text class="tel-bezel-label-mi" x="135" y="60" transform="rotate(112.5, 135, 135)">4m</text>
|
|
93
|
+
<text class="tel-bezel-label-mi" x="135" y="60" transform="rotate(140.7, 135, 135)">5m</text>
|
|
94
|
+
<text class="tel-bezel-label-mi" x="135" y="60" transform="rotate(281.4, 135, 135)">10m</text>
|
|
95
|
+
|
|
96
|
+
<text class="tel-helper-text" x="175" y="16" text-anchor="middle">START/STOP</text>
|
|
97
|
+
<path d="M 180 20 Q 220 20 236 50" fill="none" stroke="var(--accent)" stroke-width="1.25" stroke-dasharray="2,2" marker-end="url(#tel-arrow)" />
|
|
98
|
+
|
|
99
|
+
<text class="tel-helper-text tel-helper-text-gray" x="175" y="258" text-anchor="middle">RESET</text>
|
|
100
|
+
<path d="M 180 254 Q 220 254 236 224" fill="none" stroke="var(--text-base)" stroke-opacity="0.4" stroke-width="1.25" stroke-dasharray="2,2" marker-end="url(#tel-arrow-gray)" />
|
|
101
|
+
|
|
102
|
+
<text class="tel-digital-time" id="dial-digital-time" x="135" y="170" text-anchor="middle">00.0s</text>
|
|
103
|
+
<text class="tel-ambient-indicator" id="dial-ambient-indicator" x="135" y="190" text-anchor="middle">20.0°C</text>
|
|
104
|
+
|
|
105
|
+
<g class="tel-seconds-hand-group" id="seconds-hand-group">
|
|
106
|
+
<line class="tel-seconds-hand" x1="135" y1="135" x2="135" y2="40" />
|
|
107
|
+
<line class="tel-seconds-hand-counterweight" x1="135" y1="135" x2="135" y2="148" />
|
|
108
|
+
<circle class="tel-hand-center" cx="135" cy="135" r="5" />
|
|
109
|
+
<circle class="tel-center-pin" cx="135" cy="135" r="2" />
|
|
110
|
+
</g>
|
|
111
|
+
</svg>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="tel-controls-section">
|
|
116
|
+
<div class="tel-trigger-dashboard">
|
|
117
|
+
<button type="button" class="tel-trigger-pad pad-flash" id="btn-flash">
|
|
118
|
+
<div class="pad-icon-wrap">
|
|
119
|
+
<svg class="pad-icon" viewBox="0 0 24 24" width="18" height="18">
|
|
120
|
+
<path d="M7 2v11h3v9l7-12h-4l4-8H7z" fill="currentColor"></path>
|
|
121
|
+
</svg>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="pad-content">
|
|
124
|
+
<span class="pad-title">{labels.triggerFlash}</span>
|
|
125
|
+
<span class="pad-desc">See visual event</span>
|
|
126
|
+
</div>
|
|
127
|
+
</button>
|
|
128
|
+
|
|
129
|
+
<button type="button" class="tel-trigger-pad pad-sound" id="btn-sound" disabled>
|
|
130
|
+
<div class="pad-icon-wrap">
|
|
131
|
+
<svg class="pad-icon" viewBox="0 0 24 24" width="18" height="18">
|
|
132
|
+
<path d="M14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.07-.91 7-4.49 7-8.77s-2.93-7.86-7-8.77zm2.5 8.77c0-1.77-1-3.29-2.5-4.03v8.05c1.5-.73 2.5-2.25 2.5-4.02zM3 9v6h4l5 5V4L7 9H3z" fill="currentColor"></path>
|
|
133
|
+
</svg>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="pad-content">
|
|
136
|
+
<span class="pad-title">{labels.triggerSound}</span>
|
|
137
|
+
<span class="pad-desc">Hear acoustic event</span>
|
|
138
|
+
</div>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="tel-reset-bar">
|
|
143
|
+
<button type="button" class="tel-reset-btn" id="btn-reset" disabled>
|
|
144
|
+
<svg viewBox="0 0 24 24" width="14" height="14">
|
|
145
|
+
<path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"></path>
|
|
146
|
+
</svg>
|
|
147
|
+
<span>{labels.reset}</span>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="tel-setting-panel">
|
|
152
|
+
<div class="tel-field">
|
|
153
|
+
<div class="tel-label-row">
|
|
154
|
+
<span class="tel-label">{labels.unitSystem}</span>
|
|
155
|
+
<div class="tel-unit-chips">
|
|
156
|
+
<button type="button" class="tel-chip-btn active" data-unit="metric">{labels.metric}</button>
|
|
157
|
+
<button type="button" class="tel-chip-btn" data-unit="imperial">{labels.imperial}</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="tel-field">
|
|
163
|
+
<div class="tel-label-row">
|
|
164
|
+
<span class="tel-label">{labels.temperature}</span>
|
|
165
|
+
<span class="tel-label-value" id="temp-display">20°C</span>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="tel-slider-wrapper">
|
|
168
|
+
<input type="range" class="tel-slider" id="temp-slider" min="-20" max="50" value="20" step="1" />
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="tel-field">
|
|
173
|
+
<span class="tel-label">{labels.speedOfSound}</span>
|
|
174
|
+
<span class="tel-speed-badge" id="speed-display">343.3 m/s</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="tel-results-panel">
|
|
179
|
+
<div class="tel-results-primary">
|
|
180
|
+
<span class="tel-label">{labels.distanceResult}</span>
|
|
181
|
+
<span class="tel-results-val" id="result-primary">0.00 km</span>
|
|
182
|
+
<span class="tel-results-subval" id="result-secondary">0 m</span>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="tel-results-details">
|
|
185
|
+
<div class="tel-detail-item">
|
|
186
|
+
<span class="tel-detail-lbl">{labels.elapsedTime}</span>
|
|
187
|
+
<span class="tel-detail-val" id="result-time">0.00 s</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="tel-steps-section">
|
|
193
|
+
<div class="tel-step-row">
|
|
194
|
+
<div class="tel-step-marker">1</div>
|
|
195
|
+
<span class="tel-step-text">{labels.step1}</span>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="tel-step-row">
|
|
198
|
+
<div class="tel-step-marker">2</div>
|
|
199
|
+
<span class="tel-step-text">{labels.step2}</span>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="tel-step-row">
|
|
202
|
+
<div class="tel-step-marker">3</div>
|
|
203
|
+
<span class="tel-step-text">{labels.step3}</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="tel-tip-row">
|
|
208
|
+
<svg class="tel-tip-icon" viewBox="0 0 24 24" width="16" height="16">
|
|
209
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"></path>
|
|
210
|
+
</svg>
|
|
211
|
+
<span class="tel-tip-text">{labels.tipContent}</span>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div class="tel-history-section">
|
|
217
|
+
<div class="tel-history-header">
|
|
218
|
+
<h4 class="tel-history-title">{labels.historyTitle}</h4>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="tel-history-list" id="history-list">
|
|
221
|
+
<div class="tel-empty-history" id="empty-history">{labels.noHistory}</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export type TelemeterCalculatorUI = {
|
|
4
|
+
title: string;
|
|
5
|
+
triggerFlash: string;
|
|
6
|
+
triggerSound: string;
|
|
7
|
+
stop: string;
|
|
8
|
+
reset: string;
|
|
9
|
+
settings: string;
|
|
10
|
+
unitSystem: string;
|
|
11
|
+
metric: string;
|
|
12
|
+
imperial: string;
|
|
13
|
+
temperature: string;
|
|
14
|
+
speedOfSound: string;
|
|
15
|
+
distanceResult: string;
|
|
16
|
+
elapsedTime: string;
|
|
17
|
+
historyTitle: string;
|
|
18
|
+
noHistory: string;
|
|
19
|
+
sec: string;
|
|
20
|
+
km: string;
|
|
21
|
+
m: string;
|
|
22
|
+
mi: string;
|
|
23
|
+
ft: string;
|
|
24
|
+
step1: string;
|
|
25
|
+
step2: string;
|
|
26
|
+
step3: string;
|
|
27
|
+
tipTitle: string;
|
|
28
|
+
tipContent: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type TelemeterCalculatorLocaleContent = ToolLocaleContent<TelemeterCalculatorUI>;
|
|
32
|
+
|
|
33
|
+
export const telemeterCalculator: ChronoToolEntry<TelemeterCalculatorUI> = {
|
|
34
|
+
id: 'telemeter-calculator',
|
|
35
|
+
icons: { bg: 'mdi:weather-lightning', fg: 'mdi:waveform' },
|
|
36
|
+
i18n: {
|
|
37
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
38
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
39
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
40
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
41
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
42
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
43
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
44
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
45
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
46
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
47
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
48
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
49
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
50
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
51
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
52
|
+
},
|
|
53
|
+
};
|