@hapticjs/core 0.2.1 → 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
@@ -56,9 +56,9 @@ var WebVibrationAdapter = class {
56
56
  if (!this.supported || steps.length === 0) return;
57
57
  const pattern = [];
58
58
  let lastType = null;
59
- for (const step of steps) {
60
- if (step.type === "vibrate" && step.intensity > 0.05) {
61
- const dur = Math.max(step.duration, 20);
59
+ for (const step2 of steps) {
60
+ if (step2.type === "vibrate" && step2.intensity > 0.05) {
61
+ const dur = Math.max(step2.duration, 20);
62
62
  if (lastType === "vibrate") {
63
63
  pattern[pattern.length - 1] += dur;
64
64
  } else {
@@ -66,7 +66,7 @@ var WebVibrationAdapter = class {
66
66
  }
67
67
  lastType = "vibrate";
68
68
  } else {
69
- const dur = Math.max(step.duration, 10);
69
+ const dur = Math.max(step2.duration, 10);
70
70
  if (lastType === "pause") {
71
71
  pattern[pattern.length - 1] += dur;
72
72
  } else {
@@ -130,12 +130,12 @@ var IoSAudioAdapter = class {
130
130
  async playSequence(steps) {
131
131
  if (!this.supported || steps.length === 0) return;
132
132
  this._cancelled = false;
133
- for (const step of steps) {
133
+ for (const step2 of steps) {
134
134
  if (this._cancelled) break;
135
- if (step.type === "vibrate" && step.intensity > 0) {
136
- await this._playTone(step.intensity, step.duration);
135
+ if (step2.type === "vibrate" && step2.intensity > 0) {
136
+ await this._playTone(step2.intensity, step2.duration);
137
137
  } else {
138
- await delay(step.duration);
138
+ await delay(step2.duration);
139
139
  }
140
140
  }
141
141
  }
@@ -262,10 +262,10 @@ function detectAdapter() {
262
262
  // src/engine/adaptive-engine.ts
263
263
  var AdaptiveEngine = class {
264
264
  adapt(steps, capabilities) {
265
- return steps.map((step) => this._adaptStep(step, capabilities));
265
+ return steps.map((step2) => this._adaptStep(step2, capabilities));
266
266
  }
267
- _adaptStep(step, caps) {
268
- const adapted = { ...step };
267
+ _adaptStep(step2, caps) {
268
+ const adapted = { ...step2 };
269
269
  if (adapted.type === "vibrate") {
270
270
  adapted.duration = clamp(adapted.duration, caps.minDuration, caps.maxDuration);
271
271
  }
@@ -657,12 +657,12 @@ function compileNode(node) {
657
657
  }
658
658
  function mergeSustains(steps) {
659
659
  const result = [];
660
- for (const step of steps) {
661
- if (step.intensity === -1 && result.length > 0) {
660
+ for (const step2 of steps) {
661
+ if (step2.intensity === -1 && result.length > 0) {
662
662
  const prev = result[result.length - 1];
663
- prev.duration += step.duration;
663
+ prev.duration += step2.duration;
664
664
  } else {
665
- result.push({ ...step });
665
+ result.push({ ...step2 });
666
666
  }
667
667
  }
668
668
  return result;
@@ -843,6 +843,777 @@ var HapticEngine = class _HapticEngine {
843
843
  }
844
844
  };
845
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
+
846
1617
  // src/patterns/validator.ts
847
1618
  var VALID_CHARS = new Set("~#@.|\\-[] \nx0123456789");
848
1619
  function validateHPL(input) {
@@ -942,18 +1713,18 @@ function validateExport(data) {
942
1713
  throw new Error('Invalid pattern data: "steps" must be an array');
943
1714
  }
944
1715
  for (let i = 0; i < obj.steps.length; i++) {
945
- const step = obj.steps[i];
946
- if (step.type !== "vibrate" && step.type !== "pause") {
1716
+ const step2 = obj.steps[i];
1717
+ if (step2.type !== "vibrate" && step2.type !== "pause") {
947
1718
  throw new Error(
948
1719
  `Invalid step at index ${i}: "type" must be "vibrate" or "pause"`
949
1720
  );
950
1721
  }
951
- if (typeof step.duration !== "number" || step.duration < 0) {
1722
+ if (typeof step2.duration !== "number" || step2.duration < 0) {
952
1723
  throw new Error(
953
1724
  `Invalid step at index ${i}: "duration" must be a non-negative number`
954
1725
  );
955
1726
  }
956
- if (typeof step.intensity !== "number" || step.intensity < 0 || step.intensity > 1) {
1727
+ if (typeof step2.intensity !== "number" || step2.intensity < 0 || step2.intensity > 1) {
957
1728
  throw new Error(
958
1729
  `Invalid step at index ${i}: "intensity" must be a number between 0 and 1`
959
1730
  );
@@ -996,6 +1767,138 @@ function patternFromDataURL(url) {
996
1767
  return patternFromJSON(json);
997
1768
  }
998
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
+
999
1902
  // src/presets/ui.ts
1000
1903
  var ui = {
1001
1904
  /** Light button tap */
@@ -1374,18 +2277,352 @@ var system = {
1374
2277
  }
1375
2278
  };
1376
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 }
2454
+ ]
2455
+ }
2456
+ };
2457
+
1377
2458
  // src/presets/index.ts
1378
2459
  var presets = {
1379
2460
  ui,
1380
2461
  notifications,
1381
2462
  gaming,
1382
2463
  accessibility,
1383
- 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
1384
2621
  };
1385
2622
 
1386
2623
  // src/index.ts
1387
2624
  var haptic = HapticEngine.create();
1388
2625
 
1389
- 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 };
1390
2627
  //# sourceMappingURL=index.js.map
1391
2628
  //# sourceMappingURL=index.js.map