@supermousejs/labs 2.1.1 → 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 +28 -0
- package/LICENSE.md +21 -21
- package/README.md +45 -42
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +114 -107
- package/dist/index.umd.js +9 -1
- package/meta.json +148 -59
- package/package.json +5 -5
- package/src/SmartIcon.ts +200 -239
- package/src/SmartRing.ts +105 -105
- package/src/Sparkles.ts +168 -168
- package/src/TextRing.ts +159 -156
- package/src/index.ts +4 -5
- package/tsconfig.json +4 -21
- package/vite.config.ts +24 -22
package/src/Sparkles.ts
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
import type { ValueOrGetter } from "@supermousejs/core";
|
|
2
|
-
import { definePlugin, normalize, dom, math, Layers } from "@supermousejs/utils";
|
|
3
|
-
|
|
4
|
-
export interface SparklesOptions {
|
|
5
|
-
name?: string;
|
|
6
|
-
isEnabled?: boolean;
|
|
7
|
-
color?: ValueOrGetter<string>;
|
|
8
|
-
/** Number of particles to keep in the pool. Default 30. */
|
|
9
|
-
count?: number;
|
|
10
|
-
/** How fast particles fade out (0.01 - 0.1). Default 0.05. */
|
|
11
|
-
decay?: number;
|
|
12
|
-
/** Pixels of movement required to spawn a particle. Lower = denser trail. Default 10. */
|
|
13
|
-
frequency?: number;
|
|
14
|
-
/** Random position offset for spawning. Default 5. */
|
|
15
|
-
scatter?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface Particle {
|
|
19
|
-
el: HTMLDivElement;
|
|
20
|
-
isActive: boolean;
|
|
21
|
-
x: number;
|
|
22
|
-
y: number;
|
|
23
|
-
vx: number;
|
|
24
|
-
vy: number;
|
|
25
|
-
life: number; // 0.0 to 1.0
|
|
26
|
-
scale: number;
|
|
27
|
-
color: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const Sparkles = (options: SparklesOptions = {}) => {
|
|
31
|
-
const poolSize = options.count || 30;
|
|
32
|
-
const decayRate = options.decay || 0.05;
|
|
33
|
-
const frequency = options.frequency || 10;
|
|
34
|
-
const scatter = options.scatter || 5;
|
|
35
|
-
|
|
36
|
-
const defColor = "#ff00ff";
|
|
37
|
-
const getColor = normalize(options.color, defColor);
|
|
38
|
-
|
|
39
|
-
const pool: Particle[] = [];
|
|
40
|
-
|
|
41
|
-
// Track previous position for interpolation
|
|
42
|
-
let lx = 0;
|
|
43
|
-
let ly = 0;
|
|
44
|
-
let hasMoved = false;
|
|
45
|
-
let accumulatedDist = 0;
|
|
46
|
-
|
|
47
|
-
const activateParticle = (x: number, y: number, color: string) => {
|
|
48
|
-
const p = pool.find((item) => !item.isActive);
|
|
49
|
-
if (!p) return;
|
|
50
|
-
|
|
51
|
-
p.isActive = true;
|
|
52
|
-
p.life = 1.0;
|
|
53
|
-
|
|
54
|
-
// Spawn with slight scatter around the calculated point
|
|
55
|
-
p.x = x + math.random(-scatter, scatter);
|
|
56
|
-
p.y = y + math.random(-scatter, scatter);
|
|
57
|
-
|
|
58
|
-
// Pure radial burst
|
|
59
|
-
const angle = math.random(0, Math.PI * 2);
|
|
60
|
-
const speed = math.random(0.5, 1.5);
|
|
61
|
-
p.vx = Math.cos(angle) * speed;
|
|
62
|
-
p.vy = Math.sin(angle) * speed;
|
|
63
|
-
|
|
64
|
-
p.scale = math.random(0.5, 1.2);
|
|
65
|
-
p.color = color;
|
|
66
|
-
|
|
67
|
-
// Apply immediate visual updates
|
|
68
|
-
const size = math.random(2, 5);
|
|
69
|
-
dom.setStyle(p.el, "width", `${size}px`);
|
|
70
|
-
dom.setStyle(p.el, "height", `${size}px`);
|
|
71
|
-
dom.setStyle(p.el, "backgroundColor", color);
|
|
72
|
-
dom.setStyle(p.el, "opacity", "1");
|
|
73
|
-
dom.setTransform(p.el, p.x, p.y, 0, p.scale, p.scale);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return definePlugin(
|
|
77
|
-
{
|
|
78
|
-
name: options.name || "sparkles",
|
|
79
|
-
|
|
80
|
-
install(app) {
|
|
81
|
-
for (let i = 0; i < poolSize; i++) {
|
|
82
|
-
const el = dom.createCircle(0, "transparent");
|
|
83
|
-
dom.applyStyles(el, {
|
|
84
|
-
zIndex: Layers.TRACE,
|
|
85
|
-
opacity: "0",
|
|
86
|
-
willChange: "transform, opacity",
|
|
87
|
-
transition: "none"
|
|
88
|
-
});
|
|
89
|
-
app.container.appendChild(el);
|
|
90
|
-
pool.push({
|
|
91
|
-
el,
|
|
92
|
-
isActive: false,
|
|
93
|
-
x: 0,
|
|
94
|
-
y: 0,
|
|
95
|
-
vx: 0,
|
|
96
|
-
vy: 0,
|
|
97
|
-
life: 0,
|
|
98
|
-
scale: 1,
|
|
99
|
-
color: ""
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
update(app) {
|
|
105
|
-
const { x: cx, y: cy } = app.state.pointer;
|
|
106
|
-
|
|
107
|
-
// --- 1. SPAWN LOGIC (Interpolated) ---
|
|
108
|
-
if (!hasMoved) {
|
|
109
|
-
lx = cx;
|
|
110
|
-
ly = cy;
|
|
111
|
-
hasMoved = true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const dx = cx - lx;
|
|
115
|
-
const dy = cy - ly;
|
|
116
|
-
const dist = Math.hypot(dx, dy);
|
|
117
|
-
|
|
118
|
-
accumulatedDist += dist;
|
|
119
|
-
|
|
120
|
-
// If we have moved enough to spawn one or more particles
|
|
121
|
-
if (accumulatedDist > frequency) {
|
|
122
|
-
const color = getColor(app.state);
|
|
123
|
-
|
|
124
|
-
// How many particles fit in this movement?
|
|
125
|
-
const steps = Math.floor(accumulatedDist / frequency);
|
|
126
|
-
|
|
127
|
-
// Interpolate backwards from current position
|
|
128
|
-
for (let i = 0; i < steps; i++) {
|
|
129
|
-
const t = i / steps;
|
|
130
|
-
const spawnX = lx + dx * t;
|
|
131
|
-
const spawnY = ly + dy * t;
|
|
132
|
-
activateParticle(spawnX, spawnY, color);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
accumulatedDist = accumulatedDist % frequency;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
lx = cx;
|
|
139
|
-
ly = cy;
|
|
140
|
-
|
|
141
|
-
// --- 2. PHYSICS LOOP ---
|
|
142
|
-
for (let i = 0; i < poolSize; i++) {
|
|
143
|
-
const p = pool[i];
|
|
144
|
-
if (!p.isActive) continue;
|
|
145
|
-
|
|
146
|
-
p.x += p.vx;
|
|
147
|
-
p.y += p.vy;
|
|
148
|
-
p.life -= decayRate;
|
|
149
|
-
|
|
150
|
-
if (p.life <= 0) {
|
|
151
|
-
p.isActive = false;
|
|
152
|
-
p.el.style.opacity = "0";
|
|
153
|
-
} else {
|
|
154
|
-
p.el.style.opacity = String(p.life);
|
|
155
|
-
const currentScale = p.scale * p.life;
|
|
156
|
-
dom.setTransform(p.el, p.x, p.y, 0, currentScale, currentScale);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
destroy() {
|
|
162
|
-
pool.forEach((p) => p.el.remove());
|
|
163
|
-
pool.length = 0;
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
options
|
|
167
|
-
);
|
|
168
|
-
};
|
|
1
|
+
import type { ValueOrGetter, Supermouse } from "@supermousejs/core";
|
|
2
|
+
import { definePlugin, normalize, dom, math, Layers } from "@supermousejs/utils";
|
|
3
|
+
|
|
4
|
+
export interface SparklesOptions {
|
|
5
|
+
name?: string;
|
|
6
|
+
isEnabled?: boolean;
|
|
7
|
+
color?: ValueOrGetter<string>;
|
|
8
|
+
/** Number of particles to keep in the pool. Default 30. */
|
|
9
|
+
count?: number;
|
|
10
|
+
/** How fast particles fade out (0.01 - 0.1). Default 0.05. */
|
|
11
|
+
decay?: number;
|
|
12
|
+
/** Pixels of movement required to spawn a particle. Lower = denser trail. Default 10. */
|
|
13
|
+
frequency?: number;
|
|
14
|
+
/** Random position offset for spawning. Default 5. */
|
|
15
|
+
scatter?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Particle {
|
|
19
|
+
el: HTMLDivElement;
|
|
20
|
+
isActive: boolean;
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
vx: number;
|
|
24
|
+
vy: number;
|
|
25
|
+
life: number; // 0.0 to 1.0
|
|
26
|
+
scale: number;
|
|
27
|
+
color: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Sparkles = (options: SparklesOptions = {}) => {
|
|
31
|
+
const poolSize = options.count || 30;
|
|
32
|
+
const decayRate = options.decay || 0.05;
|
|
33
|
+
const frequency = options.frequency || 10;
|
|
34
|
+
const scatter = options.scatter || 5;
|
|
35
|
+
|
|
36
|
+
const defColor = "#ff00ff";
|
|
37
|
+
const getColor = normalize(options.color, defColor);
|
|
38
|
+
|
|
39
|
+
const pool: Particle[] = [];
|
|
40
|
+
|
|
41
|
+
// Track previous position for interpolation
|
|
42
|
+
let lx = 0;
|
|
43
|
+
let ly = 0;
|
|
44
|
+
let hasMoved = false;
|
|
45
|
+
let accumulatedDist = 0;
|
|
46
|
+
|
|
47
|
+
const activateParticle = (x: number, y: number, color: string) => {
|
|
48
|
+
const p = pool.find((item) => !item.isActive);
|
|
49
|
+
if (!p) return;
|
|
50
|
+
|
|
51
|
+
p.isActive = true;
|
|
52
|
+
p.life = 1.0;
|
|
53
|
+
|
|
54
|
+
// Spawn with slight scatter around the calculated point
|
|
55
|
+
p.x = x + math.random(-scatter, scatter);
|
|
56
|
+
p.y = y + math.random(-scatter, scatter);
|
|
57
|
+
|
|
58
|
+
// Pure radial burst
|
|
59
|
+
const angle = math.random(0, Math.PI * 2);
|
|
60
|
+
const speed = math.random(0.5, 1.5);
|
|
61
|
+
p.vx = Math.cos(angle) * speed;
|
|
62
|
+
p.vy = Math.sin(angle) * speed;
|
|
63
|
+
|
|
64
|
+
p.scale = math.random(0.5, 1.2);
|
|
65
|
+
p.color = color;
|
|
66
|
+
|
|
67
|
+
// Apply immediate visual updates
|
|
68
|
+
const size = math.random(2, 5);
|
|
69
|
+
dom.setStyle(p.el, "width", `${size}px`);
|
|
70
|
+
dom.setStyle(p.el, "height", `${size}px`);
|
|
71
|
+
dom.setStyle(p.el, "backgroundColor", color);
|
|
72
|
+
dom.setStyle(p.el, "opacity", "1");
|
|
73
|
+
dom.setTransform(p.el, p.x, p.y, 0, p.scale, p.scale);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return definePlugin(
|
|
77
|
+
{
|
|
78
|
+
name: options.name || "sparkles",
|
|
79
|
+
|
|
80
|
+
install(app: Supermouse) {
|
|
81
|
+
for (let i = 0; i < poolSize; i++) {
|
|
82
|
+
const el = dom.createCircle(0, "transparent");
|
|
83
|
+
dom.applyStyles(el, {
|
|
84
|
+
zIndex: Layers.TRACE,
|
|
85
|
+
opacity: "0",
|
|
86
|
+
willChange: "transform, opacity",
|
|
87
|
+
transition: "none"
|
|
88
|
+
});
|
|
89
|
+
app.container.appendChild(el);
|
|
90
|
+
pool.push({
|
|
91
|
+
el,
|
|
92
|
+
isActive: false,
|
|
93
|
+
x: 0,
|
|
94
|
+
y: 0,
|
|
95
|
+
vx: 0,
|
|
96
|
+
vy: 0,
|
|
97
|
+
life: 0,
|
|
98
|
+
scale: 1,
|
|
99
|
+
color: ""
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
update(app: Supermouse) {
|
|
105
|
+
const { x: cx, y: cy } = app.state.pointer;
|
|
106
|
+
|
|
107
|
+
// --- 1. SPAWN LOGIC (Interpolated) ---
|
|
108
|
+
if (!hasMoved) {
|
|
109
|
+
lx = cx;
|
|
110
|
+
ly = cy;
|
|
111
|
+
hasMoved = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const dx = cx - lx;
|
|
115
|
+
const dy = cy - ly;
|
|
116
|
+
const dist = Math.hypot(dx, dy);
|
|
117
|
+
|
|
118
|
+
accumulatedDist += dist;
|
|
119
|
+
|
|
120
|
+
// If we have moved enough to spawn one or more particles
|
|
121
|
+
if (accumulatedDist > frequency) {
|
|
122
|
+
const color = getColor(app.state);
|
|
123
|
+
|
|
124
|
+
// How many particles fit in this movement?
|
|
125
|
+
const steps = Math.floor(accumulatedDist / frequency);
|
|
126
|
+
|
|
127
|
+
// Interpolate backwards from current position
|
|
128
|
+
for (let i = 0; i < steps; i++) {
|
|
129
|
+
const t = i / steps;
|
|
130
|
+
const spawnX = lx + dx * t;
|
|
131
|
+
const spawnY = ly + dy * t;
|
|
132
|
+
activateParticle(spawnX, spawnY, color);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
accumulatedDist = accumulatedDist % frequency;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
lx = cx;
|
|
139
|
+
ly = cy;
|
|
140
|
+
|
|
141
|
+
// --- 2. PHYSICS LOOP ---
|
|
142
|
+
for (let i = 0; i < poolSize; i++) {
|
|
143
|
+
const p = pool[i];
|
|
144
|
+
if (!p.isActive) continue;
|
|
145
|
+
|
|
146
|
+
p.x += p.vx;
|
|
147
|
+
p.y += p.vy;
|
|
148
|
+
p.life -= decayRate;
|
|
149
|
+
|
|
150
|
+
if (p.life <= 0) {
|
|
151
|
+
p.isActive = false;
|
|
152
|
+
p.el.style.opacity = "0";
|
|
153
|
+
} else {
|
|
154
|
+
p.el.style.opacity = String(p.life);
|
|
155
|
+
const currentScale = p.scale * p.life;
|
|
156
|
+
dom.setTransform(p.el, p.x, p.y, 0, currentScale, currentScale);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
destroy() {
|
|
162
|
+
pool.forEach((p) => p.el.remove());
|
|
163
|
+
pool.length = 0;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
options
|
|
167
|
+
);
|
|
168
|
+
};
|
package/src/TextRing.ts
CHANGED
|
@@ -1,156 +1,159 @@
|
|
|
1
|
-
import type { ValueOrGetter } from
|
|
2
|
-
import { definePlugin, normalize, dom, Layers } from
|
|
3
|
-
import { getCirclePath, getCircumference, formatLoopText } from
|
|
4
|
-
|
|
5
|
-
export interface TextRingOptions {
|
|
6
|
-
name?: string;
|
|
7
|
-
isEnabled?: boolean;
|
|
8
|
-
text?: ValueOrGetter<string>;
|
|
9
|
-
radius?: ValueOrGetter<number>;
|
|
10
|
-
fontSize?: ValueOrGetter<number>;
|
|
11
|
-
speed?: ValueOrGetter<number>;
|
|
12
|
-
color?: ValueOrGetter<string>;
|
|
13
|
-
opacity?: ValueOrGetter<number>;
|
|
14
|
-
className?: string;
|
|
15
|
-
spread?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let instanceCount = 0;
|
|
19
|
-
|
|
20
|
-
export const TextRing = (options: TextRingOptions = {}) => {
|
|
21
|
-
let svg: SVGSVGElement;
|
|
22
|
-
let pathEl: SVGPathElement;
|
|
23
|
-
let textPathEl: SVGTextPathElement;
|
|
24
|
-
let textEl: SVGTextElement;
|
|
25
|
-
let textNode: Text;
|
|
26
|
-
|
|
27
|
-
const pathId = `supermouse-text-ring-path-${instanceCount++}`;
|
|
28
|
-
|
|
29
|
-
const defText =
|
|
30
|
-
const defRadius = 60;
|
|
31
|
-
const defFontSize = 12;
|
|
32
|
-
const defSpeed = 0.5;
|
|
33
|
-
const className = options.className ||
|
|
34
|
-
const spread = options.spread ?? false;
|
|
35
|
-
|
|
36
|
-
const getText = normalize(options.text, defText);
|
|
37
|
-
const getRadius = normalize(options.radius, defRadius);
|
|
38
|
-
const getFontSize = normalize(options.fontSize, defFontSize);
|
|
39
|
-
const getSpeed = normalize(options.speed, defSpeed);
|
|
40
|
-
const getOpacity = normalize(options.opacity, 1);
|
|
41
|
-
|
|
42
|
-
let currentRotation = 0;
|
|
43
|
-
let lastText =
|
|
44
|
-
let lastRadius = 0;
|
|
45
|
-
let lastFontSize = 0;
|
|
46
|
-
|
|
47
|
-
return definePlugin<HTMLDivElement, TextRingOptions>(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
text
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
1
|
+
import type { ValueOrGetter, Supermouse } from "@supermousejs/core";
|
|
2
|
+
import { definePlugin, normalize, dom, Layers } from "@supermousejs/utils";
|
|
3
|
+
import { getCirclePath, getCircumference, formatLoopText } from "@supermousejs/zoetrope";
|
|
4
|
+
|
|
5
|
+
export interface TextRingOptions {
|
|
6
|
+
name?: string;
|
|
7
|
+
isEnabled?: boolean;
|
|
8
|
+
text?: ValueOrGetter<string>;
|
|
9
|
+
radius?: ValueOrGetter<number>;
|
|
10
|
+
fontSize?: ValueOrGetter<number>;
|
|
11
|
+
speed?: ValueOrGetter<number>;
|
|
12
|
+
color?: ValueOrGetter<string>;
|
|
13
|
+
opacity?: ValueOrGetter<number>;
|
|
14
|
+
className?: string;
|
|
15
|
+
spread?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let instanceCount = 0;
|
|
19
|
+
|
|
20
|
+
export const TextRing = (options: TextRingOptions = {}) => {
|
|
21
|
+
let svg: SVGSVGElement;
|
|
22
|
+
let pathEl: SVGPathElement;
|
|
23
|
+
let textPathEl: SVGTextPathElement;
|
|
24
|
+
let textEl: SVGTextElement;
|
|
25
|
+
let textNode: Text;
|
|
26
|
+
|
|
27
|
+
const pathId = `supermouse-text-ring-path-${instanceCount++}`;
|
|
28
|
+
|
|
29
|
+
const defText = "SUPERMOUSE • SUPERMOUSE • ";
|
|
30
|
+
const defRadius = 60;
|
|
31
|
+
const defFontSize = 12;
|
|
32
|
+
const defSpeed = 0.5;
|
|
33
|
+
const className = options.className || "";
|
|
34
|
+
const spread = options.spread ?? false;
|
|
35
|
+
|
|
36
|
+
const getText = normalize(options.text, defText);
|
|
37
|
+
const getRadius = normalize(options.radius, defRadius);
|
|
38
|
+
const getFontSize = normalize(options.fontSize, defFontSize);
|
|
39
|
+
const getSpeed = normalize(options.speed, defSpeed);
|
|
40
|
+
const getOpacity = normalize(options.opacity, 1);
|
|
41
|
+
|
|
42
|
+
let currentRotation = 0;
|
|
43
|
+
let lastText = "";
|
|
44
|
+
let lastRadius = 0;
|
|
45
|
+
let lastFontSize = 0;
|
|
46
|
+
|
|
47
|
+
return definePlugin<HTMLDivElement, TextRingOptions>(
|
|
48
|
+
{
|
|
49
|
+
name: "text-ring",
|
|
50
|
+
selector: "[data-supermouse-text-ring]",
|
|
51
|
+
|
|
52
|
+
create: (app: Supermouse) => {
|
|
53
|
+
const container = dom.createActor("div") as HTMLDivElement;
|
|
54
|
+
dom.applyStyles(container, {
|
|
55
|
+
zIndex: Layers.FOLLOWER,
|
|
56
|
+
transition: "opacity 0.2s ease",
|
|
57
|
+
opacity: "1",
|
|
58
|
+
width: "0px",
|
|
59
|
+
height: "0px",
|
|
60
|
+
overflow: "visible",
|
|
61
|
+
display: "flex",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
justifyContent: "center"
|
|
64
|
+
});
|
|
65
|
+
if (className) {
|
|
66
|
+
container.classList.add(...className.split(" ").filter(Boolean));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
70
|
+
svg.style.overflow = "visible";
|
|
71
|
+
svg.style.position = "absolute";
|
|
72
|
+
svg.style.left = "0";
|
|
73
|
+
svg.style.top = "0";
|
|
74
|
+
|
|
75
|
+
pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
76
|
+
pathEl.setAttribute("id", pathId);
|
|
77
|
+
pathEl.setAttribute("fill", "none");
|
|
78
|
+
|
|
79
|
+
textEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
80
|
+
|
|
81
|
+
const fs = getFontSize(app.state);
|
|
82
|
+
textEl.setAttribute("font-size", `${fs}px`);
|
|
83
|
+
lastFontSize = fs;
|
|
84
|
+
|
|
85
|
+
textEl.setAttribute("fill", "currentColor");
|
|
86
|
+
textEl.style.textTransform = "uppercase";
|
|
87
|
+
textEl.style.letterSpacing = "2px";
|
|
88
|
+
|
|
89
|
+
textPathEl = document.createElementNS("http://www.w3.org/2000/svg", "textPath");
|
|
90
|
+
textPathEl.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", `#${pathId}`);
|
|
91
|
+
textPathEl.setAttribute("href", `#${pathId}`);
|
|
92
|
+
textPathEl.setAttribute("startOffset", "0%");
|
|
93
|
+
|
|
94
|
+
textNode = document.createTextNode("");
|
|
95
|
+
|
|
96
|
+
textPathEl.appendChild(textNode);
|
|
97
|
+
textEl.appendChild(textPathEl);
|
|
98
|
+
svg.appendChild(pathEl);
|
|
99
|
+
svg.appendChild(textEl);
|
|
100
|
+
container.appendChild(svg);
|
|
101
|
+
|
|
102
|
+
return container;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
styles: {
|
|
106
|
+
color: "color"
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
update: (app: Supermouse, container: HTMLDivElement) => {
|
|
110
|
+
let text = getText(app.state);
|
|
111
|
+
const radius = getRadius(app.state);
|
|
112
|
+
const fontSize = getFontSize(app.state);
|
|
113
|
+
const speed = getSpeed(app.state);
|
|
114
|
+
const opacity = getOpacity(app.state);
|
|
115
|
+
|
|
116
|
+
// Force update opacity
|
|
117
|
+
dom.setStyle(container, "opacity", String(opacity));
|
|
118
|
+
|
|
119
|
+
const ia = app.state.interaction;
|
|
120
|
+
// Prefer specific "textRing" property, fallback to generic "text", then default option
|
|
121
|
+
if (ia.textRing && typeof ia.textRing === "string") {
|
|
122
|
+
text = ia.textRing;
|
|
123
|
+
} else if (ia.text && typeof ia.text === "string") {
|
|
124
|
+
text = ia.text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (radius !== lastRadius) {
|
|
128
|
+
pathEl.setAttribute("d", getCirclePath(radius));
|
|
129
|
+
lastRadius = radius;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (spread) {
|
|
133
|
+
const circum = getCircumference(radius);
|
|
134
|
+
textPathEl.setAttribute("textLength", String(circum));
|
|
135
|
+
textPathEl.setAttribute("lengthAdjust", "spacing");
|
|
136
|
+
text = formatLoopText(text, true);
|
|
137
|
+
} else {
|
|
138
|
+
textPathEl.removeAttribute("textLength");
|
|
139
|
+
textPathEl.removeAttribute("lengthAdjust");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (fontSize !== lastFontSize) {
|
|
143
|
+
textEl.setAttribute("font-size", `${fontSize}px`);
|
|
144
|
+
lastFontSize = fontSize;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (text !== lastText) {
|
|
148
|
+
textNode.textContent = text;
|
|
149
|
+
lastText = text;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
currentRotation += speed;
|
|
153
|
+
const { x, y } = app.state.smooth;
|
|
154
|
+
dom.setTransform(container, x, y, currentRotation);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
options
|
|
158
|
+
);
|
|
159
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from './TextRing';
|
|
1
|
+
export * from "./SmartIcon";
|
|
2
|
+
export * from "./SmartRing";
|
|
3
|
+
export * from "./Sparkles";
|
|
4
|
+
export * from "./TextRing";
|