web-haptic-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright © 2026 Sumit Sahoo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,328 @@
1
+ <div align="center">
2
+
3
+ <h1>📳 Web Haptic Engine</h1>
4
+
5
+ <p>A cross-platform haptic feedback engine for the web.<br>
6
+ Supports Android vibration, iOS Taptic feedback, audio impulse synthesis, drag haptics, and 23 built-in presets.</p>
7
+
8
+ <p>
9
+ <a href="https://www.npmjs.com/package/web-haptic-engine"><img src="https://img.shields.io/npm/v/web-haptic-engine" alt="npm version" /></a>
10
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="MIT License" /></a>
11
+ <img src="https://img.shields.io/badge/platform-web-blue" alt="Platform" />
12
+ <img src="https://img.shields.io/badge/typescript-strict-blue" alt="TypeScript" />
13
+ </p>
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## ✨ Features
20
+
21
+ | Category | Details |
22
+ | -------------------------- | ------------------------------------------------------------------------------------------------------- |
23
+ | 📱 **Android Vibration** | Full `navigator.vibrate()` pattern support with intensity-scaled durations |
24
+ | 🍎 **iOS Taptic** | Exploits the `<input type="checkbox" switch>` toggle to trigger native Taptic Engine feedback |
25
+ | 🔊 **Audio Impulse Layer** | 8 synthesized AudioBuffer impulses (`tick`, `tap`, `thud`, `click`, `snap`, `buzz`, `confirm`, `harsh`) |
26
+ | 👆 **Drag Haptics** | Touchmove-driven haptic feedback with distance threshold — works reliably on iOS |
27
+ | 🎛️ **23 Presets** | Ready-to-use patterns: `success`, `warning`, `error`, `heartbeat`, `spring`, `buzz`, and more |
28
+ | 🔗 **Sequences** | Chain presets with delays, repeats, and custom gaps |
29
+ | 〰️ **Easing Functions** | `linear`, `easeIn`, `easeOut`, `easeInOut`, `bounce`, `spring` |
30
+ | 📦 **Zero Dependencies** | No external runtime dependencies — pure TypeScript |
31
+
32
+ ---
33
+
34
+ ## 📥 Installation
35
+
36
+ ```bash
37
+ npm install web-haptic-engine
38
+ ```
39
+
40
+ ```bash
41
+ pnpm add web-haptic-engine
42
+ ```
43
+
44
+ ```bash
45
+ yarn add web-haptic-engine
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 🚀 Quick Start
51
+
52
+ ### Basic Usage
53
+
54
+ ```ts
55
+ import { haptic } from "web-haptic-engine";
56
+
57
+ // Fire a preset
58
+ await haptic("success");
59
+ await haptic("heartbeat");
60
+ await haptic("click");
61
+
62
+ // Fire with custom intensity
63
+ await haptic("heavy", { intensity: 0.8 });
64
+
65
+ // Fire a raw duration (ms)
66
+ await haptic(50);
67
+ ```
68
+
69
+ ### Using the Engine
70
+
71
+ ```ts
72
+ import { HapticEngine } from "web-haptic-engine";
73
+
74
+ const engine = new HapticEngine({
75
+ throttleMs: 25,
76
+ audioLayer: true,
77
+ audioGain: 0.6,
78
+ });
79
+
80
+ // Trigger presets
81
+ await engine.trigger("confirm");
82
+ await engine.trigger("buzz", { intensity: 1.0 });
83
+
84
+ // Sequences
85
+ await engine.sequence([{ preset: "rampUp" }, { preset: "confirm", delay: 200 }], {
86
+ repeat: 2,
87
+ repeatGap: 300,
88
+ });
89
+
90
+ // Custom presets
91
+ engine.registerPreset("myPattern", {
92
+ pattern: [
93
+ { duration: 20, intensity: 0.6 },
94
+ { delay: 50, duration: 40, intensity: 1.0 },
95
+ ],
96
+ impulse: "tap",
97
+ iosTicks: 2,
98
+ iosTickGap: 60,
99
+ });
100
+ await engine.trigger("myPattern");
101
+
102
+ // Cleanup
103
+ engine.destroy();
104
+ ```
105
+
106
+ ### 👆 Drag Haptics
107
+
108
+ ```ts
109
+ import { HapticEngine } from "web-haptic-engine";
110
+
111
+ const engine = new HapticEngine();
112
+
113
+ const drag = engine.drag({
114
+ fireDist: 18, // px between haptic fires
115
+ impulse: "tick", // audio impulse type
116
+ intensity: 0.6,
117
+ onTick: (velocity, ticks) => {
118
+ console.log(`Tick #${ticks} at ${velocity}px/s`);
119
+ },
120
+ });
121
+
122
+ // Bind to a DOM element
123
+ const unbind = drag.bind(document.getElementById("drag-area")!);
124
+
125
+ // Later: cleanup
126
+ unbind();
127
+ drag.destroyAll();
128
+ engine.destroy();
129
+ ```
130
+
131
+ ---
132
+
133
+ ## 🎛️ Presets
134
+
135
+ | Preset | Description | Impulse |
136
+ | -------------- | ------------------------- | --------- |
137
+ | ✅ `success` | Ascending double-tap | `confirm` |
138
+ | ⚠️ `warning` | Two hesitant taps | `harsh` |
139
+ | ❌ `error` | Three rapid harsh taps | `harsh` |
140
+ | 👍 `confirm` | Strong double-tap confirm | `confirm` |
141
+ | 👎 `reject` | Harsh staccato triple | `harsh` |
142
+ | 🪶 `light` | Single light tap | `tick` |
143
+ | ⚖️ `medium` | Moderate tap | `tap` |
144
+ | 🏋️ `heavy` | Strong tap | `thud` |
145
+ | 🧸 `soft` | Cushioned tap | `tap` |
146
+ | 🔩 `rigid` | Hard crisp snap | `snap` |
147
+ | 🔘 `selection` | Subtle tick | `tick` |
148
+ | ⏱️ `tick` | Crisp tick | `click` |
149
+ | 🖱️ `click` | Ultra-short click | `click` |
150
+ | 🫰 `snap` | Sharp snap | `snap` |
151
+ | 👉 `nudge` | Two quick taps | `tap` |
152
+ | 🐝 `buzz` | Sustained vibration | `buzz` |
153
+ | 💓 `heartbeat` | Heartbeat rhythm | `thud` |
154
+ | 🧲 `spring` | Bouncy pulses | `tap` |
155
+ | 📈 `rampUp` | Escalating | `tap` |
156
+ | 📉 `rampDown` | Decreasing | `tap` |
157
+ | 💥 `thud` | Heavy impact | `thud` |
158
+ | 🎵 `trill` | Rapid flutter | `click` |
159
+ | 💗 `pulse` | Rhythmic pulse | `tap` |
160
+
161
+ ---
162
+
163
+ ## 📖 API Reference
164
+
165
+ ### `HapticEngine`
166
+
167
+ ```ts
168
+ const engine = new HapticEngine(options?: HapticEngineOptions)
169
+ ```
170
+
171
+ | Option | Type | Default | Description |
172
+ | ------------ | --------- | ------- | --------------------------- |
173
+ | `throttleMs` | `number` | `25` | Minimum ms between triggers |
174
+ | `audioLayer` | `boolean` | `true` | Enable audio impulse layer |
175
+ | `audioGain` | `number` | `0.6` | Master audio gain (0–1) |
176
+
177
+ #### 🔧 Methods
178
+
179
+ | Method | Description |
180
+ | ------------------------------ | ------------------------------- |
181
+ | `trigger(input?, options?)` | Fire a haptic pattern |
182
+ | `sequence(steps, options?)` | Play a sequence of presets |
183
+ | `drag(options?)` | Create a `DragHaptics` instance |
184
+ | `cancel()` | Cancel active haptic playback |
185
+ | `setEnabled(enabled)` | Enable/disable the engine |
186
+ | `setAudioLayer(enabled)` | Toggle audio layer |
187
+ | `setAudioGain(gain)` | Set audio gain (0–1) |
188
+ | `setThrottle(ms)` | Set throttle interval |
189
+ | `registerPreset(name, preset)` | Register a custom preset |
190
+ | `fireHapticTick(intensity)` | Fire a single platform tick |
191
+ | `fireImpulse(type, intensity)` | Fire a single audio impulse |
192
+ | `destroy()` | Clean up all resources |
193
+
194
+ #### 📊 Static Properties
195
+
196
+ | Property | Description |
197
+ | --------------------------------- | ------------------------------------------ |
198
+ | `HapticEngine.supportsVibration` | `true` if `navigator.vibrate` is available |
199
+ | `HapticEngine.supportsIOSHaptics` | `true` if iOS Taptic switch is supported |
200
+ | `HapticEngine.isSupported` | `true` if any haptic method is available |
201
+
202
+ ### `DragHaptics`
203
+
204
+ ```ts
205
+ const drag = engine.drag(options?: DragHapticsOptions)
206
+ ```
207
+
208
+ | Option | Type | Default | Description |
209
+ | ----------- | ------------- | -------- | --------------------------------------- |
210
+ | `fireDist` | `number` | `18` | Minimum px moved to trigger next haptic |
211
+ | `impulse` | `ImpulseType` | `'tick'` | Audio impulse type |
212
+ | `intensity` | `number` | `0.6` | Haptic intensity |
213
+ | `onTick` | `function` | — | Callback `(velocity, ticks) => void` |
214
+
215
+ ### `haptic()` (convenience)
216
+
217
+ ```ts
218
+ import { haptic } from "web-haptic-engine";
219
+
220
+ await haptic("success"); // preset name
221
+ await haptic(50); // raw duration ms
222
+ await haptic([20, 10, 30]); // pattern array
223
+ await haptic("heavy", { intensity: 1 }); // with options
224
+ ```
225
+
226
+ ---
227
+
228
+ ## 🌍 Platform Support
229
+
230
+ | Platform | Haptic Method | Audio |
231
+ | ----------------- | ------------------------------ | -------------------------------- |
232
+ | 📱 **Android** | `navigator.vibrate()` | AudioContext impulses |
233
+ | 🍎 **iOS Safari** | `<input switch>` Taptic toggle | AudioContext impulses |
234
+ | 🖥️ **Desktop** | — | AudioContext impulses (fallback) |
235
+
236
+ ---
237
+
238
+ ## 🛠️ Tech Stack
239
+
240
+ | Tool | Purpose |
241
+ | --------------------------------------------- | --------------------- |
242
+ | [TypeScript](https://www.typescriptlang.org/) | Type-safe source code |
243
+ | [tsdown](https://tsdown.dev/) | Library bundling |
244
+ | [Vitest](https://vitest.dev/) | Unit testing |
245
+ | [Vite+](https://vite.dev/) | Unified toolchain |
246
+
247
+ ---
248
+
249
+ ## 🧑‍💻 Development
250
+
251
+ ```bash
252
+ # Install dependencies
253
+ vp install
254
+
255
+ # Build the library
256
+ vp pack
257
+
258
+ # Run tests
259
+ vp test
260
+
261
+ # Watch mode (rebuild on changes)
262
+ vp pack --watch
263
+ ```
264
+
265
+ ---
266
+
267
+ ## 🎮 Demo
268
+
269
+ An interactive demo is included in the `demo/` directory. It showcases all 23 presets, drag haptics, impulse buffers, sequences, and real-time controls for intensity and audio gain.
270
+
271
+ ```bash
272
+ # Install dependencies (if not already done)
273
+ vp install
274
+
275
+ # Start the demo dev server
276
+ vp run demo
277
+ ```
278
+
279
+ This launches a Vite dev server. Open the URL shown in the terminal (typically `http://localhost:5173`) in your browser. For the full haptic experience, open it on a mobile device — Android for vibration, iOS Safari for Taptic feedback. On desktop, audio impulses still play as a fallback.
280
+
281
+ ---
282
+
283
+ ## 📁 Project Structure
284
+
285
+ ```
286
+ web-haptic-engine/
287
+ ├── src/
288
+ │ ├── core/ # Types, constants, easings & presets
289
+ │ ├── audio/ # Web Audio impulse synthesis & playback
290
+ │ ├── platform/ # Platform detection & adapters (Android, iOS)
291
+ │ ├── interactions/ # User interaction patterns (drag haptics)
292
+ │ ├── haptic-engine.ts # Main HapticEngine class & convenience helpers
293
+ │ └── index.ts # Public API exports
294
+ ├── demo/
295
+ │ ├── index.html # Demo page
296
+ │ ├── main.ts # Demo app (imports from library)
297
+ │ └── vite.config.ts # Vite config for demo dev server
298
+ ├── tests/
299
+ │ └── index.test.ts # Unit tests
300
+ ├── tsdown.config.ts # Library build config
301
+ ├── vite.config.ts # Vite+ unified config
302
+ ├── tsconfig.json # TypeScript config
303
+ └── package.json
304
+ ```
305
+
306
+ ---
307
+
308
+ ## 💡 Acknowledgements
309
+
310
+ This project was initially inspired by [web-haptics](https://github.com/lochie/web-haptics) by [@lochie](https://github.com/lochie).
311
+
312
+ ---
313
+
314
+ ## 🤝 Contributing
315
+
316
+ Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) guide for details.
317
+
318
+ ---
319
+
320
+ ## 📄 License
321
+
322
+ This project is licensed under the **MIT License** — feel free to use it for both personal and commercial purposes. See the [LICENSE](LICENSE) file for details.
323
+
324
+ ---
325
+
326
+ <p align="center">
327
+ Built with ❤️ by <a href="https://github.com/sumitsahoo">Sumit Sahoo</a>
328
+ </p>
@@ -0,0 +1,151 @@
1
+ //#region src/core/easings.d.ts
2
+ declare const easings: {
3
+ readonly linear: (t: number) => number;
4
+ readonly easeIn: (t: number) => number;
5
+ readonly easeOut: (t: number) => number;
6
+ readonly easeInOut: (t: number) => number;
7
+ readonly bounce: (t: number) => number;
8
+ readonly spring: (t: number) => number;
9
+ };
10
+ //#endregion
11
+ //#region src/core/types.d.ts
12
+ interface Vibration {
13
+ duration: number;
14
+ intensity?: number;
15
+ delay?: number;
16
+ }
17
+ type HapticPattern = number[] | Vibration[];
18
+ type EasingFn = (t: number) => number;
19
+ type ImpulseType = "tick" | "tap" | "thud" | "click" | "snap" | "buzz" | "confirm" | "harsh";
20
+ interface HapticPreset {
21
+ pattern: Vibration[];
22
+ description?: string;
23
+ /** Number of iOS switch-checkbox toggles to fire for this preset. */
24
+ iosTicks?: number;
25
+ /** Milliseconds between iOS ticks. Defaults to IOS_GAP (55 ms). */
26
+ iosTickGap?: number;
27
+ /** Audio impulse type to play alongside the haptic. */
28
+ impulse?: ImpulseType;
29
+ /** Easing curve applied to multi-step patterns. */
30
+ easing?: keyof typeof easings;
31
+ }
32
+ type HapticInput = number | string | HapticPattern | HapticPreset;
33
+ interface TriggerOptions {
34
+ intensity?: number;
35
+ audio?: boolean;
36
+ }
37
+ interface HapticEngineOptions {
38
+ debug?: boolean;
39
+ /** Minimum ms between trigger() calls. Default 25. */
40
+ throttleMs?: number;
41
+ /** Enable audio impulse layer. Default true. */
42
+ audioLayer?: boolean;
43
+ /** Master gain for audio impulses (0-1). Default 0.6. */
44
+ audioGain?: number;
45
+ }
46
+ interface SequenceStep {
47
+ preset: string;
48
+ delay?: number;
49
+ intensity?: number;
50
+ }
51
+ interface SequenceOptions {
52
+ repeat?: number;
53
+ repeatGap?: number;
54
+ }
55
+ interface DragHapticsOptions {
56
+ /** Minimum px moved since last fire to trigger next haptic (default: 18). */
57
+ fireDist?: number;
58
+ /** Audio impulse type for drag ticks (default: 'tick'). */
59
+ impulse?: ImpulseType;
60
+ /** Haptic/audio intensity 0-1 (default: 0.6). */
61
+ intensity?: number;
62
+ /** Callback fired on each drag tick with current velocity and tick count. */
63
+ onTick?: (velocity: number, ticks: number) => void;
64
+ }
65
+ //#endregion
66
+ //#region src/core/presets.d.ts
67
+ declare const presets: Record<string, HapticPreset>;
68
+ //#endregion
69
+ //#region src/interactions/drag-haptics.d.ts
70
+ declare class DragHaptics {
71
+ private engine;
72
+ private opts;
73
+ curX: number;
74
+ curY: number;
75
+ private lastFireX;
76
+ private lastFireY;
77
+ private lastFireT;
78
+ private active;
79
+ private tickCount;
80
+ private cleanup;
81
+ /** Set during a touch sequence to prevent mouse handlers from double-firing. */
82
+ private touchActive;
83
+ constructor(engine: HapticEngine, options?: DragHapticsOptions);
84
+ /** Attach drag-haptic listeners to an element. Returns an unbind function. */
85
+ bind(element: HTMLElement): () => void;
86
+ private distFromLastFire;
87
+ /** Unbind all elements and stop tracking. */
88
+ destroyAll(): void;
89
+ }
90
+ //#endregion
91
+ //#region src/haptic-engine.d.ts
92
+ declare class HapticEngine {
93
+ private platform;
94
+ private iosPool;
95
+ private audio;
96
+ private useAudio;
97
+ private throttleMs;
98
+ private enabled;
99
+ private lastTriggerTime;
100
+ private activeAbort;
101
+ /** True if the device supports Android-style navigator.vibrate(). */
102
+ static readonly supportsVibration: boolean;
103
+ /** True if the device supports iOS switch-checkbox haptics. */
104
+ static readonly supportsIOSHaptics: boolean;
105
+ /** True if any form of haptic feedback is available. */
106
+ static readonly isSupported: boolean;
107
+ constructor(options?: HapticEngineOptions);
108
+ /**
109
+ * Fire a haptic + audio pattern. Input can be a preset name, duration (ms),
110
+ * Vibration[], number[] (alternating on/off), or a HapticPreset object.
111
+ */
112
+ trigger(input?: HapticInput, options?: TriggerOptions): Promise<void>;
113
+ /** Play an audio impulse. Set force=true to bypass the audio-enabled toggle. */
114
+ fireImpulse(type: ImpulseType, intensity: number, force?: boolean): void;
115
+ /** Fire a single platform haptic tick (short vibrate or single iOS switch toggle). */
116
+ fireHapticTick(intensity: number): void;
117
+ /**
118
+ * Fire a drag-optimized haptic tick. Skips cancel()/vibrate(0) overhead.
119
+ * Android: velocity scales pulse duration (12-25ms) and count (1-3 taps).
120
+ * iOS: single switch toggle (requires user activation to produce Taptic).
121
+ */
122
+ fireDragTick(intensity: number, velocity: number): void;
123
+ /** Fire a drag tick only on Android (vibration platform). No-op on iOS. */
124
+ fireDragTickIfVibration(intensity: number, velocity: number): void;
125
+ /** Play a sequence of preset steps with optional delays and repeats. */
126
+ sequence(steps: SequenceStep[], options?: SequenceOptions): Promise<void>;
127
+ /** Create a DragHaptics instance bound to this engine. */
128
+ drag(options?: DragHapticsOptions): DragHaptics;
129
+ /** Cancel any in-progress pattern or vibration. */
130
+ cancel(): void;
131
+ setEnabled(e: boolean): void;
132
+ get isEnabled(): boolean;
133
+ setAudioLayer(e: boolean): void;
134
+ setAudioGain(g: number): void;
135
+ setThrottle(ms: number): void;
136
+ registerPreset(name: string, preset: HapticPreset): void;
137
+ /** Clean up all resources (audio context, iOS DOM elements). */
138
+ destroy(): void;
139
+ /** Resolve any HapticInput into a Vibration[] and optional preset metadata. */
140
+ private resolveInput;
141
+ private fireAndroid;
142
+ private fireIOS;
143
+ /** Cancellable setTimeout wrapped in a Promise. */
144
+ private sleep;
145
+ }
146
+ /** Get or create the shared default engine instance. */
147
+ declare function getDefaultEngine(opts?: HapticEngineOptions): HapticEngine;
148
+ /** Fire a one-shot haptic using the default engine. */
149
+ declare function haptic(input?: HapticInput, options?: TriggerOptions): Promise<void>;
150
+ //#endregion
151
+ export { DragHaptics, type DragHapticsOptions, type EasingFn, HapticEngine, type HapticEngineOptions, type HapticInput, type HapticPattern, type HapticPreset, type ImpulseType, type SequenceOptions, type SequenceStep, type TriggerOptions, type Vibration, easings, getDefaultEngine, haptic, presets };