one
Version:
One is a React framework for web, iOS, and Android apps.
167 lines (165 loc) • 5.33 kB
text/typescript
import { validateListener } from './validate'
import type {
MotionAvailability,
MotionReading,
MotionSensor,
MotionVector,
} from '../specs/OneMotion.nitro'
export type {
MotionAvailability,
MotionReading,
MotionSensor,
MotionVector,
} from '../specs/OneMotion.nitro'
const emptyAvailability: MotionAvailability = {
accelerometer: false,
gyroscope: false,
magnetometer: false,
deviceMotion: false,
}
const availability = { ...emptyAvailability }
const listeners = new Set<{
sensor: MotionSensor
intervalMs: number
last: number
onReading: (reading: MotionReading) => void
onError: (code: string, message: string) => void
}>()
let observing = false
let attitude: MotionVector | undefined
function vector(
x: number | null | undefined,
y: number | null | undefined,
z: number | null | undefined,
scale = 1
): MotionVector | undefined {
return [x, y, z].every((value) => typeof value === 'number' && Number.isFinite(value))
? { x: x! * scale, y: y! * scale, z: z! * scale }
: undefined
}
function receive(event: DeviceMotionEvent): void {
const total = event.accelerationIncludingGravity,
linear = event.acceleration,
rate = event.rotationRate
const acceleration = vector(total?.x, total?.y, total?.z, -1 / 9.80665)
const user = vector(linear?.x, linear?.y, linear?.z, -1 / 9.80665)
const rotation = vector(rate?.beta, rate?.gamma, rate?.alpha, Math.PI / 180)
availability.accelerometer = !!acceleration
availability.gyroscope = !!rotation
availability.deviceMotion = !!user
const gravity =
acceleration && user
? {
x: acceleration.x - user.x,
y: acceleration.y - user.y,
z: acceleration.z - user.z,
}
: undefined
for (const listener of [...listeners]) {
if (!listeners.has(listener)) continue
const value =
listener.sensor === 'accelerometer'
? acceleration
: listener.sensor === 'gyroscope'
? rotation
: listener.sensor === 'deviceMotion'
? user
: undefined
if (!value || event.timeStamp - listener.last < listener.intervalMs) continue
listener.last = event.timeStamp
const reading: MotionReading = {
sensor: listener.sensor,
timestampMs: performance.timeOrigin + event.timeStamp,
value,
}
if (listener.sensor === 'deviceMotion')
Object.assign(reading, {
gravity,
userAcceleration: user,
rotationRate: rotation,
attitude,
})
listener.onReading(reading)
}
if (!listeners.size) stopObserving()
}
function orient(event: DeviceOrientationEvent): void {
// convert the browser's intrinsic z-x-y rotation to roll/pitch/yaw (z-y-x).
const a = event.alpha,
b = event.beta,
c = event.gamma
if (!vector(a, b, c)) {
attitude = undefined
return
}
const z = (a! * Math.PI) / 180,
x = (b! * Math.PI) / 180,
y = (c! * Math.PI) / 180
attitude = {
x: Math.atan2(Math.sin(x), Math.cos(x) * Math.cos(y)),
y: Math.asin(Math.max(-1, Math.min(1, Math.cos(x) * Math.sin(y)))),
z: Math.atan2(
Math.sin(z) * Math.cos(y) + Math.cos(z) * Math.sin(x) * Math.sin(y),
Math.cos(z) * Math.cos(y) - Math.sin(z) * Math.sin(x) * Math.sin(y)
),
}
}
function stopObserving(): void {
if (!observing) return
window.removeEventListener('devicemotion', receive)
window.removeEventListener('deviceorientation', orient)
observing = false
attitude = undefined
}
function observe(): void {
if (observing || typeof window === 'undefined') return
observing = true
window.addEventListener('devicemotion', receive)
window.addEventListener('deviceorientation', orient)
}
export const Motion = Object.freeze({
getAvailability: (): MotionAvailability => {
if (typeof window === 'undefined' || !window.DeviceMotionEvent)
return { ...emptyAvailability }
observe()
return { ...availability }
},
addListener: (
sensor: MotionSensor,
intervalMs: number,
onReading: (reading: MotionReading) => void,
onError: (code: string, message: string) => void
): (() => void) => {
validateListener(sensor, intervalMs, onReading, onError)
if (typeof window === 'undefined') return () => {}
if (sensor === 'magnetometer' || !window.DeviceMotionEvent) {
onError('E_MOTION_UNAVAILABLE', 'Motion.addListener: sensor is unavailable')
return () => {}
}
observe()
const listener = { sensor, intervalMs, last: -Infinity, onReading, onError }
listeners.add(listener)
const constructor = window.DeviceMotionEvent as typeof DeviceMotionEvent & {
requestPermission?: () => Promise<string>
}
if (constructor.requestPermission) {
const fail = (message: string) => {
if (!listeners.delete(listener)) return
if (!listeners.size) stopObserving()
onError('E_MOTION_PERMISSION', message)
}
try {
constructor.requestPermission().then((permission) => {
if (permission !== 'granted')
fail('Motion.addListener: motion permission is required')
}).catch((error) => fail(String(error)))
} catch (error) {
fail(String(error))
}
}
return () => {
listeners.delete(listener)
if (!listeners.size) stopObserving()
}
},
})