fuzzyworld

nuts / files / src / vibration.ts

vibration.ts

Blame: the commit that last changed each line · file · history

44eb5832 2026-04-12 Pavel Safronov1import { CONFIG } from './config'
f1262fd9 2026-04-12 Pavel Safronov2import { dbg } from './debug'
44eb5832 2026-04-12 Pavel Safronov3
4let lastVibrationAt = 0
f1262fd9 2026-04-12 Pavel Safronov5let unlocked = false
4ea6baf7 2026-04-12 Pavel Safronov6let vibrateEnabled = true
98ba5504 2026-04-13 Pavel Safronov7let vibrateItems = true
8let vibrateEdges = true
f1262fd9 2026-04-12 Pavel Safronov9
4ea6baf7 2026-04-12 Pavel Safronov10export function setVibrateEnabled(on: boolean): void {
11 vibrateEnabled = on
12}
13
14export function isVibrateEnabled(): boolean {
15 return vibrateEnabled
16}
17
98ba5504 2026-04-13 Pavel Safronov18export 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
f1262fd9 2026-04-12 Pavel Safronov23/**
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}
44eb5832 2026-04-12 Pavel Safronov61
62/** Fire the Web Vibration API, with a minimum interval to avoid spam. */
63export function vibrate(durationMs: number): void {
98ba5504 2026-04-13 Pavel Safronov64 if (!vibrateItems) return
44eb5832 2026-04-12 Pavel Safronov65 const now = Date.now()
66 if (now - lastVibrationAt < CONFIG.vibrationCooldownMs) return
f1262fd9 2026-04-12 Pavel Safronov67 // Chrome won't vibrate if the page is backgrounded or display is off
68 if (document.visibilityState !== 'visible') return
44eb5832 2026-04-12 Pavel Safronov69 lastVibrationAt = now
4ea6baf7 2026-04-12 Pavel Safronov70 if (!vibrateEnabled) return
f1262fd9 2026-04-12 Pavel Safronov71 if (canVibrate()) {
72 const ok = navigator.vibrate(durationMs)
73 dbg('vibrate', durationMs, 'ms ->', ok)
74 }
44eb5832 2026-04-12 Pavel Safronov75}
76
98ba5504 2026-04-13 Pavel Safronov77export 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
44eb5832 2026-04-12 Pavel Safronov87/** 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
4ea6baf7 2026-04-12 Pavel Safronov92 if (!vibrateEnabled) return
f1262fd9 2026-04-12 Pavel Safronov93 if (canVibrate()) {
94 navigator.vibrate([25, 15, 25])
95 }
44eb5832 2026-04-12 Pavel Safronov96}