fuzzyworld

north-star / commit

fixed up the orientation issues

2b712221710f3db67b5062cef6c902dba3d010f5 · Pavel Safronov · 2026-03-14 21:57:43 -0700 · parents 84bc58683b31

fixed up the orientation issues
 src/app.js    | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 src/style.css |  11 ++++
 2 files changed, 195 insertions(+), 5 deletions(-)

diff --git a/src/app.js b/src/app.js
index 349f50b..89813ee 100644
--- a/src/app.js
+++ b/src/app.js
@@ -141,13 +141,151 @@ if (createRoomLink) {
 const requiresExplicitCompassPermission =
     typeof DeviceOrientationEvent !== 'undefined' &&
     typeof DeviceOrientationEvent.requestPermission === 'function';
+const hasDeviceOrientationSupport = typeof DeviceOrientationEvent !== 'undefined';
+const compassDefinitelyUnsupported = !hasDeviceOrientationSupport;
 let compassEnabled = !requiresExplicitCompassPermission;
+let hasSeenOrientationEvent = false;
+let hasSeenHeadingValue = false;
+let compassButtonStatusLabel = '';
+let compassButtonStatusTitle = '';
+let compassButtonStatusError = false;
+let compassStatusTimer = null;
+let compassProbeTimer = null;
+const COMPASS_PROBE_TIMEOUT_MS = 2500;
+
+const getHeadingUnavailableMessage = () => {
+    if (compassDefinitelyUnsupported) {
+        return 'This browser/device does not expose orientation sensors, so a compass is unavailable here.';
+    }
+
+    if (requiresExplicitCompassPermission && !compassEnabled) {
+        return 'Compass access is not enabled yet. Tap Enable Compass first.';
+    }
+
+    if (!hasSeenOrientationEvent) {
+        return 'No orientation sensor data has been detected on this device. This setup likely has no compass/motion sensor available (common on desktop/laptop hardware).';
+    }
+
+    if (!hasSeenHeadingValue) {
+        return 'Orientation events are arriving, but compass heading is unavailable in this browser/device context.';
+    }
+
+    return 'Heading sensor data is not ready yet. Try again in a moment.';
+};
+
+const getHeadingUnavailableStatus = () => {
+    if (compassDefinitelyUnsupported) {
+        return {
+            label: 'Compass Unavailable',
+            title: 'This browser/device does not expose orientation sensors, so a compass is unavailable here.',
+            isError: true
+        };
+    }
+
+    if (requiresExplicitCompassPermission && !compassEnabled) {
+        return {
+            label: 'Enable Compass First',
+            title: 'Compass access is not enabled yet. Tap Enable Compass first.',
+            isError: true
+        };
+    }
+
+    if (!hasSeenOrientationEvent) {
+        return {
+            label: 'No Sensor Data',
+            title: 'No orientation sensor data has been detected on this device. This setup likely has no compass/motion sensor available (common on desktop/laptop hardware).',
+            isError: true
+        };
+    }
+
+    if (!hasSeenHeadingValue) {
+        return {
+            label: 'Heading Unavailable',
+            title: 'Orientation events are arriving, but compass heading is unavailable in this browser/device context.',
+            isError: true
+        };
+    }
+
+    return {
+        label: 'Sensor Warming Up',
+        title: 'Heading sensor data is not ready yet. Try again in a moment.',
+        isError: false
+    };
+};
+
+const clearCompassButtonStatus = () => {
+    if (compassStatusTimer) {
+        clearTimeout(compassStatusTimer);
+        compassStatusTimer = null;
+    }
+
+    compassButtonStatusLabel = '';
+    compassButtonStatusTitle = '';
+    compassButtonStatusError = false;
+};
+
+const clearCompassProbeTimer = () => {
+    if (!compassProbeTimer) return;
+    clearTimeout(compassProbeTimer);
+    compassProbeTimer = null;
+};
+
+const scheduleCompassProbe = () => {
+    clearCompassProbeTimer();
+
+    if (compassDefinitelyUnsupported) return;
+    if (!compassEnabled) return;
+    if (hasSeenOrientationEvent || hasSeenHeadingValue) return;
+
+    compassProbeTimer = setTimeout(() => {
+        if (hasSeenOrientationEvent || hasSeenHeadingValue) return;
+
+        const status = getHeadingUnavailableStatus();
+        showCompassButtonStatus({
+            label: status.label,
+            title: status.title,
+            isError: status.isError
+        });
+    }, COMPASS_PROBE_TIMEOUT_MS);
+};
+
+const showCompassButtonStatus = ({ label, title = '', isError = false, durationMs = 0 }) => {
+    if (compassStatusTimer) {
+        clearTimeout(compassStatusTimer);
+        compassStatusTimer = null;
+    }
+
+    compassButtonStatusLabel = label;
+    compassButtonStatusTitle = title;
+    compassButtonStatusError = isError;
+
+    if (durationMs > 0) {
+        compassStatusTimer = setTimeout(() => {
+            clearCompassButtonStatus();
+            updateCompassButtonState();
+        }, durationMs);
+    }
+
+    updateCompassButtonState();
+};
 
 const updateCompassButtonState = () => {
     if (!calibrateHeadingLink) return;
 
-    calibrateHeadingLink.textContent = compassEnabled ? 'Calibrate North' : 'Enable Compass';
+    if (compassDefinitelyUnsupported) {
+        calibrateHeadingLink.textContent = 'Compass Unavailable';
+        calibrateHeadingLink.classList.add('compass-required');
+        calibrateHeadingLink.classList.remove('compass-error');
+        calibrateHeadingLink.title = getHeadingUnavailableMessage();
+        calibrateHeadingLink.disabled = true;
+        return;
+    }
+
+    calibrateHeadingLink.disabled = false;
+    calibrateHeadingLink.textContent = compassButtonStatusLabel || (compassEnabled ? 'Calibrate North' : 'Enable Compass');
+    calibrateHeadingLink.title = compassButtonStatusTitle;
     calibrateHeadingLink.classList.toggle('compass-required', !compassEnabled);
+    calibrateHeadingLink.classList.toggle('compass-error', compassButtonStatusError);
 };
 
 const requestCompassPermission = async () => {
@@ -165,6 +303,7 @@ const requestCompassPermission = async () => {
 };
 
 updateCompassButtonState();
+scheduleCompassProbe();
 
 let inviteStatusTimer = null;
 const setInviteStatus = (message, isError = false) => {
@@ -266,18 +405,34 @@ if (calibrateHeadingLink) {
         if (!compassEnabled) {
             const granted = await requestCompassPermission();
             if (!granted) {
-                alert('Compass permission was denied. Enable Motion & Orientation Access in Safari settings and try again.');
+                showCompassButtonStatus({
+                    label: 'Compass Denied',
+                    title: 'Compass permission was denied. Enable Motion & Orientation Access in Safari settings and try again.',
+                    isError: true
+                });
                 return;
             }
 
             compassEnabled = true;
+            clearCompassButtonStatus();
             updateCompassButtonState();
-            setInviteStatus('Compass enabled');
+            scheduleCompassProbe();
+            showCompassButtonStatus({
+                label: 'Compass Enabled',
+                title: 'Compass access is active. You can now calibrate north.',
+                durationMs: 1500
+            });
             return;
         }
 
         if (lastRawHeading === null) {
-            alert('Heading sensor is not ready yet. Try again in a moment.');
+            const status = getHeadingUnavailableStatus();
+            showCompassButtonStatus({
+                label: status.label,
+                title: status.title,
+                isError: status.isError,
+                durationMs: status.isError ? 0 : 1500
+            });
             return;
         }
 
@@ -286,7 +441,11 @@ if (calibrateHeadingLink) {
         localStorage.setItem(HEADING_CORRECTION_STORAGE_KEY, String(headingCorrectionDeg));
         selfHeading = normalizeAngle(lastRawHeading + headingCorrectionDeg);
 
-        setInviteStatus('North calibrated');
+        showCompassButtonStatus({
+            label: 'North Calibrated',
+            title: 'Calibration applied.',
+            durationMs: 1500
+        });
         requestRender();
     });
 }
@@ -591,6 +750,8 @@ const getHeadingFromEvent = (orientationEvent) => {
 let hasAbsoluteOrientation = false;
 
 window.addEventListener('deviceorientationabsolute', (event) => {
+    clearCompassProbeTimer();
+    hasSeenOrientationEvent = true;
     const heading = getHeadingFromEvent(event);
     if (heading === null) return;
 
@@ -599,6 +760,11 @@ window.addEventListener('deviceorientationabsolute', (event) => {
         updateCompassButtonState();
     }
 
+    hasSeenHeadingValue = true;
+    if (compassButtonStatusError) {
+        clearCompassButtonStatus();
+        updateCompassButtonState();
+    }
     hasAbsoluteOrientation = true;
     lastRawHeading = heading;
     selfHeading = normalizeAngle(heading + headingCorrectionDeg);
@@ -608,6 +774,9 @@ window.addEventListener('deviceorientationabsolute', (event) => {
 window.addEventListener('deviceorientation', (event) => {
     if (hasAbsoluteOrientation) return;
 
+    clearCompassProbeTimer();
+    hasSeenOrientationEvent = true;
+
     const heading = getHeadingFromEvent(event);
     if (heading === null) return;
 
@@ -616,11 +785,21 @@ window.addEventListener('deviceorientation', (event) => {
         updateCompassButtonState();
     }
 
+    hasSeenHeadingValue = true;
+    if (compassButtonStatusError) {
+        clearCompassButtonStatus();
+        updateCompassButtonState();
+    }
     lastRawHeading = heading;
     selfHeading = normalizeAngle(heading + headingCorrectionDeg);
     requestRender();
 });
 
+document.addEventListener('visibilitychange', () => {
+    if (document.visibilityState !== 'visible') return;
+    scheduleCompassProbe();
+});
+
 const participantsRef = ref(db, `rooms/${roomId}/participants`);
 // onValue(participantsRef, (snapshot) => {
 //     const participants = snapshot.val(); // object of uid → participant
diff --git a/src/style.css b/src/style.css
index ed05958..888fcce 100644
--- a/src/style.css
+++ b/src/style.css
@@ -177,6 +177,12 @@ a {
     color: #f8fafc;
     font-size: 12px;
     font-weight: 700;
+    cursor: pointer;
+}
+
+.participants-toggle:disabled {
+    cursor: not-allowed;
+    opacity: 0.75;
 }
 
 .participants-toggle.compass-required {
@@ -184,6 +190,11 @@ a {
     box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.35);
 }
 
+.participants-toggle.compass-error {
+    border: 2px solid #f97316;
+    background: rgba(124, 45, 18, 0.85);
+}
+
 .participants-panel {
     position: absolute;
     right: 10px;