@supermousejs/core 2.0.5 → 2.1.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/src/Supermouse.ts CHANGED
@@ -1,405 +1,339 @@
1
- declare const __VERSION__: string;
2
-
3
- import { MouseState, SupermouseOptions, SupermousePlugin } from './types';
4
- import { Stage, Input } from './systems';
5
- import { damp, angle } from './utils/math';
6
-
7
- export const DEFAULT_HOVER_SELECTORS = [
8
- 'a', 'button', 'input', 'textarea', '[data-hover]', '[data-cursor]'
9
- ];
10
-
11
- /**
12
- * The Central Conductor & Runtime Loop of Supermouse.
13
- *
14
- * This class orchestrates the application state, manages the animation loop (`requestAnimationFrame`),
15
- * and coordinates data flow between the Input system, the Stage system, and the Plugins.
16
- *
17
- * ## Architecture
18
- * 1. **Input System**: Captures raw events and writes to `state.pointer`.
19
- * 2. **Logic Plugins**: (Priority < 0) Read `pointer`, modify `state.target` (e.g. Magnetic, Stick).
20
- * 3. **Physics**: Core interpolates `state.smooth` towards `state.target`.
21
- * 4. **Visual Plugins**: (Priority >= 0) Read `state.smooth`, update DOM transforms.
22
- *
23
- * @example
24
- * ```ts
25
- * const app = new Supermouse({ smoothness: 0.15 });
26
- * app.use(Dot({ color: 'red' }));
27
- * ```
28
- */
29
- export class Supermouse {
30
- /** The current version of Supermouse.js */
31
- public static readonly version: string = __VERSION__;
32
- public readonly version: string = __VERSION__;
33
-
34
- /**
35
- * The Single Source of Truth.
36
- *
37
- * This object is shared by reference. `Input` writes to it; `Supermouse` physics reads/writes to it;
38
- * Plugins read/write to it.
39
- */
40
- state: MouseState;
41
-
42
- /**
43
- * Configuration options.
44
- */
45
- options: SupermouseOptions;
46
-
47
- /**
48
- * Registry of active plugins.
49
- * @internal Use `use()`, `enablePlugin()`, or `disablePlugin()` to interact with this.
50
- */
51
- private plugins: SupermousePlugin[] = [];
52
-
53
- /**
54
- * The Stage System responsible for the DOM container and CSS injection.
55
- * @internal
56
- */
57
- private stage: Stage;
58
-
59
- /**
60
- * The Input System responsible for event listeners.
61
- * @internal
62
- */
63
- private input: Input;
64
-
65
- private rafId: number = 0;
66
- private lastTime: number = 0;
67
- private isRunning: boolean = false;
68
-
69
- private hoverSelectors: Set<string>;
70
-
71
- /**
72
- * Creates a new Supermouse instance.
73
- *
74
- * @param options - Global configuration options.
75
- * @throws Will throw if running in a non-browser environment (window/document undefined).
76
- */
77
- constructor(options: SupermouseOptions = {}) {
78
- this.options = {
79
- smoothness: 0.15,
80
- enableTouch: false,
81
- autoDisableOnMobile: true,
82
- ignoreOnNative: 'auto',
83
- hideCursor: true,
84
- hideOnLeave: true,
85
- autoStart: true,
86
- container: document.body,
87
- ...options
88
- };
89
-
90
- // Ensure container is valid (fallback to body if null/undefined passed explicitly)
91
- if (!this.options.container) {
92
- this.options.container = document.body;
93
- }
94
-
95
- this.state = {
96
- pointer: { x: -100, y: -100 },
97
- target: { x: -100, y: -100 },
98
- smooth: { x: -100, y: -100 },
99
- velocity: { x: 0, y: 0 },
100
- angle: 0,
101
- isDown: false,
102
- isHover: false,
103
- isNative: false,
104
- forcedCursor: null,
105
- hoverTarget: null,
106
- reducedMotion: false,
107
- hasReceivedInput: false,
108
- shape: null,
109
- interaction: {}
110
- };
111
-
112
- // Initialize Selectors
113
- if (this.options.hoverSelectors) {
114
- this.hoverSelectors = new Set(this.options.hoverSelectors);
115
- } else {
116
- this.hoverSelectors = new Set(DEFAULT_HOVER_SELECTORS);
117
- }
118
-
119
- this.stage = new Stage(this.options.container, !!this.options.hideCursor);
120
- this.hoverSelectors.forEach(s => this.stage.addSelector(s));
121
-
122
- // Pass state by reference to Input. Input will mutate this state directly.
123
- this.input = new Input(
124
- this.state,
125
- this.options,
126
- () => Array.from(this.hoverSelectors).join(', '),
127
- (enabled) => { if (!enabled) this.resetPosition(); }
128
- );
129
-
130
- if (this.options.plugins) {
131
- this.options.plugins.forEach(p => this.use(p));
132
- }
133
-
134
- this.init();
135
- }
136
-
137
- /**
138
- * Retrieves a registered plugin instance by its unique name.
139
- */
140
- public getPlugin(name: string) {
141
- return this.plugins.find(p => p.name === name);
142
- }
143
-
144
- /**
145
- * Returns whether the cursor system is currently enabled (processing input).
146
- */
147
- public get isEnabled(): boolean {
148
- return this.input.isEnabled;
149
- }
150
-
151
- /**
152
- * Enables a specific plugin by name.
153
- * Triggers the `onEnable` lifecycle hook of the plugin.
154
- */
155
- public enablePlugin(name: string) {
156
- const plugin = this.getPlugin(name);
157
- if (plugin && plugin.isEnabled === false) {
158
- plugin.isEnabled = true;
159
- plugin.onEnable?.(this);
160
- }
161
- }
162
-
163
- /**
164
- * Disables a specific plugin by name.
165
- * Triggers the `onDisable` lifecycle hook.
166
- */
167
- public disablePlugin(name: string) {
168
- const plugin = this.getPlugin(name);
169
- if (plugin && plugin.isEnabled !== false) {
170
- plugin.isEnabled = false;
171
- plugin.onDisable?.(this);
172
- }
173
- }
174
-
175
- /**
176
- * Toggles the enabled state of a plugin.
177
- */
178
- public togglePlugin(name: string) {
179
- const plugin = this.getPlugin(name);
180
- if (plugin) {
181
- if (plugin.isEnabled === false) this.enablePlugin(name);
182
- else this.disablePlugin(name);
183
- }
184
- }
185
-
186
- /**
187
- * Registers a CSS selector as an "Interactive Target".
188
- *
189
- * When the mouse hovers over an element matching this selector:
190
- * 1. `state.isHover` becomes `true`.
191
- * 2. `state.hoverTarget` is set to the element.
192
- * 3. The `Stage` system injects CSS to hide the native cursor for this element (if `hideCursor: true`).
193
- *
194
- * @param selector - A valid CSS selector string (e.g., `.my-button`, `[data-trigger]`).
195
- */
196
- public registerHoverTarget(selector: string) {
197
- if (!this.hoverSelectors.has(selector)) {
198
- this.hoverSelectors.add(selector);
199
- this.stage.addSelector(selector);
200
- }
201
- }
202
-
203
- /**
204
- * The fixed container element where plugins should append their DOM nodes.
205
- */
206
- public get container(): HTMLDivElement { return this.stage.element; }
207
-
208
- /**
209
- * Manually override the native cursor visibility.
210
- * Useful for drag-and-drop operations, modals, or special UI states.
211
- *
212
- * @param type 'auto' (Show Native), 'none' (Hide Native), or null (Resume Auto-detection)
213
- */
214
- public setCursor(type: 'auto' | 'none' | null) {
215
- this.state.forcedCursor = type;
216
- }
217
-
218
- private init() {
219
- if (this.options.autoStart) {
220
- this.startLoop();
221
- }
222
- }
223
-
224
- /**
225
- * Starts the update loop and enables input listeners.
226
- * Hides the native cursor if configured.
227
- */
228
- public enable() { this.input.isEnabled = true; this.stage.setNativeCursor('none'); }
229
-
230
- /**
231
- * Stops the update loop, disables listeners, and restores the native cursor.
232
- * Resets internal state positions to off-screen.
233
- */
234
- public disable() { this.input.isEnabled = false; this.stage.setNativeCursor('auto'); this.resetPosition(); }
235
-
236
- /**
237
- * Registers a new plugin.
238
- *
239
- * @remarks
240
- * Plugins are sorted by `priority` immediately after registration.
241
- * - **Negative Priority (< 0)**: Logic plugins (run before physics).
242
- * - **Positive Priority (>= 0)**: Visual plugins (run after physics).
243
- *
244
- * @param plugin - The plugin object to install.
245
- */
246
- public use(plugin: SupermousePlugin) {
247
- if (this.plugins.find(p => p.name === plugin.name)) {
248
- console.warn(`[Supermouse] Plugin "${plugin.name}" already installed.`);
249
- return this;
250
- }
251
-
252
- if (plugin.isEnabled === undefined) {
253
- plugin.isEnabled = true;
254
- }
255
-
256
- this.plugins.push(plugin);
257
- this.plugins.sort((a, b) => (a.priority || 0) - (b.priority || 0));
258
-
259
- plugin.install?.(this);
260
- return this;
261
- }
262
-
263
- private resetPosition() {
264
- const off = { x: -100, y: -100 };
265
- this.state.pointer = { ...off };
266
- this.state.target = { ...off };
267
- this.state.smooth = { ...off };
268
- this.state.velocity = { x: 0, y: 0 };
269
- this.state.angle = 0;
270
- this.state.hasReceivedInput = false;
271
- this.state.shape = null;
272
- this.state.interaction = {};
273
- }
274
-
275
- private startLoop() {
276
- if (this.isRunning) return;
277
- this.isRunning = true;
278
- this.lastTime = performance.now();
279
- this.tick(this.lastTime);
280
- }
281
-
282
- /**
283
- * Manually steps the animation loop.
284
- * Useful when integrating with external game loops (e.g., Three.js, PixiJS) where
285
- * you want to disable the internal RAF and drive `Supermouse` from your own ticker.
286
- *
287
- * @param time Current timestamp in milliseconds.
288
- */
289
- public step(time: number) {
290
- this.tick(time);
291
- }
292
-
293
- private runPluginSafe(plugin: SupermousePlugin, deltaTime: number) {
294
- if (plugin.isEnabled === false) return;
295
- try {
296
- plugin.update?.(this, deltaTime);
297
- } catch (e) {
298
- console.error(`[Supermouse] Plugin '${plugin.name}' crashed and has been disabled.`, e);
299
- plugin.isEnabled = false;
300
- plugin.onDisable?.(this);
301
- }
302
- }
303
-
304
- /**
305
- * The Heartbeat.
306
- * Runs on every animation frame.
307
- */
308
- private tick = (time: number) => {
309
- // 1. Calculate Delta Time (in seconds)
310
- // We cap dt at 0.1s (100ms) to prevent massive jumps if the tab is inactive for a while.
311
- const dtMs = time - this.lastTime;
312
- const dt = Math.min(dtMs / 1000, 0.1);
313
- this.lastTime = time;
314
-
315
- // 2. Visibility Logic
316
- // Stage is visible only if input is active, it's not a native-cursor override situation,
317
- // and we have actually received mouse coordinates.
318
- const shouldShowStage = this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
319
- this.stage.setVisibility(shouldShowStage);
320
-
321
- // 3. Native Cursor Hiding Logic
322
- if (this.input.isEnabled && this.options.hideCursor) {
323
- let targetState: 'none' | 'auto' = 'auto';
324
-
325
- // PRIORITY 1: Manual Override (User/Plugin says so)
326
- if (this.state.forcedCursor !== null) {
327
- targetState = this.state.forcedCursor;
328
- }
329
- // PRIORITY 2: Auto-Detection (Default behavior)
330
- else {
331
- // Show native if we are in a "Native Zone" (Input) OR if we haven't moved mouse yet
332
- const showNative = this.state.isNative || !this.state.hasReceivedInput;
333
- targetState = showNative ? 'auto' : 'none';
334
- }
335
-
336
- this.stage.setNativeCursor(targetState);
337
- }
338
-
339
- if (this.input.isEnabled) {
340
- // 4. Sync Target
341
- // By default, target = raw pointer. Logic plugins may override this in the next step.
342
- this.state.target.x = this.state.pointer.x;
343
- this.state.target.y = this.state.pointer.y;
344
-
345
- // 5. Run Plugins
346
- // This iterates through the priority-sorted list.
347
- // - Logic Plugins (Magnetic) run first and modify this.state.target
348
- // - Visual Plugins (Dot, Ring) run last and read this.state.smooth
349
- for (let i = 0; i < this.plugins.length; i++) {
350
- this.runPluginSafe(this.plugins[i], dtMs);
351
- }
352
-
353
- // 6. Physics Integration (Damping)
354
- // Convert abstract smoothness (0-1) to damping factor (1-50 approx)
355
- const userSmooth = this.options.smoothness!;
356
- // Map 0.15 (floaty) -> ~10, 0.5 (snappy) -> ~25
357
- const dampFactor = this.state.reducedMotion ? 1000 : (1 / userSmooth) * 2;
358
-
359
- this.state.smooth.x = damp(this.state.smooth.x, this.state.target.x, dampFactor, dt);
360
- this.state.smooth.y = damp(this.state.smooth.y, this.state.target.y, dampFactor, dt);
361
-
362
- const vx = this.state.target.x - this.state.smooth.x;
363
- const vy = this.state.target.y - this.state.smooth.y;
364
-
365
- this.state.velocity.x = vx;
366
- this.state.velocity.y = vy;
367
-
368
- // Only update angle if moving significantly (prevents jitter at rest)
369
- if (Math.abs(vx) > 0.1 || Math.abs(vy) > 0.1) {
370
- this.state.angle = angle(vx, vy);
371
- }
372
-
373
- } else {
374
- // Inactive State: Force positions off-screen
375
- this.state.smooth.x = -100;
376
- this.state.smooth.y = -100;
377
- this.state.pointer.x = -100;
378
- this.state.pointer.y = -100;
379
- this.state.velocity.x = 0;
380
- this.state.velocity.y = 0;
381
-
382
- // Still run plugins (e.g. for exit animations)
383
- for (let i = 0; i < this.plugins.length; i++) {
384
- this.runPluginSafe(this.plugins[i], dtMs);
385
- }
386
- }
387
-
388
- if (this.options.autoStart && this.isRunning) {
389
- this.rafId = requestAnimationFrame(this.tick);
390
- }
391
- };
392
-
393
- /**
394
- * Destroys the instance.
395
- * Stops the loop, removes all DOM elements, removes all event listeners, and calls destroy on all plugins.
396
- */
397
- public destroy() {
398
- this.isRunning = false;
399
- cancelAnimationFrame(this.rafId);
400
- this.input.destroy();
401
- this.stage.destroy();
402
- this.plugins.forEach(p => p.destroy?.(this));
403
- this.plugins = [];
404
- }
405
- }
1
+ declare const __VERSION__: string;
2
+
3
+ import type { MouseState, SupermouseOptions, SupermousePlugin } from "./types";
4
+ import { Stage, Input } from "./systems";
5
+ import { damp, angle } from "./utils/math";
6
+
7
+ export const DEFAULT_HOVER_SELECTORS = [
8
+ "a",
9
+ "button",
10
+ "input",
11
+ "textarea",
12
+ "[data-hover]",
13
+ "[data-cursor]"
14
+ ];
15
+
16
+ /**
17
+ * Runtime Loop of Supermouse.
18
+ *
19
+ * This class orchestrates the application state, manages the animation loop (`requestAnimationFrame`),
20
+ * and coordinates data flow between the Input system, the Stage system, and the Plugins.
21
+ */
22
+ export class Supermouse {
23
+ public static readonly version: string = __VERSION__;
24
+ public readonly version: string = __VERSION__;
25
+
26
+ state: MouseState;
27
+
28
+ /**
29
+ * Configuration options.
30
+ */
31
+ options: SupermouseOptions;
32
+
33
+ private plugins: SupermousePlugin[] = [];
34
+ private stage: Stage;
35
+ private input: Input;
36
+
37
+ private rafId: number = 0;
38
+ private lastTime: number = 0;
39
+ private isRunning: boolean = false;
40
+
41
+ private hoverSelectors: Set<string>;
42
+
43
+ /**
44
+ * Creates a new Supermouse instance.
45
+ *
46
+ * @param options - Global configuration options.
47
+ * @throws Will throw if running in a non-browser environment (window/document undefined).
48
+ */
49
+ constructor(options: SupermouseOptions = {}) {
50
+ this.options = {
51
+ smoothness: 0.15,
52
+ enableTouch: false,
53
+ autoDisableOnMobile: true,
54
+ ignoreOnNative: "auto",
55
+ hideCursor: true,
56
+ hideOnLeave: true,
57
+ autoStart: true,
58
+ container: document.body,
59
+ ...options
60
+ };
61
+
62
+ if (!this.options.container) {
63
+ this.options.container = document.body;
64
+ }
65
+
66
+ this.state = {
67
+ pointer: { x: -100, y: -100 },
68
+ target: { x: -100, y: -100 },
69
+ smooth: { x: -100, y: -100 },
70
+ velocity: { x: 0, y: 0 },
71
+ angle: 0,
72
+ isDown: false,
73
+ isHover: false,
74
+ isNative: false,
75
+ forcedCursor: null,
76
+ hoverTarget: null,
77
+ reducedMotion: false,
78
+ hasReceivedInput: false,
79
+ shape: null,
80
+ interaction: {}
81
+ };
82
+
83
+ if (this.options.hoverSelectors) {
84
+ this.hoverSelectors = new Set(this.options.hoverSelectors);
85
+ } else {
86
+ this.hoverSelectors = new Set(DEFAULT_HOVER_SELECTORS);
87
+ }
88
+
89
+ this.stage = new Stage(this.options.container, !!this.options.hideCursor);
90
+ this.hoverSelectors.forEach((s) => this.stage.addSelector(s));
91
+
92
+ this.input = new Input(
93
+ this.state,
94
+ this.options,
95
+ () => Array.from(this.hoverSelectors).join(", "),
96
+ (enabled) => {
97
+ if (!enabled) this.resetPosition();
98
+ }
99
+ );
100
+
101
+ if (this.options.plugins) {
102
+ this.options.plugins.forEach((p) => this.use(p));
103
+ }
104
+
105
+ this.init();
106
+ }
107
+
108
+ /**
109
+ * Retrieves a registered plugin instance by its unique name.
110
+ */
111
+ public getPlugin(name: string) {
112
+ return this.plugins.find((p) => p.name === name);
113
+ }
114
+
115
+ /**
116
+ * Returns whether the cursor system is currently enabled (processing input).
117
+ */
118
+ public get isEnabled(): boolean {
119
+ return this.input.isEnabled;
120
+ }
121
+
122
+ /**
123
+ * Enables a specific plugin by name.
124
+ * Triggers the `onEnable` lifecycle hook of the plugin.
125
+ */
126
+ public enablePlugin(name: string) {
127
+ const plugin = this.getPlugin(name);
128
+ if (plugin && plugin.isEnabled === false) {
129
+ plugin.isEnabled = true;
130
+ plugin.onEnable?.(this);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Disables a specific plugin by name.
136
+ * Triggers the `onDisable` lifecycle hook.
137
+ */
138
+ public disablePlugin(name: string) {
139
+ const plugin = this.getPlugin(name);
140
+ if (plugin && plugin.isEnabled !== false) {
141
+ plugin.isEnabled = false;
142
+ plugin.onDisable?.(this);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Toggles the enabled state of a plugin.
148
+ */
149
+ public togglePlugin(name: string) {
150
+ const plugin = this.getPlugin(name);
151
+ if (plugin) {
152
+ if (plugin.isEnabled === false) this.enablePlugin(name);
153
+ else this.disablePlugin(name);
154
+ }
155
+ }
156
+
157
+ public registerHoverTarget(selector: string) {
158
+ if (!this.hoverSelectors.has(selector)) {
159
+ this.hoverSelectors.add(selector);
160
+ this.stage.addSelector(selector);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * The fixed container element where plugins should append their DOM nodes.
166
+ */
167
+ public get container(): HTMLDivElement {
168
+ return this.stage.element;
169
+ }
170
+
171
+ /**
172
+ * Manually override the native cursor visibility.
173
+ *
174
+ * @param type 'auto' (Show Native), 'none' (Hide Native), or null (Resume Auto-detection)
175
+ */
176
+ public setCursor(type: "auto" | "none" | null) {
177
+ this.state.forcedCursor = type;
178
+ }
179
+
180
+ private init() {
181
+ if (this.options.autoStart) {
182
+ this.startLoop();
183
+ }
184
+ }
185
+
186
+ public enable() {
187
+ this.input.isEnabled = true;
188
+ this.stage.setNativeCursor("none");
189
+ }
190
+ public disable() {
191
+ this.input.isEnabled = false;
192
+ this.stage.setNativeCursor("auto");
193
+ this.resetPosition();
194
+ }
195
+
196
+ /**
197
+ * Registers a new plugin.
198
+ *
199
+ * @param plugin - The plugin object to install.
200
+ */
201
+ public use(plugin: SupermousePlugin) {
202
+ if (this.plugins.find((p) => p.name === plugin.name)) {
203
+ console.warn(`[Supermouse] Plugin "${plugin.name}" already installed.`);
204
+ return this;
205
+ }
206
+
207
+ if (plugin.isEnabled === undefined) {
208
+ plugin.isEnabled = true;
209
+ }
210
+
211
+ this.plugins.push(plugin);
212
+ this.plugins.sort((a, b) => (a.priority || 0) - (b.priority || 0));
213
+
214
+ plugin.install?.(this);
215
+ return this;
216
+ }
217
+
218
+ private resetPosition() {
219
+ const off = { x: -100, y: -100 };
220
+ this.state.pointer = { ...off };
221
+ this.state.target = { ...off };
222
+ this.state.smooth = { ...off };
223
+ this.state.velocity = { x: 0, y: 0 };
224
+ this.state.angle = 0;
225
+ this.state.hasReceivedInput = false;
226
+ this.state.shape = null;
227
+ this.state.interaction = {};
228
+ }
229
+
230
+ private startLoop() {
231
+ if (this.isRunning) return;
232
+ this.isRunning = true;
233
+ this.lastTime = performance.now();
234
+ this.tick(this.lastTime);
235
+ }
236
+
237
+ /**
238
+ * Manually steps the animation loop.
239
+ *
240
+ * @param time Current timestamp in milliseconds.
241
+ */
242
+ public step(time: number) {
243
+ this.tick(time);
244
+ }
245
+
246
+ private runPluginSafe(plugin: SupermousePlugin, deltaTime: number) {
247
+ if (plugin.isEnabled === false) return;
248
+ try {
249
+ plugin.update?.(this, deltaTime);
250
+ } catch (e) {
251
+ console.error(`[Supermouse] Plugin '${plugin.name}' crashed and has been disabled.`, e);
252
+ plugin.isEnabled = false;
253
+ try {
254
+ plugin.onDisable?.(this);
255
+ } catch (err) {}
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Runs on every animation frame.
261
+ */
262
+ private tick = (time: number) => {
263
+ const dtMs = time - this.lastTime;
264
+ const dt = Math.min(dtMs / 1000, 0.1);
265
+ this.lastTime = time;
266
+
267
+ if (this.state.hoverTarget && !this.state.hoverTarget.isConnected) {
268
+ this.input.clearHover();
269
+ }
270
+
271
+ const shouldShowStage =
272
+ this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
273
+ this.stage.setVisibility(shouldShowStage);
274
+
275
+ if (this.input.isEnabled && this.options.hideCursor) {
276
+ let targetState: "none" | "auto" = "auto";
277
+
278
+ if (this.state.forcedCursor !== null) {
279
+ targetState = this.state.forcedCursor;
280
+ } else {
281
+ const showNative = this.state.isNative || !this.state.hasReceivedInput;
282
+ targetState = showNative ? "auto" : "none";
283
+ }
284
+
285
+ this.stage.setNativeCursor(targetState);
286
+ }
287
+
288
+ if (this.input.isEnabled) {
289
+ this.state.target.x = this.state.pointer.x;
290
+ this.state.target.y = this.state.pointer.y;
291
+
292
+ for (let i = 0; i < this.plugins.length; i++) {
293
+ this.runPluginSafe(this.plugins[i], dtMs);
294
+ }
295
+
296
+ const factor = this.state.reducedMotion ? 1000 : (1 / this.options.smoothness!) * 2;
297
+
298
+ this.state.smooth.x = damp(this.state.smooth.x, this.state.target.x, factor, dt);
299
+ this.state.smooth.y = damp(this.state.smooth.y, this.state.target.y, factor, dt);
300
+
301
+ const vx = this.state.target.x - this.state.smooth.x;
302
+ const vy = this.state.target.y - this.state.smooth.y;
303
+
304
+ this.state.velocity.x = vx;
305
+ this.state.velocity.y = vy;
306
+
307
+ if (Math.abs(vx) > 0.1 || Math.abs(vy) > 0.1) {
308
+ this.state.angle = angle(vx, vy);
309
+ }
310
+ } else {
311
+ this.state.smooth.x = -100;
312
+ this.state.smooth.y = -100;
313
+ this.state.pointer.x = -100;
314
+ this.state.pointer.y = -100;
315
+ this.state.velocity.x = 0;
316
+ this.state.velocity.y = 0;
317
+
318
+ for (let i = 0; i < this.plugins.length; i++) {
319
+ this.runPluginSafe(this.plugins[i], dtMs);
320
+ }
321
+ }
322
+
323
+ if (this.options.autoStart && this.isRunning) {
324
+ this.rafId = requestAnimationFrame(this.tick);
325
+ }
326
+ };
327
+
328
+ /**
329
+ * Destroys the instance.
330
+ */
331
+ public destroy() {
332
+ this.isRunning = false;
333
+ cancelAnimationFrame(this.rafId);
334
+ this.input.destroy();
335
+ this.stage.destroy();
336
+ this.plugins.forEach((p) => p.destroy?.(this));
337
+ this.plugins = [];
338
+ }
339
+ }