web-haptics 0.0.2 → 0.0.3

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.
@@ -0,0 +1,3 @@
1
+ "use client";
2
+ var b={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 v=16,g=184,m=1e3,f=20;function x(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=b[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 C(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(f*i)),e=f-t,n=[],o=a;for(;o>=f;)n.push(t),n.push(e),o-=f;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 w(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=C(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 I=0,y=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=++I,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=x(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(w(n,o)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;if(this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx){let s=Math.max(0,Math.min(1,n[0].intensity??o));this.playClick(s)}await this.runPattern(n,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){return new Promise(e=>{this.patternResolve=e;let n=[],o=0;for(let u of i){let h=Math.max(0,Math.min(1,u.intensity??t)),l=u.delay??0;l>0&&(o+=l,n.push({end:o,isOn:!1,intensity:0})),o+=u.duration,n.push({end:o,isOn:!0,intensity:h})}let s=o,r=0,d=-1,p=u=>{r===0&&(r=u);let h=u-r;if(h>=s){this.rafId=null,this.patternResolve=null,e();return}let l=n[0];for(let c of n)if(h<c.end){l=c;break}if(l.isOn){let c=v+(1-l.intensity)*g;d===-1?d=u:u-d>=c&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),d=u)}this.rafId=requestAnimationFrame(p)};this.rafId=requestAnimationFrame(p)})}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{b as a,y as b};
3
+ //# sourceMappingURL=chunk-42RGEEVN.mjs.map
@@ -0,0 +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"]}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-CLjSTxrn.mjs';
2
- export { a as HapticPattern, b as HapticPreset } from './types-CLjSTxrn.mjs';
1
+ import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-DkzcH7AA.mjs';
2
+ export { a as HapticPattern, b as HapticPreset, V as Vibration } from './types-DkzcH7AA.mjs';
3
3
 
4
- var version = "0.0.2";
4
+ var version = "0.0.3";
5
5
 
6
6
  declare class WebHaptics {
7
7
  private hapticLabel;
@@ -31,24 +31,46 @@ declare class WebHaptics {
31
31
 
32
32
  declare const defaultPatterns: {
33
33
  readonly success: {
34
- readonly pattern: [50, 50, 50];
34
+ readonly pattern: [{
35
+ readonly duration: 50;
36
+ }, {
37
+ readonly delay: 50;
38
+ readonly duration: 50;
39
+ }];
35
40
  readonly description: "A series of taps indicating success.";
36
- readonly intensity: 0.5;
37
41
  };
38
42
  readonly nudge: {
39
- readonly pattern: [80, 100, 200];
43
+ readonly pattern: [{
44
+ readonly duration: 80;
45
+ readonly intensity: 0.8;
46
+ }, {
47
+ readonly delay: 80;
48
+ readonly duration: 50;
49
+ readonly intensity: 0.3;
50
+ }];
40
51
  readonly description: "A series of taps indicating a nudge.";
41
- readonly intensity: 0.5;
42
52
  };
43
53
  readonly error: {
44
- readonly pattern: [50, 50, 50, 50, 50];
54
+ readonly pattern: [{
55
+ readonly duration: 50;
56
+ readonly intensity: 0.75;
57
+ }, {
58
+ readonly delay: 50;
59
+ readonly duration: 50;
60
+ readonly intensity: 0.75;
61
+ }, {
62
+ readonly delay: 50;
63
+ readonly duration: 50;
64
+ readonly intensity: 0.75;
65
+ }];
45
66
  readonly description: "A series of taps indicating a warning or error.";
46
- readonly intensity: 0.75;
47
67
  };
48
68
  readonly buzz: {
49
- readonly pattern: [1000];
69
+ readonly pattern: [{
70
+ readonly duration: 1000;
71
+ readonly intensity: 1;
72
+ }];
50
73
  readonly description: "A long vibration.";
51
- readonly intensity: 1;
52
74
  };
53
75
  };
54
76
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-CLjSTxrn.js';
2
- export { a as HapticPattern, b as HapticPreset } from './types-CLjSTxrn.js';
1
+ import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-DkzcH7AA.js';
2
+ export { a as HapticPattern, b as HapticPreset, V as Vibration } from './types-DkzcH7AA.js';
3
3
 
4
- var version = "0.0.2";
4
+ var version = "0.0.3";
5
5
 
6
6
  declare class WebHaptics {
7
7
  private hapticLabel;
@@ -31,24 +31,46 @@ declare class WebHaptics {
31
31
 
32
32
  declare const defaultPatterns: {
33
33
  readonly success: {
34
- readonly pattern: [50, 50, 50];
34
+ readonly pattern: [{
35
+ readonly duration: 50;
36
+ }, {
37
+ readonly delay: 50;
38
+ readonly duration: 50;
39
+ }];
35
40
  readonly description: "A series of taps indicating success.";
36
- readonly intensity: 0.5;
37
41
  };
38
42
  readonly nudge: {
39
- readonly pattern: [80, 100, 200];
43
+ readonly pattern: [{
44
+ readonly duration: 80;
45
+ readonly intensity: 0.8;
46
+ }, {
47
+ readonly delay: 80;
48
+ readonly duration: 50;
49
+ readonly intensity: 0.3;
50
+ }];
40
51
  readonly description: "A series of taps indicating a nudge.";
41
- readonly intensity: 0.5;
42
52
  };
43
53
  readonly error: {
44
- readonly pattern: [50, 50, 50, 50, 50];
54
+ readonly pattern: [{
55
+ readonly duration: 50;
56
+ readonly intensity: 0.75;
57
+ }, {
58
+ readonly delay: 50;
59
+ readonly duration: 50;
60
+ readonly intensity: 0.75;
61
+ }, {
62
+ readonly delay: 50;
63
+ readonly duration: 50;
64
+ readonly intensity: 0.75;
65
+ }];
45
66
  readonly description: "A series of taps indicating a warning or error.";
46
- readonly intensity: 0.75;
47
67
  };
48
68
  readonly buzz: {
49
- readonly pattern: [1000];
69
+ readonly pattern: [{
70
+ readonly duration: 1000;
71
+ readonly intensity: 1;
72
+ }];
50
73
  readonly description: "A long vibration.";
51
- readonly intensity: 1;
52
74
  };
53
75
  };
54
76
 
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- "use strict";var b=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var A=(a,t)=>{for(var e in t)b(a,e,{get:t[e],enumerable:!0})},P=(a,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of w(t))!I.call(a,s)&&s!==e&&b(a,s,{get:()=>t[s],enumerable:!(i=C(t,s))||i.enumerable});return a};var L=a=>P(b({},"__esModule",{value:!0}),a);var H={};A(H,{WebHaptics:()=>f,defaultPatterns:()=>c,version:()=>x});module.exports=L(H);var x="0.0.2";var c={success:{pattern:[50,50,50],description:"A series of taps indicating success.",intensity:.5},nudge:{pattern:[80,100,200],description:"A series of taps indicating a nudge.",intensity:.5},error:{pattern:[50,50,50,50,50],description:"A series of taps indicating a warning or error.",intensity:.75},buzz:{pattern:[1e3],description:"A long vibration.",intensity:1}};var k=16,S=184,g=1e3,h=20;function F(a,t){if(t>=1)return a;if(t<=0)return[];let e=Math.max(1,Math.round(h*t)),i=h-e,s=[];for(let r=0;r<a.length;r++){let n=a[r];if(r%2===0){let o=n;for(;o>=h;)s.push(e),s.push(i),o-=h;if(o>0){let d=Math.max(1,Math.round(o*t));s.push(d);let l=o-d;l>0&&s.push(l)}}else s.length>0&&s.length%2===0?s[s.length-1]+=n:s.push(n)}return s}var G=0,f=class a{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(t){this.instanceId=++G,this.debug=t?.debug??!1,this.showSwitch=t?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(t=[10],e){let i,s=.5;if(typeof t=="number")i=[t];else if(typeof t=="string"){let n=c[t];if(!n){console.warn(`[web-haptics] Unknown preset: "${t}"`);return}i=[...n.pattern],s=n.intensity}else Array.isArray(t)?i=[...t]:(i=[...t.pattern],s=t.intensity);let r=Math.max(0,Math.min(1,e?.intensity??s));for(let n=0;n<i.length;n++)if(n%2===0&&i[n]>g&&(i[n]=g),!Number.isFinite(i[n])||i[n]<0){console.warn(`[web-haptics] Invalid value at index ${n}: ${i[n]}. Pattern values must be finite non-negative numbers.`);return}if(a.isSupported&&navigator.vibrate(F(i,r)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx&&this.playClick(r),await this.runPattern(i,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(t){this.debug=t,!t&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(t){if(this.showSwitch=t,this.hapticLabel){let e=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=t?"":"none",e&&(e.style.display=t?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(t,e){return new Promise(i=>{this.patternResolve=i;let s=[],r=0;for(let u of t)r+=u,s.push(r);let n=r,o=k+(1-e)*S,d=0,l=-1,m=u=>{d===0&&(d=u);let y=u-d;if(y>=n){this.rafId=null,this.patternResolve=null,i();return}let v=0;for(let p=0;p<s.length;p++)if(y<s[p]){v=p;break}v%2===0&&(l===-1?l=u:u-l>=o&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(e),l=u)),this.rafId=requestAnimationFrame(m)};this.rafId=requestAnimationFrame(m)})}playClick(t){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;this.audioGain.gain.value=.5*t;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 t=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*t,this.audioCtx.sampleRate);let e=this.audioBuffer.getChannelData(0);for(let i=0;i<e.length;i++)e[i]=(Math.random()*2-1)*Math.exp(-i/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let t=`web-haptics-${this.instanceId}`,e=document.createElement("label");e.setAttribute("for",t),e.textContent="Haptic feedback",e.style.position="fixed",e.style.bottom="10px",e.style.left="10px",e.style.padding="5px 10px",e.style.backgroundColor="rgba(0, 0, 0, 0.7)",e.style.color="white",e.style.fontFamily="sans-serif",e.style.fontSize="14px",e.style.borderRadius="4px",e.style.zIndex="9999",e.style.userSelect="none",this.hapticLabel=e;let i=document.createElement("input");i.type="checkbox",i.setAttribute("switch",""),i.id=t,i.style.all="initial",i.style.appearance="auto",this.showSwitch||(e.style.display="none",i.style.display="none"),e.appendChild(i),document.body.appendChild(e),this.domInitialized=!0}};0&&(module.exports={WebHaptics,defaultPatterns,version});
2
+ "use strict";var m=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var w=Object.prototype.hasOwnProperty;var I=(s,e)=>{for(var t in e)m(s,t,{get:e[t],enumerable:!0})},M=(s,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of C(e))!w.call(s,n)&&n!==t&&m(s,n,{get:()=>e[n],enumerable:!(i=x(e,n))||i.enumerable});return s};var A=s=>M(m({},"__esModule",{value:!0}),s);var G={};I(G,{WebHaptics:()=>b,defaultPatterns:()=>h,version:()=>v});module.exports=A(G);var v="0.0.3";var h={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,k=184,g=1e3,f=20;function F(s){if(typeof s=="number")return{vibrations:[{duration:s}]};if(typeof s=="string"){let e=h[s];return e?{vibrations:e.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 e=s,t=[];for(let i=0;i<e.length;i+=2){let n=i>0?e[i-1]:0;t.push({...n>0&&{delay:n},duration:e[i]})}return{vibrations:t}}return{vibrations:s.map(e=>({...e}))}}return{vibrations:s.pattern.map(e=>({...e}))}}function S(s,e){if(e>=1)return[s];if(e<=0)return[];let t=Math.max(1,Math.round(f*e)),i=f-t,n=[],r=s;for(;r>=f;)n.push(t),n.push(i),r-=f;if(r>0){let a=Math.max(1,Math.round(r*e));n.push(a);let o=r-a;o>0&&n.push(o)}return n}function O(s,e){let t=[];for(let i=0;i<s.length;i++){let n=s[i],r=Math.max(0,Math.min(1,n.intensity??e)),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=S(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 H=0,b=class s{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(e){this.instanceId=++H,this.debug=e?.debug??!1,this.showSwitch=e?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(e=[{duration:10}],t){let i=F(e);if(!i)return;let{vibrations:n}=i;if(n.length===0)return;let r=Math.max(0,Math.min(1,t?.intensity??.5));for(let a of n)if(a.duration>g&&(a.duration=g),!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(O(n,r)),!s.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;if(this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx){let a=Math.max(0,Math.min(1,n[0].intensity??r));this.playClick(a)}await this.runPattern(n,r)}}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(e){this.debug=e,!e&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(e){if(this.showSwitch=e,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=e?"":"none",t&&(t.style.display=e?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(e,t){return new Promise(i=>{this.patternResolve=i;let n=[],r=0;for(let l of e){let p=Math.max(0,Math.min(1,l.intensity??t)),u=l.delay??0;u>0&&(r+=u,n.push({end:r,isOn:!1,intensity:0})),r+=l.duration,n.push({end:r,isOn:!0,intensity:p})}let a=r,o=0,d=-1,y=l=>{o===0&&(o=l);let p=l-o;if(p>=a){this.rafId=null,this.patternResolve=null,i();return}let u=n[0];for(let c of n)if(p<c.end){u=c;break}if(u.isOn){let c=L+(1-u.intensity)*k;d===-1?d=l:l-d>=c&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(u.intensity),d=l)}this.rafId=requestAnimationFrame(y)};this.rafId=requestAnimationFrame(y)})}playClick(e){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*e;let i=this.audioCtx.createBufferSource();i.buffer=this.audioBuffer,i.connect(this.audioFilter),i.onended=()=>i.disconnect(),i.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 e=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*e,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let i=0;i<t.length;i++)t[i]=(Math.random()*2-1)*Math.exp(-i/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let e=`web-haptics-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",e),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 i=document.createElement("input");i.type="checkbox",i.setAttribute("switch",""),i.id=e,i.style.all="initial",i.style.appearance="auto",this.showSwitch||(t.style.display="none",i.style.display="none"),t.appendChild(i),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 HapticPattern,\n HapticPreset,\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"./lib/web-haptics/types\";\n","{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.2\",\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: [50, 50, 50],\n description: \"A series of taps indicating success.\",\n intensity: 0.5,\n },\n nudge: {\n pattern: [80, 100, 200],\n description: \"A series of taps indicating a nudge.\",\n intensity: 0.5,\n },\n error: {\n pattern: [50, 50, 50, 50, 50],\n description: \"A series of taps indicating a warning or error.\",\n intensity: 0.75,\n },\n buzz: {\n pattern: [1000],\n description: \"A long vibration.\",\n intensity: 1,\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type { HapticInput, TriggerOptions, WebHapticsOptions } 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\nfunction modulatePattern(pattern: number[], intensity: number): number[] {\n if (intensity >= 1) return pattern;\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 for (let i = 0; i < pattern.length; i++) {\n const dur = pattern[i]!;\n\n if (i % 2 === 0) {\n let remaining = dur;\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 } else {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += dur;\n } else {\n result.push(dur);\n }\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\" && typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [10],\n options?: TriggerOptions,\n ): Promise<void> {\n let pattern: number[];\n let defaultIntensity = 0.5;\n\n if (typeof input === \"number\") {\n pattern = [input];\n } else 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;\n }\n pattern = [...preset.pattern];\n defaultIntensity = preset.intensity;\n } else if (Array.isArray(input)) {\n pattern = [...input];\n } else {\n pattern = [...input.pattern];\n defaultIntensity = input.intensity;\n }\n\n const intensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? defaultIntensity),\n );\n\n for (let i = 0; i < pattern.length; i++) {\n if (i % 2 === 0 && pattern[i]! > MAX_PHASE_MS) {\n pattern[i] = MAX_PHASE_MS;\n }\n if (!Number.isFinite(pattern[i]) || pattern[i]! < 0) {\n console.warn(\n `[web-haptics] Invalid value at index ${i}: ${pattern[i]}. Pattern values must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(modulatePattern(pattern, intensity));\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 // (iOS blocks haptics from programmatic clicks outside gesture handlers)\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n\n await this.runPattern(pattern, intensity);\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(pattern: number[], intensity: number): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n const phases: number[] = [];\n let cumulative = 0;\n for (const p of pattern) {\n cumulative += p;\n phases.push(cumulative);\n }\n const totalDuration = cumulative;\n\n const toggleInterval = TOGGLE_MIN + (1 - intensity) * TOGGLE_MAX;\n let startTime = 0;\n let lastToggleTime = -1; // -1 signals first click was already fired synchronously\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 let phaseIndex = 0;\n for (let i = 0; i < phases.length; i++) {\n if (elapsed < phases[i]!) {\n phaseIndex = i;\n break;\n }\n }\n\n if (phaseIndex % 2 === 0) {\n if (lastToggleTime === -1) {\n // Skip first toggle — already fired synchronously\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(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 (!this.audioCtx || !this.audioFilter || !this.audioGain || !this.audioBuffer) return;\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","modulatePattern","pattern","intensity","onTime","offTime","result","i","dur","remaining","remOn","remOff","instanceCounter","WebHaptics","_WebHaptics","options","input","defaultIntensity","preset","defaultPatterns","debug","show","checkbox","resolve","phases","cumulative","p","totalDuration","toggleInterval","startTime","lastToggleTime","loop","time","elapsed","phaseIndex","source","duration","data","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.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"]}
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- import{a as e,b as t}from"./chunk-33FSVC54.mjs";var s="0.0.2";export{t as WebHaptics,e as defaultPatterns,s as version};
2
+ import{a as e,b as t}from"./chunk-42RGEEVN.mjs";var s="0.0.3";export{t as WebHaptics,e as defaultPatterns,s as version};
3
3
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.2\",\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"]}
1
+ {"version":3,"sources":["../package.json"],"sourcesContent":["{\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"],"mappings":";gDAEE,IAAAA,EAAW","names":["version"]}
@@ -1,4 +1,4 @@
1
- import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from '../types-CLjSTxrn.mjs';
1
+ import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from '../types-DkzcH7AA.mjs';
2
2
 
3
3
  declare function useWebHaptics(options?: WebHapticsOptions): {
4
4
  trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void> | undefined;
@@ -1,4 +1,4 @@
1
- import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from '../types-CLjSTxrn.js';
1
+ import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from '../types-DkzcH7AA.js';
2
2
 
3
3
  declare function useWebHaptics(options?: WebHapticsOptions): {
4
4
  trigger: (input?: HapticInput, options?: TriggerOptions) => Promise<void> | undefined;
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- "use strict";var b=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var A=(a,t)=>{for(var e in t)b(a,e,{get:t[e],enumerable:!0})},P=(a,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of I(t))!S.call(a,n)&&n!==e&&b(a,n,{get:()=>t[n],enumerable:!(i=w(t,n))||i.enumerable});return a};var F=a=>P(b({},"__esModule",{value:!0}),a);var G={};A(G,{useWebHaptics:()=>C});module.exports=F(G);var o=require("react");var v={success:{pattern:[50,50,50],description:"A series of taps indicating success.",intensity:.5},nudge:{pattern:[80,100,200],description:"A series of taps indicating a nudge.",intensity:.5},error:{pattern:[50,50,50,50,50],description:"A series of taps indicating a warning or error.",intensity:.75},buzz:{pattern:[1e3],description:"A long vibration.",intensity:1}};var L=16,H=184,x=1e3,f=20;function M(a,t){if(t>=1)return a;if(t<=0)return[];let e=Math.max(1,Math.round(f*t)),i=f-e,n=[];for(let r=0;r<a.length;r++){let s=a[r];if(r%2===0){let l=s;for(;l>=f;)n.push(e),n.push(i),l-=f;if(l>0){let c=Math.max(1,Math.round(l*t));n.push(c);let u=l-c;u>0&&n.push(u)}}else n.length>0&&n.length%2===0?n[n.length-1]+=s:n.push(s)}return n}var k=0,h=class a{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(t){this.instanceId=++k,this.debug=t?.debug??!1,this.showSwitch=t?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(t=[10],e){let i,n=.5;if(typeof t=="number")i=[t];else if(typeof t=="string"){let s=v[t];if(!s){console.warn(`[web-haptics] Unknown preset: "${t}"`);return}i=[...s.pattern],n=s.intensity}else Array.isArray(t)?i=[...t]:(i=[...t.pattern],n=t.intensity);let r=Math.max(0,Math.min(1,e?.intensity??n));for(let s=0;s<i.length;s++)if(s%2===0&&i[s]>x&&(i[s]=x),!Number.isFinite(i[s])||i[s]<0){console.warn(`[web-haptics] Invalid value at index ${s}: ${i[s]}. Pattern values must be finite non-negative numbers.`);return}if(a.isSupported&&navigator.vibrate(M(i,r)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx&&this.playClick(r),await this.runPattern(i,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(t){this.debug=t,!t&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(t){if(this.showSwitch=t,this.hapticLabel){let e=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=t?"":"none",e&&(e.style.display=t?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(t,e){return new Promise(i=>{this.patternResolve=i;let n=[],r=0;for(let d of t)r+=d,n.push(r);let s=r,l=L+(1-e)*H,c=0,u=-1,m=d=>{c===0&&(c=d);let g=d-c;if(g>=s){this.rafId=null,this.patternResolve=null,i();return}let y=0;for(let p=0;p<n.length;p++)if(g<n[p]){y=p;break}y%2===0&&(u===-1?u=d:d-u>=l&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(e),u=d)),this.rafId=requestAnimationFrame(m)};this.rafId=requestAnimationFrame(m)})}playClick(t){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;this.audioGain.gain.value=.5*t;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 t=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*t,this.audioCtx.sampleRate);let e=this.audioBuffer.getChannelData(0);for(let i=0;i<e.length;i++)e[i]=(Math.random()*2-1)*Math.exp(-i/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let t=`web-haptics-${this.instanceId}`,e=document.createElement("label");e.setAttribute("for",t),e.textContent="Haptic feedback",e.style.position="fixed",e.style.bottom="10px",e.style.left="10px",e.style.padding="5px 10px",e.style.backgroundColor="rgba(0, 0, 0, 0.7)",e.style.color="white",e.style.fontFamily="sans-serif",e.style.fontSize="14px",e.style.borderRadius="4px",e.style.zIndex="9999",e.style.userSelect="none",this.hapticLabel=e;let i=document.createElement("input");i.type="checkbox",i.setAttribute("switch",""),i.id=t,i.style.all="initial",i.style.appearance="auto",this.showSwitch||(e.style.display="none",i.style.display="none"),e.appendChild(i),document.body.appendChild(e),this.domInitialized=!0}};function C(a){let t=(0,o.useRef)(null);(0,o.useEffect)(()=>(t.current=new h(a),()=>{t.current?.destroy(),t.current=null}),[]),(0,o.useEffect)(()=>{t.current?.setDebug(a?.debug??!1)},[a?.debug]),(0,o.useEffect)(()=>{t.current?.setShowSwitch(a?.showSwitch??!1)},[a?.showSwitch]);let e=(0,o.useCallback)((r,s)=>t.current?.trigger(r,s),[]),i=(0,o.useCallback)(()=>t.current?.cancel(),[]),n=h.isSupported;return{trigger:e,cancel:i,isSupported:n}}0&&(module.exports={useWebHaptics});
2
+ "use strict";var m=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var M=(a,i)=>{for(var t in i)m(a,t,{get:i[t],enumerable:!0})},S=(a,i,t,e)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of w(i))!I.call(a,n)&&n!==t&&m(a,n,{get:()=>i[n],enumerable:!(e=C(i,n))||e.enumerable});return a};var A=a=>S(m({},"__esModule",{value:!0}),a);var G={};M(G,{useWebHaptics:()=>x});module.exports=A(G);var l=require("react");var g={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 F=16,O=184,v=1e3,b=20;function P(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=g[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(b*i)),e=b-t,n=[],r=a;for(;r>=b;)n.push(t),n.push(e),r-=b;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 L(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 k=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=++k,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>v&&(s.duration=v),!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(L(n,r)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;if(this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx){let s=Math.max(0,Math.min(1,n[0].intensity??r));this.playClick(s)}await this.runPattern(n,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){return new Promise(e=>{this.patternResolve=e;let n=[],r=0;for(let u of i){let p=Math.max(0,Math.min(1,u.intensity??t)),d=u.delay??0;d>0&&(r+=d,n.push({end:r,isOn:!1,intensity:0})),r+=u.duration,n.push({end:r,isOn:!0,intensity:p})}let s=r,o=0,c=-1,y=u=>{o===0&&(o=u);let p=u-o;if(p>=s){this.rafId=null,this.patternResolve=null,e();return}let d=n[0];for(let f of n)if(p<f.end){d=f;break}if(d.isOn){let f=F+(1-d.intensity)*O;c===-1?c=u:u-c>=f&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),c=u)}this.rafId=requestAnimationFrame(y)};this.rafId=requestAnimationFrame(y)})}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 x(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
@@ -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: [50, 50, 50],\n description: \"A series of taps indicating success.\",\n intensity: 0.5,\n },\n nudge: {\n pattern: [80, 100, 200],\n description: \"A series of taps indicating a nudge.\",\n intensity: 0.5,\n },\n error: {\n pattern: [50, 50, 50, 50, 50],\n description: \"A series of taps indicating a warning or error.\",\n intensity: 0.75,\n },\n buzz: {\n pattern: [1000],\n description: \"A long vibration.\",\n intensity: 1,\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type { HapticInput, TriggerOptions, WebHapticsOptions } 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\nfunction modulatePattern(pattern: number[], intensity: number): number[] {\n if (intensity >= 1) return pattern;\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 for (let i = 0; i < pattern.length; i++) {\n const dur = pattern[i]!;\n\n if (i % 2 === 0) {\n let remaining = dur;\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 } else {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += dur;\n } else {\n result.push(dur);\n }\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\" && typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [10],\n options?: TriggerOptions,\n ): Promise<void> {\n let pattern: number[];\n let defaultIntensity = 0.5;\n\n if (typeof input === \"number\") {\n pattern = [input];\n } else 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;\n }\n pattern = [...preset.pattern];\n defaultIntensity = preset.intensity;\n } else if (Array.isArray(input)) {\n pattern = [...input];\n } else {\n pattern = [...input.pattern];\n defaultIntensity = input.intensity;\n }\n\n const intensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? defaultIntensity),\n );\n\n for (let i = 0; i < pattern.length; i++) {\n if (i % 2 === 0 && pattern[i]! > MAX_PHASE_MS) {\n pattern[i] = MAX_PHASE_MS;\n }\n if (!Number.isFinite(pattern[i]) || pattern[i]! < 0) {\n console.warn(\n `[web-haptics] Invalid value at index ${i}: ${pattern[i]}. Pattern values must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(modulatePattern(pattern, intensity));\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 // (iOS blocks haptics from programmatic clicks outside gesture handlers)\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n\n await this.runPattern(pattern, intensity);\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(pattern: number[], intensity: number): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n const phases: number[] = [];\n let cumulative = 0;\n for (const p of pattern) {\n cumulative += p;\n phases.push(cumulative);\n }\n const totalDuration = cumulative;\n\n const toggleInterval = TOGGLE_MIN + (1 - intensity) * TOGGLE_MAX;\n let startTime = 0;\n let lastToggleTime = -1; // -1 signals first click was already fired synchronously\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 let phaseIndex = 0;\n for (let i = 0; i < phases.length; i++) {\n if (elapsed < phases[i]!) {\n phaseIndex = i;\n break;\n }\n }\n\n if (phaseIndex % 2 === 0) {\n if (lastToggleTime === -1) {\n // Skip first toggle — already fired synchronously\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(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 (!this.audioCtx || !this.audioFilter || !this.audioGain || !this.audioBuffer) return;\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":";yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,IAAA,eAAAC,EAAAH,GCEA,IAAAI,EAA+C,iBCAxC,IAAMC,EAAkB,CAC7B,QAAS,CACP,QAAS,CAAC,GAAI,GAAI,EAAE,EACpB,YAAa,uCACb,UAAW,EACb,EACA,MAAO,CACL,QAAS,CAAC,GAAI,IAAK,GAAG,EACtB,YAAa,uCACb,UAAW,EACb,EACA,MAAO,CACL,QAAS,CAAC,GAAI,GAAI,GAAI,GAAI,EAAE,EAC5B,YAAa,kDACb,UAAW,GACb,EACA,KAAM,CACJ,QAAS,CAAC,GAAI,EACd,YAAa,oBACb,UAAW,CACb,CACF,ECpBA,IAAMC,EAAa,GACbC,EAAa,IACbC,EAAe,IACfC,EAAY,GAElB,SAASC,EAAgBC,EAAmBC,EAA6B,CACvE,GAAIA,GAAa,EAAG,OAAOD,EAC3B,GAAIC,GAAa,EAAG,MAAO,CAAC,EAE5B,IAAMC,EAAS,KAAK,IAAI,EAAG,KAAK,MAAMJ,EAAYG,CAAS,CAAC,EACtDE,EAAUL,EAAYI,EACtBE,EAAmB,CAAC,EAE1B,QAASC,EAAI,EAAGA,EAAIL,EAAQ,OAAQK,IAAK,CACvC,IAAMC,EAAMN,EAAQK,CAAC,EAErB,GAAIA,EAAI,IAAM,EAAG,CACf,IAAIE,EAAYD,EAChB,KAAOC,GAAaT,GAClBM,EAAO,KAAKF,CAAM,EAClBE,EAAO,KAAKD,CAAO,EACnBI,GAAaT,EAEf,GAAIS,EAAY,EAAG,CACjB,IAAMC,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAMD,EAAYN,CAAS,CAAC,EAC3DG,EAAO,KAAKI,CAAK,EACjB,IAAMC,EAASF,EAAYC,EACvBC,EAAS,GAAGL,EAAO,KAAKK,CAAM,CACpC,CACF,MACML,EAAO,OAAS,GAAKA,EAAO,OAAS,IAAM,EAC7CA,EAAOA,EAAO,OAAS,CAAC,GAAME,EAE9BF,EAAO,KAAKE,CAAG,CAGrB,CAEA,OAAOF,CACT,CAEA,IAAIM,EAAkB,EAETC,EAAN,MAAMC,CAAW,CACd,YAAuC,KACvC,eAAiB,GACjB,WACA,MACA,WACA,MAAuB,KACvB,eAAsC,KACtC,SAAgC,KAChC,YAAuC,KACvC,UAA6B,KAC7B,YAAkC,KAE1C,YAAYC,EAA6B,CACvC,KAAK,WAAa,EAAEH,EACpB,KAAK,MAAQG,GAAS,OAAS,GAC/B,KAAK,WAAaA,GAAS,YAAc,EAC3C,CAEA,OAAgB,YACd,OAAO,UAAc,KAAe,OAAO,UAAU,SAAY,WAEnE,MAAM,QACJC,EAAqB,CAAC,EAAE,EACxBD,EACe,CACf,IAAIb,EACAe,EAAmB,GAEvB,GAAI,OAAOD,GAAU,SACnBd,EAAU,CAACc,CAAK,UACP,OAAOA,GAAU,SAAU,CACpC,IAAME,EAASC,EAAgBH,CAAqC,EACpE,GAAI,CAACE,EAAQ,CACX,QAAQ,KAAK,kCAAkCF,CAAK,GAAG,EACvD,MACF,CACAd,EAAU,CAAC,GAAGgB,EAAO,OAAO,EAC5BD,EAAmBC,EAAO,SAC5B,MAAW,MAAM,QAAQF,CAAK,EAC5Bd,EAAU,CAAC,GAAGc,CAAK,GAEnBd,EAAU,CAAC,GAAGc,EAAM,OAAO,EAC3BC,EAAmBD,EAAM,WAG3B,IAAMb,EAAY,KAAK,IACrB,EACA,KAAK,IAAI,EAAGY,GAAS,WAAaE,CAAgB,CACpD,EAEA,QAASV,EAAI,EAAGA,EAAIL,EAAQ,OAAQK,IAIlC,GAHIA,EAAI,IAAM,GAAKL,EAAQK,CAAC,EAAKR,IAC/BG,EAAQK,CAAC,EAAIR,GAEX,CAAC,OAAO,SAASG,EAAQK,CAAC,CAAC,GAAKL,EAAQK,CAAC,EAAK,EAAG,CACnD,QAAQ,KACN,wCAAwCA,CAAC,KAAKL,EAAQK,CAAC,CAAC,uDAC1D,EACA,MACF,CAOF,GAJIO,EAAW,aACb,UAAU,QAAQb,EAAgBC,EAASC,CAAS,CAAC,EAGnD,CAACW,EAAW,aAAe,KAAK,MAAO,CAEzC,GADA,KAAK,UAAU,EACX,CAAC,KAAK,YAAa,OAEnB,KAAK,OACP,MAAM,KAAK,YAAY,EAGzB,KAAK,YAAY,EAIjB,KAAK,YAAY,MAAM,EACnB,KAAK,OAAS,KAAK,UACrB,KAAK,UAAUX,CAAS,EAG1B,MAAM,KAAK,WAAWD,EAASC,CAAS,CAC1C,CACF,CAEA,QAAe,CACb,KAAK,YAAY,EACbW,EAAW,aACb,UAAU,QAAQ,CAAC,CAEvB,CAEA,SAAgB,CACd,KAAK,YAAY,EACb,KAAK,cACP,KAAK,YAAY,OAAO,EACxB,KAAK,YAAc,KACnB,KAAK,eAAiB,IAEpB,KAAK,WACP,KAAK,SAAS,MAAM,EACpB,KAAK,SAAW,KAChB,KAAK,YAAc,KACnB,KAAK,UAAY,KACjB,KAAK,YAAc,KAEvB,CAEA,SAASM,EAAsB,CAC7B,KAAK,MAAQA,EACT,CAACA,GAAS,KAAK,WACjB,KAAK,SAAS,MAAM,EACpB,KAAK,SAAW,KAChB,KAAK,YAAc,KACnB,KAAK,UAAY,KACjB,KAAK,YAAc,KAEvB,CAEA,cAAcC,EAAqB,CAEjC,GADA,KAAK,WAAaA,EACd,KAAK,YAAa,CACpB,IAAMC,EAAW,KAAK,YAAY,cAAc,OAAO,EACvD,KAAK,YAAY,MAAM,QAAUD,EAAO,GAAK,OACzCC,IAAUA,EAAS,MAAM,QAAUD,EAAO,GAAK,OACrD,CACF,CAEQ,aAAoB,CACtB,KAAK,QAAU,OACjB,qBAAqB,KAAK,KAAK,EAC/B,KAAK,MAAQ,MAEf,KAAK,iBAAiB,EACtB,KAAK,eAAiB,IACxB,CAEQ,WAAWnB,EAAmBC,EAAkC,CACtE,OAAO,IAAI,QAASoB,GAAY,CAC9B,KAAK,eAAiBA,EAEtB,IAAMC,EAAmB,CAAC,EACtBC,EAAa,EACjB,QAAWC,KAAKxB,EACduB,GAAcC,EACdF,EAAO,KAAKC,CAAU,EAExB,IAAME,EAAgBF,EAEhBG,EAAiB/B,GAAc,EAAIM,GAAaL,EAClD+B,EAAY,EACZC,EAAiB,GAEfC,EAAQC,GAAiB,CACzBH,IAAc,IAAGA,EAAYG,GACjC,IAAMC,EAAUD,EAAOH,EAEvB,GAAII,GAAWN,EAAe,CAC5B,KAAK,MAAQ,KACb,KAAK,eAAiB,KACtBJ,EAAQ,EACR,MACF,CAEA,IAAIW,EAAa,EACjB,QAAS3B,EAAI,EAAGA,EAAIiB,EAAO,OAAQjB,IACjC,GAAI0B,EAAUT,EAAOjB,CAAC,EAAI,CACxB2B,EAAa3B,EACb,KACF,CAGE2B,EAAa,IAAM,IACjBJ,IAAmB,GAErBA,EAAiBE,EACRA,EAAOF,GAAkBF,IAClC,KAAK,aAAa,MAAM,EACpB,KAAK,OAAS,KAAK,UACrB,KAAK,UAAUzB,CAAS,EAE1B2B,EAAiBE,IAIrB,KAAK,MAAQ,sBAAsBD,CAAI,CACzC,EACA,KAAK,MAAQ,sBAAsBA,CAAI,CACzC,CAAC,CACH,CAEQ,UAAU5B,EAAyB,CACzC,GAAI,CAAC,KAAK,UAAY,CAAC,KAAK,aAAe,CAAC,KAAK,WAAa,CAAC,KAAK,YAAa,OAEjF,KAAK,UAAU,KAAK,MAAQ,GAAMA,EAElC,IAAMgC,EAAS,KAAK,SAAS,mBAAmB,EAChDA,EAAO,OAAS,KAAK,YACrBA,EAAO,QAAQ,KAAK,WAAW,EAC/BA,EAAO,QAAU,IAAMA,EAAO,WAAW,EACzCA,EAAO,MAAM,CACf,CAEA,MAAc,aAA6B,CACzC,GAAI,CAAC,KAAK,UAAY,OAAO,aAAiB,IAAa,CACzD,KAAK,SAAW,IAAI,aAEpB,KAAK,YAAc,KAAK,SAAS,mBAAmB,EACpD,KAAK,YAAY,KAAO,WACxB,KAAK,YAAY,UAAU,MAAQ,IACnC,KAAK,YAAY,EAAE,MAAQ,EAE3B,KAAK,UAAY,KAAK,SAAS,WAAW,EAC1C,KAAK,YAAY,QAAQ,KAAK,SAAS,EACvC,KAAK,UAAU,QAAQ,KAAK,SAAS,WAAW,EAEhD,IAAMC,EAAW,KACjB,KAAK,YAAc,KAAK,SAAS,aAC/B,EACA,KAAK,SAAS,WAAaA,EAC3B,KAAK,SAAS,UAChB,EACA,IAAMC,EAAO,KAAK,YAAY,eAAe,CAAC,EAC9C,QAAS,EAAI,EAAG,EAAIA,EAAK,OAAQ,IAC/BA,EAAK,CAAC,GAAK,KAAK,OAAO,EAAI,EAAI,GAAK,KAAK,IAAI,CAAC,EAAI,EAAE,CAExD,CACI,KAAK,UAAU,QAAU,aAC3B,MAAM,KAAK,SAAS,OAAO,CAE/B,CAEQ,WAAkB,CAExB,GADI,KAAK,gBACL,OAAO,SAAa,IAAa,OAErC,IAAMC,EAAK,eAAe,KAAK,UAAU,GAEnCC,EAAc,SAAS,cAAc,OAAO,EAClDA,EAAY,aAAa,MAAOD,CAAE,EAClCC,EAAY,YAAc,kBAC1BA,EAAY,MAAM,SAAW,QAC7BA,EAAY,MAAM,OAAS,OAC3BA,EAAY,MAAM,KAAO,OACzBA,EAAY,MAAM,QAAU,WAC5BA,EAAY,MAAM,gBAAkB,qBACpCA,EAAY,MAAM,MAAQ,QAC1BA,EAAY,MAAM,WAAa,aAC/BA,EAAY,MAAM,SAAW,OAC7BA,EAAY,MAAM,aAAe,MACjCA,EAAY,MAAM,OAAS,OAC3BA,EAAY,MAAM,WAAa,OAC/B,KAAK,YAAcA,EAEnB,IAAMC,EAAiB,SAAS,cAAc,OAAO,EACrDA,EAAe,KAAO,WACtBA,EAAe,aAAa,SAAU,EAAE,EACxCA,EAAe,GAAKF,EACpBE,EAAe,MAAM,IAAM,UAC3BA,EAAe,MAAM,WAAa,OAE7B,KAAK,aACRD,EAAY,MAAM,QAAU,OAC5BC,EAAe,MAAM,QAAU,QAGjCD,EAAY,YAAYC,CAAc,EACtC,SAAS,KAAK,YAAYD,CAAW,EACrC,KAAK,eAAiB,EACxB,CACF,EFrTO,SAASE,EAAcC,EAA6B,CACzD,IAAMC,KAAc,UAA0B,IAAI,KAElD,aAAU,KACRA,EAAY,QAAU,IAAIC,EAAWF,CAAO,EACrC,IAAM,CACXC,EAAY,SAAS,QAAQ,EAC7BA,EAAY,QAAU,IACxB,GACC,CAAC,CAAC,KAEL,aAAU,IAAM,CACdA,EAAY,SAAS,SAASD,GAAS,OAAS,EAAK,CACvD,EAAG,CAACA,GAAS,KAAK,CAAC,KAEnB,aAAU,IAAM,CACdC,EAAY,SAAS,cAAcD,GAAS,YAAc,EAAK,CACjE,EAAG,CAACA,GAAS,UAAU,CAAC,EAExB,IAAMG,KAAU,eACd,CAACC,EAAqBJ,IACpBC,EAAY,SAAS,QAAQG,EAAOJ,CAAO,EAC7C,CAAC,CACH,EAEMK,KAAS,eAAY,IAAMJ,EAAY,SAAS,OAAO,EAAG,CAAC,CAAC,EAE5DK,EAAcJ,EAAW,YAE/B,MAAO,CAAE,QAAAC,EAAS,OAAAE,EAAQ,YAAAC,CAAY,CACxC","names":["react_exports","__export","useWebHaptics","__toCommonJS","import_react","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","modulatePattern","pattern","intensity","onTime","offTime","result","i","dur","remaining","remOn","remOff","instanceCounter","WebHaptics","_WebHaptics","options","input","defaultIntensity","preset","defaultPatterns","debug","show","checkbox","resolve","phases","cumulative","p","totalDuration","toggleInterval","startTime","lastToggleTime","loop","time","elapsed","phaseIndex","source","duration","data","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 // 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,3 +1,3 @@
1
1
  "use client";
2
- import{b as r}from"../chunk-33FSVC54.mjs";import{useRef as o,useEffect as c,useCallback as s}from"react";function l(t){let e=o(null);c(()=>(e.current=new r(t),()=>{e.current?.destroy(),e.current=null}),[]),c(()=>{e.current?.setDebug(t?.debug??!1)},[t?.debug]),c(()=>{e.current?.setShowSwitch(t?.showSwitch??!1)},[t?.showSwitch]);let n=s((p,a)=>e.current?.trigger(p,a),[]),u=s(()=>e.current?.cancel(),[]),i=r.isSupported;return{trigger:n,cancel:u,isSupported:i}}export{l as useWebHaptics};
2
+ import{b as r}from"../chunk-42RGEEVN.mjs";import{useRef as o,useEffect as c,useCallback as s}from"react";function l(t){let e=o(null);c(()=>(e.current=new r(t),()=>{e.current?.destroy(),e.current=null}),[]),c(()=>{e.current?.setDebug(t?.debug??!1)},[t?.debug]),c(()=>{e.current?.setShowSwitch(t?.showSwitch??!1)},[t?.showSwitch]);let n=s((p,a)=>e.current?.trigger(p,a),[]),u=s(()=>e.current?.cancel(),[]),i=r.isSupported;return{trigger:n,cancel:u,isSupported:i}}export{l as useWebHaptics};
3
3
  //# sourceMappingURL=index.mjs.map
@@ -1,8 +1,12 @@
1
- type HapticPattern = number[];
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
2
7
  interface HapticPreset {
3
- pattern: HapticPattern;
8
+ pattern: Vibration[];
4
9
  description: string;
5
- intensity: number;
6
10
  }
7
11
  type HapticInput = number | string | HapticPattern | HapticPreset;
8
12
  interface TriggerOptions {