@supermousejs/core 2.0.5 → 2.2.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/CHANGELOG.md +20 -0
- package/LICENSE.md +21 -21
- package/README.md +30 -31
- package/dist/index.d.ts +16 -56
- package/dist/index.mjs +181 -234
- package/dist/index.umd.js +2 -2
- package/package.json +1 -1
- package/src/Supermouse.ts +708 -405
- package/src/index.ts +2 -2
- package/src/types.ts +168 -168
- package/tsconfig.json +9 -16
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +32 -32
- package/src/systems/Input.ts +0 -262
- package/src/systems/Stage.ts +0 -155
- package/src/systems/index.ts +0 -2
- package/src/utils/math.ts +0 -20
package/src/Supermouse.ts
CHANGED
|
@@ -1,405 +1,708 @@
|
|
|
1
|
-
declare const __VERSION__: string;
|
|
2
|
-
|
|
3
|
-
import { MouseState, SupermouseOptions, SupermousePlugin } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
*/
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
public
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1
|
+
declare const __VERSION__: string;
|
|
2
|
+
|
|
3
|
+
import type { MouseState, SupermouseOptions, SupermousePlugin } from "./types";
|
|
4
|
+
|
|
5
|
+
function lerp(start: number, end: number, factor: number): number {
|
|
6
|
+
return start + (end - start) * factor;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function damp(a: number, b: number, lambda: number, dt: number): number {
|
|
10
|
+
return lerp(a, b, 1 - Math.exp(-lambda * dt));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function angle(x: number, y: number): number {
|
|
14
|
+
return Math.atan2(y, x) * (180 / Math.PI);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Input.ts
|
|
19
|
+
*
|
|
20
|
+
* This class listens to browser events and mutates the shared `MouseState` object.
|
|
21
|
+
*
|
|
22
|
+
* @internal This is an internal system class instantiated by `Supermouse`.
|
|
23
|
+
*/
|
|
24
|
+
export class Input {
|
|
25
|
+
private mediaQueryList?: MediaQueryList;
|
|
26
|
+
private mediaQueryHandler?: (e: MediaQueryListEvent) => void;
|
|
27
|
+
private motionQuery?: MediaQueryList;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Master switch for input processing.
|
|
31
|
+
* Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
|
|
32
|
+
*/
|
|
33
|
+
public isEnabled: boolean = true;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private state: MouseState,
|
|
37
|
+
private options: SupermouseOptions,
|
|
38
|
+
private getHoverSelector: () => string,
|
|
39
|
+
private onEnableChange: (enabled: boolean) => void
|
|
40
|
+
) {
|
|
41
|
+
this.checkDeviceCapability();
|
|
42
|
+
this.checkMotionPreference();
|
|
43
|
+
this.bindEvents();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private abortController = new AbortController();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Automatically disables the custom cursor on devices without fine pointer control.
|
|
50
|
+
*/
|
|
51
|
+
private checkDeviceCapability(): void {
|
|
52
|
+
if (!this.options.autoDisableOnMobile) return;
|
|
53
|
+
|
|
54
|
+
this.mediaQueryList = window.matchMedia("(pointer: fine)");
|
|
55
|
+
this.updateEnabledState(this.mediaQueryList.matches);
|
|
56
|
+
|
|
57
|
+
this.mediaQueryHandler = (e: MediaQueryListEvent) => {
|
|
58
|
+
this.updateEnabledState(e.matches);
|
|
59
|
+
};
|
|
60
|
+
this.mediaQueryList.addEventListener("change", this.mediaQueryHandler, {
|
|
61
|
+
signal: this.abortController.signal
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks for `prefers-reduced-motion`.
|
|
67
|
+
* If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
|
|
68
|
+
*/
|
|
69
|
+
private checkMotionPreference(): void {
|
|
70
|
+
this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
71
|
+
this.state.reducedMotion = this.motionQuery.matches;
|
|
72
|
+
|
|
73
|
+
this.motionQuery.addEventListener(
|
|
74
|
+
"change",
|
|
75
|
+
(e) => {
|
|
76
|
+
this.state.reducedMotion = e.matches;
|
|
77
|
+
},
|
|
78
|
+
{ signal: this.abortController.signal }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private updateEnabledState(enabled: boolean): void {
|
|
83
|
+
this.isEnabled = enabled;
|
|
84
|
+
this.onEnableChange(enabled);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private parseDOMInteraction(element: HTMLElement): void {
|
|
88
|
+
if (this.options.resolveInteraction) {
|
|
89
|
+
this.state.interaction = this.options.resolveInteraction(element);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data: Record<string, string | boolean> = {};
|
|
94
|
+
|
|
95
|
+
if (this.options.rules) {
|
|
96
|
+
for (const [selector, rules] of Object.entries(this.options.rules)) {
|
|
97
|
+
if (element.matches(selector)) {
|
|
98
|
+
Object.assign(data, rules);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const dataset = element.dataset;
|
|
104
|
+
for (const key in dataset) {
|
|
105
|
+
if (key.startsWith("supermouse")) {
|
|
106
|
+
const prop = key.slice(10);
|
|
107
|
+
if (prop) {
|
|
108
|
+
const cleanKey = prop.charAt(0).toLowerCase() + prop.slice(1);
|
|
109
|
+
const val = dataset[key];
|
|
110
|
+
if (val !== undefined) {
|
|
111
|
+
data[cleanKey] = val === "" ? true : val;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.state.interaction = data;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private handleMove(e: PointerEvent): void {
|
|
121
|
+
if (!this.isEnabled) return;
|
|
122
|
+
|
|
123
|
+
if (this.options.autoDisableOnMobile && e.pointerType === "touch") return;
|
|
124
|
+
|
|
125
|
+
let x = e.clientX;
|
|
126
|
+
let y = e.clientY;
|
|
127
|
+
|
|
128
|
+
if (this.options.container && this.options.container !== document.body) {
|
|
129
|
+
const rect = this.options.container.getBoundingClientRect();
|
|
130
|
+
x -= rect.left;
|
|
131
|
+
y -= rect.top;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.state.pointer.x = x;
|
|
135
|
+
this.state.pointer.y = y;
|
|
136
|
+
|
|
137
|
+
if (!this.state.hasReceivedInput) {
|
|
138
|
+
this.state.hasReceivedInput = true;
|
|
139
|
+
this.state.target.x = this.state.smooth.x = x;
|
|
140
|
+
this.state.target.y = this.state.smooth.y = y;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private handleDown(): void {
|
|
145
|
+
if (this.isEnabled) this.state.isDown = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private handleUp(): void {
|
|
149
|
+
if (this.isEnabled) this.state.isDown = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handleMouseOver(e: MouseEvent): void {
|
|
153
|
+
if (!this.isEnabled) return;
|
|
154
|
+
const target = e.target as HTMLElement;
|
|
155
|
+
|
|
156
|
+
if (target.closest("[data-supermouse-ignore]")) {
|
|
157
|
+
this.state.isNative = true;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const selector = this.getHoverSelector();
|
|
162
|
+
const hoverable = target.closest(selector);
|
|
163
|
+
|
|
164
|
+
if (hoverable) {
|
|
165
|
+
this.state.isHover = true;
|
|
166
|
+
this.state.hoverTarget = hoverable as HTMLElement;
|
|
167
|
+
this.parseDOMInteraction(this.state.hoverTarget);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const strategy = this.options.ignoreOnNative;
|
|
171
|
+
|
|
172
|
+
if (strategy) {
|
|
173
|
+
const checkTags = strategy === true || strategy === "auto" || strategy === "tag";
|
|
174
|
+
const checkCSS = strategy === true || strategy === "auto" || strategy === "css";
|
|
175
|
+
let isNative = false;
|
|
176
|
+
|
|
177
|
+
if (checkTags) {
|
|
178
|
+
const tag = target.localName;
|
|
179
|
+
if (tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable) {
|
|
180
|
+
isNative = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!isNative && checkCSS) {
|
|
185
|
+
const style = window.getComputedStyle(target).cursor;
|
|
186
|
+
const supermouseAllowed = ["default", "auto", "pointer", "none", "inherit"];
|
|
187
|
+
if (!supermouseAllowed.includes(style)) {
|
|
188
|
+
isNative = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isNative) {
|
|
193
|
+
this.state.isNative = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private handleMouseOut(e: MouseEvent): void {
|
|
199
|
+
if (!this.isEnabled) return;
|
|
200
|
+
const target = e.target as HTMLElement;
|
|
201
|
+
|
|
202
|
+
if (target === this.state.hoverTarget || target.contains(this.state.hoverTarget)) {
|
|
203
|
+
if (!e.relatedTarget || !this.state.hoverTarget?.contains(e.relatedTarget as Node)) {
|
|
204
|
+
this.state.isHover = false;
|
|
205
|
+
this.state.hoverTarget = null;
|
|
206
|
+
this.state.interaction = {};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.state.isNative) {
|
|
211
|
+
this.state.isNative = false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private handleWindowLeave(): void {
|
|
216
|
+
if (this.options.hideOnLeave) {
|
|
217
|
+
this.state.hasReceivedInput = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public clearHover(): void {
|
|
222
|
+
this.state.isHover = false;
|
|
223
|
+
this.state.hoverTarget = null;
|
|
224
|
+
this.state.isNative = false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private bindEvents(): void {
|
|
228
|
+
const { signal } = this.abortController;
|
|
229
|
+
window.addEventListener("pointermove", this.handleMove.bind(this), { passive: true, signal });
|
|
230
|
+
window.addEventListener("pointerdown", this.handleDown.bind(this), { passive: true, signal });
|
|
231
|
+
window.addEventListener("pointerup", this.handleUp.bind(this), { signal });
|
|
232
|
+
|
|
233
|
+
document.addEventListener("mouseover", this.handleMouseOver.bind(this), { signal });
|
|
234
|
+
document.addEventListener("mouseout", this.handleMouseOut.bind(this), { signal });
|
|
235
|
+
document.addEventListener("mouseleave", this.handleWindowLeave.bind(this), { signal });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public destroy(): void {
|
|
239
|
+
this.abortController.abort();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let stageCount = 0;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Stage.ts
|
|
247
|
+
*
|
|
248
|
+
* This class manages the DOM container for the custom cursor and handles native cursor visibility.
|
|
249
|
+
* It is instantiated by the `Supermouse` class and is not intended for direct use by plugins.
|
|
250
|
+
*
|
|
251
|
+
* @internal
|
|
252
|
+
*/
|
|
253
|
+
export class Stage {
|
|
254
|
+
/** The container element appended to the document. */
|
|
255
|
+
public readonly element: HTMLDivElement;
|
|
256
|
+
private styleTag: HTMLStyleElement;
|
|
257
|
+
private id: string;
|
|
258
|
+
private scopeClass: string;
|
|
259
|
+
|
|
260
|
+
private currentCursorState: "none" | "auto" | "" | null = null;
|
|
261
|
+
|
|
262
|
+
private selectors: Set<string> = new Set([
|
|
263
|
+
"a",
|
|
264
|
+
"button",
|
|
265
|
+
"input",
|
|
266
|
+
"textarea",
|
|
267
|
+
"select",
|
|
268
|
+
'[role="button"]',
|
|
269
|
+
"[tabindex]"
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
constructor(
|
|
273
|
+
private container: HTMLElement = document.body,
|
|
274
|
+
private hideNativeCursor: boolean
|
|
275
|
+
) {
|
|
276
|
+
if (!container || !(container instanceof HTMLElement)) {
|
|
277
|
+
throw new Error(`[Supermouse] Invalid container: ${container}. Must be an HTMLElement.`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const instanceId = stageCount++;
|
|
281
|
+
this.id = `supermouse-style-${instanceId}`;
|
|
282
|
+
this.scopeClass = `supermouse-scope-${instanceId}`;
|
|
283
|
+
|
|
284
|
+
const isBody = container === document.body;
|
|
285
|
+
|
|
286
|
+
this.element = document.createElement("div");
|
|
287
|
+
Object.assign(this.element.style, {
|
|
288
|
+
position: isBody ? "fixed" : "absolute",
|
|
289
|
+
top: "0",
|
|
290
|
+
left: "0",
|
|
291
|
+
width: "100%",
|
|
292
|
+
height: "100%",
|
|
293
|
+
pointerEvents: "none",
|
|
294
|
+
zIndex: "9999",
|
|
295
|
+
opacity: "1",
|
|
296
|
+
transition: "opacity 0.15s ease"
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (!isBody) {
|
|
300
|
+
const computed = window.getComputedStyle(container);
|
|
301
|
+
if (computed.position === "static") {
|
|
302
|
+
container.style.position = "relative";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
container.appendChild(this.element);
|
|
307
|
+
|
|
308
|
+
this.styleTag = document.createElement("style");
|
|
309
|
+
this.styleTag.id = this.id;
|
|
310
|
+
document.head.appendChild(this.styleTag);
|
|
311
|
+
|
|
312
|
+
this.container.classList.add(this.scopeClass);
|
|
313
|
+
|
|
314
|
+
if (this.hideNativeCursor) {
|
|
315
|
+
this.setNativeCursor("none");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Adds a new CSS selector to the `selectors` set.
|
|
321
|
+
* Called by `Supermouse` and subsequently plugins during install to ensure
|
|
322
|
+
* the native cursor is hidden on their specific interactive targets.
|
|
323
|
+
*/
|
|
324
|
+
public addSelector(selector: string): void {
|
|
325
|
+
this.selectors.add(selector);
|
|
326
|
+
if (this.hideNativeCursor) {
|
|
327
|
+
this.updateCursorCSS();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public setVisibility(visible: boolean): void {
|
|
332
|
+
this.element.style.opacity = visible ? "1" : "0";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Toggles the visibility of the native cursor via CSS injection.
|
|
337
|
+
* @param type 'none' to hide, 'auto' to show.
|
|
338
|
+
*/
|
|
339
|
+
public setNativeCursor(type: "none" | "auto" | ""): void {
|
|
340
|
+
if (!this.hideNativeCursor && type === "none") return;
|
|
341
|
+
|
|
342
|
+
if (type === this.currentCursorState) return;
|
|
343
|
+
this.currentCursorState = type;
|
|
344
|
+
|
|
345
|
+
if (type === "none") {
|
|
346
|
+
this.container.style.cursor = "none";
|
|
347
|
+
this.updateCursorCSS();
|
|
348
|
+
} else {
|
|
349
|
+
this.container.style.cursor = "";
|
|
350
|
+
this.styleTag.innerText = "";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private updateCursorCSS(): void {
|
|
355
|
+
const rawSelectors = Array.from(this.selectors);
|
|
356
|
+
if (rawSelectors.length === 0) {
|
|
357
|
+
this.styleTag.innerText = "";
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const scopedSelectors = rawSelectors.map((s) => `.${this.scopeClass} ${s}`).join(", ");
|
|
362
|
+
|
|
363
|
+
this.styleTag.innerText = `
|
|
364
|
+
${scopedSelectors} {
|
|
365
|
+
cursor: none !important;
|
|
366
|
+
}
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public destroy(): void {
|
|
371
|
+
this.element.remove();
|
|
372
|
+
this.styleTag.remove();
|
|
373
|
+
this.container.style.cursor = "";
|
|
374
|
+
this.container.classList.remove(this.scopeClass);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const OFFSCREEN = { x: -100, y: -100 } as const;
|
|
379
|
+
export const DEFAULT_HOVER_SELECTORS = [
|
|
380
|
+
"a",
|
|
381
|
+
"button",
|
|
382
|
+
"input",
|
|
383
|
+
"textarea",
|
|
384
|
+
"[data-hover]",
|
|
385
|
+
"[data-cursor]"
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Supermouse Runtime Loop
|
|
390
|
+
*
|
|
391
|
+
* This class orchestrates the application state, manages the animation loop,
|
|
392
|
+
* and coordinates data flow between the internal systems, and the plugins.
|
|
393
|
+
*
|
|
394
|
+
* @default
|
|
395
|
+
*/
|
|
396
|
+
export class Supermouse {
|
|
397
|
+
public static readonly version: string = __VERSION__;
|
|
398
|
+
public readonly version: string = __VERSION__;
|
|
399
|
+
|
|
400
|
+
state: MouseState;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Configuration options.
|
|
404
|
+
*/
|
|
405
|
+
options: SupermouseOptions;
|
|
406
|
+
|
|
407
|
+
private plugins: SupermousePlugin[] = [];
|
|
408
|
+
private stage: Stage;
|
|
409
|
+
private input: Input;
|
|
410
|
+
|
|
411
|
+
private rafId: number = 0;
|
|
412
|
+
private lastTime: number = 0;
|
|
413
|
+
private isRunning: boolean = false;
|
|
414
|
+
|
|
415
|
+
private hoverSelectors: Set<string>;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Creates a new Supermouse instance.
|
|
419
|
+
*
|
|
420
|
+
* @param options - Global configuration options.
|
|
421
|
+
* @throws Will throw if running in a non-browser environment (window/document undefined).
|
|
422
|
+
*/
|
|
423
|
+
constructor(options: SupermouseOptions = {}) {
|
|
424
|
+
this.options = {
|
|
425
|
+
smoothness: 0.15,
|
|
426
|
+
enableTouch: false,
|
|
427
|
+
autoDisableOnMobile: true,
|
|
428
|
+
ignoreOnNative: "auto",
|
|
429
|
+
hideCursor: true,
|
|
430
|
+
hideOnLeave: true,
|
|
431
|
+
autoStart: true,
|
|
432
|
+
container: document.body,
|
|
433
|
+
...options
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
if (!this.options.container) {
|
|
437
|
+
this.options.container = document.body;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.state = {
|
|
441
|
+
pointer: { x: -100, y: -100 },
|
|
442
|
+
target: { x: -100, y: -100 },
|
|
443
|
+
smooth: { x: -100, y: -100 },
|
|
444
|
+
velocity: { x: 0, y: 0 },
|
|
445
|
+
angle: 0,
|
|
446
|
+
isDown: false,
|
|
447
|
+
isHover: false,
|
|
448
|
+
isNative: false,
|
|
449
|
+
forcedCursor: null,
|
|
450
|
+
hoverTarget: null,
|
|
451
|
+
reducedMotion: false,
|
|
452
|
+
hasReceivedInput: false,
|
|
453
|
+
shape: null,
|
|
454
|
+
interaction: {}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
if (this.options.hoverSelectors) {
|
|
458
|
+
this.hoverSelectors = new Set(this.options.hoverSelectors);
|
|
459
|
+
} else {
|
|
460
|
+
this.hoverSelectors = new Set(DEFAULT_HOVER_SELECTORS);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.stage = new Stage(this.options.container, !!this.options.hideCursor);
|
|
464
|
+
this.hoverSelectors.forEach((s) => this.stage.addSelector(s));
|
|
465
|
+
|
|
466
|
+
this.input = new Input(
|
|
467
|
+
this.state,
|
|
468
|
+
this.options,
|
|
469
|
+
() => Array.from(this.hoverSelectors).join(", "),
|
|
470
|
+
(enabled) => {
|
|
471
|
+
if (!enabled) this.resetPosition();
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (this.options.plugins) {
|
|
476
|
+
this.options.plugins.forEach((p) => this.use(p));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.init();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Retrieves a registered plugin instance by its unique name.
|
|
484
|
+
*/
|
|
485
|
+
public getPlugin(name: string): SupermousePlugin | undefined {
|
|
486
|
+
return this.plugins.find((p) => p.name === name);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Returns whether the cursor system is currently enabled (processing input).
|
|
491
|
+
*/
|
|
492
|
+
public get isEnabled(): boolean {
|
|
493
|
+
return this.input.isEnabled;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Enables a specific plugin by name.
|
|
498
|
+
* Triggers the `onEnable` lifecycle hook of the plugin.
|
|
499
|
+
*/
|
|
500
|
+
public enablePlugin(name: string): void {
|
|
501
|
+
const plugin = this.getPlugin(name);
|
|
502
|
+
if (plugin && plugin.isEnabled === false) {
|
|
503
|
+
plugin.isEnabled = true;
|
|
504
|
+
plugin.onEnable?.(this);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Disables a specific plugin by name.
|
|
510
|
+
* Triggers the `onDisable` lifecycle hook.
|
|
511
|
+
*/
|
|
512
|
+
public disablePlugin(name: string): void {
|
|
513
|
+
const plugin = this.getPlugin(name);
|
|
514
|
+
if (plugin && plugin.isEnabled !== false) {
|
|
515
|
+
plugin.isEnabled = false;
|
|
516
|
+
plugin.onDisable?.(this);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Toggles the enabled state of a plugin.
|
|
522
|
+
*/
|
|
523
|
+
public togglePlugin(name: string): void {
|
|
524
|
+
const plugin = this.getPlugin(name);
|
|
525
|
+
if (plugin) {
|
|
526
|
+
if (plugin.isEnabled === false) this.enablePlugin(name);
|
|
527
|
+
else this.disablePlugin(name);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
public registerHoverTarget(selector: string): void {
|
|
532
|
+
if (!this.hoverSelectors.has(selector)) {
|
|
533
|
+
this.hoverSelectors.add(selector);
|
|
534
|
+
this.stage.addSelector(selector);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* The fixed container element where plugins should append their DOM nodes.
|
|
540
|
+
*/
|
|
541
|
+
public get container(): HTMLDivElement {
|
|
542
|
+
return this.stage.element;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Manually override the native cursor visibility.
|
|
547
|
+
*
|
|
548
|
+
* @param type 'auto' (Show Native), 'none' (Hide Native), or null (Resume Auto-detection)
|
|
549
|
+
*/
|
|
550
|
+
public setCursor(type: "auto" | "none" | null): void {
|
|
551
|
+
this.state.forcedCursor = type;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private init(): void {
|
|
555
|
+
if (this.options.autoStart) {
|
|
556
|
+
this.startLoop();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
public enable(): void {
|
|
561
|
+
this.input.isEnabled = true;
|
|
562
|
+
this.stage.setNativeCursor("none");
|
|
563
|
+
}
|
|
564
|
+
public disable(): void {
|
|
565
|
+
this.input.isEnabled = false;
|
|
566
|
+
this.stage.setNativeCursor("auto");
|
|
567
|
+
this.resetPosition();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Registers a new plugin.
|
|
572
|
+
*
|
|
573
|
+
* @param plugin - The plugin object to install.
|
|
574
|
+
*/
|
|
575
|
+
public use(plugin: SupermousePlugin): this {
|
|
576
|
+
if (this.plugins.find((p) => p.name === plugin.name)) {
|
|
577
|
+
console.warn(`[Supermouse] Plugin "${plugin.name}" already installed.`);
|
|
578
|
+
return this;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (plugin.isEnabled === undefined) {
|
|
582
|
+
plugin.isEnabled = true;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
this.plugins.push(plugin);
|
|
586
|
+
this.plugins.sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
587
|
+
|
|
588
|
+
plugin.install?.(this);
|
|
589
|
+
return this;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private resetCoords(): void {
|
|
593
|
+
this.state.pointer = { ...OFFSCREEN };
|
|
594
|
+
this.state.target = { ...OFFSCREEN };
|
|
595
|
+
this.state.smooth = { ...OFFSCREEN };
|
|
596
|
+
this.state.velocity = { x: 0, y: 0 };
|
|
597
|
+
this.state.angle = 0;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private resetPosition(): void {
|
|
601
|
+
this.resetCoords();
|
|
602
|
+
this.state.hasReceivedInput = false;
|
|
603
|
+
this.state.shape = null;
|
|
604
|
+
this.state.interaction = {};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private startLoop(): void {
|
|
608
|
+
if (this.isRunning) return;
|
|
609
|
+
this.isRunning = true;
|
|
610
|
+
this.lastTime = performance.now();
|
|
611
|
+
this.tick(this.lastTime);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Manually steps the animation loop.
|
|
616
|
+
*
|
|
617
|
+
* @param time Current timestamp in milliseconds.
|
|
618
|
+
*/
|
|
619
|
+
public step(time: number): void {
|
|
620
|
+
this.tick(time);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private runPluginSafe(plugin: SupermousePlugin, deltaTime: number): void {
|
|
624
|
+
if (plugin.isEnabled === false) return;
|
|
625
|
+
try {
|
|
626
|
+
plugin.update?.(this, deltaTime);
|
|
627
|
+
} catch (e) {
|
|
628
|
+
console.error(`[Supermouse] Plugin '${plugin.name}' crashed and has been disabled.`, e);
|
|
629
|
+
plugin.isEnabled = false;
|
|
630
|
+
try {
|
|
631
|
+
plugin.onDisable?.(this);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
console.error(`[Supermouse] Failed to execute onDisable for plugin '${plugin.name}'.`, err);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Runs on every animation frame.
|
|
640
|
+
*/
|
|
641
|
+
private tick = (time: number): void => {
|
|
642
|
+
const dtMs = time - this.lastTime;
|
|
643
|
+
const dt = Math.min(dtMs / 1000, 0.1);
|
|
644
|
+
this.lastTime = time;
|
|
645
|
+
|
|
646
|
+
if (this.state.hoverTarget && !this.state.hoverTarget.isConnected) {
|
|
647
|
+
this.input.clearHover();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const shouldShowStage =
|
|
651
|
+
this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
|
|
652
|
+
this.stage.setVisibility(shouldShowStage);
|
|
653
|
+
|
|
654
|
+
if (this.input.isEnabled && this.options.hideCursor) {
|
|
655
|
+
let targetState: "none" | "auto" = "auto";
|
|
656
|
+
if (this.state.forcedCursor !== null) {
|
|
657
|
+
targetState = this.state.forcedCursor;
|
|
658
|
+
} else {
|
|
659
|
+
const showNative = this.state.isNative || !this.state.hasReceivedInput;
|
|
660
|
+
targetState = showNative ? "auto" : "none";
|
|
661
|
+
}
|
|
662
|
+
this.stage.setNativeCursor(targetState);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (this.input.isEnabled) {
|
|
666
|
+
this.state.target.x = this.state.pointer.x;
|
|
667
|
+
this.state.target.y = this.state.pointer.y;
|
|
668
|
+
} else {
|
|
669
|
+
this.resetCoords();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
for (let i = 0; i < this.plugins.length; i++) {
|
|
673
|
+
this.runPluginSafe(this.plugins[i], dtMs);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (this.input.isEnabled) {
|
|
677
|
+
const factor = this.state.reducedMotion ? 1000 : (1 / this.options.smoothness!) * 2;
|
|
678
|
+
|
|
679
|
+
this.state.smooth.x = damp(this.state.smooth.x, this.state.target.x, factor, dt);
|
|
680
|
+
this.state.smooth.y = damp(this.state.smooth.y, this.state.target.y, factor, dt);
|
|
681
|
+
|
|
682
|
+
const vx = this.state.target.x - this.state.smooth.x;
|
|
683
|
+
const vy = this.state.target.y - this.state.smooth.y;
|
|
684
|
+
this.state.velocity.x = vx;
|
|
685
|
+
this.state.velocity.y = vy;
|
|
686
|
+
|
|
687
|
+
if (Math.abs(vx) > 0.1 || Math.abs(vy) > 0.1) {
|
|
688
|
+
this.state.angle = angle(vx, vy);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (this.options.autoStart && this.isRunning) {
|
|
693
|
+
this.rafId = requestAnimationFrame(this.tick);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Destroys the instance.
|
|
699
|
+
*/
|
|
700
|
+
public destroy(): void {
|
|
701
|
+
this.isRunning = false;
|
|
702
|
+
cancelAnimationFrame(this.rafId);
|
|
703
|
+
this.input.destroy();
|
|
704
|
+
this.stage.destroy();
|
|
705
|
+
this.plugins.forEach((p) => p.destroy?.(this));
|
|
706
|
+
this.plugins = [];
|
|
707
|
+
}
|
|
708
|
+
}
|