@hapticjs/core 0.2.0 → 0.3.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 CHANGED
@@ -32,29 +32,16 @@ var NoopAdapter = class {
32
32
  }
33
33
  };
34
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
35
  // src/adapters/web-vibration.adapter.ts
47
36
  var WebVibrationAdapter = class {
48
37
  constructor() {
49
38
  this.name = "web-vibration";
50
- this._cancelled = false;
51
39
  this.supported = typeof navigator !== "undefined" && "vibrate" in navigator;
52
40
  }
53
41
  capabilities() {
54
42
  return {
55
43
  maxIntensityLevels: 1,
56
- // on/off only
57
- minDuration: 10,
44
+ minDuration: 20,
58
45
  maxDuration: 1e4,
59
46
  supportsPattern: true,
60
47
  supportsIntensity: false,
@@ -63,36 +50,39 @@ var WebVibrationAdapter = class {
63
50
  }
64
51
  async pulse(_intensity, duration) {
65
52
  if (!this.supported) return;
66
- navigator.vibrate(duration);
53
+ navigator.vibrate(Math.max(duration, 20));
67
54
  }
68
55
  async playSequence(steps) {
69
56
  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
- }
57
+ const pattern = [];
58
+ let lastType = null;
59
+ for (const step2 of steps) {
60
+ if (step2.type === "vibrate" && step2.intensity > 0.05) {
61
+ const dur = Math.max(step2.duration, 20);
62
+ if (lastType === "vibrate") {
63
+ pattern[pattern.length - 1] += dur;
86
64
  } else {
87
- await delay(step.duration);
65
+ pattern.push(dur);
88
66
  }
67
+ lastType = "vibrate";
89
68
  } else {
90
- await delay(step.duration);
69
+ const dur = Math.max(step2.duration, 10);
70
+ if (lastType === "pause") {
71
+ pattern[pattern.length - 1] += dur;
72
+ } else {
73
+ pattern.push(dur);
74
+ }
75
+ lastType = "pause";
91
76
  }
92
77
  }
78
+ if (pattern.length > 0) {
79
+ if (steps[0]?.type === "pause" || steps[0]?.type === "vibrate" && steps[0]?.intensity <= 0.05) {
80
+ pattern.unshift(0);
81
+ }
82
+ navigator.vibrate(pattern);
83
+ }
93
84
  }
94
85
  cancel() {
95
- this._cancelled = true;
96
86
  if (this.supported) {
97
87
  navigator.vibrate(0);
98
88
  }
@@ -100,37 +90,19 @@ var WebVibrationAdapter = class {
100
90
  dispose() {
101
91
  this.cancel();
102
92
  }
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
93
  };
133
94
 
95
+ // src/utils/scheduling.ts
96
+ function delay(ms) {
97
+ return new Promise((resolve) => setTimeout(resolve, ms));
98
+ }
99
+ function clamp(value, min, max) {
100
+ return Math.min(Math.max(value, min), max);
101
+ }
102
+ function normalizeIntensity(intensity) {
103
+ return clamp(intensity, 0, 1);
104
+ }
105
+
134
106
  // src/adapters/ios-audio.adapter.ts
135
107
  var IoSAudioAdapter = class {
136
108
  constructor() {
@@ -158,12 +130,12 @@ var IoSAudioAdapter = class {
158
130
  async playSequence(steps) {
159
131
  if (!this.supported || steps.length === 0) return;
160
132
  this._cancelled = false;
161
- for (const step of steps) {
133
+ for (const step2 of steps) {
162
134
  if (this._cancelled) break;
163
- if (step.type === "vibrate" && step.intensity > 0) {
164
- await this._playTone(step.intensity, step.duration);
135
+ if (step2.type === "vibrate" && step2.intensity > 0) {
136
+ await this._playTone(step2.intensity, step2.duration);
165
137
  } else {
166
- await delay(step.duration);
138
+ await delay(step2.duration);
167
139
  }
168
140
  }
169
141
  }
@@ -290,10 +262,10 @@ function detectAdapter() {
290
262
  // src/engine/adaptive-engine.ts
291
263
  var AdaptiveEngine = class {
292
264
  adapt(steps, capabilities) {
293
- return steps.map((step) => this._adaptStep(step, capabilities));
265
+ return steps.map((step2) => this._adaptStep(step2, capabilities));
294
266
  }
295
- _adaptStep(step, caps) {
296
- const adapted = { ...step };
267
+ _adaptStep(step2, caps) {
268
+ const adapted = { ...step2 };
297
269
  if (adapted.type === "vibrate") {
298
270
  adapted.duration = clamp(adapted.duration, caps.minDuration, caps.maxDuration);
299
271
  }
@@ -685,12 +657,12 @@ function compileNode(node) {
685
657
  }
686
658
  function mergeSustains(steps) {
687
659
  const result = [];
688
- for (const step of steps) {
689
- if (step.intensity === -1 && result.length > 0) {
660
+ for (const step2 of steps) {
661
+ if (step2.intensity === -1 && result.length > 0) {
690
662
  const prev = result[result.length - 1];
691
- prev.duration += step.duration;
663
+ prev.duration += step2.duration;
692
664
  } else {
693
- result.push({ ...step });
665
+ result.push({ ...step2 });
694
666
  }
695
667
  }
696
668
  return result;
@@ -734,9 +706,9 @@ var HapticEngine = class _HapticEngine {
734
706
  /** Double tap */
735
707
  async doubleTap(intensity = 0.6) {
736
708
  await this._playSteps([
737
- { type: "vibrate", duration: 10, intensity },
709
+ { type: "vibrate", duration: 25, intensity },
738
710
  { type: "pause", duration: 80, intensity: 0 },
739
- { type: "vibrate", duration: 10, intensity }
711
+ { type: "vibrate", duration: 25, intensity }
740
712
  ]);
741
713
  }
742
714
  /** Long press feedback */
@@ -771,24 +743,24 @@ var HapticEngine = class _HapticEngine {
771
743
  }
772
744
  /** Selection change feedback */
773
745
  async selection() {
774
- await this._playSteps([{ type: "vibrate", duration: 8, intensity: 0.4 }]);
746
+ await this._playSteps([{ type: "vibrate", duration: 25, intensity: 0.5 }]);
775
747
  }
776
748
  /** Toggle feedback */
777
749
  async toggle(on) {
778
750
  if (on) {
779
- await this._playSteps([{ type: "vibrate", duration: 15, intensity: 0.6 }]);
751
+ await this._playSteps([{ type: "vibrate", duration: 30, intensity: 0.6 }]);
780
752
  } else {
781
- await this._playSteps([{ type: "vibrate", duration: 10, intensity: 0.3 }]);
753
+ await this._playSteps([{ type: "vibrate", duration: 25, intensity: 0.4 }]);
782
754
  }
783
755
  }
784
756
  /** Impact with style (matches iOS UIImpactFeedbackGenerator) */
785
757
  async impact(style = "medium") {
786
758
  const presets2 = {
787
- light: [{ type: "vibrate", duration: 10, intensity: 0.3 }],
788
- medium: [{ type: "vibrate", duration: 15, intensity: 0.6 }],
789
- heavy: [{ type: "vibrate", duration: 25, intensity: 1 }],
790
- rigid: [{ type: "vibrate", duration: 8, intensity: 0.9 }],
791
- soft: [{ type: "vibrate", duration: 30, intensity: 0.4 }]
759
+ light: [{ type: "vibrate", duration: 25, intensity: 0.4 }],
760
+ medium: [{ type: "vibrate", duration: 35, intensity: 0.7 }],
761
+ heavy: [{ type: "vibrate", duration: 50, intensity: 1 }],
762
+ rigid: [{ type: "vibrate", duration: 30, intensity: 0.9 }],
763
+ soft: [{ type: "vibrate", duration: 35, intensity: 0.5 }]
792
764
  };
793
765
  await this._playSteps(presets2[style]);
794
766
  }
@@ -871,6 +843,777 @@ var HapticEngine = class _HapticEngine {
871
843
  }
872
844
  };
873
845
 
846
+ // src/sound/sound-engine.ts
847
+ var NOTE_FREQUENCIES = {
848
+ C4: 261.63,
849
+ E4: 329.63,
850
+ G4: 392,
851
+ C5: 523.25
852
+ };
853
+ var SoundEngine = class {
854
+ constructor(options) {
855
+ this.ctx = null;
856
+ this._enabled = options?.enabled ?? true;
857
+ this.masterVolume = options?.volume ?? 0.5;
858
+ this._muted = options?.muted ?? false;
859
+ }
860
+ // ─── Public API ──────────────────────────────────────────
861
+ /** Short click sound */
862
+ async click(options) {
863
+ const pitchMap = { low: 800, mid: 1200, high: 2e3 };
864
+ const freq = pitchMap[options?.pitch ?? "mid"];
865
+ const vol = options?.volume ?? 0.3;
866
+ await this.playTone(freq, 4, { waveform: "sine", volume: vol, decay: true });
867
+ }
868
+ /** Ultra-short tick sound */
869
+ async tick() {
870
+ await this.playTone(4e3, 2, { waveform: "sine", volume: 0.15, decay: true });
871
+ }
872
+ /** Bubbly pop sound — quick frequency sweep high to low */
873
+ async pop() {
874
+ const ctx = this._getContext();
875
+ if (!ctx) return;
876
+ const osc = ctx.createOscillator();
877
+ const gain = ctx.createGain();
878
+ osc.connect(gain);
879
+ gain.connect(ctx.destination);
880
+ const now = ctx.currentTime;
881
+ const vol = this._effectiveVolume(0.25);
882
+ osc.type = "sine";
883
+ osc.frequency.setValueAtTime(1600, now);
884
+ osc.frequency.exponentialRampToValueAtTime(300, now + 0.04);
885
+ gain.gain.setValueAtTime(vol, now);
886
+ gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.06);
887
+ osc.start(now);
888
+ osc.stop(now + 0.06);
889
+ }
890
+ /** Swipe/swoosh sound — noise with quick fade */
891
+ async whoosh() {
892
+ const ctx = this._getContext();
893
+ if (!ctx) return;
894
+ const bufferSize = ctx.sampleRate * 0.08;
895
+ const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
896
+ const data = buffer.getChannelData(0);
897
+ for (let i = 0; i < bufferSize; i++) {
898
+ data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize);
899
+ }
900
+ const source = ctx.createBufferSource();
901
+ const gain = ctx.createGain();
902
+ const filter = ctx.createBiquadFilter();
903
+ source.buffer = buffer;
904
+ filter.type = "bandpass";
905
+ filter.frequency.value = 2e3;
906
+ filter.Q.value = 0.5;
907
+ source.connect(filter);
908
+ filter.connect(gain);
909
+ gain.connect(ctx.destination);
910
+ const now = ctx.currentTime;
911
+ const vol = this._effectiveVolume(0.2);
912
+ gain.gain.setValueAtTime(vol, now);
913
+ gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.08);
914
+ source.start(now);
915
+ }
916
+ /** Musical chime */
917
+ async chime(note = "C5") {
918
+ const freq = NOTE_FREQUENCIES[note] ?? 523;
919
+ await this.playTone(freq, 100, { waveform: "sine", volume: 0.2, decay: true });
920
+ }
921
+ /** Low buzzer/error tone — two descending tones */
922
+ async error() {
923
+ const ctx = this._getContext();
924
+ if (!ctx) return;
925
+ const now = ctx.currentTime;
926
+ this._scheduleTone(ctx, 400, now, 0.08, "square", 0.15);
927
+ this._scheduleTone(ctx, 280, now + 0.1, 0.1, "square", 0.15);
928
+ }
929
+ /** Ascending two-tone success sound */
930
+ async success() {
931
+ const ctx = this._getContext();
932
+ if (!ctx) return;
933
+ const now = ctx.currentTime;
934
+ this._scheduleTone(ctx, 880, now, 0.06, "sine", 0.15);
935
+ this._scheduleTone(ctx, 1320, now + 0.08, 0.08, "sine", 0.15);
936
+ }
937
+ /** Subtle tap sound */
938
+ async tap() {
939
+ await this.playTone(1e3, 3, { waveform: "sine", volume: 0.2, decay: true });
940
+ }
941
+ /** Toggle sound — ascending for on, descending for off */
942
+ async toggle(on) {
943
+ const ctx = this._getContext();
944
+ if (!ctx) return;
945
+ const now = ctx.currentTime;
946
+ if (on) {
947
+ this._scheduleTone(ctx, 600, now, 0.04, "sine", 0.15);
948
+ this._scheduleTone(ctx, 900, now + 0.06, 0.04, "sine", 0.15);
949
+ } else {
950
+ this._scheduleTone(ctx, 900, now, 0.04, "sine", 0.15);
951
+ this._scheduleTone(ctx, 600, now + 0.06, 0.04, "sine", 0.15);
952
+ }
953
+ }
954
+ /** Generic tone player */
955
+ async playTone(frequency, duration, options) {
956
+ const ctx = this._getContext();
957
+ if (!ctx) return;
958
+ const waveform = options?.waveform ?? "sine";
959
+ const vol = this._effectiveVolume(options?.volume ?? 0.3);
960
+ const decay = options?.decay ?? false;
961
+ const osc = ctx.createOscillator();
962
+ const gain = ctx.createGain();
963
+ osc.connect(gain);
964
+ gain.connect(ctx.destination);
965
+ osc.type = waveform;
966
+ osc.frequency.value = frequency;
967
+ const now = ctx.currentTime;
968
+ const durSec = duration / 1e3;
969
+ gain.gain.setValueAtTime(vol, now);
970
+ if (decay) {
971
+ gain.gain.exponentialRampToValueAtTime(1e-3, now + durSec);
972
+ }
973
+ osc.start(now);
974
+ osc.stop(now + durSec);
975
+ }
976
+ /** Set master volume (0-1) */
977
+ setVolume(volume) {
978
+ this.masterVolume = Math.max(0, Math.min(1, volume));
979
+ }
980
+ /** Mute all sounds */
981
+ mute() {
982
+ this._muted = true;
983
+ }
984
+ /** Unmute sounds */
985
+ unmute() {
986
+ this._muted = false;
987
+ }
988
+ /** Whether sounds are currently muted */
989
+ get muted() {
990
+ return this._muted;
991
+ }
992
+ /** Current master volume */
993
+ get volume() {
994
+ return this.masterVolume;
995
+ }
996
+ /** Whether the engine is enabled */
997
+ get enabled() {
998
+ return this._enabled;
999
+ }
1000
+ /** Close AudioContext and release resources */
1001
+ dispose() {
1002
+ if (this.ctx) {
1003
+ this.ctx.close().catch(() => {
1004
+ });
1005
+ this.ctx = null;
1006
+ }
1007
+ }
1008
+ // ─── Internal ──────────────────────────────────────────────
1009
+ /** Lazily create and return AudioContext, handling autoplay policy */
1010
+ _getContext() {
1011
+ if (!this._enabled || this._muted) return null;
1012
+ if (typeof AudioContext === "undefined") return null;
1013
+ if (!this.ctx) {
1014
+ this.ctx = new AudioContext();
1015
+ }
1016
+ if (this.ctx.state === "suspended") {
1017
+ this.ctx.resume().catch(() => {
1018
+ });
1019
+ }
1020
+ return this.ctx;
1021
+ }
1022
+ /** Compute effective volume from master + per-sound volume */
1023
+ _effectiveVolume(soundVolume) {
1024
+ return this.masterVolume * soundVolume;
1025
+ }
1026
+ /** Schedule a tone at a specific AudioContext time */
1027
+ _scheduleTone(ctx, frequency, startTime, durationSec, waveform, volume) {
1028
+ const osc = ctx.createOscillator();
1029
+ const gain = ctx.createGain();
1030
+ osc.connect(gain);
1031
+ gain.connect(ctx.destination);
1032
+ osc.type = waveform;
1033
+ osc.frequency.value = frequency;
1034
+ const vol = this._effectiveVolume(volume);
1035
+ gain.gain.setValueAtTime(vol, startTime);
1036
+ gain.gain.exponentialRampToValueAtTime(1e-3, startTime + durationSec);
1037
+ osc.start(startTime);
1038
+ osc.stop(startTime + durationSec);
1039
+ }
1040
+ };
1041
+
1042
+ // src/visual/visual-engine.ts
1043
+ var STYLE_ID = "__hapticjs_visual_keyframes__";
1044
+ var VisualEngine = class {
1045
+ constructor(options) {
1046
+ this._styleInjected = false;
1047
+ this._cleanups = [];
1048
+ this._enabled = options?.enabled ?? true;
1049
+ this._target = options?.target ?? null;
1050
+ this._intensity = options?.intensity ?? 1;
1051
+ }
1052
+ // ─── Public API ──────────────────────────────────────────
1053
+ /** Quick screen flash overlay */
1054
+ flash(options) {
1055
+ if (!this._canRun()) return;
1056
+ const color = options?.color ?? "white";
1057
+ const duration = options?.duration ?? 100;
1058
+ const opacity = options?.opacity ?? 0.15;
1059
+ const overlay = document.createElement("div");
1060
+ Object.assign(overlay.style, {
1061
+ position: "fixed",
1062
+ inset: "0",
1063
+ backgroundColor: color,
1064
+ opacity: String(opacity * this._intensity),
1065
+ pointerEvents: "none",
1066
+ zIndex: "99999",
1067
+ transition: `opacity ${duration}ms ease-out`
1068
+ });
1069
+ document.body.appendChild(overlay);
1070
+ requestAnimationFrame(() => {
1071
+ overlay.style.opacity = "0";
1072
+ });
1073
+ const timer = setTimeout(() => {
1074
+ overlay.remove();
1075
+ this._removeCleanup(cleanup);
1076
+ }, duration + 50);
1077
+ const cleanup = () => {
1078
+ clearTimeout(timer);
1079
+ overlay.remove();
1080
+ };
1081
+ this._cleanups.push(cleanup);
1082
+ }
1083
+ /** CSS shake animation on target */
1084
+ shake(options) {
1085
+ if (!this._canRun()) return;
1086
+ this._injectStyles();
1087
+ const target = this._getTarget();
1088
+ const intensity = (options?.intensity ?? 3) * this._intensity;
1089
+ const duration = options?.duration ?? 200;
1090
+ const magnitude = Math.round(intensity);
1091
+ const name = `hapticjs-shake-${magnitude}`;
1092
+ this._ensureKeyframes(
1093
+ name,
1094
+ `0%,100%{transform:translateX(0)}10%,30%,50%,70%,90%{transform:translateX(-${magnitude}px)}20%,40%,60%,80%{transform:translateX(${magnitude}px)}`
1095
+ );
1096
+ this._applyAnimation(target, name, duration);
1097
+ }
1098
+ /** Scale pulse animation */
1099
+ pulse(options) {
1100
+ if (!this._canRun()) return;
1101
+ this._injectStyles();
1102
+ const target = this._getTarget();
1103
+ const scale = options?.scale ?? 1.02;
1104
+ const duration = options?.duration ?? 150;
1105
+ const adjusted = 1 + (scale - 1) * this._intensity;
1106
+ const name = `hapticjs-pulse-${Math.round(adjusted * 1e3)}`;
1107
+ this._ensureKeyframes(
1108
+ name,
1109
+ `0%,100%{transform:scale(1)}50%{transform:scale(${adjusted})}`
1110
+ );
1111
+ this._applyAnimation(target, name, duration);
1112
+ }
1113
+ /** Material Design style ripple at coordinates */
1114
+ ripple(x, y, options) {
1115
+ if (!this._canRun()) return;
1116
+ this._injectStyles();
1117
+ const color = options?.color ?? "rgba(255,255,255,0.4)";
1118
+ const size = options?.size ?? 100;
1119
+ const duration = options?.duration ?? 400;
1120
+ this._ensureKeyframes(
1121
+ "hapticjs-ripple",
1122
+ `0%{transform:scale(0);opacity:1}100%{transform:scale(4);opacity:0}`
1123
+ );
1124
+ const el = document.createElement("div");
1125
+ const half = size / 2;
1126
+ Object.assign(el.style, {
1127
+ position: "fixed",
1128
+ left: `${x - half}px`,
1129
+ top: `${y - half}px`,
1130
+ width: `${size}px`,
1131
+ height: `${size}px`,
1132
+ borderRadius: "50%",
1133
+ backgroundColor: color,
1134
+ pointerEvents: "none",
1135
+ zIndex: "99999",
1136
+ animation: `hapticjs-ripple ${duration}ms ease-out forwards`
1137
+ });
1138
+ document.body.appendChild(el);
1139
+ const timer = setTimeout(() => {
1140
+ el.remove();
1141
+ this._removeCleanup(cleanup);
1142
+ }, duration + 50);
1143
+ const cleanup = () => {
1144
+ clearTimeout(timer);
1145
+ el.remove();
1146
+ };
1147
+ this._cleanups.push(cleanup);
1148
+ }
1149
+ /** Box shadow glow effect */
1150
+ glow(options) {
1151
+ if (!this._canRun()) return;
1152
+ const target = this._getTarget();
1153
+ const color = options?.color ?? "rgba(59,130,246,0.5)";
1154
+ const duration = options?.duration ?? 300;
1155
+ const size = (options?.size ?? 15) * this._intensity;
1156
+ const prev = target.style.boxShadow;
1157
+ const prevTransition = target.style.transition;
1158
+ target.style.transition = `box-shadow ${duration / 2}ms ease-in-out`;
1159
+ target.style.boxShadow = `0 0 ${size}px ${color}`;
1160
+ const timer = setTimeout(() => {
1161
+ target.style.boxShadow = prev;
1162
+ setTimeout(() => {
1163
+ target.style.transition = prevTransition;
1164
+ this._removeCleanup(cleanup);
1165
+ }, duration / 2);
1166
+ }, duration / 2);
1167
+ const cleanup = () => {
1168
+ clearTimeout(timer);
1169
+ target.style.boxShadow = prev;
1170
+ target.style.transition = prevTransition;
1171
+ };
1172
+ this._cleanups.push(cleanup);
1173
+ }
1174
+ /** Bounce animation on target */
1175
+ bounce(options) {
1176
+ if (!this._canRun()) return;
1177
+ this._injectStyles();
1178
+ const target = this._getTarget();
1179
+ const height = (options?.height ?? 8) * this._intensity;
1180
+ const duration = options?.duration ?? 300;
1181
+ const name = `hapticjs-bounce-${Math.round(height)}`;
1182
+ this._ensureKeyframes(
1183
+ name,
1184
+ `0%,100%{transform:translateY(0)}40%{transform:translateY(-${height}px)}60%{transform:translateY(-${Math.round(height * 0.4)}px)}`
1185
+ );
1186
+ this._applyAnimation(target, name, duration);
1187
+ }
1188
+ /** Jello/wobble animation */
1189
+ jello(options) {
1190
+ if (!this._canRun()) return;
1191
+ this._injectStyles();
1192
+ const target = this._getTarget();
1193
+ const intensity = (options?.intensity ?? 5) * this._intensity;
1194
+ const duration = options?.duration ?? 400;
1195
+ const skew = intensity;
1196
+ const name = `hapticjs-jello-${Math.round(skew)}`;
1197
+ this._ensureKeyframes(
1198
+ name,
1199
+ `0%,100%{transform:skew(0)}20%{transform:skew(-${skew}deg)}40%{transform:skew(${skew * 0.6}deg)}60%{transform:skew(-${skew * 0.3}deg)}80%{transform:skew(${skew * 0.1}deg)}`
1200
+ );
1201
+ this._applyAnimation(target, name, duration);
1202
+ }
1203
+ /** Rubber band scale effect */
1204
+ rubber(options) {
1205
+ if (!this._canRun()) return;
1206
+ this._injectStyles();
1207
+ const target = this._getTarget();
1208
+ const scaleX = options?.scaleX ?? 1.15;
1209
+ const scaleY = options?.scaleY ?? 0.85;
1210
+ const duration = options?.duration ?? 300;
1211
+ const name = `hapticjs-rubber-${Math.round(scaleX * 100)}-${Math.round(scaleY * 100)}`;
1212
+ this._ensureKeyframes(
1213
+ name,
1214
+ `0%,100%{transform:scale(1,1)}30%{transform:scale(${scaleX},${scaleY})}60%{transform:scale(${2 - scaleX},${2 - scaleY})}`
1215
+ );
1216
+ this._applyAnimation(target, name, duration);
1217
+ }
1218
+ /** Brief background color highlight */
1219
+ highlight(options) {
1220
+ if (!this._canRun()) return;
1221
+ const target = this._getTarget();
1222
+ const color = options?.color ?? "rgba(255,255,0,0.2)";
1223
+ const duration = options?.duration ?? 300;
1224
+ const prev = target.style.backgroundColor;
1225
+ const prevTransition = target.style.transition;
1226
+ target.style.transition = `background-color ${duration / 2}ms ease-in-out`;
1227
+ target.style.backgroundColor = color;
1228
+ const timer = setTimeout(() => {
1229
+ target.style.backgroundColor = prev;
1230
+ setTimeout(() => {
1231
+ target.style.transition = prevTransition;
1232
+ this._removeCleanup(cleanup);
1233
+ }, duration / 2);
1234
+ }, duration / 2);
1235
+ const cleanup = () => {
1236
+ clearTimeout(timer);
1237
+ target.style.backgroundColor = prev;
1238
+ target.style.transition = prevTransition;
1239
+ };
1240
+ this._cleanups.push(cleanup);
1241
+ }
1242
+ /** Change the target element for animations */
1243
+ setTarget(element) {
1244
+ this._target = element;
1245
+ }
1246
+ /** Whether the engine is enabled */
1247
+ get enabled() {
1248
+ return this._enabled;
1249
+ }
1250
+ /** Current intensity multiplier */
1251
+ get intensity() {
1252
+ return this._intensity;
1253
+ }
1254
+ /** Remove all active animations and clean up */
1255
+ dispose() {
1256
+ for (const cleanup of this._cleanups) {
1257
+ cleanup();
1258
+ }
1259
+ this._cleanups = [];
1260
+ }
1261
+ // ─── Internal ──────────────────────────────────────────────
1262
+ _canRun() {
1263
+ return this._enabled && typeof document !== "undefined";
1264
+ }
1265
+ _getTarget() {
1266
+ return this._target ?? document.body;
1267
+ }
1268
+ /** Inject a <style> tag for keyframes on first use */
1269
+ _injectStyles() {
1270
+ if (this._styleInjected) return;
1271
+ if (typeof document === "undefined") return;
1272
+ if (!document.getElementById(STYLE_ID)) {
1273
+ const style = document.createElement("style");
1274
+ style.id = STYLE_ID;
1275
+ style.textContent = "";
1276
+ document.head.appendChild(style);
1277
+ }
1278
+ this._styleInjected = true;
1279
+ }
1280
+ /** Ensure a @keyframes rule exists in our style tag */
1281
+ _ensureKeyframes(name, frames) {
1282
+ const style = document.getElementById(STYLE_ID);
1283
+ if (!style) return;
1284
+ if (style.textContent?.includes(`@keyframes ${name}`)) return;
1285
+ style.textContent += `@keyframes ${name}{${frames}}`;
1286
+ }
1287
+ /** Apply a CSS animation to an element and clean up after */
1288
+ _applyAnimation(target, animationName, duration) {
1289
+ const prev = target.style.animation;
1290
+ target.style.animation = `${animationName} ${duration}ms ease-in-out`;
1291
+ const timer = setTimeout(() => {
1292
+ target.style.animation = prev;
1293
+ this._removeCleanup(cleanup);
1294
+ }, duration + 50);
1295
+ const cleanup = () => {
1296
+ clearTimeout(timer);
1297
+ target.style.animation = prev;
1298
+ };
1299
+ this._cleanups.push(cleanup);
1300
+ }
1301
+ _removeCleanup(fn) {
1302
+ const idx = this._cleanups.indexOf(fn);
1303
+ if (idx !== -1) this._cleanups.splice(idx, 1);
1304
+ }
1305
+ };
1306
+
1307
+ // src/themes/theme-manager.ts
1308
+ var themes = {
1309
+ default: {
1310
+ name: "default",
1311
+ hapticIntensity: 0.7,
1312
+ soundEnabled: true,
1313
+ soundVolume: 0.3,
1314
+ visualEnabled: true,
1315
+ visualStyle: "flash",
1316
+ colors: {
1317
+ primary: "#3b82f6",
1318
+ success: "#22c55e",
1319
+ error: "#ef4444",
1320
+ warning: "#eab308"
1321
+ }
1322
+ },
1323
+ gaming: {
1324
+ name: "gaming",
1325
+ hapticIntensity: 1,
1326
+ soundEnabled: true,
1327
+ soundVolume: 0.8,
1328
+ visualEnabled: true,
1329
+ visualStyle: "shake",
1330
+ colors: {
1331
+ primary: "#a855f7",
1332
+ success: "#00ff88",
1333
+ error: "#ff2222",
1334
+ warning: "#ff8800"
1335
+ }
1336
+ },
1337
+ minimal: {
1338
+ name: "minimal",
1339
+ hapticIntensity: 0.4,
1340
+ soundEnabled: false,
1341
+ soundVolume: 0,
1342
+ visualEnabled: true,
1343
+ visualStyle: "pulse",
1344
+ colors: {
1345
+ primary: "#6b7280",
1346
+ success: "#9ca3af",
1347
+ error: "#4b5563",
1348
+ warning: "#d1d5db"
1349
+ }
1350
+ },
1351
+ luxury: {
1352
+ name: "luxury",
1353
+ hapticIntensity: 0.6,
1354
+ soundEnabled: true,
1355
+ soundVolume: 0.25,
1356
+ visualEnabled: true,
1357
+ visualStyle: "glow",
1358
+ colors: {
1359
+ primary: "#d4af37",
1360
+ success: "#50c878",
1361
+ error: "#8b0000",
1362
+ warning: "#cd853f"
1363
+ }
1364
+ },
1365
+ retro: {
1366
+ name: "retro",
1367
+ hapticIntensity: 0.9,
1368
+ soundEnabled: true,
1369
+ soundVolume: 0.5,
1370
+ visualEnabled: true,
1371
+ visualStyle: "flash",
1372
+ colors: {
1373
+ primary: "#00ff00",
1374
+ success: "#00ffff",
1375
+ error: "#ff0000",
1376
+ warning: "#ffff00"
1377
+ }
1378
+ },
1379
+ nature: {
1380
+ name: "nature",
1381
+ hapticIntensity: 0.5,
1382
+ soundEnabled: true,
1383
+ soundVolume: 0.2,
1384
+ visualEnabled: true,
1385
+ visualStyle: "pulse",
1386
+ colors: {
1387
+ primary: "#2d6a4f",
1388
+ success: "#40916c",
1389
+ error: "#9b2226",
1390
+ warning: "#ee9b00"
1391
+ }
1392
+ },
1393
+ silent: {
1394
+ name: "silent",
1395
+ hapticIntensity: 0.7,
1396
+ soundEnabled: false,
1397
+ soundVolume: 0,
1398
+ visualEnabled: false,
1399
+ visualStyle: "flash",
1400
+ colors: {
1401
+ primary: "#3b82f6",
1402
+ success: "#22c55e",
1403
+ error: "#ef4444",
1404
+ warning: "#eab308"
1405
+ }
1406
+ },
1407
+ accessible: {
1408
+ name: "accessible",
1409
+ hapticIntensity: 1,
1410
+ soundEnabled: true,
1411
+ soundVolume: 0.6,
1412
+ visualEnabled: true,
1413
+ visualStyle: "flash",
1414
+ colors: {
1415
+ primary: "#0000ff",
1416
+ success: "#008000",
1417
+ error: "#ff0000",
1418
+ warning: "#ff8c00"
1419
+ }
1420
+ }
1421
+ };
1422
+ var ThemeManager = class {
1423
+ constructor() {
1424
+ this._registry = new Map(Object.entries(themes));
1425
+ this._current = themes.default;
1426
+ }
1427
+ /** Apply a theme by name or provide a custom preset */
1428
+ setTheme(name) {
1429
+ if (typeof name === "string") {
1430
+ const preset = this._registry.get(name);
1431
+ if (!preset) {
1432
+ throw new Error(`Unknown theme: "${name}". Available: ${this.listThemes().join(", ")}`);
1433
+ }
1434
+ this._current = preset;
1435
+ } else {
1436
+ this._registry.set(name.name, name);
1437
+ this._current = name;
1438
+ }
1439
+ }
1440
+ /** Get the current theme preset */
1441
+ getTheme() {
1442
+ return this._current;
1443
+ }
1444
+ /** Current theme name */
1445
+ get current() {
1446
+ return this._current.name;
1447
+ }
1448
+ /** List all available theme names */
1449
+ listThemes() {
1450
+ return Array.from(this._registry.keys());
1451
+ }
1452
+ /** Register a custom theme preset */
1453
+ registerTheme(preset) {
1454
+ this._registry.set(preset.name, preset);
1455
+ }
1456
+ };
1457
+
1458
+ // src/engine/sensory-engine.ts
1459
+ var SensoryEngine = class _SensoryEngine {
1460
+ constructor(options) {
1461
+ this._haptic = new HapticEngine(options?.haptic);
1462
+ this._sound = new SoundEngine(options?.sound);
1463
+ this._visual = new VisualEngine(options?.visual);
1464
+ this._themes = new ThemeManager();
1465
+ if (options?.theme) {
1466
+ this.setTheme(options.theme);
1467
+ }
1468
+ }
1469
+ /** Factory method */
1470
+ static create(options) {
1471
+ return new _SensoryEngine(options);
1472
+ }
1473
+ // ─── Multi-sensory API ─────────────────────────────────────
1474
+ /** Tap: vibrate + click sound + pulse visual */
1475
+ async tap() {
1476
+ const theme = this._themes.getTheme();
1477
+ await Promise.all([
1478
+ this._haptic.tap(),
1479
+ theme.soundEnabled ? this._sound.click({ pitch: "mid" }) : Promise.resolve(),
1480
+ theme.visualEnabled ? this._runVisual(theme, "tap") : Promise.resolve()
1481
+ ]);
1482
+ }
1483
+ /** Success: haptic success + success sound + green glow */
1484
+ async success() {
1485
+ const theme = this._themes.getTheme();
1486
+ await Promise.all([
1487
+ this._haptic.success(),
1488
+ theme.soundEnabled ? this._sound.success() : Promise.resolve(),
1489
+ theme.visualEnabled ? this._visual.glow({ color: theme.colors.success, duration: 300 }) : Promise.resolve()
1490
+ ]);
1491
+ }
1492
+ /** Error: haptic error + error sound + red flash */
1493
+ async error() {
1494
+ const theme = this._themes.getTheme();
1495
+ await Promise.all([
1496
+ this._haptic.error(),
1497
+ theme.soundEnabled ? this._sound.error() : Promise.resolve(),
1498
+ theme.visualEnabled ? this._visual.flash({ color: theme.colors.error, duration: 150, opacity: 0.2 }) : Promise.resolve()
1499
+ ]);
1500
+ }
1501
+ /** Warning: haptic warning + warning sound + yellow flash */
1502
+ async warning() {
1503
+ const theme = this._themes.getTheme();
1504
+ await Promise.all([
1505
+ this._haptic.warning(),
1506
+ theme.soundEnabled ? this._sound.chime("E4") : Promise.resolve(),
1507
+ theme.visualEnabled ? this._visual.flash({ color: theme.colors.warning, duration: 150, opacity: 0.2 }) : Promise.resolve()
1508
+ ]);
1509
+ }
1510
+ /** Selection: haptic selection + tick sound + subtle pulse */
1511
+ async selection() {
1512
+ const theme = this._themes.getTheme();
1513
+ await Promise.all([
1514
+ this._haptic.selection(),
1515
+ theme.soundEnabled ? this._sound.tick() : Promise.resolve(),
1516
+ theme.visualEnabled ? this._visual.pulse({ scale: 1.01, duration: 100 }) : Promise.resolve()
1517
+ ]);
1518
+ }
1519
+ /** Toggle: haptic toggle + toggle sound + pulse */
1520
+ async toggle(on) {
1521
+ const theme = this._themes.getTheme();
1522
+ await Promise.all([
1523
+ this._haptic.toggle(on),
1524
+ theme.soundEnabled ? this._sound.toggle(on) : Promise.resolve(),
1525
+ theme.visualEnabled ? this._visual.pulse({ scale: on ? 1.03 : 0.98, duration: 150 }) : Promise.resolve()
1526
+ ]);
1527
+ }
1528
+ /** Play a haptic pattern (sound/visual auto-mapped from theme) */
1529
+ async play(pattern) {
1530
+ const theme = this._themes.getTheme();
1531
+ await Promise.all([
1532
+ this._haptic.play(pattern),
1533
+ theme.soundEnabled ? this._sound.tap() : Promise.resolve(),
1534
+ theme.visualEnabled ? this._runVisual(theme, "play") : Promise.resolve()
1535
+ ]);
1536
+ }
1537
+ // ─── Theme ─────────────────────────────────────────────────
1538
+ /** Apply a theme by name or preset */
1539
+ setTheme(name) {
1540
+ this._themes.setTheme(name);
1541
+ const theme = this._themes.getTheme();
1542
+ this._haptic.configure({ intensity: theme.hapticIntensity });
1543
+ this._sound.setVolume(theme.soundVolume);
1544
+ if (!theme.soundEnabled) {
1545
+ this._sound.mute();
1546
+ } else {
1547
+ this._sound.unmute();
1548
+ }
1549
+ }
1550
+ // ─── Accessors ─────────────────────────────────────────────
1551
+ /** Access the underlying HapticEngine */
1552
+ get haptic() {
1553
+ return this._haptic;
1554
+ }
1555
+ /** Access the SoundEngine */
1556
+ get sound() {
1557
+ return this._sound;
1558
+ }
1559
+ /** Access the VisualEngine */
1560
+ get visual() {
1561
+ return this._visual;
1562
+ }
1563
+ /** Access the ThemeManager */
1564
+ get themes() {
1565
+ return this._themes;
1566
+ }
1567
+ // ─── Configuration ─────────────────────────────────────────
1568
+ /** Configure all engines */
1569
+ configure(options) {
1570
+ if (options.haptic) {
1571
+ this._haptic.configure(options.haptic);
1572
+ }
1573
+ if (options.sound) {
1574
+ this._sound = new SoundEngine(options.sound);
1575
+ }
1576
+ if (options.visual) {
1577
+ this._visual = new VisualEngine(options.visual);
1578
+ }
1579
+ if (options.theme) {
1580
+ this.setTheme(options.theme);
1581
+ }
1582
+ }
1583
+ // ─── Lifecycle ─────────────────────────────────────────────
1584
+ /** Clean up all engines */
1585
+ dispose() {
1586
+ this._haptic.dispose();
1587
+ this._sound.dispose();
1588
+ this._visual.dispose();
1589
+ }
1590
+ // ─── Internal ──────────────────────────────────────────────
1591
+ /** Run the appropriate visual effect based on theme style */
1592
+ _runVisual(theme, context) {
1593
+ switch (theme.visualStyle) {
1594
+ case "flash":
1595
+ this._visual.flash({ color: theme.colors.primary });
1596
+ break;
1597
+ case "ripple":
1598
+ if (typeof window !== "undefined") {
1599
+ this._visual.ripple(window.innerWidth / 2, window.innerHeight / 2, {
1600
+ color: theme.colors.primary
1601
+ });
1602
+ }
1603
+ break;
1604
+ case "shake":
1605
+ this._visual.shake({ intensity: context === "play" ? 5 : 3 });
1606
+ break;
1607
+ case "glow":
1608
+ this._visual.glow({ color: theme.colors.primary });
1609
+ break;
1610
+ case "pulse":
1611
+ this._visual.pulse();
1612
+ break;
1613
+ }
1614
+ }
1615
+ };
1616
+
874
1617
  // src/patterns/validator.ts
875
1618
  var VALID_CHARS = new Set("~#@.|\\-[] \nx0123456789");
876
1619
  function validateHPL(input) {
@@ -970,18 +1713,18 @@ function validateExport(data) {
970
1713
  throw new Error('Invalid pattern data: "steps" must be an array');
971
1714
  }
972
1715
  for (let i = 0; i < obj.steps.length; i++) {
973
- const step = obj.steps[i];
974
- if (step.type !== "vibrate" && step.type !== "pause") {
1716
+ const step2 = obj.steps[i];
1717
+ if (step2.type !== "vibrate" && step2.type !== "pause") {
975
1718
  throw new Error(
976
1719
  `Invalid step at index ${i}: "type" must be "vibrate" or "pause"`
977
1720
  );
978
1721
  }
979
- if (typeof step.duration !== "number" || step.duration < 0) {
1722
+ if (typeof step2.duration !== "number" || step2.duration < 0) {
980
1723
  throw new Error(
981
1724
  `Invalid step at index ${i}: "duration" must be a non-negative number`
982
1725
  );
983
1726
  }
984
- if (typeof step.intensity !== "number" || step.intensity < 0 || step.intensity > 1) {
1727
+ if (typeof step2.intensity !== "number" || step2.intensity < 0 || step2.intensity > 1) {
985
1728
  throw new Error(
986
1729
  `Invalid step at index ${i}: "intensity" must be a number between 0 and 1`
987
1730
  );
@@ -1024,20 +1767,152 @@ function patternFromDataURL(url) {
1024
1767
  return patternFromJSON(json);
1025
1768
  }
1026
1769
 
1770
+ // src/recorder/pattern-recorder.ts
1771
+ var DEFAULT_TAP_DURATION = 10;
1772
+ var DEFAULT_GRID_MS = 50;
1773
+ var DEFAULT_INTENSITY = 0.6;
1774
+ var PatternRecorder = class {
1775
+ constructor(options) {
1776
+ this.taps = [];
1777
+ this.recording = false;
1778
+ this.startTime = 0;
1779
+ this.stopTime = 0;
1780
+ this.tapCallbacks = [];
1781
+ this.nowFn = options?.now ?? (() => Date.now());
1782
+ }
1783
+ /** Whether the recorder is currently recording */
1784
+ get isRecording() {
1785
+ return this.recording;
1786
+ }
1787
+ /** Total duration of the recording in ms */
1788
+ get duration() {
1789
+ if (this.taps.length === 0) return 0;
1790
+ if (this.recording) {
1791
+ return this.nowFn() - this.startTime;
1792
+ }
1793
+ return this.stopTime - this.startTime;
1794
+ }
1795
+ /** Number of taps recorded */
1796
+ get tapCount() {
1797
+ return this.taps.length;
1798
+ }
1799
+ /** Begin recording. Records timestamps of tap events. */
1800
+ start() {
1801
+ this.taps = [];
1802
+ this.recording = true;
1803
+ this.startTime = this.nowFn();
1804
+ this.stopTime = 0;
1805
+ }
1806
+ /** Register a tap at current time. */
1807
+ tap(intensity = DEFAULT_INTENSITY) {
1808
+ if (!this.recording) return;
1809
+ const now = this.nowFn();
1810
+ const time = now - this.startTime;
1811
+ const clamped = Math.max(0, Math.min(1, intensity));
1812
+ const recorded = { time, intensity: clamped };
1813
+ this.taps.push(recorded);
1814
+ for (const cb of this.tapCallbacks) {
1815
+ cb(recorded, this.taps.length - 1);
1816
+ }
1817
+ }
1818
+ /** Stop recording, return the recorded taps. */
1819
+ stop() {
1820
+ this.recording = false;
1821
+ this.stopTime = this.nowFn();
1822
+ return [...this.taps];
1823
+ }
1824
+ /** Register a callback for each tap (for visual feedback during recording). */
1825
+ onTap(callback) {
1826
+ this.tapCallbacks.push(callback);
1827
+ }
1828
+ /** Reset the recorder. */
1829
+ clear() {
1830
+ this.taps = [];
1831
+ this.recording = false;
1832
+ this.startTime = 0;
1833
+ this.stopTime = 0;
1834
+ }
1835
+ /**
1836
+ * Snap taps to a grid for cleaner patterns.
1837
+ * @param gridMs Grid size in ms (default 50ms)
1838
+ */
1839
+ quantize(gridMs = DEFAULT_GRID_MS) {
1840
+ this.taps = this.taps.map((tap) => ({
1841
+ ...tap,
1842
+ time: Math.round(tap.time / gridMs) * gridMs
1843
+ }));
1844
+ }
1845
+ /**
1846
+ * Convert recorded taps to an HPL string.
1847
+ *
1848
+ * Maps gaps between taps to `.` characters (each 50ms),
1849
+ * and tap intensities to `~` (light), `#` (medium), `@` (heavy).
1850
+ */
1851
+ toHPL() {
1852
+ if (this.taps.length === 0) return "";
1853
+ const sorted = [...this.taps].sort((a, b) => a.time - b.time);
1854
+ let hpl = "";
1855
+ for (let i = 0; i < sorted.length; i++) {
1856
+ if (i > 0) {
1857
+ const gap = sorted[i].time - sorted[i - 1].time;
1858
+ if (gap >= 25) {
1859
+ const pauses = Math.round(gap / DEFAULT_GRID_MS);
1860
+ hpl += ".".repeat(pauses);
1861
+ }
1862
+ }
1863
+ hpl += intensityToChar(sorted[i].intensity);
1864
+ }
1865
+ return hpl;
1866
+ }
1867
+ /** Convert recorded taps to HapticStep[]. */
1868
+ toSteps() {
1869
+ if (this.taps.length === 0) return [];
1870
+ const sorted = [...this.taps].sort((a, b) => a.time - b.time);
1871
+ const steps = [];
1872
+ for (let i = 0; i < sorted.length; i++) {
1873
+ if (i > 0) {
1874
+ const gap = sorted[i].time - sorted[i - 1].time;
1875
+ if (gap >= 25) {
1876
+ const quantized = Math.round(gap / DEFAULT_GRID_MS) * DEFAULT_GRID_MS;
1877
+ steps.push({ type: "pause", duration: quantized, intensity: 0 });
1878
+ }
1879
+ }
1880
+ steps.push({
1881
+ type: "vibrate",
1882
+ duration: DEFAULT_TAP_DURATION,
1883
+ intensity: sorted[i].intensity
1884
+ });
1885
+ }
1886
+ return steps;
1887
+ }
1888
+ /** Convert recorded taps to a HapticPattern. */
1889
+ toPattern(name) {
1890
+ return {
1891
+ name: name ?? "recorded-pattern",
1892
+ steps: this.toSteps()
1893
+ };
1894
+ }
1895
+ };
1896
+ function intensityToChar(intensity) {
1897
+ if (intensity < 0.35) return "~";
1898
+ if (intensity <= 0.7) return "#";
1899
+ return "@";
1900
+ }
1901
+
1027
1902
  // src/presets/ui.ts
1028
1903
  var ui = {
1029
1904
  /** Light button tap */
1030
1905
  tap: {
1031
1906
  name: "ui.tap",
1032
- steps: [{ type: "vibrate", duration: 10, intensity: 0.6 }]
1907
+ steps: [{ type: "vibrate", duration: 30, intensity: 0.6 }]
1033
1908
  },
1034
1909
  /** Double tap */
1035
1910
  doubleTap: {
1036
1911
  name: "ui.doubleTap",
1037
1912
  steps: [
1038
- { type: "vibrate", duration: 10, intensity: 0.6 },
1913
+ { type: "vibrate", duration: 25, intensity: 0.6 },
1039
1914
  { type: "pause", duration: 80, intensity: 0 },
1040
- { type: "vibrate", duration: 10, intensity: 0.6 }
1915
+ { type: "vibrate", duration: 25, intensity: 0.6 }
1041
1916
  ]
1042
1917
  },
1043
1918
  /** Long press acknowledgment */
@@ -1048,58 +1923,58 @@ var ui = {
1048
1923
  /** Toggle switch on */
1049
1924
  toggleOn: {
1050
1925
  name: "ui.toggleOn",
1051
- steps: [{ type: "vibrate", duration: 15, intensity: 0.6 }]
1926
+ steps: [{ type: "vibrate", duration: 30, intensity: 0.6 }]
1052
1927
  },
1053
1928
  /** Toggle switch off */
1054
1929
  toggleOff: {
1055
1930
  name: "ui.toggleOff",
1056
- steps: [{ type: "vibrate", duration: 10, intensity: 0.3 }]
1931
+ steps: [{ type: "vibrate", duration: 25, intensity: 0.4 }]
1057
1932
  },
1058
1933
  /** Slider snap to value */
1059
1934
  sliderSnap: {
1060
1935
  name: "ui.sliderSnap",
1061
- steps: [{ type: "vibrate", duration: 5, intensity: 0.4 }]
1936
+ steps: [{ type: "vibrate", duration: 25, intensity: 0.5 }]
1062
1937
  },
1063
1938
  /** Selection changed */
1064
1939
  selection: {
1065
1940
  name: "ui.selection",
1066
- steps: [{ type: "vibrate", duration: 8, intensity: 0.4 }]
1941
+ steps: [{ type: "vibrate", duration: 25, intensity: 0.5 }]
1067
1942
  },
1068
1943
  /** Pull to refresh threshold reached */
1069
1944
  pullToRefresh: {
1070
1945
  name: "ui.pullToRefresh",
1071
1946
  steps: [
1072
- { type: "vibrate", duration: 20, intensity: 0.5 },
1947
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1073
1948
  { type: "pause", duration: 40, intensity: 0 },
1074
- { type: "vibrate", duration: 30, intensity: 0.7 }
1949
+ { type: "vibrate", duration: 40, intensity: 0.7 }
1075
1950
  ]
1076
1951
  },
1077
1952
  /** Swipe action triggered */
1078
1953
  swipe: {
1079
1954
  name: "ui.swipe",
1080
1955
  steps: [
1081
- { type: "vibrate", duration: 12, intensity: 0.4 },
1956
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1082
1957
  { type: "pause", duration: 30, intensity: 0 },
1083
- { type: "vibrate", duration: 8, intensity: 0.3 }
1958
+ { type: "vibrate", duration: 25, intensity: 0.4 }
1084
1959
  ]
1085
1960
  },
1086
1961
  /** Context menu appearance */
1087
1962
  contextMenu: {
1088
1963
  name: "ui.contextMenu",
1089
- steps: [{ type: "vibrate", duration: 20, intensity: 0.7 }]
1964
+ steps: [{ type: "vibrate", duration: 35, intensity: 0.7 }]
1090
1965
  },
1091
1966
  /** Drag start */
1092
1967
  dragStart: {
1093
1968
  name: "ui.dragStart",
1094
- steps: [{ type: "vibrate", duration: 12, intensity: 0.5 }]
1969
+ steps: [{ type: "vibrate", duration: 30, intensity: 0.5 }]
1095
1970
  },
1096
1971
  /** Drag drop */
1097
1972
  drop: {
1098
1973
  name: "ui.drop",
1099
1974
  steps: [
1100
- { type: "vibrate", duration: 20, intensity: 0.8 },
1975
+ { type: "vibrate", duration: 30, intensity: 0.8 },
1101
1976
  { type: "pause", duration: 30, intensity: 0 },
1102
- { type: "vibrate", duration: 10, intensity: 0.4 }
1977
+ { type: "vibrate", duration: 25, intensity: 0.5 }
1103
1978
  ]
1104
1979
  }
1105
1980
  };
@@ -1110,9 +1985,9 @@ var notifications = {
1110
1985
  success: {
1111
1986
  name: "notifications.success",
1112
1987
  steps: [
1113
- { type: "vibrate", duration: 30, intensity: 0.5 },
1988
+ { type: "vibrate", duration: 35, intensity: 0.5 },
1114
1989
  { type: "pause", duration: 60, intensity: 0 },
1115
- { type: "vibrate", duration: 40, intensity: 0.8 }
1990
+ { type: "vibrate", duration: 45, intensity: 0.8 }
1116
1991
  ]
1117
1992
  },
1118
1993
  /** Warning — three even pulses */
@@ -1139,16 +2014,16 @@ var notifications = {
1139
2014
  info: {
1140
2015
  name: "notifications.info",
1141
2016
  steps: [
1142
- { type: "vibrate", duration: 20, intensity: 0.4 }
2017
+ { type: "vibrate", duration: 35, intensity: 0.5 }
1143
2018
  ]
1144
2019
  },
1145
2020
  /** Message received */
1146
2021
  messageReceived: {
1147
2022
  name: "notifications.messageReceived",
1148
2023
  steps: [
1149
- { type: "vibrate", duration: 15, intensity: 0.5 },
2024
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1150
2025
  { type: "pause", duration: 100, intensity: 0 },
1151
- { type: "vibrate", duration: 15, intensity: 0.5 }
2026
+ { type: "vibrate", duration: 30, intensity: 0.5 }
1152
2027
  ]
1153
2028
  },
1154
2029
  /** Alarm — urgent repeating pattern */
@@ -1172,9 +2047,9 @@ var notifications = {
1172
2047
  reminder: {
1173
2048
  name: "notifications.reminder",
1174
2049
  steps: [
1175
- { type: "vibrate", duration: 25, intensity: 0.5 },
2050
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1176
2051
  { type: "pause", duration: 150, intensity: 0 },
1177
- { type: "vibrate", duration: 25, intensity: 0.5 }
2052
+ { type: "vibrate", duration: 30, intensity: 0.5 }
1178
2053
  ]
1179
2054
  }
1180
2055
  };
@@ -1188,26 +2063,25 @@ var gaming = {
1188
2063
  { type: "vibrate", duration: 100, intensity: 1 },
1189
2064
  { type: "vibrate", duration: 80, intensity: 0.8 },
1190
2065
  { type: "vibrate", duration: 60, intensity: 0.5 },
1191
- { type: "vibrate", duration: 40, intensity: 0.3 },
1192
- { type: "vibrate", duration: 30, intensity: 0.1 }
2066
+ { type: "vibrate", duration: 40, intensity: 0.3 }
1193
2067
  ]
1194
2068
  },
1195
2069
  /** Collision — sharp impact */
1196
2070
  collision: {
1197
2071
  name: "gaming.collision",
1198
2072
  steps: [
1199
- { type: "vibrate", duration: 30, intensity: 1 },
1200
- { type: "pause", duration: 20, intensity: 0 },
1201
- { type: "vibrate", duration: 15, intensity: 0.5 }
2073
+ { type: "vibrate", duration: 40, intensity: 1 },
2074
+ { type: "pause", duration: 30, intensity: 0 },
2075
+ { type: "vibrate", duration: 25, intensity: 0.5 }
1202
2076
  ]
1203
2077
  },
1204
2078
  /** Heartbeat — rhythmic pulse */
1205
2079
  heartbeat: {
1206
2080
  name: "gaming.heartbeat",
1207
2081
  steps: [
1208
- { type: "vibrate", duration: 20, intensity: 0.8 },
2082
+ { type: "vibrate", duration: 30, intensity: 0.8 },
1209
2083
  { type: "pause", duration: 80, intensity: 0 },
1210
- { type: "vibrate", duration: 30, intensity: 1 },
2084
+ { type: "vibrate", duration: 40, intensity: 1 },
1211
2085
  { type: "pause", duration: 400, intensity: 0 }
1212
2086
  ]
1213
2087
  },
@@ -1215,17 +2089,17 @@ var gaming = {
1215
2089
  gunshot: {
1216
2090
  name: "gaming.gunshot",
1217
2091
  steps: [
1218
- { type: "vibrate", duration: 15, intensity: 1 },
1219
- { type: "vibrate", duration: 30, intensity: 0.4 }
2092
+ { type: "vibrate", duration: 30, intensity: 1 },
2093
+ { type: "vibrate", duration: 40, intensity: 0.4 }
1220
2094
  ]
1221
2095
  },
1222
2096
  /** Sword clash — metallic ring */
1223
2097
  swordClash: {
1224
2098
  name: "gaming.swordClash",
1225
2099
  steps: [
1226
- { type: "vibrate", duration: 10, intensity: 1 },
1227
- { type: "pause", duration: 10, intensity: 0 },
1228
- { type: "vibrate", duration: 30, intensity: 0.6 },
2100
+ { type: "vibrate", duration: 25, intensity: 1 },
2101
+ { type: "pause", duration: 20, intensity: 0 },
2102
+ { type: "vibrate", duration: 40, intensity: 0.6 },
1229
2103
  { type: "vibrate", duration: 50, intensity: 0.3 }
1230
2104
  ]
1231
2105
  },
@@ -1233,10 +2107,10 @@ var gaming = {
1233
2107
  powerUp: {
1234
2108
  name: "gaming.powerUp",
1235
2109
  steps: [
1236
- { type: "vibrate", duration: 40, intensity: 0.2 },
1237
- { type: "vibrate", duration: 40, intensity: 0.4 },
1238
- { type: "vibrate", duration: 40, intensity: 0.6 },
1239
- { type: "vibrate", duration: 40, intensity: 0.8 },
2110
+ { type: "vibrate", duration: 40, intensity: 0.3 },
2111
+ { type: "vibrate", duration: 40, intensity: 0.5 },
2112
+ { type: "vibrate", duration: 40, intensity: 0.7 },
2113
+ { type: "vibrate", duration: 40, intensity: 0.9 },
1240
2114
  { type: "vibrate", duration: 60, intensity: 1 }
1241
2115
  ]
1242
2116
  },
@@ -1244,46 +2118,46 @@ var gaming = {
1244
2118
  damage: {
1245
2119
  name: "gaming.damage",
1246
2120
  steps: [
1247
- { type: "vibrate", duration: 40, intensity: 0.9 },
1248
- { type: "pause", duration: 20, intensity: 0 },
1249
- { type: "vibrate", duration: 30, intensity: 0.6 },
1250
- { type: "pause", duration: 20, intensity: 0 },
1251
- { type: "vibrate", duration: 20, intensity: 0.3 }
2121
+ { type: "vibrate", duration: 50, intensity: 0.9 },
2122
+ { type: "pause", duration: 25, intensity: 0 },
2123
+ { type: "vibrate", duration: 40, intensity: 0.6 },
2124
+ { type: "pause", duration: 25, intensity: 0 },
2125
+ { type: "vibrate", duration: 30, intensity: 0.4 }
1252
2126
  ]
1253
2127
  },
1254
2128
  /** Item pickup — light cheerful */
1255
2129
  pickup: {
1256
2130
  name: "gaming.pickup",
1257
2131
  steps: [
1258
- { type: "vibrate", duration: 10, intensity: 0.3 },
2132
+ { type: "vibrate", duration: 25, intensity: 0.4 },
1259
2133
  { type: "pause", duration: 40, intensity: 0 },
1260
- { type: "vibrate", duration: 15, intensity: 0.6 }
2134
+ { type: "vibrate", duration: 30, intensity: 0.7 }
1261
2135
  ]
1262
2136
  },
1263
2137
  /** Level complete — celebratory */
1264
2138
  levelComplete: {
1265
2139
  name: "gaming.levelComplete",
1266
2140
  steps: [
1267
- { type: "vibrate", duration: 20, intensity: 0.5 },
2141
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1268
2142
  { type: "pause", duration: 60, intensity: 0 },
1269
- { type: "vibrate", duration: 20, intensity: 0.5 },
2143
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1270
2144
  { type: "pause", duration: 60, intensity: 0 },
1271
- { type: "vibrate", duration: 30, intensity: 0.7 },
2145
+ { type: "vibrate", duration: 40, intensity: 0.7 },
1272
2146
  { type: "pause", duration: 60, intensity: 0 },
1273
- { type: "vibrate", duration: 50, intensity: 1 }
2147
+ { type: "vibrate", duration: 60, intensity: 1 }
1274
2148
  ]
1275
2149
  },
1276
2150
  /** Engine rumble — continuous vibration */
1277
2151
  engineRumble: {
1278
2152
  name: "gaming.engineRumble",
1279
2153
  steps: [
1280
- { type: "vibrate", duration: 30, intensity: 0.4 },
1281
- { type: "pause", duration: 10, intensity: 0 },
1282
- { type: "vibrate", duration: 30, intensity: 0.5 },
1283
- { type: "pause", duration: 10, intensity: 0 },
1284
- { type: "vibrate", duration: 30, intensity: 0.4 },
1285
- { type: "pause", duration: 10, intensity: 0 },
1286
- { type: "vibrate", duration: 30, intensity: 0.5 }
2154
+ { type: "vibrate", duration: 40, intensity: 0.5 },
2155
+ { type: "pause", duration: 15, intensity: 0 },
2156
+ { type: "vibrate", duration: 40, intensity: 0.6 },
2157
+ { type: "pause", duration: 15, intensity: 0 },
2158
+ { type: "vibrate", duration: 40, intensity: 0.5 },
2159
+ { type: "pause", duration: 15, intensity: 0 },
2160
+ { type: "vibrate", duration: 40, intensity: 0.6 }
1287
2161
  ]
1288
2162
  }
1289
2163
  };
@@ -1294,9 +2168,9 @@ var accessibility = {
1294
2168
  confirm: {
1295
2169
  name: "accessibility.confirm",
1296
2170
  steps: [
1297
- { type: "vibrate", duration: 30, intensity: 0.7 },
2171
+ { type: "vibrate", duration: 35, intensity: 0.7 },
1298
2172
  { type: "pause", duration: 100, intensity: 0 },
1299
- { type: "vibrate", duration: 30, intensity: 0.7 }
2173
+ { type: "vibrate", duration: 35, intensity: 0.7 }
1300
2174
  ]
1301
2175
  },
1302
2176
  /** Deny/reject — long single buzz */
@@ -1310,41 +2184,41 @@ var accessibility = {
1310
2184
  boundary: {
1311
2185
  name: "accessibility.boundary",
1312
2186
  steps: [
1313
- { type: "vibrate", duration: 15, intensity: 1 }
2187
+ { type: "vibrate", duration: 30, intensity: 1 }
1314
2188
  ]
1315
2189
  },
1316
2190
  /** Focus change — subtle tick */
1317
2191
  focusChange: {
1318
2192
  name: "accessibility.focusChange",
1319
2193
  steps: [
1320
- { type: "vibrate", duration: 5, intensity: 0.3 }
2194
+ { type: "vibrate", duration: 25, intensity: 0.5 }
1321
2195
  ]
1322
2196
  },
1323
2197
  /** Counting rhythm — one tick per count */
1324
2198
  countTick: {
1325
2199
  name: "accessibility.countTick",
1326
2200
  steps: [
1327
- { type: "vibrate", duration: 8, intensity: 0.5 }
2201
+ { type: "vibrate", duration: 25, intensity: 0.5 }
1328
2202
  ]
1329
2203
  },
1330
2204
  /** Navigation landmark reached */
1331
2205
  landmark: {
1332
2206
  name: "accessibility.landmark",
1333
2207
  steps: [
1334
- { type: "vibrate", duration: 15, intensity: 0.6 },
2208
+ { type: "vibrate", duration: 25, intensity: 0.6 },
1335
2209
  { type: "pause", duration: 40, intensity: 0 },
1336
- { type: "vibrate", duration: 15, intensity: 0.6 },
2210
+ { type: "vibrate", duration: 25, intensity: 0.6 },
1337
2211
  { type: "pause", duration: 40, intensity: 0 },
1338
- { type: "vibrate", duration: 15, intensity: 0.6 }
2212
+ { type: "vibrate", duration: 25, intensity: 0.6 }
1339
2213
  ]
1340
2214
  },
1341
2215
  /** Progress checkpoint — escalating feedback */
1342
2216
  progressCheckpoint: {
1343
2217
  name: "accessibility.progressCheckpoint",
1344
2218
  steps: [
1345
- { type: "vibrate", duration: 20, intensity: 0.4 },
2219
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1346
2220
  { type: "pause", duration: 60, intensity: 0 },
1347
- { type: "vibrate", duration: 25, intensity: 0.7 }
2221
+ { type: "vibrate", duration: 35, intensity: 0.7 }
1348
2222
  ]
1349
2223
  }
1350
2224
  };
@@ -1354,51 +2228,229 @@ var system = {
1354
2228
  /** Keyboard key press */
1355
2229
  keyPress: {
1356
2230
  name: "system.keyPress",
1357
- steps: [{ type: "vibrate", duration: 5, intensity: 0.3 }]
2231
+ steps: [{ type: "vibrate", duration: 25, intensity: 0.5 }]
1358
2232
  },
1359
2233
  /** Scroll tick (detent-like) */
1360
2234
  scrollTick: {
1361
2235
  name: "system.scrollTick",
1362
- steps: [{ type: "vibrate", duration: 3, intensity: 0.2 }]
2236
+ steps: [{ type: "vibrate", duration: 20, intensity: 0.4 }]
1363
2237
  },
1364
2238
  /** Scroll boundary reached */
1365
2239
  scrollBounce: {
1366
2240
  name: "system.scrollBounce",
1367
2241
  steps: [
1368
- { type: "vibrate", duration: 10, intensity: 0.5 },
1369
- { type: "vibrate", duration: 20, intensity: 0.3 }
2242
+ { type: "vibrate", duration: 25, intensity: 0.6 },
2243
+ { type: "vibrate", duration: 30, intensity: 0.4 }
1370
2244
  ]
1371
2245
  },
1372
2246
  /** Delete action */
1373
2247
  delete: {
1374
2248
  name: "system.delete",
1375
2249
  steps: [
1376
- { type: "vibrate", duration: 15, intensity: 0.5 },
2250
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1377
2251
  { type: "pause", duration: 50, intensity: 0 },
1378
- { type: "vibrate", duration: 25, intensity: 0.8 }
2252
+ { type: "vibrate", duration: 40, intensity: 0.8 }
1379
2253
  ]
1380
2254
  },
1381
2255
  /** Undo action */
1382
2256
  undo: {
1383
2257
  name: "system.undo",
1384
2258
  steps: [
1385
- { type: "vibrate", duration: 20, intensity: 0.5 },
2259
+ { type: "vibrate", duration: 30, intensity: 0.5 },
1386
2260
  { type: "pause", duration: 80, intensity: 0 },
1387
- { type: "vibrate", duration: 10, intensity: 0.3 }
2261
+ { type: "vibrate", duration: 25, intensity: 0.4 }
1388
2262
  ]
1389
2263
  },
1390
2264
  /** Copy to clipboard */
1391
2265
  copy: {
1392
2266
  name: "system.copy",
1393
- steps: [{ type: "vibrate", duration: 12, intensity: 0.4 }]
2267
+ steps: [{ type: "vibrate", duration: 30, intensity: 0.5 }]
1394
2268
  },
1395
2269
  /** Paste from clipboard */
1396
2270
  paste: {
1397
2271
  name: "system.paste",
1398
2272
  steps: [
1399
- { type: "vibrate", duration: 8, intensity: 0.3 },
2273
+ { type: "vibrate", duration: 25, intensity: 0.4 },
1400
2274
  { type: "pause", duration: 30, intensity: 0 },
1401
- { type: "vibrate", duration: 12, intensity: 0.5 }
2275
+ { type: "vibrate", duration: 30, intensity: 0.6 }
2276
+ ]
2277
+ }
2278
+ };
2279
+
2280
+ // src/presets/emotions.ts
2281
+ var emotions = {
2282
+ /** Excited — fast, energetic pulses building up */
2283
+ excited: {
2284
+ name: "emotions.excited",
2285
+ steps: [
2286
+ { type: "vibrate", duration: 30, intensity: 0.5 },
2287
+ { type: "pause", duration: 30, intensity: 0 },
2288
+ { type: "vibrate", duration: 30, intensity: 0.6 },
2289
+ { type: "pause", duration: 25, intensity: 0 },
2290
+ { type: "vibrate", duration: 35, intensity: 0.7 },
2291
+ { type: "pause", duration: 25, intensity: 0 },
2292
+ { type: "vibrate", duration: 35, intensity: 0.8 },
2293
+ { type: "pause", duration: 25, intensity: 0 },
2294
+ { type: "vibrate", duration: 40, intensity: 0.9 },
2295
+ { type: "vibrate", duration: 50, intensity: 1 }
2296
+ ]
2297
+ },
2298
+ /** Calm — slow, gentle wave with soft sustained vibrations */
2299
+ calm: {
2300
+ name: "emotions.calm",
2301
+ steps: [
2302
+ { type: "vibrate", duration: 80, intensity: 0.2 },
2303
+ { type: "pause", duration: 200, intensity: 0 },
2304
+ { type: "vibrate", duration: 100, intensity: 0.25 },
2305
+ { type: "pause", duration: 250, intensity: 0 },
2306
+ { type: "vibrate", duration: 80, intensity: 0.2 },
2307
+ { type: "pause", duration: 200, intensity: 0 },
2308
+ { type: "vibrate", duration: 100, intensity: 0.15 }
2309
+ ]
2310
+ },
2311
+ /** Tense — tight, irregular short heavy bursts */
2312
+ tense: {
2313
+ name: "emotions.tense",
2314
+ steps: [
2315
+ { type: "vibrate", duration: 35, intensity: 0.8 },
2316
+ { type: "pause", duration: 40, intensity: 0 },
2317
+ { type: "vibrate", duration: 30, intensity: 0.9 },
2318
+ { type: "pause", duration: 30, intensity: 0 },
2319
+ { type: "vibrate", duration: 40, intensity: 0.85 },
2320
+ { type: "pause", duration: 35, intensity: 0 },
2321
+ { type: "vibrate", duration: 30, intensity: 0.9 },
2322
+ { type: "pause", duration: 45, intensity: 0 },
2323
+ { type: "vibrate", duration: 35, intensity: 0.8 }
2324
+ ]
2325
+ },
2326
+ /** Happy — bouncy, playful ascending rhythm */
2327
+ happy: {
2328
+ name: "emotions.happy",
2329
+ steps: [
2330
+ { type: "vibrate", duration: 30, intensity: 0.4 },
2331
+ { type: "pause", duration: 50, intensity: 0 },
2332
+ { type: "vibrate", duration: 35, intensity: 0.5 },
2333
+ { type: "pause", duration: 50, intensity: 0 },
2334
+ { type: "vibrate", duration: 35, intensity: 0.6 },
2335
+ { type: "pause", duration: 40, intensity: 0 },
2336
+ { type: "vibrate", duration: 40, intensity: 0.7 },
2337
+ { type: "pause", duration: 40, intensity: 0 },
2338
+ { type: "vibrate", duration: 45, intensity: 0.8 }
2339
+ ]
2340
+ },
2341
+ /** Sad — slow, heavy, descending vibrations that fade */
2342
+ sad: {
2343
+ name: "emotions.sad",
2344
+ steps: [
2345
+ { type: "vibrate", duration: 100, intensity: 0.8 },
2346
+ { type: "pause", duration: 120, intensity: 0 },
2347
+ { type: "vibrate", duration: 90, intensity: 0.6 },
2348
+ { type: "pause", duration: 140, intensity: 0 },
2349
+ { type: "vibrate", duration: 80, intensity: 0.4 },
2350
+ { type: "pause", duration: 160, intensity: 0 },
2351
+ { type: "vibrate", duration: 70, intensity: 0.25 }
2352
+ ]
2353
+ },
2354
+ /** Angry — aggressive, chaotic heavy rapid hits */
2355
+ angry: {
2356
+ name: "emotions.angry",
2357
+ steps: [
2358
+ { type: "vibrate", duration: 40, intensity: 1 },
2359
+ { type: "pause", duration: 25, intensity: 0 },
2360
+ { type: "vibrate", duration: 35, intensity: 0.9 },
2361
+ { type: "pause", duration: 25, intensity: 0 },
2362
+ { type: "vibrate", duration: 45, intensity: 1 },
2363
+ { type: "pause", duration: 30, intensity: 0 },
2364
+ { type: "vibrate", duration: 40, intensity: 0.95 },
2365
+ { type: "vibrate", duration: 50, intensity: 1 },
2366
+ { type: "pause", duration: 25, intensity: 0 },
2367
+ { type: "vibrate", duration: 45, intensity: 0.9 }
2368
+ ]
2369
+ },
2370
+ /** Surprised — sharp sudden hit, silence, then lighter hit */
2371
+ surprised: {
2372
+ name: "emotions.surprised",
2373
+ steps: [
2374
+ { type: "vibrate", duration: 40, intensity: 1 },
2375
+ { type: "pause", duration: 200, intensity: 0 },
2376
+ { type: "vibrate", duration: 30, intensity: 0.4 }
2377
+ ]
2378
+ },
2379
+ /** Anxious — fast irregular heartbeat with inconsistent spacing */
2380
+ anxious: {
2381
+ name: "emotions.anxious",
2382
+ steps: [
2383
+ { type: "vibrate", duration: 30, intensity: 0.7 },
2384
+ { type: "pause", duration: 60, intensity: 0 },
2385
+ { type: "vibrate", duration: 35, intensity: 0.8 },
2386
+ { type: "pause", duration: 40, intensity: 0 },
2387
+ { type: "vibrate", duration: 25, intensity: 0.6 },
2388
+ { type: "pause", duration: 80, intensity: 0 },
2389
+ { type: "vibrate", duration: 30, intensity: 0.75 },
2390
+ { type: "pause", duration: 50, intensity: 0 },
2391
+ { type: "vibrate", duration: 35, intensity: 0.85 },
2392
+ { type: "pause", duration: 35, intensity: 0 },
2393
+ { type: "vibrate", duration: 30, intensity: 0.7 }
2394
+ ]
2395
+ },
2396
+ /** Confident — strong, steady, measured even pulses */
2397
+ confident: {
2398
+ name: "emotions.confident",
2399
+ steps: [
2400
+ { type: "vibrate", duration: 50, intensity: 0.8 },
2401
+ { type: "pause", duration: 80, intensity: 0 },
2402
+ { type: "vibrate", duration: 50, intensity: 0.8 },
2403
+ { type: "pause", duration: 80, intensity: 0 },
2404
+ { type: "vibrate", duration: 50, intensity: 0.8 },
2405
+ { type: "pause", duration: 80, intensity: 0 },
2406
+ { type: "vibrate", duration: 50, intensity: 0.8 }
2407
+ ]
2408
+ },
2409
+ /** Playful — alternating light-heavy in bouncy rhythm */
2410
+ playful: {
2411
+ name: "emotions.playful",
2412
+ steps: [
2413
+ { type: "vibrate", duration: 25, intensity: 0.3 },
2414
+ { type: "pause", duration: 40, intensity: 0 },
2415
+ { type: "vibrate", duration: 40, intensity: 0.7 },
2416
+ { type: "pause", duration: 50, intensity: 0 },
2417
+ { type: "vibrate", duration: 25, intensity: 0.35 },
2418
+ { type: "pause", duration: 40, intensity: 0 },
2419
+ { type: "vibrate", duration: 40, intensity: 0.75 },
2420
+ { type: "pause", duration: 50, intensity: 0 },
2421
+ { type: "vibrate", duration: 25, intensity: 0.3 },
2422
+ { type: "pause", duration: 40, intensity: 0 },
2423
+ { type: "vibrate", duration: 45, intensity: 0.8 }
2424
+ ]
2425
+ },
2426
+ /** Romantic — gentle heartbeat rhythm, two soft pulses, long pause, repeat */
2427
+ romantic: {
2428
+ name: "emotions.romantic",
2429
+ steps: [
2430
+ { type: "vibrate", duration: 35, intensity: 0.4 },
2431
+ { type: "pause", duration: 60, intensity: 0 },
2432
+ { type: "vibrate", duration: 45, intensity: 0.5 },
2433
+ { type: "pause", duration: 300, intensity: 0 },
2434
+ { type: "vibrate", duration: 35, intensity: 0.4 },
2435
+ { type: "pause", duration: 60, intensity: 0 },
2436
+ { type: "vibrate", duration: 45, intensity: 0.5 },
2437
+ { type: "pause", duration: 300, intensity: 0 },
2438
+ { type: "vibrate", duration: 35, intensity: 0.4 },
2439
+ { type: "pause", duration: 60, intensity: 0 },
2440
+ { type: "vibrate", duration: 45, intensity: 0.5 }
2441
+ ]
2442
+ },
2443
+ /** Peaceful — very subtle, barely-there ultra-light slow pulses */
2444
+ peaceful: {
2445
+ name: "emotions.peaceful",
2446
+ steps: [
2447
+ { type: "vibrate", duration: 60, intensity: 0.1 },
2448
+ { type: "pause", duration: 300, intensity: 0 },
2449
+ { type: "vibrate", duration: 70, intensity: 0.12 },
2450
+ { type: "pause", duration: 350, intensity: 0 },
2451
+ { type: "vibrate", duration: 60, intensity: 0.1 },
2452
+ { type: "pause", duration: 300, intensity: 0 },
2453
+ { type: "vibrate", duration: 70, intensity: 0.08 }
1402
2454
  ]
1403
2455
  }
1404
2456
  };
@@ -1409,12 +2461,168 @@ var presets = {
1409
2461
  notifications,
1410
2462
  gaming,
1411
2463
  accessibility,
1412
- system
2464
+ system,
2465
+ emotions
2466
+ };
2467
+
2468
+ // src/physics/physics-patterns.ts
2469
+ var clamp2 = (v, min, max) => Math.min(max, Math.max(min, v));
2470
+ var step = (type, duration, intensity) => ({
2471
+ type,
2472
+ duration: Math.max(25, Math.round(duration)),
2473
+ intensity: clamp2(Math.round(intensity * 100) / 100, 0, 1)
2474
+ });
2475
+ function spring(options) {
2476
+ const stiffness = clamp2(options?.stiffness ?? 0.7, 0.5, 1);
2477
+ const damping = clamp2(options?.damping ?? 0.3, 0.1, 0.9);
2478
+ const duration = options?.duration ?? 500;
2479
+ const numSteps = Math.round(10 + stiffness * 5);
2480
+ const stepDuration = duration / numSteps;
2481
+ const steps = [];
2482
+ for (let i = 0; i < numSteps; i++) {
2483
+ const t = i / (numSteps - 1);
2484
+ const decay = Math.exp(-damping * t * 5);
2485
+ const oscillation = Math.abs(Math.cos(t * Math.PI * stiffness * 4));
2486
+ const intensity = decay * oscillation * stiffness;
2487
+ if (intensity > 0.05) {
2488
+ steps.push(step("vibrate", stepDuration, intensity));
2489
+ } else {
2490
+ steps.push(step("pause", stepDuration, 0));
2491
+ }
2492
+ }
2493
+ return { name: "physics.spring", steps };
2494
+ }
2495
+ function bounce(options) {
2496
+ const height = clamp2(options?.height ?? 1, 0.5, 1);
2497
+ const bounciness = clamp2(options?.bounciness ?? 0.6, 0.3, 0.9);
2498
+ const bounces = options?.bounces ?? 5;
2499
+ const steps = [];
2500
+ let currentHeight = height;
2501
+ for (let i = 0; i < bounces; i++) {
2502
+ const vibDuration = 40 + currentHeight * 60;
2503
+ const pauseDuration = 30 + currentHeight * 70;
2504
+ steps.push(step("vibrate", vibDuration, currentHeight));
2505
+ if (i < bounces - 1) {
2506
+ steps.push(step("pause", pauseDuration, 0));
2507
+ }
2508
+ currentHeight *= bounciness;
2509
+ }
2510
+ return { name: "physics.bounce", steps };
2511
+ }
2512
+ function friction(options) {
2513
+ const roughness = clamp2(options?.roughness ?? 0.5, 0.1, 1);
2514
+ const speed = clamp2(options?.speed ?? 0.5, 0.1, 1);
2515
+ const duration = options?.duration ?? 300;
2516
+ const pulseCount = Math.round(4 + speed * 8);
2517
+ const pulseDuration = duration / pulseCount;
2518
+ const steps = [];
2519
+ for (let i = 0; i < pulseCount; i++) {
2520
+ const variation = (Math.sin(i * 7.3) + 1) / 2 * roughness * 0.4;
2521
+ const baseIntensity = 0.3 + roughness * 0.4;
2522
+ const intensity = baseIntensity + variation;
2523
+ steps.push(step("vibrate", pulseDuration * 0.7, intensity));
2524
+ steps.push(step("pause", pulseDuration * 0.3, 0));
2525
+ }
2526
+ return { name: "physics.friction", steps };
2527
+ }
2528
+ function impact(options) {
2529
+ const mass = clamp2(options?.mass ?? 0.5, 0.1, 1);
2530
+ const hardness = clamp2(options?.hardness ?? 0.7, 0.1, 1);
2531
+ const steps = [];
2532
+ const hitDuration = 30 + mass * 50;
2533
+ const hitIntensity = 0.5 + mass * 0.5;
2534
+ steps.push(step("vibrate", hitDuration, hitIntensity));
2535
+ const resonanceCount = Math.round(2 + hardness * 4);
2536
+ let decayIntensity = hitIntensity * 0.6;
2537
+ for (let i = 0; i < resonanceCount; i++) {
2538
+ const pauseDur = 25 + (1 - hardness) * 30;
2539
+ steps.push(step("pause", pauseDur, 0));
2540
+ steps.push(step("vibrate", 25 + mass * 20, decayIntensity));
2541
+ decayIntensity *= 0.5;
2542
+ }
2543
+ return { name: "physics.impact", steps };
2544
+ }
2545
+ function gravity(options) {
2546
+ const distance = clamp2(options?.distance ?? 1, 0.3, 1);
2547
+ const duration = options?.duration ?? 400;
2548
+ const numSteps = Math.round(6 + distance * 4);
2549
+ const stepDuration = duration / numSteps;
2550
+ const steps = [];
2551
+ for (let i = 0; i < numSteps; i++) {
2552
+ const t = i / (numSteps - 1);
2553
+ const intensity = t * t * distance;
2554
+ steps.push(step("vibrate", stepDuration, Math.max(0.1, intensity)));
2555
+ }
2556
+ return { name: "physics.gravity", steps };
2557
+ }
2558
+ function elastic(options) {
2559
+ const stretch = clamp2(options?.stretch ?? 0.7, 0.3, 1);
2560
+ const snapSpeed = clamp2(options?.snapSpeed ?? 0.8, 0.3, 1);
2561
+ const steps = [];
2562
+ const tensionSteps = Math.round(3 + stretch * 3);
2563
+ for (let i = 0; i < tensionSteps; i++) {
2564
+ const t = (i + 1) / tensionSteps;
2565
+ const intensity = t * stretch * 0.6;
2566
+ steps.push(step("vibrate", 40 + (1 - snapSpeed) * 30, intensity));
2567
+ }
2568
+ steps.push(step("vibrate", 25 + (1 - snapSpeed) * 20, 0.8 + stretch * 0.2));
2569
+ const recoilIntensity = 0.4 * snapSpeed;
2570
+ steps.push(step("vibrate", 30, recoilIntensity));
2571
+ steps.push(step("vibrate", 25, recoilIntensity * 0.4));
2572
+ return { name: "physics.elastic", steps };
2573
+ }
2574
+ function wave(options) {
2575
+ const amplitude = clamp2(options?.amplitude ?? 0.7, 0.3, 1);
2576
+ const frequency = clamp2(options?.frequency ?? 1, 0.5, 2);
2577
+ const cycles = options?.cycles ?? 2;
2578
+ const stepsPerCycle = Math.round(8 / frequency);
2579
+ const totalSteps = stepsPerCycle * cycles;
2580
+ const stepDuration = 400 / frequency / stepsPerCycle;
2581
+ const steps = [];
2582
+ for (let i = 0; i < totalSteps; i++) {
2583
+ const t = i / stepsPerCycle;
2584
+ const sineValue = (Math.sin(t * 2 * Math.PI) + 1) / 2;
2585
+ const intensity = 0.1 + sineValue * amplitude * 0.9;
2586
+ steps.push(step("vibrate", stepDuration, intensity));
2587
+ }
2588
+ return { name: "physics.wave", steps };
2589
+ }
2590
+ function pendulum(options) {
2591
+ const energy = clamp2(options?.energy ?? 0.8, 0.3, 1);
2592
+ const swings = options?.swings ?? 3;
2593
+ const stepsPerSwing = 6;
2594
+ const steps = [];
2595
+ for (let s = 0; s < swings; s++) {
2596
+ const swingEnergy = energy * Math.pow(0.8, s);
2597
+ for (let i = 0; i < stepsPerSwing; i++) {
2598
+ const t = i / (stepsPerSwing - 1);
2599
+ const swing = Math.abs(Math.cos(t * Math.PI));
2600
+ const intensity = swing * swingEnergy;
2601
+ if (intensity > 0.05) {
2602
+ steps.push(step("vibrate", 35, intensity));
2603
+ } else {
2604
+ steps.push(step("pause", 35, 0));
2605
+ }
2606
+ }
2607
+ }
2608
+ return { name: "physics.pendulum", steps };
2609
+ }
2610
+
2611
+ // src/physics/index.ts
2612
+ var physics = {
2613
+ spring,
2614
+ bounce,
2615
+ friction,
2616
+ impact,
2617
+ gravity,
2618
+ elastic,
2619
+ wave,
2620
+ pendulum
1413
2621
  };
1414
2622
 
1415
2623
  // src/index.ts
1416
2624
  var haptic = HapticEngine.create();
1417
2625
 
1418
- export { AdaptiveEngine, FallbackManager, HPLParser, HPLParserError, HPLTokenizerError, HapticEngine, IoSAudioAdapter, NoopAdapter, PatternComposer, WebVibrationAdapter, accessibility, compile, detectAdapter, detectPlatform, exportPattern, gaming, haptic, importPattern, notifications, optimizeSteps, parseHPL, patternFromDataURL, patternFromJSON, patternToDataURL, patternToJSON, presets, system, tokenize, ui, validateHPL };
2626
+ export { AdaptiveEngine, FallbackManager, HPLParser, HPLParserError, HPLTokenizerError, HapticEngine, IoSAudioAdapter, NoopAdapter, PatternComposer, PatternRecorder, SensoryEngine, SoundEngine, ThemeManager, VisualEngine, WebVibrationAdapter, accessibility, bounce, compile, detectAdapter, detectPlatform, elastic, emotions, exportPattern, friction, gaming, gravity, haptic, impact, importPattern, notifications, optimizeSteps, parseHPL, patternFromDataURL, patternFromJSON, patternToDataURL, patternToJSON, pendulum, physics, presets, spring, system, themes, tokenize, ui, validateHPL, wave };
1419
2627
  //# sourceMappingURL=index.js.map
1420
2628
  //# sourceMappingURL=index.js.map