@brandocms/jupiter 5.0.0-beta.1 → 5.0.0-beta.10
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/package.json +7 -9
- package/src/modules/Cookies/index.js +190 -8
- package/src/modules/DoubleHeader/index.js +4 -4
- package/src/modules/FixedHeader/index.js +4 -4
- package/src/modules/HeroSlider/index.js +22 -12
- package/src/modules/Lazyload/index.js +67 -8
- package/src/modules/Looper/index.js +192 -192
- package/src/modules/StickyHeader/index.js +4 -4
- package/src/utils/motion-helpers.js +2 -0
- package/types/index.d.ts +1 -1
- package/types/modules/Cookies/index.d.ts +22 -0
- package/types/modules/DoubleHeader/index.d.ts +1 -0
- package/types/modules/Dropdown/index.d.ts +1 -1
- package/types/modules/FixedHeader/index.d.ts +1 -0
- package/types/modules/HeroSlider/index.d.ts +10 -0
- package/types/modules/Lazyload/index.d.ts +7 -0
- package/types/modules/Lightbox/index.d.ts +3 -2
- package/types/modules/Looper/index.d.ts +10 -123
- package/types/modules/Marquee/index.d.ts +2 -0
- package/types/modules/Moonwalk/index.d.ts +33 -3
- package/types/modules/Popover/index.d.ts +1 -1
- package/types/modules/StickyHeader/index.d.ts +1 -0
- package/types/modules/Toggler/index.d.ts +16 -0
- package/types/utils/motion-helpers.d.ts +91 -0
|
@@ -77,9 +77,7 @@ function horizontalLoop(app, items, config) {
|
|
|
77
77
|
let originalItemsWidth = 0 // Width of ONLY original items (for wrapping)
|
|
78
78
|
let pixelsPerSecond = (config.speed || 1) * 100
|
|
79
79
|
let animation = null
|
|
80
|
-
let position = motionValue(0) // Source of truth for position
|
|
81
|
-
let boundedPos = motionValue(0) // Bounded position (0 to originalItemsWidth)
|
|
82
|
-
let lastBoundedValue = 0 // Track last value to detect wraps
|
|
80
|
+
let position = motionValue(0) // Source of truth for position (raw, unbounded)
|
|
83
81
|
let originalItemCount = 0 // Track count of ORIGINAL items (before clones)
|
|
84
82
|
let maxScrollPosition = 0 // For non-looping: max scroll where last item is at right edge
|
|
85
83
|
|
|
@@ -96,6 +94,7 @@ function horizontalLoop(app, items, config) {
|
|
|
96
94
|
|
|
97
95
|
// Drag state and cleanup handlers
|
|
98
96
|
let dragState = {}
|
|
97
|
+
let isDragging = false // Track if user is actively dragging (for wrap detection guard)
|
|
99
98
|
let speedRampAnimation = null // Track speed ramp animation
|
|
100
99
|
let inertiaAnimation = null // Track inertia animation
|
|
101
100
|
let snapAnimation = null // Track snap animation
|
|
@@ -103,6 +102,10 @@ function horizontalLoop(app, items, config) {
|
|
|
103
102
|
let positionUnsubscribe = null // Track position listener for cleanup
|
|
104
103
|
let renderUnsubscribe = null // Track frame.render loop for cleanup
|
|
105
104
|
|
|
105
|
+
// Scroll direction tracking for wrap logic
|
|
106
|
+
let scrollDirection = 0 // -1 = backward, 0 = neutral, 1 = forward
|
|
107
|
+
let lastPositionForDirection = 0
|
|
108
|
+
|
|
106
109
|
// Display elements for index/count
|
|
107
110
|
let indexElements = []
|
|
108
111
|
let countElements = []
|
|
@@ -163,24 +166,28 @@ function horizontalLoop(app, items, config) {
|
|
|
163
166
|
// This prevents items from visibly moving to the back before they're off-screen
|
|
164
167
|
const minRequiredWidth = containerWidth * 2.5 + maxItemWidth
|
|
165
168
|
|
|
166
|
-
// Only replicate if needed
|
|
167
|
-
if (totalWidth >= minRequiredWidth) {
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
|
|
171
169
|
// Store original count to prevent exponential growth
|
|
172
170
|
const originalItemCount = items.length
|
|
173
171
|
const maxReplications = 10
|
|
174
172
|
let count = 0
|
|
175
173
|
let previousTotalWidth = totalWidth
|
|
176
174
|
|
|
177
|
-
|
|
175
|
+
// Always create at least TWO sets of clones - needed for starting at first clone position
|
|
176
|
+
// Then continue until we have enough width for seamless looping
|
|
177
|
+
while ((count < 2 || totalWidth < minRequiredWidth) && count < maxReplications) {
|
|
178
178
|
// Clone ONLY original items
|
|
179
179
|
for (let i = 0; i < originalItemCount; i++) {
|
|
180
180
|
const clone = items[i].cloneNode(true)
|
|
181
181
|
clone.setAttribute('data-looper-clone', 'true')
|
|
182
182
|
container.appendChild(clone)
|
|
183
183
|
items.push(clone)
|
|
184
|
+
|
|
185
|
+
// Force-load cloned lazyload elements without revealing them
|
|
186
|
+
// Sources are swapped immediately (no flash on wrap), but data-ll-loaded
|
|
187
|
+
// is not set — the reveal happens after the wrapper fade-in animation
|
|
188
|
+
if (app?.lazyload?.forceLoad) {
|
|
189
|
+
app.lazyload.forceLoad(clone, { reveal: false })
|
|
190
|
+
}
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
// Force layout recalculation
|
|
@@ -332,83 +339,48 @@ function horizontalLoop(app, items, config) {
|
|
|
332
339
|
|
|
333
340
|
/**
|
|
334
341
|
* Check item positions and wrap when needed
|
|
335
|
-
* Container
|
|
336
|
-
*
|
|
337
|
-
* @param {number} pos - Current position value (can grow infinitely)
|
|
342
|
+
* Container uses RAW position - items wrap individually when far off-screen
|
|
343
|
+
* @param {number} rawPos - Current raw position value (unbounded)
|
|
338
344
|
*/
|
|
339
|
-
function updateItemPositions(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (!shouldLoop) {
|
|
343
|
-
// Non-looping: we'll handle this with direct animation
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Calculate bounded position for checking item wrap points
|
|
348
|
-
// Use same wrapping formula as frame.render to handle negative positions (reversed mode)
|
|
349
|
-
const boundedPos = ((pos % originalItemsWidth) + originalItemsWidth) % originalItemsWidth
|
|
345
|
+
function updateItemPositions(rawPos) {
|
|
346
|
+
if (!shouldLoop) return
|
|
350
347
|
|
|
351
348
|
// Initialize wrap offsets cache if needed
|
|
352
349
|
if (itemWrapOffsets.length === 0) {
|
|
353
350
|
itemWrapOffsets = new Array(items.length).fill(0)
|
|
354
351
|
}
|
|
355
352
|
|
|
356
|
-
//
|
|
357
|
-
// This
|
|
358
|
-
|
|
359
|
-
// Skip clones - they stay in natural flow! (use cached value for performance)
|
|
360
|
-
if (isCloneCache[i]) {
|
|
361
|
-
return
|
|
362
|
-
}
|
|
353
|
+
// Items wrap by the full cycle distance (totalWidth) to maintain relative positions
|
|
354
|
+
// This keeps all items within viewing distance as position grows/shrinks
|
|
355
|
+
const cycleDistance = totalWidth
|
|
363
356
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (nearForwardWrap && itemWrapOffsets[i] === wrapOffset) {
|
|
384
|
-
// Container about to wrap (forward), reset items at END back to START
|
|
385
|
-
newOffset = 0
|
|
386
|
-
} else if (nearReverseWrap && itemWrapOffsets[i] === -wrapOffset) {
|
|
387
|
-
// Container about to wrap (reverse), reset items at START back to END
|
|
388
|
-
newOffset = 0
|
|
389
|
-
} else if (nearForwardWrap || nearReverseWrap) {
|
|
390
|
-
// In reset zone but item doesn't need reset → keep current offset
|
|
391
|
-
newOffset = itemWrapOffsets[i]
|
|
392
|
-
} else if (itemLeft < -(widths[i] + containerWidth * 0.5)) {
|
|
393
|
-
// Item exited LEFT edge
|
|
394
|
-
// Forward drag (low boundedPos): wrap to END
|
|
395
|
-
// Backward drag (high boundedPos): don't wrap, clones fill in from right
|
|
396
|
-
newOffset = boundedPos < originalItemsWidth / 2 ? wrapOffset : 0
|
|
397
|
-
} else if (itemLeft > containerWidth + containerWidth * 0.5) {
|
|
398
|
-
// Item exited RIGHT edge
|
|
399
|
-
// This shouldn't happen much, but handle it
|
|
400
|
-
newOffset = 0
|
|
401
|
-
} else {
|
|
402
|
-
// Keep current offset
|
|
403
|
-
newOffset = itemWrapOffsets[i]
|
|
357
|
+
// Wrap threshold: when an item is more than half a cycle from view, wrap it
|
|
358
|
+
const wrapThreshold = cycleDistance / 2
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < items.length; i++) {
|
|
361
|
+
// Calculate this item's effective position (DOM position + wrap offset)
|
|
362
|
+
const effectivePos = offsetLefts[i] + itemWrapOffsets[i]
|
|
363
|
+
|
|
364
|
+
// Distance from current view position
|
|
365
|
+
// Positive = item is ahead (to the right), Negative = item is behind (to the left)
|
|
366
|
+
const distanceFromView = effectivePos - rawPos
|
|
367
|
+
|
|
368
|
+
let newOffset = itemWrapOffsets[i]
|
|
369
|
+
|
|
370
|
+
if (distanceFromView < -wrapThreshold) {
|
|
371
|
+
// Item is too far left (behind), wrap it forward (to the right)
|
|
372
|
+
newOffset = itemWrapOffsets[i] + cycleDistance
|
|
373
|
+
} else if (distanceFromView > wrapThreshold + containerWidth) {
|
|
374
|
+
// Item is too far right (ahead), wrap it backward (to the left)
|
|
375
|
+
newOffset = itemWrapOffsets[i] - cycleDistance
|
|
404
376
|
}
|
|
405
377
|
|
|
406
|
-
//
|
|
378
|
+
// Only update DOM if offset changed
|
|
407
379
|
if (newOffset !== itemWrapOffsets[i]) {
|
|
408
|
-
|
|
380
|
+
items[i].style.transform = newOffset !== 0 ? `translateX(${newOffset}px)` : 'none'
|
|
409
381
|
itemWrapOffsets[i] = newOffset
|
|
410
382
|
}
|
|
411
|
-
}
|
|
383
|
+
}
|
|
412
384
|
}
|
|
413
385
|
|
|
414
386
|
/**
|
|
@@ -416,32 +388,58 @@ function horizontalLoop(app, items, config) {
|
|
|
416
388
|
* @param {boolean} deep - Whether to rebuild animation (on resize)
|
|
417
389
|
*/
|
|
418
390
|
function refresh(deep = false) {
|
|
419
|
-
// Save progress to preserve position
|
|
420
|
-
const progress = animation ? animation.time / animation.duration : 0
|
|
421
|
-
const currentPos = position.get()
|
|
422
|
-
|
|
423
391
|
// Pause animation if running
|
|
424
392
|
const wasPlaying = animation && animation.speed !== 0
|
|
425
393
|
if (animation) {
|
|
426
394
|
animation.pause()
|
|
427
395
|
}
|
|
428
396
|
|
|
397
|
+
if (deep && shouldLoop) {
|
|
398
|
+
// DEEP REFRESH: Reset everything for new dimensions
|
|
399
|
+
// Clear all item wrap offsets and transforms
|
|
400
|
+
for (let i = 0; i < items.length; i++) {
|
|
401
|
+
items[i].style.transform = 'none'
|
|
402
|
+
if (itemWrapOffsets[i] !== undefined) {
|
|
403
|
+
itemWrapOffsets[i] = 0
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
itemWrapOffsets = []
|
|
407
|
+
}
|
|
408
|
+
|
|
429
409
|
// Remeasure everything
|
|
430
410
|
populateWidths()
|
|
431
411
|
|
|
432
412
|
if (deep) {
|
|
433
413
|
// Check if we need to replicate more items
|
|
434
|
-
const
|
|
414
|
+
const currentContainerWidth = container.offsetWidth
|
|
435
415
|
const currentTotalWidth = getTotalWidthOfItems()
|
|
436
416
|
|
|
437
417
|
// Use same 2.5x buffer as replication logic
|
|
438
|
-
if (shouldLoop && currentTotalWidth <
|
|
418
|
+
if (shouldLoop && currentTotalWidth < currentContainerWidth * 2.5) {
|
|
439
419
|
replicateItemsIfNeeded()
|
|
420
|
+
// Re-cache clone status for any new items
|
|
421
|
+
isCloneCache = items.map((item, i) => i >= originalItemCount)
|
|
440
422
|
populateWidths()
|
|
441
423
|
}
|
|
442
424
|
|
|
443
425
|
populateSnapTimes()
|
|
444
426
|
|
|
427
|
+
// Reset position to start at first clone (like initial state)
|
|
428
|
+
if (shouldLoop && !config.centerSlide) {
|
|
429
|
+
position.set(originalItemsWidth)
|
|
430
|
+
lastPositionForDirection = originalItemsWidth
|
|
431
|
+
items[0].parentElement.style.transform = `translateX(${-originalItemsWidth}px)`
|
|
432
|
+
} else if (shouldLoop && config.centerSlide) {
|
|
433
|
+
// For center mode, go to middle slide
|
|
434
|
+
const middleIndex = Math.floor(originalItemCount / 2)
|
|
435
|
+
const targetTime = times[middleIndex]
|
|
436
|
+
const initialPos = targetTime * pixelsPerSecond
|
|
437
|
+
position.set(initialPos)
|
|
438
|
+
lastPositionForDirection = initialPos
|
|
439
|
+
items[0].parentElement.style.transform = `translateX(${-initialPos}px)`
|
|
440
|
+
curIndex = middleIndex
|
|
441
|
+
}
|
|
442
|
+
|
|
445
443
|
// Recreate animation with new measurements
|
|
446
444
|
if (shouldLoop && config.crawl) {
|
|
447
445
|
// Stop old animation
|
|
@@ -449,7 +447,7 @@ function horizontalLoop(app, items, config) {
|
|
|
449
447
|
animation.stop()
|
|
450
448
|
}
|
|
451
449
|
|
|
452
|
-
//
|
|
450
|
+
// Recreate loop animation with new measurements
|
|
453
451
|
animation = startLoopAnimation()
|
|
454
452
|
|
|
455
453
|
// Restore playback state
|
|
@@ -472,19 +470,20 @@ function horizontalLoop(app, items, config) {
|
|
|
472
470
|
})
|
|
473
471
|
|
|
474
472
|
if (wasPlaying) {
|
|
475
|
-
animation.time = progress * animation.duration
|
|
476
473
|
animation.play()
|
|
477
474
|
} else {
|
|
478
475
|
animation.pause()
|
|
479
476
|
}
|
|
480
477
|
}
|
|
478
|
+
|
|
479
|
+
// Update index display
|
|
480
|
+
updateIndexDisplay()
|
|
481
481
|
} else {
|
|
482
482
|
// Light refresh - just update measurements
|
|
483
483
|
populateSnapTimes()
|
|
484
|
+
// Update positions based on current scroll
|
|
485
|
+
updateItemPositions(position.get())
|
|
484
486
|
}
|
|
485
|
-
|
|
486
|
-
// Update positions based on current scroll
|
|
487
|
-
updateItemPositions(currentPos)
|
|
488
487
|
}
|
|
489
488
|
|
|
490
489
|
/**
|
|
@@ -499,6 +498,46 @@ function horizontalLoop(app, items, config) {
|
|
|
499
498
|
})
|
|
500
499
|
}
|
|
501
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Create and start the crawl animation loop
|
|
503
|
+
* Animates the position motionValue (frame.render loop applies to DOM)
|
|
504
|
+
*/
|
|
505
|
+
function startLoopAnimation() {
|
|
506
|
+
if (!shouldLoop || !config.crawl) return null
|
|
507
|
+
|
|
508
|
+
const duration = originalItemsWidth / pixelsPerSecond
|
|
509
|
+
const currentPos = position.get()
|
|
510
|
+
// Reversed: crawl backwards (right-to-left), Normal: crawl forward (left-to-right)
|
|
511
|
+
const target = config.reversed
|
|
512
|
+
? currentPos - originalItemsWidth
|
|
513
|
+
: currentPos + originalItemsWidth
|
|
514
|
+
|
|
515
|
+
// Animate the position motionValue
|
|
516
|
+
// frame.render loop will apply raw position to container and wrap items
|
|
517
|
+
animation = animate(position, target, {
|
|
518
|
+
duration,
|
|
519
|
+
repeat: Infinity,
|
|
520
|
+
ease: 'linear',
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
return animation
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Stop the frame.render loop and cleanup listeners
|
|
528
|
+
* Called from init() and destroy()
|
|
529
|
+
*/
|
|
530
|
+
function stopRenderLoop() {
|
|
531
|
+
if (renderUnsubscribe) {
|
|
532
|
+
// Unsubscribe from motionValue listener
|
|
533
|
+
if (renderUnsubscribe.positionUnsubscribe) {
|
|
534
|
+
renderUnsubscribe.positionUnsubscribe()
|
|
535
|
+
}
|
|
536
|
+
cancelFrame(renderUnsubscribe)
|
|
537
|
+
renderUnsubscribe = null
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
502
541
|
/**
|
|
503
542
|
* Initialize the loop animation
|
|
504
543
|
*/
|
|
@@ -519,89 +558,45 @@ function horizontalLoop(app, items, config) {
|
|
|
519
558
|
// Set initial container position
|
|
520
559
|
const containerElement = items[0].parentElement
|
|
521
560
|
containerElement.style.willChange = 'transform'
|
|
522
|
-
containerElement.style.transform = 'translateX(0px)'
|
|
523
561
|
|
|
524
|
-
//
|
|
525
|
-
//
|
|
562
|
+
// For looping (non-center mode): start viewing first CLONE, not originals
|
|
563
|
+
// This positions originals OFF-SCREEN LEFT so backward scroll reveals them smoothly
|
|
564
|
+
if (shouldLoop && !config.centerSlide) {
|
|
565
|
+
position.set(originalItemsWidth)
|
|
566
|
+
lastPositionForDirection = originalItemsWidth
|
|
567
|
+
containerElement.style.transform = `translateX(${-originalItemsWidth}px)`
|
|
568
|
+
} else {
|
|
569
|
+
containerElement.style.transform = 'translateX(0px)'
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Set up RAF loop to update container position and wrap items
|
|
573
|
+
// Uses RAW position (no modulo) for container transform
|
|
526
574
|
// This is Motion's optimized render loop - prevents layout thrashing
|
|
527
575
|
function startRenderLoop() {
|
|
528
576
|
if (renderUnsubscribe) return // Already running
|
|
529
577
|
|
|
530
578
|
const containerElement = items[0].parentElement
|
|
531
579
|
|
|
532
|
-
//
|
|
533
|
-
// This calculates the bounded position (0 to originalItemsWidth)
|
|
580
|
+
// Track scroll direction for wrap logic
|
|
534
581
|
const positionUnsubscribe = position.on('change', latest => {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
// Detect when boundedPos wraps (makes large jump) and reset all items
|
|
540
|
-
// This prevents stuck items during fast drags in either direction
|
|
541
|
-
const boundedPosUnsubscribe = boundedPos.on('change', latest => {
|
|
542
|
-
const delta = Math.abs(latest - lastBoundedValue)
|
|
543
|
-
|
|
544
|
-
// If boundedPos jumped by more than 40% of the width, it wrapped
|
|
545
|
-
// Using 40% instead of 50% to catch edge cases
|
|
546
|
-
const didWrap = delta > originalItemsWidth * 0.4
|
|
547
|
-
|
|
548
|
-
if (didWrap) {
|
|
549
|
-
const direction =
|
|
550
|
-
latest > lastBoundedValue ? 'backward (drag right)' : 'forward (drag left)'
|
|
551
|
-
|
|
552
|
-
// Count how many items have non-zero offset before reset
|
|
553
|
-
const itemsWithOffset = items.filter(
|
|
554
|
-
(item, i) => !isCloneCache[i] && itemWrapOffsets[i] !== 0
|
|
555
|
-
).length
|
|
556
|
-
|
|
557
|
-
// Reset ALL original items to 0 (both positive and negative offsets)
|
|
558
|
-
items.forEach((item, i) => {
|
|
559
|
-
if (!isCloneCache[i] && itemWrapOffsets[i] !== 0) {
|
|
560
|
-
item.style.transform = 'none'
|
|
561
|
-
itemWrapOffsets[i] = 0
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
// CRITICAL: Sync unbounded position with bounded position to prevent
|
|
566
|
-
// inertia calculation bugs when dragging RIGHT across boundaries
|
|
567
|
-
// BUT only do this when NOT animating snap or nav, otherwise it interferes
|
|
568
|
-
if (!snapAnimation && !navAnimation) {
|
|
569
|
-
position.set(latest)
|
|
570
|
-
} else {
|
|
571
|
-
}
|
|
582
|
+
const delta = latest - lastPositionForDirection
|
|
583
|
+
if (Math.abs(delta) > 1) {
|
|
584
|
+
scrollDirection = delta > 0 ? 1 : -1
|
|
572
585
|
}
|
|
573
|
-
|
|
574
|
-
lastBoundedValue = latest
|
|
586
|
+
lastPositionForDirection = latest
|
|
575
587
|
})
|
|
576
588
|
|
|
577
589
|
renderUnsubscribe = frame.render(() => {
|
|
578
|
-
//
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
// Apply bounded transform to container
|
|
582
|
-
containerElement.style.transform = `translateX(${-currentBoundedPos}px)`
|
|
590
|
+
// Use RAW position (no bounded) for container
|
|
591
|
+
const currentPos = position.get()
|
|
592
|
+
containerElement.style.transform = `translateX(${-currentPos}px)`
|
|
583
593
|
|
|
584
|
-
// Wrap items based on
|
|
585
|
-
updateItemPositions(
|
|
594
|
+
// Wrap items based on raw position
|
|
595
|
+
updateItemPositions(currentPos)
|
|
586
596
|
}, true) // true = keep alive
|
|
587
597
|
|
|
588
|
-
// Store unsubscribe
|
|
598
|
+
// Store unsubscribe function for cleanup
|
|
589
599
|
renderUnsubscribe.positionUnsubscribe = positionUnsubscribe
|
|
590
|
-
renderUnsubscribe.boundedPosUnsubscribe = boundedPosUnsubscribe
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function stopRenderLoop() {
|
|
594
|
-
if (renderUnsubscribe) {
|
|
595
|
-
// Unsubscribe from motionValue listeners
|
|
596
|
-
if (renderUnsubscribe.positionUnsubscribe) {
|
|
597
|
-
renderUnsubscribe.positionUnsubscribe()
|
|
598
|
-
}
|
|
599
|
-
if (renderUnsubscribe.boundedPosUnsubscribe) {
|
|
600
|
-
renderUnsubscribe.boundedPosUnsubscribe()
|
|
601
|
-
}
|
|
602
|
-
cancelFrame(renderUnsubscribe)
|
|
603
|
-
renderUnsubscribe = null
|
|
604
|
-
}
|
|
605
600
|
}
|
|
606
601
|
|
|
607
602
|
// Start the frame.render loop
|
|
@@ -615,29 +610,6 @@ function horizontalLoop(app, items, config) {
|
|
|
615
610
|
})
|
|
616
611
|
}
|
|
617
612
|
|
|
618
|
-
// Function to create and start the animation loop
|
|
619
|
-
// Animates the position motionValue (frame.render loop applies to DOM)
|
|
620
|
-
function startLoopAnimation() {
|
|
621
|
-
if (!shouldLoop || !config.crawl) return null
|
|
622
|
-
|
|
623
|
-
const duration = originalItemsWidth / pixelsPerSecond
|
|
624
|
-
const currentPos = position.get()
|
|
625
|
-
// Reversed: crawl backwards (right-to-left), Normal: crawl forward (left-to-right)
|
|
626
|
-
const target = config.reversed
|
|
627
|
-
? currentPos - originalItemsWidth
|
|
628
|
-
: currentPos + originalItemsWidth
|
|
629
|
-
|
|
630
|
-
// Animate the position motionValue
|
|
631
|
-
// frame.render loop will apply bounded position to DOM
|
|
632
|
-
animation = animate(position, target, {
|
|
633
|
-
duration,
|
|
634
|
-
repeat: Infinity,
|
|
635
|
-
ease: 'linear',
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
return animation
|
|
639
|
-
}
|
|
640
|
-
|
|
641
613
|
// Create animation by animating the position motionValue
|
|
642
614
|
if (shouldLoop && config.crawl) {
|
|
643
615
|
const duration = totalWidth / pixelsPerSecond
|
|
@@ -712,7 +684,7 @@ function horizontalLoop(app, items, config) {
|
|
|
712
684
|
// Update display in real-time as position changes
|
|
713
685
|
let lastDisplayedIndex = -1
|
|
714
686
|
const updateIndexOnChange = () => {
|
|
715
|
-
// Find closest slide to current
|
|
687
|
+
// Find closest slide to current position
|
|
716
688
|
const closest = closestIndex(false)
|
|
717
689
|
|
|
718
690
|
// Only update DOM if index changed (avoid thrashing)
|
|
@@ -725,12 +697,8 @@ function horizontalLoop(app, items, config) {
|
|
|
725
697
|
}
|
|
726
698
|
}
|
|
727
699
|
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
boundedPos.on('change', updateIndexOnChange)
|
|
731
|
-
} else {
|
|
732
|
-
position.on('change', updateIndexOnChange)
|
|
733
|
-
}
|
|
700
|
+
// Update index display whenever position changes (closestIndex normalizes internally)
|
|
701
|
+
position.on('change', updateIndexOnChange)
|
|
734
702
|
}
|
|
735
703
|
}
|
|
736
704
|
|
|
@@ -774,7 +742,7 @@ function horizontalLoop(app, items, config) {
|
|
|
774
742
|
* Replaces GSAP Draggable with optimized pointer events
|
|
775
743
|
*/
|
|
776
744
|
function setupDrag() {
|
|
777
|
-
|
|
745
|
+
// isDragging is now module-level so wrap detection can see it
|
|
778
746
|
let startX = 0
|
|
779
747
|
let startPosition = 0
|
|
780
748
|
let velocityTracker = [] // Track recent movements for velocity calculation
|
|
@@ -900,9 +868,9 @@ function horizontalLoop(app, items, config) {
|
|
|
900
868
|
const newPosition = startPosition + deltaX
|
|
901
869
|
|
|
902
870
|
// Update position motionValue
|
|
903
|
-
// frame.render loop
|
|
871
|
+
// frame.render loop applies raw position to container
|
|
904
872
|
if (shouldLoop) {
|
|
905
|
-
// For looping,
|
|
873
|
+
// For looping, position grows freely - items wrap as groups
|
|
906
874
|
position.set(newPosition)
|
|
907
875
|
} else {
|
|
908
876
|
// For non-looping, clamp position to maxScrollPosition (last item at right edge)
|
|
@@ -1136,7 +1104,7 @@ function horizontalLoop(app, items, config) {
|
|
|
1136
1104
|
snapAnimation = null
|
|
1137
1105
|
// Update display to reflect landed position
|
|
1138
1106
|
updateIndexDisplay()
|
|
1139
|
-
if (config.crawl
|
|
1107
|
+
if (config.crawl) {
|
|
1140
1108
|
resumeCrawl()
|
|
1141
1109
|
}
|
|
1142
1110
|
})
|
|
@@ -1165,7 +1133,8 @@ function horizontalLoop(app, items, config) {
|
|
|
1165
1133
|
const currentPos = position.get()
|
|
1166
1134
|
|
|
1167
1135
|
// Calculate position within current cycle (using originalItemsWidth)
|
|
1168
|
-
|
|
1136
|
+
// Use proper modulo for negative positions (dragging right/backward)
|
|
1137
|
+
const cyclePos = ((currentPos % originalItemsWidth) + originalItemsWidth) % originalItemsWidth
|
|
1169
1138
|
const remainingDist = originalItemsWidth - cyclePos
|
|
1170
1139
|
const remainingDuration = remainingDist / pixelsPerSecond
|
|
1171
1140
|
|
|
@@ -1490,13 +1459,18 @@ function horizontalLoop(app, items, config) {
|
|
|
1490
1459
|
closestIndex(true)
|
|
1491
1460
|
let nextIndex = curIndex + 1
|
|
1492
1461
|
|
|
1493
|
-
// Non-looping:
|
|
1462
|
+
// Non-looping: clamp at boundaries
|
|
1494
1463
|
if (!shouldLoop) {
|
|
1495
1464
|
const currentPos = position.get()
|
|
1496
1465
|
const atEnd = currentPos >= maxScrollPosition - 1
|
|
1497
1466
|
|
|
1498
1467
|
if (nextIndex >= originalItemCount || atEnd) {
|
|
1499
|
-
|
|
1468
|
+
// Autoplay resets to start, user navigation clamps
|
|
1469
|
+
if (options.autoplay) {
|
|
1470
|
+
nextIndex = 0
|
|
1471
|
+
} else {
|
|
1472
|
+
return // Clamp - do nothing at boundary
|
|
1473
|
+
}
|
|
1500
1474
|
}
|
|
1501
1475
|
}
|
|
1502
1476
|
|
|
@@ -1512,9 +1486,14 @@ function horizontalLoop(app, items, config) {
|
|
|
1512
1486
|
closestIndex(true)
|
|
1513
1487
|
let prevIndex = curIndex - 1
|
|
1514
1488
|
|
|
1515
|
-
// Non-looping:
|
|
1489
|
+
// Non-looping: clamp at boundaries
|
|
1516
1490
|
if (!shouldLoop && prevIndex < 0) {
|
|
1517
|
-
|
|
1491
|
+
// Autoplay resets to end, user navigation clamps
|
|
1492
|
+
if (options.autoplay) {
|
|
1493
|
+
prevIndex = originalItemCount - 1
|
|
1494
|
+
} else {
|
|
1495
|
+
return // Clamp - do nothing at boundary
|
|
1496
|
+
}
|
|
1518
1497
|
}
|
|
1519
1498
|
|
|
1520
1499
|
return toIndex(prevIndex, vars)
|
|
@@ -1717,8 +1696,29 @@ export default class Looper {
|
|
|
1717
1696
|
})
|
|
1718
1697
|
}
|
|
1719
1698
|
|
|
1720
|
-
//
|
|
1721
|
-
|
|
1699
|
+
// Reveal lazyload images: immediately reveal off-screen items (no visible transition),
|
|
1700
|
+
// defer reveal of viewport items until after wrapper fade-in for a nice per-image fade
|
|
1701
|
+
if (this.app?.lazyload && wrapper) {
|
|
1702
|
+
const wrapperRect = wrapper.getBoundingClientRect()
|
|
1703
|
+
const pictures = Dom.all(wrapper, '[data-ll-srcset]')
|
|
1704
|
+
const viewportPictures = []
|
|
1705
|
+
|
|
1706
|
+
pictures.forEach(picture => {
|
|
1707
|
+
const rect = picture.getBoundingClientRect()
|
|
1708
|
+
const inViewport = rect.right > wrapperRect.left && rect.left < wrapperRect.right
|
|
1709
|
+
if (inViewport) {
|
|
1710
|
+
viewportPictures.push(picture)
|
|
1711
|
+
} else {
|
|
1712
|
+
this.app.lazyload.revealPicture(picture)
|
|
1713
|
+
}
|
|
1714
|
+
})
|
|
1715
|
+
|
|
1716
|
+
// Fade in the WRAPPER (not the outer element!)
|
|
1717
|
+
animate(wrapper, { opacity: 1 }, { duration: 0.5, delay: 0.5, ease: 'easeOut' })
|
|
1718
|
+
.then(() => {
|
|
1719
|
+
viewportPictures.forEach(picture => this.app.lazyload.revealPicture(picture))
|
|
1720
|
+
})
|
|
1721
|
+
} else if (wrapper) {
|
|
1722
1722
|
animate(wrapper, { opacity: 1 }, { duration: 0.5, delay: 0.5, ease: 'easeOut' })
|
|
1723
1723
|
}
|
|
1724
1724
|
|
|
@@ -81,7 +81,7 @@ import { set } from '../../utils/motion-helpers'
|
|
|
81
81
|
const DEFAULT_EVENTS = {
|
|
82
82
|
onPin: (h) => {
|
|
83
83
|
animate(h.el, {
|
|
84
|
-
|
|
84
|
+
y: '0%'
|
|
85
85
|
}, {
|
|
86
86
|
duration: 0.35,
|
|
87
87
|
ease: 'easeOut'
|
|
@@ -91,7 +91,7 @@ const DEFAULT_EVENTS = {
|
|
|
91
91
|
onUnpin: (h) => {
|
|
92
92
|
h._hiding = true
|
|
93
93
|
animate(h.el, {
|
|
94
|
-
|
|
94
|
+
y: '-100%'
|
|
95
95
|
}, {
|
|
96
96
|
duration: 0.25,
|
|
97
97
|
ease: 'easeIn'
|
|
@@ -158,14 +158,14 @@ const DEFAULT_OPTIONS = {
|
|
|
158
158
|
canvas: window,
|
|
159
159
|
intersects: null,
|
|
160
160
|
beforeEnter: (h) => {
|
|
161
|
-
set(h.el, {
|
|
161
|
+
set(h.el, { y: '-100%' })
|
|
162
162
|
set(h.lis, { opacity: 0 })
|
|
163
163
|
},
|
|
164
164
|
|
|
165
165
|
enter: (h) => {
|
|
166
166
|
// Header slides down
|
|
167
167
|
animate(h.el, {
|
|
168
|
-
|
|
168
|
+
y: '0%'
|
|
169
169
|
}, {
|
|
170
170
|
duration: 1,
|
|
171
171
|
delay: h.opts.enterDelay,
|
|
@@ -33,6 +33,8 @@ export function set(target, values) {
|
|
|
33
33
|
transformProps.push(`translateX(${typeof value === 'number' ? value + 'px' : value})`)
|
|
34
34
|
} else if (key === 'y') {
|
|
35
35
|
transformProps.push(`translateY(${typeof value === 'number' ? value + 'px' : value})`)
|
|
36
|
+
} else if (key === 'yPercent') {
|
|
37
|
+
transformProps.push(`translateY(${value}%)`)
|
|
36
38
|
} else if (key === 'scale') {
|
|
37
39
|
transformProps.push(`scale(${value})`)
|
|
38
40
|
} else if (key === 'scaleX') {
|
package/types/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ import imagesAreLoaded from './utils/imagesAreLoaded';
|
|
|
3
3
|
import loadScript from './utils/loadScript';
|
|
4
4
|
import prefersReducedMotion from './utils/prefersReducedMotion';
|
|
5
5
|
import rafCallback from './utils/rafCallback';
|
|
6
|
-
export { Application, Breakpoints, Cookies, CoverOverlay, Dataloader, Dom, DoubleHeader,
|
|
6
|
+
export { Application, Breakpoints, Cookies, CoverOverlay, Dataloader, Dom, DoubleHeader, Dropdown, EqualHeightElements, EqualHeightImages, Events, FixedHeader, FooterReveal, Parallax, HeroSlider, HeroVideo, Lazyload, Lightbox, Links, Looper, Marquee, MobileMenu, Moonwalk, Popover, Popup, ScrollSpy, StackedBoxes, StickyHeader, Toggler, Typography, imageIsLoaded, imagesAreLoaded, loadScript, prefersReducedMotion, rafCallback, _defaultsDeep, animate, scroll, stagger, motionValue };
|