@supermousejs/utils 2.3.1 → 2.4.1
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 +35 -0
- package/README.md +613 -8
- package/dist/css.d.ts +14 -0
- package/dist/css.d.ts.map +1 -1
- package/dist/doctor.d.ts +7 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/dom.d.ts +24 -14
- package/dist/dom.d.ts.map +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +425 -131
- package/dist/index.umd.js +5 -1
- package/dist/math.d.ts +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/options.d.ts +30 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/plugin.d.ts +99 -22
- package/dist/plugin.d.ts.map +1 -1
- package/dist/svg.d.ts +36 -0
- package/dist/svg.d.ts.map +1 -0
- package/package.json +6 -4
- package/src/css.ts +18 -0
- package/src/doctor.ts +285 -32
- package/src/dom.ts +53 -31
- package/src/index.ts +15 -11
- package/src/math.ts +4 -0
- package/src/options.ts +65 -22
- package/src/plugin.ts +183 -119
- package/src/svg.ts +119 -0
- package/dist/layers.d.ts +0 -15
- package/dist/layers.d.ts.map +0 -1
- package/src/layers.ts +0 -17
package/src/doctor.ts
CHANGED
|
@@ -1,47 +1,300 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export interface DoctorIssue {
|
|
2
|
+
severity: "error" | "warn" | "info";
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface PluginLike {
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
readonly priority?: number;
|
|
11
|
+
readonly element?: HTMLElement;
|
|
12
|
+
readonly update?: (...args: any[]) => void;
|
|
13
|
+
readonly [key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AppLike {
|
|
17
|
+
readonly plugins?: readonly PluginLike[];
|
|
18
|
+
readonly options?: Readonly<Record<string, unknown>>;
|
|
19
|
+
readonly state?: {
|
|
20
|
+
readonly cursorMode?: "auto" | "custom" | "native" | "both";
|
|
21
|
+
readonly hasReceivedInput?: boolean;
|
|
22
|
+
};
|
|
23
|
+
readonly input?: {
|
|
24
|
+
readonly isEnabled?: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const BRAND = "[supermouse] doctor";
|
|
29
|
+
|
|
30
|
+
const S = {
|
|
31
|
+
brand:
|
|
32
|
+
"color: #ffffff; background: #42b883; padding: 2px 4px; border-radius: 2px; font-weight: 700;",
|
|
33
|
+
error:
|
|
34
|
+
"color: #ffffff; background: #f43f5e; padding: 2px 4px; border-radius: 2px; font-weight: 700;",
|
|
35
|
+
warn: "color: #1f2937; background: #f59e0b; padding: 2px 4px; border-radius: 2px; font-weight: 700;",
|
|
36
|
+
info: "color: #ffffff; background: #3b82f6; padding: 2px 4px; border-radius: 2px; font-weight: 700;",
|
|
37
|
+
dim: "color: #6b7280;",
|
|
38
|
+
code: "color: #a1a1aa; font-weight: 500;",
|
|
39
|
+
hint: "color: #10b981; font-weight: 600;"
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export function doctor(app?: any): void {
|
|
43
|
+
const issues = app ? audit(app) : scanDom();
|
|
44
|
+
printReport(issues);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function audit(app: any): readonly DoctorIssue[] {
|
|
48
|
+
const plugins = app.plugins ?? [];
|
|
49
|
+
const opts = app.options ?? {};
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
...checkPluginPriorities(plugins),
|
|
53
|
+
...checkStatesPriority(plugins),
|
|
54
|
+
...checkGhostElements(plugins),
|
|
55
|
+
...checkMultiInstance(opts),
|
|
56
|
+
...checkContainerPosition(opts),
|
|
57
|
+
...checkCursorMode(opts, app.state),
|
|
58
|
+
...scanDom(app)
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isVisualPlugin(plugin: PluginLike): boolean {
|
|
63
|
+
if (plugin.element !== undefined) return true;
|
|
64
|
+
if (typeof plugin.update === "function" && plugin.update.length >= 3) return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isLogicPlugin(plugin: PluginLike): boolean {
|
|
69
|
+
if (plugin.element !== undefined) return false;
|
|
70
|
+
if (typeof plugin.update === "function" && plugin.update.length === 2) return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkPluginPriorities(plugins: readonly PluginLike[]): readonly DoctorIssue[] {
|
|
75
|
+
const out: DoctorIssue[] = [];
|
|
76
|
+
|
|
77
|
+
for (const p of plugins) {
|
|
78
|
+
const name = p.name ?? "unknown";
|
|
79
|
+
const pri = p.priority ?? 0;
|
|
80
|
+
|
|
81
|
+
if (isLogicPlugin(p) && pri >= 0) {
|
|
82
|
+
out.push({
|
|
83
|
+
severity: "warn",
|
|
84
|
+
code: "PRIORITY_LOGIC",
|
|
85
|
+
message: `Plugin "${name}" has priority ${pri} but appears to be a logic plugin (should run before physics).`,
|
|
86
|
+
hint: `Set priority to a negative value (e.g. -10) on "${name}"`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isVisualPlugin(p) && pri < 0) {
|
|
91
|
+
out.push({
|
|
92
|
+
severity: "warn",
|
|
93
|
+
code: "PRIORITY_VISUAL",
|
|
94
|
+
message: `Plugin "${name}" has priority ${pri} but appears to be a visual plugin (should run after physics).`,
|
|
95
|
+
hint: "Remove priority or use a positive value"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkStatesPriority(plugins: readonly PluginLike[]): readonly DoctorIssue[] {
|
|
104
|
+
const states = plugins.find((p) => p.name === "states");
|
|
105
|
+
if (!states) return [];
|
|
106
|
+
|
|
107
|
+
const pri = states.priority ?? 0;
|
|
108
|
+
if (pri >= 0) {
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
severity: "error",
|
|
112
|
+
code: "STATES_PRIORITY",
|
|
113
|
+
message: `States plugin has priority ${pri}. It must run before all visual plugins.`,
|
|
114
|
+
hint: "Set priority: -999 on the States plugin"
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function checkGhostElements(plugins: readonly PluginLike[]): readonly DoctorIssue[] {
|
|
123
|
+
const out: DoctorIssue[] = [];
|
|
124
|
+
|
|
125
|
+
for (const p of plugins) {
|
|
126
|
+
const name = p.name ?? "unknown";
|
|
127
|
+
if (isVisualPlugin(p) && !p.element) {
|
|
128
|
+
out.push({
|
|
129
|
+
severity: "warn",
|
|
130
|
+
code: "MISSING_ELEMENT",
|
|
131
|
+
message: `Plugin "${name}" has no 'element' property — disablePlugin() will not hide its DOM.`,
|
|
132
|
+
hint: "Assign plugin.element = el in install(), or use definePlugin()"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkMultiInstance(opts: Readonly<Record<string, unknown>>): readonly DoctorIssue[] {
|
|
141
|
+
const scopes = document.querySelectorAll('[class*="supermouse-scope-"]');
|
|
142
|
+
if (scopes.length <= 1) return [];
|
|
143
|
+
if (opts.container !== document.body) return [];
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
severity: "warn",
|
|
148
|
+
code: "MULTI_INSTANCE",
|
|
149
|
+
message: `Detected ${scopes.length} Supermouse instances. The body instance may conflict with scoped ones.`,
|
|
150
|
+
hint: "Use suspend() / resume() or scope instances to different containers"
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function checkContainerPosition(opts: Readonly<Record<string, unknown>>): readonly DoctorIssue[] {
|
|
156
|
+
const container = opts.container as HTMLElement | undefined;
|
|
157
|
+
if (!container || container === document.body) return [];
|
|
158
|
+
|
|
159
|
+
const position = window.getComputedStyle(container).position;
|
|
160
|
+
if (position !== "static") return [];
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
severity: "info",
|
|
165
|
+
code: "CONTAINER_POSITION",
|
|
166
|
+
message: "Container has position:static. Supermouse will mutate it to relative.",
|
|
167
|
+
hint: "Set position: relative in your CSS to avoid the mutation"
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkCursorMode(
|
|
173
|
+
opts: Readonly<Record<string, unknown>>,
|
|
174
|
+
state?: AppLike["state"]
|
|
175
|
+
): readonly DoctorIssue[] {
|
|
176
|
+
const cursor = opts.cursor;
|
|
177
|
+
if (cursor === undefined) return [];
|
|
178
|
+
|
|
179
|
+
const validModes = ["auto", "custom", "native", "both"];
|
|
180
|
+
if (typeof cursor === "string" && !validModes.includes(cursor)) {
|
|
181
|
+
return [
|
|
182
|
+
{
|
|
183
|
+
severity: "error",
|
|
184
|
+
code: "INVALID_CURSOR_MODE",
|
|
185
|
+
message: `Invalid cursor mode "${cursor}". Expected one of: ${validModes.join(", ")}.`,
|
|
186
|
+
hint: `Set cursor to one of: ${validModes.join(", ")}`
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (cursor === "both" && opts.container !== document.body && state?.cursorMode === "both") {
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
severity: "warn",
|
|
195
|
+
code: "NESTED_BOTH_MODE",
|
|
196
|
+
message:
|
|
197
|
+
"Scoped instance uses cursor: 'both'. In nested scopes, the native cursor may be hidden by the outer instance's cursor suppression.",
|
|
198
|
+
hint: "Offset the custom cursor when cursorMode === 'both' or consider a single-instance scope architecture"
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function scanDom(app?: AppLike): readonly DoctorIssue[] {
|
|
207
|
+
const out: DoctorIssue[] = [];
|
|
6
208
|
|
|
7
|
-
const
|
|
209
|
+
const isInitialized = Array.from(document.querySelectorAll("style")).some((s) =>
|
|
8
210
|
s.id.startsWith("supermouse-style-")
|
|
9
211
|
);
|
|
10
212
|
|
|
11
|
-
if (!
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
213
|
+
if (!isInitialized) {
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
severity: "error",
|
|
217
|
+
code: "NOT_INITIALIZED",
|
|
218
|
+
message: "Supermouse has not been initialized yet.",
|
|
219
|
+
hint: "Call doctor() after new Supermouse() has run"
|
|
220
|
+
}
|
|
221
|
+
];
|
|
16
222
|
}
|
|
17
223
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (el.getAttribute("style")?.includes("cursor: none")) return;
|
|
224
|
+
document.querySelectorAll('[style*="cursor"]').forEach((el) => {
|
|
225
|
+
const style = el.getAttribute("style") ?? "";
|
|
226
|
+
if (style.includes("cursor: none")) return;
|
|
227
|
+
if (!style.includes("cursor")) return;
|
|
23
228
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
229
|
+
out.push({
|
|
230
|
+
severity: "warn",
|
|
231
|
+
code: "INLINE_CURSOR",
|
|
232
|
+
message: "Element has an inline cursor style that may override Supermouse.",
|
|
233
|
+
hint: "Remove inline cursor styles"
|
|
27
234
|
});
|
|
28
235
|
});
|
|
29
236
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
237
|
+
const bodyInstance = document.querySelector(".supermouse-scope-0");
|
|
238
|
+
if (bodyInstance) {
|
|
239
|
+
const hasHideClass = bodyInstance.classList.contains("supermouse-hide-0");
|
|
240
|
+
const cursorMode = app?.state?.cursorMode ?? "auto";
|
|
241
|
+
const hasReceivedInput = app?.state?.hasReceivedInput ?? true;
|
|
242
|
+
const inputEnabled = app?.input?.isEnabled ?? true;
|
|
243
|
+
|
|
244
|
+
if (cursorMode === "custom" && inputEnabled && hasReceivedInput && !hasHideClass) {
|
|
245
|
+
out.push({
|
|
246
|
+
severity: "warn",
|
|
247
|
+
code: "BODY_CURSOR_LEAK",
|
|
248
|
+
message:
|
|
249
|
+
"Body instance has cursor: 'custom' but native cursor suppression class is not applied.",
|
|
250
|
+
hint: "Check that the animation loop is running and setCursor('custom') was called"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function printReport(issues: readonly DoctorIssue[]): void {
|
|
259
|
+
if (issues.length === 0) {
|
|
260
|
+
console.log(`%c${BRAND}%c No issues found`, S.brand, S.dim);
|
|
261
|
+
return;
|
|
34
262
|
}
|
|
35
263
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
issues
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
264
|
+
const summary = [
|
|
265
|
+
formatCount(issues, "error", "error"),
|
|
266
|
+
formatCount(issues, "warn", "warning"),
|
|
267
|
+
formatCount(issues, "info", "note")
|
|
268
|
+
]
|
|
269
|
+
.filter((s): s is string => s !== null)
|
|
270
|
+
.join(" · ");
|
|
271
|
+
|
|
272
|
+
console.log(`%c${BRAND}%c ${summary}`, S.brand, S.dim);
|
|
273
|
+
|
|
274
|
+
for (const issue of issues) {
|
|
275
|
+
printIssue(issue);
|
|
44
276
|
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function formatCount(
|
|
280
|
+
issues: readonly DoctorIssue[],
|
|
281
|
+
severity: DoctorIssue["severity"],
|
|
282
|
+
label: string
|
|
283
|
+
): string | null {
|
|
284
|
+
const n = issues.filter((i) => i.severity === severity).length;
|
|
285
|
+
return n > 0 ? `${n} ${label}${n > 1 ? "s" : ""}` : null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function printIssue(issue: DoctorIssue): void {
|
|
289
|
+
console.log(
|
|
290
|
+
`%c${issue.severity.toUpperCase()}%c %c${issue.code}%c ${issue.message}`,
|
|
291
|
+
S[issue.severity],
|
|
292
|
+
S.dim,
|
|
293
|
+
S.code,
|
|
294
|
+
"color: inherit;"
|
|
295
|
+
);
|
|
45
296
|
|
|
46
|
-
|
|
297
|
+
if (issue.hint) {
|
|
298
|
+
console.log(` %c→%c ${issue.hint}`, S.hint, S.dim);
|
|
299
|
+
}
|
|
47
300
|
}
|
package/src/dom.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @param id A unique identifier for this style block
|
|
6
6
|
* @param css A string of CSS rules to inject.
|
|
7
7
|
*/
|
|
8
|
-
export const injectStyles = (id: string, css: string) => {
|
|
8
|
+
export const injectStyles = (id: string, css: string): void => {
|
|
9
9
|
if (typeof document === "undefined") return;
|
|
10
10
|
if (document.getElementById(id)) return;
|
|
11
11
|
|
|
@@ -15,16 +15,31 @@ export const injectStyles = (id: string, css: string) => {
|
|
|
15
15
|
document.head.appendChild(style);
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const styleCache = new WeakMap<HTMLElement | SVGElement, Record<string, string | number>>();
|
|
19
|
+
|
|
20
|
+
type CSSProperties = {
|
|
21
|
+
[K in keyof CSSStyleDeclaration as CSSStyleDeclaration[K] extends (...args: any) => any
|
|
22
|
+
? never
|
|
23
|
+
: K]?: string | number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function camelToKebab(str: string): string {
|
|
27
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* @
|
|
31
|
+
* Applies CSS properties to an element. Only touches the DOM when a value
|
|
32
|
+
* has actually changed.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* css(el, {
|
|
36
|
+
* width: `${size}px`,
|
|
37
|
+
* height: `${size}px`,
|
|
38
|
+
* opacity: state.isHover ? 1 : 0,
|
|
39
|
+
* backgroundColor: state.interaction.color || "#000",
|
|
40
|
+
* });
|
|
26
41
|
*/
|
|
27
|
-
export function
|
|
42
|
+
export function css(el: HTMLElement | SVGElement, styles: CSSProperties): void {
|
|
28
43
|
if (typeof document === "undefined" || !el) return;
|
|
29
44
|
|
|
30
45
|
let cache = styleCache.get(el);
|
|
@@ -33,28 +48,32 @@ export function applyStyles(el: HTMLElement, styles: Partial<CSSStyleDeclaration
|
|
|
33
48
|
styleCache.set(el, cache);
|
|
34
49
|
}
|
|
35
50
|
|
|
36
|
-
for (const prop
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
for (const [prop, value] of Object.entries(styles)) {
|
|
52
|
+
if (value === undefined) continue;
|
|
53
|
+
const stringValue = String(value);
|
|
54
|
+
const kebabProp = camelToKebab(prop);
|
|
55
|
+
if (cache[kebabProp] !== stringValue) {
|
|
56
|
+
el.style.setProperty(kebabProp, stringValue);
|
|
57
|
+
cache[kebabProp] = stringValue;
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
/**
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
* @deprecated Use `dom.css()` instead. This function will be removed in future versions.
|
|
64
|
+
*/
|
|
65
|
+
export function setStyle(el: HTMLElement | SVGElement, prop: string, value: string | number): void {
|
|
66
|
+
css(el, { [prop]: value });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Use `dom.css()` instead. This function will be removed in future versions.
|
|
51
71
|
*/
|
|
52
|
-
export function
|
|
53
|
-
el: HTMLElement,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
applyStyles(el, { [property]: value } as any);
|
|
72
|
+
export function applyStyles(
|
|
73
|
+
el: HTMLElement | SVGElement,
|
|
74
|
+
styles: Record<string, string | number>
|
|
75
|
+
): void {
|
|
76
|
+
css(el, styles);
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
/**
|
|
@@ -71,7 +90,7 @@ export function setStyle(
|
|
|
71
90
|
* @param skewY Skew Y (deg) - Default 0
|
|
72
91
|
*/
|
|
73
92
|
export function setTransform(
|
|
74
|
-
el: HTMLElement,
|
|
93
|
+
el: HTMLElement | SVGElement,
|
|
75
94
|
x: number,
|
|
76
95
|
y: number,
|
|
77
96
|
rotation: number = 0,
|
|
@@ -79,16 +98,19 @@ export function setTransform(
|
|
|
79
98
|
scaleY: number = 1,
|
|
80
99
|
skewX: number = 0,
|
|
81
100
|
skewY: number = 0
|
|
82
|
-
) {
|
|
101
|
+
): void {
|
|
83
102
|
const transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) rotate(${rotation}deg) skew(${skewX}deg, ${skewY}deg) scale(${scaleX}, ${scaleY})`;
|
|
84
103
|
|
|
85
|
-
|
|
104
|
+
css(el, { transform });
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
/**
|
|
89
108
|
* Calculates the bounding rectangle of an element relative to a container.
|
|
90
109
|
*/
|
|
91
|
-
export function projectRect(
|
|
110
|
+
export function projectRect(
|
|
111
|
+
element: HTMLElement | SVGSVGElement,
|
|
112
|
+
container: HTMLElement | SVGSVGElement = document.body
|
|
113
|
+
): DOMRect {
|
|
92
114
|
const rect = element.getBoundingClientRect();
|
|
93
115
|
|
|
94
116
|
if (container !== document.body) {
|
|
@@ -108,9 +130,9 @@ export function projectRect(element: HTMLElement, container: HTMLElement = docum
|
|
|
108
130
|
*
|
|
109
131
|
* @param tagName The HTML tag to create (default: 'div')
|
|
110
132
|
*/
|
|
111
|
-
export function createActor(tagName: string = "div"): HTMLElement {
|
|
133
|
+
export function createActor(tagName: string = "div"): HTMLElement | SVGSVGElement {
|
|
112
134
|
const el = document.createElement(tagName);
|
|
113
|
-
|
|
135
|
+
css(el, {
|
|
114
136
|
position: "absolute",
|
|
115
137
|
top: "0",
|
|
116
138
|
left: "0",
|
|
@@ -127,7 +149,7 @@ export function createActor(tagName: string = "div"): HTMLElement {
|
|
|
127
149
|
*/
|
|
128
150
|
export function createCircle(size: number, color: string): HTMLDivElement {
|
|
129
151
|
const el = createActor("div") as HTMLDivElement;
|
|
130
|
-
|
|
152
|
+
css(el, {
|
|
131
153
|
width: `${size}px`,
|
|
132
154
|
height: `${size}px`,
|
|
133
155
|
borderRadius: "50%",
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export * from "./
|
|
7
|
-
export * from "./
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
export * from "./math";
|
|
2
|
+
export * from "./dom";
|
|
3
|
+
export * from "./effects";
|
|
4
|
+
export * from "./options";
|
|
5
|
+
export * from "./doctor";
|
|
6
|
+
export * from "./plugin";
|
|
7
|
+
export * from "./svg";
|
|
8
|
+
export * from "./css";
|
|
9
|
+
|
|
10
|
+
import * as math from "./math";
|
|
11
|
+
import * as dom from "./dom";
|
|
12
|
+
import * as effects from "./effects";
|
|
13
|
+
import * as svg from "./svg";
|
|
14
|
+
|
|
15
|
+
export { math, dom, effects, svg };
|
package/src/math.ts
CHANGED
|
@@ -58,3 +58,7 @@ export function dist(x1: number, y1: number, x2: number = 0, y2: number = 0): nu
|
|
|
58
58
|
export function angle(x: number, y: number): number {
|
|
59
59
|
return Math.atan2(y, x) * (180 / Math.PI);
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
export function circumference(r: number): number {
|
|
63
|
+
return 2 * Math.PI * r;
|
|
64
|
+
}
|
package/src/options.ts
CHANGED
|
@@ -1,22 +1,65 @@
|
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
* @returns A function that returns the resolved value of the option
|
|
11
|
+
*/
|
|
12
|
+
export function normalize<T>(
|
|
13
|
+
option: ValueOrGetter<T> | undefined,
|
|
14
|
+
defaultValue: T
|
|
15
|
+
): (state: MouseState) => T {
|
|
16
|
+
if (option === undefined) {
|
|
17
|
+
return () => defaultValue;
|
|
18
|
+
}
|
|
19
|
+
if (typeof option === "function") {
|
|
20
|
+
return option as (state: MouseState) => T;
|
|
21
|
+
}
|
|
22
|
+
return () => option;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes multiple options in one call.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const cfg = normalizeAll(options, {
|
|
30
|
+
* size: 20,
|
|
31
|
+
* color: "#fff",
|
|
32
|
+
* opacity: 1,
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // In update():
|
|
36
|
+
* const size = cfg.size(app.state);
|
|
37
|
+
*
|
|
38
|
+
* @param defaults Default values for the options
|
|
39
|
+
* @param options User-provided options to normalize
|
|
40
|
+
* @returns An object with normalized getter functions for each option
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeAll<T extends Record<string, unknown>>(
|
|
43
|
+
options: Partial<{ [K in keyof T]: ValueOrGetter<T[K]> }>,
|
|
44
|
+
defaults: T
|
|
45
|
+
): { [K in keyof T]: (state: MouseState) => T[K] } {
|
|
46
|
+
const result = {} as { [K in keyof T]: (state: MouseState) => T[K] };
|
|
47
|
+
|
|
48
|
+
for (const key of Object.keys(defaults) as Array<keyof T>) {
|
|
49
|
+
result[key] = normalize(
|
|
50
|
+
options[key] as ValueOrGetter<T[typeof key]> | undefined,
|
|
51
|
+
defaults[key]
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks if the current device has a fine pointer (e.g., mouse) or a coarse pointer (e.g., touch).
|
|
60
|
+
* This is useful for conditionally enabling or disabling cursor effects based on the input device.
|
|
61
|
+
*
|
|
62
|
+
* @returns True if the device has a fine pointer, false otherwise.
|
|
63
|
+
*/
|
|
64
|
+
export const hasFinePointer = (): boolean =>
|
|
65
|
+
typeof window !== "undefined" && window.matchMedia("(pointer: fine)").matches;
|