fuzzyworld

north-star / commit

room links and trying to fix heading issue

e2d68fe2d072b6a33cf7b0670de3d97a33e990a8 · Pavel Safronov · 2026-03-14 13:34:56 -0700 · parents b7d4b972f5d2

room links and trying to fix heading issue
 src/app.js     | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 src/index.html |   5 ++-
 2 files changed, 111 insertions(+), 7 deletions(-)

diff --git a/src/app.js b/src/app.js
index 9abc076..488f6da 100644
--- a/src/app.js
+++ b/src/app.js
@@ -13,6 +13,7 @@ const selfNameInput = document.getElementById('self-name-input');
 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 inviteRoomStatus = document.getElementById('invite-room-status');
 const createRoomLink = document.getElementById('create-room-link');
 const logDebug = (message) => {
@@ -22,6 +23,7 @@ const logDebug = (message) => {
 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;
 
 /**
  * @typedef {Object} Position
@@ -93,6 +95,7 @@ if (roomCurrent) {
 }
 
 const BASE_ROOM_URL = `${window.location.origin}${window.location.pathname}`;
+const ROOM_ID_PATTERN = /^[a-f0-9]{8}$/i;
 if (inviteRoomLink) {
     inviteRoomLink.href = `${BASE_ROOM_URL}#room=${encodeURIComponent(roomId)}`;
 }
@@ -139,6 +142,30 @@ const copyTextToClipboard = async (text) => {
     }
 };
 
+const parseRoomIdFromInput = (value) => {
+    const input = (value || '').trim();
+    if (!input) return null;
+
+    if (ROOM_ID_PATTERN.test(input)) {
+        return input.toLowerCase();
+    }
+
+    try {
+        const parsedUrl = new URL(input, window.location.href);
+        const hashValue = parsedUrl.hash.startsWith('#') ? parsedUrl.hash.slice(1) : parsedUrl.hash;
+        const hashParams = new URLSearchParams(hashValue);
+        const fromHash = (hashParams.get('room') || '').trim();
+
+        if (ROOM_ID_PATTERN.test(fromHash)) {
+            return fromHash.toLowerCase();
+        }
+    } catch (err) {
+        return null;
+    }
+
+    return null;
+};
+
 if (inviteRoomLink) {
     inviteRoomLink.addEventListener('click', async (event) => {
         event.preventDefault();
@@ -153,6 +180,30 @@ if (inviteRoomLink) {
     });
 }
 
+if (joinRoomLink) {
+    joinRoomLink.addEventListener('click', (event) => {
+        event.preventDefault();
+
+        const roomInput = window.prompt('Enter room ID or room URL');
+        if (roomInput === null) {
+            return;
+        }
+
+        const parsedRoomId = parseRoomIdFromInput(roomInput);
+        if (!parsedRoomId) {
+            alert('Invalid room ID or URL. Please enter an 8-character hexadecimal room ID or a URL containing it.');
+            return;
+        }
+
+        if (parsedRoomId === roomId.toLowerCase()) {
+            alert('You are already in this room!');
+            return;
+        }
+
+        window.location.assign(`${BASE_ROOM_URL}#room=${encodeURIComponent(parsedRoomId)}`);
+    });
+}
+
 const NAME_STORAGE_KEY = 'north-star-self-name';
 const COLOR_STORAGE_KEY = 'north-star-self-color';
 const normalizeName = (value) => (value || '').trim().slice(0, 32);
@@ -272,10 +323,62 @@ navigator.geolocation.watchPosition(async (pos) => {
     console.error('Error getting geolocation', err);
 });
 window.addEventListener('deviceorientationabsolute', (event) => {
-    if (event.absolute && event.alpha !== null) {
-        selfHeading = event.alpha;
-        render();
-    }
+    // Some browsers expose absolute compass heading as webkitCompassHeading.
+    // Others expose alpha that needs conversion and screen-orientation compensation.
+    const getScreenOrientationAngle = () => {
+        if (window.screen?.orientation && typeof window.screen.orientation.angle === 'number') {
+            return window.screen.orientation.angle;
+        }
+        if (typeof window.orientation === 'number') {
+            return window.orientation;
+        }
+        return 0;
+    };
+
+    const getHeadingFromEvent = (orientationEvent) => {
+        if (typeof orientationEvent.webkitCompassHeading === 'number') {
+            return normalizeAngle(orientationEvent.webkitCompassHeading);
+        }
+        if (orientationEvent.alpha === null) {
+            return null;
+        }
+        // Convert alpha to compass-style heading (clockwise from North).
+        return normalizeAngle(360 - orientationEvent.alpha);
+    };
+
+    const heading = getHeadingFromEvent(event);
+    if (heading === null) return;
+
+    selfHeading = normalizeAngle(heading + getScreenOrientationAngle());
+    render();
+});
+
+window.addEventListener('deviceorientation', (event) => {
+    const getScreenOrientationAngle = () => {
+        if (window.screen?.orientation && typeof window.screen.orientation.angle === 'number') {
+            return window.screen.orientation.angle;
+        }
+        if (typeof window.orientation === 'number') {
+            return window.orientation;
+        }
+        return 0;
+    };
+
+    const getHeadingFromEvent = (orientationEvent) => {
+        if (typeof orientationEvent.webkitCompassHeading === 'number') {
+            return normalizeAngle(orientationEvent.webkitCompassHeading);
+        }
+        if (orientationEvent.alpha === null) {
+            return null;
+        }
+        return normalizeAngle(360 - orientationEvent.alpha);
+    };
+
+    const heading = getHeadingFromEvent(event);
+    if (heading === null) return;
+
+    selfHeading = normalizeAngle(heading + getScreenOrientationAngle());
+    render();
 });
 
 const participantsRef = ref(db, `rooms/${roomId}/participants`);
@@ -372,7 +475,7 @@ const render = () => {
         const rawAngle = Math.atan2(dy, dx) * (180 / Math.PI) + 90;
 
         // Consider the heading of the device to rotate the arrow accordingly
-        const targetAngle = (rawAngle - selfHeading + 360) % 360;
+        const targetAngle = normalizeAngle(rawAngle - selfHeading);
 
         let currentArrowAngle = existingArrow.currentAngle;
         if (currentArrowAngle === null) {
diff --git a/src/index.html b/src/index.html
index b5bee25..8b8a0bf 100644
--- a/src/index.html
+++ b/src/index.html
@@ -14,8 +14,9 @@
     <p id="room-current" class="room-current"></p>
     <p class="room-links">
         <a id="invite-room-link" href="#">Invite to Room</a>
-        <span id="invite-room-status" class="room-link-status" aria-live="polite"></span>
-
+        <span aria-hidden="true">·</span>
+        <a id="join-room-link" href="#">Join Room</a>
+        <span aria-hidden="true">·</span>
         <a id="create-room-link" href="https://pavelsafronov.gitlab.io/north-star/">Create New Room</a>
     </p>
     <form id="name-form" class="name-form" autocomplete="off">