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