@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
|
|
2
|
+
let stageCount = 0;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Environment / DOM Manager.
|
|
6
|
+
*
|
|
7
|
+
* This class handles the DOM container where the cursor lives and manages the global CSS
|
|
8
|
+
* required to hide the default OS cursor without flickering.
|
|
9
|
+
*
|
|
10
|
+
* ## Why CSS Injection?
|
|
11
|
+
* Simply adding `cursor: none` to the body isn't enough. Interactive elements like `<input>`
|
|
12
|
+
* or `<a>` often have their own user-agent styles that force `cursor: text` or `cursor: pointer`.
|
|
13
|
+
* This results in the "double cursor" glitch.
|
|
14
|
+
*
|
|
15
|
+
* The Stage system generates a scoped stylesheet that aggressively targets registered selectors
|
|
16
|
+
* with `cursor: none !important` to ensure a seamless experience.
|
|
17
|
+
*
|
|
18
|
+
* @internal This is an internal system class instantiated by `Supermouse`.
|
|
19
|
+
*/
|
|
20
|
+
export class Stage {
|
|
21
|
+
/** The container element appended to the document. Plugins must append here. */
|
|
22
|
+
public readonly element: HTMLDivElement;
|
|
23
|
+
|
|
24
|
+
private styleTag: HTMLStyleElement;
|
|
25
|
+
private id: string;
|
|
26
|
+
private scopeClass: string;
|
|
27
|
+
|
|
28
|
+
// Cache to prevent redundant DOM updates
|
|
29
|
+
private currentCursorState: 'none' | 'auto' | '' | null = null;
|
|
30
|
+
|
|
31
|
+
// Defaults for CSS hiding. We must override user-agent styles on these elements
|
|
32
|
+
// to prevent the native cursor from popping through.
|
|
33
|
+
private selectors: Set<string> = new Set([
|
|
34
|
+
'a', 'button', 'input', 'textarea', 'select',
|
|
35
|
+
'[role="button"]', '[tabindex]'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
constructor(private container: HTMLElement = document.body, private hideNativeCursor: boolean) {
|
|
39
|
+
if (!container || !(container instanceof HTMLElement)) {
|
|
40
|
+
throw new Error(`[Supermouse] Invalid container: ${container}. Must be an HTMLElement.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const instanceId = stageCount++;
|
|
44
|
+
this.id = `supermouse-style-${instanceId}`;
|
|
45
|
+
this.scopeClass = `supermouse-scope-${instanceId}`;
|
|
46
|
+
|
|
47
|
+
const isBody = container === document.body;
|
|
48
|
+
|
|
49
|
+
// 1. Create Container
|
|
50
|
+
// If attached to body, use fixed positioning to cover viewport.
|
|
51
|
+
// If attached to a specific div, use absolute positioning to cover that div.
|
|
52
|
+
this.element = document.createElement('div');
|
|
53
|
+
Object.assign(this.element.style, {
|
|
54
|
+
position: isBody ? 'fixed' : 'absolute',
|
|
55
|
+
top: '0', left: '0', width: '100%', height: '100%',
|
|
56
|
+
pointerEvents: 'none',
|
|
57
|
+
zIndex: '9999',
|
|
58
|
+
opacity: '1',
|
|
59
|
+
transition: 'opacity 0.15s ease'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Ensure parent is relative if we are using absolute positioning
|
|
63
|
+
if (!isBody) {
|
|
64
|
+
const computed = window.getComputedStyle(container);
|
|
65
|
+
if (computed.position === 'static') {
|
|
66
|
+
container.style.position = 'relative';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
container.appendChild(this.element);
|
|
71
|
+
|
|
72
|
+
// 2. Create Dynamic Style Tag
|
|
73
|
+
this.styleTag = document.createElement('style');
|
|
74
|
+
this.styleTag.id = this.id;
|
|
75
|
+
document.head.appendChild(this.styleTag);
|
|
76
|
+
|
|
77
|
+
// 3. Apply Scope Class to Container
|
|
78
|
+
// This allows us to target CSS purely within this container (or body)
|
|
79
|
+
// preventing styles from leaking if multiple Supermouse instances exist.
|
|
80
|
+
this.container.classList.add(this.scopeClass);
|
|
81
|
+
|
|
82
|
+
// 4. Apply Initial Cursor State
|
|
83
|
+
if (this.hideNativeCursor) {
|
|
84
|
+
this.setNativeCursor('none');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Adds a new CSS selector to the "Hide Native Cursor" list.
|
|
90
|
+
* Called by `Supermouse` (and subsequently plugins) during install to ensure
|
|
91
|
+
* the native cursor is hidden on their specific interactive targets.
|
|
92
|
+
*/
|
|
93
|
+
public addSelector(selector: string) {
|
|
94
|
+
this.selectors.add(selector);
|
|
95
|
+
if (this.hideNativeCursor) {
|
|
96
|
+
this.updateCursorCSS();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Controls the opacity of the entire stage (all custom cursor elements).
|
|
102
|
+
*/
|
|
103
|
+
public setVisibility(visible: boolean) {
|
|
104
|
+
this.element.style.opacity = visible ? '1' : '0';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Toggles the visibility of the native cursor via CSS injection.
|
|
109
|
+
* @param type 'none' to hide, 'auto' to show.
|
|
110
|
+
*/
|
|
111
|
+
public setNativeCursor(type: 'none' | 'auto' | '') {
|
|
112
|
+
// If global hiding is disabled via options, do nothing (unless specifically forcing auto)
|
|
113
|
+
if (!this.hideNativeCursor && type === 'none') return;
|
|
114
|
+
|
|
115
|
+
// PERFORMANCE FIX: Don't touch DOM if state hasn't changed
|
|
116
|
+
if (type === this.currentCursorState) return;
|
|
117
|
+
this.currentCursorState = type;
|
|
118
|
+
|
|
119
|
+
if (type === 'none') {
|
|
120
|
+
// 1. Hide on container directly (covers background/empty space)
|
|
121
|
+
this.container.style.cursor = 'none';
|
|
122
|
+
// 2. Hide on interactive elements (overrides UA stylesheet)
|
|
123
|
+
this.updateCursorCSS();
|
|
124
|
+
} else {
|
|
125
|
+
this.container.style.cursor = '';
|
|
126
|
+
this.styleTag.innerText = '';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private updateCursorCSS() {
|
|
131
|
+
const rawSelectors = Array.from(this.selectors);
|
|
132
|
+
if (rawSelectors.length === 0) {
|
|
133
|
+
this.styleTag.innerText = '';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Scoped Selector Logic:
|
|
138
|
+
// We prepend the scope class to every selector to ensure we don't bleed into
|
|
139
|
+
// other parts of the page if the user is using a specific container.
|
|
140
|
+
// e.g. .supermouse-scope-0 a, .supermouse-scope-0 button { ... }
|
|
141
|
+
const scopedSelectors = rawSelectors.map(s => `.${this.scopeClass} ${s}`).join(', ');
|
|
142
|
+
|
|
143
|
+
this.styleTag.innerText = `
|
|
144
|
+
${scopedSelectors} {
|
|
145
|
+
cursor: none !important;
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public destroy() {
|
|
151
|
+
this.element.remove();
|
|
152
|
+
this.styleTag.remove();
|
|
153
|
+
this.container.style.cursor = '';
|
|
154
|
+
this.container.classList.remove(this.scopeClass);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
|
|
2
|
+
import { Supermouse } from './Supermouse';
|
|
3
|
+
|
|
4
|
+
export interface MousePosition {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ShapeState {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
borderRadius: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The Interface for interaction state.
|
|
17
|
+
* Plugins should use Module Augmentation to add their specific properties to this interface.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* declare module '@supermousejs/core' {
|
|
21
|
+
* interface InteractionState {
|
|
22
|
+
* magnetic: boolean | number;
|
|
23
|
+
* text: string;
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
export interface InteractionState {
|
|
28
|
+
/**
|
|
29
|
+
* Allow arbitrary keys for rapid prototyping.
|
|
30
|
+
* For type safety, use module augmentation to define expected keys.
|
|
31
|
+
*/
|
|
32
|
+
[key: string]: any;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MouseState {
|
|
36
|
+
/** The raw position of the input pointer (mouse/touch). */
|
|
37
|
+
pointer: MousePosition;
|
|
38
|
+
/** The target position the cursor logic wants to reach. */
|
|
39
|
+
target: MousePosition;
|
|
40
|
+
/** The smoothed/interpolated position used for rendering. */
|
|
41
|
+
smooth: MousePosition;
|
|
42
|
+
/** The current velocity vector of the smooth position. */
|
|
43
|
+
velocity: MousePosition;
|
|
44
|
+
/** The angle of movement in degrees. Calculated from velocity. */
|
|
45
|
+
angle: number;
|
|
46
|
+
/** Whether the pointer is currently pressed down. */
|
|
47
|
+
isDown: boolean;
|
|
48
|
+
/** Whether the pointer is currently hovering over a registered interactive element. */
|
|
49
|
+
isHover: boolean;
|
|
50
|
+
/** Whether the native cursor is currently forced visible by internal logic (e.g. input elements). */
|
|
51
|
+
isNative: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* If set, this overrides all auto-detection logic.
|
|
54
|
+
* 'auto' = Force Native Cursor (Show)
|
|
55
|
+
* 'none' = Force Custom Cursor (Hide Native)
|
|
56
|
+
* null = Let the Core decide based on isNative/isHover
|
|
57
|
+
*/
|
|
58
|
+
forcedCursor: 'auto' | 'none' | null;
|
|
59
|
+
/** The DOM element currently being hovered, if any. */
|
|
60
|
+
hoverTarget: HTMLElement | null;
|
|
61
|
+
/** Whether the user has `prefers-reduced-motion` enabled. */
|
|
62
|
+
reducedMotion: boolean;
|
|
63
|
+
/** Whether the system has received valid input coordinates at least once. */
|
|
64
|
+
hasReceivedInput: boolean;
|
|
65
|
+
/** Defines a specific geometric shape the cursor should conform to. */
|
|
66
|
+
shape: ShapeState | null;
|
|
67
|
+
/** Centralized store for hover metadata from data attributes. */
|
|
68
|
+
interaction: InteractionState;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type NativeIgnoreStrategy = 'auto' | 'tag' | 'css';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configuration options passed to the Supermouse constructor.
|
|
75
|
+
*/
|
|
76
|
+
export interface SupermouseOptions {
|
|
77
|
+
/**
|
|
78
|
+
* The interpolation factor (0 to 1). Lower is smoother/slower.
|
|
79
|
+
* @default 0.15
|
|
80
|
+
*/
|
|
81
|
+
smoothness?: number;
|
|
82
|
+
/**
|
|
83
|
+
* List of CSS selectors that trigger the "Hover" state.
|
|
84
|
+
* Overrides the default set if provided.
|
|
85
|
+
*/
|
|
86
|
+
hoverSelectors?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Whether to enable custom cursor effects on touch devices.
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
91
|
+
enableTouch?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Whether to automatically disable the custom cursor on devices with coarse pointers.
|
|
94
|
+
* @default true
|
|
95
|
+
*/
|
|
96
|
+
autoDisableOnMobile?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Strategy for detecting when to fallback to the native cursor.
|
|
99
|
+
* - `true` / `'auto'`: Checks both HTML tags and CSS cursor styles (Accurate but slower).
|
|
100
|
+
* - `'tag'`: Checks only semantic tags like <input>, <textarea> (Fastest, prevents layout thrashing).
|
|
101
|
+
* - `'css'`: Checks only computed CSS cursor styles (Slow, triggers reflow).
|
|
102
|
+
* - `false`: Never fallback to native cursor.
|
|
103
|
+
* @default 'auto'
|
|
104
|
+
*/
|
|
105
|
+
ignoreOnNative?: boolean | NativeIgnoreStrategy;
|
|
106
|
+
/**
|
|
107
|
+
* Whether to hide the native cursor via global CSS injection.
|
|
108
|
+
* @default true
|
|
109
|
+
*/
|
|
110
|
+
hideCursor?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Whether to hide the custom cursor when the mouse leaves the browser window.
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
115
|
+
hideOnLeave?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* List of plugins to initialize with the instance.
|
|
118
|
+
*/
|
|
119
|
+
plugins?: SupermousePlugin[];
|
|
120
|
+
/**
|
|
121
|
+
* The DOM element to append the cursor stage to.
|
|
122
|
+
* @default document.body
|
|
123
|
+
*/
|
|
124
|
+
container?: HTMLElement;
|
|
125
|
+
/**
|
|
126
|
+
* Whether to start the internal animation loop automatically.
|
|
127
|
+
* @default true
|
|
128
|
+
*/
|
|
129
|
+
autoStart?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Semantic rules mapping CSS selectors to interaction state.
|
|
132
|
+
* @example { 'button': { icon: 'pointer' } }
|
|
133
|
+
*/
|
|
134
|
+
rules?: Record<string, InteractionState>;
|
|
135
|
+
/**
|
|
136
|
+
* Custom strategy to resolve interaction state from a hovered element.
|
|
137
|
+
* Overrides the default data-attribute scraping.
|
|
138
|
+
*/
|
|
139
|
+
resolveInteraction?: (target: HTMLElement) => InteractionState;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Helper type: Allows a property to be a static value OR a function that returns the value based on state.
|
|
144
|
+
*/
|
|
145
|
+
export type ValueOrGetter<T> = T | ((state: MouseState) => T);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Interface for defining a Supermouse Plugin.
|
|
149
|
+
*/
|
|
150
|
+
export interface SupermousePlugin {
|
|
151
|
+
/** Unique name for the plugin. Used for toggling/retrieval. */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Execution priority. Lower numbers run first. */
|
|
154
|
+
priority?: number;
|
|
155
|
+
/** If false, update() will not be called. */
|
|
156
|
+
isEnabled?: boolean;
|
|
157
|
+
|
|
158
|
+
/** Called when `app.use()` is executed. */
|
|
159
|
+
install?: (instance: Supermouse) => void;
|
|
160
|
+
/** Called on every animation frame. */
|
|
161
|
+
update?: (instance: Supermouse, deltaTime: number) => void;
|
|
162
|
+
/** Called when the plugin is removed or the app is destroyed. */
|
|
163
|
+
destroy?: (instance: Supermouse) => void;
|
|
164
|
+
|
|
165
|
+
/** Called when the plugin is enabled via .enablePlugin() */
|
|
166
|
+
onEnable?: (instance: Supermouse) => void;
|
|
167
|
+
/** Called when the plugin is disabled via .disablePlugin() */
|
|
168
|
+
onDisable?: (instance: Supermouse) => void;
|
|
169
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"include": [
|
|
4
|
+
"src"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"baseUrl": ".",
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"paths": {}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import dts from 'vite-plugin-dts';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'));
|
|
11
|
+
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
define: {
|
|
14
|
+
__VERSION__: JSON.stringify(pkg.version)
|
|
15
|
+
},
|
|
16
|
+
build: {
|
|
17
|
+
lib: {
|
|
18
|
+
entry: path.resolve(__dirname, 'src/index.ts'),
|
|
19
|
+
name: 'SupermouseCore',
|
|
20
|
+
fileName: (format) => format === 'es' ? 'index.mjs' : 'index.umd.js',
|
|
21
|
+
},
|
|
22
|
+
rollupOptions: {
|
|
23
|
+
external: [],
|
|
24
|
+
output: {
|
|
25
|
+
globals: {
|
|
26
|
+
'@supermousejs/core': 'SupermouseCore'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
plugins: [dts({ rollupTypes: true })]
|
|
32
|
+
});
|