fuzzyworld

north-star / commit

adding a calibrate north button

30e810b37a568cf93a57a74ebbf71ec9e03a7fd0 · Pavel Safronov · 2026-03-14 20:06:45 -0700 · parents f7854852fcc0

adding a calibrate north button
 src/app.js     | 38 ++++++++++++++++++++++++++++++++++----
 src/index.html |  7 +++++--
 src/style.css  | 14 +++++++++++---
 3 files changed, 50 insertions(+), 9 deletions(-)

diff --git a/src/app.js b/src/app.js
index e05b4d9..ba73a95 100644
--- a/src/app.js
+++ b/src/app.js
@@ -13,6 +13,7 @@ const selfColorInput = document.getElementById('self-color-input');
 const roomCurrent = document.getElementById('room-current');
 const inviteRoomLink = document.getElementById('invite-room-link');
 const joinRoomLink = document.getElementById('join-room-link');
+const calibrateHeadingLink = document.getElementById('calibrate-heading-link');
 const inviteRoomStatus = document.getElementById('invite-room-status');
 const createRoomLink = document.getElementById('create-room-link');
 const participantsPanel = document.getElementById('participants-panel');
@@ -25,9 +26,17 @@ const arrow = document.getElementById('default-arrow');
 document.head.appendChild(Object.assign(document.createElement("link"), { rel: "icon", href: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>" + arrow.innerText + "</text></svg>" }))
 const getShortestDelta = (fromAngle, toAngle) => ((toAngle - fromAngle + 540) % 360) - 180;
 const normalizeAngle = (angle) => ((angle % 360) + 360) % 360;
+const normalizeSignedAngle = (angle) => {
+    const normalized = normalizeAngle(angle);
+    return normalized > 180 ? normalized - 360 : normalized;
+};
 const RENDER_INTERVAL_MS = 180;
 const LOCATION_SYNC_INTERVAL_MS = 3000;
-const HEADING_CORRECTION_DEG = 0;
+const HEADING_CORRECTION_STORAGE_KEY = 'north-star-heading-correction-deg';
+let headingCorrectionDeg = (() => {
+    const parsed = Number(localStorage.getItem(HEADING_CORRECTION_STORAGE_KEY));
+    return Number.isFinite(parsed) ? parsed : 0;
+})();
 let GIT_COMMIT = 'local-dev';
 try {
     const commitModule = await import('./commit.js');
@@ -75,6 +84,7 @@ const selfPosition = {
 };
 // Heading, assume north orientation by default, but if we get a heading from the device, use that to rotate the arrow accordingly
 let selfHeading = 0;
+let lastRawHeading = null;
 
 const firebaseConfig = {
     apiKey: "AIzaSyA4G7e13Gt1XAIds1Y63X_RFw-2O48iCLM",
@@ -217,6 +227,24 @@ if (joinRoomLink) {
     });
 }
 
+if (calibrateHeadingLink) {
+    calibrateHeadingLink.addEventListener('click', (event) => {
+        event.preventDefault();
+
+        if (lastRawHeading === null) {
+            alert('Heading sensor is not ready yet. Try again in a moment.');
+            return;
+        }
+
+        // Make current phone-forward direction equal to north (0 deg).
+        headingCorrectionDeg = normalizeSignedAngle(-lastRawHeading);
+        localStorage.setItem(HEADING_CORRECTION_STORAGE_KEY, String(headingCorrectionDeg));
+        selfHeading = normalizeAngle(lastRawHeading + headingCorrectionDeg);
+
+        setInviteStatus('North calibrated');
+    });
+}
+
 const NAME_STORAGE_KEY = 'north-star-self-name';
 const COLOR_STORAGE_KEY = 'north-star-self-color';
 const PARTICIPANT_VISIBILITY_STORAGE_KEY = 'north-star-participant-visibility';
@@ -433,7 +461,8 @@ window.addEventListener('deviceorientationabsolute', (event) => {
     if (heading === null) return;
 
     hasAbsoluteOrientation = true;
-    selfHeading = normalizeAngle(heading + HEADING_CORRECTION_DEG);
+    lastRawHeading = heading;
+    selfHeading = normalizeAngle(heading + headingCorrectionDeg);
 });
 
 window.addEventListener('deviceorientation', (event) => {
@@ -442,7 +471,8 @@ window.addEventListener('deviceorientation', (event) => {
     const heading = getHeadingFromEvent(event);
     if (heading === null) return;
 
-    selfHeading = normalizeAngle(heading + HEADING_CORRECTION_DEG);
+    lastRawHeading = heading;
+    selfHeading = normalizeAngle(heading + headingCorrectionDeg);
 });
 
 const participantsRef = ref(db, `rooms/${roomId}/participants`);
@@ -644,7 +674,7 @@ const render = () => {
     document.body.style.backgroundColor = selfParticipant.color;
 
     // output debug info
-    logDebug(`Name: ${selfParticipant.name} | Position: ${selfParticipant.position.lat.toFixed(1)}, ${selfParticipant.position.lon.toFixed(1)} | Heading: ${selfHeading.toFixed(2)}° | Real participants: ${participants.length} | Commit: ${GIT_COMMIT}`);
+    logDebug(`Name: ${selfParticipant.name} | Position: ${selfParticipant.position.lat.toFixed(1)}, ${selfParticipant.position.lon.toFixed(1)} | Heading: ${selfHeading.toFixed(2)}° | Corr: ${headingCorrectionDeg.toFixed(1)}° | Real participants: ${participants.length} | Commit: ${GIT_COMMIT}`);
 };
 
 await syncSelfParticipant();
diff --git a/src/index.html b/src/index.html
index 54861a6..67ab0ed 100644
--- a/src/index.html
+++ b/src/index.html
@@ -25,8 +25,11 @@
     </form>
     <div id="default-arrow" style="display: none;" class="arrow">⇡</div>
     <div id="content">
-        <button id="participants-toggle" class="participants-toggle" type="button" aria-expanded="true">Hide
-            Participants</button>
+        <div class="panel-actions">
+            <button id="calibrate-heading-link" class="participants-toggle" type="button">Calibrate North</button>
+            <button id="participants-toggle" class="participants-toggle" type="button" aria-expanded="true">Hide
+                Participants</button>
+        </div>
         <aside id="participants-panel" class="participants-panel" aria-label="Participants"></aside>
     </div>
     <div>
diff --git a/src/style.css b/src/style.css
index f8a3c8a..d50226b 100644
--- a/src/style.css
+++ b/src/style.css
@@ -158,10 +158,18 @@ a {
     z-index: 1;
 }
 
-.participants-toggle {
+.panel-actions {
     position: absolute;
+    left: 10px;
     right: 10px;
     top: 8px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    z-index: 4;
+}
+
+.participants-toggle {
     padding: 6px 10px;
     border: 0;
     border-radius: 8px;
@@ -169,7 +177,6 @@ a {
     color: #f8fafc;
     font-size: 12px;
     font-weight: 700;
-    z-index: 4;
 }
 
 .participants-panel {
@@ -291,7 +298,8 @@ a {
 }
 
 @media (max-width: 720px) {
-    .participants-toggle {
+    .panel-actions {
+        left: 6px;
         right: 6px;
         top: 6px;
     }