@brandocms/jupiter 3.55.0 → 4.0.0-beta.2

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.
Files changed (76) hide show
  1. package/README.md +509 -54
  2. package/package.json +30 -18
  3. package/src/index.js +15 -10
  4. package/src/modules/Application/index.js +236 -158
  5. package/src/modules/Breakpoints/index.js +116 -36
  6. package/src/modules/Cookies/index.js +95 -64
  7. package/src/modules/CoverOverlay/index.js +21 -14
  8. package/src/modules/Dataloader/index.js +71 -24
  9. package/src/modules/Dataloader/url-sync.js +238 -0
  10. package/src/modules/Dom/index.js +24 -0
  11. package/src/modules/DoubleHeader/index.js +571 -0
  12. package/src/modules/Dropdown/index.js +108 -73
  13. package/src/modules/EqualHeightElements/index.js +8 -8
  14. package/src/modules/EqualHeightImages/index.js +15 -7
  15. package/src/modules/FixedHeader/index.js +116 -30
  16. package/src/modules/FooterReveal/index.js +5 -5
  17. package/src/modules/HeroSlider/index.js +231 -106
  18. package/src/modules/HeroVideo/index.js +72 -44
  19. package/src/modules/Lazyload/index.js +128 -80
  20. package/src/modules/Lightbox/index.js +101 -80
  21. package/src/modules/Links/index.js +77 -51
  22. package/src/modules/Looper/index.js +1737 -0
  23. package/src/modules/Marquee/index.js +106 -37
  24. package/src/modules/MobileMenu/index.js +105 -130
  25. package/src/modules/Moonwalk/index.js +479 -153
  26. package/src/modules/Parallax/index.js +280 -57
  27. package/src/modules/Popover/index.js +187 -17
  28. package/src/modules/Popup/index.js +172 -53
  29. package/src/modules/ScrollSpy/index.js +21 -0
  30. package/src/modules/StackedBoxes/index.js +8 -6
  31. package/src/modules/StickyHeader/index.js +394 -164
  32. package/src/modules/Toggler/index.js +207 -11
  33. package/src/modules/Typography/index.js +33 -20
  34. package/src/utils/motion-helpers.js +330 -0
  35. package/types/README.md +159 -0
  36. package/types/events/index.d.ts +20 -0
  37. package/types/index.d.ts +6 -0
  38. package/types/modules/Application/index.d.ts +168 -0
  39. package/types/modules/Breakpoints/index.d.ts +40 -0
  40. package/types/modules/Cookies/index.d.ts +81 -0
  41. package/types/modules/CoverOverlay/index.d.ts +6 -0
  42. package/types/modules/Dataloader/index.d.ts +38 -0
  43. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  44. package/types/modules/Dom/index.d.ts +47 -0
  45. package/types/modules/DoubleHeader/index.d.ts +63 -0
  46. package/types/modules/Dropdown/index.d.ts +15 -0
  47. package/types/modules/EqualHeightElements/index.d.ts +8 -0
  48. package/types/modules/EqualHeightImages/index.d.ts +11 -0
  49. package/types/modules/FeatureTests/index.d.ts +27 -0
  50. package/types/modules/FixedHeader/index.d.ts +219 -0
  51. package/types/modules/Fontloader/index.d.ts +5 -0
  52. package/types/modules/FooterReveal/index.d.ts +5 -0
  53. package/types/modules/HeroSlider/index.d.ts +28 -0
  54. package/types/modules/HeroVideo/index.d.ts +83 -0
  55. package/types/modules/Lazyload/index.d.ts +80 -0
  56. package/types/modules/Lightbox/index.d.ts +123 -0
  57. package/types/modules/Links/index.d.ts +55 -0
  58. package/types/modules/Looper/index.d.ts +127 -0
  59. package/types/modules/Marquee/index.d.ts +23 -0
  60. package/types/modules/MobileMenu/index.d.ts +63 -0
  61. package/types/modules/Moonwalk/index.d.ts +322 -0
  62. package/types/modules/Parallax/index.d.ts +71 -0
  63. package/types/modules/Popover/index.d.ts +29 -0
  64. package/types/modules/Popup/index.d.ts +76 -0
  65. package/types/modules/ScrollSpy/index.d.ts +29 -0
  66. package/types/modules/StackedBoxes/index.d.ts +9 -0
  67. package/types/modules/StickyHeader/index.d.ts +220 -0
  68. package/types/modules/Toggler/index.d.ts +48 -0
  69. package/types/modules/Typography/index.d.ts +77 -0
  70. package/types/utils/dispatchElementEvent.d.ts +1 -0
  71. package/types/utils/imageIsLoaded.d.ts +1 -0
  72. package/types/utils/imagesAreLoaded.d.ts +1 -0
  73. package/types/utils/loadScript.d.ts +2 -0
  74. package/types/utils/prefersReducedMotion.d.ts +4 -0
  75. package/types/utils/rafCallback.d.ts +2 -0
  76. package/types/utils/zoom.d.ts +4 -0
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Vendor imports
3
3
  */
4
- import { gsap } from 'gsap'
5
- import { CSSPlugin } from 'gsap/CSSPlugin'
4
+ import { animate, stagger } from 'motion'
6
5
  import _defaultsDeep from 'lodash.defaultsdeep'
7
6
 
8
7
  /**
@@ -12,10 +11,74 @@ import * as Events from '../../events'
12
11
  import prefersReducedMotion from '../../utils/prefersReducedMotion'
13
12
  import imageIsLoaded from '../../utils/imageIsLoaded'
14
13
  import imagesAreLoaded from '../../utils/imagesAreLoaded'
14
+ import { set, animateAutoAlpha, delayedCall, convertEasing } from '../../utils/motion-helpers'
15
15
  import Dom from '../Dom'
16
16
 
17
- gsap.registerPlugin(CSSPlugin)
17
+ /**
18
+ * Debug logging
19
+ */
20
+ const DEBUG = false
21
+
22
+ function logMoonwalk(category, message, data = {}) {
23
+ if (DEBUG) {
24
+ console.log(`[Moonwalk:${category}]`, message, data)
25
+ }
26
+ }
27
+
28
+ function logComputedStyle(element, props = ['opacity', 'transform']) {
29
+ if (DEBUG && element) {
30
+ const computed = window.getComputedStyle(element)
31
+ const values = {}
32
+ props.forEach((prop) => (values[prop] = computed[prop]))
33
+ console.log('[Moonwalk:ComputedStyle]', element, values)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @typedef {Object} MoonwalkTransition
39
+ * @property {Object} from - Starting properties for the transition
40
+ * @property {Object} to - Ending properties for the transition
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} MoonwalkWalk
45
+ * @property {number} [startDelay=0] - Delay before the animation starts
46
+ * @property {number} [interval=0.15] - Time between animations in a sequence
47
+ * @property {number} [duration=0.65] - Duration of the animation
48
+ * @property {boolean|Object} [alphaTween=false] - Whether to add a separate opacity tween
49
+ * @property {MoonwalkTransition} transition - The transition configuration
50
+ * @property {string} [sectionTargets] - CSS selector for targeting elements in named sections
51
+ */
52
+
53
+ /**
54
+ * @typedef {Object} MoonwalkRun
55
+ * @property {number} [threshold=0] - IntersectionObserver threshold
56
+ * @property {Function} callback - Function called when element enters viewport
57
+ * @property {Function} [onExit] - Function called when element exits viewport
58
+ * @property {boolean} [repeated=false] - Whether the run should repeat
59
+ * @property {string} [rootMargin] - IntersectionObserver rootMargin
60
+ * @property {Function} [initialize] - Function called during initialization
61
+ * @property {Function} [onReady] - Function called when APPLICATION_REVEALED fires, before viewport observers start
62
+ */
18
63
 
64
+ /**
65
+ * @typedef {Object} MoonwalkOptions
66
+ * @property {string|Function} [on=Events.APPLICATION_REVEALED] - Event to trigger animations
67
+ * @property {number} [initialDelay=0.1] - Delay before starting animations
68
+ * @property {boolean} [clearLazyload=false] - Clear data-ll-srcset attributes
69
+ * @property {boolean} [clearNestedSections=true] - Remove nested data-moonwalk-section attributes
70
+ * @property {boolean} [clearNestedWalks=true] - Remove nested data-moonwalk attributes
71
+ * @property {boolean} [clearMoonwalkOnAnchors=true] - Disable animations when page loaded via anchor
72
+ * @property {boolean} [warnRunWithSection=true] - Warn when run and section on same element
73
+ * @property {string} [rootMargin='-10% 0%'] - Default IntersectionObserver rootMargin
74
+ * @property {number} [threshold=0] - Default IntersectionObserver threshold
75
+ * @property {boolean} [uniqueIds=false] - Generate unique IDs for moonwalk elements
76
+ * @property {boolean} [addIndexes=false] - Add index attributes to elements
77
+ * @property {Object.<string, MoonwalkRun>} [runs={}] - Run configurations
78
+ * @property {Object.<string, MoonwalkWalk>} walks - Walk configurations
79
+ */
80
+
81
+ /** @type {MoonwalkOptions} */
19
82
  const DEFAULT_OPTIONS = {
20
83
  /**
21
84
  * If your app needs to do some initialization before the
@@ -102,7 +165,15 @@ const DEFAULT_OPTIONS = {
102
165
  },
103
166
  }
104
167
 
168
+ /**
169
+ * Moonwalk animation system for scroll-based reveal animations
170
+ */
105
171
  export default class Moonwalk {
172
+ /**
173
+ * @param {Object} app - The application instance
174
+ * @param {MoonwalkOptions} [opts={}] - Configuration options
175
+ * @param {HTMLElement} [container=document.body] - Container element
176
+ */
106
177
  constructor(app, opts = {}, container = document.body) {
107
178
  this.app = app
108
179
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
@@ -293,6 +364,7 @@ export default class Moonwalk {
293
364
  el: run,
294
365
  threshold: foundRun.threshold || 0,
295
366
  initialize: foundRun.initialize,
367
+ onReady: foundRun.onReady,
296
368
  callback: foundRun.callback,
297
369
  onExit: foundRun.onExit,
298
370
  repeated: foundRun.repeated,
@@ -335,16 +407,15 @@ export default class Moonwalk {
335
407
  this.addIndexes(section)
336
408
  }
337
409
 
338
- const timeline = gsap.timeline({
339
- autoRemoveChildren: false,
340
- smoothChildTiming: false,
341
- })
342
-
343
410
  return {
344
411
  id: Math.random().toString(36).substring(7),
345
412
  el: section,
346
413
  name: section.getAttribute('data-moonwalk-section') || null,
347
- timeline,
414
+ animation: {
415
+ lastDelay: 0,
416
+ lastDuration: 0,
417
+ lastStartTime: null,
418
+ },
348
419
  observer: null,
349
420
  stage: {
350
421
  name: section.getAttribute('data-moonwalk-stage') || null,
@@ -430,14 +501,32 @@ export default class Moonwalk {
430
501
  section.children = this.orderChildren(section.el.children)
431
502
  }
432
503
 
433
- const fromTransition = sectionWalk.alphaTween
434
- ? {
435
- ...sectionWalk.transition.from,
436
- opacity: 0,
437
- }
438
- : sectionWalk.transition.from
439
-
440
- gsap.set(section.children, fromTransition)
504
+ // Only set initial states for JS animations (transition !== null)
505
+ if (sectionWalk.transition) {
506
+ const fromTransition = sectionWalk.alphaTween
507
+ ? {
508
+ ...sectionWalk.transition.from,
509
+ opacity: 0,
510
+ }
511
+ : sectionWalk.transition.from
512
+
513
+ logMoonwalk('InitialState', 'Setting initial state for named section', {
514
+ section: section.name,
515
+ childCount: section.children.length,
516
+ fromTransition,
517
+ })
518
+ set(section.children, fromTransition)
519
+
520
+ // Check if styles were actually applied
521
+ if (section.children.length > 0) {
522
+ logComputedStyle(section.children[0])
523
+ }
524
+ } else {
525
+ logMoonwalk('InitialState', 'Skipping initial state for CSS-only section', {
526
+ section: section.name,
527
+ childCount: section.children.length,
528
+ })
529
+ }
441
530
  }
442
531
 
443
532
  if (section.stage.name) {
@@ -449,7 +538,12 @@ export default class Moonwalk {
449
538
  section.stage.name
450
539
  )
451
540
  } else {
452
- gsap.set(section.el, stageTween.transition.from)
541
+ logMoonwalk('InitialState', 'Setting stage initial state', {
542
+ stage: section.stage.name,
543
+ from: stageTween.transition.from,
544
+ })
545
+ set(section.el, stageTween.transition.from)
546
+ logComputedStyle(section.el)
453
547
  }
454
548
  }
455
549
 
@@ -479,12 +573,9 @@ export default class Moonwalk {
479
573
  // run stage tween
480
574
  const stageTween = walks[section.stage.name]
481
575
 
482
- const to = {
483
- ...stageTween.transition.to,
576
+ animate(entry.target, stageTween.transition.to, {
484
577
  duration: stageTween.duration,
485
- }
486
-
487
- section.timeline.to(entry.target, to, 0)
578
+ })
488
579
  section.stage.firstTween = true
489
580
  }
490
581
  }
@@ -499,44 +590,75 @@ export default class Moonwalk {
499
590
  )
500
591
  }
501
592
 
502
- if (typeof tween.alphaTween === 'object') {
503
- tween.alphaTween.duration = tween.alphaTween.duration
504
- ? tween.alphaTween.duration
505
- : tween.duration
506
- } else if (tween.alphaTween === true) {
507
- tween.alphaTween = {
508
- duration: tween.duration,
509
- ease: 'sine.in',
593
+ logMoonwalk('SectionObserver', 'Named section triggered', {
594
+ sectionName: section.name,
595
+ childCount: section.children.length,
596
+ interval: tween.interval,
597
+ duration: tween.duration,
598
+ hasAlphaTween: !!tween.alphaTween,
599
+ isCssOnly: !tween.transition,
600
+ })
601
+
602
+ // Check if this is CSS-only animation (transition: null)
603
+ if (!tween.transition) {
604
+ // CSS-only mode - stagger adding the data-moonwalked attribute
605
+ logMoonwalk('SectionObserver', 'Using CSS-only mode', {
606
+ sectionName: section.name,
607
+ })
608
+
609
+ section.children.forEach((child, index) => {
610
+ const delay = (tween.startDelay || 0) + index * tween.interval
611
+ delayedCall(delay, () => {
612
+ child.setAttribute('data-moonwalked', '')
613
+ })
614
+ })
615
+ } else {
616
+ // JS animation mode
617
+ if (typeof tween.alphaTween === 'object') {
618
+ tween.alphaTween.duration = tween.alphaTween.duration
619
+ ? tween.alphaTween.duration
620
+ : tween.duration
621
+ } else if (tween.alphaTween === true) {
622
+ tween.alphaTween = {
623
+ duration: tween.duration,
624
+ ease: 'easeIn',
625
+ }
510
626
  }
511
- }
512
627
 
513
- if (tween.startDelay) {
514
- tween.transition.to = {
515
- ...tween.transition.to,
516
- delay: tween.startDelay,
517
- }
518
- }
628
+ // Extract ease from to values and convert for Motion.js
629
+ const { ease: tweenEase, ...toValues } = tween.transition.to
630
+ const convertedEase = convertEasing(tweenEase || 'easeOut')
519
631
 
520
- section.timeline.staggerTo(
521
- section.children,
522
- tween.duration,
523
- tween.transition.to,
524
- tween.interval,
525
- 0
526
- )
632
+ const animationOptions = {
633
+ duration: tween.duration,
634
+ ease: convertedEase,
635
+ delay: stagger(tween.interval, {
636
+ startDelay: tween.startDelay || 0,
637
+ }),
638
+ }
527
639
 
528
- if (tween.alphaTween) {
529
- section.timeline.staggerTo(
530
- section.children,
531
- tween.alphaTween.duration,
532
- {
533
- opacity: 1,
534
- ease: tween.alphaTween.ease,
535
- delay: tween.startDelay || 0,
536
- },
537
- tween.interval,
538
- 0
539
- )
640
+ logMoonwalk('SectionObserver', 'Starting stagger animation', {
641
+ sectionName: section.name,
642
+ to: toValues,
643
+ ease: convertedEase,
644
+ options: animationOptions,
645
+ })
646
+
647
+ animate(section.children, toValues, animationOptions)
648
+
649
+ if (tween.alphaTween) {
650
+ animate(
651
+ section.children,
652
+ { opacity: 1 },
653
+ {
654
+ duration: tween.alphaTween.duration,
655
+ ease: convertEasing(tween.alphaTween.ease || 'easeIn'),
656
+ delay: stagger(tween.interval, {
657
+ startDelay: tween.startDelay || 0,
658
+ }),
659
+ }
660
+ )
661
+ }
540
662
  }
541
663
  }
542
664
 
@@ -578,6 +700,64 @@ export default class Moonwalk {
578
700
  })
579
701
  }
580
702
 
703
+ /**
704
+ * Calculate the delay for the next animation in the section.
705
+ * This replaces GSAP's timeline.recent() logic.
706
+ *
707
+ * @param {*} section - The section object
708
+ * @param {*} duration - Duration of the animation to add
709
+ * @param {*} overlap - How much the animations should overlap
710
+ * @returns {number} The delay in seconds
711
+ */
712
+ calculateDelay(section, duration, overlap) {
713
+ if (!section.animation.lastStartTime) {
714
+ // First animation in section
715
+ logMoonwalk('DelayCalc', 'First animation in section', { delay: 0 })
716
+ return 0
717
+ }
718
+
719
+ const now = performance.now()
720
+ const elapsed = (now - section.animation.lastStartTime) / 1000
721
+ // overlap is negative when animations should stagger (start before previous ends)
722
+ // So we ADD overlap (which is negative) to get the correct next start time
723
+ const idealNextStart =
724
+ section.animation.lastDelay + section.animation.lastDuration + overlap
725
+ const actualDelay = Math.max(0, idealNextStart - elapsed)
726
+
727
+ logMoonwalk('DelayCalc', 'Calculating delay', {
728
+ elapsed: elapsed.toFixed(3),
729
+ lastDelay: section.animation.lastDelay,
730
+ lastDuration: section.animation.lastDuration,
731
+ overlap,
732
+ idealNextStart,
733
+ actualDelay: actualDelay.toFixed(3),
734
+ })
735
+
736
+ return actualDelay
737
+ }
738
+
739
+ /**
740
+ * Update the animation state after adding an animation.
741
+ *
742
+ * @param {*} section - The section object
743
+ * @param {*} delay - The delay that was used
744
+ * @param {*} duration - The duration of the animation
745
+ */
746
+ updateAnimationState(section, delay, duration) {
747
+ const previousState = { ...section.animation }
748
+
749
+ section.animation.lastDelay = delay
750
+ section.animation.lastDuration = duration
751
+ section.animation.lastStartTime = performance.now()
752
+
753
+ logMoonwalk('StateUpdate', 'Updating animation state', {
754
+ delay,
755
+ duration,
756
+ previousState,
757
+ newState: { ...section.animation },
758
+ })
759
+ }
760
+
581
761
  onReady() {
582
762
  if (this.opts.initialDelay) {
583
763
  setTimeout(() => {
@@ -595,6 +775,14 @@ export default class Moonwalk {
595
775
  ready() {
596
776
  const { opts } = this
597
777
 
778
+ // Execute onReady callbacks for all runs
779
+ for (let idx = 0; idx < this.runs.length; idx += 1) {
780
+ const run = this.runs[idx]
781
+ if (run && run.onReady) {
782
+ run.onReady(run.el)
783
+ }
784
+ }
785
+
598
786
  for (let idx = 0; idx < this.runs.length; idx += 1) {
599
787
  const run = this.runs[idx]
600
788
 
@@ -637,7 +825,29 @@ export default class Moonwalk {
637
825
  }
638
826
 
639
827
  section.elements = section.el.querySelectorAll('[data-moonwalk]')
640
- section.elements.forEach((box) => section.observer.observe(box))
828
+
829
+ // Only set initial states and observe individual elements for unnamed sections
830
+ // Named sections are observed at the section level via sectionObserver
831
+ if (!section.name) {
832
+ // Set initial states for tweenJS elements BEFORE observing
833
+ section.elements.forEach((element) => {
834
+ const walkName = element.getAttribute('data-moonwalk')
835
+ const cfg = !walkName.length
836
+ ? opts.walks.default
837
+ : opts.walks[walkName]
838
+
839
+ // Only set initial state if this uses tweenJS (has transition property)
840
+ if (cfg && cfg.transition) {
841
+ logMoonwalk('InitialState', 'Setting initial state for individual element', {
842
+ walkName: walkName || 'default',
843
+ from: cfg.transition.from,
844
+ })
845
+ set(element, cfg.transition.from)
846
+ }
847
+ })
848
+
849
+ section.elements.forEach((box) => section.observer.observe(box))
850
+ }
641
851
  }
642
852
  }
643
853
 
@@ -648,15 +858,56 @@ export default class Moonwalk {
648
858
  * @param {*} rootMargin
649
859
  */
650
860
  runObserver(run, rootMargin) {
861
+ // Store the previous positions of observed elements to compare for exit direction
862
+ const elementPositions = new WeakMap()
863
+
651
864
  return new IntersectionObserver(
652
865
  (entries, self) => {
653
866
  for (let i = 0; i < entries.length; i += 1) {
654
867
  const entry = entries[i]
868
+
869
+ // Store the element's current position in the viewport
870
+ const boundingRect = entry.boundingClientRect
871
+ const viewportHeight = window.innerHeight
872
+ const viewportWidth = window.innerWidth
873
+
655
874
  if (entry.isIntersecting && run.callback) {
875
+ // Calculate entry direction
876
+ let meta = { direction: null }
877
+
878
+ // Use the app's scroll direction for reliable detection
879
+ // If scrollDirection is null, the element was likely revealed on initial load
880
+ if (this.app.state && this.app.state.scrollDirection) {
881
+ // Map scroll direction to viewport entry direction
882
+ switch (this.app.state.scrollDirection) {
883
+ case 'down':
884
+ meta.direction = 'bottom' // When scrolling down, elements enter from bottom
885
+ break
886
+ case 'up':
887
+ meta.direction = 'top' // When scrolling up, elements enter from top
888
+ break
889
+ case 'right':
890
+ meta.direction = 'left' // When scrolling right, elements enter from left
891
+ break
892
+ case 'left':
893
+ meta.direction = 'right' // When scrolling left, elements enter from right
894
+ break
895
+ }
896
+ }
897
+ // If no scroll direction is available, direction remains null
898
+
899
+ // Store the element's position when it enters the viewport
900
+ elementPositions.set(entry.target, {
901
+ top: boundingRect.top,
902
+ bottom: boundingRect.bottom,
903
+ left: boundingRect.left,
904
+ right: boundingRect.right
905
+ })
906
+
656
907
  const runRepeated = entry.target.hasAttribute(
657
908
  'data-moonwalk-run-triggered'
658
909
  )
659
- run.callback(entry.target, runRepeated)
910
+ run.callback(entry.target, runRepeated, meta)
660
911
  entry.target.setAttribute('data-moonwalk-run-triggered', '')
661
912
  if (!run.onExit && !run.repeated) {
662
913
  self.unobserve(entry.target)
@@ -670,7 +921,42 @@ export default class Moonwalk {
670
921
  'data-moonwalk-run-exit-triggered'
671
922
  )
672
923
  entry.target.setAttribute('data-moonwalk-run-exit-triggered', '')
673
- run.onExit(entry.target, runExited)
924
+
925
+ // Calculate exit direction
926
+ let meta = { direction: null }
927
+
928
+ // Use the app's scroll direction for reliable detection
929
+ // For exit direction, it's the opposite of the entry direction for the same scroll
930
+ if (this.app.state && this.app.state.scrollDirection) {
931
+ // Map scroll direction to viewport exit direction
932
+ switch (this.app.state.scrollDirection) {
933
+ case 'down':
934
+ meta.direction = 'top' // When scrolling down, elements exit from top
935
+ break
936
+ case 'up':
937
+ meta.direction = 'bottom' // When scrolling up, elements exit from bottom
938
+ break
939
+ case 'right':
940
+ meta.direction = 'right' // When scrolling right, elements exit from right
941
+ break
942
+ case 'left':
943
+ meta.direction = 'left' // When scrolling left, elements exit from left
944
+ break
945
+ }
946
+ } else {
947
+ // If no scroll direction is available, use the simplest position-based check
948
+ if (boundingRect.bottom <= 0) {
949
+ meta.direction = 'top'
950
+ } else if (boundingRect.top >= viewportHeight) {
951
+ meta.direction = 'bottom'
952
+ } else if (boundingRect.right <= 0) {
953
+ meta.direction = 'left'
954
+ } else if (boundingRect.left >= viewportWidth) {
955
+ meta.direction = 'right'
956
+ }
957
+ }
958
+
959
+ run.onExit(entry.target, runExited, meta)
674
960
  if (!run.repeated) {
675
961
  self.unobserve(entry.target)
676
962
  }
@@ -703,16 +989,25 @@ export default class Moonwalk {
703
989
  if (entry.isIntersecting || entry.intersectionRatio > 0) {
704
990
  section.running = true
705
991
 
706
- if (entry.target.dataset.moonwalkId) {
707
- console.debug('-- intersecting', entry.target.dataset.moonwalkId)
708
- }
709
-
710
992
  const walkName = entry.target.getAttribute('data-moonwalk')
993
+ const targetId =
994
+ entry.target.getAttribute('data-testid') ||
995
+ walkName ||
996
+ entry.target.className
997
+
998
+ logMoonwalk('Observer', 'Element entered viewport', {
999
+ target: targetId,
1000
+ walkName,
1001
+ isIntersecting: entry.isIntersecting,
1002
+ intersectionRatio: entry.intersectionRatio,
1003
+ })
711
1004
  const cfg = !walkName.length
712
1005
  ? opts.walks.default
713
1006
  : opts.walks[walkName]
714
1007
 
715
- const { duration, transition, interval, startDelay } = cfg
1008
+ const { duration, transition, startDelay } = cfg
1009
+ // Default interval to 0.15 if not specified (same as default walk)
1010
+ const interval = cfg.interval !== undefined ? cfg.interval : 0.15
716
1011
 
717
1012
  let { alphaTween } = cfg
718
1013
  let overlap = (duration - interval) * -1 // flip it
@@ -729,27 +1024,36 @@ export default class Moonwalk {
729
1024
  } else if (alphaTween === true) {
730
1025
  alphaTween = {
731
1026
  duration,
732
- ease: 'sine.in',
1027
+ ease: 'easeIn',
733
1028
  }
734
1029
  }
735
1030
 
736
- const tween = transition ? this.tweenJS : this.tweenCSS
737
-
738
1031
  const tweenFn = () => {
739
- tween(
740
- section,
741
- entry.target,
742
- duration,
743
- interval,
744
- transition,
745
- overlap,
746
- alphaTween
747
- )
1032
+ if (transition) {
1033
+ this.tweenJS(
1034
+ section,
1035
+ entry.target,
1036
+ duration,
1037
+ interval,
1038
+ transition,
1039
+ overlap,
1040
+ alphaTween
1041
+ )
1042
+ } else {
1043
+ this.tweenCSS(
1044
+ section,
1045
+ entry.target,
1046
+ duration,
1047
+ interval,
1048
+ transition,
1049
+ overlap
1050
+ )
1051
+ }
748
1052
  }
749
1053
 
750
1054
  const wrappedTweenFn = () => {
751
1055
  if (startDelay) {
752
- gsap.delayedCall(startDelay, tweenFn)
1056
+ delayedCall(startDelay, tweenFn)
753
1057
  } else {
754
1058
  tweenFn()
755
1059
  }
@@ -813,56 +1117,90 @@ export default class Moonwalk {
813
1117
  tweenOverlap,
814
1118
  alphaTween
815
1119
  ) {
816
- let tweenPosition
817
- const startingPoint = tweenDuration - tweenOverlap
1120
+ const targetId =
1121
+ target.getAttribute('data-testid') ||
1122
+ target.getAttribute('data-moonwalk') ||
1123
+ target.className
1124
+
1125
+ logMoonwalk('TweenJS', 'Starting tweenJS', {
1126
+ target: targetId,
1127
+ duration: tweenDuration,
1128
+ overlap: tweenOverlap,
1129
+ hasAlphaTween: !!alphaTween,
1130
+ })
818
1131
 
819
1132
  if (Dom.hasAttribute(target, 'data-moonwalked')) {
1133
+ logMoonwalk('TweenJS', 'Already moonwalked, skipping', { target: targetId })
820
1134
  return
821
1135
  }
822
1136
 
823
- if (section.timeline.isActive() && section.timeline.recent()) {
824
- const currentTime = section.timeline.time()
825
- const lastTweenTime = section.timeline.recent().time()
826
- const lastTweenEndTime = section.timeline.recent().endTime()
827
- if (lastTweenTime > startingPoint) {
828
- /* We're late for this tween if it was supposed to be sequential,
829
- so insert at current time in timeline instead */
830
- tweenPosition = () => section.timeline.time()
831
- } else {
832
- if (currentTime + tweenOverlap * -1 < lastTweenEndTime) {
833
- /* Still time, add as normal overlap at the end */
834
- tweenPosition = () => `>${tweenOverlap}`
835
- } else {
836
- /* Won't make it */
837
- tweenPosition = () => section.timeline.time()
838
- }
839
- }
840
- } else {
841
- tweenPosition = () => '>'
842
- }
1137
+ // Calculate delay using our new helper method
1138
+ const delay = this.calculateDelay(section, tweenDuration, tweenOverlap)
1139
+
1140
+ // Initial state should already be set during ready()
1141
+ // Only log for debugging
1142
+ logMoonwalk('TweenJS', 'Element should already have initial state', {
1143
+ target: targetId,
1144
+ expectedFrom: tweenTransition.from,
1145
+ })
1146
+ logComputedStyle(target, ['opacity', 'transform', 'x', 'y'])
843
1147
 
844
- gsap.set(target, tweenTransition.from)
1148
+ // Extract ease from to values (GSAP format) and convert to Motion easing option
1149
+ const { ease, ...toValues } = tweenTransition.to
1150
+ const easingOption = convertEasing(ease || 'easeOut')
845
1151
 
846
- const toTransition = {
847
- ...tweenTransition.to,
1152
+ logMoonwalk('TweenJS', 'Starting animation', {
1153
+ target: targetId,
1154
+ toValues,
848
1155
  duration: tweenDuration,
849
- onComplete: () => target.setAttribute('data-moonwalked', ''),
850
- }
1156
+ delay: delay.toFixed(3),
1157
+ ease: easingOption,
1158
+ })
851
1159
 
852
- section.timeline.to(target, toTransition, tweenPosition())
1160
+ // Animate to final state
1161
+ const animation = animate(target, toValues, {
1162
+ duration: tweenDuration,
1163
+ delay,
1164
+ ease: easingOption,
1165
+ })
853
1166
 
1167
+ // Use .finished promise for completion callback
1168
+ if (animation && animation.finished) {
1169
+ animation.finished
1170
+ .then(() => {
1171
+ logMoonwalk('TweenJS', 'Animation completed', { target: targetId })
1172
+ target.setAttribute('data-moonwalked', '')
1173
+ })
1174
+ .catch((err) => {
1175
+ // Animation cancelled or failed, still mark as walked
1176
+ logMoonwalk('TweenJS', 'Animation failed/cancelled', {
1177
+ target: targetId,
1178
+ error: err,
1179
+ })
1180
+ target.setAttribute('data-moonwalked', '')
1181
+ })
1182
+ } else {
1183
+ // No animation object returned, mark immediately
1184
+ logMoonwalk('TweenJS', 'No animation object returned', { target: targetId })
1185
+ target.setAttribute('data-moonwalked', '')
1186
+ }
1187
+
1188
+ // Optional separate alpha animation
854
1189
  if (alphaTween) {
855
- section.timeline.to(
856
- target,
857
- {
858
- duration: alphaTween.duration,
859
- opacity: 1,
860
- ease: alphaTween.ease,
861
- delay: alphaTween.delay ? alphaTween.delay : 0,
862
- },
863
- '<'
864
- )
1190
+ logMoonwalk('TweenJS', 'Adding alpha tween', {
1191
+ target: targetId,
1192
+ duration: alphaTween.duration,
1193
+ delay: (delay + (alphaTween.delay || 0)).toFixed(3),
1194
+ })
1195
+ animate(target, { opacity: 1 }, {
1196
+ duration: alphaTween.duration,
1197
+ ease: convertEasing(alphaTween.ease || 'easeIn'),
1198
+ delay: delay + (alphaTween.delay || 0),
1199
+ })
865
1200
  }
1201
+
1202
+ // Update animation state for next element
1203
+ this.updateAnimationState(section, delay, tweenDuration)
866
1204
  }
867
1205
 
868
1206
  /**
@@ -882,47 +1220,35 @@ export default class Moonwalk {
882
1220
  tweenTransition,
883
1221
  tweenOverlap
884
1222
  ) {
885
- let tweenPosition
886
- const startingPoint = tweenDuration - tweenOverlap * -1
887
-
888
1223
  if (Dom.hasAttribute(target, 'data-moonwalked')) {
889
1224
  return
890
1225
  }
891
1226
 
892
- if (section.timeline.isActive() && section.timeline.recent()) {
893
- const currentTime = section.timeline.time()
894
- const lastTweenTime = section.timeline.recent().time()
895
- const lastTweenEndTime = section.timeline.recent().endTime()
896
- if (lastTweenTime > startingPoint) {
897
- /* We're late for this tween if it was supposed to be sequential,
898
- so insert at current time in timeline instead */
899
- tweenPosition = () => section.timeline.time()
900
- } else {
901
- if (currentTime + tweenOverlap * -1 < lastTweenEndTime) {
902
- /* Still time, add as normal overlap at the end */
903
- tweenPosition = () => `>${tweenOverlap}`
904
- } else {
905
- /* Won't make it */
906
- tweenPosition = () => section.timeline.time()
907
- }
908
- }
909
- } else {
910
- tweenPosition = () => '>'
911
- }
1227
+ // Calculate delay using our helper method for stagger effect
1228
+ const calculatedDelay = this.calculateDelay(
1229
+ section,
1230
+ tweenDuration,
1231
+ tweenOverlap
1232
+ )
912
1233
 
913
- section.timeline
914
- .to(
915
- target,
916
- {
917
- css: {
918
- className: target.className
919
- ? `${target.className} moonwalked`
920
- : 'moonwalked',
921
- },
922
- duration: tweenDuration,
923
- },
924
- tweenPosition()
925
- )
926
- .call(() => target.setAttribute('data-moonwalked', ''), null, '>')
1234
+ const targetId = target.getAttribute('data-testid') || target.className
1235
+
1236
+ logMoonwalk('TweenCSS', 'Scheduling CSS animation', {
1237
+ target: targetId,
1238
+ delay: calculatedDelay.toFixed(3),
1239
+ duration: tweenDuration,
1240
+ })
1241
+
1242
+ // Add class after delay to trigger CSS transition
1243
+ delayedCall(calculatedDelay, () => {
1244
+ logMoonwalk('TweenCSS', 'Adding moonwalked attribute', {
1245
+ target: targetId,
1246
+ })
1247
+ target.classList.add('moonwalked')
1248
+ target.setAttribute('data-moonwalked', '')
1249
+ })
1250
+
1251
+ // Update animation state for next element in section
1252
+ this.updateAnimationState(section, calculatedDelay, tweenDuration)
927
1253
  }
928
1254
  }