web-haptics 0.0.3 → 0.0.5
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/README.md +35 -12
- package/dist/chunk-4RZ72ZZU.mjs +3 -0
- package/dist/{chunk-42RGEEVN.mjs.map → chunk-4RZ72ZZU.mjs.map} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1 -1
- package/dist/svelte/index.js +1 -1
- package/dist/svelte/index.js.map +1 -1
- package/dist/svelte/index.mjs +1 -1
- package/dist/svelte/index.mjs.map +1 -1
- package/dist/vue/index.js +1 -1
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/index.mjs +1 -1
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-42RGEEVN.mjs +0 -3
package/README.md
CHANGED
|
@@ -59,34 +59,57 @@ haptics.trigger("success");
|
|
|
59
59
|
|
|
60
60
|
## Built-in Presets
|
|
61
61
|
|
|
62
|
-
| Name | Pattern
|
|
63
|
-
| --------- |
|
|
64
|
-
| `success` | `[50, 50, 50]`
|
|
65
|
-
| `nudge` | `[80,
|
|
66
|
-
| `error` | `[
|
|
67
|
-
| `buzz` | `[1000]`
|
|
62
|
+
| Name | Pattern | Description |
|
|
63
|
+
| --------- | ---------------------------------------------------------------- | ------------------------------ |
|
|
64
|
+
| `success` | `[{ duration: 50 }, { delay: 50, duration: 50 }]` | Two taps indicating success |
|
|
65
|
+
| `nudge` | `[{ duration: 80, intensity: 0.8 }, { delay: 80, duration: 50, intensity: 0.3 }]` | Strong tap followed by a soft tap |
|
|
66
|
+
| `error` | `[{ duration: 50, intensity: 0.75 }, ...]` ×3 | Three sharp taps for errors |
|
|
67
|
+
| `buzz` | `[{ duration: 1000, intensity: 1 }]` | A long vibration |
|
|
68
68
|
|
|
69
69
|
You can also pass custom patterns directly:
|
|
70
70
|
|
|
71
71
|
```ts
|
|
72
|
-
trigger([100, 50, 100]); //
|
|
73
|
-
trigger(200); // single vibration
|
|
74
|
-
trigger({
|
|
72
|
+
trigger([100, 50, 100]); // number[] shorthand (alternating on/off durations)
|
|
73
|
+
trigger(200); // single vibration (ms)
|
|
74
|
+
trigger([{ duration: 80, intensity: 0.8 }, { delay: 50, duration: 100 }]); // Vibration[]
|
|
75
|
+
trigger({
|
|
76
|
+
pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],
|
|
77
|
+
description: "custom",
|
|
78
|
+
}); // full HapticPreset
|
|
75
79
|
```
|
|
76
80
|
|
|
77
81
|
## API
|
|
78
82
|
|
|
79
|
-
### `
|
|
83
|
+
### `new WebHaptics(options?)`
|
|
84
|
+
|
|
85
|
+
Create a new instance.
|
|
86
|
+
|
|
87
|
+
- `options.debug` — enable audio feedback for testing on desktop (default `false`)
|
|
88
|
+
- `options.showSwitch` — show the haptic feedback toggle switch (default `false`)
|
|
89
|
+
|
|
90
|
+
### `trigger(input?, options?): Promise<void>`
|
|
80
91
|
|
|
81
92
|
Trigger haptic feedback.
|
|
82
93
|
|
|
83
|
-
- `input` — preset name (`"success"`),
|
|
84
|
-
- `options.intensity` — override intensity (0–1)
|
|
94
|
+
- `input` — preset name (`"success"`), duration in ms, `number[]`, `Vibration[]`, or `HapticPreset`
|
|
95
|
+
- `options.intensity` — override default intensity (0–1, default `0.5`)
|
|
85
96
|
|
|
86
97
|
### `cancel()`
|
|
87
98
|
|
|
88
99
|
Stop the current pattern and cancel any ongoing vibration.
|
|
89
100
|
|
|
101
|
+
### `destroy()`
|
|
102
|
+
|
|
103
|
+
Clean up DOM elements and audio resources. Call when the instance is no longer needed.
|
|
104
|
+
|
|
105
|
+
### `setDebug(debug: boolean)`
|
|
106
|
+
|
|
107
|
+
Enable or disable debug audio feedback.
|
|
108
|
+
|
|
109
|
+
### `setShowSwitch(show: boolean)`
|
|
110
|
+
|
|
111
|
+
Show or hide the haptic feedback toggle switch.
|
|
112
|
+
|
|
90
113
|
### `WebHaptics.isSupported`
|
|
91
114
|
|
|
92
115
|
Static boolean — `true` if the device supports the Vibration API.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var y={success:{pattern:[{duration:50},{delay:50,duration:50}],description:"A series of taps indicating success."},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],description:"A series of taps indicating a nudge."},error:{pattern:[{duration:50,intensity:.75},{delay:50,duration:50,intensity:.75},{delay:50,duration:50,intensity:.75}],description:"A series of taps indicating a warning or error."},buzz:{pattern:[{duration:1e3,intensity:1}],description:"A long vibration."}};var g=16,x=184,m=1e3,p=20;function C(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=y[a];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${a}"`),null)}if(Array.isArray(a)){if(a.length===0)return{vibrations:[]};if(typeof a[0]=="number"){let i=a,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:a.map(i=>({...i}))}}return{vibrations:a.pattern.map(i=>({...i}))}}function w(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(p*i)),e=p-t,n=[],o=a;for(;o>=p;)n.push(t),n.push(e),o-=p;if(o>0){let s=Math.max(1,Math.round(o*i));n.push(s);let r=o-s;r>0&&n.push(r)}return n}function I(a,i){let t=[];for(let e=0;e<a.length;e++){let n=a[e],o=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 r=w(n.duration,o);if(r.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 r)t.push(d)}return t}var M=0,v=class a{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++M,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=C(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let o=Math.max(0,Math.min(1,t?.intensity??.5));for(let s of n)if(s.duration>m&&(s.duration=m),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(I(n,o)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern();let r=(n[0]?.delay??0)===0;if(r&&(this.hapticLabel.click(),this.debug&&this.audioCtx)){let d=Math.max(0,Math.min(1,n[0].intensity??o));this.playClick(d)}await this.runPattern(n,o,r)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),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 o=[],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,o.push({end:s,isOn:!1,intensity:0})),s+=u.duration,o.push({end:s,isOn:!0,intensity:c})}let r=s,d=0,h=-1,b=u=>{d===0&&(d=u);let c=u-d;if(c>=r){this.rafId=null,this.patternResolve=null,n();return}let l=o[0];for(let f of o)if(c<f.end){l=f;break}if(l.isOn){let f=g+(1-l.intensity)*x;h===-1?(h=u,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),e=!0)):u-h>=f&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),h=u)}this.rafId=requestAnimationFrame(b)};this.rafId=requestAnimationFrame(b)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let n=0;n<t.length;n++)t[n]=(Math.random()*2-1)*Math.exp(-n/25);this.audioGain.gain.value=.5*i;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.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=`web-haptics-${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=!0}};export{y as a,v as b};
|
|
3
|
+
//# sourceMappingURL=chunk-4RZ72ZZU.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n\n await this.runPattern(vibrations, defaultIntensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox"]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n const firstDelay = vibrations[0]?.delay ?? 0;\n const firstClickFired = firstDelay === 0;\n\n // Fire first click synchronously to stay within user gesture context\n // (only when the first vibration has no delay)\n if (firstClickFired) {\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n }\n\n await this.runPattern(vibrations, defaultIntensity, firstClickFired);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n firstClickFired: boolean,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n if (!firstClickFired) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n firstClickFired = true;\n }\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstClickFired","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-DkzcH7AA.mjs';
|
|
2
2
|
export { a as HapticPattern, b as HapticPreset, V as Vibration } from './types-DkzcH7AA.mjs';
|
|
3
3
|
|
|
4
|
-
var version = "0.0.
|
|
4
|
+
var version = "0.0.5";
|
|
5
5
|
|
|
6
6
|
declare class WebHaptics {
|
|
7
7
|
private hapticLabel;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-DkzcH7AA.js';
|
|
2
2
|
export { a as HapticPattern, b as HapticPreset, V as Vibration } from './types-DkzcH7AA.js';
|
|
3
3
|
|
|
4
|
-
var version = "0.0.
|
|
4
|
+
var version = "0.0.5";
|
|
5
5
|
|
|
6
6
|
declare class WebHaptics {
|
|
7
7
|
private hapticLabel;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var y=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var M=(s,i)=>{for(var t in i)y(s,t,{get:i[t],enumerable:!0})},A=(s,i,t,e)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of w(i))!I.call(s,n)&&n!==t&&y(s,n,{get:()=>i[n],enumerable:!(e=C(i,n))||e.enumerable});return s};var P=s=>A(y({},"__esModule",{value:!0}),s);var B={};M(B,{WebHaptics:()=>m,defaultPatterns:()=>f,version:()=>g});module.exports=P(B);var g="0.0.5";var f={success:{pattern:[{duration:50},{delay:50,duration:50}],description:"A series of taps indicating success."},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],description:"A series of taps indicating a nudge."},error:{pattern:[{duration:50,intensity:.75},{delay:50,duration:50,intensity:.75},{delay:50,duration:50,intensity:.75}],description:"A series of taps indicating a warning or error."},buzz:{pattern:[{duration:1e3,intensity:1}],description:"A long vibration."}};var L=16,F=184,x=1e3,b=20;function S(s){if(typeof s=="number")return{vibrations:[{duration:s}]};if(typeof s=="string"){let i=f[s];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${s}"`),null)}if(Array.isArray(s)){if(s.length===0)return{vibrations:[]};if(typeof s[0]=="number"){let i=s,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:s.map(i=>({...i}))}}return{vibrations:s.pattern.map(i=>({...i}))}}function O(s,i){if(i>=1)return[s];if(i<=0)return[];let t=Math.max(1,Math.round(b*i)),e=b-t,n=[],r=s;for(;r>=b;)n.push(t),n.push(e),r-=b;if(r>0){let a=Math.max(1,Math.round(r*i));n.push(a);let o=r-a;o>0&&n.push(o)}return n}function H(s,i){let t=[];for(let e=0;e<s.length;e++){let n=s[e],r=Math.max(0,Math.min(1,n.intensity??i)),a=n.delay??0;a>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=a:(t.length===0&&t.push(0),t.push(a)));let o=O(n.duration,r);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 G=0,m=class s{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++G,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=S(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let r=Math.max(0,Math.min(1,t?.intensity??.5));for(let a of n)if(a.duration>x&&(a.duration=x),!Number.isFinite(a.duration)||a.duration<0||a.delay!==void 0&&(!Number.isFinite(a.delay)||a.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(s.isSupported&&navigator.vibrate(H(n,r)),!s.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??r));this.playClick(d)}await this.runPattern(n,r,o)}}cancel(){this.stopPattern(),s.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),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 r=[],a=0;for(let l of i){let c=Math.max(0,Math.min(1,l.intensity??t)),u=l.delay??0;u>0&&(a+=u,r.push({end:a,isOn:!1,intensity:0})),a+=l.duration,r.push({end:a,isOn:!0,intensity:c})}let o=a,d=0,p=-1,v=l=>{d===0&&(d=l);let c=l-d;if(c>=o){this.rafId=null,this.patternResolve=null,n();return}let u=r[0];for(let h of r)if(c<h.end){u=h;break}if(u.isOn){let h=L+(1-u.intensity)*F;p===-1?(p=l,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(u.intensity),e=!0)):l-p>=h&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(u.intensity),p=l)}this.rafId=requestAnimationFrame(v)};this.rafId=requestAnimationFrame(v)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let n=0;n<t.length;n++)t[n]=(Math.random()*2-1)*Math.exp(-n/25);this.audioGain.gain.value=.5*i;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.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=`web-haptics-${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=!0}};0&&(module.exports={WebHaptics,defaultPatterns,version});
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../package.json","../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["export { version } from \"./../package.json\";\n\nexport { WebHaptics } from \"./lib/web-haptics\";\nexport { defaultPatterns } from \"./lib/web-haptics/patterns\";\nexport type {\n Vibration,\n HapticPattern,\n HapticPreset,\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"./lib/web-haptics/types\";\n","{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.3\",\n \"description\": \"Haptic feedback for the mobile web.\",\n \"homepage\": \"https://haptics.lochie.me\",\n \"sideEffects\": false,\n \"author\": \"Lochie Axon\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/lochie/web-haptics.git\",\n \"directory\": \"packages/web-haptics\"\n },\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./react\": {\n \"types\": \"./dist/react/index.d.ts\",\n \"import\": \"./dist/react/index.mjs\",\n \"require\": \"./dist/react/index.js\"\n },\n \"./vue\": {\n \"types\": \"./dist/vue/index.d.ts\",\n \"import\": \"./dist/vue/index.mjs\",\n \"require\": \"./dist/vue/index.js\"\n },\n \"./svelte\": {\n \"types\": \"./dist/svelte/index.d.ts\",\n \"import\": \"./dist/svelte/index.mjs\",\n \"require\": \"./dist/svelte/index.js\"\n }\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"lint:fix\": \"eslint --fix -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"pre-commit\": \"lint-staged\",\n \"prepublishOnly\": \"pnpm build\"\n },\n \"keywords\": [\n \"haptics\",\n \"vibration\",\n \"feedback\",\n \"web\",\n \"mobile\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"typescript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/lochie/web-haptics/issues\"\n },\n \"files\": [\n \"dist/*\",\n \"LICENSE\",\n \"README.md\"\n ],\n \"peerDependencies\": {\n \"react\": \">=18\",\n \"react-dom\": \">=18\",\n \"vue\": \">=3\",\n \"svelte\": \">=4\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vue\": {\n \"optional\": true\n },\n \"svelte\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.15\",\n \"@typescript-eslint/eslint-plugin\": \"^6.8.0\",\n \"@typescript-eslint/parser\": \"^6.8.0\",\n \"eslint\": \"^9.36.0\",\n \"eslint-config-airbnb\": \"^19.0.4\",\n \"eslint-config-airbnb-typescript\": \"^17.1.0\",\n \"eslint-config-prettier\": \"^9.0.0\",\n \"eslint-plugin-import\": \"^2.28.1\",\n \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n \"eslint-plugin-prettier\": \"^5.0.1\",\n \"eslint-plugin-react\": \"^7.33.2\",\n \"eslint-plugin-react-hooks\": \"^4.6.0\",\n \"prettier\": \"^3.0.3\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"svelte\": \"^4.0.0\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.9.3\",\n \"vue\": \"^3.3.0\"\n }\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n\n await this.runPattern(vibrations, defaultIntensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["src_exports","__export","WebHaptics","defaultPatterns","version","__toCommonJS","version","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","i","source","id","hapticLabel","hapticCheckbox"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../package.json","../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["export { version } from \"./../package.json\";\n\nexport { WebHaptics } from \"./lib/web-haptics\";\nexport { defaultPatterns } from \"./lib/web-haptics/patterns\";\nexport type {\n Vibration,\n HapticPattern,\n HapticPreset,\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"./lib/web-haptics/types\";\n","{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.5\",\n \"description\": \"Haptic feedback for the mobile web.\",\n \"homepage\": \"https://haptics.lochie.me\",\n \"sideEffects\": false,\n \"author\": \"Lochie Axon\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/lochie/web-haptics.git\",\n \"directory\": \"packages/web-haptics\"\n },\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./react\": {\n \"types\": \"./dist/react/index.d.ts\",\n \"import\": \"./dist/react/index.mjs\",\n \"require\": \"./dist/react/index.js\"\n },\n \"./vue\": {\n \"types\": \"./dist/vue/index.d.ts\",\n \"import\": \"./dist/vue/index.mjs\",\n \"require\": \"./dist/vue/index.js\"\n },\n \"./svelte\": {\n \"types\": \"./dist/svelte/index.d.ts\",\n \"import\": \"./dist/svelte/index.mjs\",\n \"require\": \"./dist/svelte/index.js\"\n }\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"lint:fix\": \"eslint --fix -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"pre-commit\": \"lint-staged\",\n \"prepublishOnly\": \"pnpm build\"\n },\n \"keywords\": [\n \"haptics\",\n \"vibration\",\n \"feedback\",\n \"web\",\n \"mobile\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"typescript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/lochie/web-haptics/issues\"\n },\n \"files\": [\n \"dist/*\",\n \"LICENSE\",\n \"README.md\"\n ],\n \"peerDependencies\": {\n \"react\": \">=18\",\n \"react-dom\": \">=18\",\n \"vue\": \">=3\",\n \"svelte\": \">=4\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vue\": {\n \"optional\": true\n },\n \"svelte\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.15\",\n \"@typescript-eslint/eslint-plugin\": \"^6.8.0\",\n \"@typescript-eslint/parser\": \"^6.8.0\",\n \"eslint\": \"^9.36.0\",\n \"eslint-config-airbnb\": \"^19.0.4\",\n \"eslint-config-airbnb-typescript\": \"^17.1.0\",\n \"eslint-config-prettier\": \"^9.0.0\",\n \"eslint-plugin-import\": \"^2.28.1\",\n \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n \"eslint-plugin-prettier\": \"^5.0.1\",\n \"eslint-plugin-react\": \"^7.33.2\",\n \"eslint-plugin-react-hooks\": \"^4.6.0\",\n \"prettier\": \"^3.0.3\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"svelte\": \"^4.0.0\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.9.3\",\n \"vue\": \"^3.3.0\"\n }\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n const firstDelay = vibrations[0]?.delay ?? 0;\n const firstClickFired = firstDelay === 0;\n\n // Fire first click synchronously to stay within user gesture context\n // (only when the first vibration has no delay)\n if (firstClickFired) {\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n }\n\n await this.runPattern(vibrations, defaultIntensity, firstClickFired);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n firstClickFired: boolean,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n if (!firstClickFired) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n firstClickFired = true;\n }\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["src_exports","__export","WebHaptics","defaultPatterns","version","__toCommonJS","version","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstClickFired","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import{a as e,b as t}from"./chunk-
|
|
2
|
+
import{a as e,b as t}from"./chunk-4RZ72ZZU.mjs";var s="0.0.5";export{t as WebHaptics,e as defaultPatterns,s as version};
|
|
3
3
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.
|
|
1
|
+
{"version":3,"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.5\",\n \"description\": \"Haptic feedback for the mobile web.\",\n \"homepage\": \"https://haptics.lochie.me\",\n \"sideEffects\": false,\n \"author\": \"Lochie Axon\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/lochie/web-haptics.git\",\n \"directory\": \"packages/web-haptics\"\n },\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./react\": {\n \"types\": \"./dist/react/index.d.ts\",\n \"import\": \"./dist/react/index.mjs\",\n \"require\": \"./dist/react/index.js\"\n },\n \"./vue\": {\n \"types\": \"./dist/vue/index.d.ts\",\n \"import\": \"./dist/vue/index.mjs\",\n \"require\": \"./dist/vue/index.js\"\n },\n \"./svelte\": {\n \"types\": \"./dist/svelte/index.d.ts\",\n \"import\": \"./dist/svelte/index.mjs\",\n \"require\": \"./dist/svelte/index.js\"\n }\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"lint:fix\": \"eslint --fix -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"pre-commit\": \"lint-staged\",\n \"prepublishOnly\": \"pnpm build\"\n },\n \"keywords\": [\n \"haptics\",\n \"vibration\",\n \"feedback\",\n \"web\",\n \"mobile\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"typescript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/lochie/web-haptics/issues\"\n },\n \"files\": [\n \"dist/*\",\n \"LICENSE\",\n \"README.md\"\n ],\n \"peerDependencies\": {\n \"react\": \">=18\",\n \"react-dom\": \">=18\",\n \"vue\": \">=3\",\n \"svelte\": \">=4\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vue\": {\n \"optional\": true\n },\n \"svelte\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.15\",\n \"@typescript-eslint/eslint-plugin\": \"^6.8.0\",\n \"@typescript-eslint/parser\": \"^6.8.0\",\n \"eslint\": \"^9.36.0\",\n \"eslint-config-airbnb\": \"^19.0.4\",\n \"eslint-config-airbnb-typescript\": \"^17.1.0\",\n \"eslint-config-prettier\": \"^9.0.0\",\n \"eslint-plugin-import\": \"^2.28.1\",\n \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n \"eslint-plugin-prettier\": \"^5.0.1\",\n \"eslint-plugin-react\": \"^7.33.2\",\n \"eslint-plugin-react-hooks\": \"^4.6.0\",\n \"prettier\": \"^3.0.3\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"svelte\": \"^4.0.0\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.9.3\",\n \"vue\": \"^3.3.0\"\n }\n}\n"],"mappings":";gDAEE,IAAAA,EAAW","names":["version"]}
|
package/dist/react/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var y=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var S=(a,i)=>{for(var t in i)y(a,t,{get:i[t],enumerable:!0})},A=(a,i,t,e)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of I(i))!M.call(a,n)&&n!==t&&y(a,n,{get:()=>i[n],enumerable:!(e=w(i,n))||e.enumerable});return a};var F=a=>A(y({},"__esModule",{value:!0}),a);var B={};S(B,{useWebHaptics:()=>C});module.exports=F(B);var l=require("react");var v={success:{pattern:[{duration:50},{delay:50,duration:50}],description:"A series of taps indicating success."},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],description:"A series of taps indicating a nudge."},error:{pattern:[{duration:50,intensity:.75},{delay:50,duration:50,intensity:.75},{delay:50,duration:50,intensity:.75}],description:"A series of taps indicating a warning or error."},buzz:{pattern:[{duration:1e3,intensity:1}],description:"A long vibration."}};var L=16,O=184,x=1e3,m=20;function P(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=v[a];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${a}"`),null)}if(Array.isArray(a)){if(a.length===0)return{vibrations:[]};if(typeof a[0]=="number"){let i=a,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:a.map(i=>({...i}))}}return{vibrations:a.pattern.map(i=>({...i}))}}function H(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(m*i)),e=m-t,n=[],r=a;for(;r>=m;)n.push(t),n.push(e),r-=m;if(r>0){let s=Math.max(1,Math.round(r*i));n.push(s);let o=r-s;o>0&&n.push(o)}return n}function k(a,i){let t=[];for(let e=0;e<a.length;e++){let n=a[e],r=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=H(n.duration,r);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 c of o)t.push(c)}return t}var G=0,h=class a{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++G,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=P(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let r=Math.max(0,Math.min(1,t?.intensity??.5));for(let s of n)if(s.duration>x&&(s.duration=x),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(k(n,r)),!a.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 c=Math.max(0,Math.min(1,n[0].intensity??r));this.playClick(c)}await this.runPattern(n,r,o)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),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 r=[],s=0;for(let u of i){let f=Math.max(0,Math.min(1,u.intensity??t)),d=u.delay??0;d>0&&(s+=d,r.push({end:s,isOn:!1,intensity:0})),s+=u.duration,r.push({end:s,isOn:!0,intensity:f})}let o=s,c=0,p=-1,g=u=>{c===0&&(c=u);let f=u-c;if(f>=o){this.rafId=null,this.patternResolve=null,n();return}let d=r[0];for(let b of r)if(f<b.end){d=b;break}if(d.isOn){let b=L+(1-d.intensity)*O;p===-1?(p=u,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),e=!0)):u-p>=b&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),p=u)}this.rafId=requestAnimationFrame(g)};this.rafId=requestAnimationFrame(g)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let n=0;n<t.length;n++)t[n]=(Math.random()*2-1)*Math.exp(-n/25);this.audioGain.gain.value=.5*i;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.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=`web-haptics-${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=!0}};function C(a){let i=(0,l.useRef)(null);(0,l.useEffect)(()=>(i.current=new h(a),()=>{i.current?.destroy(),i.current=null}),[]),(0,l.useEffect)(()=>{i.current?.setDebug(a?.debug??!1)},[a?.debug]),(0,l.useEffect)(()=>{i.current?.setShowSwitch(a?.showSwitch??!1)},[a?.showSwitch]);let t=(0,l.useCallback)((r,s)=>i.current?.trigger(r,s),[]),e=(0,l.useCallback)(()=>i.current?.cancel(),[]),n=h.isSupported;return{trigger:t,cancel:e,isSupported:n}}0&&(module.exports={useWebHaptics});
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts","../../src/react/useWebHaptics.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts"],"sourcesContent":["export { useWebHaptics } from \"./useWebHaptics\";\n","\"use client\";\n\nimport { useRef, useEffect, useCallback } from \"react\";\nimport { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function useWebHaptics(options?: WebHapticsOptions) {\n const instanceRef = useRef<WebHaptics | null>(null);\n\n useEffect(() => {\n instanceRef.current = new WebHaptics(options);\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n }, []);\n\n useEffect(() => {\n instanceRef.current?.setDebug(options?.debug ?? false);\n }, [options?.debug]);\n\n useEffect(() => {\n instanceRef.current?.setShowSwitch(options?.showSwitch ?? false);\n }, [options?.showSwitch]);\n\n const trigger = useCallback(\n (input?: HapticInput, options?: TriggerOptions) =>\n instanceRef.current?.trigger(input, options),\n [],\n );\n\n const cancel = useCallback(() => instanceRef.current?.cancel(), []);\n\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, isSupported };\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n\n await this.runPattern(vibrations, defaultIntensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["react_exports","__export","useWebHaptics","__toCommonJS","import_react","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox","useWebHaptics","options","instanceRef","WebHaptics","trigger","input","cancel","isSupported"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts","../../src/react/useWebHaptics.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts"],"sourcesContent":["export { useWebHaptics } from \"./useWebHaptics\";\n","\"use client\";\n\nimport { useRef, useEffect, useCallback } from \"react\";\nimport { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function useWebHaptics(options?: WebHapticsOptions) {\n const instanceRef = useRef<WebHaptics | null>(null);\n\n useEffect(() => {\n instanceRef.current = new WebHaptics(options);\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n }, []);\n\n useEffect(() => {\n instanceRef.current?.setDebug(options?.debug ?? false);\n }, [options?.debug]);\n\n useEffect(() => {\n instanceRef.current?.setShowSwitch(options?.showSwitch ?? false);\n }, [options?.showSwitch]);\n\n const trigger = useCallback(\n (input?: HapticInput, options?: TriggerOptions) =>\n instanceRef.current?.trigger(input, options),\n [],\n );\n\n const cancel = useCallback(() => instanceRef.current?.cancel(), []);\n\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, isSupported };\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n const firstDelay = vibrations[0]?.delay ?? 0;\n const firstClickFired = firstDelay === 0;\n\n // Fire first click synchronously to stay within user gesture context\n // (only when the first vibration has no delay)\n if (firstClickFired) {\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n }\n\n await this.runPattern(vibrations, defaultIntensity, firstClickFired);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n firstClickFired: boolean,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n if (!firstClickFired) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n firstClickFired = true;\n }\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["react_exports","__export","useWebHaptics","__toCommonJS","import_react","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstClickFired","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox","useWebHaptics","options","instanceRef","WebHaptics","trigger","input","cancel","isSupported"]}
|