Repository navigation
Expand file tree
/
Copy pathmain.js
More file actions
434 lines (375 loc) · 13.3 KB
/
Copy pathmain.js
File metadata and controls
434 lines (375 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
/*
Degrees What?
Geoff Pack, June 2023
https://github.com/virtualgeoff/degreeswhat
*/
/* jshint esversion: 6 */
/* globals degreesData */
// shortcuts
const $ = document.querySelector.bind(document);
const $All = document.querySelectorAll.bind(document);
const DegreesWhat = (function() {
'use strict';
const radius = 130;
const tau = 2 * Math.PI;
const minAngle = -9999.9;
const maxAngle = 9999.9;
let center,
angleDegrees = 0,
angleOffset = 0,
displayType, // 'compass' | 'protractor'
snap, // true | false
colorScheme, // 'light' | 'dark' | 'auto'
compass, compassCard, angleIndicator, compassOverlay,
// touch event throttling variables
isUpdating = false,
pendingPointerEvent = null;
function toDegrees(radians) {
return radians * 360 / tau;
}
function drawCompass() {
let h = parseFloat($('#degreeNumbers').getAttribute('font-size')),
textOffset = 4, // tweak text position
angle, scale;
// marks
const marks = [];
for (let i=0; i<360; i++) {
let length = 5;
if ((i % 5) === 0) { length = 9;}
if ((i % 15) === 0) { length = 15; }
marks.push(`<line x1="0" y1="${radius}" x2="0" y2="${radius-length}" transform="rotate(${i})" />`);
}
$('#degreeMarks').innerHTML = marks.join('');
// numbers
const numbers = [];
for (let i=0; i<360; i+=15) {
angle = i + 180;
numbers.push(`
<g x="0" y="0" transform="rotate(${angle}) translate(0,${radius - h * textOffset})">
<text x="0" y="0" transform="rotate(180)">${i}</text>
</g>`);
}
// points of the compass
const points = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
for (let i=0; i<16; i++) {
angle = (i * 360/16 + 180);
scale = 1.3;
if (i%2 === 0) { scale = 2; }
if ((i%4 === 0)) { scale = 3.1; }
numbers.push(`
<g x="0" y="0" transform="rotate(${angle}) translate(0,${radius - h * 2.5 * textOffset}) scale(${scale})">
<polygon points="0,8.5 1.5,6 -1.5,6" />
<text x="0" y="0" transform="rotate(180)" font-size="5px">${points[i]}</text>
</g>`);
}
$('#degreeNumbers').innerHTML = numbers.join('');
// adjust text position
$('#text1').setAttribute('transform', 'translate(0,-22)');
$('#text2').setAttribute('transform', 'translate(0, 22)');
// modify indicator
angleIndicator.setAttribute('transform', 'rotate(-90)');
$('#angleIndicator path').setAttribute('d', 'M 135,0 L 60,0');
$('#angleIndicator circle').setAttribute('r', '0');
}
function drawProtractor() {
let h = parseFloat($('#degreeNumbers').getAttribute('font-size')),
textOffset = 4, // tweak text position
length, angle, angle2, yPos;
// marks
const marks = [];
for (let i=0; i<360; i++) {
length = 5;
if ((i % 5) === 0) { length = 9;}
if ((i % 10) === 0) { length = radius-15; }
if ((i % 90) === 0) { length = radius; }
marks.push(`<line x1="0" y1="${radius}" x2="0" y2="${radius-length}" transform="rotate(${i})" />`);
}
$('#degreeMarks').innerHTML = marks.join('');
// numbers
const numbers = [];
for (let i=-170; i<=180; i+=10) {
angle = (i+90) * -1;
angle2 = ((i >= 0) && (i <= 180)) ? 180 : 0;
yPos = ((i >= 0) && (i <= 180)) ? 0 : (h*0.75);
numbers.push(`
<g x="0" y="0" transform="rotate(${angle}) translate(0,${radius - h * textOffset})">
<rect x="-1" y="${-h*0.7}" width="2" height="${h*2.1}" fill="#fff" />
<text x="0" y="${yPos}" transform="rotate(${angle2})">${i}</text>
</g>`);
}
$('#degreeNumbers').innerHTML = numbers.join('');
// adjust text position
$('#text1').setAttribute('transform', 'translate(0,-45)');
$('#text2').setAttribute('transform', 'translate(0, 45)');
// modify compass card and indicator
compassCard.setAttribute('transform', 'rotate(0)');
$('#angleIndicator path').setAttribute('d', 'M 0,0 L 135,0');
$('#angleIndicator circle').setAttribute('r', '2');
}
function updateInfo() {
// calculate display values for angles and temperature conversions
let D = angleDegrees.toFixed(1), // degrees (formatted)
d = Number(D), // degrees (numeric)
R = (angleDegrees/360 * tau).toFixed(2), // radians
T = (d/360).toFixed(2), // turns (revolutions)
G = (d * 10/9).toFixed(1), // gradians (gon)
C = ((d >= -273.15) ? D : '—'), // Celsius (or dash if below absolute zero)
CtoF = ((d >= -273.15) ? (d * 9/5 + 32).toFixed(1) : '—'), // Celsius to Fahrenheit
CtoK = ((d >= -273.15) ? (d + 273.15).toFixed(1) : '—'), // Celsius to Kelvin
CtoR = ((d >= -273.15) ? (d * 9/5 + 491.67).toFixed(1) : '—'), // Celsius to Rankine
F = ((d >= -459.67) ? D : '—'), // Fahrenheit (or dash if below absolute zero)
FtoC = ((d >= -459.67) ? ((d - 32) * 5/9).toFixed(1) : '—'), // Fahrenheit to Celsius
FtoK = ((d >= -459.67) ? ((d + 459.67) * 5/9).toFixed(1) : '—'), // Fahrenheit to Kelvin
FtoR = ((d >= -459.67) ? (d + 459.67).toFixed(1) : '—'); // Fahrenheit to Rankine
// big text
$('#textAngle').textContent = `${D}°`;
$('#textC').textContent = `${C} °C = `;
$('#textCtoF').textContent = `${CtoF} °F`;
$('#textF').textContent = `${F} °F = `;
$('#textFtoC').textContent = `${FtoC} °C`;
// info1
$('#info1 dl').innerHTML = `
<dt>${D}° = </dt>
<dd>${R} rad<br>
${G} gon<br>
${T} turns</dd>
<dt>${C} °C = </dt>
<dd>${CtoF} °F<br>
${CtoK} K<br>
${CtoR} °Ra</dd>
<dt>${F} °F = </dt>
<dd>${FtoC} °C<br>
${FtoK} K<br>
${FtoR} °Ra</dd>`;
// info2
if (degreesData) {
degreesData.showInfo(d);
}
}
function updateAngle() {
// limit angles to display limits
if (angleDegrees > maxAngle) { angleDegrees = maxAngle; }
if (angleDegrees < minAngle) { angleDegrees = minAngle; }
// snap to nearest degree?
if (snap) { angleDegrees = Math.round(angleDegrees); }
// adjust range
if (displayType === 'compass') {
angleDegrees = (angleDegrees + 360) % 360; // 0 <= angle < 360;
}
// rotate compass card or indicator line
if (displayType === 'compass') {
compassCard.setAttribute('transform', `rotate(${-angleDegrees})`);
} else {
angleIndicator.setAttribute('transform', `rotate(${-angleDegrees})`);
}
}
function setDisplayType(value) {
displayType = value;
if (value === 'compass') {
drawCompass();
} else {
drawProtractor();
}
updateAngle();
updateInfo();
}
function getPointerOffset(e) {
// get angle where the pointer entered the compass overlay and save as angleOffset
e.preventDefault();
// convert screen coordinates to angle (see below)
let pointerAngle = toDegrees(-1 * Math.atan2((e.offsetY-center.y), (e.offsetX-center.x)) );
if (displayType === 'compass') {
angleOffset = pointerAngle - 90 - angleDegrees;
} else {
angleOffset = 0; // not needed for protractor
}
}
function getPointerAngle(e) {
// get pointer position and calculate angle
e.preventDefault();
// convert screen coordinates to angle
// note atan2(y,x) gives the counterclockwise angle, in radians, between the +ve x-axis and the point (x,y)
// angle will be in range -π to π (-180° to 180°)
let pointerAngle = toDegrees(-1 * Math.atan2((e.offsetY-center.y), (e.offsetX-center.x)) );
if (displayType === 'compass') {
angleDegrees = pointerAngle - 90 - angleOffset;
} else {
angleDegrees = pointerAngle;
}
updateAngle();
updateInfo();
}
function throttledGetPointerAngle(e) {
// throttle pointer events using requestAnimationFrame for better performance on low-end devices
pendingPointerEvent = e;
// only schedule update if none is pending
if (!isUpdating) {
isUpdating = true;
requestAnimationFrame(() => {
if (pendingPointerEvent) {
getPointerAngle(pendingPointerEvent);
pendingPointerEvent = null;
}
isUpdating = false;
});
}
}
function getSize(e) {
//console.log(e);
center = {x:compass.clientWidth/2, y:compass.clientHeight/2};
}
function showSection(e) {
// hide all sections, show the one you want
$All('section').forEach( item => { item.style.display = 'none'; });
if ((window.location.hash) && $(window.location.hash)) { $(window.location.hash).style.display = 'block'; }
}
function decodeURL(anchor) {
// decodes data in data-address attribute of an anchor tag — used to obfuscate mailto link
let input = anchor.dataset.address.replace(/\s+/g, ',').split(',');
let output = '';
for (let i=0; i<input.length; i++) {
output += String.fromCodePoint(parseInt(input[i],16));
}
anchor.href = output;
}
function setItem(itemName, value) {
// save item to browser local storage with error handling
try {
// Test if localStorage is available
if (typeof Storage === 'undefined') {
alert('Settings could not be saved.');
return false;
}
localStorage.setItem(itemName, value);
return true;
} catch (error) {
// Handle quota exceeded, privacy mode, etc.
console.warn('Failed to save setting:', itemName, error.message);
alert('Settings could not be saved.');
return false;
}
}
function getItem(itemName) {
// get item from browser local storage with error handling
try {
if (typeof Storage === 'undefined') {
return null;
}
const item = localStorage.getItem(itemName);
return item ? JSON.parse(item) : null;
} catch (error) {
console.warn('Failed to load setting:', itemName, error.message);
return null;
}
}
function setOption(checkbox) {
// handle options checkboxes and radio buttons
switch (checkbox.name) {
case 'snap':
snap = checkbox.checked;
setItem('snap', JSON.stringify(checkbox.checked));
updateAngle();
updateInfo();
break;
case 'displayType':
setDisplayType(checkbox.value);
setItem('displayType', JSON.stringify(checkbox.value));
break;
case 'colorScheme':
colorScheme = checkbox.value;
setItem('colorScheme', JSON.stringify(checkbox.value));
updateColorScheme();
break;
default:
alert('wot?');
}
}
function loadOptions() {
// load options from localStorage, and set checkboxes, etc. on page load
// snap
snap = getItem('snap') || false;
$('input[name="snap"]').checked = (snap) ? true : false;
// displayType
displayType = getItem('displayType') || 'protractor';
setDisplayType(displayType);
$('#display_protractor').checked = (displayType === 'protractor') ? true : false;
$('#display_compass').checked = (displayType === 'compass') ? true : false;
// colorScheme
colorScheme = getItem('colorScheme') || 'auto';
updateColorScheme();
$('#scheme_auto').checked = (colorScheme === 'auto') ? true : false;
$('#scheme_light').checked = (colorScheme === 'light') ? true : false;
$('#scheme_dark').checked = (colorScheme === 'dark') ? true : false;
}
function updateColorScheme() {
document.documentElement.classList.remove('light', 'dark'); //reset values
if (colorScheme === 'light') {
document.documentElement.classList.add('light');
} else if (colorScheme === 'dark') {
document.documentElement.classList.add('dark');
}
}
function getAngleFromURL() {
// parse and validate URL parameter for initial angle
try {
if (!location.search) return 0;
const param = location.search.substring(1);
const angle = parseFloat(param);
// validate parameter is a number
if (isNaN(angle) || !isFinite(angle)) {
console.log(`Invalid angle parameter: ${param}`);
return 0;
}
// check inside limits
const clampedAngle = Math.max(minAngle, Math.min(maxAngle, angle));
if (clampedAngle !== angle) {
console.log(`Angle parameter out of range: ${angle}° clamped to ${clampedAngle}°`);
}
return clampedAngle;
} catch (error) {
alert(`Error parsing URL parameter: ${error.message}`);
return 0;
}
}
function init() {
compass = $('#compass');
compassCard = $('#compassCard');
angleIndicator = $('#angleIndicator');
compassOverlay = $('#compassOverlay');
loadOptions(); // from localStorage
getSize();
// get query string, update angle
angleDegrees = getAngleFromURL();
updateInfo(); // update info first, so can convert temps outside angle limits
updateAngle();
// mouse & touch (throttled for performance on low-end devices)
compassOverlay.addEventListener("pointermove", throttledGetPointerAngle);
compassOverlay.addEventListener("pointerover", getPointerOffset);
// make overlays, handle section links
$All('section').forEach(item => { item.classList.add('overlay'); }); // visible if JS disabled
if (window.location.hash) { showSection(); }
window.addEventListener('hashchange', showSection);
// decode email URL
$All('a[data-address]').forEach( (a) => { decodeURL(a); });
}
return {
init,
getSize,
setOption
};
})();
window.addEventListener('DOMContentLoaded', DegreesWhat.init);
window.addEventListener('resize', DegreesWhat.getSize);
// set up service worker for PWA
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("worker.js").then(
(registration) => {
console.log("Service worker registration successful");
},
(error) => {
console.error("Service worker registration failed:", error);
}
);
} else {
console.error("Service workers are not supported");
}