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