web-haptics 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
1
- type HapticPattern = number[];
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
2
7
  interface HapticPreset {
3
- pattern: HapticPattern;
8
+ pattern: Vibration[];
4
9
  description: string;
5
- intensity: number;
6
10
  }
7
11
  type HapticInput = number | string | HapticPattern | HapticPreset;
8
12
  interface TriggerOptions {
@@ -1,2 +1,2 @@
1
- "use strict";var f=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var A=(s,t)=>{for(var e in t)f(s,e,{get:t[e],enumerable:!0})},P=(s,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of w(t))!I.call(s,a)&&a!==e&&f(s,a,{get:()=>t[a],enumerable:!(i=C(t,a))||i.enumerable});return s};var S=s=>P(f({},"__esModule",{value:!0}),s);var G={};A(G,{createWebHaptics:()=>x});module.exports=S(G);var g={success:{pattern:[50,50,50],description:"A series of taps indicating success.",intensity:.5},nudge:{pattern:[80,100,200],description:"A series of taps indicating a nudge.",intensity:.5},error:{pattern:[50,50,50,50,50],description:"A series of taps indicating a warning or error.",intensity:.75},buzz:{pattern:[1e3],description:"A long vibration.",intensity:1}};var F=16,L=184,v=1e3,h=20;function M(s,t){if(t>=1)return s;if(t<=0)return[];let e=Math.max(1,Math.round(h*t)),i=h-e,a=[];for(let o=0;o<s.length;o++){let n=s[o];if(o%2===0){let r=n;for(;r>=h;)a.push(e),a.push(i),r-=h;if(r>0){let l=Math.max(1,Math.round(r*t));a.push(l);let u=r-l;u>0&&a.push(u)}}else a.length>0&&a.length%2===0?a[a.length-1]+=n:a.push(n)}return a}var H=0,c=class s{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(t){this.instanceId=++H,this.debug=t?.debug??!1,this.showSwitch=t?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(t=[10],e){let i,a=.5;if(typeof t=="number")i=[t];else if(typeof t=="string"){let n=g[t];if(!n){console.warn(`[web-haptics] Unknown preset: "${t}"`);return}i=[...n.pattern],a=n.intensity}else Array.isArray(t)?i=[...t]:(i=[...t.pattern],a=t.intensity);let o=Math.max(0,Math.min(1,e?.intensity??a));for(let n=0;n<i.length;n++)if(n%2===0&&i[n]>v&&(i[n]=v),!Number.isFinite(i[n])||i[n]<0){console.warn(`[web-haptics] Invalid value at index ${n}: ${i[n]}. Pattern values must be finite non-negative numbers.`);return}if(s.isSupported&&navigator.vibrate(M(i,o)),!s.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx&&this.playClick(o),await this.runPattern(i,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(t){this.debug=t,!t&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(t){if(this.showSwitch=t,this.hapticLabel){let e=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=t?"":"none",e&&(e.style.display=t?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(t,e){return new Promise(i=>{this.patternResolve=i;let a=[],o=0;for(let d of t)o+=d,a.push(o);let n=o,r=F+(1-e)*L,l=0,u=-1,b=d=>{l===0&&(l=d);let m=d-l;if(m>=n){this.rafId=null,this.patternResolve=null,i();return}let y=0;for(let p=0;p<a.length;p++)if(m<a[p]){y=p;break}y%2===0&&(u===-1?u=d:d-u>=r&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(e),u=d)),this.rafId=requestAnimationFrame(b)};this.rafId=requestAnimationFrame(b)})}playClick(t){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;this.audioGain.gain.value=.5*t;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let t=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*t,this.audioCtx.sampleRate);let e=this.audioBuffer.getChannelData(0);for(let i=0;i<e.length;i++)e[i]=(Math.random()*2-1)*Math.exp(-i/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let t=`web-haptics-${this.instanceId}`,e=document.createElement("label");e.setAttribute("for",t),e.textContent="Haptic feedback",e.style.position="fixed",e.style.bottom="10px",e.style.left="10px",e.style.padding="5px 10px",e.style.backgroundColor="rgba(0, 0, 0, 0.7)",e.style.color="white",e.style.fontFamily="sans-serif",e.style.fontSize="14px",e.style.borderRadius="4px",e.style.zIndex="9999",e.style.userSelect="none",this.hapticLabel=e;let i=document.createElement("input");i.type="checkbox",i.setAttribute("switch",""),i.id=t,i.style.all="initial",i.style.appearance="auto",this.showSwitch||(e.style.display="none",i.style.display="none"),e.appendChild(i),document.body.appendChild(e),this.domInitialized=!0}};function x(s){let t=new c(s),e=(r,l)=>t.trigger(r,l),i=()=>t.cancel(),a=()=>t.destroy(),o=r=>t.setDebug(r),n=c.isSupported;return{trigger:e,cancel:i,destroy:a,setDebug:o,isSupported:n}}0&&(module.exports={createWebHaptics});
1
+ "use strict";var b=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var w=Object.prototype.hasOwnProperty;var I=(a,i)=>{for(var t in i)b(a,t,{get:i[t],enumerable:!0})},M=(a,i,t,e)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of C(i))!w.call(a,n)&&n!==t&&b(a,n,{get:()=>i[n],enumerable:!(e=x(i,n))||e.enumerable});return a};var A=a=>M(b({},"__esModule",{value:!0}),a);var G={};I(G,{createWebHaptics:()=>v});module.exports=A(G);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 S=16,F=184,g=1e3,f=20;function O(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 P(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(f*i)),e=f-t,n=[],o=a;for(;o>=f;)n.push(t),n.push(e),o-=f;if(o>0){let s=Math.max(1,Math.round(o*i));n.push(s);let r=o-s;r>0&&n.push(r)}return n}function L(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 l of r)t.push(l)}return t}var H=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=++H,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>g&&(s.duration=g),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(L(n,o)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;if(this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx){let s=Math.max(0,Math.min(1,n[0].intensity??o));this.playClick(s)}await this.runPattern(n,o)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t){return new Promise(e=>{this.patternResolve=e;let n=[],o=0;for(let u of i){let h=Math.max(0,Math.min(1,u.intensity??t)),d=u.delay??0;d>0&&(o+=d,n.push({end:o,isOn:!1,intensity:0})),o+=u.duration,n.push({end:o,isOn:!0,intensity:h})}let s=o,r=0,l=-1,m=u=>{r===0&&(r=u);let h=u-r;if(h>=s){this.rafId=null,this.patternResolve=null,e();return}let d=n[0];for(let p of n)if(h<p.end){d=p;break}if(d.isOn){let p=S+(1-d.intensity)*F;l===-1?l=u:u-l>=p&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),l=u)}this.rafId=requestAnimationFrame(m)};this.rafId=requestAnimationFrame(m)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let 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 v(a){let i=new c(a),t=(r,l)=>i.trigger(r,l),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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/svelte/index.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts","../../src/svelte/createWebHaptics.ts"],"sourcesContent":["export { createWebHaptics } from \"./createWebHaptics\";\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [50, 50, 50],\n description: \"A series of taps indicating success.\",\n intensity: 0.5,\n },\n nudge: {\n pattern: [80, 100, 200],\n description: \"A series of taps indicating a nudge.\",\n intensity: 0.5,\n },\n error: {\n pattern: [50, 50, 50, 50, 50],\n description: \"A series of taps indicating a warning or error.\",\n intensity: 0.75,\n },\n buzz: {\n pattern: [1000],\n description: \"A long vibration.\",\n intensity: 1,\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type { HapticInput, TriggerOptions, WebHapticsOptions } from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\nfunction modulatePattern(pattern: number[], intensity: number): number[] {\n if (intensity >= 1) return pattern;\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n for (let i = 0; i < pattern.length; i++) {\n const dur = pattern[i]!;\n\n if (i % 2 === 0) {\n let remaining = dur;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n } else {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += dur;\n } else {\n result.push(dur);\n }\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" && typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [10],\n options?: TriggerOptions,\n ): Promise<void> {\n let pattern: number[];\n let defaultIntensity = 0.5;\n\n if (typeof input === \"number\") {\n pattern = [input];\n } else if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return;\n }\n pattern = [...preset.pattern];\n defaultIntensity = preset.intensity;\n } else if (Array.isArray(input)) {\n pattern = [...input];\n } else {\n pattern = [...input.pattern];\n defaultIntensity = input.intensity;\n }\n\n const intensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? defaultIntensity),\n );\n\n for (let i = 0; i < pattern.length; i++) {\n if (i % 2 === 0 && pattern[i]! > MAX_PHASE_MS) {\n pattern[i] = MAX_PHASE_MS;\n }\n if (!Number.isFinite(pattern[i]) || pattern[i]! < 0) {\n console.warn(\n `[web-haptics] Invalid value at index ${i}: ${pattern[i]}. Pattern values must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(modulatePattern(pattern, intensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n // (iOS blocks haptics from programmatic clicks outside gesture handlers)\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n\n await this.runPattern(pattern, intensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(pattern: number[], intensity: number): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n const phases: number[] = [];\n let cumulative = 0;\n for (const p of pattern) {\n cumulative += p;\n phases.push(cumulative);\n }\n const totalDuration = cumulative;\n\n const toggleInterval = TOGGLE_MIN + (1 - intensity) * TOGGLE_MAX;\n let startTime = 0;\n let lastToggleTime = -1; // -1 signals first click was already fired synchronously\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n let phaseIndex = 0;\n for (let i = 0; i < phases.length; i++) {\n if (elapsed < phases[i]!) {\n phaseIndex = i;\n break;\n }\n }\n\n if (phaseIndex % 2 === 0) {\n if (lastToggleTime === -1) {\n // Skip first toggle — already fired synchronously\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (!this.audioCtx || !this.audioFilter || !this.audioGain || !this.audioBuffer) return;\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n","import { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function createWebHaptics(options?: WebHapticsOptions) {\n const instance = new WebHaptics(options);\n\n const trigger = (input?: HapticInput, options?: TriggerOptions) =>\n instance.trigger(input, options);\n const cancel = () => instance.cancel();\n const destroy = () => instance.destroy();\n const setDebug = (debug: boolean) => instance.setDebug(debug);\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, destroy, setDebug, isSupported };\n}\n"],"mappings":"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","names":["svelte_exports","__export","createWebHaptics","__toCommonJS","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","modulatePattern","pattern","intensity","onTime","offTime","result","i","dur","remaining","remOn","remOff","instanceCounter","WebHaptics","_WebHaptics","options","input","defaultIntensity","preset","defaultPatterns","debug","show","checkbox","resolve","phases","cumulative","p","totalDuration","toggleInterval","startTime","lastToggleTime","loop","time","elapsed","phaseIndex","source","duration","data","id","hapticLabel","hapticCheckbox","createWebHaptics","options","instance","WebHaptics","trigger","input","cancel","destroy","setDebug","debug","isSupported"]}
1
+ {"version":3,"sources":["../../src/svelte/index.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts","../../src/svelte/createWebHaptics.ts"],"sourcesContent":["export { createWebHaptics } from \"./createWebHaptics\";\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","import { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function createWebHaptics(options?: WebHapticsOptions) {\n const instance = new WebHaptics(options);\n\n const trigger = (input?: HapticInput, options?: TriggerOptions) =>\n instance.trigger(input, options);\n const cancel = () => instance.cancel();\n const destroy = () => instance.destroy();\n const setDebug = (debug: boolean) => instance.setDebug(debug);\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, destroy, setDebug, isSupported };\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,sBAAAE,IAAA,eAAAC,EAAAH,GCEO,IAAMI,EAAkB,CAC7B,QAAS,CACP,QAAS,CAAC,CAAE,SAAU,EAAG,EAAG,CAAE,MAAO,GAAI,SAAU,EAAG,CAAC,EACvD,YAAa,sCACf,EACA,MAAO,CACL,QAAS,CACP,CAAE,SAAU,GAAI,UAAW,EAAI,EAC/B,CAAE,MAAO,GAAI,SAAU,GAAI,UAAW,EAAI,CAC5C,EACA,YAAa,sCACf,EACA,MAAO,CACL,QAAS,CACP,CAAE,SAAU,GAAI,UAAW,GAAK,EAChC,CAAE,MAAO,GAAI,SAAU,GAAI,UAAW,GAAK,EAC3C,CAAE,MAAO,GAAI,SAAU,GAAI,UAAW,GAAK,CAC7C,EACA,YAAa,iDACf,EACA,KAAM,CACJ,QAAS,CAAC,CAAE,SAAU,IAAM,UAAW,CAAE,CAAC,EAC1C,YAAa,mBACf,CACF,EClBA,IAAMC,EAAa,GACbC,EAAa,IACbC,EAAe,IACfC,EAAY,GAGlB,SAASC,EAAeC,EAEf,CACP,GAAI,OAAOA,GAAU,SACnB,MAAO,CAAE,WAAY,CAAC,CAAE,SAAUA,CAAM,CAAC,CAAE,EAG7C,GAAI,OAAOA,GAAU,SAAU,CAC7B,IAAMC,EAASC,EAAgBF,CAAqC,EACpE,OAAKC,EAIE,CAAE,WAAYA,EAAO,QAAQ,IAAKE,IAAO,CAAE,GAAGA,CAAE,EAAE,CAAE,GAHzD,QAAQ,KAAK,kCAAkCH,CAAK,GAAG,EAChD,KAGX,CAEA,GAAI,MAAM,QAAQA,CAAK,EAAG,CACxB,GAAIA,EAAM,SAAW,EAAG,MAAO,CAAE,WAAY,CAAC,CAAE,EAGhD,GAAI,OAAOA,EAAM,CAAC,GAAM,SAAU,CAChC,IAAMI,EAAOJ,EACPK,EAA0B,CAAC,EACjC,QAASC,EAAI,EAAGA,EAAIF,EAAK,OAAQE,GAAK,EAAG,CACvC,IAAMC,EAAQD,EAAI,EAAIF,EAAKE,EAAI,CAAC,EAAK,EACrCD,EAAW,KAAK,CACd,GAAIE,EAAQ,GAAK,CAAE,MAAAA,CAAM,EACzB,SAAUH,EAAKE,CAAC,CAClB,CAAC,CACH,CACA,MAAO,CAAE,WAAAD,CAAW,CACtB,CAGA,MAAO,CAAE,WAAaL,EAAsB,IAAKG,IAAO,CAAE,GAAGA,CAAE,EAAE,CAAE,CACrE,CAGA,MAAO,CAAE,WAAYH,EAAM,QAAQ,IAAKG,IAAO,CAAE,GAAGA,CAAE,EAAE,CAAE,CAC5D,CAMA,SAASK,EAAkBC,EAAkBC,EAA6B,CACxE,GAAIA,GAAa,EAAG,MAAO,CAACD,CAAQ,EACpC,GAAIC,GAAa,EAAG,MAAO,CAAC,EAE5B,IAAMC,EAAS,KAAK,IAAI,EAAG,KAAK,MAAMb,EAAYY,CAAS,CAAC,EACtDE,EAAUd,EAAYa,EACtBE,EAAmB,CAAC,EAEtBC,EAAYL,EAChB,KAAOK,GAAahB,GAClBe,EAAO,KAAKF,CAAM,EAClBE,EAAO,KAAKD,CAAO,EACnBE,GAAahB,EAEf,GAAIgB,EAAY,EAAG,CACjB,IAAMC,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAMD,EAAYJ,CAAS,CAAC,EAC3DG,EAAO,KAAKE,CAAK,EACjB,IAAMC,EAASF,EAAYC,EACvBC,EAAS,GAAGH,EAAO,KAAKG,CAAM,CACpC,CAEA,OAAOH,CACT,CAMA,SAASI,EACPZ,EACAa,EACU,CACV,IAAML,EAAmB,CAAC,EAE1B,QAASP,EAAI,EAAGA,EAAID,EAAW,OAAQC,IAAK,CAC1C,IAAMa,EAAMd,EAAWC,CAAC,EAClBI,EAAY,KAAK,IACrB,EACA,KAAK,IAAI,EAAGS,EAAI,WAAaD,CAAgB,CAC/C,EACMX,EAAQY,EAAI,OAAS,EAGvBZ,EAAQ,IACNM,EAAO,OAAS,GAAKA,EAAO,OAAS,IAAM,EAC7CA,EAAOA,EAAO,OAAS,CAAC,GAAMN,GAE1BM,EAAO,SAAW,GAAGA,EAAO,KAAK,CAAC,EACtCA,EAAO,KAAKN,CAAK,IAIrB,IAAMa,EAAYZ,EAAkBW,EAAI,SAAUT,CAAS,EAE3D,GAAIU,EAAU,SAAW,EAAG,CAEtBP,EAAO,OAAS,GAAKA,EAAO,OAAS,IAAM,EAC7CA,EAAOA,EAAO,OAAS,CAAC,GAAMM,EAAI,SACzBA,EAAI,SAAW,IACxBN,EAAO,KAAK,CAAC,EACbA,EAAO,KAAKM,EAAI,QAAQ,GAE1B,QACF,CAGA,QAAWE,KAAOD,EAChBP,EAAO,KAAKQ,CAAG,CAEnB,CAEA,OAAOR,CACT,CAEA,IAAIS,EAAkB,EAETC,EAAN,MAAMC,CAAW,CACd,YAAuC,KACvC,eAAiB,GACjB,WACA,MACA,WACA,MAAuB,KACvB,eAAsC,KACtC,SAAgC,KAChC,YAAuC,KACvC,UAA6B,KAC7B,YAAkC,KAE1C,YAAYC,EAA6B,CACvC,KAAK,WAAa,EAAEH,EACpB,KAAK,MAAQG,GAAS,OAAS,GAC/B,KAAK,WAAaA,GAAS,YAAc,EAC3C,CAEA,OAAgB,YACd,OAAO,UAAc,KACrB,OAAO,UAAU,SAAY,WAE/B,MAAM,QACJzB,EAAqB,CAAC,CAAE,SAAU,EAAG,CAAC,EACtCyB,EACe,CACf,IAAMC,EAAa3B,EAAeC,CAAK,EACvC,GAAI,CAAC0B,EAAY,OAEjB,GAAM,CAAE,WAAArB,CAAW,EAAIqB,EACvB,GAAIrB,EAAW,SAAW,EAAG,OAE7B,IAAMa,EAAmB,KAAK,IAC5B,EACA,KAAK,IAAI,EAAGO,GAAS,WAAa,EAAG,CACvC,EAGA,QAAWN,KAAOd,EAEhB,GADIc,EAAI,SAAWtB,IAAcsB,EAAI,SAAWtB,GAE9C,CAAC,OAAO,SAASsB,EAAI,QAAQ,GAC7BA,EAAI,SAAW,GACdA,EAAI,QAAU,SACZ,CAAC,OAAO,SAASA,EAAI,KAAK,GAAKA,EAAI,MAAQ,GAC9C,CACA,QAAQ,KACN,mGACF,EACA,MACF,CAOF,GAJIK,EAAW,aACb,UAAU,QAAQP,EAAiBZ,EAAYa,CAAgB,CAAC,EAG9D,CAACM,EAAW,aAAe,KAAK,MAAO,CAEzC,GADA,KAAK,UAAU,EACX,CAAC,KAAK,YAAa,OAUvB,GARI,KAAK,OACP,MAAM,KAAK,YAAY,EAGzB,KAAK,YAAY,EAGjB,KAAK,YAAY,MAAM,EACnB,KAAK,OAAS,KAAK,SAAU,CAC/B,IAAMG,EAAiB,KAAK,IAC1B,EACA,KAAK,IAAI,EAAGtB,EAAW,CAAC,EAAG,WAAaa,CAAgB,CAC1D,EACA,KAAK,UAAUS,CAAc,CAC/B,CAEA,MAAM,KAAK,WAAWtB,EAAYa,CAAgB,CACpD,CACF,CAEA,QAAe,CACb,KAAK,YAAY,EACbM,EAAW,aACb,UAAU,QAAQ,CAAC,CAEvB,CAEA,SAAgB,CACd,KAAK,YAAY,EACb,KAAK,cACP,KAAK,YAAY,OAAO,EACxB,KAAK,YAAc,KACnB,KAAK,eAAiB,IAEpB,KAAK,WACP,KAAK,SAAS,MAAM,EACpB,KAAK,SAAW,KAChB,KAAK,YAAc,KACnB,KAAK,UAAY,KACjB,KAAK,YAAc,KAEvB,CAEA,SAASI,EAAsB,CAC7B,KAAK,MAAQA,EACT,CAACA,GAAS,KAAK,WACjB,KAAK,SAAS,MAAM,EACpB,KAAK,SAAW,KAChB,KAAK,YAAc,KACnB,KAAK,UAAY,KACjB,KAAK,YAAc,KAEvB,CAEA,cAAcC,EAAqB,CAEjC,GADA,KAAK,WAAaA,EACd,KAAK,YAAa,CACpB,IAAMC,EAAW,KAAK,YAAY,cAAc,OAAO,EACvD,KAAK,YAAY,MAAM,QAAUD,EAAO,GAAK,OACzCC,IAAUA,EAAS,MAAM,QAAUD,EAAO,GAAK,OACrD,CACF,CAEQ,aAAoB,CACtB,KAAK,QAAU,OACjB,qBAAqB,KAAK,KAAK,EAC/B,KAAK,MAAQ,MAEf,KAAK,iBAAiB,EACtB,KAAK,eAAiB,IACxB,CAEQ,WACNxB,EACAa,EACe,CACf,OAAO,IAAI,QAASa,GAAY,CAC9B,KAAK,eAAiBA,EAGtB,IAAMC,EAA8D,CAAC,EACjEC,EAAa,EACjB,QAAWd,KAAOd,EAAY,CAC5B,IAAMK,EAAY,KAAK,IACrB,EACA,KAAK,IAAI,EAAGS,EAAI,WAAaD,CAAgB,CAC/C,EACMX,EAAQY,EAAI,OAAS,EACvBZ,EAAQ,IACV0B,GAAc1B,EACdyB,EAAO,KAAK,CAAE,IAAKC,EAAY,KAAM,GAAO,UAAW,CAAE,CAAC,GAE5DA,GAAcd,EAAI,SAClBa,EAAO,KAAK,CAAE,IAAKC,EAAY,KAAM,GAAM,UAAAvB,CAAU,CAAC,CACxD,CACA,IAAMwB,EAAgBD,EAElBE,EAAY,EACZC,EAAiB,GAEfC,EAAQC,GAAiB,CACzBH,IAAc,IAAGA,EAAYG,GACjC,IAAMC,EAAUD,EAAOH,EAEvB,GAAII,GAAWL,EAAe,CAC5B,KAAK,MAAQ,KACb,KAAK,eAAiB,KACtBH,EAAQ,EACR,MACF,CAGA,IAAIS,EAAQR,EAAO,CAAC,EACpB,QAAW,KAAKA,EACd,GAAIO,EAAU,EAAE,IAAK,CACnBC,EAAQ,EACR,KACF,CAGF,GAAIA,EAAM,KAAM,CACd,IAAMC,EACJ9C,GAAc,EAAI6C,EAAM,WAAa5C,EAEnCwC,IAAmB,GACrBA,EAAiBE,EACRA,EAAOF,GAAkBK,IAClC,KAAK,aAAa,MAAM,EACpB,KAAK,OAAS,KAAK,UACrB,KAAK,UAAUD,EAAM,SAAS,EAEhCJ,EAAiBE,EAErB,CAEA,KAAK,MAAQ,sBAAsBD,CAAI,CACzC,EACA,KAAK,MAAQ,sBAAsBA,CAAI,CACzC,CAAC,CACH,CAEQ,UAAU3B,EAAyB,CACzC,GACE,CAAC,KAAK,UACN,CAAC,KAAK,aACN,CAAC,KAAK,WACN,CAAC,KAAK,YAEN,OAEF,IAAMgC,EAAO,KAAK,YAAY,eAAe,CAAC,EAC9C,QAASpC,EAAI,EAAGA,EAAIoC,EAAK,OAAQpC,IAC/BoC,EAAKpC,CAAC,GAAK,KAAK,OAAO,EAAI,EAAI,GAAK,KAAK,IAAI,CAACA,EAAI,EAAE,EAGtD,KAAK,UAAU,KAAK,MAAQ,GAAMI,EAElC,IAAMiC,EAAS,KAAK,SAAS,mBAAmB,EAChDA,EAAO,OAAS,KAAK,YACrBA,EAAO,QAAQ,KAAK,WAAW,EAC/BA,EAAO,QAAU,IAAMA,EAAO,WAAW,EACzCA,EAAO,MAAM,CACf,CAEA,MAAc,aAA6B,CACzC,GAAI,CAAC,KAAK,UAAY,OAAO,aAAiB,IAAa,CACzD,KAAK,SAAW,IAAI,aAEpB,KAAK,YAAc,KAAK,SAAS,mBAAmB,EACpD,KAAK,YAAY,KAAO,WACxB,KAAK,YAAY,UAAU,MAAQ,IACnC,KAAK,YAAY,EAAE,MAAQ,EAE3B,KAAK,UAAY,KAAK,SAAS,WAAW,EAC1C,KAAK,YAAY,QAAQ,KAAK,SAAS,EACvC,KAAK,UAAU,QAAQ,KAAK,SAAS,WAAW,EAEhD,IAAMlC,EAAW,KACjB,KAAK,YAAc,KAAK,SAAS,aAC/B,EACA,KAAK,SAAS,WAAaA,EAC3B,KAAK,SAAS,UAChB,EACA,IAAMiC,EAAO,KAAK,YAAY,eAAe,CAAC,EAC9C,QAASpC,EAAI,EAAGA,EAAIoC,EAAK,OAAQpC,IAC/BoC,EAAKpC,CAAC,GAAK,KAAK,OAAO,EAAI,EAAI,GAAK,KAAK,IAAI,CAACA,EAAI,EAAE,CAExD,CACI,KAAK,UAAU,QAAU,aAC3B,MAAM,KAAK,SAAS,OAAO,CAE/B,CAEQ,WAAkB,CAExB,GADI,KAAK,gBACL,OAAO,SAAa,IAAa,OAErC,IAAMsC,EAAK,eAAe,KAAK,UAAU,GAEnCC,EAAc,SAAS,cAAc,OAAO,EAClDA,EAAY,aAAa,MAAOD,CAAE,EAClCC,EAAY,YAAc,kBAC1BA,EAAY,MAAM,SAAW,QAC7BA,EAAY,MAAM,OAAS,OAC3BA,EAAY,MAAM,KAAO,OACzBA,EAAY,MAAM,QAAU,WAC5BA,EAAY,MAAM,gBAAkB,qBACpCA,EAAY,MAAM,MAAQ,QAC1BA,EAAY,MAAM,WAAa,aAC/BA,EAAY,MAAM,SAAW,OAC7BA,EAAY,MAAM,aAAe,MACjCA,EAAY,MAAM,OAAS,OAC3BA,EAAY,MAAM,WAAa,OAC/B,KAAK,YAAcA,EAEnB,IAAMC,EAAiB,SAAS,cAAc,OAAO,EACrDA,EAAe,KAAO,WACtBA,EAAe,aAAa,SAAU,EAAE,EACxCA,EAAe,GAAKF,EACpBE,EAAe,MAAM,IAAM,UAC3BA,EAAe,MAAM,WAAa,OAE7B,KAAK,aACRD,EAAY,MAAM,QAAU,OAC5BC,EAAe,MAAM,QAAU,QAGjCD,EAAY,YAAYC,CAAc,EACtC,SAAS,KAAK,YAAYD,CAAW,EACrC,KAAK,eAAiB,EACxB,CACF,ECraO,SAASE,EAAiBC,EAA6B,CAC5D,IAAMC,EAAW,IAAIC,EAAWF,CAAO,EAEjCG,EAAU,CAACC,EAAqBJ,IACpCC,EAAS,QAAQG,EAAOJ,CAAO,EAC3BK,EAAS,IAAMJ,EAAS,OAAO,EAC/BK,EAAU,IAAML,EAAS,QAAQ,EACjCM,EAAYC,GAAmBP,EAAS,SAASO,CAAK,EACtDC,EAAcP,EAAW,YAE/B,MAAO,CAAE,QAAAC,EAAS,OAAAE,EAAQ,QAAAC,EAAS,SAAAC,EAAU,YAAAE,CAAY,CAC3D","names":["svelte_exports","__export","createWebHaptics","__toCommonJS","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","toggleInterval","data","source","id","hapticLabel","hapticCheckbox","createWebHaptics","options","instance","WebHaptics","trigger","input","cancel","destroy","setDebug","debug","isSupported"]}
@@ -1,2 +1,2 @@
1
- var y={success:{pattern:[50,50,50],description:"A series of taps indicating success.",intensity:.5},nudge:{pattern:[80,100,200],description:"A series of taps indicating a nudge.",intensity:.5},error:{pattern:[50,50,50,50,50],description:"A series of taps indicating a warning or error.",intensity:.75},buzz:{pattern:[1e3],description:"A long vibration.",intensity:1}};var v=16,x=184,g=1e3,h=20;function C(r,e){if(e>=1)return r;if(e<=0)return[];let t=Math.max(1,Math.round(h*e)),i=h-t,n=[];for(let s=0;s<r.length;s++){let a=r[s];if(s%2===0){let o=a;for(;o>=h;)n.push(t),n.push(i),o-=h;if(o>0){let l=Math.max(1,Math.round(o*e));n.push(l);let u=o-l;u>0&&n.push(u)}}else n.length>0&&n.length%2===0?n[n.length-1]+=a:n.push(a)}return n}var w=0,c=class r{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(e){this.instanceId=++w,this.debug=e?.debug??!1,this.showSwitch=e?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(e=[10],t){let i,n=.5;if(typeof e=="number")i=[e];else if(typeof e=="string"){let a=y[e];if(!a){console.warn(`[web-haptics] Unknown preset: "${e}"`);return}i=[...a.pattern],n=a.intensity}else Array.isArray(e)?i=[...e]:(i=[...e.pattern],n=e.intensity);let s=Math.max(0,Math.min(1,t?.intensity??n));for(let a=0;a<i.length;a++)if(a%2===0&&i[a]>g&&(i[a]=g),!Number.isFinite(i[a])||i[a]<0){console.warn(`[web-haptics] Invalid value at index ${a}: ${i[a]}. Pattern values must be finite non-negative numbers.`);return}if(r.isSupported&&navigator.vibrate(C(i,s)),!r.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx&&this.playClick(s),await this.runPattern(i,s)}}cancel(){this.stopPattern(),r.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(e){this.debug=e,!e&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(e){if(this.showSwitch=e,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=e?"":"none",t&&(t.style.display=e?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(e,t){return new Promise(i=>{this.patternResolve=i;let n=[],s=0;for(let d of e)s+=d,n.push(s);let a=s,o=v+(1-t)*x,l=0,u=-1,f=d=>{l===0&&(l=d);let b=d-l;if(b>=a){this.rafId=null,this.patternResolve=null,i();return}let m=0;for(let p=0;p<n.length;p++)if(b<n[p]){m=p;break}m%2===0&&(u===-1?u=d:d-u>=o&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(t),u=d)),this.rafId=requestAnimationFrame(f)};this.rafId=requestAnimationFrame(f)})}playClick(e){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;this.audioGain.gain.value=.5*e;let t=this.audioCtx.createBufferSource();t.buffer=this.audioBuffer,t.connect(this.audioFilter),t.onended=()=>t.disconnect(),t.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let e=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*e,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let i=0;i<t.length;i++)t[i]=(Math.random()*2-1)*Math.exp(-i/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let e=`web-haptics-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",e),t.textContent="Haptic feedback",t.style.position="fixed",t.style.bottom="10px",t.style.left="10px",t.style.padding="5px 10px",t.style.backgroundColor="rgba(0, 0, 0, 0.7)",t.style.color="white",t.style.fontFamily="sans-serif",t.style.fontSize="14px",t.style.borderRadius="4px",t.style.zIndex="9999",t.style.userSelect="none",this.hapticLabel=t;let i=document.createElement("input");i.type="checkbox",i.setAttribute("switch",""),i.id=e,i.style.all="initial",i.style.appearance="auto",this.showSwitch||(t.style.display="none",i.style.display="none"),t.appendChild(i),document.body.appendChild(t),this.domInitialized=!0}};function I(r){let e=new c(r),t=(o,l)=>e.trigger(o,l),i=()=>e.cancel(),n=()=>e.destroy(),s=o=>e.setDebug(o),a=c.isSupported;return{trigger:t,cancel:i,destroy:n,setDebug:s,isSupported:a}}export{I as createWebHaptics};
1
+ var m={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,v=184,y=1e3,f=20;function x(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=m[a];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${a}"`),null)}if(Array.isArray(a)){if(a.length===0)return{vibrations:[]};if(typeof a[0]=="number"){let i=a,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]})}return{vibrations:t}}return{vibrations:a.map(i=>({...i}))}}return{vibrations:a.pattern.map(i=>({...i}))}}function C(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(f*i)),e=f-t,n=[],o=a;for(;o>=f;)n.push(t),n.push(e),o-=f;if(o>0){let s=Math.max(1,Math.round(o*i));n.push(s);let r=o-s;r>0&&n.push(r)}return n}function w(a,i){let t=[];for(let e=0;e<a.length;e++){let n=a[e],o=Math.max(0,Math.min(1,n.intensity??i)),s=n.delay??0;s>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=s:(t.length===0&&t.push(0),t.push(s)));let r=C(n.duration,o);if(r.length===0){t.length>0&&t.length%2===0?t[t.length-1]+=n.duration:n.duration>0&&(t.push(0),t.push(n.duration));continue}for(let l of r)t.push(l)}return t}var I=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=++I,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=x(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let o=Math.max(0,Math.min(1,t?.intensity??.5));for(let s of n)if(s.duration>y&&(s.duration=y),!Number.isFinite(s.duration)||s.duration<0||s.delay!==void 0&&(!Number.isFinite(s.delay)||s.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(w(n,o)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;if(this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx){let s=Math.max(0,Math.min(1,n[0].intensity??o));this.playClick(s)}await this.runPattern(n,o)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t){return new Promise(e=>{this.patternResolve=e;let n=[],o=0;for(let u of i){let h=Math.max(0,Math.min(1,u.intensity??t)),d=u.delay??0;d>0&&(o+=d,n.push({end:o,isOn:!1,intensity:0})),o+=u.duration,n.push({end:o,isOn:!0,intensity:h})}let s=o,r=0,l=-1,b=u=>{r===0&&(r=u);let h=u-r;if(h>=s){this.rafId=null,this.patternResolve=null,e();return}let d=n[0];for(let p of n)if(h<p.end){d=p;break}if(d.isOn){let p=g+(1-d.intensity)*v;l===-1?l=u:u-l>=p&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(d.intensity),l=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}};function M(a){let i=new c(a),t=(r,l)=>i.trigger(r,l),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}}export{M as createWebHaptics};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts","../../src/svelte/createWebHaptics.ts"],"sourcesContent":["import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [50, 50, 50],\n description: \"A series of taps indicating success.\",\n intensity: 0.5,\n },\n nudge: {\n pattern: [80, 100, 200],\n description: \"A series of taps indicating a nudge.\",\n intensity: 0.5,\n },\n error: {\n pattern: [50, 50, 50, 50, 50],\n description: \"A series of taps indicating a warning or error.\",\n intensity: 0.75,\n },\n buzz: {\n pattern: [1000],\n description: \"A long vibration.\",\n intensity: 1,\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type { HapticInput, TriggerOptions, WebHapticsOptions } from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\nfunction modulatePattern(pattern: number[], intensity: number): number[] {\n if (intensity >= 1) return pattern;\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n for (let i = 0; i < pattern.length; i++) {\n const dur = pattern[i]!;\n\n if (i % 2 === 0) {\n let remaining = dur;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n } else {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += dur;\n } else {\n result.push(dur);\n }\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" && typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [10],\n options?: TriggerOptions,\n ): Promise<void> {\n let pattern: number[];\n let defaultIntensity = 0.5;\n\n if (typeof input === \"number\") {\n pattern = [input];\n } else if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return;\n }\n pattern = [...preset.pattern];\n defaultIntensity = preset.intensity;\n } else if (Array.isArray(input)) {\n pattern = [...input];\n } else {\n pattern = [...input.pattern];\n defaultIntensity = input.intensity;\n }\n\n const intensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? defaultIntensity),\n );\n\n for (let i = 0; i < pattern.length; i++) {\n if (i % 2 === 0 && pattern[i]! > MAX_PHASE_MS) {\n pattern[i] = MAX_PHASE_MS;\n }\n if (!Number.isFinite(pattern[i]) || pattern[i]! < 0) {\n console.warn(\n `[web-haptics] Invalid value at index ${i}: ${pattern[i]}. Pattern values must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(modulatePattern(pattern, intensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n // (iOS blocks haptics from programmatic clicks outside gesture handlers)\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n\n await this.runPattern(pattern, intensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(pattern: number[], intensity: number): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n const phases: number[] = [];\n let cumulative = 0;\n for (const p of pattern) {\n cumulative += p;\n phases.push(cumulative);\n }\n const totalDuration = cumulative;\n\n const toggleInterval = TOGGLE_MIN + (1 - intensity) * TOGGLE_MAX;\n let startTime = 0;\n let lastToggleTime = -1; // -1 signals first click was already fired synchronously\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n let phaseIndex = 0;\n for (let i = 0; i < phases.length; i++) {\n if (elapsed < phases[i]!) {\n phaseIndex = i;\n break;\n }\n }\n\n if (phaseIndex % 2 === 0) {\n if (lastToggleTime === -1) {\n // Skip first toggle — already fired synchronously\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (!this.audioCtx || !this.audioFilter || !this.audioGain || !this.audioBuffer) return;\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n","import { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function createWebHaptics(options?: WebHapticsOptions) {\n const instance = new WebHaptics(options);\n\n const trigger = (input?: HapticInput, options?: TriggerOptions) =>\n instance.trigger(input, options);\n const cancel = () => instance.cancel();\n const destroy = () => instance.destroy();\n const setDebug = (debug: boolean) => instance.setDebug(debug);\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, destroy, setDebug, isSupported };\n}\n"],"mappings":"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","names":["defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","modulatePattern","pattern","intensity","onTime","offTime","result","i","dur","remaining","remOn","remOff","instanceCounter","WebHaptics","_WebHaptics","options","input","defaultIntensity","preset","defaultPatterns","debug","show","checkbox","resolve","phases","cumulative","p","totalDuration","toggleInterval","startTime","lastToggleTime","loop","time","elapsed","phaseIndex","source","duration","data","id","hapticLabel","hapticCheckbox","createWebHaptics","options","instance","WebHaptics","trigger","input","cancel","destroy","setDebug","debug","isSupported"]}
1
+ {"version":3,"sources":["../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts","../../src/svelte/createWebHaptics.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","import { WebHaptics } from \"../lib/web-haptics\";\nimport type {\n HapticInput,\n TriggerOptions,\n WebHapticsOptions,\n} from \"../lib/web-haptics/types\";\n\nexport function createWebHaptics(options?: WebHapticsOptions) {\n const instance = new WebHaptics(options);\n\n const trigger = (input?: HapticInput, options?: TriggerOptions) =>\n instance.trigger(input, options);\n const cancel = () => instance.cancel();\n const destroy = () => instance.destroy();\n const setDebug = (debug: boolean) => instance.setDebug(debug);\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, destroy, setDebug, isSupported };\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","toggleInterval","data","source","id","hapticLabel","hapticCheckbox","createWebHaptics","options","instance","WebHaptics","trigger","input","cancel","destroy","setDebug","debug","isSupported"]}
@@ -0,0 +1,20 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ description: string;
10
+ }
11
+ type HapticInput = number | string | HapticPattern | HapticPreset;
12
+ interface TriggerOptions {
13
+ intensity?: number;
14
+ }
15
+ interface WebHapticsOptions {
16
+ debug?: boolean;
17
+ showSwitch?: boolean;
18
+ }
19
+
20
+ export type { HapticInput as H, TriggerOptions as T, Vibration as V, WebHapticsOptions as W, HapticPattern as a, HapticPreset as b };
@@ -0,0 +1,20 @@
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
7
+ interface HapticPreset {
8
+ pattern: Vibration[];
9
+ description: string;
10
+ }
11
+ type HapticInput = number | string | HapticPattern | HapticPreset;
12
+ interface TriggerOptions {
13
+ intensity?: number;
14
+ }
15
+ interface WebHapticsOptions {
16
+ debug?: boolean;
17
+ showSwitch?: boolean;
18
+ }
19
+
20
+ export type { HapticInput as H, TriggerOptions as T, Vibration as V, WebHapticsOptions as W, HapticPattern as a, HapticPreset as b };
@@ -1,8 +1,12 @@
1
- type HapticPattern = number[];
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
2
7
  interface HapticPreset {
3
- pattern: HapticPattern;
8
+ pattern: Vibration[];
4
9
  description: string;
5
- intensity: number;
6
10
  }
7
11
  type HapticInput = number | string | HapticPattern | HapticPreset;
8
12
  interface TriggerOptions {
@@ -1,8 +1,12 @@
1
- type HapticPattern = number[];
1
+ interface Vibration {
2
+ duration: number;
3
+ intensity?: number;
4
+ delay?: number;
5
+ }
6
+ type HapticPattern = number[] | Vibration[];
2
7
  interface HapticPreset {
3
- pattern: HapticPattern;
8
+ pattern: Vibration[];
4
9
  description: string;
5
- intensity: number;
6
10
  }
7
11
  type HapticInput = number | string | HapticPattern | HapticPreset;
8
12
  interface TriggerOptions {
package/dist/vue/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var b=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var P=(s,t)=>{for(var e in t)b(s,e,{get:t[e],enumerable:!0})},S=(s,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of I(t))!A.call(s,n)&&n!==e&&b(s,n,{get:()=>t[n],enumerable:!(i=w(t,n))||i.enumerable});return s};var F=s=>S(b({},"__esModule",{value:!0}),s);var O={};P(O,{useWebHaptics:()=>C});module.exports=F(O);var p=require("vue");var v={success:{pattern:[50,50,50],description:"A series of taps indicating success.",intensity:.5},nudge:{pattern:[80,100,200],description:"A series of taps indicating a nudge.",intensity:.5},error:{pattern:[50,50,50,50,50],description:"A series of taps indicating a warning or error.",intensity:.75},buzz:{pattern:[1e3],description:"A long vibration.",intensity:1}};var L=16,M=184,x=1e3,f=20;function H(s,t){if(t>=1)return s;if(t<=0)return[];let e=Math.max(1,Math.round(f*t)),i=f-e,n=[];for(let o=0;o<s.length;o++){let a=s[o];if(o%2===0){let r=a;for(;r>=f;)n.push(e),n.push(i),r-=f;if(r>0){let d=Math.max(1,Math.round(r*t));n.push(d);let l=r-d;l>0&&n.push(l)}}else n.length>0&&n.length%2===0?n[n.length-1]+=a:n.push(a)}return n}var G=0,c=class s{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(t){this.instanceId=++G,this.debug=t?.debug??!1,this.showSwitch=t?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(t=[10],e){let i,n=.5;if(typeof t=="number")i=[t];else if(typeof t=="string"){let a=v[t];if(!a){console.warn(`[web-haptics] Unknown preset: "${t}"`);return}i=[...a.pattern],n=a.intensity}else Array.isArray(t)?i=[...t]:(i=[...t.pattern],n=t.intensity);let o=Math.max(0,Math.min(1,e?.intensity??n));for(let a=0;a<i.length;a++)if(a%2===0&&i[a]>x&&(i[a]=x),!Number.isFinite(i[a])||i[a]<0){console.warn(`[web-haptics] Invalid value at index ${a}: ${i[a]}. Pattern values must be finite non-negative numbers.`);return}if(s.isSupported&&navigator.vibrate(H(i,o)),!s.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx&&this.playClick(o),await this.runPattern(i,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(t){this.debug=t,!t&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(t){if(this.showSwitch=t,this.hapticLabel){let e=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=t?"":"none",e&&(e.style.display=t?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(t,e){return new Promise(i=>{this.patternResolve=i;let n=[],o=0;for(let u of t)o+=u,n.push(o);let a=o,r=L+(1-e)*M,d=0,l=-1,m=u=>{d===0&&(d=u);let y=u-d;if(y>=a){this.rafId=null,this.patternResolve=null,i();return}let g=0;for(let h=0;h<n.length;h++)if(y<n[h]){g=h;break}g%2===0&&(l===-1?l=u:u-l>=r&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(e),l=u)),this.rafId=requestAnimationFrame(m)};this.rafId=requestAnimationFrame(m)})}playClick(t){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;this.audioGain.gain.value=.5*t;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let t=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*t,this.audioCtx.sampleRate);let e=this.audioBuffer.getChannelData(0);for(let i=0;i<e.length;i++)e[i]=(Math.random()*2-1)*Math.exp(-i/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let t=`web-haptics-${this.instanceId}`,e=document.createElement("label");e.setAttribute("for",t),e.textContent="Haptic feedback",e.style.position="fixed",e.style.bottom="10px",e.style.left="10px",e.style.padding="5px 10px",e.style.backgroundColor="rgba(0, 0, 0, 0.7)",e.style.color="white",e.style.fontFamily="sans-serif",e.style.fontSize="14px",e.style.borderRadius="4px",e.style.zIndex="9999",e.style.userSelect="none",this.hapticLabel=e;let i=document.createElement("input");i.type="checkbox",i.setAttribute("switch",""),i.id=t,i.style.all="initial",i.style.appearance="auto",this.showSwitch||(e.style.display="none",i.style.display="none"),e.appendChild(i),document.body.appendChild(e),this.domInitialized=!0}};function C(s){let t=null;(0,p.onMounted)(()=>{t=new c(s)}),(0,p.onUnmounted)(()=>{t?.destroy(),t=null}),(0,p.watch)(()=>s?.debug,o=>{t?.setDebug(o??!1)});let e=(o,a)=>t?.trigger(o,a),i=()=>t?.cancel(),n=c.isSupported;return{trigger:e,cancel:i,isSupported:n}}0&&(module.exports={useWebHaptics});
1
+ "use strict";var m=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var M=(a,i)=>{for(var t in i)m(a,t,{get:i[t],enumerable:!0})},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&&m(a,n,{get:()=>i[n],enumerable:!(e=C(i,n))||e.enumerable});return a};var S=a=>A(m({},"__esModule",{value:!0}),a);var k={};M(k,{useWebHaptics:()=>x});module.exports=S(k);var c=require("vue");var g={success:{pattern:[{duration:50},{delay:50,duration:50}],description:"A series of taps indicating success."},nudge:{pattern:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],description:"A series of taps indicating a nudge."},error:{pattern:[{duration:50,intensity:.75},{delay:50,duration:50,intensity:.75},{delay:50,duration:50,intensity:.75}],description:"A series of taps indicating a warning or error."},buzz:{pattern:[{duration:1e3,intensity:1}],description:"A long vibration."}};var F=16,O=184,v=1e3,b=20;function P(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=g[a];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${a}"`),null)}if(Array.isArray(a)){if(a.length===0)return{vibrations:[]};if(typeof a[0]=="number"){let i=a,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]})}return{vibrations:t}}return{vibrations:a.map(i=>({...i}))}}return{vibrations:a.pattern.map(i=>({...i}))}}function H(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(b*i)),e=b-t,n=[],s=a;for(;s>=b;)n.push(t),n.push(e),s-=b;if(s>0){let o=Math.max(1,Math.round(s*i));n.push(o);let r=s-o;r>0&&n.push(r)}return n}function L(a,i){let t=[];for(let e=0;e<a.length;e++){let n=a[e],s=Math.max(0,Math.min(1,n.intensity??i)),o=n.delay??0;o>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=o:(t.length===0&&t.push(0),t.push(o)));let r=H(n.duration,s);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 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 s=Math.max(0,Math.min(1,t?.intensity??.5));for(let o of n)if(o.duration>v&&(o.duration=v),!Number.isFinite(o.duration)||o.duration<0||o.delay!==void 0&&(!Number.isFinite(o.delay)||o.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(L(n,s)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;if(this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx){let o=Math.max(0,Math.min(1,n[0].intensity??s));this.playClick(o)}await this.runPattern(n,s)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t){return new Promise(e=>{this.patternResolve=e;let n=[],s=0;for(let u of i){let p=Math.max(0,Math.min(1,u.intensity??t)),l=u.delay??0;l>0&&(s+=l,n.push({end:s,isOn:!1,intensity:0})),s+=u.duration,n.push({end:s,isOn:!0,intensity:p})}let o=s,r=0,d=-1,y=u=>{r===0&&(r=u);let p=u-r;if(p>=o){this.rafId=null,this.patternResolve=null,e();return}let l=n[0];for(let f of n)if(p<f.end){l=f;break}if(l.isOn){let f=F+(1-l.intensity)*O;d===-1?d=u:u-d>=f&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),d=u)}this.rafId=requestAnimationFrame(y)};this.rafId=requestAnimationFrame(y)})}playClick(i){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;let t=this.audioBuffer.getChannelData(0);for(let n=0;n<t.length;n++)t[n]=(Math.random()*2-1)*Math.exp(-n/25);this.audioGain.gain.value=.5*i;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let i=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*i,this.audioCtx.sampleRate);let t=this.audioBuffer.getChannelData(0);for(let e=0;e<t.length;e++)t[e]=(Math.random()*2-1)*Math.exp(-e/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let i=`web-haptics-${this.instanceId}`,t=document.createElement("label");t.setAttribute("for",i),t.textContent="Haptic feedback",t.style.position="fixed",t.style.bottom="10px",t.style.left="10px",t.style.padding="5px 10px",t.style.backgroundColor="rgba(0, 0, 0, 0.7)",t.style.color="white",t.style.fontFamily="sans-serif",t.style.fontSize="14px",t.style.borderRadius="4px",t.style.zIndex="9999",t.style.userSelect="none",this.hapticLabel=t;let e=document.createElement("input");e.type="checkbox",e.setAttribute("switch",""),e.id=i,e.style.all="initial",e.style.appearance="auto",this.showSwitch||(t.style.display="none",e.style.display="none"),t.appendChild(e),document.body.appendChild(t),this.domInitialized=!0}};function x(a){let i=null;(0,c.onMounted)(()=>{i=new h(a)}),(0,c.onUnmounted)(()=>{i?.destroy(),i=null}),(0,c.watch)(()=>a?.debug,s=>{i?.setDebug(s??!1)});let t=(s,o)=>i?.trigger(s,o),e=()=>i?.cancel(),n=h.isSupported;return{trigger:t,cancel:e,isSupported:n}}0&&(module.exports={useWebHaptics});
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vue/index.ts","../../src/vue/useWebHaptics.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts"],"sourcesContent":["export { useWebHaptics } from \"./useWebHaptics\";\n","import { onMounted, onUnmounted, watch } from \"vue\";\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 let instance: WebHaptics | null = null;\n\n onMounted(() => {\n instance = new WebHaptics(options);\n });\n\n onUnmounted(() => {\n instance?.destroy();\n instance = null;\n });\n\n watch(\n () => options?.debug,\n (val) => {\n instance?.setDebug(val ?? false);\n },\n );\n\n const trigger = (input?: HapticInput, options?: TriggerOptions) =>\n instance?.trigger(input, options);\n const cancel = () => instance?.cancel();\n const isSupported = WebHaptics.isSupported;\n\n return { trigger, cancel, isSupported };\n}\n","import type { HapticPreset } from \"./types\";\n\nexport const defaultPatterns = {\n success: {\n pattern: [50, 50, 50],\n description: \"A series of taps indicating success.\",\n intensity: 0.5,\n },\n nudge: {\n pattern: [80, 100, 200],\n description: \"A series of taps indicating a nudge.\",\n intensity: 0.5,\n },\n error: {\n pattern: [50, 50, 50, 50, 50],\n description: \"A series of taps indicating a warning or error.\",\n intensity: 0.75,\n },\n buzz: {\n pattern: [1000],\n description: \"A long vibration.\",\n intensity: 1,\n },\n} as const satisfies Record<string, HapticPreset>;\n","import { defaultPatterns } from \"./patterns\";\nimport type { HapticInput, TriggerOptions, WebHapticsOptions } from \"./types\";\n\nconst TOGGLE_MIN = 16; // ms at intensity 1 (every frame)\nconst TOGGLE_MAX = 184; // range above min (0.5 intensity ≈ 100ms)\nconst MAX_PHASE_MS = 1000; // browser haptic window limit\nconst PWM_CYCLE = 20; // ms per intensity modulation cycle\n\nfunction modulatePattern(pattern: number[], intensity: number): number[] {\n if (intensity >= 1) return pattern;\n if (intensity <= 0) return [];\n\n const onTime = Math.max(1, Math.round(PWM_CYCLE * intensity));\n const offTime = PWM_CYCLE - onTime;\n const result: number[] = [];\n\n for (let i = 0; i < pattern.length; i++) {\n const dur = pattern[i]!;\n\n if (i % 2 === 0) {\n let remaining = dur;\n while (remaining >= PWM_CYCLE) {\n result.push(onTime);\n result.push(offTime);\n remaining -= PWM_CYCLE;\n }\n if (remaining > 0) {\n const remOn = Math.max(1, Math.round(remaining * intensity));\n result.push(remOn);\n const remOff = remaining - remOn;\n if (remOff > 0) result.push(remOff);\n }\n } else {\n if (result.length > 0 && result.length % 2 === 0) {\n result[result.length - 1]! += dur;\n } else {\n result.push(dur);\n }\n }\n }\n\n return result;\n}\n\nlet instanceCounter = 0;\n\nexport class WebHaptics {\n private hapticLabel: HTMLLabelElement | null = null;\n private domInitialized = false;\n private instanceId: number;\n private debug: boolean;\n private showSwitch: boolean;\n private rafId: number | null = null;\n private patternResolve: (() => void) | null = null;\n private audioCtx: AudioContext | null = null;\n private audioFilter: BiquadFilterNode | null = null;\n private audioGain: GainNode | null = null;\n private audioBuffer: AudioBuffer | null = null;\n\n constructor(options?: WebHapticsOptions) {\n this.instanceId = ++instanceCounter;\n this.debug = options?.debug ?? false;\n this.showSwitch = options?.showSwitch ?? false;\n }\n\n static readonly isSupported: boolean =\n typeof navigator !== \"undefined\" && typeof navigator.vibrate === \"function\";\n\n async trigger(\n input: HapticInput = [10],\n options?: TriggerOptions,\n ): Promise<void> {\n let pattern: number[];\n let defaultIntensity = 0.5;\n\n if (typeof input === \"number\") {\n pattern = [input];\n } else if (typeof input === \"string\") {\n const preset = defaultPatterns[input as keyof typeof defaultPatterns];\n if (!preset) {\n console.warn(`[web-haptics] Unknown preset: \"${input}\"`);\n return;\n }\n pattern = [...preset.pattern];\n defaultIntensity = preset.intensity;\n } else if (Array.isArray(input)) {\n pattern = [...input];\n } else {\n pattern = [...input.pattern];\n defaultIntensity = input.intensity;\n }\n\n const intensity = Math.max(\n 0,\n Math.min(1, options?.intensity ?? defaultIntensity),\n );\n\n for (let i = 0; i < pattern.length; i++) {\n if (i % 2 === 0 && pattern[i]! > MAX_PHASE_MS) {\n pattern[i] = MAX_PHASE_MS;\n }\n if (!Number.isFinite(pattern[i]) || pattern[i]! < 0) {\n console.warn(\n `[web-haptics] Invalid value at index ${i}: ${pattern[i]}. Pattern values must be finite non-negative numbers.`,\n );\n return;\n }\n }\n\n if (WebHaptics.isSupported) {\n navigator.vibrate(modulatePattern(pattern, intensity));\n }\n\n if (!WebHaptics.isSupported || this.debug) {\n this.ensureDOM();\n if (!this.hapticLabel) return;\n\n if (this.debug) {\n await this.ensureAudio();\n }\n\n this.stopPattern();\n\n // Fire first click synchronously to stay within user gesture context\n // (iOS blocks haptics from programmatic clicks outside gesture handlers)\n this.hapticLabel.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n\n await this.runPattern(pattern, intensity);\n }\n }\n\n cancel(): void {\n this.stopPattern();\n if (WebHaptics.isSupported) {\n navigator.vibrate(0);\n }\n }\n\n destroy(): void {\n this.stopPattern();\n if (this.hapticLabel) {\n this.hapticLabel.remove();\n this.hapticLabel = null;\n this.domInitialized = false;\n }\n if (this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setDebug(debug: boolean): void {\n this.debug = debug;\n if (!debug && this.audioCtx) {\n this.audioCtx.close();\n this.audioCtx = null;\n this.audioFilter = null;\n this.audioGain = null;\n this.audioBuffer = null;\n }\n }\n\n setShowSwitch(show: boolean): void {\n this.showSwitch = show;\n if (this.hapticLabel) {\n const checkbox = this.hapticLabel.querySelector(\"input\");\n this.hapticLabel.style.display = show ? \"\" : \"none\";\n if (checkbox) checkbox.style.display = show ? \"\" : \"none\";\n }\n }\n\n private stopPattern(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.patternResolve?.();\n this.patternResolve = null;\n }\n\n private runPattern(pattern: number[], intensity: number): Promise<void> {\n return new Promise((resolve) => {\n this.patternResolve = resolve;\n\n const phases: number[] = [];\n let cumulative = 0;\n for (const p of pattern) {\n cumulative += p;\n phases.push(cumulative);\n }\n const totalDuration = cumulative;\n\n const toggleInterval = TOGGLE_MIN + (1 - intensity) * TOGGLE_MAX;\n let startTime = 0;\n let lastToggleTime = -1; // -1 signals first click was already fired synchronously\n\n const loop = (time: number) => {\n if (startTime === 0) startTime = time;\n const elapsed = time - startTime;\n\n if (elapsed >= totalDuration) {\n this.rafId = null;\n this.patternResolve = null;\n resolve();\n return;\n }\n\n let phaseIndex = 0;\n for (let i = 0; i < phases.length; i++) {\n if (elapsed < phases[i]!) {\n phaseIndex = i;\n break;\n }\n }\n\n if (phaseIndex % 2 === 0) {\n if (lastToggleTime === -1) {\n // Skip first toggle — already fired synchronously\n lastToggleTime = time;\n } else if (time - lastToggleTime >= toggleInterval) {\n this.hapticLabel?.click();\n if (this.debug && this.audioCtx) {\n this.playClick(intensity);\n }\n lastToggleTime = time;\n }\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n });\n }\n\n private playClick(intensity: number): void {\n if (!this.audioCtx || !this.audioFilter || !this.audioGain || !this.audioBuffer) return;\n\n this.audioGain.gain.value = 0.5 * intensity;\n\n const source = this.audioCtx.createBufferSource();\n source.buffer = this.audioBuffer;\n source.connect(this.audioFilter);\n source.onended = () => source.disconnect();\n source.start();\n }\n\n private async ensureAudio(): Promise<void> {\n if (!this.audioCtx && typeof AudioContext !== \"undefined\") {\n this.audioCtx = new AudioContext();\n\n this.audioFilter = this.audioCtx.createBiquadFilter();\n this.audioFilter.type = \"bandpass\";\n this.audioFilter.frequency.value = 4000;\n this.audioFilter.Q.value = 8;\n\n this.audioGain = this.audioCtx.createGain();\n this.audioFilter.connect(this.audioGain);\n this.audioGain.connect(this.audioCtx.destination);\n\n const duration = 0.004;\n this.audioBuffer = this.audioCtx.createBuffer(\n 1,\n this.audioCtx.sampleRate * duration,\n this.audioCtx.sampleRate,\n );\n const data = this.audioBuffer.getChannelData(0);\n for (let i = 0; i < data.length; i++) {\n data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 25);\n }\n }\n if (this.audioCtx?.state === \"suspended\") {\n await this.audioCtx.resume();\n }\n }\n\n private ensureDOM(): void {\n if (this.domInitialized) return;\n if (typeof document === \"undefined\") return;\n\n const id = `web-haptics-${this.instanceId}`;\n\n const hapticLabel = document.createElement(\"label\");\n hapticLabel.setAttribute(\"for\", id);\n hapticLabel.textContent = \"Haptic feedback\";\n hapticLabel.style.position = \"fixed\";\n hapticLabel.style.bottom = \"10px\";\n hapticLabel.style.left = \"10px\";\n hapticLabel.style.padding = \"5px 10px\";\n hapticLabel.style.backgroundColor = \"rgba(0, 0, 0, 0.7)\";\n hapticLabel.style.color = \"white\";\n hapticLabel.style.fontFamily = \"sans-serif\";\n hapticLabel.style.fontSize = \"14px\";\n hapticLabel.style.borderRadius = \"4px\";\n hapticLabel.style.zIndex = \"9999\";\n hapticLabel.style.userSelect = \"none\";\n this.hapticLabel = hapticLabel;\n\n const hapticCheckbox = document.createElement(\"input\");\n hapticCheckbox.type = \"checkbox\";\n hapticCheckbox.setAttribute(\"switch\", \"\");\n hapticCheckbox.id = id;\n hapticCheckbox.style.all = \"initial\";\n hapticCheckbox.style.appearance = \"auto\";\n\n if (!this.showSwitch) {\n hapticLabel.style.display = \"none\";\n hapticCheckbox.style.display = \"none\";\n }\n\n hapticLabel.appendChild(hapticCheckbox);\n document.body.appendChild(hapticLabel);\n this.domInitialized = true;\n }\n}\n"],"mappings":"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","names":["vue_exports","__export","useWebHaptics","__toCommonJS","import_vue","defaultPatterns","TOGGLE_MIN","TOGGLE_MAX","MAX_PHASE_MS","PWM_CYCLE","modulatePattern","pattern","intensity","onTime","offTime","result","i","dur","remaining","remOn","remOff","instanceCounter","WebHaptics","_WebHaptics","options","input","defaultIntensity","preset","defaultPatterns","debug","show","checkbox","resolve","phases","cumulative","p","totalDuration","toggleInterval","startTime","lastToggleTime","loop","time","elapsed","phaseIndex","source","duration","data","id","hapticLabel","hapticCheckbox","useWebHaptics","options","instance","WebHaptics","val","trigger","input","cancel","isSupported"]}
1
+ {"version":3,"sources":["../../src/vue/index.ts","../../src/vue/useWebHaptics.ts","../../src/lib/web-haptics/patterns.ts","../../src/lib/web-haptics/index.ts"],"sourcesContent":["export { useWebHaptics } from \"./useWebHaptics\";\n","import { onMounted, onUnmounted, watch } from \"vue\";\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 let instance: WebHaptics | null = null;\n\n onMounted(() => {\n instance = new WebHaptics(options);\n });\n\n onUnmounted(() => {\n instance?.destroy();\n instance = null;\n });\n\n watch(\n () => options?.debug,\n (val) => {\n instance?.setDebug(val ?? false);\n },\n );\n\n const trigger = (input?: HapticInput, options?: TriggerOptions) =>\n instance?.trigger(input, options);\n const cancel = () => instance?.cancel();\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":["vue_exports","__export","useWebHaptics","__toCommonJS","import_vue","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","instance","WebHaptics","val","trigger","input","cancel","isSupported"]}
@@ -1,2 +1,2 @@
1
- import{onMounted as I,onUnmounted as A,watch as P}from"vue";var y={success:{pattern:[50,50,50],description:"A series of taps indicating success.",intensity:.5},nudge:{pattern:[80,100,200],description:"A series of taps indicating a nudge.",intensity:.5},error:{pattern:[50,50,50,50,50],description:"A series of taps indicating a warning or error.",intensity:.75},buzz:{pattern:[1e3],description:"A long vibration.",intensity:1}};var v=16,x=184,g=1e3,h=20;function C(o,t){if(t>=1)return o;if(t<=0)return[];let e=Math.max(1,Math.round(h*t)),i=h-e,a=[];for(let s=0;s<o.length;s++){let n=o[s];if(s%2===0){let r=n;for(;r>=h;)a.push(e),a.push(i),r-=h;if(r>0){let d=Math.max(1,Math.round(r*t));a.push(d);let l=r-d;l>0&&a.push(l)}}else a.length>0&&a.length%2===0?a[a.length-1]+=n:a.push(n)}return a}var w=0,p=class o{hapticLabel=null;domInitialized=!1;instanceId;debug;showSwitch;rafId=null;patternResolve=null;audioCtx=null;audioFilter=null;audioGain=null;audioBuffer=null;constructor(t){this.instanceId=++w,this.debug=t?.debug??!1,this.showSwitch=t?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(t=[10],e){let i,a=.5;if(typeof t=="number")i=[t];else if(typeof t=="string"){let n=y[t];if(!n){console.warn(`[web-haptics] Unknown preset: "${t}"`);return}i=[...n.pattern],a=n.intensity}else Array.isArray(t)?i=[...t]:(i=[...t.pattern],a=t.intensity);let s=Math.max(0,Math.min(1,e?.intensity??a));for(let n=0;n<i.length;n++)if(n%2===0&&i[n]>g&&(i[n]=g),!Number.isFinite(i[n])||i[n]<0){console.warn(`[web-haptics] Invalid value at index ${n}: ${i[n]}. Pattern values must be finite non-negative numbers.`);return}if(o.isSupported&&navigator.vibrate(C(i,s)),!o.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx&&this.playClick(s),await this.runPattern(i,s)}}cancel(){this.stopPattern(),o.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(t){this.debug=t,!t&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(t){if(this.showSwitch=t,this.hapticLabel){let e=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=t?"":"none",e&&(e.style.display=t?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(t,e){return new Promise(i=>{this.patternResolve=i;let a=[],s=0;for(let u of t)s+=u,a.push(s);let n=s,r=v+(1-e)*x,d=0,l=-1,f=u=>{d===0&&(d=u);let b=u-d;if(b>=n){this.rafId=null,this.patternResolve=null,i();return}let m=0;for(let c=0;c<a.length;c++)if(b<a[c]){m=c;break}m%2===0&&(l===-1?l=u:u-l>=r&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(e),l=u)),this.rafId=requestAnimationFrame(f)};this.rafId=requestAnimationFrame(f)})}playClick(t){if(!this.audioCtx||!this.audioFilter||!this.audioGain||!this.audioBuffer)return;this.audioGain.gain.value=.5*t;let e=this.audioCtx.createBufferSource();e.buffer=this.audioBuffer,e.connect(this.audioFilter),e.onended=()=>e.disconnect(),e.start()}async ensureAudio(){if(!this.audioCtx&&typeof AudioContext<"u"){this.audioCtx=new AudioContext,this.audioFilter=this.audioCtx.createBiquadFilter(),this.audioFilter.type="bandpass",this.audioFilter.frequency.value=4e3,this.audioFilter.Q.value=8,this.audioGain=this.audioCtx.createGain(),this.audioFilter.connect(this.audioGain),this.audioGain.connect(this.audioCtx.destination);let t=.004;this.audioBuffer=this.audioCtx.createBuffer(1,this.audioCtx.sampleRate*t,this.audioCtx.sampleRate);let e=this.audioBuffer.getChannelData(0);for(let i=0;i<e.length;i++)e[i]=(Math.random()*2-1)*Math.exp(-i/25)}this.audioCtx?.state==="suspended"&&await this.audioCtx.resume()}ensureDOM(){if(this.domInitialized||typeof document>"u")return;let t=`web-haptics-${this.instanceId}`,e=document.createElement("label");e.setAttribute("for",t),e.textContent="Haptic feedback",e.style.position="fixed",e.style.bottom="10px",e.style.left="10px",e.style.padding="5px 10px",e.style.backgroundColor="rgba(0, 0, 0, 0.7)",e.style.color="white",e.style.fontFamily="sans-serif",e.style.fontSize="14px",e.style.borderRadius="4px",e.style.zIndex="9999",e.style.userSelect="none",this.hapticLabel=e;let i=document.createElement("input");i.type="checkbox",i.setAttribute("switch",""),i.id=t,i.style.all="initial",i.style.appearance="auto",this.showSwitch||(e.style.display="none",i.style.display="none"),e.appendChild(i),document.body.appendChild(e),this.domInitialized=!0}};function S(o){let t=null;I(()=>{t=new p(o)}),A(()=>{t?.destroy(),t=null}),P(()=>o?.debug,s=>{t?.setDebug(s??!1)});let e=(s,n)=>t?.trigger(s,n),i=()=>t?.cancel(),a=p.isSupported;return{trigger:e,cancel:i,isSupported:a}}export{S as useWebHaptics};
1
+ import{onMounted as M,onUnmounted as A,watch as S}from"vue";var m={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,v=184,y=1e3,f=20;function x(a){if(typeof a=="number")return{vibrations:[{duration:a}]};if(typeof a=="string"){let i=m[a];return i?{vibrations:i.pattern.map(t=>({...t}))}:(console.warn(`[web-haptics] Unknown preset: "${a}"`),null)}if(Array.isArray(a)){if(a.length===0)return{vibrations:[]};if(typeof a[0]=="number"){let i=a,t=[];for(let e=0;e<i.length;e+=2){let n=e>0?i[e-1]:0;t.push({...n>0&&{delay:n},duration:i[e]})}return{vibrations:t}}return{vibrations:a.map(i=>({...i}))}}return{vibrations:a.pattern.map(i=>({...i}))}}function C(a,i){if(i>=1)return[a];if(i<=0)return[];let t=Math.max(1,Math.round(f*i)),e=f-t,n=[],s=a;for(;s>=f;)n.push(t),n.push(e),s-=f;if(s>0){let o=Math.max(1,Math.round(s*i));n.push(o);let r=s-o;r>0&&n.push(r)}return n}function w(a,i){let t=[];for(let e=0;e<a.length;e++){let n=a[e],s=Math.max(0,Math.min(1,n.intensity??i)),o=n.delay??0;o>0&&(t.length>0&&t.length%2===0?t[t.length-1]+=o:(t.length===0&&t.push(0),t.push(o)));let r=C(n.duration,s);if(r.length===0){t.length>0&&t.length%2===0?t[t.length-1]+=n.duration:n.duration>0&&(t.push(0),t.push(n.duration));continue}for(let d of r)t.push(d)}return t}var I=0,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=++I,this.debug=i?.debug??!1,this.showSwitch=i?.showSwitch??!1}static isSupported=typeof navigator<"u"&&typeof navigator.vibrate=="function";async trigger(i=[{duration:10}],t){let e=x(i);if(!e)return;let{vibrations:n}=e;if(n.length===0)return;let s=Math.max(0,Math.min(1,t?.intensity??.5));for(let o of n)if(o.duration>y&&(o.duration=y),!Number.isFinite(o.duration)||o.duration<0||o.delay!==void 0&&(!Number.isFinite(o.delay)||o.delay<0)){console.warn("[web-haptics] Invalid vibration values. Durations and delays must be finite non-negative numbers.");return}if(a.isSupported&&navigator.vibrate(w(n,s)),!a.isSupported||this.debug){if(this.ensureDOM(),!this.hapticLabel)return;if(this.debug&&await this.ensureAudio(),this.stopPattern(),this.hapticLabel.click(),this.debug&&this.audioCtx){let o=Math.max(0,Math.min(1,n[0].intensity??s));this.playClick(o)}await this.runPattern(n,s)}}cancel(){this.stopPattern(),a.isSupported&&navigator.vibrate(0)}destroy(){this.stopPattern(),this.hapticLabel&&(this.hapticLabel.remove(),this.hapticLabel=null,this.domInitialized=!1),this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setDebug(i){this.debug=i,!i&&this.audioCtx&&(this.audioCtx.close(),this.audioCtx=null,this.audioFilter=null,this.audioGain=null,this.audioBuffer=null)}setShowSwitch(i){if(this.showSwitch=i,this.hapticLabel){let t=this.hapticLabel.querySelector("input");this.hapticLabel.style.display=i?"":"none",t&&(t.style.display=i?"":"none")}}stopPattern(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.patternResolve?.(),this.patternResolve=null}runPattern(i,t){return new Promise(e=>{this.patternResolve=e;let n=[],s=0;for(let u of i){let h=Math.max(0,Math.min(1,u.intensity??t)),l=u.delay??0;l>0&&(s+=l,n.push({end:s,isOn:!1,intensity:0})),s+=u.duration,n.push({end:s,isOn:!0,intensity:h})}let o=s,r=0,d=-1,b=u=>{r===0&&(r=u);let h=u-r;if(h>=o){this.rafId=null,this.patternResolve=null,e();return}let l=n[0];for(let p of n)if(h<p.end){l=p;break}if(l.isOn){let p=g+(1-l.intensity)*v;d===-1?d=u:u-d>=p&&(this.hapticLabel?.click(),this.debug&&this.audioCtx&&this.playClick(l.intensity),d=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}};function F(a){let i=null;M(()=>{i=new c(a)}),A(()=>{i?.destroy(),i=null}),S(()=>a?.debug,s=>{i?.setDebug(s??!1)});let t=(s,o)=>i?.trigger(s,o),e=()=>i?.cancel(),n=c.isSupported;return{trigger:t,cancel:e,isSupported:n}}export{F as useWebHaptics};
2
2
  //# sourceMappingURL=index.mjs.map