@linear_non/stellar-kit 3.0.5 → 3.0.6

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.
@@ -2,6 +2,7 @@ import { Manager } from "../classes/index.js"
2
2
  import { emitter, EVENTS } from "../events/index.js"
3
3
  import { Debug, qsa } from "../utils/index.js"
4
4
  import { kitStore } from "../kitStore.js"
5
+ import { ScrollTrigger } from "../libraries/gsap/index.js"
5
6
 
6
7
  /**
7
8
  * @typedef {Object} EngineOwner
@@ -23,9 +24,16 @@ import { kitStore } from "../kitStore.js"
23
24
  /**
24
25
  * @typedef {Object} EngineSetupConfig
25
26
  * @property {Array<any>} components - Array of component constructors/definitions.
26
- * @property {any} smooth - Smooth instance for scroll control.
27
+ * @property {Object} [lenis] - Lenis instance. If provided, uses Lenis mode.
28
+ * @property {HTMLElement} [wrapper] - Scroll wrapper element (required if lenis is provided).
29
+ * @property {any} [smooth] - Legacy: Smooth instance for internal scroll mode.
27
30
  */
28
31
 
32
+ /**
33
+ * Unified page engine that handles both internal scroll and Lenis modes.
34
+ * - Pass `lenis` + `wrapper` in setup() → Lenis mode (auto-configures ticker + ScrollTrigger)
35
+ * - Don't pass lenis → Internal scroll mode (uses APP_TICK from Raf.js)
36
+ */
29
37
  export default class PageEngine {
30
38
  /**
31
39
  * @param {{ owner: EngineOwner }} param0
@@ -52,11 +60,23 @@ export default class PageEngine {
52
60
  this.manager = null
53
61
 
54
62
  /**
55
- * Smooth instance passed in by project Page
63
+ * Smooth/Lenis instance
56
64
  * @type {any | null}
57
65
  */
58
66
  this.smooth = null
59
67
 
68
+ /**
69
+ * Lenis instance (if using Lenis mode)
70
+ * @type {any | null}
71
+ */
72
+ this.lenis = null
73
+
74
+ /**
75
+ * Wrapper element for ScrollTrigger proxy
76
+ * @type {HTMLElement | null}
77
+ */
78
+ this.wrapper = null
79
+
60
80
  /**
61
81
  * @type {EngineScrollState}
62
82
  */
@@ -81,16 +101,23 @@ export default class PageEngine {
81
101
  }
82
102
 
83
103
  /**
84
- * Called by Page.initCore().
85
- * Recreates Manager and binds a new Smooth instance.
104
+ * Setup the engine with components and optional scroll instance.
105
+ * Auto-detects Lenis mode if `lenis` is provided.
86
106
  *
87
107
  * @param {EngineSetupConfig} param0
88
108
  */
89
- setup({ components, smooth }) {
90
- Debug.log("ENGINE", "Setup", { components, smooth })
109
+ setup({ components, lenis, wrapper, smooth }) {
110
+ Debug.log("ENGINE", "Setup", { components, lenis, wrapper, smooth })
91
111
 
92
112
  this.components = components || []
93
- this.smooth = smooth || null
113
+
114
+ if (lenis && wrapper) {
115
+ // Lenis mode: auto-configure ticker and ScrollTrigger
116
+ this._setupLenis(lenis, wrapper)
117
+ } else if (smooth) {
118
+ // Legacy: smooth instance passed directly
119
+ this.smooth = smooth
120
+ }
94
121
 
95
122
  this.manager = new Manager()
96
123
 
@@ -108,6 +135,54 @@ export default class PageEngine {
108
135
  this.resize()
109
136
  }
110
137
 
138
+ /**
139
+ * Setup Lenis: register with global Raf and configure ScrollTrigger.
140
+ * Raf.js handles the lenis.raf() call on each tick.
141
+ * @param {Object} lenis - Lenis instance
142
+ * @param {HTMLElement} wrapper - Scroll wrapper element
143
+ * @private
144
+ */
145
+ _setupLenis(lenis, wrapper) {
146
+ Debug.log("ENGINE", "Setting up Lenis mode")
147
+
148
+ this.lenis = lenis
149
+ this.wrapper = wrapper
150
+ this.smooth = lenis
151
+
152
+ // Store scroller globally so components can access it
153
+ kitStore.scroller = wrapper
154
+
155
+ // Register Lenis with global Raf - it will call lenis.raf() on each tick
156
+ if (kitStore.raf) {
157
+ kitStore.raf.setLenis(lenis)
158
+ }
159
+
160
+ // Wire Lenis scroll events to ScrollTrigger
161
+ this.lenis.on("scroll", ScrollTrigger.update)
162
+
163
+ // Setup ScrollTrigger scroller proxy
164
+ ScrollTrigger.scrollerProxy(wrapper, {
165
+ scrollTop: (value) => {
166
+ if (value !== undefined) {
167
+ this.lenis.scrollTo(value, { immediate: true })
168
+ }
169
+ return this.lenis.scroll
170
+ },
171
+ getBoundingClientRect: () => ({
172
+ top: 0,
173
+ left: 0,
174
+ width: window.innerWidth,
175
+ height: window.innerHeight,
176
+ }),
177
+ pinType: wrapper.style.transform ? "transform" : "fixed",
178
+ })
179
+
180
+ // Set default scroller for all ScrollTrigger instances
181
+ ScrollTrigger.defaults({ scroller: wrapper })
182
+
183
+ Debug.log("ENGINE", "Lenis setup complete", { wrapper, scrollerSet: true })
184
+ }
185
+
111
186
  /**
112
187
  * Adds `.loaded` class to images when loaded.
113
188
  * Looks for `.lazy` images.
@@ -134,42 +209,22 @@ export default class PageEngine {
134
209
 
135
210
  /**
136
211
  * Receives data from global Raf (APP_TICK).
137
- * In internal mode, uses {current, diff} from Scroll.js.
138
- * In external mode (Lenis), calls smooth.raf() and derives scroll.
212
+ * Raf already handles Lenis.raf() call and provides correct scroll values.
139
213
  *
140
214
  * @param {{ current: number, diff: number, mouse: EngineMouseState }} param0
141
215
  */
142
216
  tick = ({ current, diff, mouse }) => {
143
- const { isResizing, isLocked, useExternalScroll } = kitStore.flags
217
+ const { isResizing, isLocked } = kitStore.flags
144
218
 
145
219
  if (isResizing || isLocked) return
146
220
  if (!this.manager) return
147
221
 
148
- let nextScroll = current
149
- let delta = diff
150
-
151
- if (useExternalScroll && this.smooth?.raf) {
152
- const t = performance.now()
153
- this.smooth.raf(t)
154
-
155
- if (typeof this.smooth.scroll === "number") {
156
- nextScroll = this.smooth.scroll
157
- } else if (typeof this.smooth.targetScroll === "number") {
158
- nextScroll = this.smooth.targetScroll
159
- } else {
160
- nextScroll = window.scrollY || window.pageYOffset || 0
161
- }
162
-
163
- delta = nextScroll - this.scroll.current
164
- }
222
+ this.scroll.current = current
223
+ this.scroll.direction = diff > 0 ? "down" : "up"
165
224
 
166
- this.scroll.current = nextScroll
167
- this.scroll.direction = delta > 0 ? "down" : "up"
168
-
169
225
  this.mouse.x = mouse.x
170
226
  this.mouse.y = mouse.y
171
-
172
- // Debug.log("ENGINE", "Tick scroll:", this.scroll)
227
+
173
228
  this.manager.tick?.({
174
229
  mouse: this.mouse,
175
230
  scroll: this.scroll,
@@ -178,15 +233,15 @@ export default class PageEngine {
178
233
 
179
234
  /**
180
235
  * Called during APP_RESIZE.
181
- * Updates Smooth then calls Manager.resize().
236
+ * Updates Smooth/Lenis then calls Manager.resize().
182
237
  */
183
238
  resize = () => {
184
239
  Debug.log("ENGINE", "Resize")
185
240
 
241
+ // Lenis
242
+ this.lenis?.resize?.()
186
243
  // Internal Smooth
187
244
  this.smooth?.update?.()
188
- // Lenis
189
- this.smooth?.resize?.()
190
245
 
191
246
  this.manager?.resize?.()
192
247
  }
@@ -229,6 +284,25 @@ export default class PageEngine {
229
284
  this.destroy()
230
285
  }
231
286
 
287
+ /**
288
+ * Cleanup Lenis if in Lenis mode.
289
+ * @private
290
+ */
291
+ _destroyLenis() {
292
+ // Unregister Lenis from global Raf
293
+ if (kitStore.raf) {
294
+ kitStore.raf.setLenis(null)
295
+ }
296
+
297
+ if (this.lenis) {
298
+ this.lenis.destroy()
299
+ this.lenis = null
300
+ }
301
+
302
+ this.wrapper = null
303
+ kitStore.scroller = null
304
+ }
305
+
232
306
  /**
233
307
  * Cleanup Smooth + Manager.
234
308
  * Called by Page.onLeaveCompleted().
@@ -237,9 +311,13 @@ export default class PageEngine {
237
311
  this.manager?.destroy?.()
238
312
  this.manager = null
239
313
 
240
- if (this.smooth) {
241
- this.smooth.destroy?.()
242
- this.smooth = null
314
+ // Cleanup Lenis if in Lenis mode
315
+ this._destroyLenis()
316
+
317
+ // Cleanup internal smooth
318
+ if (this.smooth?.destroy) {
319
+ this.smooth.destroy()
243
320
  }
321
+ this.smooth = null
244
322
  }
245
- }
323
+ }
package/events/Raf.js CHANGED
@@ -3,8 +3,54 @@ import emitter, { EVENTS } from "./Emitter"
3
3
  import { Debug, lerp } from "../utils"
4
4
  import { gsap, ScrollTrigger } from "../libraries/gsap"
5
5
 
6
+ /**
7
+ * @typedef {Object} RafScrollState
8
+ * @property {number} target - Target scroll position (used in internal scroll mode).
9
+ * @property {number} current - Current interpolated scroll position.
10
+ * @property {number} rounded - Rounded scroll position for rendering.
11
+ * @property {number} ease - Lerp easing factor for smooth interpolation.
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} RafMouseState
16
+ * @property {number} x - Current mouse X position.
17
+ * @property {number} y - Current mouse Y position.
18
+ * @property {EventTarget|null} target - Current mouse event target element.
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} RafTickPayload
23
+ * @property {number} target - Target scroll position.
24
+ * @property {number} current - Current scroll position.
25
+ * @property {RafMouseState} mouse - Current mouse state.
26
+ * @property {number} diff - Scroll delta/difference.
27
+ */
28
+
29
+ /**
30
+ * Global RAF (Request Animation Frame) manager.
31
+ *
32
+ * Handles the main animation loop and scroll state for the application.
33
+ * Supports two scroll modes:
34
+ *
35
+ * **Internal Scroll Mode** (`useExternalScroll: false`):
36
+ * - Receives scroll delta from VirtualScroll via APP_SCROLL events
37
+ * - Interpolates scroll position using lerp for smooth scrolling
38
+ * - Sets up ScrollTrigger scroller proxy on pageContent
39
+ *
40
+ * **Lenis Mode** (`useExternalScroll: true`):
41
+ * - Lenis instance is registered via `setLenis()`
42
+ * - Calls `lenis.raf()` on each tick
43
+ * - Reads scroll position directly from Lenis
44
+ * - PageEngine handles ScrollTrigger proxy setup
45
+ *
46
+ * Emits `APP_TICK` event on each frame with scroll and mouse data.
47
+ */
6
48
  export default class Raf {
7
49
  constructor() {
50
+ /**
51
+ * Scroll state for internal scroll mode.
52
+ * @type {RafScrollState}
53
+ */
8
54
  this.scroll = {
9
55
  target: 0,
10
56
  current: 0,
@@ -12,14 +58,28 @@ export default class Raf {
12
58
  ease: 0.115,
13
59
  }
14
60
 
61
+ /**
62
+ * Current mouse position state.
63
+ * @type {RafMouseState}
64
+ */
15
65
  this.mouse = {
16
66
  x: 0,
17
67
  y: 0,
18
68
  target: null,
19
69
  }
20
70
 
71
+ /**
72
+ * Scroll delta between frames.
73
+ * @type {number}
74
+ */
21
75
  this.diff = 0
22
76
 
77
+ /**
78
+ * Lenis instance when using external scroll mode.
79
+ * @type {Object|null}
80
+ */
81
+ this.lenis = null
82
+
23
83
  this.tick = this.tick.bind(this)
24
84
  this.onScroll = this.onScroll.bind(this)
25
85
  this.onMouseMove = this.onMouseMove.bind(this)
@@ -29,10 +89,29 @@ export default class Raf {
29
89
  this.onPageShow = this.resume.bind(this)
30
90
  }
31
91
 
92
+ /**
93
+ * Register Lenis instance for external scroll mode.
94
+ * When set, tick() will call lenis.raf() and read scroll position from Lenis.
95
+ * @param {Object|null} lenis - Lenis instance, or null to unregister.
96
+ */
97
+ setLenis(lenis) {
98
+ this.lenis = lenis
99
+ if (lenis) {
100
+ gsap.ticker.lagSmoothing(0)
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Initialize the RAF system.
106
+ * Sets up ScrollTrigger scroller proxy for internal scroll mode.
107
+ */
32
108
  init() {
33
109
  this.setScrollTrigger()
34
110
  }
35
111
 
112
+ /**
113
+ * Activate all event listeners and start the animation loop.
114
+ */
36
115
  on() {
37
116
  gsap.ticker.add(this.tick)
38
117
 
@@ -44,6 +123,9 @@ export default class Raf {
44
123
  window.addEventListener("pageshow", this.onPageShow)
45
124
  }
46
125
 
126
+ /**
127
+ * Deactivate all event listeners and stop the animation loop.
128
+ */
47
129
  off() {
48
130
  gsap.ticker.remove(this.tick)
49
131
 
@@ -55,10 +137,18 @@ export default class Raf {
55
137
  window.removeEventListener("pageshow", this.onPageShow)
56
138
  }
57
139
 
140
+ /**
141
+ * Cleanup and destroy the RAF system.
142
+ */
58
143
  destroy() {
59
144
  this.off()
60
145
  }
61
146
 
147
+ /**
148
+ * Handle smooth resize events.
149
+ * Clamps scroll values to valid range and updates ScrollTrigger.
150
+ * @private
151
+ */
62
152
  onAppResize() {
63
153
  const { fh } = kitStore.sizes
64
154
 
@@ -69,17 +159,39 @@ export default class Raf {
69
159
  ScrollTrigger.update()
70
160
  }
71
161
 
72
- tick() {
162
+ /**
163
+ * Main animation loop tick.
164
+ * Called on each frame via GSAP ticker.
165
+ *
166
+ * - In Lenis mode: calls lenis.raf() and reads scroll from Lenis
167
+ * - In internal mode: interpolates scroll using lerp
168
+ *
169
+ * Emits APP_TICK event with current scroll and mouse state.
170
+ *
171
+ * @param {number} time - Current time from GSAP ticker (in seconds).
172
+ */
173
+ tick(time) {
73
174
  if (kitStore.flags.isResizing) return
74
175
 
75
- const { target, current, ease } = this.scroll
176
+ // Lenis mode: update Lenis first, then read scroll from it
177
+ if (this.lenis) {
178
+ this.lenis.raf(time * 1000)
179
+
180
+ const lenisScroll = this.lenis.scroll ?? 0
181
+ this.diff = lenisScroll - this.scroll.current
182
+ this.scroll.current = lenisScroll
183
+ this.scroll.rounded = lenisScroll
184
+ } else {
185
+ // Internal scroll mode
186
+ const { target, current, ease } = this.scroll
76
187
 
77
- this.scroll.current = lerp(current, target, ease)
78
- this.scroll.rounded = Math.round(this.scroll.current * 100) / 100
79
- this.diff = (target - this.scroll.current) * 0.005
188
+ this.scroll.current = lerp(current, target, ease)
189
+ this.scroll.rounded = Math.round(this.scroll.current * 100) / 100
190
+ this.diff = (target - this.scroll.current) * 0.005
191
+ }
80
192
 
81
193
  emitter.emit(EVENTS.APP_TICK, {
82
- target,
194
+ target: this.lenis ? this.scroll.current : this.scroll.target,
83
195
  current: this.getScroll(),
84
196
  mouse: this.mouse,
85
197
  diff: this.diff,
@@ -88,29 +200,61 @@ export default class Raf {
88
200
  ScrollTrigger.update()
89
201
  }
90
202
 
203
+ /**
204
+ * Get the current scroll position.
205
+ * - In Lenis mode: returns the rounded Lenis scroll value.
206
+ * - In internal smooth mode: returns the interpolated scroll value.
207
+ * - In native mode: returns the container's scrollTop.
208
+ * @returns {number} Current scroll position.
209
+ */
91
210
  getScroll() {
211
+ // Lenis mode: return Lenis scroll position
212
+ if (this.lenis) {
213
+ return this.scroll.rounded
214
+ }
215
+
216
+ // Internal mode
92
217
  const { pageContent, flags } = kitStore
93
218
  const container = pageContent ? pageContent.parentNode : document.body
94
219
  return flags.isSmooth ? this.scroll.rounded : container.scrollTop
95
220
  }
96
221
 
222
+ /**
223
+ * Clamp scroll target to valid range (0 to full height).
224
+ * @private
225
+ */
97
226
  clamp() {
98
227
  const { fh } = kitStore.sizes
99
228
  this.scroll.target = Math.min(Math.max(this.scroll.target, 0), fh)
100
229
  }
101
230
 
231
+ /**
232
+ * Handle scroll events from VirtualScroll (internal scroll mode).
233
+ * Adds delta to target and clamps to valid range.
234
+ * @param {{ y: number }} param0 - Scroll delta.
235
+ * @private
236
+ */
102
237
  onScroll({ y }) {
103
238
  if (kitStore.flags.isLocked) return
104
239
  this.scroll.target += y
105
240
  this.clamp()
106
241
  }
107
242
 
243
+ /**
244
+ * Handle mouse move events.
245
+ * @param {{ x: number, y: number, target: EventTarget }} param0 - Mouse position and target.
246
+ * @private
247
+ */
108
248
  onMouseMove({ x, y, target }) {
109
249
  this.mouse.x = x
110
250
  this.mouse.y = y
111
251
  this.mouse.target = target
112
252
  }
113
253
 
254
+ /**
255
+ * Smoothly scroll to a target offset.
256
+ * @param {number} offset - Target scroll position in pixels.
257
+ */
114
258
  scrollTo(offset) {
115
259
  if (kitStore.flags.isSmooth) {
116
260
  gsap.to(this.scroll, {
@@ -123,6 +267,10 @@ export default class Raf {
123
267
  }
124
268
  }
125
269
 
270
+ /**
271
+ * Immediately set scroll position (no animation).
272
+ * @param {number} offset - Target scroll position in pixels.
273
+ */
126
274
  setScroll(offset) {
127
275
  if (kitStore.flags.isSmooth) {
128
276
  gsap.set(this.scroll, {
@@ -135,10 +283,25 @@ export default class Raf {
135
283
  }
136
284
  }
137
285
 
286
+ /**
287
+ * Configure ScrollTrigger for internal scroll mode.
288
+ * Sets up scroller proxy and defaults.
289
+ * Skipped when useExternalScroll is true (PageEngine handles it).
290
+ * @private
291
+ */
138
292
  setScrollTrigger() {
293
+ // Skip if useExternalScroll flag is set - PageEngine will handle ScrollTrigger setup
294
+ if (kitStore.flags.useExternalScroll) {
295
+ Debug.log("RAF", "Skipping ScrollTrigger setup (external scroll mode)")
296
+ return
297
+ }
298
+
139
299
  const { pageContent } = kitStore
140
300
  const scroller = pageContent || document.body
141
301
 
302
+ // Store scroller globally so components can access it
303
+ kitStore.scroller = scroller
304
+
142
305
  Debug.log(
143
306
  "RAF",
144
307
  "Setting ScrollTrigger scroller to:",
@@ -159,10 +322,17 @@ export default class Raf {
159
322
  })
160
323
  }
161
324
 
325
+ /**
326
+ * Pause the animation loop (e.g., when page is hidden).
327
+ */
162
328
  stop() {
163
329
  gsap.ticker.remove(this.tick)
164
330
  }
165
331
 
332
+ /**
333
+ * Resume the animation loop (e.g., when page becomes visible).
334
+ * Syncs current scroll to target to prevent jumps.
335
+ */
166
336
  resume() {
167
337
  this.scroll.current = this.scroll.target
168
338
  this.scroll.rounded = this.scroll.target
package/kitStore.js CHANGED
@@ -42,4 +42,10 @@ export const kitStore = {
42
42
  flags,
43
43
  pageContent: null,
44
44
  assets,
45
+ /**
46
+ * Scroll scroller element (wrapper for Lenis, pageContent for internal scroll).
47
+ * Used by ScrollTrigger components to get the correct scroller.
48
+ * @type {HTMLElement | null}
49
+ */
50
+ scroller: null,
45
51
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@linear_non/stellar-kit",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "Stellar frontend core for Non-Linear Studio projects.",
5
5
  "type": "module",
6
6
  "main": "/core/index.js",
7
7
  "sass": "./styles/index.scss",
8
8
  "exports": {
9
9
  ".": "./kitStore.js",
10
+ "./kitStore": "./kitStore.js",
10
11
  "./utils": "./utils/index.js",
11
12
  "./core": "./core/index.js",
12
13
  "./classes": "./classes/index.js",
@@ -1,5 +1,6 @@
1
1
  import { Emitter } from "../events"
2
2
  import { ScrollTrigger } from "../libraries/gsap"
3
+ import { kitStore } from "../kitStore"
3
4
 
4
5
  const DEFAULTS = {
5
6
  trigger: null,
@@ -33,6 +34,7 @@ export default class Observer extends Emitter {
33
34
  scrub,
34
35
  once,
35
36
  markers,
37
+ scroller: kitStore.scroller || undefined,
36
38
  onEnter: forward("enter"),
37
39
  onLeave: forward("leave"),
38
40
  onEnterBack: forward("enterBack"),
package/styles/helpers/_ DELETED
File without changes