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