@supermousejs/utils 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 +12 -0
- package/LICENSE.md +21 -0
- package/dist/index.d.ts +227 -0
- package/dist/index.mjs +205 -0
- package/dist/index.umd.js +7 -0
- package/package.json +23 -0
- package/src/css.ts +11 -0
- package/src/doctor.ts +49 -0
- package/src/dom.ts +131 -0
- package/src/effects.ts +35 -0
- package/src/index.ts +10 -0
- package/src/layers.ts +17 -0
- package/src/math.ts +60 -0
- package/src/options.ts +22 -0
- package/src/plugin.ts +149 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +22 -0
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sijibomi Olusunmbola
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { MouseState } from '../../core/src/index.ts';
|
|
2
|
+
import { Supermouse } from '../../core/src/index.ts';
|
|
3
|
+
import { SupermousePlugin } from '../../core/src/index.ts';
|
|
4
|
+
import { ValueOrGetter } from '../../core/src/index.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculates the angle in degrees between two points (or vector direction).
|
|
8
|
+
*/
|
|
9
|
+
declare function angle(x: number, y: number): number;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Applies a dictionary of styles to an HTMLElement.
|
|
13
|
+
*/
|
|
14
|
+
declare function applyStyles(el: HTMLElement, styles: Partial<CSSStyleDeclaration>): void;
|
|
15
|
+
|
|
16
|
+
export declare interface BasePluginOptions {
|
|
17
|
+
name?: string;
|
|
18
|
+
isEnabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Constrains a value between a minimum and maximum.
|
|
23
|
+
*/
|
|
24
|
+
declare function clamp(value: number, min: number, max: number): number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a standard Supermouse actor element with optimal performance settings.
|
|
28
|
+
* Includes absolute positioning, pointer-events: none, and will-change: transform.
|
|
29
|
+
*
|
|
30
|
+
* @param tagName The HTML tag to create (default: 'div')
|
|
31
|
+
*/
|
|
32
|
+
declare function createActor(tagName?: string): HTMLElement;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a circular HTML div using the standard actor base.
|
|
36
|
+
*/
|
|
37
|
+
declare function createCircle(size: number, color: string): HTMLDivElement;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Legacy alias for createActor.
|
|
41
|
+
* @deprecated Use createActor() instead.
|
|
42
|
+
*/
|
|
43
|
+
declare function createDiv(): HTMLDivElement;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Frame-rate independent damping (Time-based Lerp).
|
|
47
|
+
* Ensures smooth animation consistent across 60hz, 120hz, etc.
|
|
48
|
+
*
|
|
49
|
+
* @param a Current value
|
|
50
|
+
* @param b Target value
|
|
51
|
+
* @param lambda Smoothing factor (approx 1-20). Higher is faster.
|
|
52
|
+
* @param dt Delta time in seconds (not milliseconds)
|
|
53
|
+
*/
|
|
54
|
+
declare function damp(a: number, b: number, lambda: number, dt: number): number;
|
|
55
|
+
|
|
56
|
+
export declare function definePlugin<E extends HTMLElement, O extends BasePluginOptions>(config: VisualConfig<E, O>, userOptions?: O): SupermousePlugin;
|
|
57
|
+
|
|
58
|
+
export declare function definePlugin(config: LogicConfig, userOptions?: BasePluginOptions): SupermousePlugin;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Calculates the distance (hypotenuse) between two points (or magnitude of a vector).
|
|
62
|
+
* If x2/y2 are omitted, calculates magnitude of vector x1/y1.
|
|
63
|
+
*/
|
|
64
|
+
declare function dist(x1: number, y1: number, x2?: number, y2?: number): number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Supermouse Doctor
|
|
68
|
+
* A diagnostic utility to find common CSS conflicts that cause "Double Cursor" glitches.
|
|
69
|
+
*/
|
|
70
|
+
export declare function doctor(): void;
|
|
71
|
+
|
|
72
|
+
declare namespace dom {
|
|
73
|
+
export {
|
|
74
|
+
applyStyles,
|
|
75
|
+
setStyle,
|
|
76
|
+
setTransform,
|
|
77
|
+
projectRect,
|
|
78
|
+
createActor,
|
|
79
|
+
createCircle,
|
|
80
|
+
createDiv
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export { dom }
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Common cubic-bezier easing strings for CSS transitions.
|
|
87
|
+
*/
|
|
88
|
+
export declare const Easings: {
|
|
89
|
+
/** Good for entrance animations (starts fast, slows down) */
|
|
90
|
+
readonly EASE_OUT_EXPO: "cubic-bezier(0.16, 1, 0.3, 1)";
|
|
91
|
+
/** Good for elastic/bouncy UI elements */
|
|
92
|
+
readonly ELASTIC_OUT: "cubic-bezier(0.34, 1.56, 0.64, 1)";
|
|
93
|
+
/** Standard smooth movement */
|
|
94
|
+
readonly SMOOTH: "ease-out";
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
declare namespace effects {
|
|
98
|
+
export {
|
|
99
|
+
getVelocityDistortion
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export { effects }
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Calculates Rotation and Scale based on velocity to create a "Squash and Stretch" effect.
|
|
106
|
+
*
|
|
107
|
+
* @param vx Velocity X
|
|
108
|
+
* @param vy Velocity Y
|
|
109
|
+
* @param intensity Stretch factor (default: 0.004)
|
|
110
|
+
* @param maxStretch Max stretch percentage (default: 0.5 = 150% length)
|
|
111
|
+
*/
|
|
112
|
+
declare function getVelocityDistortion(vx: number, vy: number, intensity?: number, maxStretch?: number): {
|
|
113
|
+
rotation: number;
|
|
114
|
+
scaleX: number;
|
|
115
|
+
scaleY: number;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Standard Z-Index layers for the Supermouse ecosystem.
|
|
120
|
+
* Relative to the Supermouse Container.
|
|
121
|
+
*/
|
|
122
|
+
export declare const Layers: {
|
|
123
|
+
/** The top-most layer. For text, tooltips, and crucial UI. */
|
|
124
|
+
readonly OVERLAY: "400";
|
|
125
|
+
/** The main cursor layer. For the primary Dot/Pointer. */
|
|
126
|
+
readonly CURSOR: "300";
|
|
127
|
+
/** The secondary layer. For Rings, brackets, or followers. */
|
|
128
|
+
readonly FOLLOWER: "200";
|
|
129
|
+
/** The background layer. For trails, sparkles, and particles. */
|
|
130
|
+
readonly TRACE: "100";
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Linear Interpolation between two values.
|
|
135
|
+
*/
|
|
136
|
+
declare function lerp(start: number, end: number, factor: number): number;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Linear Interpolation between two angles in degrees, taking the shortest path.
|
|
140
|
+
* Handles wrap-around at 360 degrees.
|
|
141
|
+
*/
|
|
142
|
+
declare function lerpAngle(start: number, end: number, factor: number): number;
|
|
143
|
+
|
|
144
|
+
declare interface LogicConfig {
|
|
145
|
+
name: string;
|
|
146
|
+
priority?: number;
|
|
147
|
+
install?: (app: Supermouse) => void;
|
|
148
|
+
update?: (app: Supermouse, deltaTime: number) => void;
|
|
149
|
+
destroy?: (app: Supermouse) => void;
|
|
150
|
+
onEnable?: (app: Supermouse) => void;
|
|
151
|
+
onDisable?: (app: Supermouse) => void;
|
|
152
|
+
create?: never;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
declare namespace math {
|
|
156
|
+
export {
|
|
157
|
+
lerp,
|
|
158
|
+
damp,
|
|
159
|
+
lerpAngle,
|
|
160
|
+
random,
|
|
161
|
+
clamp,
|
|
162
|
+
dist,
|
|
163
|
+
angle
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export { math }
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns a function that always resolves the option value.
|
|
170
|
+
* Eliminates 'typeof' checks inside the render loop by normalizing
|
|
171
|
+
* static values into getter functions during initialization.
|
|
172
|
+
*
|
|
173
|
+
* @param option The option passed by the user
|
|
174
|
+
* @param defaultValue Fallback value
|
|
175
|
+
*/
|
|
176
|
+
export declare function normalize<T>(option: ValueOrGetter<T> | undefined, defaultValue: T): (state: MouseState) => T;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Calculates the bounding rectangle of an element relative to a container.
|
|
180
|
+
* Useful for logic plugins when the cursor is confined to a specific div.
|
|
181
|
+
*/
|
|
182
|
+
declare function projectRect(element: HTMLElement, container?: HTMLElement): DOMRect;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Returns a random number between min and max.
|
|
186
|
+
* Usage: math.random(10, 20) -> 14.5
|
|
187
|
+
*/
|
|
188
|
+
declare function random(min: number, max: number): number;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Smart Style Setter.
|
|
192
|
+
* Only writes to the DOM if the value has actually changed.
|
|
193
|
+
*/
|
|
194
|
+
declare function setStyle(el: HTMLElement, property: keyof CSSStyleDeclaration, value: string | number): void;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Universal Transform Setter.
|
|
198
|
+
* Handles centering (-50%) automatically.
|
|
199
|
+
*
|
|
200
|
+
* @param el The element
|
|
201
|
+
* @param x X Position (px)
|
|
202
|
+
* @param y Y Position (px)
|
|
203
|
+
* @param rotation Rotation (deg) - Default 0
|
|
204
|
+
* @param scaleX Scale X - Default 1
|
|
205
|
+
* @param scaleY Scale Y - Default 1
|
|
206
|
+
* @param skewX Skew X (deg) - Default 0
|
|
207
|
+
* @param skewY Skew Y (deg) - Default 0
|
|
208
|
+
*/
|
|
209
|
+
declare function setTransform(el: HTMLElement, x: number, y: number, rotation?: number, scaleX?: number, scaleY?: number, skewX?: number, skewY?: number): void;
|
|
210
|
+
|
|
211
|
+
declare interface VisualConfig<E extends HTMLElement, O extends object> {
|
|
212
|
+
name: string;
|
|
213
|
+
/** Automatically register this attribute selector */
|
|
214
|
+
selector?: string;
|
|
215
|
+
/** Create and return the DOM Element */
|
|
216
|
+
create: (app: Supermouse) => E;
|
|
217
|
+
/** Map option keys to CSS properties */
|
|
218
|
+
styles?: Partial<Record<keyof O, keyof CSSStyleDeclaration>>;
|
|
219
|
+
/** Update loop with access to the element */
|
|
220
|
+
update?: (app: Supermouse, element: E, deltaTime: number) => void;
|
|
221
|
+
onEnable?: (app: Supermouse, element: E) => void;
|
|
222
|
+
onDisable?: (app: Supermouse, element: E) => void;
|
|
223
|
+
cleanup?: (element: E) => void;
|
|
224
|
+
destroy?: never;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export { }
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
function y(e, t, n) {
|
|
2
|
+
return e + (t - e) * n;
|
|
3
|
+
}
|
|
4
|
+
function O(e, t, n, o) {
|
|
5
|
+
return y(e, t, 1 - Math.exp(-n * o));
|
|
6
|
+
}
|
|
7
|
+
function _(e, t, n) {
|
|
8
|
+
const o = ((t - e) % 360 + 540) % 360 - 180;
|
|
9
|
+
return e + o * n;
|
|
10
|
+
}
|
|
11
|
+
function C(e, t) {
|
|
12
|
+
return Math.random() * (t - e) + e;
|
|
13
|
+
}
|
|
14
|
+
function h(e, t, n) {
|
|
15
|
+
return Math.min(Math.max(e, t), n);
|
|
16
|
+
}
|
|
17
|
+
function b(e, t, n = 0, o = 0) {
|
|
18
|
+
const r = e - n, i = t - o;
|
|
19
|
+
return Math.sqrt(r * r + i * i);
|
|
20
|
+
}
|
|
21
|
+
function m(e, t) {
|
|
22
|
+
return Math.atan2(t, e) * (180 / Math.PI);
|
|
23
|
+
}
|
|
24
|
+
const x = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
25
|
+
__proto__: null,
|
|
26
|
+
angle: m,
|
|
27
|
+
clamp: h,
|
|
28
|
+
damp: O,
|
|
29
|
+
dist: b,
|
|
30
|
+
lerp: y,
|
|
31
|
+
lerpAngle: _,
|
|
32
|
+
random: C
|
|
33
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
34
|
+
function a(e, t) {
|
|
35
|
+
Object.assign(e.style, t);
|
|
36
|
+
}
|
|
37
|
+
const p = /* @__PURE__ */ new WeakMap();
|
|
38
|
+
function u(e, t, n) {
|
|
39
|
+
let o = p.get(e);
|
|
40
|
+
o || (o = {}, p.set(e, o)), o[t] !== n && (e.style[t] = n, o[t] = n);
|
|
41
|
+
}
|
|
42
|
+
function v(e, t, n, o = 0, r = 1, i = 1, s = 0, l = 0) {
|
|
43
|
+
e.style.transform = `
|
|
44
|
+
translate3d(${t}px, ${n}px, 0)
|
|
45
|
+
translate(-50%, -50%)
|
|
46
|
+
rotate(${o}deg)
|
|
47
|
+
skew(${s}deg, ${l}deg)
|
|
48
|
+
scale(${r}, ${i})
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
function w(e, t = document.body) {
|
|
52
|
+
const n = e.getBoundingClientRect();
|
|
53
|
+
if (t !== document.body) {
|
|
54
|
+
const o = t.getBoundingClientRect(), r = n.left - o.left, i = n.top - o.top;
|
|
55
|
+
return {
|
|
56
|
+
x: r,
|
|
57
|
+
y: i,
|
|
58
|
+
width: n.width,
|
|
59
|
+
height: n.height,
|
|
60
|
+
top: i,
|
|
61
|
+
left: r,
|
|
62
|
+
right: r + n.width,
|
|
63
|
+
bottom: i + n.height,
|
|
64
|
+
toJSON: () => ({})
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
function d(e = "div") {
|
|
70
|
+
const t = document.createElement(e);
|
|
71
|
+
return a(t, {
|
|
72
|
+
position: "absolute",
|
|
73
|
+
top: "0",
|
|
74
|
+
left: "0",
|
|
75
|
+
pointerEvents: "none",
|
|
76
|
+
boxSizing: "border-box",
|
|
77
|
+
display: "block",
|
|
78
|
+
willChange: "transform"
|
|
79
|
+
}), t;
|
|
80
|
+
}
|
|
81
|
+
function A(e, t) {
|
|
82
|
+
const n = d("div");
|
|
83
|
+
return a(n, {
|
|
84
|
+
width: `${e}px`,
|
|
85
|
+
height: `${e}px`,
|
|
86
|
+
borderRadius: "50%",
|
|
87
|
+
backgroundColor: t
|
|
88
|
+
}), n;
|
|
89
|
+
}
|
|
90
|
+
function M() {
|
|
91
|
+
return d("div");
|
|
92
|
+
}
|
|
93
|
+
const j = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
94
|
+
__proto__: null,
|
|
95
|
+
applyStyles: a,
|
|
96
|
+
createActor: d,
|
|
97
|
+
createCircle: A,
|
|
98
|
+
createDiv: M,
|
|
99
|
+
projectRect: w,
|
|
100
|
+
setStyle: u,
|
|
101
|
+
setTransform: v
|
|
102
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
103
|
+
function R(e, t, n = 4e-3, o = 0.5) {
|
|
104
|
+
const r = b(e, t);
|
|
105
|
+
if (r < 0.1)
|
|
106
|
+
return { rotation: 0, scaleX: 1, scaleY: 1 };
|
|
107
|
+
const i = m(e, t), s = h(r * n, 0, o), l = 1 + s, c = 1 - s * 0.5;
|
|
108
|
+
return {
|
|
109
|
+
rotation: i,
|
|
110
|
+
scaleX: l,
|
|
111
|
+
scaleY: c
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const z = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
115
|
+
__proto__: null,
|
|
116
|
+
getVelocityDistortion: R
|
|
117
|
+
}, Symbol.toStringTag, { value: "Module" })), P = {
|
|
118
|
+
/** The top-most layer. For text, tooltips, and crucial UI. */
|
|
119
|
+
OVERLAY: "400",
|
|
120
|
+
/** The main cursor layer. For the primary Dot/Pointer. */
|
|
121
|
+
CURSOR: "300",
|
|
122
|
+
/** The secondary layer. For Rings, brackets, or followers. */
|
|
123
|
+
FOLLOWER: "200",
|
|
124
|
+
/** The background layer. For trails, sparkles, and particles. */
|
|
125
|
+
TRACE: "100"
|
|
126
|
+
}, D = {
|
|
127
|
+
/** Good for entrance animations (starts fast, slows down) */
|
|
128
|
+
EASE_OUT_EXPO: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
129
|
+
/** Good for elastic/bouncy UI elements */
|
|
130
|
+
ELASTIC_OUT: "cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
131
|
+
/** Standard smooth movement */
|
|
132
|
+
SMOOTH: "ease-out"
|
|
133
|
+
};
|
|
134
|
+
function T(e, t) {
|
|
135
|
+
return e === void 0 ? () => t : typeof e == "function" ? e : () => e;
|
|
136
|
+
}
|
|
137
|
+
function $(e) {
|
|
138
|
+
return "create" in e && typeof e.create == "function";
|
|
139
|
+
}
|
|
140
|
+
function L(e, t = {}) {
|
|
141
|
+
const n = t.name || e.name, o = t.isEnabled ?? !0;
|
|
142
|
+
if ($(e)) {
|
|
143
|
+
let r;
|
|
144
|
+
const i = [];
|
|
145
|
+
if (e.styles)
|
|
146
|
+
for (const [s, l] of Object.entries(e.styles)) {
|
|
147
|
+
const c = T(t[s], void 0), g = l;
|
|
148
|
+
i.push((E, S) => {
|
|
149
|
+
const f = c(E.state);
|
|
150
|
+
f !== void 0 && u(S, g, f);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
name: n,
|
|
155
|
+
isEnabled: o,
|
|
156
|
+
install(s) {
|
|
157
|
+
r = e.create(s), e.selector && s.registerHoverTarget(e.selector), this.isEnabled === !1 && (r.style.opacity = "0"), s.container.appendChild(r);
|
|
158
|
+
},
|
|
159
|
+
update(s, l) {
|
|
160
|
+
if (r) {
|
|
161
|
+
for (let c = 0; c < i.length; c++)
|
|
162
|
+
i[c](s, r);
|
|
163
|
+
e.update?.(s, r, l);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
onDisable(s) {
|
|
167
|
+
r && (u(r, "opacity", 0), e.onDisable?.(s, r));
|
|
168
|
+
},
|
|
169
|
+
onEnable(s) {
|
|
170
|
+
r && (u(r, "opacity", 1), e.onEnable?.(s, r));
|
|
171
|
+
},
|
|
172
|
+
destroy() {
|
|
173
|
+
r && (e.cleanup?.(r), r.remove());
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
} else
|
|
177
|
+
return {
|
|
178
|
+
...e,
|
|
179
|
+
name: n,
|
|
180
|
+
isEnabled: o
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function k() {
|
|
184
|
+
if (console.group("🐭 Supermouse Doctor"), !Array.from(document.querySelectorAll("style")).some((o) => o.id.startsWith("supermouse-style-"))) {
|
|
185
|
+
console.warn("❌ Supermouse has not been initialized yet."), console.warn(' Please call doctor() AFTER "new Supermouse()" has run.'), console.groupEnd();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const t = [];
|
|
189
|
+
document.querySelectorAll('[style*="cursor"]').forEach((o) => {
|
|
190
|
+
o.getAttribute("style")?.includes("cursor: none") || t.push({
|
|
191
|
+
el: o,
|
|
192
|
+
reason: 'Element has inline "cursor" style. Remove it and let Supermouse handle state.'
|
|
193
|
+
});
|
|
194
|
+
}), document.body.style.cursor !== "none" && console.warn('[Global] document.body.style.cursor is not "none". Ensure { hideCursor: true } is passed to Supermouse.'), t.length > 0 ? (console.warn(`Found ${t.length} potential conflicts:`), t.forEach((o) => console.warn(`[${o.reason}]`, o.el)), console.info('Tip: Avoid setting "cursor: pointer" manually. Use plugins or "rules" config instead.')) : console.log("✅ No obvious inline-style conflicts found."), console.groupEnd();
|
|
195
|
+
}
|
|
196
|
+
export {
|
|
197
|
+
D as Easings,
|
|
198
|
+
P as Layers,
|
|
199
|
+
L as definePlugin,
|
|
200
|
+
k as doctor,
|
|
201
|
+
j as dom,
|
|
202
|
+
z as effects,
|
|
203
|
+
x as math,
|
|
204
|
+
T as normalize
|
|
205
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
(function(l,a){typeof exports=="object"&&typeof module<"u"?a(exports):typeof define=="function"&&define.amd?define(["exports"],a):(l=typeof globalThis<"u"?globalThis:l||self,a(l.SupermouseUtils={}))})(this,(function(l){"use strict";function a(e,t,n){return e+(t-e)*n}function S(e,t,n,o){return a(e,t,1-Math.exp(-n*o))}function O(e,t,n){const o=((t-e)%360+540)%360-180;return e+o*n}function T(e,t){return Math.random()*(t-e)+e}function h(e,t,n){return Math.min(Math.max(e,t),n)}function m(e,t,n=0,o=0){const r=e-n,i=t-o;return Math.sqrt(r*r+i*i)}function b(e,t){return Math.atan2(t,e)*(180/Math.PI)}const _=Object.freeze(Object.defineProperty({__proto__:null,angle:b,clamp:h,damp:S,dist:m,lerp:a,lerpAngle:O,random:T},Symbol.toStringTag,{value:"Module"}));function f(e,t){Object.assign(e.style,t)}const p=new WeakMap;function d(e,t,n){let o=p.get(e);o||(o={},p.set(e,o)),o[t]!==n&&(e.style[t]=n,o[t]=n)}function v(e,t,n,o=0,r=1,i=1,s=0,c=0){e.style.transform=`
|
|
2
|
+
translate3d(${t}px, ${n}px, 0)
|
|
3
|
+
translate(-50%, -50%)
|
|
4
|
+
rotate(${o}deg)
|
|
5
|
+
skew(${s}deg, ${c}deg)
|
|
6
|
+
scale(${r}, ${i})
|
|
7
|
+
`}function C(e,t=document.body){const n=e.getBoundingClientRect();if(t!==document.body){const o=t.getBoundingClientRect(),r=n.left-o.left,i=n.top-o.top;return{x:r,y:i,width:n.width,height:n.height,top:i,left:r,right:r+n.width,bottom:i+n.height,toJSON:()=>({})}}return n}function y(e="div"){const t=document.createElement(e);return f(t,{position:"absolute",top:"0",left:"0",pointerEvents:"none",boxSizing:"border-box",display:"block",willChange:"transform"}),t}function w(e,t){const n=y("div");return f(n,{width:`${e}px`,height:`${e}px`,borderRadius:"50%",backgroundColor:t}),n}function M(){return y("div")}const A=Object.freeze(Object.defineProperty({__proto__:null,applyStyles:f,createActor:y,createCircle:w,createDiv:M,projectRect:C,setStyle:d,setTransform:v},Symbol.toStringTag,{value:"Module"}));function R(e,t,n=.004,o=.5){const r=m(e,t);if(r<.1)return{rotation:0,scaleX:1,scaleY:1};const i=b(e,t),s=h(r*n,0,o),c=1+s,u=1-s*.5;return{rotation:i,scaleX:c,scaleY:u}}const j=Object.freeze(Object.defineProperty({__proto__:null,getVelocityDistortion:R},Symbol.toStringTag,{value:"Module"})),$={OVERLAY:"400",CURSOR:"300",FOLLOWER:"200",TRACE:"100"},z={EASE_OUT_EXPO:"cubic-bezier(0.16, 1, 0.3, 1)",ELASTIC_OUT:"cubic-bezier(0.34, 1.56, 0.64, 1)",SMOOTH:"ease-out"};function g(e,t){return e===void 0?()=>t:typeof e=="function"?e:()=>e}function P(e){return"create"in e&&typeof e.create=="function"}function L(e,t={}){const n=t.name||e.name,o=t.isEnabled??!0;if(P(e)){let r;const i=[];if(e.styles)for(const[s,c]of Object.entries(e.styles)){const u=g(t[s],void 0),U=c;i.push((k,x)=>{const E=u(k.state);E!==void 0&&d(x,U,E)})}return{name:n,isEnabled:o,install(s){r=e.create(s),e.selector&&s.registerHoverTarget(e.selector),this.isEnabled===!1&&(r.style.opacity="0"),s.container.appendChild(r)},update(s,c){if(r){for(let u=0;u<i.length;u++)i[u](s,r);e.update?.(s,r,c)}},onDisable(s){r&&(d(r,"opacity",0),e.onDisable?.(s,r))},onEnable(s){r&&(d(r,"opacity",1),e.onEnable?.(s,r))},destroy(){r&&(e.cleanup?.(r),r.remove())}}}else return{...e,name:n,isEnabled:o}}function D(){if(console.group("🐭 Supermouse Doctor"),!Array.from(document.querySelectorAll("style")).some(o=>o.id.startsWith("supermouse-style-"))){console.warn("❌ Supermouse has not been initialized yet."),console.warn(' Please call doctor() AFTER "new Supermouse()" has run.'),console.groupEnd();return}const t=[];document.querySelectorAll('[style*="cursor"]').forEach(o=>{o.getAttribute("style")?.includes("cursor: none")||t.push({el:o,reason:'Element has inline "cursor" style. Remove it and let Supermouse handle state.'})}),document.body.style.cursor!=="none"&&console.warn('[Global] document.body.style.cursor is not "none". Ensure { hideCursor: true } is passed to Supermouse.'),t.length>0?(console.warn(`Found ${t.length} potential conflicts:`),t.forEach(o=>console.warn(`[${o.reason}]`,o.el)),console.info('Tip: Avoid setting "cursor: pointer" manually. Use plugins or "rules" config instead.')):console.log("✅ No obvious inline-style conflicts found."),console.groupEnd()}l.Easings=z,l.Layers=$,l.definePlugin=L,l.doctor=D,l.dom=A,l.effects=j,l.math=_,l.normalize=g,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supermousejs/utils",
|
|
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
|
+
"@supermousejs/core": "2.0.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.umd.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "vite build"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/css.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common cubic-bezier easing strings for CSS transitions.
|
|
3
|
+
*/
|
|
4
|
+
export const Easings = {
|
|
5
|
+
/** Good for entrance animations (starts fast, slows down) */
|
|
6
|
+
EASE_OUT_EXPO: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
7
|
+
/** Good for elastic/bouncy UI elements */
|
|
8
|
+
ELASTIC_OUT: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
9
|
+
/** Standard smooth movement */
|
|
10
|
+
SMOOTH: 'ease-out',
|
|
11
|
+
} as const;
|
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Supermouse Doctor
|
|
4
|
+
* A diagnostic utility to find common CSS conflicts that cause "Double Cursor" glitches.
|
|
5
|
+
*/
|
|
6
|
+
export function doctor() {
|
|
7
|
+
console.group('🐭 Supermouse Doctor');
|
|
8
|
+
|
|
9
|
+
// 0. Check Initialization
|
|
10
|
+
// Supermouse injects a style tag with an ID starting with 'supermouse-style-'
|
|
11
|
+
const hasInitialized = Array.from(document.querySelectorAll('style')).some(s => s.id.startsWith('supermouse-style-'));
|
|
12
|
+
|
|
13
|
+
if (!hasInitialized) {
|
|
14
|
+
console.warn('❌ Supermouse has not been initialized yet.');
|
|
15
|
+
console.warn(' Please call doctor() AFTER "new Supermouse()" has run.');
|
|
16
|
+
console.groupEnd();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const issues: { el: Element, reason: string }[] = [];
|
|
21
|
+
|
|
22
|
+
// 1. Check for Inline Styles (Hard Overrides)
|
|
23
|
+
const inlineCursor = document.querySelectorAll('[style*="cursor"]');
|
|
24
|
+
inlineCursor.forEach(el => {
|
|
25
|
+
// Ignore the supermouse container itself (usually has cursor: none)
|
|
26
|
+
if (el.getAttribute('style')?.includes('cursor: none')) return;
|
|
27
|
+
|
|
28
|
+
issues.push({
|
|
29
|
+
el,
|
|
30
|
+
reason: 'Element has inline "cursor" style. Remove it and let Supermouse handle state.'
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 2. Check Global Body State
|
|
35
|
+
if (document.body.style.cursor !== 'none') {
|
|
36
|
+
console.warn('[Global] document.body.style.cursor is not "none". Ensure { hideCursor: true } is passed to Supermouse.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. Report
|
|
40
|
+
if (issues.length > 0) {
|
|
41
|
+
console.warn(`Found ${issues.length} potential conflicts:`);
|
|
42
|
+
issues.forEach(i => console.warn(`[${i.reason}]`, i.el));
|
|
43
|
+
console.info('Tip: Avoid setting "cursor: pointer" manually. Use plugins or "rules" config instead.');
|
|
44
|
+
} else {
|
|
45
|
+
console.log('✅ No obvious inline-style conflicts found.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.groupEnd();
|
|
49
|
+
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Applies a dictionary of styles to an HTMLElement.
|
|
4
|
+
*/
|
|
5
|
+
export function applyStyles(el: HTMLElement, styles: Partial<CSSStyleDeclaration>) {
|
|
6
|
+
Object.assign(el.style, styles);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// WeakMap to store previous styles for elements to prevent DOM thrashing
|
|
10
|
+
const styleCache = new WeakMap<HTMLElement, Record<string, string | number>>();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Smart Style Setter.
|
|
14
|
+
* Only writes to the DOM if the value has actually changed.
|
|
15
|
+
*/
|
|
16
|
+
export function setStyle(el: HTMLElement, property: keyof CSSStyleDeclaration, value: string | number) {
|
|
17
|
+
let cache = styleCache.get(el);
|
|
18
|
+
if (!cache) {
|
|
19
|
+
cache = {};
|
|
20
|
+
styleCache.set(el, cache);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Only write to DOM if value changed
|
|
24
|
+
if (cache[property as string] !== value) {
|
|
25
|
+
// @ts-ignore - Dynamic access
|
|
26
|
+
el.style[property] = value;
|
|
27
|
+
cache[property as string] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Universal Transform Setter.
|
|
33
|
+
* Handles centering (-50%) automatically.
|
|
34
|
+
*
|
|
35
|
+
* @param el The element
|
|
36
|
+
* @param x X Position (px)
|
|
37
|
+
* @param y Y Position (px)
|
|
38
|
+
* @param rotation Rotation (deg) - Default 0
|
|
39
|
+
* @param scaleX Scale X - Default 1
|
|
40
|
+
* @param scaleY Scale Y - Default 1
|
|
41
|
+
* @param skewX Skew X (deg) - Default 0
|
|
42
|
+
* @param skewY Skew Y (deg) - Default 0
|
|
43
|
+
*/
|
|
44
|
+
export function setTransform(
|
|
45
|
+
el: HTMLElement,
|
|
46
|
+
x: number,
|
|
47
|
+
y: number,
|
|
48
|
+
rotation: number = 0,
|
|
49
|
+
scaleX: number = 1,
|
|
50
|
+
scaleY: number = 1,
|
|
51
|
+
skewX: number = 0,
|
|
52
|
+
skewY: number = 0
|
|
53
|
+
) {
|
|
54
|
+
el.style.transform = `
|
|
55
|
+
translate3d(${x}px, ${y}px, 0)
|
|
56
|
+
translate(-50%, -50%)
|
|
57
|
+
rotate(${rotation}deg)
|
|
58
|
+
skew(${skewX}deg, ${skewY}deg)
|
|
59
|
+
scale(${scaleX}, ${scaleY})
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calculates the bounding rectangle of an element relative to a container.
|
|
65
|
+
* Useful for logic plugins when the cursor is confined to a specific div.
|
|
66
|
+
*/
|
|
67
|
+
export function projectRect(element: HTMLElement, container: HTMLElement = document.body): DOMRect {
|
|
68
|
+
const rect = element.getBoundingClientRect();
|
|
69
|
+
|
|
70
|
+
if (container !== document.body) {
|
|
71
|
+
const containerRect = container.getBoundingClientRect();
|
|
72
|
+
const x = rect.left - containerRect.left;
|
|
73
|
+
const y = rect.top - containerRect.top;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
x,
|
|
77
|
+
y,
|
|
78
|
+
width: rect.width,
|
|
79
|
+
height: rect.height,
|
|
80
|
+
top: y,
|
|
81
|
+
left: x,
|
|
82
|
+
right: x + rect.width,
|
|
83
|
+
bottom: y + rect.height,
|
|
84
|
+
toJSON: () => ({})
|
|
85
|
+
} as DOMRect;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return rect;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a standard Supermouse actor element with optimal performance settings.
|
|
93
|
+
* Includes absolute positioning, pointer-events: none, and will-change: transform.
|
|
94
|
+
*
|
|
95
|
+
* @param tagName The HTML tag to create (default: 'div')
|
|
96
|
+
*/
|
|
97
|
+
export function createActor(tagName: string = 'div'): HTMLElement {
|
|
98
|
+
const el = document.createElement(tagName);
|
|
99
|
+
applyStyles(el, {
|
|
100
|
+
position: 'absolute',
|
|
101
|
+
top: '0',
|
|
102
|
+
left: '0',
|
|
103
|
+
pointerEvents: 'none',
|
|
104
|
+
boxSizing: 'border-box',
|
|
105
|
+
display: 'block',
|
|
106
|
+
willChange: 'transform'
|
|
107
|
+
});
|
|
108
|
+
return el;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates a circular HTML div using the standard actor base.
|
|
113
|
+
*/
|
|
114
|
+
export function createCircle(size: number, color: string): HTMLDivElement {
|
|
115
|
+
const el = createActor('div') as HTMLDivElement;
|
|
116
|
+
applyStyles(el, {
|
|
117
|
+
width: `${size}px`,
|
|
118
|
+
height: `${size}px`,
|
|
119
|
+
borderRadius: '50%',
|
|
120
|
+
backgroundColor: color,
|
|
121
|
+
});
|
|
122
|
+
return el;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Legacy alias for createActor.
|
|
127
|
+
* @deprecated Use createActor() instead.
|
|
128
|
+
*/
|
|
129
|
+
export function createDiv(): HTMLDivElement {
|
|
130
|
+
return createActor('div') as HTMLDivElement;
|
|
131
|
+
}
|
package/src/effects.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
import { dist, angle, clamp } from './math';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculates Rotation and Scale based on velocity to create a "Squash and Stretch" effect.
|
|
6
|
+
*
|
|
7
|
+
* @param vx Velocity X
|
|
8
|
+
* @param vy Velocity Y
|
|
9
|
+
* @param intensity Stretch factor (default: 0.004)
|
|
10
|
+
* @param maxStretch Max stretch percentage (default: 0.5 = 150% length)
|
|
11
|
+
*/
|
|
12
|
+
export function getVelocityDistortion(vx: number, vy: number, intensity = 0.004, maxStretch = 0.5) {
|
|
13
|
+
const speed = dist(vx, vy);
|
|
14
|
+
|
|
15
|
+
// Deadzone: If moving too slow, don't rotate (prevents jittering at rest)
|
|
16
|
+
if (speed < 0.1) {
|
|
17
|
+
return { rotation: 0, scaleX: 1, scaleY: 1 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 1. Point towards movement
|
|
21
|
+
const rotation = angle(vx, vy);
|
|
22
|
+
|
|
23
|
+
// 2. Stretch based on speed
|
|
24
|
+
const stretch = clamp(speed * intensity, 0, maxStretch);
|
|
25
|
+
|
|
26
|
+
// 3. Scale X grows, Scale Y shrinks (to preserve volume-ish)
|
|
27
|
+
const scaleX = 1 + stretch;
|
|
28
|
+
const scaleY = 1 - (stretch * 0.5); // Squash factor
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
rotation,
|
|
32
|
+
scaleX,
|
|
33
|
+
scaleY
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as math from './math';
|
|
2
|
+
import * as dom from './dom';
|
|
3
|
+
import * as effects from './effects';
|
|
4
|
+
|
|
5
|
+
export { math, dom, effects };
|
|
6
|
+
export * from './layers';
|
|
7
|
+
export * from './css';
|
|
8
|
+
export * from './plugin';
|
|
9
|
+
export * from './options';
|
|
10
|
+
export * from './doctor';
|
package/src/layers.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Z-Index layers for the Supermouse ecosystem.
|
|
3
|
+
* Relative to the Supermouse Container.
|
|
4
|
+
*/
|
|
5
|
+
export const Layers = {
|
|
6
|
+
/** The top-most layer. For text, tooltips, and crucial UI. */
|
|
7
|
+
OVERLAY: '400',
|
|
8
|
+
|
|
9
|
+
/** The main cursor layer. For the primary Dot/Pointer. */
|
|
10
|
+
CURSOR: '300',
|
|
11
|
+
|
|
12
|
+
/** The secondary layer. For Rings, brackets, or followers. */
|
|
13
|
+
FOLLOWER: '200',
|
|
14
|
+
|
|
15
|
+
/** The background layer. For trails, sparkles, and particles. */
|
|
16
|
+
TRACE: '100',
|
|
17
|
+
} as const;
|
package/src/math.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
* Ensures smooth animation consistent across 60hz, 120hz, etc.
|
|
11
|
+
*
|
|
12
|
+
* @param a Current value
|
|
13
|
+
* @param b Target value
|
|
14
|
+
* @param lambda Smoothing factor (approx 1-20). Higher is faster.
|
|
15
|
+
* @param dt Delta time in seconds (not milliseconds)
|
|
16
|
+
*/
|
|
17
|
+
export function damp(a: number, b: number, lambda: number, dt: number): number {
|
|
18
|
+
return lerp(a, b, 1 - Math.exp(-lambda * dt));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Linear Interpolation between two angles in degrees, taking the shortest path.
|
|
23
|
+
* Handles wrap-around at 360 degrees.
|
|
24
|
+
*/
|
|
25
|
+
export function lerpAngle(start: number, end: number, factor: number): number {
|
|
26
|
+
const diff = ((((end - start) % 360) + 540) % 360) - 180;
|
|
27
|
+
return start + diff * factor;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a random number between min and max.
|
|
32
|
+
* Usage: math.random(10, 20) -> 14.5
|
|
33
|
+
*/
|
|
34
|
+
export function random(min: number, max: number): number {
|
|
35
|
+
return Math.random() * (max - min) + min;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Constrains a value between a minimum and maximum.
|
|
40
|
+
*/
|
|
41
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
42
|
+
return Math.min(Math.max(value, min), max);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Calculates the distance (hypotenuse) between two points (or magnitude of a vector).
|
|
47
|
+
* If x2/y2 are omitted, calculates magnitude of vector x1/y1.
|
|
48
|
+
*/
|
|
49
|
+
export function dist(x1: number, y1: number, x2: number = 0, y2: number = 0): number {
|
|
50
|
+
const dx = x1 - x2;
|
|
51
|
+
const dy = y1 - y2;
|
|
52
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculates the angle in degrees between two points (or vector direction).
|
|
57
|
+
*/
|
|
58
|
+
export function angle(x: number, y: number): number {
|
|
59
|
+
return Math.atan2(y, x) * (180 / Math.PI);
|
|
60
|
+
}
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MouseState, ValueOrGetter } from '@supermousejs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a function that always resolves the option value.
|
|
5
|
+
* Eliminates 'typeof' checks inside the render loop by normalizing
|
|
6
|
+
* static values into getter functions during initialization.
|
|
7
|
+
*
|
|
8
|
+
* @param option The option passed by the user
|
|
9
|
+
* @param defaultValue Fallback value
|
|
10
|
+
*/
|
|
11
|
+
export function normalize<T>(
|
|
12
|
+
option: ValueOrGetter<T> | undefined,
|
|
13
|
+
defaultValue: T
|
|
14
|
+
): (state: MouseState) => T {
|
|
15
|
+
if (option === undefined) {
|
|
16
|
+
return () => defaultValue;
|
|
17
|
+
}
|
|
18
|
+
if (typeof option === 'function') {
|
|
19
|
+
return option as (state: MouseState) => T;
|
|
20
|
+
}
|
|
21
|
+
return () => option;
|
|
22
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Supermouse, SupermousePlugin } from '@supermousejs/core';
|
|
2
|
+
import { normalize } from './options';
|
|
3
|
+
import { setStyle } from './dom';
|
|
4
|
+
|
|
5
|
+
// --- SHARED OPTIONS ---
|
|
6
|
+
export interface BasePluginOptions {
|
|
7
|
+
name?: string;
|
|
8
|
+
isEnabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// --- MODE A: LOGIC PLUGIN CONFIG ---
|
|
12
|
+
interface LogicConfig {
|
|
13
|
+
name: string;
|
|
14
|
+
priority?: number;
|
|
15
|
+
install?: (app: Supermouse) => void;
|
|
16
|
+
update?: (app: Supermouse, deltaTime: number) => void;
|
|
17
|
+
destroy?: (app: Supermouse) => void;
|
|
18
|
+
onEnable?: (app: Supermouse) => void;
|
|
19
|
+
onDisable?: (app: Supermouse) => void;
|
|
20
|
+
// Explicitly disallow 'create' here to ensure type separation
|
|
21
|
+
create?: never;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- MODE B: VISUAL PLUGIN CONFIG ---
|
|
25
|
+
interface VisualConfig<E extends HTMLElement, O extends object> {
|
|
26
|
+
name: string;
|
|
27
|
+
/** Automatically register this attribute selector */
|
|
28
|
+
selector?: string;
|
|
29
|
+
/** Create and return the DOM Element */
|
|
30
|
+
create: (app: Supermouse) => E;
|
|
31
|
+
/** Map option keys to CSS properties */
|
|
32
|
+
styles?: Partial<Record<keyof O, keyof CSSStyleDeclaration>>;
|
|
33
|
+
/** Update loop with access to the element */
|
|
34
|
+
update?: (app: Supermouse, element: E, deltaTime: number) => void;
|
|
35
|
+
onEnable?: (app: Supermouse, element: E) => void;
|
|
36
|
+
onDisable?: (app: Supermouse, element: E) => void;
|
|
37
|
+
cleanup?: (element: E) => void;
|
|
38
|
+
destroy?: never; // Visual plugins use cleanup(), not destroy()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Helper Type Guard to safely distinguish VisualConfig at runtime
|
|
42
|
+
function isVisualConfig<E extends HTMLElement, O extends object>(
|
|
43
|
+
config: LogicConfig | VisualConfig<E, O>
|
|
44
|
+
): config is VisualConfig<E, O> {
|
|
45
|
+
return 'create' in config && typeof (config as any).create === 'function';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- THE OVERLOADS ---
|
|
49
|
+
|
|
50
|
+
// Overload 1: Visual Plugin (Infer Element E and Options O)
|
|
51
|
+
export function definePlugin<E extends HTMLElement, O extends BasePluginOptions>(
|
|
52
|
+
config: VisualConfig<E, O>,
|
|
53
|
+
userOptions?: O
|
|
54
|
+
): SupermousePlugin;
|
|
55
|
+
|
|
56
|
+
// Overload 2: Logic Plugin
|
|
57
|
+
export function definePlugin(
|
|
58
|
+
config: LogicConfig,
|
|
59
|
+
userOptions?: BasePluginOptions
|
|
60
|
+
): SupermousePlugin;
|
|
61
|
+
|
|
62
|
+
// --- THE IMPLEMENTATION ---
|
|
63
|
+
|
|
64
|
+
export function definePlugin(
|
|
65
|
+
config: LogicConfig | VisualConfig<HTMLElement, any>,
|
|
66
|
+
userOptions: any = {}
|
|
67
|
+
): SupermousePlugin {
|
|
68
|
+
const name = userOptions.name || config.name;
|
|
69
|
+
const initialEnabled = userOptions.isEnabled ?? true;
|
|
70
|
+
|
|
71
|
+
// MODE A: VISUAL
|
|
72
|
+
if (isVisualConfig(config)) {
|
|
73
|
+
let element: HTMLElement;
|
|
74
|
+
|
|
75
|
+
// PRE-COMPILE STYLE SETTERS
|
|
76
|
+
const styleSetters: ((app: Supermouse, el: HTMLElement) => void)[] = [];
|
|
77
|
+
|
|
78
|
+
if (config.styles) {
|
|
79
|
+
for (const [optKey, cssProp] of Object.entries(config.styles)) {
|
|
80
|
+
const getter = normalize(userOptions[optKey], undefined);
|
|
81
|
+
const prop = cssProp as any;
|
|
82
|
+
|
|
83
|
+
styleSetters.push((app, el) => {
|
|
84
|
+
const val = getter(app.state);
|
|
85
|
+
if (val !== undefined) {
|
|
86
|
+
setStyle(el, prop, val);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
isEnabled: initialEnabled,
|
|
95
|
+
|
|
96
|
+
install(app) {
|
|
97
|
+
// 1. Create & Append
|
|
98
|
+
element = config.create(app);
|
|
99
|
+
if (config.selector) app.registerHoverTarget(config.selector);
|
|
100
|
+
|
|
101
|
+
// 2. Handle Initial State
|
|
102
|
+
if (this.isEnabled === false) {
|
|
103
|
+
element.style.opacity = '0';
|
|
104
|
+
}
|
|
105
|
+
app.container.appendChild(element);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
update(app, dt) {
|
|
109
|
+
if (!element) return;
|
|
110
|
+
|
|
111
|
+
// 3. Run Pre-compiled Style Setters
|
|
112
|
+
for (let i = 0; i < styleSetters.length; i++) {
|
|
113
|
+
styleSetters[i](app, element);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Run Custom Update
|
|
117
|
+
config.update?.(app, element, dt);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
onDisable(app) {
|
|
121
|
+
if (!element) return;
|
|
122
|
+
// Use setStyle to ensure cache remains in sync (0)
|
|
123
|
+
setStyle(element, 'opacity', 0);
|
|
124
|
+
config.onDisable?.(app, element);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
onEnable(app) {
|
|
128
|
+
if (!element) return;
|
|
129
|
+
setStyle(element, "opacity", 1)
|
|
130
|
+
config.onEnable?.(app, element);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
destroy() {
|
|
134
|
+
if (!element) return;
|
|
135
|
+
config.cleanup?.(element);
|
|
136
|
+
element.remove();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MODE B: LOGIC (Standard Pass-through)
|
|
142
|
+
else {
|
|
143
|
+
return {
|
|
144
|
+
...config,
|
|
145
|
+
name,
|
|
146
|
+
isEnabled: initialEnabled,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
package/tsconfig.json
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import dts from 'vite-plugin-dts';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
build: {
|
|
7
|
+
lib: {
|
|
8
|
+
entry: path.resolve(__dirname, 'src/index.ts'),
|
|
9
|
+
name: 'SupermouseUtils',
|
|
10
|
+
fileName: (format) => format === 'es' ? 'index.mjs' : 'index.umd.js',
|
|
11
|
+
},
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
external: ['@supermousejs/core'],
|
|
14
|
+
output: {
|
|
15
|
+
globals: {
|
|
16
|
+
'@supermousejs/core': 'SupermouseCore'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
plugins: [dts({ rollupTypes: true })]
|
|
22
|
+
});
|