fuzzyworld

nuts / files / src / vibration.ts

vibration.ts

3.0 KB · history · blame

1import { CONFIG } from './config'
2import { dbg } from './debug'
3
4let lastVibrationAt = 0
5let unlocked = false
6let vibrateEnabled = true
7let vibrateItems = true
8let vibrateEdges = true
9
10export function setVibrateEnabled(on: boolean): void {
11 vibrateEnabled = on
12}
13
14export function isVibrateEnabled(): boolean {
15 return vibrateEnabled
16}
17
18export function setVibrateItems(on: boolean): void { vibrateItems = on }
19export function isVibrateItems(): boolean { return vibrateItems }
20export function setVibrateEdges(on: boolean): void { vibrateEdges = on }
21export function isVibrateEdges(): boolean { return vibrateEdges }
22
23/**
24 * Call once from a user-gesture handler (e.g. the tap-to-start).
25 * Chrome requires at least one vibrate() call from a gesture context before
26 * it will honour subsequent calls from rAF / event callbacks.
27 */
28export function unlockVibration(): void {
29 if (!('vibrate' in navigator && typeof navigator.vibrate === 'function')) {
30 dbg('vibrate: API not present')
31 return
32 }
33 navigator.vibrate(1)
34 unlocked = true
35 dbg('vibrate: unlocked')
36 showVibrateHint()
37}
38
39function showVibrateHint(): void {
40 const STORAGE_KEY = 'vibrate-hint-dismissed'
41 if (localStorage.getItem(STORAGE_KEY)) return
42
43 const toast = document.createElement('div')
44 toast.id = 'vibration-hint'
45 toast.textContent = '📳 Tip: turn off Do Not Disturb for haptics'
46 document.body.appendChild(toast)
47
48 function dismiss(): void {
49 localStorage.setItem(STORAGE_KEY, '1')
50 toast.classList.add('fade-out')
51 setTimeout(() => toast.remove(), 420)
52 }
53
54 toast.addEventListener('click', dismiss)
55 setTimeout(dismiss, 5000)
56}
57
58function canVibrate(): boolean {
59 return unlocked && 'vibrate' in navigator && typeof navigator.vibrate === 'function'
60}
61
62/** Fire the Web Vibration API, with a minimum interval to avoid spam. */
63export function vibrate(durationMs: number): void {
64 if (!vibrateItems) return
65 const now = Date.now()
66 if (now - lastVibrationAt < CONFIG.vibrationCooldownMs) return
67 // Chrome won't vibrate if the page is backgrounded or display is off
68 if (document.visibilityState !== 'visible') return
69 lastVibrationAt = now
70 if (!vibrateEnabled) return
71 if (canVibrate()) {
72 const ok = navigator.vibrate(durationMs)
73 dbg('vibrate', durationMs, 'ms ->', ok)
74 }
75}
76
77export function vibrateWall(durationMs: number): void {
78 if (!vibrateEdges) return
79 const now = Date.now()
80 if (now - lastVibrationAt < CONFIG.vibrationCooldownMs) return
81 if (document.visibilityState !== 'visible') return
82 lastVibrationAt = now
83 if (!vibrateEnabled) return
84 if (canVibrate()) navigator.vibrate(durationMs)
85}
86
87/** Short double-pulse for a shake event. */
88export function vibrateShake(): void {
89 const now = Date.now()
90 if (now - lastVibrationAt < CONFIG.vibrationCooldownMs) return
91 lastVibrationAt = now
92 if (!vibrateEnabled) return
93 if (canVibrate()) {
94 navigator.vibrate([25, 15, 25])
95 }
96}