@hapticjs/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1181 @@
1
+ // src/types/config.ts
2
+ var DEFAULT_CONFIG = {
3
+ intensity: 1,
4
+ enabled: true,
5
+ fallback: { type: "none" },
6
+ respectSystemSettings: true
7
+ };
8
+
9
+ // src/adapters/noop.adapter.ts
10
+ var NoopAdapter = class {
11
+ constructor() {
12
+ this.name = "noop";
13
+ this.supported = false;
14
+ }
15
+ capabilities() {
16
+ return {
17
+ maxIntensityLevels: 0,
18
+ minDuration: 0,
19
+ maxDuration: 0,
20
+ supportsPattern: false,
21
+ supportsIntensity: false,
22
+ dualMotor: false
23
+ };
24
+ }
25
+ async pulse(_intensity, _duration) {
26
+ }
27
+ async playSequence(_steps) {
28
+ }
29
+ cancel() {
30
+ }
31
+ dispose() {
32
+ }
33
+ };
34
+
35
+ // src/utils/scheduling.ts
36
+ function delay(ms) {
37
+ return new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
39
+ function clamp(value, min, max) {
40
+ return Math.min(Math.max(value, min), max);
41
+ }
42
+ function normalizeIntensity(intensity) {
43
+ return clamp(intensity, 0, 1);
44
+ }
45
+
46
+ // src/adapters/web-vibration.adapter.ts
47
+ var WebVibrationAdapter = class {
48
+ constructor() {
49
+ this.name = "web-vibration";
50
+ this._cancelled = false;
51
+ this.supported = typeof navigator !== "undefined" && "vibrate" in navigator;
52
+ }
53
+ capabilities() {
54
+ return {
55
+ maxIntensityLevels: 1,
56
+ // on/off only
57
+ minDuration: 10,
58
+ maxDuration: 1e4,
59
+ supportsPattern: true,
60
+ supportsIntensity: false,
61
+ dualMotor: false
62
+ };
63
+ }
64
+ async pulse(_intensity, duration) {
65
+ if (!this.supported) return;
66
+ navigator.vibrate(duration);
67
+ }
68
+ async playSequence(steps) {
69
+ if (!this.supported || steps.length === 0) return;
70
+ this._cancelled = false;
71
+ const pattern = this._toVibrationPattern(steps);
72
+ if (this._canUseNativePattern(steps)) {
73
+ navigator.vibrate(pattern);
74
+ return;
75
+ }
76
+ for (const step of steps) {
77
+ if (this._cancelled) break;
78
+ if (step.type === "vibrate") {
79
+ if (step.intensity > 0.1) {
80
+ if (step.intensity < 0.5) {
81
+ await this._pwmVibrate(step.duration, step.intensity);
82
+ } else {
83
+ navigator.vibrate(step.duration);
84
+ await delay(step.duration);
85
+ }
86
+ } else {
87
+ await delay(step.duration);
88
+ }
89
+ } else {
90
+ await delay(step.duration);
91
+ }
92
+ }
93
+ }
94
+ cancel() {
95
+ this._cancelled = true;
96
+ if (this.supported) {
97
+ navigator.vibrate(0);
98
+ }
99
+ }
100
+ dispose() {
101
+ this.cancel();
102
+ }
103
+ /** Convert steps to Vibration API pattern array */
104
+ _toVibrationPattern(steps) {
105
+ const pattern = [];
106
+ for (const step of steps) {
107
+ pattern.push(step.duration);
108
+ }
109
+ return pattern;
110
+ }
111
+ /** Check if all steps can be played with native pattern (no intensity variation) */
112
+ _canUseNativePattern(steps) {
113
+ return steps.every(
114
+ (s) => s.type === "pause" || s.type === "vibrate" && s.intensity >= 0.5
115
+ );
116
+ }
117
+ /** Simulate lower intensity via pulse-width modulation */
118
+ async _pwmVibrate(duration, intensity) {
119
+ const cycleTime = 20;
120
+ const onTime = Math.round(cycleTime * intensity);
121
+ const offTime = cycleTime - onTime;
122
+ const cycles = Math.floor(duration / cycleTime);
123
+ const pattern = [];
124
+ for (let i = 0; i < cycles; i++) {
125
+ pattern.push(onTime, offTime);
126
+ }
127
+ if (pattern.length > 0) {
128
+ navigator.vibrate(pattern);
129
+ await delay(duration);
130
+ }
131
+ }
132
+ };
133
+
134
+ // src/utils/platform.ts
135
+ function detectPlatform() {
136
+ const isWeb = typeof window !== "undefined" && typeof document !== "undefined";
137
+ const isNode = typeof globalThis !== "undefined" && typeof globalThis.process === "object" && typeof globalThis.process?.versions === "object";
138
+ const isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
139
+ const hasVibrationAPI = isWeb && "vibrate" in navigator;
140
+ const hasGamepadAPI = isWeb && "getGamepads" in navigator;
141
+ const ua = isWeb ? navigator.userAgent : "";
142
+ const isIOS = /iPad|iPhone|iPod/.test(ua) || isWeb && navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
143
+ const isAndroid = /Android/.test(ua);
144
+ const isMobile = isIOS || isAndroid;
145
+ return {
146
+ isWeb,
147
+ isNode,
148
+ isReactNative,
149
+ hasVibrationAPI,
150
+ hasGamepadAPI,
151
+ isIOS,
152
+ isAndroid,
153
+ isMobile
154
+ };
155
+ }
156
+
157
+ // src/engine/capability-detector.ts
158
+ function detectAdapter() {
159
+ const platform = detectPlatform();
160
+ if (platform.hasVibrationAPI) {
161
+ return new WebVibrationAdapter();
162
+ }
163
+ return new NoopAdapter();
164
+ }
165
+
166
+ // src/engine/adaptive-engine.ts
167
+ var AdaptiveEngine = class {
168
+ adapt(steps, capabilities) {
169
+ return steps.map((step) => this._adaptStep(step, capabilities));
170
+ }
171
+ _adaptStep(step, caps) {
172
+ const adapted = { ...step };
173
+ if (adapted.type === "vibrate") {
174
+ adapted.duration = clamp(adapted.duration, caps.minDuration, caps.maxDuration);
175
+ }
176
+ if (!caps.supportsIntensity && adapted.type === "vibrate") {
177
+ adapted.intensity = adapted.intensity > 0.1 ? 1 : 0;
178
+ } else if (caps.maxIntensityLevels > 1 && caps.maxIntensityLevels < 100) {
179
+ const levels = caps.maxIntensityLevels;
180
+ adapted.intensity = Math.round(adapted.intensity * levels) / levels;
181
+ }
182
+ return adapted;
183
+ }
184
+ };
185
+
186
+ // src/engine/fallback-manager.ts
187
+ var FallbackManager = class {
188
+ constructor(config) {
189
+ this.config = config;
190
+ }
191
+ updateConfig(config) {
192
+ this.config = config;
193
+ }
194
+ /** Execute fallback feedback for the given steps */
195
+ async execute(steps) {
196
+ if (this.config.type === "none") return;
197
+ const totalDuration = steps.reduce((sum, s) => sum + s.duration, 0);
198
+ const maxIntensity = Math.max(...steps.filter((s) => s.type === "vibrate").map((s) => s.intensity), 0);
199
+ if (this.config.type === "visual" || this.config.type === "both") {
200
+ await this._visualFallback(totalDuration, maxIntensity);
201
+ }
202
+ if (this.config.type === "audio" || this.config.type === "both") {
203
+ await this._audioFallback(totalDuration, maxIntensity);
204
+ }
205
+ }
206
+ async _visualFallback(duration, intensity) {
207
+ const visual = this.config.visual;
208
+ if (!visual || typeof document === "undefined") return;
209
+ const element = visual.element ?? document.body;
210
+ const style = visual.style ?? "pulse";
211
+ const className = visual.className;
212
+ if (className) {
213
+ element.classList.add(className);
214
+ setTimeout(() => element.classList.remove(className), duration);
215
+ return;
216
+ }
217
+ switch (style) {
218
+ case "flash": {
219
+ const opacity = 0.1 + intensity * 0.3;
220
+ const overlay = document.createElement("div");
221
+ Object.assign(overlay.style, {
222
+ position: "fixed",
223
+ inset: "0",
224
+ backgroundColor: `rgba(0, 0, 0, ${opacity})`,
225
+ pointerEvents: "none",
226
+ zIndex: "99999",
227
+ transition: `opacity ${duration}ms ease-out`
228
+ });
229
+ document.body.appendChild(overlay);
230
+ requestAnimationFrame(() => {
231
+ overlay.style.opacity = "0";
232
+ });
233
+ setTimeout(() => overlay.remove(), duration + 100);
234
+ break;
235
+ }
236
+ case "shake": {
237
+ const magnitude = Math.round(intensity * 5);
238
+ element.style.transition = "none";
239
+ element.style.transform = `translateX(${magnitude}px)`;
240
+ setTimeout(() => {
241
+ element.style.transition = `transform ${duration}ms ease-out`;
242
+ element.style.transform = "";
243
+ }, 50);
244
+ break;
245
+ }
246
+ case "pulse": {
247
+ const scale = 1 + intensity * 0.02;
248
+ element.style.transition = `transform ${duration}ms ease-out`;
249
+ element.style.transform = `scale(${scale})`;
250
+ setTimeout(() => {
251
+ element.style.transform = "";
252
+ }, duration);
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ async _audioFallback(duration, intensity) {
258
+ const audio = this.config.audio;
259
+ if (!audio?.enabled || typeof AudioContext === "undefined") return;
260
+ try {
261
+ const ctx = new AudioContext();
262
+ const oscillator = ctx.createOscillator();
263
+ const gainNode = ctx.createGain();
264
+ oscillator.connect(gainNode);
265
+ gainNode.connect(ctx.destination);
266
+ oscillator.frequency.value = 200 + intensity * 200;
267
+ gainNode.gain.value = audio.volume * intensity * 0.1;
268
+ oscillator.start();
269
+ oscillator.stop(ctx.currentTime + duration / 1e3);
270
+ await new Promise((resolve) => {
271
+ oscillator.onended = () => {
272
+ ctx.close();
273
+ resolve();
274
+ };
275
+ });
276
+ } catch {
277
+ }
278
+ }
279
+ };
280
+
281
+ // src/composer/pattern-composer.ts
282
+ var PatternComposer = class {
283
+ constructor() {
284
+ this.steps = [];
285
+ }
286
+ /** Register a play callback (used by HapticEngine) */
287
+ onPlay(fn) {
288
+ this._onPlay = fn;
289
+ return this;
290
+ }
291
+ /** Add a short tap vibration */
292
+ tap(intensity = 0.6) {
293
+ this.steps.push({ type: "vibrate", duration: 10, intensity });
294
+ return this;
295
+ }
296
+ /** Add a vibration with specified duration and intensity */
297
+ vibrate(duration, intensity = 1) {
298
+ this.steps.push({ type: "vibrate", duration, intensity });
299
+ return this;
300
+ }
301
+ /** Add a buzz (medium-length vibration) */
302
+ buzz(duration = 100, intensity = 0.7) {
303
+ this.steps.push({ type: "vibrate", duration, intensity });
304
+ return this;
305
+ }
306
+ /** Add a pause */
307
+ pause(duration = 50) {
308
+ this.steps.push({ type: "pause", duration, intensity: 0 });
309
+ return this;
310
+ }
311
+ /** Add an intensity ramp from start to end intensity over duration */
312
+ ramp(startIntensity, endIntensity, duration, easing = "linear") {
313
+ const stepCount = Math.max(1, Math.floor(duration / 20));
314
+ const stepDuration = duration / stepCount;
315
+ for (let i = 0; i < stepCount; i++) {
316
+ const t = i / (stepCount - 1 || 1);
317
+ const easedT = applyEasing(t, easing);
318
+ const intensity = startIntensity + (endIntensity - startIntensity) * easedT;
319
+ this.steps.push({ type: "vibrate", duration: stepDuration, intensity });
320
+ }
321
+ return this;
322
+ }
323
+ /** Add a pulse pattern (on-off-on-off) */
324
+ pulse(count, onDuration = 50, offDuration = 50, intensity = 0.8) {
325
+ for (let i = 0; i < count; i++) {
326
+ this.steps.push({ type: "vibrate", duration: onDuration, intensity });
327
+ if (i < count - 1) {
328
+ this.steps.push({ type: "pause", duration: offDuration, intensity: 0 });
329
+ }
330
+ }
331
+ return this;
332
+ }
333
+ /** Repeat the entire current sequence N times */
334
+ repeat(times) {
335
+ const original = [...this.steps];
336
+ for (let i = 1; i < times; i++) {
337
+ this.steps.push(...original.map((s) => ({ ...s })));
338
+ }
339
+ return this;
340
+ }
341
+ /** Build and return the step array */
342
+ build() {
343
+ return [...this.steps];
344
+ }
345
+ /** Build and immediately play the pattern */
346
+ async play() {
347
+ if (this._onPlay) {
348
+ await this._onPlay(this.steps);
349
+ }
350
+ }
351
+ /** Reset the composer */
352
+ clear() {
353
+ this.steps = [];
354
+ return this;
355
+ }
356
+ /** Get total duration in ms */
357
+ get duration() {
358
+ return this.steps.reduce((sum, s) => sum + s.duration, 0);
359
+ }
360
+ };
361
+ function applyEasing(t, easing) {
362
+ switch (easing) {
363
+ case "linear":
364
+ return t;
365
+ case "ease-in":
366
+ return t * t;
367
+ case "ease-out":
368
+ return t * (2 - t);
369
+ case "ease-in-out":
370
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
371
+ }
372
+ }
373
+
374
+ // src/patterns/tokenizer.ts
375
+ var TOKEN_MAP = {
376
+ "~": "LIGHT",
377
+ "#": "MEDIUM",
378
+ "@": "HEAVY",
379
+ ".": "PAUSE",
380
+ "|": "TAP",
381
+ "-": "SUSTAIN",
382
+ "[": "GROUP_START",
383
+ "]": "GROUP_END"
384
+ };
385
+ var HPLTokenizerError = class extends Error {
386
+ constructor(message, position) {
387
+ super(`HPL Tokenizer Error at position ${position}: ${message}`);
388
+ this.position = position;
389
+ this.name = "HPLTokenizerError";
390
+ }
391
+ };
392
+ function tokenize(input) {
393
+ const tokens = [];
394
+ let i = 0;
395
+ while (i < input.length) {
396
+ const char = input[i];
397
+ if (char === " " || char === " " || char === "\n") {
398
+ i++;
399
+ continue;
400
+ }
401
+ if (char === "x" && i + 1 < input.length) {
402
+ let numStr = "";
403
+ let j = i + 1;
404
+ while (j < input.length && input[j] >= "0" && input[j] <= "9") {
405
+ numStr += input[j];
406
+ j++;
407
+ }
408
+ if (numStr.length > 0) {
409
+ tokens.push({
410
+ type: "REPEAT",
411
+ value: `x${numStr}`,
412
+ repeatCount: parseInt(numStr, 10)
413
+ });
414
+ i = j;
415
+ continue;
416
+ }
417
+ }
418
+ const tokenType = TOKEN_MAP[char];
419
+ if (tokenType) {
420
+ tokens.push({ type: tokenType, value: char });
421
+ i++;
422
+ continue;
423
+ }
424
+ throw new HPLTokenizerError(`Unexpected character '${char}'`, i);
425
+ }
426
+ return tokens;
427
+ }
428
+
429
+ // src/patterns/parser.ts
430
+ var UNIT_DURATION = 50;
431
+ var TAP_DURATION = 10;
432
+ var INTENSITY_MAP = {
433
+ LIGHT: 0.3,
434
+ MEDIUM: 0.6,
435
+ HEAVY: 1
436
+ };
437
+ var HPLParserError = class extends Error {
438
+ constructor(message) {
439
+ super(`HPL Parser Error: ${message}`);
440
+ this.name = "HPLParserError";
441
+ }
442
+ };
443
+ var HPLParser = class {
444
+ constructor() {
445
+ this.tokens = [];
446
+ this.pos = 0;
447
+ }
448
+ parse(input) {
449
+ this.tokens = tokenize(input);
450
+ this.pos = 0;
451
+ if (this.tokens.length === 0) {
452
+ return { type: "sequence", children: [] };
453
+ }
454
+ const children = this._parseSegments();
455
+ if (this.pos < this.tokens.length) {
456
+ throw new HPLParserError(
457
+ `Unexpected token '${this.tokens[this.pos].value}' at position ${this.pos}`
458
+ );
459
+ }
460
+ return { type: "sequence", children };
461
+ }
462
+ _parseSegments() {
463
+ const nodes = [];
464
+ while (this.pos < this.tokens.length) {
465
+ const token = this.tokens[this.pos];
466
+ if (token.type === "GROUP_END") {
467
+ break;
468
+ }
469
+ nodes.push(this._parseSegment());
470
+ }
471
+ return nodes;
472
+ }
473
+ _parseSegment() {
474
+ const token = this.tokens[this.pos];
475
+ if (token.type === "GROUP_START") {
476
+ return this._parseGroup();
477
+ }
478
+ return this._parseAtom();
479
+ }
480
+ _parseGroup() {
481
+ this.pos++;
482
+ const children = this._parseSegments();
483
+ if (this.pos >= this.tokens.length || this.tokens[this.pos].type !== "GROUP_END") {
484
+ throw new HPLParserError('Unclosed group \u2014 expected "]"');
485
+ }
486
+ this.pos++;
487
+ let repeat = 1;
488
+ if (this.pos < this.tokens.length && this.tokens[this.pos].type === "REPEAT") {
489
+ repeat = this.tokens[this.pos].repeatCount ?? 1;
490
+ this.pos++;
491
+ }
492
+ return { type: "group", children, repeat };
493
+ }
494
+ _parseAtom() {
495
+ const token = this.tokens[this.pos];
496
+ this.pos++;
497
+ switch (token.type) {
498
+ case "LIGHT":
499
+ case "MEDIUM":
500
+ case "HEAVY":
501
+ return this._makeVibrate(token.type);
502
+ case "PAUSE":
503
+ return this._makePause();
504
+ case "TAP":
505
+ return this._makeTap();
506
+ case "SUSTAIN":
507
+ return this._makeSustain();
508
+ default:
509
+ throw new HPLParserError(`Unexpected token type '${token.type}'`);
510
+ }
511
+ }
512
+ _makeVibrate(level) {
513
+ return {
514
+ type: "vibrate",
515
+ intensity: INTENSITY_MAP[level] ?? 0.5,
516
+ duration: UNIT_DURATION
517
+ };
518
+ }
519
+ _makePause() {
520
+ return { type: "pause", duration: UNIT_DURATION };
521
+ }
522
+ _makeTap() {
523
+ return { type: "tap", duration: TAP_DURATION, intensity: 1 };
524
+ }
525
+ _makeSustain() {
526
+ return { type: "sustain", extension: UNIT_DURATION };
527
+ }
528
+ };
529
+ function parseHPL(input) {
530
+ const parser = new HPLParser();
531
+ return parser.parse(input);
532
+ }
533
+
534
+ // src/patterns/compiler.ts
535
+ function compile(ast) {
536
+ const raw = compileNode(ast);
537
+ return mergeSustains(raw);
538
+ }
539
+ function compileNode(node) {
540
+ switch (node.type) {
541
+ case "vibrate":
542
+ return [{ type: "vibrate", duration: node.duration, intensity: node.intensity }];
543
+ case "pause":
544
+ return [{ type: "pause", duration: node.duration, intensity: 0 }];
545
+ case "tap":
546
+ return [{ type: "vibrate", duration: node.duration, intensity: node.intensity }];
547
+ case "sustain":
548
+ return [{ type: "vibrate", duration: node.extension, intensity: -1 }];
549
+ // sentinel
550
+ case "group": {
551
+ const groupSteps = node.children.flatMap(compileNode);
552
+ const result = [];
553
+ for (let i = 0; i < node.repeat; i++) {
554
+ result.push(...groupSteps.map((s) => ({ ...s })));
555
+ }
556
+ return result;
557
+ }
558
+ case "sequence":
559
+ return node.children.flatMap(compileNode);
560
+ }
561
+ }
562
+ function mergeSustains(steps) {
563
+ const result = [];
564
+ for (const step of steps) {
565
+ if (step.intensity === -1 && result.length > 0) {
566
+ const prev = result[result.length - 1];
567
+ prev.duration += step.duration;
568
+ } else {
569
+ result.push({ ...step });
570
+ }
571
+ }
572
+ return result;
573
+ }
574
+ function optimizeSteps(steps) {
575
+ if (steps.length === 0) return [];
576
+ const result = [{ ...steps[0] }];
577
+ for (let i = 1; i < steps.length; i++) {
578
+ const current = steps[i];
579
+ const prev = result[result.length - 1];
580
+ if (current.type === "pause" && prev.type === "pause") {
581
+ prev.duration += current.duration;
582
+ continue;
583
+ }
584
+ if (current.type === "vibrate" && prev.type === "vibrate" && Math.abs(current.intensity - prev.intensity) < 0.01) {
585
+ prev.duration += current.duration;
586
+ continue;
587
+ }
588
+ result.push({ ...current });
589
+ }
590
+ return result;
591
+ }
592
+
593
+ // src/engine/haptic-engine.ts
594
+ var HapticEngine = class _HapticEngine {
595
+ constructor(config) {
596
+ this.config = { ...DEFAULT_CONFIG, ...config };
597
+ this.adapter = this.config.adapter ?? detectAdapter();
598
+ this.adaptive = new AdaptiveEngine();
599
+ this.fallback = new FallbackManager(this.config.fallback);
600
+ }
601
+ /** Create a new engine with auto-detected adapter */
602
+ static create(config) {
603
+ return new _HapticEngine(config);
604
+ }
605
+ // ─── Semantic API ──────────────────────────────────────────
606
+ /** Light tap feedback */
607
+ async tap(intensity = 0.6) {
608
+ await this._playSteps([{ type: "vibrate", duration: 10, intensity }]);
609
+ }
610
+ /** Double tap */
611
+ async doubleTap(intensity = 0.6) {
612
+ await this._playSteps([
613
+ { type: "vibrate", duration: 10, intensity },
614
+ { type: "pause", duration: 80, intensity: 0 },
615
+ { type: "vibrate", duration: 10, intensity }
616
+ ]);
617
+ }
618
+ /** Long press feedback */
619
+ async longPress(intensity = 0.8) {
620
+ await this._playSteps([{ type: "vibrate", duration: 50, intensity }]);
621
+ }
622
+ /** Success notification */
623
+ async success() {
624
+ await this._playSteps([
625
+ { type: "vibrate", duration: 30, intensity: 0.5 },
626
+ { type: "pause", duration: 60, intensity: 0 },
627
+ { type: "vibrate", duration: 40, intensity: 0.8 }
628
+ ]);
629
+ }
630
+ /** Warning notification */
631
+ async warning() {
632
+ await this._playSteps([
633
+ { type: "vibrate", duration: 40, intensity: 0.7 },
634
+ { type: "pause", duration: 50, intensity: 0 },
635
+ { type: "vibrate", duration: 40, intensity: 0.7 },
636
+ { type: "pause", duration: 50, intensity: 0 },
637
+ { type: "vibrate", duration: 40, intensity: 0.7 }
638
+ ]);
639
+ }
640
+ /** Error notification */
641
+ async error() {
642
+ await this._playSteps([
643
+ { type: "vibrate", duration: 80, intensity: 1 },
644
+ { type: "pause", duration: 100, intensity: 0 },
645
+ { type: "vibrate", duration: 80, intensity: 1 }
646
+ ]);
647
+ }
648
+ /** Selection change feedback */
649
+ async selection() {
650
+ await this._playSteps([{ type: "vibrate", duration: 8, intensity: 0.4 }]);
651
+ }
652
+ /** Toggle feedback */
653
+ async toggle(on) {
654
+ if (on) {
655
+ await this._playSteps([{ type: "vibrate", duration: 15, intensity: 0.6 }]);
656
+ } else {
657
+ await this._playSteps([{ type: "vibrate", duration: 10, intensity: 0.3 }]);
658
+ }
659
+ }
660
+ /** Impact with style (matches iOS UIImpactFeedbackGenerator) */
661
+ async impact(style = "medium") {
662
+ const presets2 = {
663
+ light: [{ type: "vibrate", duration: 10, intensity: 0.3 }],
664
+ medium: [{ type: "vibrate", duration: 15, intensity: 0.6 }],
665
+ heavy: [{ type: "vibrate", duration: 25, intensity: 1 }],
666
+ rigid: [{ type: "vibrate", duration: 8, intensity: 0.9 }],
667
+ soft: [{ type: "vibrate", duration: 30, intensity: 0.4 }]
668
+ };
669
+ await this._playSteps(presets2[style]);
670
+ }
671
+ // ─── Parametric API ────────────────────────────────────────
672
+ /** Vibrate for a specified duration */
673
+ async vibrate(duration, intensity = 1) {
674
+ await this._playSteps([
675
+ { type: "vibrate", duration, intensity: normalizeIntensity(intensity) }
676
+ ]);
677
+ }
678
+ /**
679
+ * Play a haptic pattern.
680
+ * Accepts:
681
+ * - HPL string: "~~..##..@@"
682
+ * - HapticPattern object
683
+ * - Raw HapticStep array
684
+ */
685
+ async play(pattern) {
686
+ let steps;
687
+ if (typeof pattern === "string") {
688
+ const ast = parseHPL(pattern);
689
+ steps = compile(ast);
690
+ } else if (Array.isArray(pattern)) {
691
+ steps = pattern;
692
+ } else {
693
+ steps = pattern.steps;
694
+ }
695
+ await this._playSteps(steps);
696
+ }
697
+ // ─── Composer ──────────────────────────────────────────────
698
+ /** Create a new pattern composer */
699
+ compose() {
700
+ const composer = new PatternComposer();
701
+ composer.onPlay((steps) => this._playSteps(steps));
702
+ return composer;
703
+ }
704
+ // ─── Configuration ─────────────────────────────────────────
705
+ /** Update engine configuration */
706
+ configure(config) {
707
+ Object.assign(this.config, config);
708
+ if (config.adapter) {
709
+ this.adapter = config.adapter;
710
+ }
711
+ if (config.fallback) {
712
+ this.fallback.updateConfig(this.config.fallback);
713
+ }
714
+ }
715
+ /** Check if haptics are supported on this device */
716
+ get isSupported() {
717
+ return this.adapter.supported;
718
+ }
719
+ /** Get the current adapter name */
720
+ get adapterName() {
721
+ return this.adapter.name;
722
+ }
723
+ // ─── Lifecycle ─────────────────────────────────────────────
724
+ /** Cancel any ongoing haptic effect */
725
+ cancel() {
726
+ this.adapter.cancel();
727
+ }
728
+ /** Clean up resources */
729
+ dispose() {
730
+ this.adapter.dispose();
731
+ }
732
+ // ─── Internal ──────────────────────────────────────────────
733
+ async _playSteps(steps) {
734
+ if (!this.config.enabled || steps.length === 0) return;
735
+ let adjusted = steps.map((s) => ({
736
+ ...s,
737
+ intensity: s.type === "vibrate" ? normalizeIntensity(s.intensity * this.config.intensity) : 0
738
+ }));
739
+ const caps = this.adapter.capabilities();
740
+ adjusted = this.adaptive.adapt(adjusted, caps);
741
+ adjusted = optimizeSteps(adjusted);
742
+ if (this.adapter.supported) {
743
+ await this.adapter.playSequence(adjusted);
744
+ } else {
745
+ await this.fallback.execute(adjusted);
746
+ }
747
+ }
748
+ };
749
+
750
+ // src/patterns/validator.ts
751
+ var VALID_CHARS = new Set("~#@.|\\-[] \nx0123456789");
752
+ function validateHPL(input) {
753
+ const errors = [];
754
+ if (input.length === 0) {
755
+ return { valid: true, errors: [] };
756
+ }
757
+ for (let i = 0; i < input.length; i++) {
758
+ if (!VALID_CHARS.has(input[i])) {
759
+ errors.push(`Invalid character '${input[i]}' at position ${i}`);
760
+ }
761
+ }
762
+ let depth = 0;
763
+ for (let i = 0; i < input.length; i++) {
764
+ if (input[i] === "[") depth++;
765
+ if (input[i] === "]") depth--;
766
+ if (depth < 0) {
767
+ errors.push(`Unmatched ']' at position ${i}`);
768
+ }
769
+ }
770
+ if (depth > 0) {
771
+ errors.push(`${depth} unclosed '[' bracket(s)`);
772
+ }
773
+ for (let i = 0; i < input.length - 1; i++) {
774
+ if (input[i] === "[" && input[i + 1] === "]") {
775
+ errors.push(`Empty group at position ${i}`);
776
+ }
777
+ }
778
+ for (let i = 0; i < input.length; i++) {
779
+ if (input[i] === "x" && i + 1 < input.length && /\d/.test(input[i + 1])) {
780
+ if (i === 0) {
781
+ errors.push(`Repeat modifier 'x' at position ${i} must follow a group or atom`);
782
+ }
783
+ }
784
+ }
785
+ return { valid: errors.length === 0, errors };
786
+ }
787
+
788
+ // src/presets/ui.ts
789
+ var ui = {
790
+ /** Light button tap */
791
+ tap: {
792
+ name: "ui.tap",
793
+ steps: [{ type: "vibrate", duration: 10, intensity: 0.6 }]
794
+ },
795
+ /** Double tap */
796
+ doubleTap: {
797
+ name: "ui.doubleTap",
798
+ steps: [
799
+ { type: "vibrate", duration: 10, intensity: 0.6 },
800
+ { type: "pause", duration: 80, intensity: 0 },
801
+ { type: "vibrate", duration: 10, intensity: 0.6 }
802
+ ]
803
+ },
804
+ /** Long press acknowledgment */
805
+ longPress: {
806
+ name: "ui.longPress",
807
+ steps: [{ type: "vibrate", duration: 50, intensity: 0.8 }]
808
+ },
809
+ /** Toggle switch on */
810
+ toggleOn: {
811
+ name: "ui.toggleOn",
812
+ steps: [{ type: "vibrate", duration: 15, intensity: 0.6 }]
813
+ },
814
+ /** Toggle switch off */
815
+ toggleOff: {
816
+ name: "ui.toggleOff",
817
+ steps: [{ type: "vibrate", duration: 10, intensity: 0.3 }]
818
+ },
819
+ /** Slider snap to value */
820
+ sliderSnap: {
821
+ name: "ui.sliderSnap",
822
+ steps: [{ type: "vibrate", duration: 5, intensity: 0.4 }]
823
+ },
824
+ /** Selection changed */
825
+ selection: {
826
+ name: "ui.selection",
827
+ steps: [{ type: "vibrate", duration: 8, intensity: 0.4 }]
828
+ },
829
+ /** Pull to refresh threshold reached */
830
+ pullToRefresh: {
831
+ name: "ui.pullToRefresh",
832
+ steps: [
833
+ { type: "vibrate", duration: 20, intensity: 0.5 },
834
+ { type: "pause", duration: 40, intensity: 0 },
835
+ { type: "vibrate", duration: 30, intensity: 0.7 }
836
+ ]
837
+ },
838
+ /** Swipe action triggered */
839
+ swipe: {
840
+ name: "ui.swipe",
841
+ steps: [
842
+ { type: "vibrate", duration: 12, intensity: 0.4 },
843
+ { type: "pause", duration: 30, intensity: 0 },
844
+ { type: "vibrate", duration: 8, intensity: 0.3 }
845
+ ]
846
+ },
847
+ /** Context menu appearance */
848
+ contextMenu: {
849
+ name: "ui.contextMenu",
850
+ steps: [{ type: "vibrate", duration: 20, intensity: 0.7 }]
851
+ },
852
+ /** Drag start */
853
+ dragStart: {
854
+ name: "ui.dragStart",
855
+ steps: [{ type: "vibrate", duration: 12, intensity: 0.5 }]
856
+ },
857
+ /** Drag drop */
858
+ drop: {
859
+ name: "ui.drop",
860
+ steps: [
861
+ { type: "vibrate", duration: 20, intensity: 0.8 },
862
+ { type: "pause", duration: 30, intensity: 0 },
863
+ { type: "vibrate", duration: 10, intensity: 0.4 }
864
+ ]
865
+ }
866
+ };
867
+
868
+ // src/presets/notifications.ts
869
+ var notifications = {
870
+ /** Success — two ascending pulses */
871
+ success: {
872
+ name: "notifications.success",
873
+ steps: [
874
+ { type: "vibrate", duration: 30, intensity: 0.5 },
875
+ { type: "pause", duration: 60, intensity: 0 },
876
+ { type: "vibrate", duration: 40, intensity: 0.8 }
877
+ ]
878
+ },
879
+ /** Warning — three even pulses */
880
+ warning: {
881
+ name: "notifications.warning",
882
+ steps: [
883
+ { type: "vibrate", duration: 40, intensity: 0.7 },
884
+ { type: "pause", duration: 50, intensity: 0 },
885
+ { type: "vibrate", duration: 40, intensity: 0.7 },
886
+ { type: "pause", duration: 50, intensity: 0 },
887
+ { type: "vibrate", duration: 40, intensity: 0.7 }
888
+ ]
889
+ },
890
+ /** Error — two heavy pulses */
891
+ error: {
892
+ name: "notifications.error",
893
+ steps: [
894
+ { type: "vibrate", duration: 80, intensity: 1 },
895
+ { type: "pause", duration: 100, intensity: 0 },
896
+ { type: "vibrate", duration: 80, intensity: 1 }
897
+ ]
898
+ },
899
+ /** Info — soft single pulse */
900
+ info: {
901
+ name: "notifications.info",
902
+ steps: [
903
+ { type: "vibrate", duration: 20, intensity: 0.4 }
904
+ ]
905
+ },
906
+ /** Message received */
907
+ messageReceived: {
908
+ name: "notifications.messageReceived",
909
+ steps: [
910
+ { type: "vibrate", duration: 15, intensity: 0.5 },
911
+ { type: "pause", duration: 100, intensity: 0 },
912
+ { type: "vibrate", duration: 15, intensity: 0.5 }
913
+ ]
914
+ },
915
+ /** Alarm — urgent repeating pattern */
916
+ alarm: {
917
+ name: "notifications.alarm",
918
+ steps: [
919
+ { type: "vibrate", duration: 100, intensity: 1 },
920
+ { type: "pause", duration: 50, intensity: 0 },
921
+ { type: "vibrate", duration: 100, intensity: 1 },
922
+ { type: "pause", duration: 50, intensity: 0 },
923
+ { type: "vibrate", duration: 100, intensity: 1 },
924
+ { type: "pause", duration: 200, intensity: 0 },
925
+ { type: "vibrate", duration: 100, intensity: 1 },
926
+ { type: "pause", duration: 50, intensity: 0 },
927
+ { type: "vibrate", duration: 100, intensity: 1 },
928
+ { type: "pause", duration: 50, intensity: 0 },
929
+ { type: "vibrate", duration: 100, intensity: 1 }
930
+ ]
931
+ },
932
+ /** Reminder — gentle nudge */
933
+ reminder: {
934
+ name: "notifications.reminder",
935
+ steps: [
936
+ { type: "vibrate", duration: 25, intensity: 0.5 },
937
+ { type: "pause", duration: 150, intensity: 0 },
938
+ { type: "vibrate", duration: 25, intensity: 0.5 }
939
+ ]
940
+ }
941
+ };
942
+
943
+ // src/presets/gaming.ts
944
+ var gaming = {
945
+ /** Explosion — heavy descending */
946
+ explosion: {
947
+ name: "gaming.explosion",
948
+ steps: [
949
+ { type: "vibrate", duration: 100, intensity: 1 },
950
+ { type: "vibrate", duration: 80, intensity: 0.8 },
951
+ { type: "vibrate", duration: 60, intensity: 0.5 },
952
+ { type: "vibrate", duration: 40, intensity: 0.3 },
953
+ { type: "vibrate", duration: 30, intensity: 0.1 }
954
+ ]
955
+ },
956
+ /** Collision — sharp impact */
957
+ collision: {
958
+ name: "gaming.collision",
959
+ steps: [
960
+ { type: "vibrate", duration: 30, intensity: 1 },
961
+ { type: "pause", duration: 20, intensity: 0 },
962
+ { type: "vibrate", duration: 15, intensity: 0.5 }
963
+ ]
964
+ },
965
+ /** Heartbeat — rhythmic pulse */
966
+ heartbeat: {
967
+ name: "gaming.heartbeat",
968
+ steps: [
969
+ { type: "vibrate", duration: 20, intensity: 0.8 },
970
+ { type: "pause", duration: 80, intensity: 0 },
971
+ { type: "vibrate", duration: 30, intensity: 1 },
972
+ { type: "pause", duration: 400, intensity: 0 }
973
+ ]
974
+ },
975
+ /** Gunshot — sharp burst */
976
+ gunshot: {
977
+ name: "gaming.gunshot",
978
+ steps: [
979
+ { type: "vibrate", duration: 15, intensity: 1 },
980
+ { type: "vibrate", duration: 30, intensity: 0.4 }
981
+ ]
982
+ },
983
+ /** Sword clash — metallic ring */
984
+ swordClash: {
985
+ name: "gaming.swordClash",
986
+ steps: [
987
+ { type: "vibrate", duration: 10, intensity: 1 },
988
+ { type: "pause", duration: 10, intensity: 0 },
989
+ { type: "vibrate", duration: 30, intensity: 0.6 },
990
+ { type: "vibrate", duration: 50, intensity: 0.3 }
991
+ ]
992
+ },
993
+ /** Power up — ascending intensity */
994
+ powerUp: {
995
+ name: "gaming.powerUp",
996
+ steps: [
997
+ { type: "vibrate", duration: 40, intensity: 0.2 },
998
+ { type: "vibrate", duration: 40, intensity: 0.4 },
999
+ { type: "vibrate", duration: 40, intensity: 0.6 },
1000
+ { type: "vibrate", duration: 40, intensity: 0.8 },
1001
+ { type: "vibrate", duration: 60, intensity: 1 }
1002
+ ]
1003
+ },
1004
+ /** Damage taken — heavy stutter */
1005
+ damage: {
1006
+ name: "gaming.damage",
1007
+ steps: [
1008
+ { type: "vibrate", duration: 40, intensity: 0.9 },
1009
+ { type: "pause", duration: 20, intensity: 0 },
1010
+ { type: "vibrate", duration: 30, intensity: 0.6 },
1011
+ { type: "pause", duration: 20, intensity: 0 },
1012
+ { type: "vibrate", duration: 20, intensity: 0.3 }
1013
+ ]
1014
+ },
1015
+ /** Item pickup — light cheerful */
1016
+ pickup: {
1017
+ name: "gaming.pickup",
1018
+ steps: [
1019
+ { type: "vibrate", duration: 10, intensity: 0.3 },
1020
+ { type: "pause", duration: 40, intensity: 0 },
1021
+ { type: "vibrate", duration: 15, intensity: 0.6 }
1022
+ ]
1023
+ },
1024
+ /** Level complete — celebratory */
1025
+ levelComplete: {
1026
+ name: "gaming.levelComplete",
1027
+ steps: [
1028
+ { type: "vibrate", duration: 20, intensity: 0.5 },
1029
+ { type: "pause", duration: 60, intensity: 0 },
1030
+ { type: "vibrate", duration: 20, intensity: 0.5 },
1031
+ { type: "pause", duration: 60, intensity: 0 },
1032
+ { type: "vibrate", duration: 30, intensity: 0.7 },
1033
+ { type: "pause", duration: 60, intensity: 0 },
1034
+ { type: "vibrate", duration: 50, intensity: 1 }
1035
+ ]
1036
+ },
1037
+ /** Engine rumble — continuous vibration */
1038
+ engineRumble: {
1039
+ name: "gaming.engineRumble",
1040
+ steps: [
1041
+ { type: "vibrate", duration: 30, intensity: 0.4 },
1042
+ { type: "pause", duration: 10, intensity: 0 },
1043
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1044
+ { type: "pause", duration: 10, intensity: 0 },
1045
+ { type: "vibrate", duration: 30, intensity: 0.4 },
1046
+ { type: "pause", duration: 10, intensity: 0 },
1047
+ { type: "vibrate", duration: 30, intensity: 0.5 }
1048
+ ]
1049
+ }
1050
+ };
1051
+
1052
+ // src/presets/accessibility.ts
1053
+ var accessibility = {
1054
+ /** Confirm action — clear double pulse */
1055
+ confirm: {
1056
+ name: "accessibility.confirm",
1057
+ steps: [
1058
+ { type: "vibrate", duration: 30, intensity: 0.7 },
1059
+ { type: "pause", duration: 100, intensity: 0 },
1060
+ { type: "vibrate", duration: 30, intensity: 0.7 }
1061
+ ]
1062
+ },
1063
+ /** Deny/reject — long single buzz */
1064
+ deny: {
1065
+ name: "accessibility.deny",
1066
+ steps: [
1067
+ { type: "vibrate", duration: 200, intensity: 0.8 }
1068
+ ]
1069
+ },
1070
+ /** Boundary reached (e.g., scroll limit, min/max value) */
1071
+ boundary: {
1072
+ name: "accessibility.boundary",
1073
+ steps: [
1074
+ { type: "vibrate", duration: 15, intensity: 1 }
1075
+ ]
1076
+ },
1077
+ /** Focus change — subtle tick */
1078
+ focusChange: {
1079
+ name: "accessibility.focusChange",
1080
+ steps: [
1081
+ { type: "vibrate", duration: 5, intensity: 0.3 }
1082
+ ]
1083
+ },
1084
+ /** Counting rhythm — one tick per count */
1085
+ countTick: {
1086
+ name: "accessibility.countTick",
1087
+ steps: [
1088
+ { type: "vibrate", duration: 8, intensity: 0.5 }
1089
+ ]
1090
+ },
1091
+ /** Navigation landmark reached */
1092
+ landmark: {
1093
+ name: "accessibility.landmark",
1094
+ steps: [
1095
+ { type: "vibrate", duration: 15, intensity: 0.6 },
1096
+ { type: "pause", duration: 40, intensity: 0 },
1097
+ { type: "vibrate", duration: 15, intensity: 0.6 },
1098
+ { type: "pause", duration: 40, intensity: 0 },
1099
+ { type: "vibrate", duration: 15, intensity: 0.6 }
1100
+ ]
1101
+ },
1102
+ /** Progress checkpoint — escalating feedback */
1103
+ progressCheckpoint: {
1104
+ name: "accessibility.progressCheckpoint",
1105
+ steps: [
1106
+ { type: "vibrate", duration: 20, intensity: 0.4 },
1107
+ { type: "pause", duration: 60, intensity: 0 },
1108
+ { type: "vibrate", duration: 25, intensity: 0.7 }
1109
+ ]
1110
+ }
1111
+ };
1112
+
1113
+ // src/presets/system.ts
1114
+ var system = {
1115
+ /** Keyboard key press */
1116
+ keyPress: {
1117
+ name: "system.keyPress",
1118
+ steps: [{ type: "vibrate", duration: 5, intensity: 0.3 }]
1119
+ },
1120
+ /** Scroll tick (detent-like) */
1121
+ scrollTick: {
1122
+ name: "system.scrollTick",
1123
+ steps: [{ type: "vibrate", duration: 3, intensity: 0.2 }]
1124
+ },
1125
+ /** Scroll boundary reached */
1126
+ scrollBounce: {
1127
+ name: "system.scrollBounce",
1128
+ steps: [
1129
+ { type: "vibrate", duration: 10, intensity: 0.5 },
1130
+ { type: "vibrate", duration: 20, intensity: 0.3 }
1131
+ ]
1132
+ },
1133
+ /** Delete action */
1134
+ delete: {
1135
+ name: "system.delete",
1136
+ steps: [
1137
+ { type: "vibrate", duration: 15, intensity: 0.5 },
1138
+ { type: "pause", duration: 50, intensity: 0 },
1139
+ { type: "vibrate", duration: 25, intensity: 0.8 }
1140
+ ]
1141
+ },
1142
+ /** Undo action */
1143
+ undo: {
1144
+ name: "system.undo",
1145
+ steps: [
1146
+ { type: "vibrate", duration: 20, intensity: 0.5 },
1147
+ { type: "pause", duration: 80, intensity: 0 },
1148
+ { type: "vibrate", duration: 10, intensity: 0.3 }
1149
+ ]
1150
+ },
1151
+ /** Copy to clipboard */
1152
+ copy: {
1153
+ name: "system.copy",
1154
+ steps: [{ type: "vibrate", duration: 12, intensity: 0.4 }]
1155
+ },
1156
+ /** Paste from clipboard */
1157
+ paste: {
1158
+ name: "system.paste",
1159
+ steps: [
1160
+ { type: "vibrate", duration: 8, intensity: 0.3 },
1161
+ { type: "pause", duration: 30, intensity: 0 },
1162
+ { type: "vibrate", duration: 12, intensity: 0.5 }
1163
+ ]
1164
+ }
1165
+ };
1166
+
1167
+ // src/presets/index.ts
1168
+ var presets = {
1169
+ ui,
1170
+ notifications,
1171
+ gaming,
1172
+ accessibility,
1173
+ system
1174
+ };
1175
+
1176
+ // src/index.ts
1177
+ var haptic = HapticEngine.create();
1178
+
1179
+ export { AdaptiveEngine, FallbackManager, HPLParser, HPLParserError, HPLTokenizerError, HapticEngine, NoopAdapter, PatternComposer, WebVibrationAdapter, accessibility, compile, detectAdapter, detectPlatform, gaming, haptic, notifications, optimizeSteps, parseHPL, presets, system, tokenize, ui, validateHPL };
1180
+ //# sourceMappingURL=index.js.map
1181
+ //# sourceMappingURL=index.js.map