js.foresight 0.0.8 → 0.0.9
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.
|
@@ -33,7 +33,7 @@ export class ForesightDebugger {
|
|
|
33
33
|
transition: opacity 0.2s ease, border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
|
34
34
|
}
|
|
35
35
|
.jsforesight-link-overlay.active {
|
|
36
|
-
border-color:
|
|
36
|
+
border-color: orange; background-color: rgba(255, 0, 0, 0.1);
|
|
37
37
|
}
|
|
38
38
|
.jsforesight-link-overlay.trajectory-hit {
|
|
39
39
|
border-color: lime; background-color: rgba(0, 255, 0, 0.3);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ForesightCallback, ForesightManagerProps, ForesightElement, Rect } from "../types/types";
|
|
2
2
|
export declare class ForesightManager {
|
|
3
|
-
private static
|
|
3
|
+
private static manager;
|
|
4
4
|
private links;
|
|
5
5
|
private isSetup;
|
|
6
6
|
private debugMode;
|
|
@@ -13,7 +13,7 @@ export declare class ForesightManager {
|
|
|
13
13
|
private resizeScrollThrottleTimeoutId;
|
|
14
14
|
private constructor();
|
|
15
15
|
static initialize(props?: Partial<ForesightManagerProps>): ForesightManager;
|
|
16
|
-
static
|
|
16
|
+
static get instance(): ForesightManager;
|
|
17
17
|
private checkTrajectoryHitExpiration;
|
|
18
18
|
private normalizeHitSlop;
|
|
19
19
|
register(element: ForesightElement, callback: ForesightCallback, hitSlop?: number | Rect): () => void;
|
|
@@ -24,7 +24,15 @@ export declare class ForesightManager {
|
|
|
24
24
|
private updateExpandedRect;
|
|
25
25
|
private updateAllRects;
|
|
26
26
|
private predictMousePosition;
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a line segment intersects with an axis-aligned rectangle.
|
|
29
|
+
* Uses the Liang-Barsky line clipping algorithm.
|
|
30
|
+
* @param p1 Start point of the line segment.
|
|
31
|
+
* @param p2 End point of the line segment.
|
|
32
|
+
* @param rect The rectangle to check against.
|
|
33
|
+
* @returns True if the line segment intersects the rectangle, false otherwise.
|
|
34
|
+
*/
|
|
35
|
+
private lineSegmentIntersectsRect;
|
|
28
36
|
private isMouseInExpandedArea;
|
|
29
37
|
private handleMouseMove;
|
|
30
38
|
private handleResizeOrScroll;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { ForesightDebugger } from "./ForesightDebugger";
|
|
3
3
|
export class ForesightManager {
|
|
4
|
-
static
|
|
4
|
+
static manager;
|
|
5
5
|
links = new Map();
|
|
6
6
|
isSetup = false;
|
|
7
7
|
debugMode = false; // Synced with globalSettings.debug
|
|
@@ -9,8 +9,8 @@ export class ForesightManager {
|
|
|
9
9
|
globalSettings = {
|
|
10
10
|
debug: false,
|
|
11
11
|
enableMouseTrajectory: true,
|
|
12
|
-
positionHistorySize:
|
|
13
|
-
trajectoryPredictionTime:
|
|
12
|
+
positionHistorySize: 8,
|
|
13
|
+
trajectoryPredictionTime: 80,
|
|
14
14
|
defaultHitSlop: { top: 0, left: 0, right: 0, bottom: 0 },
|
|
15
15
|
resizeScrollThrottleDelay: 50,
|
|
16
16
|
};
|
|
@@ -20,44 +20,40 @@ export class ForesightManager {
|
|
|
20
20
|
lastResizeScrollCallTimestamp = 0;
|
|
21
21
|
resizeScrollThrottleTimeoutId = null;
|
|
22
22
|
constructor() {
|
|
23
|
-
// Ensure defaultHitSlop is normalized if it's a number initially
|
|
24
23
|
this.globalSettings.defaultHitSlop = this.normalizeHitSlop(this.globalSettings.defaultHitSlop);
|
|
25
24
|
setInterval(this.checkTrajectoryHitExpiration.bind(this), 100);
|
|
26
25
|
}
|
|
27
26
|
static initialize(props) {
|
|
28
|
-
if (!ForesightManager.
|
|
29
|
-
ForesightManager.
|
|
30
|
-
// Apply initial props, which also handles initial debugger setup
|
|
27
|
+
if (!ForesightManager.manager) {
|
|
28
|
+
ForesightManager.manager = new ForesightManager();
|
|
31
29
|
if (props) {
|
|
32
|
-
ForesightManager.
|
|
30
|
+
ForesightManager.manager.alterGlobalSettings(props);
|
|
33
31
|
}
|
|
34
32
|
else {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ForesightManager.instance.turnOnDebugMode();
|
|
33
|
+
if (ForesightManager.manager.globalSettings.debug) {
|
|
34
|
+
ForesightManager.manager.turnOnDebugMode();
|
|
38
35
|
}
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
else if (props) {
|
|
42
|
-
|
|
43
|
-
ForesightManager.
|
|
39
|
+
console.error("ForesightManager is already initialized. Use alterGlobalSettings to update settings. Make sure to not put the ForesightManager.initialize() in a place that rerenders often.");
|
|
40
|
+
ForesightManager.manager.alterGlobalSettings(props);
|
|
44
41
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return ForesightManager.instance;
|
|
42
|
+
ForesightManager.manager.debugMode = ForesightManager.manager.globalSettings.debug;
|
|
43
|
+
return ForesightManager.manager;
|
|
48
44
|
}
|
|
49
|
-
static
|
|
50
|
-
if (!ForesightManager.
|
|
51
|
-
return this.initialize();
|
|
45
|
+
static get instance() {
|
|
46
|
+
if (!ForesightManager.manager) {
|
|
47
|
+
return this.initialize();
|
|
52
48
|
}
|
|
53
|
-
return ForesightManager.
|
|
49
|
+
return ForesightManager.manager;
|
|
54
50
|
}
|
|
55
51
|
checkTrajectoryHitExpiration() {
|
|
56
52
|
const now = performance.now();
|
|
57
53
|
let needsVisualUpdate = false;
|
|
58
54
|
const updatedForesightElements = [];
|
|
59
55
|
this.links.forEach((elementData, element) => {
|
|
60
|
-
if (elementData.isTrajectoryHit && now - elementData.trajectoryHitTime >
|
|
56
|
+
if (elementData.isTrajectoryHit && now - elementData.trajectoryHitTime > 100) {
|
|
61
57
|
this.links.set(element, {
|
|
62
58
|
...elementData,
|
|
63
59
|
isTrajectoryHit: false,
|
|
@@ -89,7 +85,7 @@ export class ForesightManager {
|
|
|
89
85
|
register(element, callback, hitSlop) {
|
|
90
86
|
const normalizedHitSlop = hitSlop
|
|
91
87
|
? this.normalizeHitSlop(hitSlop)
|
|
92
|
-
: this.globalSettings.defaultHitSlop; // Already normalized in constructor
|
|
88
|
+
: this.globalSettings.defaultHitSlop; // Already normalized in constructor
|
|
93
89
|
const originalRect = element.getBoundingClientRect();
|
|
94
90
|
const newElementData = {
|
|
95
91
|
callback,
|
|
@@ -145,7 +141,6 @@ export class ForesightManager {
|
|
|
145
141
|
if (JSON.stringify(this.globalSettings.defaultHitSlop) !== JSON.stringify(newSlop)) {
|
|
146
142
|
this.globalSettings.defaultHitSlop = newSlop;
|
|
147
143
|
settingsActuallyChanged = true;
|
|
148
|
-
// Note: This won't update existing elements' hitSlop unless they are re-registered.
|
|
149
144
|
}
|
|
150
145
|
}
|
|
151
146
|
if (props?.resizeScrollThrottleDelay !== undefined &&
|
|
@@ -172,14 +167,12 @@ export class ForesightManager {
|
|
|
172
167
|
}
|
|
173
168
|
}
|
|
174
169
|
turnOnDebugMode() {
|
|
175
|
-
this.debugMode = true;
|
|
170
|
+
this.debugMode = true;
|
|
176
171
|
if (!this.debugger) {
|
|
177
172
|
this.debugger = new ForesightDebugger(this);
|
|
178
173
|
this.debugger.initialize(this.links, this.globalSettings, this.currentPoint, this.predictedPoint);
|
|
179
174
|
}
|
|
180
175
|
else {
|
|
181
|
-
// If debugger exists, ensure its controls are up-to-date with current globalSettings
|
|
182
|
-
// This could happen if debug was false, then true again, or settings changed.
|
|
183
176
|
this.debugger.updateControlsState(this.globalSettings);
|
|
184
177
|
this.debugger.updateTrajectoryVisuals(this.currentPoint, this.predictedPoint, this.globalSettings.enableMouseTrajectory);
|
|
185
178
|
}
|
|
@@ -243,9 +236,67 @@ export class ForesightManager {
|
|
|
243
236
|
const predictedY = y + vy * trajectoryPredictionTimeInSeconds;
|
|
244
237
|
return { x: predictedX, y: predictedY };
|
|
245
238
|
};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Checks if a line segment intersects with an axis-aligned rectangle.
|
|
241
|
+
* Uses the Liang-Barsky line clipping algorithm.
|
|
242
|
+
* @param p1 Start point of the line segment.
|
|
243
|
+
* @param p2 End point of the line segment.
|
|
244
|
+
* @param rect The rectangle to check against.
|
|
245
|
+
* @returns True if the line segment intersects the rectangle, false otherwise.
|
|
246
|
+
*/
|
|
247
|
+
lineSegmentIntersectsRect(p1, p2, rect) {
|
|
248
|
+
let t0 = 0.0;
|
|
249
|
+
let t1 = 1.0;
|
|
250
|
+
const dx = p2.x - p1.x;
|
|
251
|
+
const dy = p2.y - p1.y;
|
|
252
|
+
// Helper function for Liang-Barsky algorithm
|
|
253
|
+
// p: parameter related to edge normal and line direction
|
|
254
|
+
// q: parameter related to distance from p1 to edge
|
|
255
|
+
const clipTest = (p, q) => {
|
|
256
|
+
if (p === 0) {
|
|
257
|
+
// Line is parallel to the clip edge
|
|
258
|
+
if (q < 0) {
|
|
259
|
+
// Line is outside the clip edge (p1 is on the "wrong" side)
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const r = q / p;
|
|
265
|
+
if (p < 0) {
|
|
266
|
+
// Line proceeds from outside to inside (potential entry)
|
|
267
|
+
if (r > t1)
|
|
268
|
+
return false; // Enters after already exited
|
|
269
|
+
if (r > t0)
|
|
270
|
+
t0 = r; // Update latest entry time
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// Line proceeds from inside to outside (potential exit) (p > 0)
|
|
274
|
+
if (r < t0)
|
|
275
|
+
return false; // Exits before already entered
|
|
276
|
+
if (r < t1)
|
|
277
|
+
t1 = r; // Update earliest exit time
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
};
|
|
282
|
+
// Left edge: rect.left
|
|
283
|
+
if (!clipTest(-dx, p1.x - rect.left))
|
|
284
|
+
return false;
|
|
285
|
+
// Right edge: rect.right
|
|
286
|
+
if (!clipTest(dx, rect.right - p1.x))
|
|
287
|
+
return false;
|
|
288
|
+
// Top edge: rect.top
|
|
289
|
+
if (!clipTest(-dy, p1.y - rect.top))
|
|
290
|
+
return false;
|
|
291
|
+
// Bottom edge: rect.bottom
|
|
292
|
+
if (!clipTest(dy, rect.bottom - p1.y))
|
|
293
|
+
return false;
|
|
294
|
+
// If t0 > t1, the segment is completely outside or misses the clip window.
|
|
295
|
+
// Also, the valid intersection must be within the segment [0,1].
|
|
296
|
+
// Since t0 and t1 are initialized to 0 and 1, and clamped,
|
|
297
|
+
// this also ensures the intersection lies on the segment.
|
|
298
|
+
return t0 <= t1;
|
|
299
|
+
}
|
|
249
300
|
isMouseInExpandedArea = (area, clientPoint, isAlreadyHovering) => {
|
|
250
301
|
const isInExpandedArea = clientPoint.x >= area.left &&
|
|
251
302
|
clientPoint.x <= area.right &&
|
|
@@ -268,24 +319,29 @@ export class ForesightManager {
|
|
|
268
319
|
const { isHoveringInArea, shouldRunCallback } = this.isMouseInExpandedArea(elementData.elementBounds.expandedRect, this.currentPoint, elementData.isHovering);
|
|
269
320
|
let linkStateChanged = false;
|
|
270
321
|
if (this.globalSettings.enableMouseTrajectory && !isHoveringInArea) {
|
|
271
|
-
if
|
|
322
|
+
// Check if the trajectory line segment intersects the expanded rect
|
|
323
|
+
if (this.lineSegmentIntersectsRect(this.currentPoint, this.predictedPoint, elementData.elementBounds.expandedRect)) {
|
|
272
324
|
if (!elementData.isTrajectoryHit) {
|
|
273
325
|
elementData.callback();
|
|
274
326
|
this.links.set(element, {
|
|
275
327
|
...elementData,
|
|
276
328
|
isTrajectoryHit: true,
|
|
277
329
|
trajectoryHitTime: performance.now(),
|
|
278
|
-
isHovering: isHoveringInArea,
|
|
330
|
+
isHovering: isHoveringInArea, // isHoveringInArea is false here
|
|
279
331
|
});
|
|
280
332
|
linkStateChanged = true;
|
|
281
333
|
}
|
|
282
334
|
}
|
|
335
|
+
// Note: No 'else' here to turn off isTrajectoryHit immediately.
|
|
336
|
+
// It's managed by checkTrajectoryHitExpiration or when actual hover occurs.
|
|
283
337
|
}
|
|
284
338
|
if (elementData.isHovering !== isHoveringInArea) {
|
|
285
339
|
this.links.set(element, {
|
|
286
340
|
...elementData,
|
|
287
341
|
isHovering: isHoveringInArea,
|
|
288
|
-
// Preserve trajectory hit state if it was already hit
|
|
342
|
+
// Preserve trajectory hit state if it was already hit,
|
|
343
|
+
// unless actual hover is now false and trajectory also doesn't hit
|
|
344
|
+
// (though trajectory hit is primarily for non-hovering states)
|
|
289
345
|
isTrajectoryHit: this.links.get(element).isTrajectoryHit,
|
|
290
346
|
trajectoryHitTime: this.links.get(element).trajectoryHitTime,
|
|
291
347
|
});
|
package/dist/types/types.d.ts
CHANGED
|
@@ -68,13 +68,13 @@ export type ForesightManagerProps = {
|
|
|
68
68
|
/**
|
|
69
69
|
* Number of mouse positions to keep in history for trajectory calculation.
|
|
70
70
|
* A higher number might lead to smoother but slightly delayed predictions.
|
|
71
|
-
* @default
|
|
71
|
+
* @default 8
|
|
72
72
|
*/
|
|
73
73
|
positionHistorySize: number;
|
|
74
74
|
/**
|
|
75
75
|
* How far ahead (in milliseconds) to predict the mouse trajectory.
|
|
76
76
|
* A larger value means the prediction extends further into the future. (meaning it will trigger callbacks sooner)
|
|
77
|
-
* @default
|
|
77
|
+
* @default 80
|
|
78
78
|
*/
|
|
79
79
|
trajectoryPredictionTime: number;
|
|
80
80
|
/**
|