nuts / shampoo / files / src / vibration.ts
Branch shampoo · default branch · all branches
vibration.ts
44eb5832 2026-04-12 Pavel Safronov | 1 | import { CONFIG } from './config' |
f1262fd9 2026-04-12 Pavel Safronov | 2 | import { dbg } from './debug' |
44eb5832 2026-04-12 Pavel Safronov | 3 | |
| 4 | let lastVibrationAt = 0 | |
f1262fd9 2026-04-12 Pavel Safronov | 5 | let unlocked = false |
4ea6baf7 2026-04-12 Pavel Safronov | 6 | let vibrateEnabled = true |
98ba5504 2026-04-13 Pavel Safronov | 7 | let vibrateItems = true |
| 8 | let vibrateEdges = true | |
f1262fd9 2026-04-12 Pavel Safronov | 9 | |
4ea6baf7 2026-04-12 Pavel Safronov | 10 | export function setVibrateEnabled(on: boolean): void { |
| 11 | vibrateEnabled = on | |
| 12 | } | |
| 13 | ||
| 14 | export function isVibrateEnabled(): boolean { | |
| 15 | return vibrateEnabled | |
| 16 | } | |
| 17 | ||
98ba5504 2026-04-13 Pavel Safronov | 18 | export function setVibrateItems(on: boolean): void { vibrateItems = on } |
| 19 | export function isVibrateItems(): boolean { return vibrateItems } | |
| 20 | export function setVibrateEdges(on: boolean): void { vibrateEdges = on } | |
| 21 | export function isVibrateEdges(): boolean { return vibrateEdges } | |
| 22 | ||
f1262fd9 2026-04-12 Pavel Safronov | 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 | */ | |
| 28 | export 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 | ||
| 39 | function 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 | ||
| 58 | function canVibrate(): boolean { | |
| 59 | return unlocked && 'vibrate' in navigator && typeof navigator.vibrate === 'function' | |
| 60 | } | |
44eb5832 2026-04-12 Pavel Safronov | 61 | |
| 62 | /** Fire the Web Vibration API, with a minimum interval to avoid spam. */ | |
| 63 | export function vibrate(durationMs: number): void { | |
98ba5504 2026-04-13 Pavel Safronov | 64 | if (!vibrateItems) return |
44eb5832 2026-04-12 Pavel Safronov | 65 | const now = Date.now() |
| 66 | if (now - lastVibrationAt < CONFIG.vibrationCooldownMs) return | |
f1262fd9 2026-04-12 Pavel Safronov | 67 | // Chrome won't vibrate if the page is backgrounded or display is off |
| 68 | if (document.visibilityState !== 'visible') return | |
44eb5832 2026-04-12 Pavel Safronov | 69 | lastVibrationAt = now |
4ea6baf7 2026-04-12 Pavel Safronov | 70 | if (!vibrateEnabled) return |
f1262fd9 2026-04-12 Pavel Safronov | 71 | if (canVibrate()) { |
| 72 | const ok = navigator.vibrate(durationMs) | |
| 73 | dbg('vibrate', durationMs, 'ms ->', ok) | |
| 74 | } | |
44eb5832 2026-04-12 Pavel Safronov | 75 | } |
| 76 | ||
98ba5504 2026-04-13 Pavel Safronov | 77 | export 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 Safronov | 87 | /** Short double-pulse for a shake event. */ |
| 88 | export function vibrateShake(): void { | |
| 89 | const now = Date.now() | |
| 90 | if (now - lastVibrationAt < CONFIG.vibrationCooldownMs) return | |
| 91 | lastVibrationAt = now | |
4ea6baf7 2026-04-12 Pavel Safronov | 92 | if (!vibrateEnabled) return |
f1262fd9 2026-04-12 Pavel Safronov | 93 | if (canVibrate()) { |
| 94 | navigator.vibrate([25, 15, 25]) | |
| 95 | } | |
44eb5832 2026-04-12 Pavel Safronov | 96 | } |