Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | 'use client' import { useEffect, useRef, useState } from 'react' export interface JoltEvent { directionX: number // normalized screen direction (-1 to 1) directionY: number // normalized screen direction (-1 to 1) magnitude: number // jerk magnitude (m/s^3) timestamp: number // performance.now() } export interface JoltConfig { joltThreshold: number // m/s^3, minimum jerk for a tip wobbleThreshold: number // m/s^3, minimum jerk for a visible wobble cooldownMs: number // minimum ms between jolt events tumbleThreshold: number // m/s^3, above this → chain 2 tips heavyTumbleThreshold: number // m/s^3, above this → chain 3 tips } export const DEFAULT_JOLT_CONFIG: JoltConfig = { joltThreshold: 25, wobbleThreshold: 12, cooldownMs: 200, tumbleThreshold: 50, heavyTumbleThreshold: 80, } /** * Detects sudden acceleration changes (jolts) via DeviceMotionEvent. * * Uses accelerationIncludingGravity to compute jerk (rate of change of acceleration). * When jerk exceeds the threshold and cooldown has elapsed, writes a JoltEvent to the ref. * Consumer reads from the ref each animation frame and clears it. */ export function useDeviceJolt(active: boolean, configOverrides?: Partial<JoltConfig>) { const joltRef = useRef<JoltEvent | null>(null) const [supported, setSupported] = useState(false) // Merge config with defaults — keep in a ref so the event handler always sees current values const configRef = useRef<JoltConfig>({ ...DEFAULT_JOLT_CONFIG, ...configOverrides }) configRef.current = { ...DEFAULT_JOLT_CONFIG, ...configOverrides } useEffect(() => { if (typeof window !== 'undefined' && 'DeviceMotionEvent' in window) { setSupported(true) } }, []) useEffect(() => { if (!active || typeof window === 'undefined') return const FILTER_ALPHA = 0.5 let prevFiltered = { x: 0, y: 0, z: 0 } let filtered = { x: 0, y: 0, z: 0 } let prevTime = 0 let lastJoltTime = 0 let initialized = false const handler = (e: DeviceMotionEvent) => { const accel = e.accelerationIncludingGravity if (!accel || accel.x == null || accel.y == null || accel.z == null) return const now = performance.now() const raw = { x: accel.x, y: accel.y, z: accel.z } // Low-pass filter const newFiltered = { x: filtered.x + (raw.x - filtered.x) * FILTER_ALPHA, y: filtered.y + (raw.y - filtered.y) * FILTER_ALPHA, z: filtered.z + (raw.z - filtered.z) * FILTER_ALPHA, } if (!initialized) { // Need at least 2 samples to compute jerk filtered = newFiltered prevFiltered = newFiltered prevTime = now initialized = true return } const dt = (now - prevTime) / 1000 // seconds if (dt <= 0) return // Jerk = d(acceleration)/dt const jerk = { x: (newFiltered.x - prevFiltered.x) / dt, y: (newFiltered.y - prevFiltered.y) / dt, z: (newFiltered.z - prevFiltered.z) / dt, } const magnitude = Math.sqrt(jerk.x * jerk.x + jerk.y * jerk.y + jerk.z * jerk.z) prevFiltered = filtered filtered = newFiltered prevTime = now const config = configRef.current // Check thresholds if (magnitude < config.wobbleThreshold) return if (now - lastJoltTime < config.cooldownMs) return // Project jerk onto screen plane: // Device x → screen x (positive = right) // Device y → screen y (but flip: device forward = negative screen y = up) const screenX = jerk.x const screenY = -jerk.y const screenMag = Math.sqrt(screenX * screenX + screenY * screenY) if (screenMag < 0.01) return lastJoltTime = now joltRef.current = { directionX: screenX / screenMag, directionY: screenY / screenMag, magnitude, timestamp: now, } // Auto-clear stale jolts after 50ms so consumers don't re-read setTimeout(() => { if (joltRef.current?.timestamp === now) { joltRef.current = null } }, 50) } window.addEventListener('devicemotion', handler) return () => window.removeEventListener('devicemotion', handler) }, [active]) return { joltRef, supported } } |