taptickit 0.0.6

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 thor-op
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # TapticKit
2
+
3
+ Haptic feedback for the mobile web. Zero dependencies.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i taptickit
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ **React:**
14
+ ```tsx
15
+ import { useTaptic } from "taptickit/react";
16
+
17
+ const haptic = useTaptic();
18
+ <button onClick={() => haptic.trigger("success")}>Tap</button>
19
+ ```
20
+
21
+ **Vue:**
22
+ ```vue
23
+ <script setup>
24
+ import { useTaptic } from "taptickit/vue";
25
+ const haptic = useTaptic();
26
+ </script>
27
+ <button @click="haptic.trigger('success')">Tap</button>
28
+ ```
29
+
30
+ **Svelte:**
31
+ ```svelte
32
+ <script>
33
+ import { createTaptic } from "taptickit/svelte";
34
+ const haptic = createTaptic();
35
+ </script>
36
+ <button on:click={() => haptic.trigger('success')}>Tap</button>
37
+ ```
38
+
39
+ **Vanilla:**
40
+ ```js
41
+ import { TapticKit } from "taptickit";
42
+ const haptic = new TapticKit();
43
+ haptic.trigger("success");
44
+ ```
45
+
46
+ ## Presets
47
+
48
+ - `trigger()` - medium tap (default)
49
+ - `trigger("light")` - subtle tap
50
+ - `trigger("medium")` - standard tap
51
+ - `trigger("heavy")` - strong tap
52
+ - `trigger("success")` - positive outcome
53
+ - `trigger("warning")` - cautionary
54
+ - `trigger("error")` - negative outcome
55
+ - `trigger("selection")` - discrete tick
56
+
57
+ Next.js: add `"use client"` when using hooks.
@@ -0,0 +1,144 @@
1
+ var version = "0.0.6";
2
+
3
+ interface Vibration {
4
+ duration: number;
5
+ intensity?: number;
6
+ delay?: number;
7
+ }
8
+ type HapticPattern = number[] | Vibration[];
9
+ interface HapticPreset {
10
+ pattern: Vibration[];
11
+ }
12
+ type HapticInput = number | string | HapticPattern | HapticPreset;
13
+ interface TriggerOptions {
14
+ intensity?: number;
15
+ }
16
+ interface TapticKitOptions {
17
+ debug?: boolean;
18
+ showSwitch?: boolean;
19
+ }
20
+ /** @deprecated Use TapticKitOptions instead */
21
+ type WebHapticsOptions = TapticKitOptions;
22
+
23
+ declare class TapticKit {
24
+ private hapticLabel;
25
+ private domInitialized;
26
+ private instanceId;
27
+ private debug;
28
+ private showSwitch;
29
+ private rafId;
30
+ private patternResolve;
31
+ private audioCtx;
32
+ private audioFilter;
33
+ private audioGain;
34
+ private audioBuffer;
35
+ constructor(options?: TapticKitOptions);
36
+ static readonly isSupported: boolean;
37
+ trigger(input?: HapticInput, options?: TriggerOptions): Promise<void>;
38
+ cancel(): void;
39
+ destroy(): void;
40
+ setDebug(debug: boolean): void;
41
+ setShowSwitch(show: boolean): void;
42
+ private stopPattern;
43
+ private runPattern;
44
+ private playClick;
45
+ private ensureAudio;
46
+ private ensureDOM;
47
+ }
48
+ /** @deprecated Use TapticKit instead */
49
+ declare const WebHaptics: typeof TapticKit;
50
+
51
+ declare const defaultPatterns: {
52
+ readonly success: {
53
+ readonly pattern: [{
54
+ readonly duration: 30;
55
+ readonly intensity: 0.5;
56
+ }, {
57
+ readonly delay: 60;
58
+ readonly duration: 40;
59
+ readonly intensity: 1;
60
+ }];
61
+ };
62
+ readonly warning: {
63
+ readonly pattern: [{
64
+ readonly duration: 40;
65
+ readonly intensity: 0.8;
66
+ }, {
67
+ readonly delay: 100;
68
+ readonly duration: 40;
69
+ readonly intensity: 0.6;
70
+ }];
71
+ };
72
+ readonly error: {
73
+ readonly pattern: [{
74
+ readonly duration: 40;
75
+ readonly intensity: 0.7;
76
+ }, {
77
+ readonly delay: 40;
78
+ readonly duration: 40;
79
+ readonly intensity: 0.7;
80
+ }, {
81
+ readonly delay: 40;
82
+ readonly duration: 40;
83
+ readonly intensity: 0.9;
84
+ }, {
85
+ readonly delay: 40;
86
+ readonly duration: 50;
87
+ readonly intensity: 0.6;
88
+ }];
89
+ };
90
+ readonly light: {
91
+ readonly pattern: [{
92
+ readonly duration: 15;
93
+ readonly intensity: 0.4;
94
+ }];
95
+ };
96
+ readonly medium: {
97
+ readonly pattern: [{
98
+ readonly duration: 25;
99
+ readonly intensity: 0.7;
100
+ }];
101
+ };
102
+ readonly heavy: {
103
+ readonly pattern: [{
104
+ readonly duration: 35;
105
+ readonly intensity: 1;
106
+ }];
107
+ };
108
+ readonly soft: {
109
+ readonly pattern: [{
110
+ readonly duration: 40;
111
+ readonly intensity: 0.5;
112
+ }];
113
+ };
114
+ readonly rigid: {
115
+ readonly pattern: [{
116
+ readonly duration: 10;
117
+ readonly intensity: 1;
118
+ }];
119
+ };
120
+ readonly selection: {
121
+ readonly pattern: [{
122
+ readonly duration: 8;
123
+ readonly intensity: 0.3;
124
+ }];
125
+ };
126
+ readonly nudge: {
127
+ readonly pattern: [{
128
+ readonly duration: 80;
129
+ readonly intensity: 0.8;
130
+ }, {
131
+ readonly delay: 80;
132
+ readonly duration: 50;
133
+ readonly intensity: 0.3;
134
+ }];
135
+ };
136
+ readonly buzz: {
137
+ readonly pattern: [{
138
+ readonly duration: 1000;
139
+ readonly intensity: 1;
140
+ }];
141
+ };
142
+ };
143
+
144
+ export { type HapticInput, type HapticPattern, type HapticPreset, TapticKit, type TapticKitOptions, type TriggerOptions, type Vibration, WebHaptics, type WebHapticsOptions, defaultPatterns, version };
@@ -0,0 +1,144 @@
1
+ var version = "0.0.6";
2
+
3
+ interface Vibration {
4
+ duration: number;
5
+ intensity?: number;
6
+ delay?: number;
7
+ }
8
+ type HapticPattern = number[] | Vibration[];
9
+ interface HapticPreset {
10
+ pattern: Vibration[];
11
+ }
12
+ type HapticInput = number | string | HapticPattern | HapticPreset;
13
+ interface TriggerOptions {
14
+ intensity?: number;
15
+ }
16
+ interface TapticKitOptions {
17
+ debug?: boolean;
18
+ showSwitch?: boolean;
19
+ }
20
+ /** @deprecated Use TapticKitOptions instead */
21
+ type WebHapticsOptions = TapticKitOptions;
22
+
23
+ declare class TapticKit {
24
+ private hapticLabel;
25
+ private domInitialized;
26
+ private instanceId;
27
+ private debug;
28
+ private showSwitch;
29
+ private rafId;
30
+ private patternResolve;
31
+ private audioCtx;
32
+ private audioFilter;
33
+ private audioGain;
34
+ private audioBuffer;
35
+ constructor(options?: TapticKitOptions);
36
+ static readonly isSupported: boolean;
37
+ trigger(input?: HapticInput, options?: TriggerOptions): Promise<void>;
38
+ cancel(): void;
39
+ destroy(): void;
40
+ setDebug(debug: boolean): void;
41
+ setShowSwitch(show: boolean): void;
42
+ private stopPattern;
43
+ private runPattern;
44
+ private playClick;
45
+ private ensureAudio;
46
+ private ensureDOM;
47
+ }
48
+ /** @deprecated Use TapticKit instead */
49
+ declare const WebHaptics: typeof TapticKit;
50
+
51
+ declare const defaultPatterns: {
52
+ readonly success: {
53
+ readonly pattern: [{
54
+ readonly duration: 30;
55
+ readonly intensity: 0.5;
56
+ }, {
57
+ readonly delay: 60;
58
+ readonly duration: 40;
59
+ readonly intensity: 1;
60
+ }];
61
+ };
62
+ readonly warning: {
63
+ readonly pattern: [{
64
+ readonly duration: 40;
65
+ readonly intensity: 0.8;
66
+ }, {
67
+ readonly delay: 100;
68
+ readonly duration: 40;
69
+ readonly intensity: 0.6;
70
+ }];
71
+ };
72
+ readonly error: {
73
+ readonly pattern: [{
74
+ readonly duration: 40;
75
+ readonly intensity: 0.7;
76
+ }, {
77
+ readonly delay: 40;
78
+ readonly duration: 40;
79
+ readonly intensity: 0.7;
80
+ }, {
81
+ readonly delay: 40;
82
+ readonly duration: 40;
83
+ readonly intensity: 0.9;
84
+ }, {
85
+ readonly delay: 40;
86
+ readonly duration: 50;
87
+ readonly intensity: 0.6;
88
+ }];
89
+ };
90
+ readonly light: {
91
+ readonly pattern: [{
92
+ readonly duration: 15;
93
+ readonly intensity: 0.4;
94
+ }];
95
+ };
96
+ readonly medium: {
97
+ readonly pattern: [{
98
+ readonly duration: 25;
99
+ readonly intensity: 0.7;
100
+ }];
101
+ };
102
+ readonly heavy: {
103
+ readonly pattern: [{
104
+ readonly duration: 35;
105
+ readonly intensity: 1;
106
+ }];
107
+ };
108
+ readonly soft: {
109
+ readonly pattern: [{
110
+ readonly duration: 40;
111
+ readonly intensity: 0.5;
112
+ }];
113
+ };
114
+ readonly rigid: {
115
+ readonly pattern: [{
116
+ readonly duration: 10;
117
+ readonly intensity: 1;
118
+ }];
119
+ };
120
+ readonly selection: {
121
+ readonly pattern: [{
122
+ readonly duration: 8;
123
+ readonly intensity: 0.3;
124
+ }];
125
+ };
126
+ readonly nudge: {
127
+ readonly pattern: [{
128
+ readonly duration: 80;
129
+ readonly intensity: 0.8;
130
+ }, {
131
+ readonly delay: 80;
132
+ readonly duration: 50;
133
+ readonly intensity: 0.3;
134
+ }];
135
+ };
136
+ readonly buzz: {
137
+ readonly pattern: [{
138
+ readonly duration: 1000;
139
+ readonly intensity: 1;
140
+ }];
141
+ };
142
+ };
143
+
144
+ export { type HapticInput, type HapticPattern, type HapticPreset, TapticKit, type TapticKitOptions, type TriggerOptions, type Vibration, WebHaptics, type WebHapticsOptions, defaultPatterns, version };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ 'use strict';var g="0.0.6";var b={success:{pattern:[{duration:30,intensity:.5},{delay:60,duration:40,intensity:1}]},warning:{pattern:[{duration:40,intensity:.8},{delay:100,duration:40,intensity:.6}]},error:{pattern:[{duration:40,intensity:.7},{delay:40,duration:40,intensity:.7},{delay:40,duration:40,intensity:.9},{delay:40,duration:50,intensity:.6}]},light:{pattern:[{duration:15,intensity:.4}]},medium:{pattern:[{duration:25,intensity:.7}]},heavy:{pattern:[{duration:35,intensity:1}]},soft:{pattern:[{duration:40,intensity:.5}]},rigid:{pattern:[{duration:10,intensity:1}]},selection:{pattern:[{duration:8,intensity:.3}]},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}]},buzz:{pattern:[{duration:1e3,intensity:1}]}};var C=16,M=184,v=1e3,f=20;function k(r){if(typeof r=="number")return {vibrations:[{duration:r}]};if(typeof r=="string"){let i=b[r];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[taptickit] Unknown preset: "${r}"`),null)}if(Array.isArray(r)){if(r.length===0)return {vibrations:[]};if(typeof r[0]=="number"){let i=r,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]});}return {vibrations:t}}return {vibrations:r.map(i=>({...i}))}}return {vibrations:r.pattern.map(i=>({...i}))}}function w(r,i){if(i>=1)return [r];if(i<=0)return [];let t=Math.max(1,Math.round(f*i)),e=f-t,n=[],a=r;for(;a>=f;)n.push(t),n.push(e),a-=f;if(a>0){let s=Math.max(1,Math.round(a*i));n.push(s);let o=a-s;o>0&&n.push(o);}return n}function I(r,i){let t=[];for(let e=0;e<r.length;e++){let n=r[e],a=Math.max(0,Math.min(1,n.intensity??i)),s=n.delay??0;s>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=s:(t.length===0&&t.push(0),t.push(s)));let o=w(n.duration,a);if(o.length===0){t.length>0&&t.length%2===0?t[t.length-1]+=n.duration:n.duration>0&&(t.push(0),t.push(n.duration));continue}for(let d of o)t.push(d);}return t}var P=0,y=class r{hapticLabel=null;domInitialized=false;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++P,this.debug=i?.debug??false,this.showSwitch=i?.showSwitch??false;}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:25,intensity:.7}],t){let e=k(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let a=Math.max(0,Math.min(1,t?.intensity??.5));for(let s of n)if(s.duration>v&&(s.duration=v),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[taptickit] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(r.isSupported&&navigator.vibrate(I(n,a)),!r.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern();let o=(n[0]?.delay??0)===0;if(o&&(this.hapticLabel.click(),this.debug&&this.audioCtx)){let d=Math.max(0,Math.min(1,n[0].intensity??a));this.playClick(d);}await this.runPattern(n,a,o);}}cancel(){this.stopPattern(),r.isSupported&&navigator.vibrate(0);}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=false),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null);}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null);}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none");}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null;}runPattern(i,t,e){return new Promise(n=>{this.patternResolve=n;let a=[],s=0;for(let u of i){let c=Math.max(0,Math.min(1,u.intensity??t)),l=u.delay??0;l>0&&(s+=l,a.push({end:s,isOn:false,intensity:0})),s+=u.duration,a.push({end:s,isOn:true,intensity:c});}let o=s,d=0,p=-1,m=u=>{d===0&&(d=u);let c=u-d;if(c>=o){this.rafId=null,this.patternResolve=null,n();return}let l=a[0];for(let h of a)if(c<h.end){l=h;break}if(l.isOn){let h=C+(1-l.intensity)*M;p===-1?(p=u,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),e=true)):u-p>=h&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),p=u);}this.rafId=requestAnimationFrame(m);};this.rafId=requestAnimationFrame(m);})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let s=0;s<t.length;s++)t[s]=(Math.random()*2-1)*Math.exp(-s/25);this.audioGain.gain.value=.5*i;let e=2e3+i*2e3,n=1+(Math.random()-.5)*.3;this.audioFilter.frequency.value=e*n;let a=this.audioCtx.createBufferSource();a.buffer=this.audioBuffer,a.connect(this.audioFilter),a.onended=()=>a.disconnect(),a.start();}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let i=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*i,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let e=0;e<t.length;e++)t[e]=(Math.random()*2-1)*Math.exp(-e/25);}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume();}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let i=`taptickit-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",i),t.textContent="Haptic feedback",t.style.position="fixed",t.style.bottom="10px",t.style.left="10px",t.style.padding="5px 10px",t.style.backgroundColor="rgba(0, 0, 0, 0.7)",t.style.color="white",t.style.fontFamily="sans-serif",t.style.fontSize="14px",t.style.borderRadius="4px",t.style.zIndex="9999",t.style.userSelect="none",this.hapticLabel=t;let e=document.createElement("input");e.type="checkbox",e.setAttribute("switch",""),e.id=i,e.style.all="initial",e.style.appearance="auto",this.showSwitch||(t.style.display="none",e.style.display="none"),t.appendChild(e),document.body.appendChild(t),this.domInitialized=true;}},F=y;exports.TapticKit=y;exports.WebHaptics=F;exports.defaultPatterns=b;exports.version=g;
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ var g="0.0.6";var b={success:{pattern:[{duration:30,intensity:.5},{delay:60,duration:40,intensity:1}]},warning:{pattern:[{duration:40,intensity:.8},{delay:100,duration:40,intensity:.6}]},error:{pattern:[{duration:40,intensity:.7},{delay:40,duration:40,intensity:.7},{delay:40,duration:40,intensity:.9},{delay:40,duration:50,intensity:.6}]},light:{pattern:[{duration:15,intensity:.4}]},medium:{pattern:[{duration:25,intensity:.7}]},heavy:{pattern:[{duration:35,intensity:1}]},soft:{pattern:[{duration:40,intensity:.5}]},rigid:{pattern:[{duration:10,intensity:1}]},selection:{pattern:[{duration:8,intensity:.3}]},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}]},buzz:{pattern:[{duration:1e3,intensity:1}]}};var C=16,M=184,v=1e3,f=20;function k(r){if(typeof r=="number")return {vibrations:[{duration:r}]};if(typeof r=="string"){let i=b[r];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[taptickit] Unknown preset: "${r}"`),null)}if(Array.isArray(r)){if(r.length===0)return {vibrations:[]};if(typeof r[0]=="number"){let i=r,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]});}return {vibrations:t}}return {vibrations:r.map(i=>({...i}))}}return {vibrations:r.pattern.map(i=>({...i}))}}function w(r,i){if(i>=1)return [r];if(i<=0)return [];let t=Math.max(1,Math.round(f*i)),e=f-t,n=[],a=r;for(;a>=f;)n.push(t),n.push(e),a-=f;if(a>0){let s=Math.max(1,Math.round(a*i));n.push(s);let o=a-s;o>0&&n.push(o);}return n}function I(r,i){let t=[];for(let e=0;e<r.length;e++){let n=r[e],a=Math.max(0,Math.min(1,n.intensity??i)),s=n.delay??0;s>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=s:(t.length===0&&t.push(0),t.push(s)));let o=w(n.duration,a);if(o.length===0){t.length>0&&t.length%2===0?t[t.length-1]+=n.duration:n.duration>0&&(t.push(0),t.push(n.duration));continue}for(let d of o)t.push(d);}return t}var P=0,y=class r{hapticLabel=null;domInitialized=false;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++P,this.debug=i?.debug??false,this.showSwitch=i?.showSwitch??false;}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:25,intensity:.7}],t){let e=k(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let a=Math.max(0,Math.min(1,t?.intensity??.5));for(let s of n)if(s.duration>v&&(s.duration=v),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[taptickit] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(r.isSupported&&navigator.vibrate(I(n,a)),!r.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern();let o=(n[0]?.delay??0)===0;if(o&&(this.hapticLabel.click(),this.debug&&this.audioCtx)){let d=Math.max(0,Math.min(1,n[0].intensity??a));this.playClick(d);}await this.runPattern(n,a,o);}}cancel(){this.stopPattern(),r.isSupported&&navigator.vibrate(0);}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=false),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null);}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null);}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none");}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null;}runPattern(i,t,e){return new Promise(n=>{this.patternResolve=n;let a=[],s=0;for(let u of i){let c=Math.max(0,Math.min(1,u.intensity??t)),l=u.delay??0;l>0&&(s+=l,a.push({end:s,isOn:false,intensity:0})),s+=u.duration,a.push({end:s,isOn:true,intensity:c});}let o=s,d=0,p=-1,m=u=>{d===0&&(d=u);let c=u-d;if(c>=o){this.rafId=null,this.patternResolve=null,n();return}let l=a[0];for(let h of a)if(c<h.end){l=h;break}if(l.isOn){let h=C+(1-l.intensity)*M;p===-1?(p=u,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),e=true)):u-p>=h&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),p=u);}this.rafId=requestAnimationFrame(m);};this.rafId=requestAnimationFrame(m);})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let s=0;s<t.length;s++)t[s]=(Math.random()*2-1)*Math.exp(-s/25);this.audioGain.gain.value=.5*i;let e=2e3+i*2e3,n=1+(Math.random()-.5)*.3;this.audioFilter.frequency.value=e*n;let a=this.audioCtx.createBufferSource();a.buffer=this.audioBuffer,a.connect(this.audioFilter),a.onended=()=>a.disconnect(),a.start();}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let i=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*i,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let e=0;e<t.length;e++)t[e]=(Math.random()*2-1)*Math.exp(-e/25);}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume();}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let i=`taptickit-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",i),t.textContent="Haptic feedback",t.style.position="fixed",t.style.bottom="10px",t.style.left="10px",t.style.padding="5px 10px",t.style.backgroundColor="rgba(0, 0, 0, 0.7)",t.style.color="white",t.style.fontFamily="sans-serif",t.style.fontSize="14px",t.style.borderRadius="4px",t.style.zIndex="9999",t.style.userSelect="none",this.hapticLabel=t;let e=document.createElement("input");e.type="checkbox",e.setAttribute("switch",""),e.id=i,e.style.all="initial",e.style.appearance="auto",this.showSwitch||(t.style.display="none",e.style.display="none"),t.appendChild(e),document.body.appendChild(t),this.domInitialized=true;}},F=y;export{y as TapticKit,F as WebHaptics,b as defaultPatterns,g as version};
@@ -0,0 +1,27 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ }
10
+ type HapticInput = number | string | HapticPattern | HapticPreset;
11
+ interface TriggerOptions {
12
+ intensity?: number;
13
+ }
14
+ interface TapticKitOptions {
15
+ debug?: boolean;
16
+ showSwitch?: boolean;
17
+ }
18
+
19
+ declare function useTaptic(options?: TapticKitOptions): {
20
+ trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void> | undefined;
21
+ cancel: () => void | undefined;
22
+ isSupported: boolean;
23
+ };
24
+ /** @deprecated Use useTaptic instead */
25
+ declare const useWebHaptics: typeof useTaptic;
26
+
27
+ export { useTaptic, useWebHaptics };
@@ -0,0 +1,27 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ }
10
+ type HapticInput = number | string | HapticPattern | HapticPreset;
11
+ interface TriggerOptions {
12
+ intensity?: number;
13
+ }
14
+ interface TapticKitOptions {
15
+ debug?: boolean;
16
+ showSwitch?: boolean;
17
+ }
18
+
19
+ declare function useTaptic(options?: TapticKitOptions): {
20
+ trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void> | undefined;
21
+ cancel: () => void | undefined;
22
+ isSupported: boolean;
23
+ };
24
+ /** @deprecated Use useTaptic instead */
25
+ declare const useWebHaptics: typeof useTaptic;
26
+
27
+ export { useTaptic, useWebHaptics };
@@ -0,0 +1 @@
1
+ 'use strict';var react=require('react'),taptickit=require('taptickit');function u(e){let t=react.useRef(null);react.useEffect(()=>(t.current=new taptickit.TapticKit(e),()=>{t.current?.destroy(),t.current=null;}),[]),react.useEffect(()=>{t.current?.setDebug(e?.debug??false);},[e?.debug]),react.useEffect(()=>{t.current?.setShowSwitch(e?.showSwitch??false);},[e?.showSwitch]);let n=react.useCallback((a,o)=>t.current?.trigger(a,o),[]),s=react.useCallback(()=>t.current?.cancel(),[]),p=taptickit.TapticKit.isSupported;return {trigger:n,cancel:s,isSupported:p}}var f=u;exports.useTaptic=u;exports.useWebHaptics=f;
@@ -0,0 +1 @@
1
+ import {useRef,useEffect,useCallback}from'react';import {TapticKit}from'taptickit';function u(e){let t=useRef(null);useEffect(()=>(t.current=new TapticKit(e),()=>{t.current?.destroy(),t.current=null;}),[]),useEffect(()=>{t.current?.setDebug(e?.debug??false);},[e?.debug]),useEffect(()=>{t.current?.setShowSwitch(e?.showSwitch??false);},[e?.showSwitch]);let n=useCallback((a,o)=>t.current?.trigger(a,o),[]),s=useCallback(()=>t.current?.cancel(),[]),p=TapticKit.isSupported;return {trigger:n,cancel:s,isSupported:p}}var f=u;export{u as useTaptic,f as useWebHaptics};
@@ -0,0 +1,29 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ }
10
+ type HapticInput = number | string | HapticPattern | HapticPreset;
11
+ interface TriggerOptions {
12
+ intensity?: number;
13
+ }
14
+ interface TapticKitOptions {
15
+ debug?: boolean;
16
+ showSwitch?: boolean;
17
+ }
18
+
19
+ declare function createTaptic(options?: TapticKitOptions): {
20
+ trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void>;
21
+ cancel: () => void;
22
+ destroy: () => void;
23
+ setDebug: (debug: boolean) => void;
24
+ isSupported: boolean;
25
+ };
26
+ /** @deprecated Use createTaptic instead */
27
+ declare const createWebHaptics: typeof createTaptic;
28
+
29
+ export { createTaptic, createWebHaptics };
@@ -0,0 +1,29 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ }
10
+ type HapticInput = number | string | HapticPattern | HapticPreset;
11
+ interface TriggerOptions {
12
+ intensity?: number;
13
+ }
14
+ interface TapticKitOptions {
15
+ debug?: boolean;
16
+ showSwitch?: boolean;
17
+ }
18
+
19
+ declare function createTaptic(options?: TapticKitOptions): {
20
+ trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void>;
21
+ cancel: () => void;
22
+ destroy: () => void;
23
+ setDebug: (debug: boolean) => void;
24
+ isSupported: boolean;
25
+ };
26
+ /** @deprecated Use createTaptic instead */
27
+ declare const createWebHaptics: typeof createTaptic;
28
+
29
+ export { createTaptic, createWebHaptics };
@@ -0,0 +1 @@
1
+ 'use strict';var taptickit=require('taptickit');function c(p){let t=new taptickit.TapticKit(p),o=(e,g)=>t.trigger(e,g),r=()=>t.cancel(),n=()=>t.destroy(),s=e=>t.setDebug(e),a=taptickit.TapticKit.isSupported;return {trigger:o,cancel:r,destroy:n,setDebug:s,isSupported:a}}var u=c;exports.createTaptic=c;exports.createWebHaptics=u;
@@ -0,0 +1 @@
1
+ import {TapticKit}from'taptickit';function c(p){let t=new TapticKit(p),o=(e,g)=>t.trigger(e,g),r=()=>t.cancel(),n=()=>t.destroy(),s=e=>t.setDebug(e),a=TapticKit.isSupported;return {trigger:o,cancel:r,destroy:n,setDebug:s,isSupported:a}}var u=c;export{c as createTaptic,u as createWebHaptics};
@@ -0,0 +1,27 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ }
10
+ type HapticInput = number | string | HapticPattern | HapticPreset;
11
+ interface TriggerOptions {
12
+ intensity?: number;
13
+ }
14
+ interface TapticKitOptions {
15
+ debug?: boolean;
16
+ showSwitch?: boolean;
17
+ }
18
+
19
+ declare function useTaptic(options?: TapticKitOptions): {
20
+ trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void> | undefined;
21
+ cancel: () => void | undefined;
22
+ isSupported: boolean;
23
+ };
24
+ /** @deprecated Use useTaptic instead */
25
+ declare const useWebHaptics: typeof useTaptic;
26
+
27
+ export { useTaptic, useWebHaptics };
@@ -0,0 +1,27 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ }
10
+ type HapticInput = number | string | HapticPattern | HapticPreset;
11
+ interface TriggerOptions {
12
+ intensity?: number;
13
+ }
14
+ interface TapticKitOptions {
15
+ debug?: boolean;
16
+ showSwitch?: boolean;
17
+ }
18
+
19
+ declare function useTaptic(options?: TapticKitOptions): {
20
+ trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void> | undefined;
21
+ cancel: () => void | undefined;
22
+ isSupported: boolean;
23
+ };
24
+ /** @deprecated Use useTaptic instead */
25
+ declare const useWebHaptics: typeof useTaptic;
26
+
27
+ export { useTaptic, useWebHaptics };
@@ -0,0 +1 @@
1
+ 'use strict';var vue=require('vue'),taptickit=require('taptickit');function n(i){let t=null;vue.onMounted(()=>{t=new taptickit.TapticKit(i);}),vue.onUnmounted(()=>{t?.destroy(),t=null;}),vue.watch(()=>i?.debug,e=>{t?.setDebug(e??false);});let o=(e,s)=>t?.trigger(e,s),r=()=>t?.cancel(),c=taptickit.TapticKit.isSupported;return {trigger:o,cancel:r,isSupported:c}}var g=n;exports.useTaptic=n;exports.useWebHaptics=g;
@@ -0,0 +1 @@
1
+ import {onMounted,onUnmounted,watch}from'vue';import {TapticKit}from'taptickit';function n(i){let t=null;onMounted(()=>{t=new TapticKit(i);}),onUnmounted(()=>{t?.destroy(),t=null;}),watch(()=>i?.debug,e=>{t?.setDebug(e??false);});let o=(e,s)=>t?.trigger(e,s),r=()=>t?.cancel(),c=TapticKit.isSupported;return {trigger:o,cancel:r,isSupported:c}}var g=n;export{n as useTaptic,g as useWebHaptics};
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "taptickit",
3
+ "version": "0.0.6",
4
+ "description": "Haptic for the mobile web.",
5
+ "homepage": "https://taptickit.thorxop.dev",
6
+ "sideEffects": false,
7
+ "author": "thor-op",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/thor-op/taptickit.git",
12
+ "directory": "packages/taptickit"
13
+ },
14
+ "main": "dist/index.js",
15
+ "module": "dist/index.mjs",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.mjs",
21
+ "require": "./dist/index.js"
22
+ },
23
+ "./react": {
24
+ "types": "./dist/react/index.d.ts",
25
+ "import": "./dist/react/index.mjs",
26
+ "require": "./dist/react/index.js"
27
+ },
28
+ "./vue": {
29
+ "types": "./dist/vue/index.d.ts",
30
+ "import": "./dist/vue/index.mjs",
31
+ "require": "./dist/vue/index.js"
32
+ },
33
+ "./svelte": {
34
+ "types": "./dist/svelte/index.d.ts",
35
+ "import": "./dist/svelte/index.mjs",
36
+ "require": "./dist/svelte/index.js"
37
+ }
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "dev": "tsup --watch",
45
+ "lint": "eslint -c .eslintrc.cjs ./src/**/*.{ts,tsx}",
46
+ "lint:fix": "eslint --fix -c .eslintrc.cjs ./src/**/*.{ts,tsx}",
47
+ "pre-commit": "lint-staged",
48
+ "prepublishOnly": "pnpm build"
49
+ },
50
+ "keywords": [
51
+ "haptics",
52
+ "vibration",
53
+ "feedback",
54
+ "web",
55
+ "mobile",
56
+ "react",
57
+ "vue",
58
+ "svelte",
59
+ "typescript"
60
+ ],
61
+ "bugs": {
62
+ "url": "https://github.com/thor-op/taptickit/issues"
63
+ },
64
+ "files": [
65
+ "dist/**/*.js",
66
+ "dist/**/*.mjs",
67
+ "dist/**/*.d.ts",
68
+ "dist/**/*.d.mts",
69
+ "LICENSE",
70
+ "README.md"
71
+ ],
72
+ "peerDependencies": {
73
+ "react": ">=18",
74
+ "react-dom": ">=18",
75
+ "vue": ">=3",
76
+ "svelte": ">=4"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "react": {
80
+ "optional": true
81
+ },
82
+ "react-dom": {
83
+ "optional": true
84
+ },
85
+ "vue": {
86
+ "optional": true
87
+ },
88
+ "svelte": {
89
+ "optional": true
90
+ }
91
+ },
92
+ "devDependencies": {
93
+ "@types/react": "^18.2.15",
94
+ "@typescript-eslint/eslint-plugin": "^6.8.0",
95
+ "@typescript-eslint/parser": "^6.8.0",
96
+ "eslint": "^9.36.0",
97
+ "eslint-config-airbnb": "^19.0.4",
98
+ "eslint-config-airbnb-typescript": "^17.1.0",
99
+ "eslint-config-prettier": "^9.0.0",
100
+ "eslint-plugin-import": "^2.28.1",
101
+ "eslint-plugin-jsx-a11y": "^6.7.1",
102
+ "eslint-plugin-prettier": "^5.0.1",
103
+ "eslint-plugin-react": "^7.33.2",
104
+ "eslint-plugin-react-hooks": "^4.6.0",
105
+ "prettier": "^3.0.3",
106
+ "react": "^18.2.0",
107
+ "react-dom": "^18.2.0",
108
+ "svelte": "^4.0.0",
109
+ "tsup": "^8.5.0",
110
+ "typescript": "^5.9.3",
111
+ "vue": "^3.3.0"
112
+ }
113
+ }