@supermousejs/core 2.0.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 +7 -0
- package/LICENSE.md +21 -0
- package/dist/index.d.ts +304 -0
- package/dist/index.mjs +453 -0
- package/dist/index.umd.js +5 -0
- package/package.json +23 -0
- package/src/Supermouse.ts +405 -0
- package/src/index.ts +2 -0
- package/src/systems/Input.ts +262 -0
- package/src/systems/Stage.ts +156 -0
- package/src/systems/index.ts +2 -0
- package/src/types.ts +169 -0
- package/src/utils/math.ts +20 -0
- package/tsconfig.json +16 -0
- package/vite.config.ts +32 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
let d = 0;
|
|
2
|
+
class c {
|
|
3
|
+
constructor(t = document.body, e) {
|
|
4
|
+
if (this.container = t, this.hideNativeCursor = e, !t || !(t instanceof HTMLElement))
|
|
5
|
+
throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);
|
|
6
|
+
const s = d++;
|
|
7
|
+
this.id = `supermouse-style-${s}`, this.scopeClass = `supermouse-scope-${s}`;
|
|
8
|
+
const n = t === document.body;
|
|
9
|
+
this.element = document.createElement("div"), Object.assign(this.element.style, {
|
|
10
|
+
position: n ? "fixed" : "absolute",
|
|
11
|
+
top: "0",
|
|
12
|
+
left: "0",
|
|
13
|
+
width: "100%",
|
|
14
|
+
height: "100%",
|
|
15
|
+
pointerEvents: "none",
|
|
16
|
+
zIndex: "9999",
|
|
17
|
+
opacity: "1",
|
|
18
|
+
transition: "opacity 0.15s ease"
|
|
19
|
+
}), n || window.getComputedStyle(t).position === "static" && (t.style.position = "relative"), t.appendChild(this.element), this.styleTag = document.createElement("style"), this.styleTag.id = this.id, document.head.appendChild(this.styleTag), this.container.classList.add(this.scopeClass), this.hideNativeCursor && this.setNativeCursor("none");
|
|
20
|
+
}
|
|
21
|
+
/** The container element appended to the document. Plugins must append here. */
|
|
22
|
+
element;
|
|
23
|
+
styleTag;
|
|
24
|
+
id;
|
|
25
|
+
scopeClass;
|
|
26
|
+
// Cache to prevent redundant DOM updates
|
|
27
|
+
currentCursorState = null;
|
|
28
|
+
// Defaults for CSS hiding. We must override user-agent styles on these elements
|
|
29
|
+
// to prevent the native cursor from popping through.
|
|
30
|
+
selectors = /* @__PURE__ */ new Set([
|
|
31
|
+
"a",
|
|
32
|
+
"button",
|
|
33
|
+
"input",
|
|
34
|
+
"textarea",
|
|
35
|
+
"select",
|
|
36
|
+
'[role="button"]',
|
|
37
|
+
"[tabindex]"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Adds a new CSS selector to the "Hide Native Cursor" list.
|
|
41
|
+
* Called by `Supermouse` (and subsequently plugins) during install to ensure
|
|
42
|
+
* the native cursor is hidden on their specific interactive targets.
|
|
43
|
+
*/
|
|
44
|
+
addSelector(t) {
|
|
45
|
+
this.selectors.add(t), this.hideNativeCursor && this.updateCursorCSS();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Controls the opacity of the entire stage (all custom cursor elements).
|
|
49
|
+
*/
|
|
50
|
+
setVisibility(t) {
|
|
51
|
+
this.element.style.opacity = t ? "1" : "0";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Toggles the visibility of the native cursor via CSS injection.
|
|
55
|
+
* @param type 'none' to hide, 'auto' to show.
|
|
56
|
+
*/
|
|
57
|
+
setNativeCursor(t) {
|
|
58
|
+
!this.hideNativeCursor && t === "none" || t !== this.currentCursorState && (this.currentCursorState = t, t === "none" ? (this.container.style.cursor = "none", this.updateCursorCSS()) : (this.container.style.cursor = "", this.styleTag.innerText = ""));
|
|
59
|
+
}
|
|
60
|
+
updateCursorCSS() {
|
|
61
|
+
const t = Array.from(this.selectors);
|
|
62
|
+
if (t.length === 0) {
|
|
63
|
+
this.styleTag.innerText = "";
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const e = t.map((s) => `.${this.scopeClass} ${s}`).join(", ");
|
|
67
|
+
this.styleTag.innerText = `
|
|
68
|
+
${e} {
|
|
69
|
+
cursor: none !important;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
destroy() {
|
|
74
|
+
this.element.remove(), this.styleTag.remove(), this.container.style.cursor = "", this.container.classList.remove(this.scopeClass);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class p {
|
|
78
|
+
constructor(t, e, s, n) {
|
|
79
|
+
this.state = t, this.options = e, this.getHoverSelector = s, this.onEnableChange = n, this.checkDeviceCapability(), this.checkMotionPreference(), this.bindEvents();
|
|
80
|
+
}
|
|
81
|
+
mediaQueryList;
|
|
82
|
+
mediaQueryHandler;
|
|
83
|
+
motionQuery;
|
|
84
|
+
/**
|
|
85
|
+
* Master switch for input processing.
|
|
86
|
+
* Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
|
|
87
|
+
*/
|
|
88
|
+
isEnabled = !0;
|
|
89
|
+
/**
|
|
90
|
+
* Performance Optimization:
|
|
91
|
+
* We cache the resolved InteractionState for every element we encounter in a WeakMap.
|
|
92
|
+
*/
|
|
93
|
+
interactionCache = /* @__PURE__ */ new WeakMap();
|
|
94
|
+
/**
|
|
95
|
+
* Automatically disables the custom cursor on devices without fine pointer control (e.g. phones/tablets).
|
|
96
|
+
* Relies on `matchMedia('(pointer: fine)')`.
|
|
97
|
+
*/
|
|
98
|
+
checkDeviceCapability() {
|
|
99
|
+
this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.mediaQueryHandler = (t) => {
|
|
100
|
+
this.updateEnabledState(t.matches);
|
|
101
|
+
}, this.mediaQueryList.addEventListener("change", this.mediaQueryHandler));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Checks for `prefers-reduced-motion`.
|
|
105
|
+
* If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
|
|
106
|
+
*/
|
|
107
|
+
checkMotionPreference() {
|
|
108
|
+
this.motionQuery = window.matchMedia("(prefer-reduced-motion: reduce)"), this.state.reducedMotion = this.motionQuery.matches, this.motionQuery.addEventListener("change", (t) => {
|
|
109
|
+
this.state.reducedMotion = t.matches;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
updateEnabledState(t) {
|
|
113
|
+
this.isEnabled = t, this.onEnableChange(t);
|
|
114
|
+
}
|
|
115
|
+
// --- Interaction Parsing ---
|
|
116
|
+
/**
|
|
117
|
+
* Scrapes the DOM element for metadata to populate `state.interaction`.
|
|
118
|
+
*
|
|
119
|
+
* **Strategy:**
|
|
120
|
+
* 1. Check WeakMap cache.
|
|
121
|
+
* 2. Apply config-based `rules`.
|
|
122
|
+
* 3. Scrape `dataset` (supermouse*).
|
|
123
|
+
* 4. Cache result.
|
|
124
|
+
*/
|
|
125
|
+
parseDOMInteraction(t) {
|
|
126
|
+
if (this.options.resolveInteraction) {
|
|
127
|
+
this.state.interaction = this.options.resolveInteraction(t);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (this.interactionCache.has(t)) {
|
|
131
|
+
this.state.interaction = this.interactionCache.get(t);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const e = {};
|
|
135
|
+
if (this.options.rules)
|
|
136
|
+
for (const [n, i] of Object.entries(this.options.rules))
|
|
137
|
+
t.matches(n) && Object.assign(e, i);
|
|
138
|
+
const s = t.dataset;
|
|
139
|
+
for (const n in s)
|
|
140
|
+
if (n.startsWith("supermouse")) {
|
|
141
|
+
const i = n.slice(10);
|
|
142
|
+
if (i) {
|
|
143
|
+
const l = i.charAt(0).toLowerCase() + i.slice(1), r = s[n];
|
|
144
|
+
e[l] = r === "" ? !0 : r;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.interactionCache.set(t, e), this.state.interaction = e;
|
|
148
|
+
}
|
|
149
|
+
// --- Handlers ---
|
|
150
|
+
// Unified Pointer Event Handler
|
|
151
|
+
handleMove = (t) => {
|
|
152
|
+
if (!this.isEnabled || this.options.autoDisableOnMobile && t.pointerType === "touch") return;
|
|
153
|
+
let e = t.clientX, s = t.clientY;
|
|
154
|
+
if (this.options.container && this.options.container !== document.body) {
|
|
155
|
+
const n = this.options.container.getBoundingClientRect();
|
|
156
|
+
e -= n.left, s -= n.top;
|
|
157
|
+
}
|
|
158
|
+
this.state.pointer.x = e, this.state.pointer.y = s, this.state.hasReceivedInput || (this.state.hasReceivedInput = !0, this.state.target.x = this.state.smooth.x = e, this.state.target.y = this.state.smooth.y = s);
|
|
159
|
+
};
|
|
160
|
+
handleDown = () => {
|
|
161
|
+
this.isEnabled && (this.state.isDown = !0);
|
|
162
|
+
};
|
|
163
|
+
handleUp = () => {
|
|
164
|
+
this.isEnabled && (this.state.isDown = !1);
|
|
165
|
+
};
|
|
166
|
+
handleMouseOver = (t) => {
|
|
167
|
+
if (!this.isEnabled) return;
|
|
168
|
+
const e = t.target;
|
|
169
|
+
if (e.closest("[data-supermouse-ignore]")) {
|
|
170
|
+
this.state.isNative = !0;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const s = this.getHoverSelector(), n = e.closest(s);
|
|
174
|
+
n && (this.state.isHover = !0, this.state.hoverTarget = n, this.parseDOMInteraction(this.state.hoverTarget));
|
|
175
|
+
const i = this.options.ignoreOnNative;
|
|
176
|
+
if (i) {
|
|
177
|
+
const l = i === !0 || i === "auto" || i === "tag", r = i === !0 || i === "auto" || i === "css";
|
|
178
|
+
let h = !1;
|
|
179
|
+
if (l) {
|
|
180
|
+
const a = e.localName;
|
|
181
|
+
(a === "input" || a === "textarea" || a === "select" || e.isContentEditable) && (h = !0);
|
|
182
|
+
}
|
|
183
|
+
if (!h && r) {
|
|
184
|
+
const a = window.getComputedStyle(e).cursor;
|
|
185
|
+
["default", "auto", "pointer", "none", "inherit"].includes(a) || (h = !0);
|
|
186
|
+
}
|
|
187
|
+
h && (this.state.isNative = !0);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
handleMouseOut = (t) => {
|
|
191
|
+
if (!this.isEnabled) return;
|
|
192
|
+
const e = t.target;
|
|
193
|
+
(e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!t.relatedTarget || !this.state.hoverTarget?.contains(t.relatedTarget)) && (this.state.isHover = !1, this.state.hoverTarget = null, this.state.interaction = {}), this.state.isNative && (this.state.isNative = !1);
|
|
194
|
+
};
|
|
195
|
+
handleWindowLeave = () => {
|
|
196
|
+
this.options.hideOnLeave && (this.state.hasReceivedInput = !1);
|
|
197
|
+
};
|
|
198
|
+
bindEvents() {
|
|
199
|
+
window.addEventListener("pointermove", this.handleMove, { passive: !0 }), window.addEventListener("pointerdown", this.handleDown, { passive: !0 }), window.addEventListener("pointerup", this.handleUp), document.addEventListener("mouseover", this.handleMouseOver), document.addEventListener("mouseout", this.handleMouseOut), document.addEventListener("mouseleave", this.handleWindowLeave);
|
|
200
|
+
}
|
|
201
|
+
destroy() {
|
|
202
|
+
this.mediaQueryList && this.mediaQueryHandler && this.mediaQueryList.removeEventListener("change", this.mediaQueryHandler), window.removeEventListener("pointermove", this.handleMove), window.removeEventListener("pointerdown", this.handleDown), window.removeEventListener("pointerup", this.handleUp), document.removeEventListener("mouseover", this.handleMouseOver), document.removeEventListener("mouseout", this.handleMouseOut), document.removeEventListener("mouseleave", this.handleWindowLeave);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function v(o, t, e) {
|
|
206
|
+
return o + (t - o) * e;
|
|
207
|
+
}
|
|
208
|
+
function u(o, t, e, s) {
|
|
209
|
+
return v(o, t, 1 - Math.exp(-e * s));
|
|
210
|
+
}
|
|
211
|
+
function f(o, t) {
|
|
212
|
+
return Math.atan2(t, o) * (180 / Math.PI);
|
|
213
|
+
}
|
|
214
|
+
const g = [
|
|
215
|
+
"a",
|
|
216
|
+
"button",
|
|
217
|
+
"input",
|
|
218
|
+
"textarea",
|
|
219
|
+
"[data-hover]",
|
|
220
|
+
"[data-cursor]"
|
|
221
|
+
];
|
|
222
|
+
class y {
|
|
223
|
+
/** The current version of Supermouse.js */
|
|
224
|
+
static version = "2.0.0";
|
|
225
|
+
version = "2.0.0";
|
|
226
|
+
/**
|
|
227
|
+
* The Single Source of Truth.
|
|
228
|
+
*
|
|
229
|
+
* This object is shared by reference. `Input` writes to it; `Supermouse` physics reads/writes to it;
|
|
230
|
+
* Plugins read/write to it.
|
|
231
|
+
*/
|
|
232
|
+
state;
|
|
233
|
+
/**
|
|
234
|
+
* Configuration options.
|
|
235
|
+
*/
|
|
236
|
+
options;
|
|
237
|
+
/**
|
|
238
|
+
* Registry of active plugins.
|
|
239
|
+
* @internal Use `use()`, `enablePlugin()`, or `disablePlugin()` to interact with this.
|
|
240
|
+
*/
|
|
241
|
+
plugins = [];
|
|
242
|
+
/**
|
|
243
|
+
* The Stage System responsible for the DOM container and CSS injection.
|
|
244
|
+
* @internal
|
|
245
|
+
*/
|
|
246
|
+
stage;
|
|
247
|
+
/**
|
|
248
|
+
* The Input System responsible for event listeners.
|
|
249
|
+
* @internal
|
|
250
|
+
*/
|
|
251
|
+
input;
|
|
252
|
+
rafId = 0;
|
|
253
|
+
lastTime = 0;
|
|
254
|
+
isRunning = !1;
|
|
255
|
+
hoverSelectors;
|
|
256
|
+
/**
|
|
257
|
+
* Creates a new Supermouse instance.
|
|
258
|
+
*
|
|
259
|
+
* @param options - Global configuration options.
|
|
260
|
+
* @throws Will throw if running in a non-browser environment (window/document undefined).
|
|
261
|
+
*/
|
|
262
|
+
constructor(t = {}) {
|
|
263
|
+
this.options = {
|
|
264
|
+
smoothness: 0.15,
|
|
265
|
+
enableTouch: !1,
|
|
266
|
+
autoDisableOnMobile: !0,
|
|
267
|
+
ignoreOnNative: "auto",
|
|
268
|
+
hideCursor: !0,
|
|
269
|
+
hideOnLeave: !0,
|
|
270
|
+
autoStart: !0,
|
|
271
|
+
container: document.body,
|
|
272
|
+
...t
|
|
273
|
+
}, this.options.container || (this.options.container = document.body), this.state = {
|
|
274
|
+
pointer: { x: -100, y: -100 },
|
|
275
|
+
target: { x: -100, y: -100 },
|
|
276
|
+
smooth: { x: -100, y: -100 },
|
|
277
|
+
velocity: { x: 0, y: 0 },
|
|
278
|
+
angle: 0,
|
|
279
|
+
isDown: !1,
|
|
280
|
+
isHover: !1,
|
|
281
|
+
isNative: !1,
|
|
282
|
+
forcedCursor: null,
|
|
283
|
+
hoverTarget: null,
|
|
284
|
+
reducedMotion: !1,
|
|
285
|
+
hasReceivedInput: !1,
|
|
286
|
+
shape: null,
|
|
287
|
+
interaction: {}
|
|
288
|
+
}, this.options.hoverSelectors ? this.hoverSelectors = new Set(this.options.hoverSelectors) : this.hoverSelectors = new Set(g), this.stage = new c(this.options.container, !!this.options.hideCursor), this.hoverSelectors.forEach((e) => this.stage.addSelector(e)), this.input = new p(
|
|
289
|
+
this.state,
|
|
290
|
+
this.options,
|
|
291
|
+
() => Array.from(this.hoverSelectors).join(", "),
|
|
292
|
+
(e) => {
|
|
293
|
+
e || this.resetPosition();
|
|
294
|
+
}
|
|
295
|
+
), this.options.plugins && this.options.plugins.forEach((e) => this.use(e)), this.init();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Retrieves a registered plugin instance by its unique name.
|
|
299
|
+
*/
|
|
300
|
+
getPlugin(t) {
|
|
301
|
+
return this.plugins.find((e) => e.name === t);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Returns whether the cursor system is currently enabled (processing input).
|
|
305
|
+
*/
|
|
306
|
+
get isEnabled() {
|
|
307
|
+
return this.input.isEnabled;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Enables a specific plugin by name.
|
|
311
|
+
* Triggers the `onEnable` lifecycle hook of the plugin.
|
|
312
|
+
*/
|
|
313
|
+
enablePlugin(t) {
|
|
314
|
+
const e = this.getPlugin(t);
|
|
315
|
+
e && e.isEnabled === !1 && (e.isEnabled = !0, e.onEnable?.(this));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Disables a specific plugin by name.
|
|
319
|
+
* Triggers the `onDisable` lifecycle hook.
|
|
320
|
+
*/
|
|
321
|
+
disablePlugin(t) {
|
|
322
|
+
const e = this.getPlugin(t);
|
|
323
|
+
e && e.isEnabled !== !1 && (e.isEnabled = !1, e.onDisable?.(this));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Toggles the enabled state of a plugin.
|
|
327
|
+
*/
|
|
328
|
+
togglePlugin(t) {
|
|
329
|
+
const e = this.getPlugin(t);
|
|
330
|
+
e && (e.isEnabled === !1 ? this.enablePlugin(t) : this.disablePlugin(t));
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Registers a CSS selector as an "Interactive Target".
|
|
334
|
+
*
|
|
335
|
+
* When the mouse hovers over an element matching this selector:
|
|
336
|
+
* 1. `state.isHover` becomes `true`.
|
|
337
|
+
* 2. `state.hoverTarget` is set to the element.
|
|
338
|
+
* 3. The `Stage` system injects CSS to hide the native cursor for this element (if `hideCursor: true`).
|
|
339
|
+
*
|
|
340
|
+
* @param selector - A valid CSS selector string (e.g., `.my-button`, `[data-trigger]`).
|
|
341
|
+
*/
|
|
342
|
+
registerHoverTarget(t) {
|
|
343
|
+
this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.stage.addSelector(t));
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The fixed container element where plugins should append their DOM nodes.
|
|
347
|
+
*/
|
|
348
|
+
get container() {
|
|
349
|
+
return this.stage.element;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Manually override the native cursor visibility.
|
|
353
|
+
* Useful for drag-and-drop operations, modals, or special UI states.
|
|
354
|
+
*
|
|
355
|
+
* @param type 'auto' (Show Native), 'none' (Hide Native), or null (Resume Auto-detection)
|
|
356
|
+
*/
|
|
357
|
+
setCursor(t) {
|
|
358
|
+
this.state.forcedCursor = t;
|
|
359
|
+
}
|
|
360
|
+
init() {
|
|
361
|
+
this.options.autoStart && this.startLoop();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Starts the update loop and enables input listeners.
|
|
365
|
+
* Hides the native cursor if configured.
|
|
366
|
+
*/
|
|
367
|
+
enable() {
|
|
368
|
+
this.input.isEnabled = !0, this.stage.setNativeCursor("none");
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Stops the update loop, disables listeners, and restores the native cursor.
|
|
372
|
+
* Resets internal state positions to off-screen.
|
|
373
|
+
*/
|
|
374
|
+
disable() {
|
|
375
|
+
this.input.isEnabled = !1, this.stage.setNativeCursor("auto"), this.resetPosition();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Registers a new plugin.
|
|
379
|
+
*
|
|
380
|
+
* @remarks
|
|
381
|
+
* Plugins are sorted by `priority` immediately after registration.
|
|
382
|
+
* - **Negative Priority (< 0)**: Logic plugins (run before physics).
|
|
383
|
+
* - **Positive Priority (>= 0)**: Visual plugins (run after physics).
|
|
384
|
+
*
|
|
385
|
+
* @param plugin - The plugin object to install.
|
|
386
|
+
*/
|
|
387
|
+
use(t) {
|
|
388
|
+
return this.plugins.find((e) => e.name === t.name) ? (console.warn(`[Supermouse] Plugin "${t.name}" already installed.`), this) : (t.isEnabled === void 0 && (t.isEnabled = !0), this.plugins.push(t), this.plugins.sort((e, s) => (e.priority || 0) - (s.priority || 0)), t.install?.(this), this);
|
|
389
|
+
}
|
|
390
|
+
resetPosition() {
|
|
391
|
+
const t = { x: -100, y: -100 };
|
|
392
|
+
this.state.pointer = { ...t }, this.state.target = { ...t }, this.state.smooth = { ...t }, this.state.velocity = { x: 0, y: 0 }, this.state.angle = 0, this.state.hasReceivedInput = !1, this.state.shape = null, this.state.interaction = {};
|
|
393
|
+
}
|
|
394
|
+
startLoop() {
|
|
395
|
+
this.isRunning || (this.isRunning = !0, this.lastTime = performance.now(), this.tick(this.lastTime));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Manually steps the animation loop.
|
|
399
|
+
* Useful when integrating with external game loops (e.g., Three.js, PixiJS) where
|
|
400
|
+
* you want to disable the internal RAF and drive `Supermouse` from your own ticker.
|
|
401
|
+
*
|
|
402
|
+
* @param time Current timestamp in milliseconds.
|
|
403
|
+
*/
|
|
404
|
+
step(t) {
|
|
405
|
+
this.tick(t);
|
|
406
|
+
}
|
|
407
|
+
runPluginSafe(t, e) {
|
|
408
|
+
if (t.isEnabled !== !1)
|
|
409
|
+
try {
|
|
410
|
+
t.update?.(this, e);
|
|
411
|
+
} catch (s) {
|
|
412
|
+
console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, s), t.isEnabled = !1, t.onDisable?.(this);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The Heartbeat.
|
|
417
|
+
* Runs on every animation frame.
|
|
418
|
+
*/
|
|
419
|
+
tick = (t) => {
|
|
420
|
+
const e = t - this.lastTime, s = Math.min(e / 1e3, 0.1);
|
|
421
|
+
this.lastTime = t;
|
|
422
|
+
const n = this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
|
|
423
|
+
if (this.stage.setVisibility(n), this.input.isEnabled && this.options.hideCursor) {
|
|
424
|
+
let i = "auto";
|
|
425
|
+
this.state.forcedCursor !== null ? i = this.state.forcedCursor : i = this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none", this.stage.setNativeCursor(i);
|
|
426
|
+
}
|
|
427
|
+
if (this.input.isEnabled) {
|
|
428
|
+
this.state.target.x = this.state.pointer.x, this.state.target.y = this.state.pointer.y;
|
|
429
|
+
for (let a = 0; a < this.plugins.length; a++)
|
|
430
|
+
this.runPluginSafe(this.plugins[a], e);
|
|
431
|
+
const i = this.options.smoothness, l = this.state.reducedMotion ? 1e3 : 1 / i * 2;
|
|
432
|
+
this.state.smooth.x = u(this.state.smooth.x, this.state.target.x, l, s), this.state.smooth.y = u(this.state.smooth.y, this.state.target.y, l, s);
|
|
433
|
+
const r = this.state.target.x - this.state.smooth.x, h = this.state.target.y - this.state.smooth.y;
|
|
434
|
+
this.state.velocity.x = r, this.state.velocity.y = h, (Math.abs(r) > 0.1 || Math.abs(h) > 0.1) && (this.state.angle = f(r, h));
|
|
435
|
+
} else {
|
|
436
|
+
this.state.smooth.x = -100, this.state.smooth.y = -100, this.state.pointer.x = -100, this.state.pointer.y = -100, this.state.velocity.x = 0, this.state.velocity.y = 0;
|
|
437
|
+
for (let i = 0; i < this.plugins.length; i++)
|
|
438
|
+
this.runPluginSafe(this.plugins[i], e);
|
|
439
|
+
}
|
|
440
|
+
this.options.autoStart && this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* Destroys the instance.
|
|
444
|
+
* Stops the loop, removes all DOM elements, removes all event listeners, and calls destroy on all plugins.
|
|
445
|
+
*/
|
|
446
|
+
destroy() {
|
|
447
|
+
this.isRunning = !1, cancelAnimationFrame(this.rafId), this.input.destroy(), this.stage.destroy(), this.plugins.forEach((t) => t.destroy?.(this)), this.plugins = [];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
export {
|
|
451
|
+
g as DEFAULT_HOVER_SELECTORS,
|
|
452
|
+
y as Supermouse
|
|
453
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
(function(u,d){typeof exports=="object"&&typeof module<"u"?d(exports):typeof define=="function"&&define.amd?define(["exports"],d):(u=typeof globalThis<"u"?globalThis:u||self,d(u.SupermouseCore={}))})(this,(function(u){"use strict";let d=0;class f{constructor(t=document.body,e){if(this.container=t,this.hideNativeCursor=e,!t||!(t instanceof HTMLElement))throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);const s=d++;this.id=`supermouse-style-${s}`,this.scopeClass=`supermouse-scope-${s}`;const n=t===document.body;this.element=document.createElement("div"),Object.assign(this.element.style,{position:n?"fixed":"absolute",top:"0",left:"0",width:"100%",height:"100%",pointerEvents:"none",zIndex:"9999",opacity:"1",transition:"opacity 0.15s ease"}),n||window.getComputedStyle(t).position==="static"&&(t.style.position="relative"),t.appendChild(this.element),this.styleTag=document.createElement("style"),this.styleTag.id=this.id,document.head.appendChild(this.styleTag),this.container.classList.add(this.scopeClass),this.hideNativeCursor&&this.setNativeCursor("none")}element;styleTag;id;scopeClass;currentCursorState=null;selectors=new Set(["a","button","input","textarea","select",'[role="button"]',"[tabindex]"]);addSelector(t){this.selectors.add(t),this.hideNativeCursor&&this.updateCursorCSS()}setVisibility(t){this.element.style.opacity=t?"1":"0"}setNativeCursor(t){!this.hideNativeCursor&&t==="none"||t!==this.currentCursorState&&(this.currentCursorState=t,t==="none"?(this.container.style.cursor="none",this.updateCursorCSS()):(this.container.style.cursor="",this.styleTag.innerText=""))}updateCursorCSS(){const t=Array.from(this.selectors);if(t.length===0){this.styleTag.innerText="";return}const e=t.map(s=>`.${this.scopeClass} ${s}`).join(", ");this.styleTag.innerText=`
|
|
2
|
+
${e} {
|
|
3
|
+
cursor: none !important;
|
|
4
|
+
}
|
|
5
|
+
`}destroy(){this.element.remove(),this.styleTag.remove(),this.container.style.cursor="",this.container.classList.remove(this.scopeClass)}}class v{constructor(t,e,s,n){this.state=t,this.options=e,this.getHoverSelector=s,this.onEnableChange=n,this.checkDeviceCapability(),this.checkMotionPreference(),this.bindEvents()}mediaQueryList;mediaQueryHandler;motionQuery;isEnabled=!0;interactionCache=new WeakMap;checkDeviceCapability(){this.options.autoDisableOnMobile&&(this.mediaQueryList=window.matchMedia("(pointer: fine)"),this.updateEnabledState(this.mediaQueryList.matches),this.mediaQueryHandler=t=>{this.updateEnabledState(t.matches)},this.mediaQueryList.addEventListener("change",this.mediaQueryHandler))}checkMotionPreference(){this.motionQuery=window.matchMedia("(prefer-reduced-motion: reduce)"),this.state.reducedMotion=this.motionQuery.matches,this.motionQuery.addEventListener("change",t=>{this.state.reducedMotion=t.matches})}updateEnabledState(t){this.isEnabled=t,this.onEnableChange(t)}parseDOMInteraction(t){if(this.options.resolveInteraction){this.state.interaction=this.options.resolveInteraction(t);return}if(this.interactionCache.has(t)){this.state.interaction=this.interactionCache.get(t);return}const e={};if(this.options.rules)for(const[n,i]of Object.entries(this.options.rules))t.matches(n)&&Object.assign(e,i);const s=t.dataset;for(const n in s)if(n.startsWith("supermouse")){const i=n.slice(10);if(i){const l=i.charAt(0).toLowerCase()+i.slice(1),r=s[n];e[l]=r===""?!0:r}}this.interactionCache.set(t,e),this.state.interaction=e}handleMove=t=>{if(!this.isEnabled||this.options.autoDisableOnMobile&&t.pointerType==="touch")return;let e=t.clientX,s=t.clientY;if(this.options.container&&this.options.container!==document.body){const n=this.options.container.getBoundingClientRect();e-=n.left,s-=n.top}this.state.pointer.x=e,this.state.pointer.y=s,this.state.hasReceivedInput||(this.state.hasReceivedInput=!0,this.state.target.x=this.state.smooth.x=e,this.state.target.y=this.state.smooth.y=s)};handleDown=()=>{this.isEnabled&&(this.state.isDown=!0)};handleUp=()=>{this.isEnabled&&(this.state.isDown=!1)};handleMouseOver=t=>{if(!this.isEnabled)return;const e=t.target;if(e.closest("[data-supermouse-ignore]")){this.state.isNative=!0;return}const s=this.getHoverSelector(),n=e.closest(s);n&&(this.state.isHover=!0,this.state.hoverTarget=n,this.parseDOMInteraction(this.state.hoverTarget));const i=this.options.ignoreOnNative;if(i){const l=i===!0||i==="auto"||i==="tag",r=i===!0||i==="auto"||i==="css";let h=!1;if(l){const a=e.localName;(a==="input"||a==="textarea"||a==="select"||e.isContentEditable)&&(h=!0)}if(!h&&r){const a=window.getComputedStyle(e).cursor;["default","auto","pointer","none","inherit"].includes(a)||(h=!0)}h&&(this.state.isNative=!0)}};handleMouseOut=t=>{if(!this.isEnabled)return;const e=t.target;(e===this.state.hoverTarget||e.contains(this.state.hoverTarget))&&(!t.relatedTarget||!this.state.hoverTarget?.contains(t.relatedTarget))&&(this.state.isHover=!1,this.state.hoverTarget=null,this.state.interaction={}),this.state.isNative&&(this.state.isNative=!1)};handleWindowLeave=()=>{this.options.hideOnLeave&&(this.state.hasReceivedInput=!1)};bindEvents(){window.addEventListener("pointermove",this.handleMove,{passive:!0}),window.addEventListener("pointerdown",this.handleDown,{passive:!0}),window.addEventListener("pointerup",this.handleUp),document.addEventListener("mouseover",this.handleMouseOver),document.addEventListener("mouseout",this.handleMouseOut),document.addEventListener("mouseleave",this.handleWindowLeave)}destroy(){this.mediaQueryList&&this.mediaQueryHandler&&this.mediaQueryList.removeEventListener("change",this.mediaQueryHandler),window.removeEventListener("pointermove",this.handleMove),window.removeEventListener("pointerdown",this.handleDown),window.removeEventListener("pointerup",this.handleUp),document.removeEventListener("mouseover",this.handleMouseOver),document.removeEventListener("mouseout",this.handleMouseOut),document.removeEventListener("mouseleave",this.handleWindowLeave)}}function m(o,t,e){return o+(t-o)*e}function c(o,t,e,s){return m(o,t,1-Math.exp(-e*s))}function g(o,t){return Math.atan2(t,o)*(180/Math.PI)}const p=["a","button","input","textarea","[data-hover]","[data-cursor]"];class y{static version="2.0.0";version="2.0.0";state;options;plugins=[];stage;input;rafId=0;lastTime=0;isRunning=!1;hoverSelectors;constructor(t={}){this.options={smoothness:.15,enableTouch:!1,autoDisableOnMobile:!0,ignoreOnNative:"auto",hideCursor:!0,hideOnLeave:!0,autoStart:!0,container:document.body,...t},this.options.container||(this.options.container=document.body),this.state={pointer:{x:-100,y:-100},target:{x:-100,y:-100},smooth:{x:-100,y:-100},velocity:{x:0,y:0},angle:0,isDown:!1,isHover:!1,isNative:!1,forcedCursor:null,hoverTarget:null,reducedMotion:!1,hasReceivedInput:!1,shape:null,interaction:{}},this.options.hoverSelectors?this.hoverSelectors=new Set(this.options.hoverSelectors):this.hoverSelectors=new Set(p),this.stage=new f(this.options.container,!!this.options.hideCursor),this.hoverSelectors.forEach(e=>this.stage.addSelector(e)),this.input=new v(this.state,this.options,()=>Array.from(this.hoverSelectors).join(", "),e=>{e||this.resetPosition()}),this.options.plugins&&this.options.plugins.forEach(e=>this.use(e)),this.init()}getPlugin(t){return this.plugins.find(e=>e.name===t)}get isEnabled(){return this.input.isEnabled}enablePlugin(t){const e=this.getPlugin(t);e&&e.isEnabled===!1&&(e.isEnabled=!0,e.onEnable?.(this))}disablePlugin(t){const e=this.getPlugin(t);e&&e.isEnabled!==!1&&(e.isEnabled=!1,e.onDisable?.(this))}togglePlugin(t){const e=this.getPlugin(t);e&&(e.isEnabled===!1?this.enablePlugin(t):this.disablePlugin(t))}registerHoverTarget(t){this.hoverSelectors.has(t)||(this.hoverSelectors.add(t),this.stage.addSelector(t))}get container(){return this.stage.element}setCursor(t){this.state.forcedCursor=t}init(){this.options.autoStart&&this.startLoop()}enable(){this.input.isEnabled=!0,this.stage.setNativeCursor("none")}disable(){this.input.isEnabled=!1,this.stage.setNativeCursor("auto"),this.resetPosition()}use(t){return this.plugins.find(e=>e.name===t.name)?(console.warn(`[Supermouse] Plugin "${t.name}" already installed.`),this):(t.isEnabled===void 0&&(t.isEnabled=!0),this.plugins.push(t),this.plugins.sort((e,s)=>(e.priority||0)-(s.priority||0)),t.install?.(this),this)}resetPosition(){const t={x:-100,y:-100};this.state.pointer={...t},this.state.target={...t},this.state.smooth={...t},this.state.velocity={x:0,y:0},this.state.angle=0,this.state.hasReceivedInput=!1,this.state.shape=null,this.state.interaction={}}startLoop(){this.isRunning||(this.isRunning=!0,this.lastTime=performance.now(),this.tick(this.lastTime))}step(t){this.tick(t)}runPluginSafe(t,e){if(t.isEnabled!==!1)try{t.update?.(this,e)}catch(s){console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`,s),t.isEnabled=!1,t.onDisable?.(this)}}tick=t=>{const e=t-this.lastTime,s=Math.min(e/1e3,.1);this.lastTime=t;const n=this.input.isEnabled&&!this.state.isNative&&this.state.hasReceivedInput;if(this.stage.setVisibility(n),this.input.isEnabled&&this.options.hideCursor){let i="auto";this.state.forcedCursor!==null?i=this.state.forcedCursor:i=this.state.isNative||!this.state.hasReceivedInput?"auto":"none",this.stage.setNativeCursor(i)}if(this.input.isEnabled){this.state.target.x=this.state.pointer.x,this.state.target.y=this.state.pointer.y;for(let a=0;a<this.plugins.length;a++)this.runPluginSafe(this.plugins[a],e);const i=this.options.smoothness,l=this.state.reducedMotion?1e3:1/i*2;this.state.smooth.x=c(this.state.smooth.x,this.state.target.x,l,s),this.state.smooth.y=c(this.state.smooth.y,this.state.target.y,l,s);const r=this.state.target.x-this.state.smooth.x,h=this.state.target.y-this.state.smooth.y;this.state.velocity.x=r,this.state.velocity.y=h,(Math.abs(r)>.1||Math.abs(h)>.1)&&(this.state.angle=g(r,h))}else{this.state.smooth.x=-100,this.state.smooth.y=-100,this.state.pointer.x=-100,this.state.pointer.y=-100,this.state.velocity.x=0,this.state.velocity.y=0;for(let i=0;i<this.plugins.length;i++)this.runPluginSafe(this.plugins[i],e)}this.options.autoStart&&this.isRunning&&(this.rafId=requestAnimationFrame(this.tick))};destroy(){this.isRunning=!1,cancelAnimationFrame(this.rafId),this.input.destroy(),this.stage.destroy(),this.plugins.forEach(t=>t.destroy?.(this)),this.plugins=[]}}u.DEFAULT_HOVER_SELECTORS=p,u.Supermouse=y,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supermousejs/core",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"main": "dist/index.umd.js",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"dependencies": {},
|
|
8
|
+
"devDependencies": {},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.umd.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"test": "vitest"
|
|
22
|
+
}
|
|
23
|
+
}
|