@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.
@@ -1,262 +0,0 @@
1
-
2
- import { MouseState, SupermouseOptions, InteractionState } from '../types';
3
-
4
- /**
5
- * The Sensor / State Writer.
6
- *
7
- * This class listens to browser events (pointermove, pointerdown, hover) and mutates the shared `MouseState` object.
8
- * It acts as the "Producer" of data for the Supermouse system.
9
- *
10
- * @internal This is an internal system class instantiated by `Supermouse`.
11
- */
12
- export class Input {
13
- private mediaQueryList?: MediaQueryList;
14
- private mediaQueryHandler?: (e: MediaQueryListEvent) => void;
15
- private motionQuery?: MediaQueryList;
16
-
17
- /**
18
- * Master switch for input processing.
19
- * Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
20
- */
21
- public isEnabled: boolean = true;
22
-
23
- /**
24
- * Performance Optimization:
25
- * We cache the resolved InteractionState for every element we encounter in a WeakMap.
26
- */
27
- private interactionCache = new WeakMap<HTMLElement, InteractionState>();
28
-
29
- constructor(
30
- private state: MouseState,
31
- private options: SupermouseOptions,
32
- private getHoverSelector: () => string,
33
- private onEnableChange: (enabled: boolean) => void
34
- ) {
35
- this.checkDeviceCapability();
36
- this.checkMotionPreference();
37
- this.bindEvents();
38
- }
39
-
40
- /**
41
- * Automatically disables the custom cursor on devices without fine pointer control (e.g. phones/tablets).
42
- * Relies on `matchMedia('(pointer: fine)')`.
43
- */
44
- private checkDeviceCapability() {
45
- if (!this.options.autoDisableOnMobile) return;
46
-
47
- this.mediaQueryList = window.matchMedia('(pointer: fine)');
48
- this.updateEnabledState(this.mediaQueryList.matches);
49
-
50
- this.mediaQueryHandler = (e: MediaQueryListEvent) => {
51
- this.updateEnabledState(e.matches);
52
- };
53
- this.mediaQueryList.addEventListener('change', this.mediaQueryHandler);
54
- }
55
-
56
- /**
57
- * Checks for `prefers-reduced-motion`.
58
- * If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
59
- */
60
- private checkMotionPreference() {
61
- this.motionQuery = window.matchMedia('(prefer-reduced-motion: reduce)');
62
- this.state.reducedMotion = this.motionQuery.matches;
63
-
64
- this.motionQuery.addEventListener('change', e => {
65
- this.state.reducedMotion = e.matches;
66
- });
67
- }
68
-
69
- private updateEnabledState(enabled: boolean) {
70
- this.isEnabled = enabled;
71
- this.onEnableChange(enabled);
72
- }
73
-
74
- // --- Interaction Parsing ---
75
-
76
- /**
77
- * Scrapes the DOM element for metadata to populate `state.interaction`.
78
- *
79
- * **Strategy:**
80
- * 1. Check WeakMap cache.
81
- * 2. Apply config-based `rules`.
82
- * 3. Scrape `dataset` (supermouse*).
83
- * 4. Cache result.
84
- */
85
- private parseDOMInteraction(element: HTMLElement) {
86
- // 0. Custom Strategy override
87
- if (this.options.resolveInteraction) {
88
- this.state.interaction = this.options.resolveInteraction(element);
89
- return;
90
- }
91
-
92
- // 1. Check Cache
93
- if (this.interactionCache.has(element)) {
94
- this.state.interaction = this.interactionCache.get(element)!;
95
- return;
96
- }
97
-
98
- const data: Record<string, any> = {};
99
-
100
- // 2. Semantic Rules (Config-based)
101
- if (this.options.rules) {
102
- for (const [selector, rules] of Object.entries(this.options.rules)) {
103
- if (element.matches(selector)) {
104
- Object.assign(data, rules);
105
- }
106
- }
107
- }
108
-
109
- // 3. Dataset Scraping (Fast Native)
110
- // Converts data-supermouse-my-val -> myVal
111
- const dataset = element.dataset;
112
- for (const key in dataset) {
113
- if (key.startsWith('supermouse')) {
114
- // 'supermouseColor' -> 'color'
115
- // 'supermouseStick' -> 'stick'
116
- const prop = key.slice(10);
117
- if (prop) {
118
- const cleanKey = prop.charAt(0).toLowerCase() + prop.slice(1);
119
- const val = dataset[key];
120
- // Treat empty string (bool attribute) as true
121
- data[cleanKey] = val === '' ? true : val;
122
- }
123
- }
124
- }
125
-
126
- // 4. Cache and Set
127
- this.interactionCache.set(element, data);
128
- this.state.interaction = data;
129
- }
130
-
131
- // --- Handlers ---
132
-
133
- // Unified Pointer Event Handler
134
- private handleMove = (e: PointerEvent) => {
135
- if (!this.isEnabled) return;
136
-
137
- // Ignore non-mouse inputs if not on touch device (e.g. pen hovering but not touching)
138
- // unless we strictly want to track everything. PointerType check is robust.
139
- if (this.options.autoDisableOnMobile && e.pointerType === 'touch') return;
140
-
141
- let x = e.clientX;
142
- let y = e.clientY;
143
-
144
- // Handle Custom Container Coordinates
145
- if (this.options.container && this.options.container !== document.body) {
146
- const rect = this.options.container.getBoundingClientRect();
147
- x -= rect.left;
148
- y -= rect.top;
149
- }
150
-
151
- this.state.pointer.x = x;
152
- this.state.pointer.y = y;
153
-
154
- if (!this.state.hasReceivedInput) {
155
- this.state.hasReceivedInput = true;
156
- this.state.target.x = this.state.smooth.x = x;
157
- this.state.target.y = this.state.smooth.y = y;
158
- }
159
- };
160
-
161
- private handleDown = () => { if (this.isEnabled) this.state.isDown = true; };
162
- private handleUp = () => { if (this.isEnabled) this.state.isDown = false; };
163
-
164
- private handleMouseOver = (e: MouseEvent) => {
165
- if (!this.isEnabled) return;
166
- const target = e.target as HTMLElement;
167
-
168
- // 0. THE VETO: Explicit Ignore
169
- if (target.closest('[data-supermouse-ignore]')) {
170
- this.state.isNative = true;
171
- return;
172
- }
173
-
174
- // 1. Dynamic Hover Check
175
- const selector = this.getHoverSelector();
176
- const hoverable = target.closest(selector);
177
-
178
- if (hoverable) {
179
- this.state.isHover = true;
180
- this.state.hoverTarget = hoverable as HTMLElement;
181
- this.parseDOMInteraction(this.state.hoverTarget);
182
- }
183
-
184
- // 2. Semantic Native Cursor Check (Configurable Strategy)
185
- const strategy = this.options.ignoreOnNative;
186
-
187
- if (strategy) {
188
- const checkTags = strategy === true || strategy === 'auto' || strategy === 'tag';
189
- const checkCSS = strategy === true || strategy === 'auto' || strategy === 'css';
190
- let isNative = false;
191
-
192
- // A. Tag Check (Fast, O(1))
193
- if (checkTags) {
194
- // Check localName for speed (always lowercase)
195
- const tag = target.localName;
196
- if (tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable) {
197
- isNative = true;
198
- }
199
- }
200
-
201
- // B. CSS Check (Slow, causes Layout Reflow)
202
- // Only run if not already detected and strategy allows it.
203
- if (!isNative && checkCSS) {
204
- const style = window.getComputedStyle(target).cursor;
205
- const supermouseAllowed = ['default', 'auto', 'pointer', 'none', 'inherit'];
206
- if (!supermouseAllowed.includes(style)) {
207
- isNative = true;
208
- }
209
- }
210
-
211
- if (isNative) {
212
- this.state.isNative = true;
213
- }
214
- }
215
- };
216
-
217
- private handleMouseOut = (e: MouseEvent) => {
218
- if (!this.isEnabled) return;
219
- const target = e.target as HTMLElement;
220
-
221
- if (target === this.state.hoverTarget || target.contains(this.state.hoverTarget)) {
222
- if (!e.relatedTarget || !(this.state.hoverTarget?.contains(e.relatedTarget as Node))) {
223
- this.state.isHover = false;
224
- this.state.hoverTarget = null;
225
- this.state.interaction = {};
226
- }
227
- }
228
-
229
- if (this.state.isNative) {
230
- this.state.isNative = false;
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,155 +0,0 @@
1
- let stageCount = 0;
2
-
3
- /**
4
- * The Environment / DOM Manager.
5
- *
6
- * This class handles the DOM container where the cursor lives and manages the global CSS
7
- * required to hide the default OS cursor without flickering.
8
- *
9
- * ## Why CSS Injection?
10
- * Simply adding `cursor: none` to the body isn't enough. Interactive elements like `<input>`
11
- * or `<a>` often have their own user-agent styles that force `cursor: text` or `cursor: pointer`.
12
- * This results in the "double cursor" glitch.
13
- *
14
- * The Stage system generates a scoped stylesheet that aggressively targets registered selectors
15
- * with `cursor: none !important` to ensure a seamless experience.
16
- *
17
- * @internal This is an internal system class instantiated by `Supermouse`.
18
- */
19
- export class Stage {
20
- /** The container element appended to the document. Plugins must append here. */
21
- public readonly element: HTMLDivElement;
22
-
23
- private styleTag: HTMLStyleElement;
24
- private id: string;
25
- private scopeClass: string;
26
-
27
- // Cache to prevent redundant DOM updates
28
- private currentCursorState: 'none' | 'auto' | '' | null = null;
29
-
30
- // Defaults for CSS hiding. We must override user-agent styles on these elements
31
- // to prevent the native cursor from popping through.
32
- private selectors: Set<string> = new Set([
33
- 'a', 'button', 'input', 'textarea', 'select',
34
- '[role="button"]', '[tabindex]'
35
- ]);
36
-
37
- constructor(private container: HTMLElement = document.body, private hideNativeCursor: boolean) {
38
- if (!container || !(container instanceof HTMLElement)) {
39
- throw new Error(`[Supermouse] Invalid container: ${container}. Must be an HTMLElement.`);
40
- }
41
-
42
- const instanceId = stageCount++;
43
- this.id = `supermouse-style-${instanceId}`;
44
- this.scopeClass = `supermouse-scope-${instanceId}`;
45
-
46
- const isBody = container === document.body;
47
-
48
- // 1. Create Container
49
- // If attached to body, use fixed positioning to cover viewport.
50
- // If attached to a specific div, use absolute positioning to cover that div.
51
- this.element = document.createElement('div');
52
- Object.assign(this.element.style, {
53
- position: isBody ? 'fixed' : 'absolute',
54
- top: '0', left: '0', width: '100%', height: '100%',
55
- pointerEvents: 'none',
56
- zIndex: '9999',
57
- opacity: '1',
58
- transition: 'opacity 0.15s ease'
59
- });
60
-
61
- // Ensure parent is relative if we are using absolute positioning
62
- if (!isBody) {
63
- const computed = window.getComputedStyle(container);
64
- if (computed.position === 'static') {
65
- container.style.position = 'relative';
66
- }
67
- }
68
-
69
- container.appendChild(this.element);
70
-
71
- // 2. Create Dynamic Style Tag
72
- this.styleTag = document.createElement('style');
73
- this.styleTag.id = this.id;
74
- document.head.appendChild(this.styleTag);
75
-
76
- // 3. Apply Scope Class to Container
77
- // This allows us to target CSS purely within this container (or body)
78
- // preventing styles from leaking if multiple Supermouse instances exist.
79
- this.container.classList.add(this.scopeClass);
80
-
81
- // 4. Apply Initial Cursor State
82
- if (this.hideNativeCursor) {
83
- this.setNativeCursor('none');
84
- }
85
- }
86
-
87
- /**
88
- * Adds a new CSS selector to the "Hide Native Cursor" list.
89
- * Called by `Supermouse` (and subsequently plugins) during install to ensure
90
- * the native cursor is hidden on their specific interactive targets.
91
- */
92
- public addSelector(selector: string) {
93
- this.selectors.add(selector);
94
- if (this.hideNativeCursor) {
95
- this.updateCursorCSS();
96
- }
97
- }
98
-
99
- /**
100
- * Controls the opacity of the entire stage (all custom cursor elements).
101
- */
102
- public setVisibility(visible: boolean) {
103
- this.element.style.opacity = visible ? '1' : '0';
104
- }
105
-
106
- /**
107
- * Toggles the visibility of the native cursor via CSS injection.
108
- * @param type 'none' to hide, 'auto' to show.
109
- */
110
- public setNativeCursor(type: 'none' | 'auto' | '') {
111
- // If global hiding is disabled via options, do nothing (unless specifically forcing auto)
112
- if (!this.hideNativeCursor && type === 'none') return;
113
-
114
- // PERFORMANCE FIX: Don't touch DOM if state hasn't changed
115
- if (type === this.currentCursorState) return;
116
- this.currentCursorState = type;
117
-
118
- if (type === 'none') {
119
- // 1. Hide on container directly (covers background/empty space)
120
- this.container.style.cursor = 'none';
121
- // 2. Hide on interactive elements (overrides UA stylesheet)
122
- this.updateCursorCSS();
123
- } else {
124
- this.container.style.cursor = '';
125
- this.styleTag.innerText = '';
126
- }
127
- }
128
-
129
- private updateCursorCSS() {
130
- const rawSelectors = Array.from(this.selectors);
131
- if (rawSelectors.length === 0) {
132
- this.styleTag.innerText = '';
133
- return;
134
- }
135
-
136
- // Scoped Selector Logic:
137
- // We prepend the scope class to every selector to ensure we don't bleed into
138
- // other parts of the page if the user is using a specific container.
139
- // e.g. .supermouse-scope-0 a, .supermouse-scope-0 button { ... }
140
- const scopedSelectors = rawSelectors.map(s => `.${this.scopeClass} ${s}`).join(', ');
141
-
142
- this.styleTag.innerText = `
143
- ${scopedSelectors} {
144
- cursor: none !important;
145
- }
146
- `;
147
- }
148
-
149
- public destroy() {
150
- this.element.remove();
151
- this.styleTag.remove();
152
- this.container.style.cursor = '';
153
- this.container.classList.remove(this.scopeClass);
154
- }
155
- }
@@ -1,2 +0,0 @@
1
- export * from './Stage';
2
- export * from './Input';
package/src/utils/math.ts DELETED
@@ -1,20 +0,0 @@
1
- /**
2
- * Linear Interpolation between two values.
3
- */
4
- export function lerp(start: number, end: number, factor: number): number {
5
- return start + (end - start) * factor;
6
- }
7
-
8
- /**
9
- * Frame-rate independent damping (Time-based Lerp).
10
- */
11
- export function damp(a: number, b: number, lambda: number, dt: number): number {
12
- return lerp(a, b, 1 - Math.exp(-lambda * dt));
13
- }
14
-
15
- /**
16
- * Calculates the angle in degrees between two points.
17
- */
18
- export function angle(x: number, y: number): number {
19
- return Math.atan2(y, x) * (180 / Math.PI);
20
- }