@supermousejs/utils 2.2.0 → 2.3.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 +81 -74
- package/dist/options.d.ts +1 -1
- package/dist/plugin.d.ts +1 -1
- package/package.json +3 -3
- package/src/dom.ts +145 -145
- package/src/effects.ts +34 -34
- package/src/math.ts +60 -60
- package/src/plugin.ts +147 -147
- package/tsconfig.json +9 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,74 +1,81 @@
|
|
|
1
|
-
# @supermousejs/utils
|
|
2
|
-
|
|
3
|
-
## 2.
|
|
4
|
-
|
|
5
|
-
###
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- Updated dependencies
|
|
47
|
-
- @supermousejs/core@2.0.
|
|
48
|
-
|
|
49
|
-
## 2.0.
|
|
50
|
-
|
|
51
|
-
### Patch Changes
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
# @supermousejs/utils
|
|
2
|
+
|
|
3
|
+
## 2.3.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [f6f44b2]
|
|
8
|
+
- @supermousejs/core@2.2.0
|
|
9
|
+
|
|
10
|
+
## 2.2.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 9fe1a7b: - Implemented idiomatic approach to using supermouse/utils
|
|
15
|
+
- Rewrote `applyStyles`, `setTransform` and `setStyles` to share a global cache registry
|
|
16
|
+
- 9fa6ece: Add `injectStyles` to safely handle injecting global CSS styles into the document head
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 6d70c18: remove legacy package and update supermouse domain in readme
|
|
21
|
+
- 14fb5b6: Updated tsconfig to be reference-compliant with core, utils and zoetrope when required
|
|
22
|
+
- Updated dependencies [6d70c18]
|
|
23
|
+
- Updated dependencies [2590af3]
|
|
24
|
+
- Updated dependencies [14fb5b6]
|
|
25
|
+
- @supermousejs/core@2.1.0
|
|
26
|
+
|
|
27
|
+
## 2.1.1
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 67f771b: Add relevant npm metadata to package.json file
|
|
32
|
+
- Updated dependencies [67f771b]
|
|
33
|
+
- @supermousejs/core@2.0.5
|
|
34
|
+
|
|
35
|
+
## 2.1.0
|
|
36
|
+
|
|
37
|
+
### Minor Changes
|
|
38
|
+
|
|
39
|
+
- 0a1652d: fixed build architecture and updated plugin metadata
|
|
40
|
+
|
|
41
|
+
## 2.0.4
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- 993dc67: Updated supemousejs packages with proper author, license and url descriptors to repo
|
|
46
|
+
- Updated dependencies [993dc67]
|
|
47
|
+
- @supermousejs/core@2.0.4
|
|
48
|
+
|
|
49
|
+
## 2.0.3
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- Updated dependencies
|
|
54
|
+
- @supermousejs/core@2.0.3
|
|
55
|
+
|
|
56
|
+
## 2.0.2
|
|
57
|
+
|
|
58
|
+
### Patch Changes
|
|
59
|
+
|
|
60
|
+
- ae219a0: Update READMEs with correct link to documentation
|
|
61
|
+
- Updated dependencies [ae219a0]
|
|
62
|
+
- @supermousejs/core@2.0.2
|
|
63
|
+
|
|
64
|
+
## 2.0.1
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- Add minimal README.md files to packages
|
|
69
|
+
- Updated dependencies
|
|
70
|
+
- @supermousejs/core@2.0.1
|
|
71
|
+
|
|
72
|
+
## 2.0.0
|
|
73
|
+
|
|
74
|
+
### Major Changes
|
|
75
|
+
|
|
76
|
+
- Initial v2.0.0 release
|
|
77
|
+
|
|
78
|
+
### Patch Changes
|
|
79
|
+
|
|
80
|
+
- Updated dependencies
|
|
81
|
+
- @supermousejs/core@2.0.0
|
package/dist/options.d.ts
CHANGED
package/dist/plugin.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supermousejs/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"main": "dist/index.umd.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@supermousejs/core": "2.
|
|
30
|
+
"@supermousejs/core": "2.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@supermousejs/core": "2.
|
|
33
|
+
"@supermousejs/core": "2.2.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "vite build"
|
package/src/dom.ts
CHANGED
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Injects global CSS styles into the document head safely.
|
|
3
|
-
* Checks for existing IDs to prevent duplication during SPA routing or HMR.
|
|
4
|
-
*
|
|
5
|
-
* @param id A unique identifier for this style block
|
|
6
|
-
* @param css A string of CSS rules to inject.
|
|
7
|
-
*/
|
|
8
|
-
export const injectStyles = (id: string, css: string) => {
|
|
9
|
-
if (typeof document === "undefined") return;
|
|
10
|
-
if (document.getElementById(id)) return;
|
|
11
|
-
|
|
12
|
-
const style = document.createElement("style");
|
|
13
|
-
style.id = id;
|
|
14
|
-
style.innerHTML = css;
|
|
15
|
-
document.head.appendChild(style);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// WeakMap to store previous styles for elements to prevent DOM thrashing
|
|
19
|
-
const styleCache = new WeakMap<HTMLElement, Record<string, string | number>>();
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Smart Style Setter (Batch).
|
|
23
|
-
* Only writes to the DOM if the value has actually changed.
|
|
24
|
-
* @param el The element to style
|
|
25
|
-
* @param styles An object of CSS properties and values
|
|
26
|
-
*/
|
|
27
|
-
export function applyStyles(el: HTMLElement, styles: Partial<CSSStyleDeclaration>) {
|
|
28
|
-
if (typeof document === "undefined" || !el) return;
|
|
29
|
-
|
|
30
|
-
let cache = styleCache.get(el);
|
|
31
|
-
if (!cache) {
|
|
32
|
-
cache = {};
|
|
33
|
-
styleCache.set(el, cache);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
for (const prop in styles) {
|
|
37
|
-
const value = (styles as any)[prop];
|
|
38
|
-
if (cache[prop] !== value) {
|
|
39
|
-
(el.style as any)[prop] = value;
|
|
40
|
-
cache[prop] = value;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Smart Style Setter (Single).
|
|
47
|
-
* Proxies to applyStyles for consistency.
|
|
48
|
-
* @param el The element to style
|
|
49
|
-
* @param property The CSS property to set
|
|
50
|
-
* @param value The value to set for the property
|
|
51
|
-
*/
|
|
52
|
-
export function setStyle(
|
|
53
|
-
el: HTMLElement,
|
|
54
|
-
property: keyof CSSStyleDeclaration,
|
|
55
|
-
value: string | number
|
|
56
|
-
) {
|
|
57
|
-
applyStyles(el, { [property]: value } as any);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Universal Transform Setter.
|
|
62
|
-
* Handles centering (-50%) automatically.
|
|
63
|
-
*
|
|
64
|
-
* @param el The element
|
|
65
|
-
* @param x X Position (px)
|
|
66
|
-
* @param y Y Position (px)
|
|
67
|
-
* @param rotation Rotation (deg) - Default 0
|
|
68
|
-
* @param scaleX Scale X - Default 1
|
|
69
|
-
* @param scaleY Scale Y - Default 1
|
|
70
|
-
* @param skewX Skew X (deg) - Default 0
|
|
71
|
-
* @param skewY Skew Y (deg) - Default 0
|
|
72
|
-
*/
|
|
73
|
-
export function setTransform(
|
|
74
|
-
el: HTMLElement,
|
|
75
|
-
x: number,
|
|
76
|
-
y: number,
|
|
77
|
-
rotation: number = 0,
|
|
78
|
-
scaleX: number = 1,
|
|
79
|
-
scaleY: number = 1,
|
|
80
|
-
skewX: number = 0,
|
|
81
|
-
skewY: number = 0
|
|
82
|
-
) {
|
|
83
|
-
const transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) rotate(${rotation}deg) skew(${skewX}deg, ${skewY}deg) scale(${scaleX}, ${scaleY})`;
|
|
84
|
-
|
|
85
|
-
setStyle(el, "transform", transform);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Calculates the bounding rectangle of an element relative to a container.
|
|
90
|
-
*/
|
|
91
|
-
export function projectRect(element: HTMLElement, container: HTMLElement = document.body): DOMRect {
|
|
92
|
-
const rect = element.getBoundingClientRect();
|
|
93
|
-
|
|
94
|
-
if (container !== document.body) {
|
|
95
|
-
const containerRect = container.getBoundingClientRect();
|
|
96
|
-
const x = rect.left - containerRect.left;
|
|
97
|
-
const y = rect.top - containerRect.top;
|
|
98
|
-
|
|
99
|
-
return new DOMRect(x, y, rect.width, rect.height);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return rect;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Creates a standard Supermouse actor element with optimal performance settings.
|
|
107
|
-
* Includes absolute positioning, pointer-events: none, and will-change: transform.
|
|
108
|
-
*
|
|
109
|
-
* @param tagName The HTML tag to create (default: 'div')
|
|
110
|
-
*/
|
|
111
|
-
export function createActor(tagName: string = "div"): HTMLElement {
|
|
112
|
-
const el = document.createElement(tagName);
|
|
113
|
-
applyStyles(el, {
|
|
114
|
-
position: "absolute",
|
|
115
|
-
top: "0",
|
|
116
|
-
left: "0",
|
|
117
|
-
pointerEvents: "none",
|
|
118
|
-
boxSizing: "border-box",
|
|
119
|
-
display: "block",
|
|
120
|
-
willChange: "transform"
|
|
121
|
-
});
|
|
122
|
-
return el;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Creates a circular HTML div using the standard actor base.
|
|
127
|
-
*/
|
|
128
|
-
export function createCircle(size: number, color: string): HTMLDivElement {
|
|
129
|
-
const el = createActor("div") as HTMLDivElement;
|
|
130
|
-
applyStyles(el, {
|
|
131
|
-
width: `${size}px`,
|
|
132
|
-
height: `${size}px`,
|
|
133
|
-
borderRadius: "50%",
|
|
134
|
-
backgroundColor: color
|
|
135
|
-
});
|
|
136
|
-
return el;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Legacy alias for createActor.
|
|
141
|
-
* @deprecated Use createActor() instead.
|
|
142
|
-
*/
|
|
143
|
-
export function createDiv(): HTMLDivElement {
|
|
144
|
-
return createActor("div") as HTMLDivElement;
|
|
145
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Injects global CSS styles into the document head safely.
|
|
3
|
+
* Checks for existing IDs to prevent duplication during SPA routing or HMR.
|
|
4
|
+
*
|
|
5
|
+
* @param id A unique identifier for this style block
|
|
6
|
+
* @param css A string of CSS rules to inject.
|
|
7
|
+
*/
|
|
8
|
+
export const injectStyles = (id: string, css: string) => {
|
|
9
|
+
if (typeof document === "undefined") return;
|
|
10
|
+
if (document.getElementById(id)) return;
|
|
11
|
+
|
|
12
|
+
const style = document.createElement("style");
|
|
13
|
+
style.id = id;
|
|
14
|
+
style.innerHTML = css;
|
|
15
|
+
document.head.appendChild(style);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// WeakMap to store previous styles for elements to prevent DOM thrashing
|
|
19
|
+
const styleCache = new WeakMap<HTMLElement, Record<string, string | number>>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Smart Style Setter (Batch).
|
|
23
|
+
* Only writes to the DOM if the value has actually changed.
|
|
24
|
+
* @param el The element to style
|
|
25
|
+
* @param styles An object of CSS properties and values
|
|
26
|
+
*/
|
|
27
|
+
export function applyStyles(el: HTMLElement, styles: Partial<CSSStyleDeclaration>) {
|
|
28
|
+
if (typeof document === "undefined" || !el) return;
|
|
29
|
+
|
|
30
|
+
let cache = styleCache.get(el);
|
|
31
|
+
if (!cache) {
|
|
32
|
+
cache = {};
|
|
33
|
+
styleCache.set(el, cache);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const prop in styles) {
|
|
37
|
+
const value = (styles as any)[prop];
|
|
38
|
+
if (cache[prop] !== value) {
|
|
39
|
+
(el.style as any)[prop] = value;
|
|
40
|
+
cache[prop] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Smart Style Setter (Single).
|
|
47
|
+
* Proxies to applyStyles for consistency.
|
|
48
|
+
* @param el The element to style
|
|
49
|
+
* @param property The CSS property to set
|
|
50
|
+
* @param value The value to set for the property
|
|
51
|
+
*/
|
|
52
|
+
export function setStyle(
|
|
53
|
+
el: HTMLElement,
|
|
54
|
+
property: keyof CSSStyleDeclaration,
|
|
55
|
+
value: string | number
|
|
56
|
+
) {
|
|
57
|
+
applyStyles(el, { [property]: value } as any);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Universal Transform Setter.
|
|
62
|
+
* Handles centering (-50%) automatically.
|
|
63
|
+
*
|
|
64
|
+
* @param el The element
|
|
65
|
+
* @param x X Position (px)
|
|
66
|
+
* @param y Y Position (px)
|
|
67
|
+
* @param rotation Rotation (deg) - Default 0
|
|
68
|
+
* @param scaleX Scale X - Default 1
|
|
69
|
+
* @param scaleY Scale Y - Default 1
|
|
70
|
+
* @param skewX Skew X (deg) - Default 0
|
|
71
|
+
* @param skewY Skew Y (deg) - Default 0
|
|
72
|
+
*/
|
|
73
|
+
export function setTransform(
|
|
74
|
+
el: HTMLElement,
|
|
75
|
+
x: number,
|
|
76
|
+
y: number,
|
|
77
|
+
rotation: number = 0,
|
|
78
|
+
scaleX: number = 1,
|
|
79
|
+
scaleY: number = 1,
|
|
80
|
+
skewX: number = 0,
|
|
81
|
+
skewY: number = 0
|
|
82
|
+
) {
|
|
83
|
+
const transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) rotate(${rotation}deg) skew(${skewX}deg, ${skewY}deg) scale(${scaleX}, ${scaleY})`;
|
|
84
|
+
|
|
85
|
+
setStyle(el, "transform", transform);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Calculates the bounding rectangle of an element relative to a container.
|
|
90
|
+
*/
|
|
91
|
+
export function projectRect(element: HTMLElement, container: HTMLElement = document.body): DOMRect {
|
|
92
|
+
const rect = element.getBoundingClientRect();
|
|
93
|
+
|
|
94
|
+
if (container !== document.body) {
|
|
95
|
+
const containerRect = container.getBoundingClientRect();
|
|
96
|
+
const x = rect.left - containerRect.left;
|
|
97
|
+
const y = rect.top - containerRect.top;
|
|
98
|
+
|
|
99
|
+
return new DOMRect(x, y, rect.width, rect.height);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return rect;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a standard Supermouse actor element with optimal performance settings.
|
|
107
|
+
* Includes absolute positioning, pointer-events: none, and will-change: transform.
|
|
108
|
+
*
|
|
109
|
+
* @param tagName The HTML tag to create (default: 'div')
|
|
110
|
+
*/
|
|
111
|
+
export function createActor(tagName: string = "div"): HTMLElement {
|
|
112
|
+
const el = document.createElement(tagName);
|
|
113
|
+
applyStyles(el, {
|
|
114
|
+
position: "absolute",
|
|
115
|
+
top: "0",
|
|
116
|
+
left: "0",
|
|
117
|
+
pointerEvents: "none",
|
|
118
|
+
boxSizing: "border-box",
|
|
119
|
+
display: "block",
|
|
120
|
+
willChange: "transform"
|
|
121
|
+
});
|
|
122
|
+
return el;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates a circular HTML div using the standard actor base.
|
|
127
|
+
*/
|
|
128
|
+
export function createCircle(size: number, color: string): HTMLDivElement {
|
|
129
|
+
const el = createActor("div") as HTMLDivElement;
|
|
130
|
+
applyStyles(el, {
|
|
131
|
+
width: `${size}px`,
|
|
132
|
+
height: `${size}px`,
|
|
133
|
+
borderRadius: "50%",
|
|
134
|
+
backgroundColor: color
|
|
135
|
+
});
|
|
136
|
+
return el;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Legacy alias for createActor.
|
|
141
|
+
* @deprecated Use createActor() instead.
|
|
142
|
+
*/
|
|
143
|
+
export function createDiv(): HTMLDivElement {
|
|
144
|
+
return createActor("div") as HTMLDivElement;
|
|
145
|
+
}
|
package/src/effects.ts
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { dist, angle, clamp } from "./math";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Calculates Rotation and Scale based on velocity to create a "Squash and Stretch" effect.
|
|
5
|
-
*
|
|
6
|
-
* @param vx Velocity X
|
|
7
|
-
* @param vy Velocity Y
|
|
8
|
-
* @param intensity Stretch factor (default: 0.004)
|
|
9
|
-
* @param maxStretch Max stretch percentage (default: 0.5 = 150% length)
|
|
10
|
-
*/
|
|
11
|
-
export function getVelocityDistortion(vx: number, vy: number, intensity = 0.004, maxStretch = 0.5) {
|
|
12
|
-
const speed = dist(vx, vy);
|
|
13
|
-
|
|
14
|
-
// Deadzone: If moving too slow, don't rotate (prevents jittering at rest)
|
|
15
|
-
if (speed < 0.1) {
|
|
16
|
-
return { rotation: 0, scaleX: 1, scaleY: 1 };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// 1. Point towards movement
|
|
20
|
-
const rotation = angle(vx, vy);
|
|
21
|
-
|
|
22
|
-
// 2. Stretch based on speed
|
|
23
|
-
const stretch = clamp(speed * intensity, 0, maxStretch);
|
|
24
|
-
|
|
25
|
-
// 3. Scale X grows, Scale Y shrinks (to preserve volume-ish)
|
|
26
|
-
const scaleX = 1 + stretch;
|
|
27
|
-
const scaleY = 1 - stretch * 0.5; // Squash factor
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
rotation,
|
|
31
|
-
scaleX,
|
|
32
|
-
scaleY
|
|
33
|
-
};
|
|
34
|
-
}
|
|
1
|
+
import { dist, angle, clamp } from "./math";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculates Rotation and Scale based on velocity to create a "Squash and Stretch" effect.
|
|
5
|
+
*
|
|
6
|
+
* @param vx Velocity X
|
|
7
|
+
* @param vy Velocity Y
|
|
8
|
+
* @param intensity Stretch factor (default: 0.004)
|
|
9
|
+
* @param maxStretch Max stretch percentage (default: 0.5 = 150% length)
|
|
10
|
+
*/
|
|
11
|
+
export function getVelocityDistortion(vx: number, vy: number, intensity = 0.004, maxStretch = 0.5) {
|
|
12
|
+
const speed = dist(vx, vy);
|
|
13
|
+
|
|
14
|
+
// Deadzone: If moving too slow, don't rotate (prevents jittering at rest)
|
|
15
|
+
if (speed < 0.1) {
|
|
16
|
+
return { rotation: 0, scaleX: 1, scaleY: 1 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 1. Point towards movement
|
|
20
|
+
const rotation = angle(vx, vy);
|
|
21
|
+
|
|
22
|
+
// 2. Stretch based on speed
|
|
23
|
+
const stretch = clamp(speed * intensity, 0, maxStretch);
|
|
24
|
+
|
|
25
|
+
// 3. Scale X grows, Scale Y shrinks (to preserve volume-ish)
|
|
26
|
+
const scaleX = 1 + stretch;
|
|
27
|
+
const scaleY = 1 - stretch * 0.5; // Squash factor
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
rotation,
|
|
31
|
+
scaleX,
|
|
32
|
+
scaleY
|
|
33
|
+
};
|
|
34
|
+
}
|
package/src/math.ts
CHANGED
|
@@ -1,60 +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
|
-
}
|
|
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/plugin.ts
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
import type { Supermouse, SupermousePlugin } from "@supermousejs/core";
|
|
2
|
-
import { normalize } from "./options";
|
|
3
|
-
import { setStyle } from "./dom";
|
|
4
|
-
|
|
5
|
-
export interface BasePluginOptions {
|
|
6
|
-
name?: string;
|
|
7
|
-
isEnabled?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface LogicConfig {
|
|
11
|
-
name: string;
|
|
12
|
-
priority?: number;
|
|
13
|
-
install?: (app: Supermouse) => void;
|
|
14
|
-
update?: (app: Supermouse, deltaTime: number) => void;
|
|
15
|
-
destroy?: (app: Supermouse) => void;
|
|
16
|
-
onEnable?: (app: Supermouse) => void;
|
|
17
|
-
onDisable?: (app: Supermouse) => void;
|
|
18
|
-
// Explicitly disallow 'create' here to ensure type separation
|
|
19
|
-
create?: never;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// --- MODE B: VISUAL PLUGIN CONFIG ---
|
|
23
|
-
interface VisualConfig<E extends HTMLElement, O extends object> {
|
|
24
|
-
name: string;
|
|
25
|
-
/** Automatically register this attribute selector */
|
|
26
|
-
selector?: string;
|
|
27
|
-
/** Create and return the DOM Element */
|
|
28
|
-
create: (app: Supermouse) => E;
|
|
29
|
-
/** Map option keys to CSS properties */
|
|
30
|
-
styles?: Partial<Record<keyof O, keyof CSSStyleDeclaration>>;
|
|
31
|
-
/** Update loop with access to the element */
|
|
32
|
-
update?: (app: Supermouse, element: E, deltaTime: number) => void;
|
|
33
|
-
onEnable?: (app: Supermouse, element: E) => void;
|
|
34
|
-
onDisable?: (app: Supermouse, element: E) => void;
|
|
35
|
-
cleanup?: (element: E) => void;
|
|
36
|
-
destroy?: never; // Visual plugins use cleanup(), not destroy()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Helper Type Guard to safely distinguish VisualConfig at runtime
|
|
40
|
-
function isVisualConfig<E extends HTMLElement, O extends object>(
|
|
41
|
-
config: LogicConfig | VisualConfig<E, O>
|
|
42
|
-
): config is VisualConfig<E, O> {
|
|
43
|
-
return "create" in config && typeof (config as any).create === "function";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// --- THE OVERLOADS ---
|
|
47
|
-
|
|
48
|
-
// Overload 1: Visual Plugin (Infer Element E and Options O)
|
|
49
|
-
export function definePlugin<E extends HTMLElement, O extends BasePluginOptions>(
|
|
50
|
-
config: VisualConfig<E, O>,
|
|
51
|
-
userOptions?: O
|
|
52
|
-
): SupermousePlugin;
|
|
53
|
-
|
|
54
|
-
// Overload 2: Logic Plugin
|
|
55
|
-
export function definePlugin(
|
|
56
|
-
config: LogicConfig,
|
|
57
|
-
userOptions?: BasePluginOptions
|
|
58
|
-
): SupermousePlugin;
|
|
59
|
-
|
|
60
|
-
// --- THE IMPLEMENTATION ---
|
|
61
|
-
|
|
62
|
-
export function definePlugin(
|
|
63
|
-
config: LogicConfig | VisualConfig<HTMLElement, any>,
|
|
64
|
-
userOptions: any = {}
|
|
65
|
-
): SupermousePlugin {
|
|
66
|
-
const name = userOptions.name || config.name;
|
|
67
|
-
const initialEnabled = userOptions.isEnabled ?? true;
|
|
68
|
-
|
|
69
|
-
// MODE A: VISUAL
|
|
70
|
-
if (isVisualConfig(config)) {
|
|
71
|
-
let element: HTMLElement;
|
|
72
|
-
|
|
73
|
-
// PRE-COMPILE STYLE SETTERS
|
|
74
|
-
const styleSetters: ((app: Supermouse, el: HTMLElement) => void)[] = [];
|
|
75
|
-
|
|
76
|
-
if (config.styles) {
|
|
77
|
-
for (const [optKey, cssProp] of Object.entries(config.styles)) {
|
|
78
|
-
const getter = normalize(userOptions[optKey], undefined);
|
|
79
|
-
const prop = cssProp as any;
|
|
80
|
-
|
|
81
|
-
styleSetters.push((app, el) => {
|
|
82
|
-
const val = getter(app.state);
|
|
83
|
-
if (val !== undefined) {
|
|
84
|
-
setStyle(el, prop, val);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
name,
|
|
92
|
-
isEnabled: initialEnabled,
|
|
93
|
-
|
|
94
|
-
install(app) {
|
|
95
|
-
// 1. Create & Append
|
|
96
|
-
element = config.create(app);
|
|
97
|
-
if (config.selector) app.registerHoverTarget(config.selector);
|
|
98
|
-
|
|
99
|
-
// 2. Handle Initial State
|
|
100
|
-
if (this.isEnabled === false) {
|
|
101
|
-
element.style.opacity = "0";
|
|
102
|
-
}
|
|
103
|
-
app.container.appendChild(element);
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
update(app, dt) {
|
|
107
|
-
if (!element) return;
|
|
108
|
-
|
|
109
|
-
// 3. Run Pre-compiled Style Setters
|
|
110
|
-
for (let i = 0; i < styleSetters.length; i++) {
|
|
111
|
-
styleSetters[i](app, element);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 4. Run Custom Update
|
|
115
|
-
config.update?.(app, element, dt);
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
onDisable(app) {
|
|
119
|
-
if (!element) return;
|
|
120
|
-
// Use setStyle to ensure cache remains in sync (0)
|
|
121
|
-
setStyle(element, "opacity", 0);
|
|
122
|
-
config.onDisable?.(app, element);
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
onEnable(app) {
|
|
126
|
-
if (!element) return;
|
|
127
|
-
setStyle(element, "opacity", 1);
|
|
128
|
-
config.onEnable?.(app, element);
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
destroy() {
|
|
132
|
-
if (!element) return;
|
|
133
|
-
config.cleanup?.(element);
|
|
134
|
-
element.remove();
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// MODE B: LOGIC (Standard Pass-through)
|
|
140
|
-
else {
|
|
141
|
-
return {
|
|
142
|
-
...config,
|
|
143
|
-
name,
|
|
144
|
-
isEnabled: initialEnabled
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1
|
+
import type { Supermouse, SupermousePlugin } from "@supermousejs/core";
|
|
2
|
+
import { normalize } from "./options";
|
|
3
|
+
import { setStyle } from "./dom";
|
|
4
|
+
|
|
5
|
+
export interface BasePluginOptions {
|
|
6
|
+
name?: string;
|
|
7
|
+
isEnabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface LogicConfig {
|
|
11
|
+
name: string;
|
|
12
|
+
priority?: number;
|
|
13
|
+
install?: (app: Supermouse) => void;
|
|
14
|
+
update?: (app: Supermouse, deltaTime: number) => void;
|
|
15
|
+
destroy?: (app: Supermouse) => void;
|
|
16
|
+
onEnable?: (app: Supermouse) => void;
|
|
17
|
+
onDisable?: (app: Supermouse) => void;
|
|
18
|
+
// Explicitly disallow 'create' here to ensure type separation
|
|
19
|
+
create?: never;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- MODE B: VISUAL PLUGIN CONFIG ---
|
|
23
|
+
interface VisualConfig<E extends HTMLElement, O extends object> {
|
|
24
|
+
name: string;
|
|
25
|
+
/** Automatically register this attribute selector */
|
|
26
|
+
selector?: string;
|
|
27
|
+
/** Create and return the DOM Element */
|
|
28
|
+
create: (app: Supermouse) => E;
|
|
29
|
+
/** Map option keys to CSS properties */
|
|
30
|
+
styles?: Partial<Record<keyof O, keyof CSSStyleDeclaration>>;
|
|
31
|
+
/** Update loop with access to the element */
|
|
32
|
+
update?: (app: Supermouse, element: E, deltaTime: number) => void;
|
|
33
|
+
onEnable?: (app: Supermouse, element: E) => void;
|
|
34
|
+
onDisable?: (app: Supermouse, element: E) => void;
|
|
35
|
+
cleanup?: (element: E) => void;
|
|
36
|
+
destroy?: never; // Visual plugins use cleanup(), not destroy()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper Type Guard to safely distinguish VisualConfig at runtime
|
|
40
|
+
function isVisualConfig<E extends HTMLElement, O extends object>(
|
|
41
|
+
config: LogicConfig | VisualConfig<E, O>
|
|
42
|
+
): config is VisualConfig<E, O> {
|
|
43
|
+
return "create" in config && typeof (config as any).create === "function";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- THE OVERLOADS ---
|
|
47
|
+
|
|
48
|
+
// Overload 1: Visual Plugin (Infer Element E and Options O)
|
|
49
|
+
export function definePlugin<E extends HTMLElement, O extends BasePluginOptions>(
|
|
50
|
+
config: VisualConfig<E, O>,
|
|
51
|
+
userOptions?: O
|
|
52
|
+
): SupermousePlugin;
|
|
53
|
+
|
|
54
|
+
// Overload 2: Logic Plugin
|
|
55
|
+
export function definePlugin(
|
|
56
|
+
config: LogicConfig,
|
|
57
|
+
userOptions?: BasePluginOptions
|
|
58
|
+
): SupermousePlugin;
|
|
59
|
+
|
|
60
|
+
// --- THE IMPLEMENTATION ---
|
|
61
|
+
|
|
62
|
+
export function definePlugin(
|
|
63
|
+
config: LogicConfig | VisualConfig<HTMLElement, any>,
|
|
64
|
+
userOptions: any = {}
|
|
65
|
+
): SupermousePlugin {
|
|
66
|
+
const name = userOptions.name || config.name;
|
|
67
|
+
const initialEnabled = userOptions.isEnabled ?? true;
|
|
68
|
+
|
|
69
|
+
// MODE A: VISUAL
|
|
70
|
+
if (isVisualConfig(config)) {
|
|
71
|
+
let element: HTMLElement;
|
|
72
|
+
|
|
73
|
+
// PRE-COMPILE STYLE SETTERS
|
|
74
|
+
const styleSetters: ((app: Supermouse, el: HTMLElement) => void)[] = [];
|
|
75
|
+
|
|
76
|
+
if (config.styles) {
|
|
77
|
+
for (const [optKey, cssProp] of Object.entries(config.styles)) {
|
|
78
|
+
const getter = normalize(userOptions[optKey], undefined);
|
|
79
|
+
const prop = cssProp as any;
|
|
80
|
+
|
|
81
|
+
styleSetters.push((app, el) => {
|
|
82
|
+
const val = getter(app.state);
|
|
83
|
+
if (val !== undefined) {
|
|
84
|
+
setStyle(el, prop, val);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
isEnabled: initialEnabled,
|
|
93
|
+
|
|
94
|
+
install(app) {
|
|
95
|
+
// 1. Create & Append
|
|
96
|
+
element = config.create(app);
|
|
97
|
+
if (config.selector) app.registerHoverTarget(config.selector);
|
|
98
|
+
|
|
99
|
+
// 2. Handle Initial State
|
|
100
|
+
if (this.isEnabled === false) {
|
|
101
|
+
element.style.opacity = "0";
|
|
102
|
+
}
|
|
103
|
+
app.container.appendChild(element);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
update(app, dt) {
|
|
107
|
+
if (!element) return;
|
|
108
|
+
|
|
109
|
+
// 3. Run Pre-compiled Style Setters
|
|
110
|
+
for (let i = 0; i < styleSetters.length; i++) {
|
|
111
|
+
styleSetters[i](app, element);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4. Run Custom Update
|
|
115
|
+
config.update?.(app, element, dt);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
onDisable(app) {
|
|
119
|
+
if (!element) return;
|
|
120
|
+
// Use setStyle to ensure cache remains in sync (0)
|
|
121
|
+
setStyle(element, "opacity", 0);
|
|
122
|
+
config.onDisable?.(app, element);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
onEnable(app) {
|
|
126
|
+
if (!element) return;
|
|
127
|
+
setStyle(element, "opacity", 1);
|
|
128
|
+
config.onEnable?.(app, element);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
destroy() {
|
|
132
|
+
if (!element) return;
|
|
133
|
+
config.cleanup?.(element);
|
|
134
|
+
element.remove();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MODE B: LOGIC (Standard Pass-through)
|
|
140
|
+
else {
|
|
141
|
+
return {
|
|
142
|
+
...config,
|
|
143
|
+
name,
|
|
144
|
+
isEnabled: initialEnabled
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.composite-lib.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "src",
|
|
5
|
-
"outDir": "dist"
|
|
6
|
-
},
|
|
7
|
-
"include": ["src"],
|
|
8
|
-
"references": [
|
|
9
|
-
|
|
10
|
-
]
|
|
11
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.composite-lib.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "dist"
|
|
6
|
+
},
|
|
7
|
+
"include": ["src"],
|
|
8
|
+
"references": [{ "path": "../core" }]
|
|
9
|
+
}
|