@linear_non/stellar-kit 3.0.5 → 3.0.7

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
@@ -22,10 +23,28 @@ import { kitStore } from "../kitStore.js"
22
23
 
23
24
  /**
24
25
  * @typedef {Object} EngineSetupConfig
25
- * @property {Array<any>} components - Array of component constructors/definitions.
26
- * @property {any} smooth - Smooth instance for scroll control.
26
+ * @property {Array<any>} [components] - Array of component constructors/definitions.
27
+ * @property {Object} [lenis] - Lenis instance for external scroll mode.
28
+ * @property {HTMLElement} [wrapper] - Scroll wrapper element (required with lenis).
29
+ * @property {Object} [smooth] - Smooth instance for internal scroll mode.
27
30
  */
28
31
 
32
+ /**
33
+ * Unified page engine that handles both internal scroll and Lenis modes.
34
+ *
35
+ * Usage:
36
+ * ```js
37
+ * // Lenis mode
38
+ * const wrapper = getid("main")
39
+ * const content = qs(".-page")
40
+ * const lenis = await createLenis(wrapper, content)
41
+ * engine.setup({ components, lenis, wrapper })
42
+ *
43
+ * // Internal mode
44
+ * const smooth = createSmooth(Smooth)
45
+ * engine.setup({ components, smooth })
46
+ * ```
47
+ */
29
48
  export default class PageEngine {
30
49
  /**
31
50
  * @param {{ owner: EngineOwner }} param0
@@ -41,22 +60,29 @@ export default class PageEngine {
41
60
  */
42
61
  this.components = []
43
62
 
44
- /**
45
- * @type {number}
46
- */
47
- this.componentsLength = 0
48
-
49
63
  /**
50
64
  * @type {Manager | null}
51
65
  */
52
66
  this.manager = null
53
67
 
54
68
  /**
55
- * Smooth instance passed in by project Page
69
+ * Smooth/Lenis instance
56
70
  * @type {any | null}
57
71
  */
58
72
  this.smooth = null
59
73
 
74
+ /**
75
+ * Lenis instance (if using Lenis mode)
76
+ * @type {any | null}
77
+ */
78
+ this.lenis = null
79
+
80
+ /**
81
+ * Wrapper element for ScrollTrigger proxy
82
+ * @type {HTMLElement | null}
83
+ */
84
+ this.wrapper = null
85
+
60
86
  /**
61
87
  * @type {EngineScrollState}
62
88
  */
@@ -81,16 +107,21 @@ export default class PageEngine {
81
107
  }
82
108
 
83
109
  /**
84
- * Called by Page.initCore().
85
- * Recreates Manager and binds a new Smooth instance.
110
+ * Setup the engine with components and scroll system.
86
111
  *
87
112
  * @param {EngineSetupConfig} param0
88
113
  */
89
- setup({ components, smooth }) {
90
- Debug.log("ENGINE", "Setup", { components, smooth })
114
+ setup({ components, lenis, wrapper, smooth }) {
115
+ Debug.log("ENGINE", "Setup", { components, lenis, wrapper, smooth })
91
116
 
92
117
  this.components = components || []
93
- this.smooth = smooth || null
118
+
119
+ // Setup scroll system
120
+ if (lenis && wrapper) {
121
+ this._setupLenis(lenis, wrapper)
122
+ } else if (smooth) {
123
+ this.smooth = smooth
124
+ }
94
125
 
95
126
  this.manager = new Manager()
96
127
 
@@ -108,6 +139,54 @@ export default class PageEngine {
108
139
  this.resize()
109
140
  }
110
141
 
142
+ /**
143
+ * Setup Lenis: register with global Raf and configure ScrollTrigger.
144
+ * Raf.js handles the lenis.raf() call on each tick.
145
+ * @param {Object} lenis - Lenis instance
146
+ * @param {HTMLElement} wrapper - Scroll wrapper element
147
+ * @private
148
+ */
149
+ _setupLenis(lenis, wrapper) {
150
+ Debug.log("ENGINE", "Setting up Lenis mode")
151
+
152
+ this.lenis = lenis
153
+ this.wrapper = wrapper
154
+ this.smooth = lenis
155
+
156
+ // Store scroller globally so components can access it
157
+ kitStore.scroller = wrapper
158
+
159
+ // Register Lenis with global Raf - it will call lenis.raf() on each tick
160
+ if (kitStore.raf) {
161
+ kitStore.raf.setLenis(lenis)
162
+ }
163
+
164
+ // Wire Lenis scroll events to ScrollTrigger
165
+ this.lenis.on("scroll", ScrollTrigger.update)
166
+
167
+ // Setup ScrollTrigger scroller proxy
168
+ ScrollTrigger.scrollerProxy(wrapper, {
169
+ scrollTop: (value) => {
170
+ if (value !== undefined) {
171
+ this.lenis.scrollTo(value, { immediate: true })
172
+ }
173
+ return this.lenis.scroll
174
+ },
175
+ getBoundingClientRect: () => ({
176
+ top: 0,
177
+ left: 0,
178
+ width: window.innerWidth,
179
+ height: window.innerHeight,
180
+ }),
181
+ pinType: wrapper.style.transform ? "transform" : "fixed",
182
+ })
183
+
184
+ // Set default scroller for all ScrollTrigger instances
185
+ ScrollTrigger.defaults({ scroller: wrapper })
186
+
187
+ Debug.log("ENGINE", "Lenis setup complete", { wrapper, scrollerSet: true })
188
+ }
189
+
111
190
  /**
112
191
  * Adds `.loaded` class to images when loaded.
113
192
  * Looks for `.lazy` images.
@@ -134,42 +213,22 @@ export default class PageEngine {
134
213
 
135
214
  /**
136
215
  * 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.
216
+ * Raf already handles Lenis.raf() call and provides correct scroll values.
139
217
  *
140
218
  * @param {{ current: number, diff: number, mouse: EngineMouseState }} param0
141
219
  */
142
220
  tick = ({ current, diff, mouse }) => {
143
- const { isResizing, isLocked, useExternalScroll } = kitStore.flags
221
+ const { isResizing, isLocked } = kitStore.flags
144
222
 
145
223
  if (isResizing || isLocked) return
146
224
  if (!this.manager) return
147
225
 
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)
226
+ this.scroll.current = current
227
+ this.scroll.direction = diff > 0 ? "down" : "up"
154
228
 
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
- }
165
-
166
- this.scroll.current = nextScroll
167
- this.scroll.direction = delta > 0 ? "down" : "up"
168
-
169
229
  this.mouse.x = mouse.x
170
230
  this.mouse.y = mouse.y
171
-
172
- // Debug.log("ENGINE", "Tick scroll:", this.scroll)
231
+
173
232
  this.manager.tick?.({
174
233
  mouse: this.mouse,
175
234
  scroll: this.scroll,
@@ -178,15 +237,15 @@ export default class PageEngine {
178
237
 
179
238
  /**
180
239
  * Called during APP_RESIZE.
181
- * Updates Smooth then calls Manager.resize().
240
+ * Updates Smooth/Lenis then calls Manager.resize().
182
241
  */
183
242
  resize = () => {
184
243
  Debug.log("ENGINE", "Resize")
185
244
 
245
+ // Lenis
246
+ this.lenis?.resize?.()
186
247
  // Internal Smooth
187
248
  this.smooth?.update?.()
188
- // Lenis
189
- this.smooth?.resize?.()
190
249
 
191
250
  this.manager?.resize?.()
192
251
  }
@@ -229,6 +288,25 @@ export default class PageEngine {
229
288
  this.destroy()
230
289
  }
231
290
 
291
+ /**
292
+ * Cleanup Lenis if in Lenis mode.
293
+ * @private
294
+ */
295
+ _destroyLenis() {
296
+ // Unregister Lenis from global Raf
297
+ if (kitStore.raf) {
298
+ kitStore.raf.setLenis(null)
299
+ }
300
+
301
+ if (this.lenis) {
302
+ this.lenis.destroy()
303
+ this.lenis = null
304
+ }
305
+
306
+ this.wrapper = null
307
+ kitStore.scroller = null
308
+ }
309
+
232
310
  /**
233
311
  * Cleanup Smooth + Manager.
234
312
  * Called by Page.onLeaveCompleted().
@@ -237,9 +315,13 @@ export default class PageEngine {
237
315
  this.manager?.destroy?.()
238
316
  this.manager = null
239
317
 
240
- if (this.smooth) {
241
- this.smooth.destroy?.()
242
- this.smooth = null
318
+ // Cleanup Lenis if in Lenis mode
319
+ this._destroyLenis()
320
+
321
+ // Cleanup internal smooth
322
+ if (this.smooth?.destroy) {
323
+ this.smooth.destroy()
243
324
  }
325
+ this.smooth = null
244
326
  }
245
- }
327
+ }
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.7",
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