web-haptics 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4RZ72ZZU.mjs +3 -0
- package/dist/{chunk-42RGEEVN.mjs.map → chunk-4RZ72ZZU.mjs.map} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1 -1
- package/dist/svelte/index.js +1 -1
- package/dist/svelte/index.js.map +1 -1
- package/dist/svelte/index.mjs +1 -1
- package/dist/svelte/index.mjs.map +1 -1
- package/dist/vue/index.js +1 -1
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/index.mjs +1 -1
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-42RGEEVN.mjs +0 -3
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var y={success:{pattern:[{duration:50},{delay:50,duration:50}],description:"A series of taps indicating success."},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],description:"A series of taps indicating a nudge."},error:{pattern:[{duration:50,intensity:.75},{delay:50,duration:50,intensity:.75},{delay:50,duration:50,intensity:.75}],description:"A series of taps indicating a warning or error."},buzz:{pattern:[{duration:1e3,intensity:1}],description:"A long vibration."}};var g=16,x=184,m=1e3,p=20;function C(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=y[a];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${a}"`),null)}if(Array.isArray(a)){if(a.length===0)return{vibrations:[]};if(typeof a[0]=="number"){let i=a,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]})}return{vibrations:t}}return{vibrations:a.map(i=>({...i}))}}return{vibrations:a.pattern.map(i=>({...i}))}}function w(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(p*i)),e=p-t,n=[],o=a;for(;o>=p;)n.push(t),n.push(e),o-=p;if(o>0){let s=Math.max(1,Math.round(o*i));n.push(s);let r=o-s;r>0&&n.push(r)}return n}function I(a,i){let t=[];for(let e=0;e<a.length;e++){let n=a[e],o=Math.max(0,Math.min(1,n.intensity??i)),s=n.delay??0;s>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=s:(t.length===0&&t.push(0),t.push(s)));let r=w(n.duration,o);if(r.length===0){t.length>0&&t.length%2===0?t[t.length-1]+=n.duration:n.duration>0&&(t.push(0),t.push(n.duration));continue}for(let d of r)t.push(d)}return t}var M=0,v=class a{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++M,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=C(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let o=Math.max(0,Math.min(1,t?.intensity??.5));for(let s of n)if(s.duration>m&&(s.duration=m),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(I(n,o)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern();let r=(n[0]?.delay??0)===0;if(r&&(this.hapticLabel.click(),this.debug&&this.audioCtx)){let d=Math.max(0,Math.min(1,n[0].intensity??o));this.playClick(d)}await this.runPattern(n,o,r)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t,e){return new Promise(n=>{this.patternResolve=n;let o=[],s=0;for(let u of i){let c=Math.max(0,Math.min(1,u.intensity??t)),l=u.delay??0;l>0&&(s+=l,o.push({end:s,isOn:!1,intensity:0})),s+=u.duration,o.push({end:s,isOn:!0,intensity:c})}let r=s,d=0,h=-1,b=u=>{d===0&&(d=u);let c=u-d;if(c>=r){this.rafId=null,this.patternResolve=null,n();return}let l=o[0];for(let f of o)if(c<f.end){l=f;break}if(l.isOn){let f=g+(1-l.intensity)*x;h===-1?(h=u,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),e=!0)):u-h>=f&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),h=u)}this.rafId=requestAnimationFrame(b)};this.rafId=requestAnimationFrame(b)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let n=0;n<t.length;n++)t[n]=(Math.random()*2-1)*Math.exp(-n/25);this.audioGain.gain.value=.5*i;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let i=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*i,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let e=0;e<t.length;e++)t[e]=(Math.random()*2-1)*Math.exp(-e/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let i=`web-haptics-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",i),t.textContent="Haptic feedback",t.style.position="fixed",t.style.bottom="10px",t.style.left="10px",t.style.padding="5px 10px",t.style.backgroundColor="rgba(0, 0, 0, 0.7)",t.style.color="white",t.style.fontFamily="sans-serif",t.style.fontSize="14px",t.style.borderRadius="4px",t.style.zIndex="9999",t.style.userSelect="none",this.hapticLabel=t;let e=document.createElement("input");e.type="checkbox",e.setAttribute("switch",""),e.id=i,e.style.all="initial",e.style.appearance="auto",this.showSwitch||(t.style.display="none",e.style.display="none"),t.appendChild(e),document.body.appendChild(t),this.domInitialized=!0}};export{y as a,v as b};
|
|
3
|
+
//# sourceMappingURL=chunk-4RZ72ZZU.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n\n await this.runPattern(vibrations, defaultIntensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox"]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n const firstDelay = vibrations[0]?.delay ?? 0;\n const firstClickFired = firstDelay === 0;\n\n // Fire first click synchronously to stay within user gesture context\n // (only when the first vibration has no delay)\n if (firstClickFired) {\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n }\n\n await this.runPattern(vibrations, defaultIntensity, firstClickFired);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n firstClickFired: boolean,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n if (!firstClickFired) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n firstClickFired = true;\n }\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstClickFired","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-DkzcH7AA.mjs';
|
|
2
2
|
export { a as HapticPattern, b as HapticPreset, V as Vibration } from './types-DkzcH7AA.mjs';
|
|
3
3
|
|
|
4
|
-
var version = "0.0.
|
|
4
|
+
var version = "0.0.4";
|
|
5
5
|
|
|
6
6
|
declare class WebHaptics {
|
|
7
7
|
private hapticLabel;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { W as WebHapticsOptions, H as HapticInput, T as TriggerOptions } from './types-DkzcH7AA.js';
|
|
2
2
|
export { a as HapticPattern, b as HapticPreset, V as Vibration } from './types-DkzcH7AA.js';
|
|
3
3
|
|
|
4
|
-
var version = "0.0.
|
|
4
|
+
var version = "0.0.4";
|
|
5
5
|
|
|
6
6
|
declare class WebHaptics {
|
|
7
7
|
private hapticLabel;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var y=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var M=(s,i)=>{for(var t in i)y(s,t,{get:i[t],enumerable:!0})},A=(s,i,t,e)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of w(i))!I.call(s,n)&&n!==t&&y(s,n,{get:()=>i[n],enumerable:!(e=C(i,n))||e.enumerable});return s};var P=s=>A(y({},"__esModule",{value:!0}),s);var B={};M(B,{WebHaptics:()=>m,defaultPatterns:()=>f,version:()=>g});module.exports=P(B);var g="0.0.4";var f={success:{pattern:[{duration:50},{delay:50,duration:50}],description:"A series of taps indicating success."},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],description:"A series of taps indicating a nudge."},error:{pattern:[{duration:50,intensity:.75},{delay:50,duration:50,intensity:.75},{delay:50,duration:50,intensity:.75}],description:"A series of taps indicating a warning or error."},buzz:{pattern:[{duration:1e3,intensity:1}],description:"A long vibration."}};var L=16,F=184,x=1e3,b=20;function S(s){if(typeof s=="number")return{vibrations:[{duration:s}]};if(typeof s=="string"){let i=f[s];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${s}"`),null)}if(Array.isArray(s)){if(s.length===0)return{vibrations:[]};if(typeof s[0]=="number"){let i=s,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]})}return{vibrations:t}}return{vibrations:s.map(i=>({...i}))}}return{vibrations:s.pattern.map(i=>({...i}))}}function O(s,i){if(i>=1)return[s];if(i<=0)return[];let t=Math.max(1,Math.round(b*i)),e=b-t,n=[],r=s;for(;r>=b;)n.push(t),n.push(e),r-=b;if(r>0){let a=Math.max(1,Math.round(r*i));n.push(a);let o=r-a;o>0&&n.push(o)}return n}function H(s,i){let t=[];for(let e=0;e<s.length;e++){let n=s[e],r=Math.max(0,Math.min(1,n.intensity??i)),a=n.delay??0;a>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=a:(t.length===0&&t.push(0),t.push(a)));let o=O(n.duration,r);if(o.length===0){t.length>0&&t.length%2===0?t[t.length-1]+=n.duration:n.duration>0&&(t.push(0),t.push(n.duration));continue}for(let d of o)t.push(d)}return t}var G=0,m=class s{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++G,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=S(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let r=Math.max(0,Math.min(1,t?.intensity??.5));for(let a of n)if(a.duration>x&&(a.duration=x),!Number.isFinite(a.duration)||a.duration<0||a.delay!==void 0&&(!Number.isFinite(a.delay)||a.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(s.isSupported&&navigator.vibrate(H(n,r)),!s.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern();let o=(n[0]?.delay??0)===0;if(o&&(this.hapticLabel.click(),this.debug&&this.audioCtx)){let d=Math.max(0,Math.min(1,n[0].intensity??r));this.playClick(d)}await this.runPattern(n,r,o)}}cancel(){this.stopPattern(),s.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t,e){return new Promise(n=>{this.patternResolve=n;let r=[],a=0;for(let l of i){let c=Math.max(0,Math.min(1,l.intensity??t)),u=l.delay??0;u>0&&(a+=u,r.push({end:a,isOn:!1,intensity:0})),a+=l.duration,r.push({end:a,isOn:!0,intensity:c})}let o=a,d=0,p=-1,v=l=>{d===0&&(d=l);let c=l-d;if(c>=o){this.rafId=null,this.patternResolve=null,n();return}let u=r[0];for(let h of r)if(c<h.end){u=h;break}if(u.isOn){let h=L+(1-u.intensity)*F;p===-1?(p=l,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(u.intensity),e=!0)):l-p>=h&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(u.intensity),p=l)}this.rafId=requestAnimationFrame(v)};this.rafId=requestAnimationFrame(v)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let n=0;n<t.length;n++)t[n]=(Math.random()*2-1)*Math.exp(-n/25);this.audioGain.gain.value=.5*i;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let i=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*i,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let e=0;e<t.length;e++)t[e]=(Math.random()*2-1)*Math.exp(-e/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let i=`web-haptics-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",i),t.textContent="Haptic feedback",t.style.position="fixed",t.style.bottom="10px",t.style.left="10px",t.style.padding="5px 10px",t.style.backgroundColor="rgba(0, 0, 0, 0.7)",t.style.color="white",t.style.fontFamily="sans-serif",t.style.fontSize="14px",t.style.borderRadius="4px",t.style.zIndex="9999",t.style.userSelect="none",this.hapticLabel=t;let e=document.createElement("input");e.type="checkbox",e.setAttribute("switch",""),e.id=i,e.style.all="initial",e.style.appearance="auto",this.showSwitch||(t.style.display="none",e.style.display="none"),t.appendChild(e),document.body.appendChild(t),this.domInitialized=!0}};0&&(module.exports={WebHaptics,defaultPatterns,version});
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../package.json","../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["export { version } from \"./../package.json\";\n\nexport { WebHaptics } from \"./lib/web-haptics\";\nexport { defaultPatterns } from \"./lib/web-haptics/patterns\";\nexport type {\n Vibration,\n HapticPattern,\n HapticPreset,\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"./lib/web-haptics/types\";\n","{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.3\",\n \"description\": \"Haptic feedback for the mobile web.\",\n \"homepage\": \"https://haptics.lochie.me\",\n \"sideEffects\": false,\n \"author\": \"Lochie Axon\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/lochie/web-haptics.git\",\n \"directory\": \"packages/web-haptics\"\n },\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./react\": {\n \"types\": \"./dist/react/index.d.ts\",\n \"import\": \"./dist/react/index.mjs\",\n \"require\": \"./dist/react/index.js\"\n },\n \"./vue\": {\n \"types\": \"./dist/vue/index.d.ts\",\n \"import\": \"./dist/vue/index.mjs\",\n \"require\": \"./dist/vue/index.js\"\n },\n \"./svelte\": {\n \"types\": \"./dist/svelte/index.d.ts\",\n \"import\": \"./dist/svelte/index.mjs\",\n \"require\": \"./dist/svelte/index.js\"\n }\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"lint:fix\": \"eslint --fix -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"pre-commit\": \"lint-staged\",\n \"prepublishOnly\": \"pnpm build\"\n },\n \"keywords\": [\n \"haptics\",\n \"vibration\",\n \"feedback\",\n \"web\",\n \"mobile\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"typescript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/lochie/web-haptics/issues\"\n },\n \"files\": [\n \"dist/*\",\n \"LICENSE\",\n \"README.md\"\n ],\n \"peerDependencies\": {\n \"react\": \">=18\",\n \"react-dom\": \">=18\",\n \"vue\": \">=3\",\n \"svelte\": \">=4\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vue\": {\n \"optional\": true\n },\n \"svelte\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.15\",\n \"@typescript-eslint/eslint-plugin\": \"^6.8.0\",\n \"@typescript-eslint/parser\": \"^6.8.0\",\n \"eslint\": \"^9.36.0\",\n \"eslint-config-airbnb\": \"^19.0.4\",\n \"eslint-config-airbnb-typescript\": \"^17.1.0\",\n \"eslint-config-prettier\": \"^9.0.0\",\n \"eslint-plugin-import\": \"^2.28.1\",\n \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n \"eslint-plugin-prettier\": \"^5.0.1\",\n \"eslint-plugin-react\": \"^7.33.2\",\n \"eslint-plugin-react-hooks\": \"^4.6.0\",\n \"prettier\": \"^3.0.3\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"svelte\": \"^4.0.0\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.9.3\",\n \"vue\": \"^3.3.0\"\n }\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n\n await this.runPattern(vibrations, defaultIntensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["src_exports","__export","WebHaptics","defaultPatterns","version","__toCommonJS","version","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","i","source","id","hapticLabel","hapticCheckbox"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../package.json","../src/lib/web-haptics/patterns.ts","../src/lib/web-haptics/index.ts"],"sourcesContent":["export { version } from \"./../package.json\";\n\nexport { WebHaptics } from \"./lib/web-haptics\";\nexport { defaultPatterns } from \"./lib/web-haptics/patterns\";\nexport type {\n Vibration,\n HapticPattern,\n HapticPreset,\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"./lib/web-haptics/types\";\n","{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.4\",\n \"description\": \"Haptic feedback for the mobile web.\",\n \"homepage\": \"https://haptics.lochie.me\",\n \"sideEffects\": false,\n \"author\": \"Lochie Axon\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/lochie/web-haptics.git\",\n \"directory\": \"packages/web-haptics\"\n },\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./react\": {\n \"types\": \"./dist/react/index.d.ts\",\n \"import\": \"./dist/react/index.mjs\",\n \"require\": \"./dist/react/index.js\"\n },\n \"./vue\": {\n \"types\": \"./dist/vue/index.d.ts\",\n \"import\": \"./dist/vue/index.mjs\",\n \"require\": \"./dist/vue/index.js\"\n },\n \"./svelte\": {\n \"types\": \"./dist/svelte/index.d.ts\",\n \"import\": \"./dist/svelte/index.mjs\",\n \"require\": \"./dist/svelte/index.js\"\n }\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"lint:fix\": \"eslint --fix -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"pre-commit\": \"lint-staged\",\n \"prepublishOnly\": \"pnpm build\"\n },\n \"keywords\": [\n \"haptics\",\n \"vibration\",\n \"feedback\",\n \"web\",\n \"mobile\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"typescript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/lochie/web-haptics/issues\"\n },\n \"files\": [\n \"dist/*\",\n \"LICENSE\",\n \"README.md\"\n ],\n \"peerDependencies\": {\n \"react\": \">=18\",\n \"react-dom\": \">=18\",\n \"vue\": \">=3\",\n \"svelte\": \">=4\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vue\": {\n \"optional\": true\n },\n \"svelte\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.15\",\n \"@typescript-eslint/eslint-plugin\": \"^6.8.0\",\n \"@typescript-eslint/parser\": \"^6.8.0\",\n \"eslint\": \"^9.36.0\",\n \"eslint-config-airbnb\": \"^19.0.4\",\n \"eslint-config-airbnb-typescript\": \"^17.1.0\",\n \"eslint-config-prettier\": \"^9.0.0\",\n \"eslint-plugin-import\": \"^2.28.1\",\n \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n \"eslint-plugin-prettier\": \"^5.0.1\",\n \"eslint-plugin-react\": \"^7.33.2\",\n \"eslint-plugin-react-hooks\": \"^4.6.0\",\n \"prettier\": \"^3.0.3\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"svelte\": \"^4.0.0\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.9.3\",\n \"vue\": \"^3.3.0\"\n }\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n const firstDelay = vibrations[0]?.delay ?? 0;\n const firstClickFired = firstDelay === 0;\n\n // Fire first click synchronously to stay within user gesture context\n // (only when the first vibration has no delay)\n if (firstClickFired) {\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n }\n\n await this.runPattern(vibrations, defaultIntensity, firstClickFired);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n firstClickFired: boolean,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n if (!firstClickFired) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n firstClickFired = true;\n }\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["src_exports","__export","WebHaptics","defaultPatterns","version","__toCommonJS","version","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstClickFired","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import{a as e,b as t}from"./chunk-
|
|
2
|
+
import{a as e,b as t}from"./chunk-4RZ72ZZU.mjs";var s="0.0.4";export{t as WebHaptics,e as defaultPatterns,s as version};
|
|
3
3
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.
|
|
1
|
+
{"version":3,"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"web-haptics\",\n \"version\": \"0.0.4\",\n \"description\": \"Haptic feedback for the mobile web.\",\n \"homepage\": \"https://haptics.lochie.me\",\n \"sideEffects\": false,\n \"author\": \"Lochie Axon\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/lochie/web-haptics.git\",\n \"directory\": \"packages/web-haptics\"\n },\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./react\": {\n \"types\": \"./dist/react/index.d.ts\",\n \"import\": \"./dist/react/index.mjs\",\n \"require\": \"./dist/react/index.js\"\n },\n \"./vue\": {\n \"types\": \"./dist/vue/index.d.ts\",\n \"import\": \"./dist/vue/index.mjs\",\n \"require\": \"./dist/vue/index.js\"\n },\n \"./svelte\": {\n \"types\": \"./dist/svelte/index.d.ts\",\n \"import\": \"./dist/svelte/index.mjs\",\n \"require\": \"./dist/svelte/index.js\"\n }\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"lint:fix\": \"eslint --fix -c .eslintrc.cjs ./src/**/*.{ts,tsx}\",\n \"pre-commit\": \"lint-staged\",\n \"prepublishOnly\": \"pnpm build\"\n },\n \"keywords\": [\n \"haptics\",\n \"vibration\",\n \"feedback\",\n \"web\",\n \"mobile\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"typescript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/lochie/web-haptics/issues\"\n },\n \"files\": [\n \"dist/*\",\n \"LICENSE\",\n \"README.md\"\n ],\n \"peerDependencies\": {\n \"react\": \">=18\",\n \"react-dom\": \">=18\",\n \"vue\": \">=3\",\n \"svelte\": \">=4\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vue\": {\n \"optional\": true\n },\n \"svelte\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.15\",\n \"@typescript-eslint/eslint-plugin\": \"^6.8.0\",\n \"@typescript-eslint/parser\": \"^6.8.0\",\n \"eslint\": \"^9.36.0\",\n \"eslint-config-airbnb\": \"^19.0.4\",\n \"eslint-config-airbnb-typescript\": \"^17.1.0\",\n \"eslint-config-prettier\": \"^9.0.0\",\n \"eslint-plugin-import\": \"^2.28.1\",\n \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n \"eslint-plugin-prettier\": \"^5.0.1\",\n \"eslint-plugin-react\": \"^7.33.2\",\n \"eslint-plugin-react-hooks\": \"^4.6.0\",\n \"prettier\": \"^3.0.3\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"svelte\": \"^4.0.0\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.9.3\",\n \"vue\": \"^3.3.0\"\n }\n}\n"],"mappings":";gDAEE,IAAAA,EAAW","names":["version"]}
|
package/dist/react/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var y=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var S=(a,i)=>{for(var t in i)y(a,t,{get:i[t],enumerable:!0})},A=(a,i,t,e)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of I(i))!M.call(a,n)&&n!==t&&y(a,n,{get:()=>i[n],enumerable:!(e=w(i,n))||e.enumerable});return a};var F=a=>A(y({},"__esModule",{value:!0}),a);var B={};S(B,{useWebHaptics:()=>C});module.exports=F(B);var l=require("react");var v={success:{pattern:[{duration:50},{delay:50,duration:50}],description:"A series of taps indicating success."},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],description:"A series of taps indicating a nudge."},error:{pattern:[{duration:50,intensity:.75},{delay:50,duration:50,intensity:.75},{delay:50,duration:50,intensity:.75}],description:"A series of taps indicating a warning or error."},buzz:{pattern:[{duration:1e3,intensity:1}],description:"A long vibration."}};var L=16,O=184,x=1e3,m=20;function P(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=v[a];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${a}"`),null)}if(Array.isArray(a)){if(a.length===0)return{vibrations:[]};if(typeof a[0]=="number"){let i=a,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]})}return{vibrations:t}}return{vibrations:a.map(i=>({...i}))}}return{vibrations:a.pattern.map(i=>({...i}))}}function H(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(m*i)),e=m-t,n=[],r=a;for(;r>=m;)n.push(t),n.push(e),r-=m;if(r>0){let s=Math.max(1,Math.round(r*i));n.push(s);let o=r-s;o>0&&n.push(o)}return n}function k(a,i){let t=[];for(let e=0;e<a.length;e++){let n=a[e],r=Math.max(0,Math.min(1,n.intensity??i)),s=n.delay??0;s>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=s:(t.length===0&&t.push(0),t.push(s)));let o=H(n.duration,r);if(o.length===0){t.length>0&&t.length%2===0?t[t.length-1]+=n.duration:n.duration>0&&(t.push(0),t.push(n.duration));continue}for(let c of o)t.push(c)}return t}var G=0,h=class a{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(i){this.instanceId=++G,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=P(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let r=Math.max(0,Math.min(1,t?.intensity??.5));for(let s of n)if(s.duration>x&&(s.duration=x),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(k(n,r)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern();let o=(n[0]?.delay??0)===0;if(o&&(this.hapticLabel.click(),this.debug&&this.audioCtx)){let c=Math.max(0,Math.min(1,n[0].intensity??r));this.playClick(c)}await this.runPattern(n,r,o)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t,e){return new Promise(n=>{this.patternResolve=n;let r=[],s=0;for(let u of i){let f=Math.max(0,Math.min(1,u.intensity??t)),d=u.delay??0;d>0&&(s+=d,r.push({end:s,isOn:!1,intensity:0})),s+=u.duration,r.push({end:s,isOn:!0,intensity:f})}let o=s,c=0,p=-1,g=u=>{c===0&&(c=u);let f=u-c;if(f>=o){this.rafId=null,this.patternResolve=null,n();return}let d=r[0];for(let b of r)if(f<b.end){d=b;break}if(d.isOn){let b=L+(1-d.intensity)*O;p===-1?(p=u,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),e=!0)):u-p>=b&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),p=u)}this.rafId=requestAnimationFrame(g)};this.rafId=requestAnimationFrame(g)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let n=0;n<t.length;n++)t[n]=(Math.random()*2-1)*Math.exp(-n/25);this.audioGain.gain.value=.5*i;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let i=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*i,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let e=0;e<t.length;e++)t[e]=(Math.random()*2-1)*Math.exp(-e/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let i=`web-haptics-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",i),t.textContent="Haptic feedback",t.style.position="fixed",t.style.bottom="10px",t.style.left="10px",t.style.padding="5px 10px",t.style.backgroundColor="rgba(0, 0, 0, 0.7)",t.style.color="white",t.style.fontFamily="sans-serif",t.style.fontSize="14px",t.style.borderRadius="4px",t.style.zIndex="9999",t.style.userSelect="none",this.hapticLabel=t;let e=document.createElement("input");e.type="checkbox",e.setAttribute("switch",""),e.id=i,e.style.all="initial",e.style.appearance="auto",this.showSwitch||(t.style.display="none",e.style.display="none"),t.appendChild(e),document.body.appendChild(t),this.domInitialized=!0}};function C(a){let i=(0,l.useRef)(null);(0,l.useEffect)(()=>(i.current=new h(a),()=>{i.current?.destroy(),i.current=null}),[]),(0,l.useEffect)(()=>{i.current?.setDebug(a?.debug??!1)},[a?.debug]),(0,l.useEffect)(()=>{i.current?.setShowSwitch(a?.showSwitch??!1)},[a?.showSwitch]);let t=(0,l.useCallback)((r,s)=>i.current?.trigger(r,s),[]),e=(0,l.useCallback)(()=>i.current?.cancel(),[]),n=h.isSupported;return{trigger:t,cancel:e,isSupported:n}}0&&(module.exports={useWebHaptics});
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts","../../src/react/useWebHaptics.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts"],"sourcesContent":["export { useWebHaptics } from \"./useWebHaptics\";\n","\"use client\";\n\nimport { useRef, useEffect, useCallback } from \"react\";\nimport { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function useWebHaptics(options?: WebHapticsOptions) {\n const instanceRef = useRef<WebHaptics | null>(null);\n\n useEffect(() => {\n instanceRef.current = new WebHaptics(options);\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n }, []);\n\n useEffect(() => {\n instanceRef.current?.setDebug(options?.debug ?? false);\n }, [options?.debug]);\n\n useEffect(() => {\n instanceRef.current?.setShowSwitch(options?.showSwitch ?? false);\n }, [options?.showSwitch]);\n\n const trigger = useCallback(\n (input?: HapticInput, options?: TriggerOptions) =>\n instanceRef.current?.trigger(input, options),\n [],\n );\n\n const cancel = useCallback(() => instanceRef.current?.cancel(), []);\n\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, isSupported };\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n\n await this.runPattern(vibrations, defaultIntensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["react_exports","__export","useWebHaptics","__toCommonJS","import_react","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox","useWebHaptics","options","instanceRef","WebHaptics","trigger","input","cancel","isSupported"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts","../../src/react/useWebHaptics.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts"],"sourcesContent":["export { useWebHaptics } from \"./useWebHaptics\";\n","\"use client\";\n\nimport { useRef, useEffect, useCallback } from \"react\";\nimport { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function useWebHaptics(options?: WebHapticsOptions) {\n const instanceRef = useRef<WebHaptics | null>(null);\n\n useEffect(() => {\n instanceRef.current = new WebHaptics(options);\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n }, []);\n\n useEffect(() => {\n instanceRef.current?.setDebug(options?.debug ?? false);\n }, [options?.debug]);\n\n useEffect(() => {\n instanceRef.current?.setShowSwitch(options?.showSwitch ?? false);\n }, [options?.showSwitch]);\n\n const trigger = useCallback(\n (input?: HapticInput, options?: TriggerOptions) =>\n instanceRef.current?.trigger(input, options),\n [],\n );\n\n const cancel = useCallback(() => instanceRef.current?.cancel(), []);\n\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, isSupported };\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [{ duration: 50 }, { delay: 50, duration: 50 }],\n description: \"A series of taps indicating success.\",\n },\n nudge: {\n pattern: [\n { duration: 80, intensity: 0.8 },\n { delay: 80, duration: 50, intensity: 0.3 },\n ],\n description: \"A series of taps indicating a nudge.\",\n },\n error: {\n pattern: [\n { duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n { delay: 50, duration: 50, intensity: 0.75 },\n ],\n description: \"A series of taps indicating a warning or error.\",\n },\n buzz: {\n pattern: [{ duration: 1000, intensity: 1 }],\n description: \"A long vibration.\",\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type {\n HapticInput,\n TriggerOptions,\n Vibration,\n WebHapticsOptions,\n} from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\n/** Convert any HapticInput into a Vibration array. */\nfunction normalizeInput(input: HapticInput): {\n vibrations: Vibration[];\n} | null {\n if (typeof input === \"number\") {\n return { vibrations: [{ duration: input }] };\n }\n\n if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return null;\n }\n return { vibrations: preset.pattern.map((v) => ({ ...v })) };\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) return { vibrations: [] };\n\n // number[] shorthand — alternating on/off\n if (typeof input[0] === \"number\") {\n const nums = input as number[];\n const vibrations: Vibration[] = [];\n for (let i = 0; i < nums.length; i += 2) {\n const delay = i > 0 ? nums[i - 1]! : 0;\n vibrations.push({\n ...(delay > 0 && { delay }),\n duration: nums[i]!,\n });\n }\n return { vibrations };\n }\n\n // Vibration[]\n return { vibrations: (input as Vibration[]).map((v) => ({ ...v })) };\n }\n\n // HapticPreset\n return { vibrations: input.pattern.map((v) => ({ ...v })) };\n}\n\n/**\n * Apply PWM modulation to a single vibration duration at a given intensity.\n * Returns the flat on/off segments for this vibration.\n */\nfunction modulateVibration(duration: number, intensity: number): number[] {\n if (intensity >= 1) return [duration];\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n let remaining = duration;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n\n return result;\n}\n\n/**\n * Convert Vibration[] to the flat number[] pattern for navigator.vibrate(),\n * applying per-vibration PWM intensity modulation.\n */\nfunction toVibratePattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n): number[] {\n const result: number[] = [];\n\n for (let i = 0; i < vibrations.length; i++) {\n const vib = vibrations[i]!;\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n\n // Prepend delay: merge into trailing off-time or add new gap\n if (delay > 0) {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += delay;\n } else {\n if (result.length === 0) result.push(0);\n result.push(delay);\n }\n }\n\n const modulated = modulateVibration(vib.duration, intensity);\n\n if (modulated.length === 0) {\n // Zero intensity — treat vibration as silence\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += vib.duration;\n } else if (vib.duration > 0) {\n result.push(0);\n result.push(vib.duration);\n }\n continue;\n }\n\n // Append modulated vibration segments\n for (const seg of modulated) {\n result.push(seg);\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" &&\n typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [{ duration: 10 }],\n options?: TriggerOptions,\n ): Promise<void> {\n const normalized = normalizeInput(input);\n if (!normalized) return;\n\n const { vibrations } = normalized;\n if (vibrations.length === 0) return;\n\n const defaultIntensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? 0.5),\n );\n\n // Validate and clamp durations\n for (const vib of vibrations) {\n if (vib.duration > MAX_PHASE_MS) vib.duration = MAX_PHASE_MS;\n if (\n !Number.isFinite(vib.duration) ||\n vib.duration < 0 ||\n (vib.delay !== undefined &&\n (!Number.isFinite(vib.delay) || vib.delay < 0))\n ) {\n console.warn(\n `[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(toVibratePattern(vibrations, defaultIntensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n const firstDelay = vibrations[0]?.delay ?? 0;\n const firstClickFired = firstDelay === 0;\n\n // Fire first click synchronously to stay within user gesture context\n // (only when the first vibration has no delay)\n if (firstClickFired) {\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n const firstIntensity = Math.max(\n 0,\n Math.min(1, vibrations[0]!.intensity ?? defaultIntensity),\n );\n this.playClick(firstIntensity);\n }\n }\n\n await this.runPattern(vibrations, defaultIntensity, firstClickFired);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(\n vibrations: Vibration[],\n defaultIntensity: number,\n firstClickFired: boolean,\n ): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n // Build phase boundaries: each vibration has an optional delay then an \"on\" phase\n const phases: { end: number; isOn: boolean; intensity: number }[] = [];\n let cumulative = 0;\n for (const vib of vibrations) {\n const intensity = Math.max(\n 0,\n Math.min(1, vib.intensity ?? defaultIntensity),\n );\n const delay = vib.delay ?? 0;\n if (delay > 0) {\n cumulative += delay;\n phases.push({ end: cumulative, isOn: false, intensity: 0 });\n }\n cumulative += vib.duration;\n phases.push({ end: cumulative, isOn: true, intensity });\n }\n const totalDuration = cumulative;\n\n let startTime = 0;\n let lastToggleTime = -1;\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n // Find current phase\n let phase = phases[0]!;\n for (const p of phases) {\n if (elapsed < p.end) {\n phase = p;\n break;\n }\n }\n\n if (phase.isOn) {\n const toggleInterval =\n TOGGLE_MIN + (1 - phase.intensity) * TOGGLE_MAX;\n\n if (lastToggleTime === -1) {\n lastToggleTime = time;\n if (!firstClickFired) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n firstClickFired = true;\n }\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(phase.intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (\n !this.audioCtx ||\n !this.audioFilter ||\n !this.audioGain ||\n !this.audioBuffer\n )\n return;\n\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["react_exports","__export","useWebHaptics","__toCommonJS","import_react","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","normalizeInput","input","preset","defaultPatterns","v","nums","vibrations","i","delay","modulateVibration","duration","intensity","onTime","offTime","result","remaining","remOn","remOff","toVibratePattern","defaultIntensity","vib","modulated","seg","instanceCounter","WebHaptics","_WebHaptics","options","normalized","firstClickFired","firstIntensity","debug","show","checkbox","resolve","phases","cumulative","totalDuration","startTime","lastToggleTime","loop","time","elapsed","phase","p","toggleInterval","data","source","id","hapticLabel","hapticCheckbox","useWebHaptics","options","instanceRef","WebHaptics","trigger","input","cancel","isSupported"]}
|
package/dist/react/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import{b as r}from"../chunk-
|
|
2
|
+
import{b as r}from"../chunk-4RZ72ZZU.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
|
package/dist/svelte/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var y=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var M=(a,i)=>{for(var t in i)y(a,t,{get:i[t],enumerable:!0})},A=(a,i,t,e)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of w(i))!I.call(a,n)&&n!==t&&y(a,n,{get:()=>i[n],enumerable:!(e=C(i,n))||e.enumerable});return a};var S=a=>A(y({},"__esModule",{value:!0}),a);var G={};M(G,{createWebHaptics:()=>x});module.exports=S(G);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,L=184,v=1e3,b=20;function O(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 P(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(b*i)),e=b-t,n=[],o=a;for(;o>=b;)n.push(t),n.push(e),o-=b;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 H(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=P(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 u of r)t.push(u)}return t}var k=0,c=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=O(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>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(H(n,o)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern();let r=(n[0]?.delay??0)===0;if(r&&(this.hapticLabel.click(),this.debug&&this.audioCtx)){let u=Math.max(0,Math.min(1,n[0].intensity??o));this.playClick(u)}await this.runPattern(n,o,r)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t,e){return new Promise(n=>{this.patternResolve=n;let o=[],s=0;for(let l of i){let p=Math.max(0,Math.min(1,l.intensity??t)),d=l.delay??0;d>0&&(s+=d,o.push({end:s,isOn:!1,intensity:0})),s+=l.duration,o.push({end:s,isOn:!0,intensity:p})}let r=s,u=0,h=-1,m=l=>{u===0&&(u=l);let p=l-u;if(p>=r){this.rafId=null,this.patternResolve=null,n();return}let d=o[0];for(let f of o)if(p<f.end){d=f;break}if(d.isOn){let f=F+(1-d.intensity)*L;h===-1?(h=l,e||(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),e=!0)):l-h>=f&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),h=l)}this.rafId=requestAnimationFrame(m)};this.rafId=requestAnimationFrame(m)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let 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=new c(a),t=(r,u)=>i.trigger(r,u),e=()=>i.cancel(),n=()=>i.destroy(),o=r=>i.setDebug(r),s=c.isSupported;return{trigger:t,cancel:e,destroy:n,setDebug:o,isSupported:s}}0&&(module.exports={createWebHaptics});
|
|
2
2
|
//# sourceMappingURL=index.js.map
|