north-star / commit
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;