@macrulez/inview-core 0.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/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/index.cjs +416 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +153 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +379 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
type ScrollDirection = 'up' | 'down' | 'left' | 'right' | null;
|
|
2
|
+
interface ScrollState {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
direction: ScrollDirection;
|
|
6
|
+
/** 0..1 */
|
|
7
|
+
progress: number;
|
|
8
|
+
/** px per frame */
|
|
9
|
+
velocity: number;
|
|
10
|
+
isScrolling: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface ScrollToOptions {
|
|
13
|
+
behavior?: 'auto' | 'smooth';
|
|
14
|
+
}
|
|
15
|
+
interface ScrollEngineOptions {
|
|
16
|
+
/** ms of scroll inactivity before isScrolling flips back to false */
|
|
17
|
+
idleTimeout?: number;
|
|
18
|
+
}
|
|
19
|
+
interface ScrollEngine {
|
|
20
|
+
getState(): ScrollState;
|
|
21
|
+
/** returns unsubscribe */
|
|
22
|
+
subscribe(cb: (state: ScrollState) => void): () => void;
|
|
23
|
+
scrollTo(pos: number | {
|
|
24
|
+
x?: number;
|
|
25
|
+
y?: number;
|
|
26
|
+
}, opts?: ScrollToOptions): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
29
|
+
type IntersectionEdge = 'enter-top' | 'enter-bottom' | 'leave-top' | 'leave-bottom';
|
|
30
|
+
interface IntersectionInfo {
|
|
31
|
+
isIntersecting: boolean;
|
|
32
|
+
intersectionRatio: number;
|
|
33
|
+
boundingClientRect: DOMRectReadOnly;
|
|
34
|
+
edge?: IntersectionEdge;
|
|
35
|
+
}
|
|
36
|
+
interface ObserverPoolOptions {
|
|
37
|
+
root?: Element | Document | null;
|
|
38
|
+
rootMargin?: string;
|
|
39
|
+
threshold?: number | number[];
|
|
40
|
+
}
|
|
41
|
+
interface VisibilityObserveOptions extends ObserverPoolOptions {
|
|
42
|
+
once?: boolean;
|
|
43
|
+
onEnter?: (info: IntersectionInfo) => void;
|
|
44
|
+
onLeave?: (info: IntersectionInfo) => void;
|
|
45
|
+
}
|
|
46
|
+
interface VisibilityEngine {
|
|
47
|
+
/** returns unsubscribe */
|
|
48
|
+
observe(el: Element, options: VisibilityObserveOptions, cb: (info: IntersectionInfo) => void): () => void;
|
|
49
|
+
destroy(): void;
|
|
50
|
+
}
|
|
51
|
+
interface DOMRectLike {
|
|
52
|
+
top: number;
|
|
53
|
+
left: number;
|
|
54
|
+
right: number;
|
|
55
|
+
bottom: number;
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
interface ElementTrackerState {
|
|
60
|
+
rect: DOMRectLike;
|
|
61
|
+
/** 0..1, from element entering at viewport bottom to leaving at viewport top */
|
|
62
|
+
viewportProgress: number;
|
|
63
|
+
/** px offset of element center from viewport center */
|
|
64
|
+
distanceFromCenter: number;
|
|
65
|
+
}
|
|
66
|
+
interface ElementTracker {
|
|
67
|
+
getState(): ElementTrackerState;
|
|
68
|
+
subscribe(cb: (state: ElementTrackerState) => void): () => void;
|
|
69
|
+
destroy(): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type ScrollTarget = Window | HTMLElement;
|
|
73
|
+
/**
|
|
74
|
+
* Framework-agnostic scroll engine for window or a scrollable element.
|
|
75
|
+
* Batches DOM scroll events into one shared rAF loop and notifies
|
|
76
|
+
* subscribers only when the derived state actually changes.
|
|
77
|
+
*/
|
|
78
|
+
declare function createScrollEngine(target?: ScrollTarget, options?: ScrollEngineOptions): ScrollEngine;
|
|
79
|
+
|
|
80
|
+
type EntryCallback = (entry: IntersectionObserverEntry) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Pools IntersectionObserver instances by (root, rootMargin, threshold),
|
|
83
|
+
* so many observed elements sharing the same options share one observer
|
|
84
|
+
* instead of spawning one per element.
|
|
85
|
+
*/
|
|
86
|
+
declare class ObserverPool {
|
|
87
|
+
private pools;
|
|
88
|
+
observe(el: Element, options: ObserverPoolOptions, cb: EntryCallback): () => void;
|
|
89
|
+
}
|
|
90
|
+
declare const observerPool: ObserverPool;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Wraps the shared observer pool with enter/leave edge detection and
|
|
94
|
+
* `once` support, without any framework-specific reactivity.
|
|
95
|
+
*/
|
|
96
|
+
declare function createVisibilityEngine(pool?: ObserverPool): VisibilityEngine;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Tracks an element's position relative to the viewport (rect,
|
|
100
|
+
* viewportProgress, distanceFromCenter) on the shared rAF loop.
|
|
101
|
+
*/
|
|
102
|
+
declare function createElementTracker(el: HTMLElement): ElementTracker;
|
|
103
|
+
|
|
104
|
+
type Tick = (time: number) => void;
|
|
105
|
+
/**
|
|
106
|
+
* One shared requestAnimationFrame loop for the whole engine, instead of
|
|
107
|
+
* every scroll/tracker subscription running its own rAF callback.
|
|
108
|
+
*/
|
|
109
|
+
declare class RafLoop {
|
|
110
|
+
private callbacks;
|
|
111
|
+
private handle;
|
|
112
|
+
add(cb: Tick): () => void;
|
|
113
|
+
private start;
|
|
114
|
+
private stop;
|
|
115
|
+
}
|
|
116
|
+
declare const rafLoop: RafLoop;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Linearly maps `value` from the `from` range into the `to` range.
|
|
120
|
+
* Pass `clampResult: true` to keep the result within `to` at the edges.
|
|
121
|
+
*/
|
|
122
|
+
declare function mapRange(value: number, from: [number, number], to: [number, number], clampResult?: boolean): number;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Writes a numeric progress value into a CSS custom property on `el`,
|
|
126
|
+
* so the resulting visual effect can be composed in plain CSS.
|
|
127
|
+
*/
|
|
128
|
+
declare function bindCSSVar(el: HTMLElement, name: string, value: number | string): void;
|
|
129
|
+
|
|
130
|
+
declare function prefersReducedMotion(): boolean;
|
|
131
|
+
|
|
132
|
+
declare function clamp(value: number, min: number, max: number): number;
|
|
133
|
+
|
|
134
|
+
type Easing = (t: number) => number;
|
|
135
|
+
/**
|
|
136
|
+
* Standard easing presets to apply to a 0..1 progress value before
|
|
137
|
+
* feeding it into an effect (parallax offset, CSS var, etc).
|
|
138
|
+
*/
|
|
139
|
+
declare const easings: {
|
|
140
|
+
linear: (t: number) => number;
|
|
141
|
+
easeInQuad: (t: number) => number;
|
|
142
|
+
easeOutQuad: (t: number) => number;
|
|
143
|
+
easeInOutQuad: (t: number) => number;
|
|
144
|
+
easeInCubic: (t: number) => number;
|
|
145
|
+
easeOutCubic: (t: number) => number;
|
|
146
|
+
easeInOutCubic: (t: number) => number;
|
|
147
|
+
easeInSine: (t: number) => number;
|
|
148
|
+
easeOutSine: (t: number) => number;
|
|
149
|
+
easeInOutSine: (t: number) => number;
|
|
150
|
+
};
|
|
151
|
+
type EasingName = keyof typeof easings;
|
|
152
|
+
|
|
153
|
+
export { type DOMRectLike, type Easing, type EasingName, type ElementTracker, type ElementTrackerState, type IntersectionEdge, type IntersectionInfo, ObserverPool, type ObserverPoolOptions, type ScrollDirection, type ScrollEngine, type ScrollEngineOptions, type ScrollState, type ScrollToOptions, type VisibilityEngine, type VisibilityObserveOptions, bindCSSVar, clamp, createElementTracker, createScrollEngine, createVisibilityEngine, easings, mapRange, observerPool, prefersReducedMotion, rafLoop };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
type ScrollDirection = 'up' | 'down' | 'left' | 'right' | null;
|
|
2
|
+
interface ScrollState {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
direction: ScrollDirection;
|
|
6
|
+
/** 0..1 */
|
|
7
|
+
progress: number;
|
|
8
|
+
/** px per frame */
|
|
9
|
+
velocity: number;
|
|
10
|
+
isScrolling: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface ScrollToOptions {
|
|
13
|
+
behavior?: 'auto' | 'smooth';
|
|
14
|
+
}
|
|
15
|
+
interface ScrollEngineOptions {
|
|
16
|
+
/** ms of scroll inactivity before isScrolling flips back to false */
|
|
17
|
+
idleTimeout?: number;
|
|
18
|
+
}
|
|
19
|
+
interface ScrollEngine {
|
|
20
|
+
getState(): ScrollState;
|
|
21
|
+
/** returns unsubscribe */
|
|
22
|
+
subscribe(cb: (state: ScrollState) => void): () => void;
|
|
23
|
+
scrollTo(pos: number | {
|
|
24
|
+
x?: number;
|
|
25
|
+
y?: number;
|
|
26
|
+
}, opts?: ScrollToOptions): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
29
|
+
type IntersectionEdge = 'enter-top' | 'enter-bottom' | 'leave-top' | 'leave-bottom';
|
|
30
|
+
interface IntersectionInfo {
|
|
31
|
+
isIntersecting: boolean;
|
|
32
|
+
intersectionRatio: number;
|
|
33
|
+
boundingClientRect: DOMRectReadOnly;
|
|
34
|
+
edge?: IntersectionEdge;
|
|
35
|
+
}
|
|
36
|
+
interface ObserverPoolOptions {
|
|
37
|
+
root?: Element | Document | null;
|
|
38
|
+
rootMargin?: string;
|
|
39
|
+
threshold?: number | number[];
|
|
40
|
+
}
|
|
41
|
+
interface VisibilityObserveOptions extends ObserverPoolOptions {
|
|
42
|
+
once?: boolean;
|
|
43
|
+
onEnter?: (info: IntersectionInfo) => void;
|
|
44
|
+
onLeave?: (info: IntersectionInfo) => void;
|
|
45
|
+
}
|
|
46
|
+
interface VisibilityEngine {
|
|
47
|
+
/** returns unsubscribe */
|
|
48
|
+
observe(el: Element, options: VisibilityObserveOptions, cb: (info: IntersectionInfo) => void): () => void;
|
|
49
|
+
destroy(): void;
|
|
50
|
+
}
|
|
51
|
+
interface DOMRectLike {
|
|
52
|
+
top: number;
|
|
53
|
+
left: number;
|
|
54
|
+
right: number;
|
|
55
|
+
bottom: number;
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
interface ElementTrackerState {
|
|
60
|
+
rect: DOMRectLike;
|
|
61
|
+
/** 0..1, from element entering at viewport bottom to leaving at viewport top */
|
|
62
|
+
viewportProgress: number;
|
|
63
|
+
/** px offset of element center from viewport center */
|
|
64
|
+
distanceFromCenter: number;
|
|
65
|
+
}
|
|
66
|
+
interface ElementTracker {
|
|
67
|
+
getState(): ElementTrackerState;
|
|
68
|
+
subscribe(cb: (state: ElementTrackerState) => void): () => void;
|
|
69
|
+
destroy(): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type ScrollTarget = Window | HTMLElement;
|
|
73
|
+
/**
|
|
74
|
+
* Framework-agnostic scroll engine for window or a scrollable element.
|
|
75
|
+
* Batches DOM scroll events into one shared rAF loop and notifies
|
|
76
|
+
* subscribers only when the derived state actually changes.
|
|
77
|
+
*/
|
|
78
|
+
declare function createScrollEngine(target?: ScrollTarget, options?: ScrollEngineOptions): ScrollEngine;
|
|
79
|
+
|
|
80
|
+
type EntryCallback = (entry: IntersectionObserverEntry) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Pools IntersectionObserver instances by (root, rootMargin, threshold),
|
|
83
|
+
* so many observed elements sharing the same options share one observer
|
|
84
|
+
* instead of spawning one per element.
|
|
85
|
+
*/
|
|
86
|
+
declare class ObserverPool {
|
|
87
|
+
private pools;
|
|
88
|
+
observe(el: Element, options: ObserverPoolOptions, cb: EntryCallback): () => void;
|
|
89
|
+
}
|
|
90
|
+
declare const observerPool: ObserverPool;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Wraps the shared observer pool with enter/leave edge detection and
|
|
94
|
+
* `once` support, without any framework-specific reactivity.
|
|
95
|
+
*/
|
|
96
|
+
declare function createVisibilityEngine(pool?: ObserverPool): VisibilityEngine;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Tracks an element's position relative to the viewport (rect,
|
|
100
|
+
* viewportProgress, distanceFromCenter) on the shared rAF loop.
|
|
101
|
+
*/
|
|
102
|
+
declare function createElementTracker(el: HTMLElement): ElementTracker;
|
|
103
|
+
|
|
104
|
+
type Tick = (time: number) => void;
|
|
105
|
+
/**
|
|
106
|
+
* One shared requestAnimationFrame loop for the whole engine, instead of
|
|
107
|
+
* every scroll/tracker subscription running its own rAF callback.
|
|
108
|
+
*/
|
|
109
|
+
declare class RafLoop {
|
|
110
|
+
private callbacks;
|
|
111
|
+
private handle;
|
|
112
|
+
add(cb: Tick): () => void;
|
|
113
|
+
private start;
|
|
114
|
+
private stop;
|
|
115
|
+
}
|
|
116
|
+
declare const rafLoop: RafLoop;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Linearly maps `value` from the `from` range into the `to` range.
|
|
120
|
+
* Pass `clampResult: true` to keep the result within `to` at the edges.
|
|
121
|
+
*/
|
|
122
|
+
declare function mapRange(value: number, from: [number, number], to: [number, number], clampResult?: boolean): number;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Writes a numeric progress value into a CSS custom property on `el`,
|
|
126
|
+
* so the resulting visual effect can be composed in plain CSS.
|
|
127
|
+
*/
|
|
128
|
+
declare function bindCSSVar(el: HTMLElement, name: string, value: number | string): void;
|
|
129
|
+
|
|
130
|
+
declare function prefersReducedMotion(): boolean;
|
|
131
|
+
|
|
132
|
+
declare function clamp(value: number, min: number, max: number): number;
|
|
133
|
+
|
|
134
|
+
type Easing = (t: number) => number;
|
|
135
|
+
/**
|
|
136
|
+
* Standard easing presets to apply to a 0..1 progress value before
|
|
137
|
+
* feeding it into an effect (parallax offset, CSS var, etc).
|
|
138
|
+
*/
|
|
139
|
+
declare const easings: {
|
|
140
|
+
linear: (t: number) => number;
|
|
141
|
+
easeInQuad: (t: number) => number;
|
|
142
|
+
easeOutQuad: (t: number) => number;
|
|
143
|
+
easeInOutQuad: (t: number) => number;
|
|
144
|
+
easeInCubic: (t: number) => number;
|
|
145
|
+
easeOutCubic: (t: number) => number;
|
|
146
|
+
easeInOutCubic: (t: number) => number;
|
|
147
|
+
easeInSine: (t: number) => number;
|
|
148
|
+
easeOutSine: (t: number) => number;
|
|
149
|
+
easeInOutSine: (t: number) => number;
|
|
150
|
+
};
|
|
151
|
+
type EasingName = keyof typeof easings;
|
|
152
|
+
|
|
153
|
+
export { type DOMRectLike, type Easing, type EasingName, type ElementTracker, type ElementTrackerState, type IntersectionEdge, type IntersectionInfo, ObserverPool, type ObserverPoolOptions, type ScrollDirection, type ScrollEngine, type ScrollEngineOptions, type ScrollState, type ScrollToOptions, type VisibilityEngine, type VisibilityObserveOptions, bindCSSVar, clamp, createElementTracker, createScrollEngine, createVisibilityEngine, easings, mapRange, observerPool, prefersReducedMotion, rafLoop };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// src/raf-loop.ts
|
|
2
|
+
var RafLoop = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.callbacks = /* @__PURE__ */ new Set();
|
|
5
|
+
this.handle = null;
|
|
6
|
+
}
|
|
7
|
+
add(cb) {
|
|
8
|
+
this.callbacks.add(cb);
|
|
9
|
+
this.start();
|
|
10
|
+
return () => {
|
|
11
|
+
this.callbacks.delete(cb);
|
|
12
|
+
if (this.callbacks.size === 0) this.stop();
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
start() {
|
|
16
|
+
if (this.handle !== null || typeof requestAnimationFrame === "undefined") return;
|
|
17
|
+
const loop = (time) => {
|
|
18
|
+
this.callbacks.forEach((cb) => cb(time));
|
|
19
|
+
if (this.callbacks.size > 0) {
|
|
20
|
+
this.handle = requestAnimationFrame(loop);
|
|
21
|
+
} else {
|
|
22
|
+
this.handle = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
this.handle = requestAnimationFrame(loop);
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
if (this.handle !== null && typeof cancelAnimationFrame !== "undefined") {
|
|
29
|
+
cancelAnimationFrame(this.handle);
|
|
30
|
+
}
|
|
31
|
+
this.handle = null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var rafLoop = new RafLoop();
|
|
35
|
+
|
|
36
|
+
// src/utils/clamp.ts
|
|
37
|
+
function clamp(value, min, max) {
|
|
38
|
+
return Math.min(max, Math.max(min, value));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/utils/prefersReducedMotion.ts
|
|
42
|
+
function prefersReducedMotion() {
|
|
43
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
44
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/scroll-engine.ts
|
|
48
|
+
function isWindowTarget(target) {
|
|
49
|
+
return typeof window !== "undefined" && target === window;
|
|
50
|
+
}
|
|
51
|
+
function getScrollPosition(target) {
|
|
52
|
+
if (isWindowTarget(target)) return { x: window.scrollX, y: window.scrollY };
|
|
53
|
+
return { x: target.scrollLeft, y: target.scrollTop };
|
|
54
|
+
}
|
|
55
|
+
function getMaxScroll(target) {
|
|
56
|
+
if (isWindowTarget(target)) {
|
|
57
|
+
const doc = document.documentElement;
|
|
58
|
+
return {
|
|
59
|
+
x: Math.max(doc.scrollWidth - window.innerWidth, 0),
|
|
60
|
+
y: Math.max(doc.scrollHeight - window.innerHeight, 0)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
x: Math.max(target.scrollWidth - target.clientWidth, 0),
|
|
65
|
+
y: Math.max(target.scrollHeight - target.clientHeight, 0)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function computeProgress(target, x, y) {
|
|
69
|
+
const max = getMaxScroll(target);
|
|
70
|
+
if (max.y > 0) return clamp(y / max.y, 0, 1);
|
|
71
|
+
if (max.x > 0) return clamp(x / max.x, 0, 1);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
function createNoopEngine() {
|
|
75
|
+
const state = { x: 0, y: 0, direction: null, progress: 0, velocity: 0, isScrolling: false };
|
|
76
|
+
return {
|
|
77
|
+
getState: () => state,
|
|
78
|
+
subscribe: (cb) => {
|
|
79
|
+
cb(state);
|
|
80
|
+
return () => {
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
scrollTo: () => {
|
|
84
|
+
},
|
|
85
|
+
destroy: () => {
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function createScrollEngine(target = typeof window !== "undefined" ? window : void 0, options = {}) {
|
|
90
|
+
if (typeof window === "undefined" || !target) return createNoopEngine();
|
|
91
|
+
const idleTimeout = options.idleTimeout ?? 150;
|
|
92
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
93
|
+
const initial = getScrollPosition(target);
|
|
94
|
+
let state = {
|
|
95
|
+
x: initial.x,
|
|
96
|
+
y: initial.y,
|
|
97
|
+
direction: null,
|
|
98
|
+
progress: computeProgress(target, initial.x, initial.y),
|
|
99
|
+
velocity: 0,
|
|
100
|
+
isScrolling: false
|
|
101
|
+
};
|
|
102
|
+
let frameLastX = initial.x;
|
|
103
|
+
let frameLastY = initial.y;
|
|
104
|
+
let idleHandle = null;
|
|
105
|
+
function notify() {
|
|
106
|
+
subscribers.forEach((cb) => cb(state));
|
|
107
|
+
}
|
|
108
|
+
function onScroll() {
|
|
109
|
+
const pos = getScrollPosition(target);
|
|
110
|
+
const dx = pos.x - state.x;
|
|
111
|
+
const dy = pos.y - state.y;
|
|
112
|
+
let direction = state.direction;
|
|
113
|
+
if (dy > 0) direction = "down";
|
|
114
|
+
else if (dy < 0) direction = "up";
|
|
115
|
+
else if (dx > 0) direction = "right";
|
|
116
|
+
else if (dx < 0) direction = "left";
|
|
117
|
+
state = {
|
|
118
|
+
...state,
|
|
119
|
+
x: pos.x,
|
|
120
|
+
y: pos.y,
|
|
121
|
+
direction,
|
|
122
|
+
progress: computeProgress(target, pos.x, pos.y),
|
|
123
|
+
isScrolling: true
|
|
124
|
+
};
|
|
125
|
+
if (idleHandle !== null) clearTimeout(idleHandle);
|
|
126
|
+
idleHandle = setTimeout(() => {
|
|
127
|
+
idleHandle = null;
|
|
128
|
+
state = { ...state, isScrolling: false, velocity: 0 };
|
|
129
|
+
notify();
|
|
130
|
+
}, idleTimeout);
|
|
131
|
+
notify();
|
|
132
|
+
}
|
|
133
|
+
target.addEventListener("scroll", onScroll, { passive: true });
|
|
134
|
+
const removeRaf = rafLoop.add(() => {
|
|
135
|
+
const velocity = Math.hypot(state.x - frameLastX, state.y - frameLastY);
|
|
136
|
+
frameLastX = state.x;
|
|
137
|
+
frameLastY = state.y;
|
|
138
|
+
if (velocity !== state.velocity) {
|
|
139
|
+
state = { ...state, velocity };
|
|
140
|
+
notify();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
function scrollTo(pos, opts = {}) {
|
|
144
|
+
const behavior = opts.behavior ?? (prefersReducedMotion() ? "auto" : "smooth");
|
|
145
|
+
const left = typeof pos === "number" ? void 0 : pos.x;
|
|
146
|
+
const top = typeof pos === "number" ? pos : pos.y;
|
|
147
|
+
target.scrollTo({ left, top, behavior });
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
getState: () => state,
|
|
151
|
+
subscribe(cb) {
|
|
152
|
+
subscribers.add(cb);
|
|
153
|
+
cb(state);
|
|
154
|
+
return () => subscribers.delete(cb);
|
|
155
|
+
},
|
|
156
|
+
scrollTo,
|
|
157
|
+
destroy() {
|
|
158
|
+
target.removeEventListener("scroll", onScroll);
|
|
159
|
+
if (idleHandle !== null) clearTimeout(idleHandle);
|
|
160
|
+
removeRaf();
|
|
161
|
+
subscribers.clear();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/observer-pool.ts
|
|
167
|
+
function keyFor(options) {
|
|
168
|
+
const threshold = Array.isArray(options.threshold) ? options.threshold.join(",") : String(options.threshold ?? 0);
|
|
169
|
+
const root = options.root ? "r" : "window";
|
|
170
|
+
return `${root}|${options.rootMargin ?? "0px"}|${threshold}`;
|
|
171
|
+
}
|
|
172
|
+
var ObserverPool = class {
|
|
173
|
+
constructor() {
|
|
174
|
+
this.pools = /* @__PURE__ */ new Map();
|
|
175
|
+
}
|
|
176
|
+
observe(el, options, cb) {
|
|
177
|
+
if (typeof IntersectionObserver === "undefined") return () => {
|
|
178
|
+
};
|
|
179
|
+
const key = keyFor(options);
|
|
180
|
+
let pool = this.pools.get(key);
|
|
181
|
+
if (!pool) {
|
|
182
|
+
const callbacks2 = /* @__PURE__ */ new Map();
|
|
183
|
+
const observer = new IntersectionObserver(
|
|
184
|
+
(entries) => {
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
callbacks2.get(entry.target)?.forEach((fn) => fn(entry));
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
root: options.root ?? null,
|
|
191
|
+
rootMargin: options.rootMargin ?? "0px",
|
|
192
|
+
threshold: options.threshold ?? 0
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
pool = { observer, callbacks: callbacks2 };
|
|
196
|
+
this.pools.set(key, pool);
|
|
197
|
+
}
|
|
198
|
+
let callbacks = pool.callbacks.get(el);
|
|
199
|
+
if (!callbacks) {
|
|
200
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
201
|
+
pool.callbacks.set(el, callbacks);
|
|
202
|
+
}
|
|
203
|
+
const isNewElement = callbacks.size === 0;
|
|
204
|
+
callbacks.add(cb);
|
|
205
|
+
if (isNewElement) pool.observer.observe(el);
|
|
206
|
+
const currentPool = pool;
|
|
207
|
+
return () => {
|
|
208
|
+
const set = currentPool.callbacks.get(el);
|
|
209
|
+
if (!set) return;
|
|
210
|
+
set.delete(cb);
|
|
211
|
+
if (set.size === 0) {
|
|
212
|
+
currentPool.callbacks.delete(el);
|
|
213
|
+
currentPool.observer.unobserve(el);
|
|
214
|
+
}
|
|
215
|
+
if (currentPool.callbacks.size === 0) {
|
|
216
|
+
currentPool.observer.disconnect();
|
|
217
|
+
this.pools.delete(key);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var observerPool = new ObserverPool();
|
|
223
|
+
|
|
224
|
+
// src/visibility-engine.ts
|
|
225
|
+
function computeEdge(entry, wasIntersecting) {
|
|
226
|
+
if (entry.isIntersecting === wasIntersecting) return void 0;
|
|
227
|
+
const rootTop = entry.rootBounds ? entry.rootBounds.top : 0;
|
|
228
|
+
const enteringOrLeavingFromBottom = entry.boundingClientRect.top > rootTop;
|
|
229
|
+
if (entry.isIntersecting) return enteringOrLeavingFromBottom ? "enter-bottom" : "enter-top";
|
|
230
|
+
return enteringOrLeavingFromBottom ? "leave-bottom" : "leave-top";
|
|
231
|
+
}
|
|
232
|
+
function createVisibilityEngine(pool = observerPool) {
|
|
233
|
+
const unsubs = /* @__PURE__ */ new Set();
|
|
234
|
+
function observe(el, options, cb) {
|
|
235
|
+
let wasIntersecting = false;
|
|
236
|
+
const unobserve = pool.observe(el, options, (entry) => {
|
|
237
|
+
const edge = computeEdge(entry, wasIntersecting);
|
|
238
|
+
wasIntersecting = entry.isIntersecting;
|
|
239
|
+
const info = {
|
|
240
|
+
isIntersecting: entry.isIntersecting,
|
|
241
|
+
intersectionRatio: entry.intersectionRatio,
|
|
242
|
+
boundingClientRect: entry.boundingClientRect,
|
|
243
|
+
edge
|
|
244
|
+
};
|
|
245
|
+
cb(info);
|
|
246
|
+
if (edge === "enter-top" || edge === "enter-bottom") options.onEnter?.(info);
|
|
247
|
+
if (edge === "leave-top" || edge === "leave-bottom") options.onLeave?.(info);
|
|
248
|
+
if (options.once && entry.isIntersecting) {
|
|
249
|
+
unsubs.delete(unobserve);
|
|
250
|
+
unobserve();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
unsubs.add(unobserve);
|
|
254
|
+
return () => {
|
|
255
|
+
unsubs.delete(unobserve);
|
|
256
|
+
unobserve();
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
observe,
|
|
261
|
+
destroy() {
|
|
262
|
+
unsubs.forEach((fn) => fn());
|
|
263
|
+
unsubs.clear();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/element-tracker.ts
|
|
269
|
+
function toDomRectLike(rect) {
|
|
270
|
+
return {
|
|
271
|
+
top: rect.top,
|
|
272
|
+
left: rect.left,
|
|
273
|
+
right: rect.right,
|
|
274
|
+
bottom: rect.bottom,
|
|
275
|
+
width: rect.width,
|
|
276
|
+
height: rect.height
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
var emptyRect = { top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0 };
|
|
280
|
+
function computeViewportProgress(rect, viewportHeight) {
|
|
281
|
+
const total = viewportHeight + rect.height;
|
|
282
|
+
if (total <= 0) return 0;
|
|
283
|
+
const traveled = viewportHeight - rect.top;
|
|
284
|
+
return clamp(traveled / total, 0, 1);
|
|
285
|
+
}
|
|
286
|
+
function computeState(el) {
|
|
287
|
+
const rect = el.getBoundingClientRect();
|
|
288
|
+
const viewportHeight = window.innerHeight;
|
|
289
|
+
const elementCenter = rect.top + rect.height / 2;
|
|
290
|
+
const viewportCenter = viewportHeight / 2;
|
|
291
|
+
return {
|
|
292
|
+
rect: toDomRectLike(rect),
|
|
293
|
+
viewportProgress: computeViewportProgress(rect, viewportHeight),
|
|
294
|
+
distanceFromCenter: elementCenter - viewportCenter
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function statesEqual(a, b) {
|
|
298
|
+
return a.rect.top === b.rect.top && a.rect.left === b.rect.left && a.rect.width === b.rect.width && a.rect.height === b.rect.height && a.viewportProgress === b.viewportProgress && a.distanceFromCenter === b.distanceFromCenter;
|
|
299
|
+
}
|
|
300
|
+
function createElementTracker(el) {
|
|
301
|
+
if (typeof window === "undefined") {
|
|
302
|
+
const state2 = { rect: emptyRect, viewportProgress: 0, distanceFromCenter: 0 };
|
|
303
|
+
return {
|
|
304
|
+
getState: () => state2,
|
|
305
|
+
subscribe: (cb) => {
|
|
306
|
+
cb(state2);
|
|
307
|
+
return () => {
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
destroy: () => {
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
315
|
+
let state = computeState(el);
|
|
316
|
+
const removeRaf = rafLoop.add(() => {
|
|
317
|
+
const next = computeState(el);
|
|
318
|
+
if (!statesEqual(next, state)) {
|
|
319
|
+
state = next;
|
|
320
|
+
subscribers.forEach((cb) => cb(state));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
getState: () => state,
|
|
325
|
+
subscribe(cb) {
|
|
326
|
+
subscribers.add(cb);
|
|
327
|
+
cb(state);
|
|
328
|
+
return () => subscribers.delete(cb);
|
|
329
|
+
},
|
|
330
|
+
destroy() {
|
|
331
|
+
removeRaf();
|
|
332
|
+
subscribers.clear();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/utils/mapRange.ts
|
|
338
|
+
function mapRange(value, from, to, clampResult = false) {
|
|
339
|
+
const [inMin, inMax] = from;
|
|
340
|
+
const [outMin, outMax] = to;
|
|
341
|
+
const ratio = inMax === inMin ? 0 : (value - inMin) / (inMax - inMin);
|
|
342
|
+
const result = outMin + ratio * (outMax - outMin);
|
|
343
|
+
if (!clampResult) return result;
|
|
344
|
+
return clamp(result, Math.min(outMin, outMax), Math.max(outMin, outMax));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/utils/bindCSSVar.ts
|
|
348
|
+
function bindCSSVar(el, name, value) {
|
|
349
|
+
const varName = name.startsWith("--") ? name : `--${name}`;
|
|
350
|
+
el.style.setProperty(varName, String(value));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/easing.ts
|
|
354
|
+
var easings = {
|
|
355
|
+
linear: (t) => t,
|
|
356
|
+
easeInQuad: (t) => t * t,
|
|
357
|
+
easeOutQuad: (t) => t * (2 - t),
|
|
358
|
+
easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
359
|
+
easeInCubic: (t) => t * t * t,
|
|
360
|
+
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
|
|
361
|
+
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
|
362
|
+
easeInSine: (t) => 1 - Math.cos(t * Math.PI / 2),
|
|
363
|
+
easeOutSine: (t) => Math.sin(t * Math.PI / 2),
|
|
364
|
+
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2
|
|
365
|
+
};
|
|
366
|
+
export {
|
|
367
|
+
ObserverPool,
|
|
368
|
+
bindCSSVar,
|
|
369
|
+
clamp,
|
|
370
|
+
createElementTracker,
|
|
371
|
+
createScrollEngine,
|
|
372
|
+
createVisibilityEngine,
|
|
373
|
+
easings,
|
|
374
|
+
mapRange,
|
|
375
|
+
observerPool,
|
|
376
|
+
prefersReducedMotion,
|
|
377
|
+
rafLoop
|
|
378
|
+
};
|
|
379
|
+
//# sourceMappingURL=index.js.map
|