@supermousejs/core 2.0.4 → 2.1.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 +51 -31
- package/LICENSE.md +21 -21
- package/README.md +30 -31
- package/dist/index.d.ts +8 -55
- package/dist/index.mjs +85 -146
- package/dist/index.umd.js +2 -2
- package/package.json +11 -1
- package/src/Supermouse.ts +339 -405
- package/src/index.ts +2 -2
- package/src/systems/Input.ts +231 -262
- package/src/systems/Stage.ts +126 -156
- package/src/systems/index.ts +2 -2
- package/src/types.ts +168 -169
- package/src/utils/math.ts +11 -20
- package/tsconfig.json +7 -14
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +32 -32
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
1
|
+
export * from "./Supermouse";
|
|
2
|
+
export * from "./types";
|
package/src/systems/Input.ts
CHANGED
|
@@ -1,262 +1,231 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
this.
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
this.state.hoverTarget
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
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
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
private handleWindowLeave = () => {
|
|
235
|
-
if (this.options.hideOnLeave) {
|
|
236
|
-
this.state.hasReceivedInput = false;
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
private bindEvents() {
|
|
241
|
-
window.addEventListener('pointermove', this.handleMove, { passive: true });
|
|
242
|
-
window.addEventListener('pointerdown', this.handleDown, { passive: true });
|
|
243
|
-
window.addEventListener('pointerup', this.handleUp);
|
|
244
|
-
|
|
245
|
-
document.addEventListener('mouseover', this.handleMouseOver);
|
|
246
|
-
document.addEventListener('mouseout', this.handleMouseOut);
|
|
247
|
-
document.addEventListener('mouseleave', this.handleWindowLeave);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
public destroy() {
|
|
251
|
-
if (this.mediaQueryList && this.mediaQueryHandler) {
|
|
252
|
-
this.mediaQueryList.removeEventListener('change', this.mediaQueryHandler);
|
|
253
|
-
}
|
|
254
|
-
window.removeEventListener('pointermove', this.handleMove);
|
|
255
|
-
window.removeEventListener('pointerdown', this.handleDown);
|
|
256
|
-
window.removeEventListener('pointerup', this.handleUp);
|
|
257
|
-
|
|
258
|
-
document.removeEventListener('mouseover', this.handleMouseOver);
|
|
259
|
-
document.removeEventListener('mouseout', this.handleMouseOut);
|
|
260
|
-
document.removeEventListener('mouseleave', this.handleWindowLeave);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
1
|
+
import type { MouseState, SupermouseOptions } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This class listens to browser events and mutates the shared `MouseState` object.
|
|
5
|
+
*
|
|
6
|
+
* @internal This is an internal system class instantiated by `Supermouse`.
|
|
7
|
+
*/
|
|
8
|
+
export class Input {
|
|
9
|
+
private mediaQueryList?: MediaQueryList;
|
|
10
|
+
private mediaQueryHandler?: (e: MediaQueryListEvent) => void;
|
|
11
|
+
private motionQuery?: MediaQueryList;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Master switch for input processing.
|
|
15
|
+
* Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
|
|
16
|
+
*/
|
|
17
|
+
public isEnabled: boolean = true;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private state: MouseState,
|
|
21
|
+
private options: SupermouseOptions,
|
|
22
|
+
private getHoverSelector: () => string,
|
|
23
|
+
private onEnableChange: (enabled: boolean) => void
|
|
24
|
+
) {
|
|
25
|
+
this.checkDeviceCapability();
|
|
26
|
+
this.checkMotionPreference();
|
|
27
|
+
this.bindEvents();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Automatically disables the custom cursor on devices without fine pointer control.
|
|
32
|
+
* Relies on `matchMedia('(pointer: fine)')`.
|
|
33
|
+
*/
|
|
34
|
+
private checkDeviceCapability() {
|
|
35
|
+
if (!this.options.autoDisableOnMobile) return;
|
|
36
|
+
|
|
37
|
+
this.mediaQueryList = window.matchMedia("(pointer: fine)");
|
|
38
|
+
this.updateEnabledState(this.mediaQueryList.matches);
|
|
39
|
+
|
|
40
|
+
this.mediaQueryHandler = (e: MediaQueryListEvent) => {
|
|
41
|
+
this.updateEnabledState(e.matches);
|
|
42
|
+
};
|
|
43
|
+
this.mediaQueryList.addEventListener("change", this.mediaQueryHandler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks for `prefers-reduced-motion`.
|
|
48
|
+
* If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
|
|
49
|
+
*/
|
|
50
|
+
private checkMotionPreference() {
|
|
51
|
+
this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
52
|
+
this.state.reducedMotion = this.motionQuery.matches;
|
|
53
|
+
|
|
54
|
+
this.motionQuery.addEventListener("change", (e) => {
|
|
55
|
+
this.state.reducedMotion = e.matches;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private updateEnabledState(enabled: boolean) {
|
|
60
|
+
this.isEnabled = enabled;
|
|
61
|
+
this.onEnableChange(enabled);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private parseDOMInteraction(element: HTMLElement) {
|
|
65
|
+
if (this.options.resolveInteraction) {
|
|
66
|
+
this.state.interaction = this.options.resolveInteraction(element);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data: Record<string, string | boolean> = {};
|
|
71
|
+
|
|
72
|
+
if (this.options.rules) {
|
|
73
|
+
for (const [selector, rules] of Object.entries(this.options.rules)) {
|
|
74
|
+
if (element.matches(selector)) {
|
|
75
|
+
Object.assign(data, rules);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const dataset = element.dataset;
|
|
81
|
+
for (const key in dataset) {
|
|
82
|
+
if (key.startsWith("supermouse")) {
|
|
83
|
+
const prop = key.slice(10);
|
|
84
|
+
if (prop) {
|
|
85
|
+
const cleanKey = prop.charAt(0).toLowerCase() + prop.slice(1);
|
|
86
|
+
const val = dataset[key];
|
|
87
|
+
if (val !== undefined) {
|
|
88
|
+
data[cleanKey] = val === "" ? true : val;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.state.interaction = data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private handleMove(e: PointerEvent) {
|
|
98
|
+
if (!this.isEnabled) return;
|
|
99
|
+
|
|
100
|
+
if (this.options.autoDisableOnMobile && e.pointerType === "touch") return;
|
|
101
|
+
|
|
102
|
+
let x = e.clientX;
|
|
103
|
+
let y = e.clientY;
|
|
104
|
+
|
|
105
|
+
if (this.options.container && this.options.container !== document.body) {
|
|
106
|
+
const rect = this.options.container.getBoundingClientRect();
|
|
107
|
+
x -= rect.left;
|
|
108
|
+
y -= rect.top;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.state.pointer.x = x;
|
|
112
|
+
this.state.pointer.y = y;
|
|
113
|
+
|
|
114
|
+
if (!this.state.hasReceivedInput) {
|
|
115
|
+
this.state.hasReceivedInput = true;
|
|
116
|
+
this.state.target.x = this.state.smooth.x = x;
|
|
117
|
+
this.state.target.y = this.state.smooth.y = y;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private handleDown(): void {
|
|
122
|
+
if (this.isEnabled) this.state.isDown = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private handleUp(): void {
|
|
126
|
+
if (this.isEnabled) this.state.isDown = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private handleMouseOver(e: MouseEvent) {
|
|
130
|
+
if (!this.isEnabled) return;
|
|
131
|
+
const target = e.target as HTMLElement;
|
|
132
|
+
|
|
133
|
+
if (target.closest("[data-supermouse-ignore]")) {
|
|
134
|
+
this.state.isNative = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const selector = this.getHoverSelector();
|
|
139
|
+
const hoverable = target.closest(selector);
|
|
140
|
+
|
|
141
|
+
if (hoverable) {
|
|
142
|
+
this.state.isHover = true;
|
|
143
|
+
this.state.hoverTarget = hoverable as HTMLElement;
|
|
144
|
+
this.parseDOMInteraction(this.state.hoverTarget);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const strategy = this.options.ignoreOnNative;
|
|
148
|
+
|
|
149
|
+
if (strategy) {
|
|
150
|
+
const checkTags = strategy === true || strategy === "auto" || strategy === "tag";
|
|
151
|
+
const checkCSS = strategy === true || strategy === "auto" || strategy === "css";
|
|
152
|
+
let isNative = false;
|
|
153
|
+
|
|
154
|
+
if (checkTags) {
|
|
155
|
+
const tag = target.localName;
|
|
156
|
+
if (tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable) {
|
|
157
|
+
isNative = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!isNative && checkCSS) {
|
|
162
|
+
const style = window.getComputedStyle(target).cursor;
|
|
163
|
+
const supermouseAllowed = ["default", "auto", "pointer", "none", "inherit"];
|
|
164
|
+
if (!supermouseAllowed.includes(style)) {
|
|
165
|
+
isNative = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isNative) {
|
|
170
|
+
this.state.isNative = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private handleMouseOut(e: MouseEvent) {
|
|
176
|
+
if (!this.isEnabled) return;
|
|
177
|
+
const target = e.target as HTMLElement;
|
|
178
|
+
|
|
179
|
+
if (target === this.state.hoverTarget || target.contains(this.state.hoverTarget)) {
|
|
180
|
+
if (!e.relatedTarget || !this.state.hoverTarget?.contains(e.relatedTarget as Node)) {
|
|
181
|
+
this.state.isHover = false;
|
|
182
|
+
this.state.hoverTarget = null;
|
|
183
|
+
this.state.interaction = {};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.state.isNative) {
|
|
188
|
+
this.state.isNative = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private handleWindowLeave(): void {
|
|
193
|
+
if (this.options.hideOnLeave) {
|
|
194
|
+
this.state.hasReceivedInput = false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public clearHover() {
|
|
199
|
+
this.state.isHover = false;
|
|
200
|
+
this.state.hoverTarget = null;
|
|
201
|
+
this.state.isNative = false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private bindEvents() {
|
|
205
|
+
window.addEventListener("pointermove", this.handleMove.bind(this), { passive: true });
|
|
206
|
+
window.addEventListener("pointerdown", this.handleDown.bind(this), { passive: true });
|
|
207
|
+
window.addEventListener("pointerup", this.handleUp.bind(this));
|
|
208
|
+
|
|
209
|
+
document.addEventListener("mouseover", this.handleMouseOver.bind(this));
|
|
210
|
+
document.addEventListener("mouseout", this.handleMouseOut.bind(this));
|
|
211
|
+
document.addEventListener("mouseleave", this.handleWindowLeave.bind(this));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public destroy() {
|
|
215
|
+
if (this.mediaQueryList && this.mediaQueryHandler) {
|
|
216
|
+
this.mediaQueryList.removeEventListener("change", this.mediaQueryHandler);
|
|
217
|
+
}
|
|
218
|
+
if (this.motionQuery) {
|
|
219
|
+
// Modern browsers support removeEventListener on MediaQueryList
|
|
220
|
+
this.motionQuery.onchange = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
window.removeEventListener("pointermove", this.handleMove);
|
|
224
|
+
window.removeEventListener("pointerdown", this.handleDown);
|
|
225
|
+
window.removeEventListener("pointerup", this.handleUp);
|
|
226
|
+
|
|
227
|
+
document.removeEventListener("mouseover", this.handleMouseOver);
|
|
228
|
+
document.removeEventListener("mouseout", this.handleMouseOut);
|
|
229
|
+
document.removeEventListener("mouseleave", this.handleWindowLeave);
|
|
230
|
+
}
|
|
231
|
+
}
|