PageSourceSearch

https://lisamlum.com/assets/js/home-motion.js?v=9

js lisamlum.com collected 2026-10-04 23:43:50 UTC 12,054 bytes, 282 lines download raw bytes

1/* lisamlum.com homepage experience layer.
2   GSAP + ScrollTrigger + Lenis arrive via deferred CDN tags before this file.
3   Contract: if anything is missing, or reduced motion is set, every element
4   must end up visible with zero animation. */
5(function () {
6  'use strict';
7  var root = document.documentElement;
8
9  function showEverything() {
10    root.classList.remove('has-motion');
11    root.classList.add('wm-ready');
12    document.querySelectorAll('.fade-in').forEach(function (el) {
13      el.classList.add('visible');
14    });
15  }
16
17  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
18  if (reduced || !window.gsap || !window.ScrollTrigger) {
19    showEverything();
20    return;
21  }
22
23  gsap.registerPlugin(ScrollTrigger);
24  root.classList.add('has-motion');
25
26  /* The shared components.js fade-in observer must not fight GSAP on this
27     page: mark its targets visible immediately so it no-ops. */
28  document.querySelectorAll('.fade-in').forEach(function (el) {
29    el.classList.add('visible');
30  });
31
32  var isMobile = window.matchMedia('(max-width: 640px)').matches;
33  var EASE = 'power3.out';
34
35  /* ── Lenis smooth scroll: fine pointers only ── */
36  var lenis = null;
37  if (window.Lenis && window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
38    lenis = new Lenis({ duration: 1.1, smoothWheel: true });
39    lenis.on('scroll', ScrollTrigger.update);
40    gsap.ticker.add(function (t) { lenis.raf(t * 1000); });
41    gsap.ticker.lagSmoothing(0);
42    document.addEventListener('click', function (e) {
43      var a = e.target.closest ? e.target.closest('a[href^="#"]') : null;
44      if (!a) return;
45      var hash = a.getAttribute('href');
46      if (hash.length < 2) return;
47      var target = document.querySelector(hash);
48      if (!target) return;
49      e.preventDefault();
50      lenis.scrollTo(target, { offset: -80 });
51    });
52  }
53
54  /* ── Hero cinematic entrance ── */
55  function heroEntrance() {
56    var tagline = document.querySelector('.hero .h1-tagline');
57    if (tagline) {
58      var html = '';
59      Array.prototype.forEach.call(tagline.childNodes, function (n) {
60        if (n.nodeType === 3) {
61          n.textContent.split(/\s+/).forEach(function (w) {
62            if (w) html += '<span class="wm"><span class="w">' + w + '</span></span> ';
63          });
64        } else if (n.nodeType === 1) {
65          html += '<span class="wm"><span class="w">' + n.outerHTML + '</span></span> ';
66        }
67      });
68      tagline.innerHTML = html;
69    }
70    var words = document.querySelectorAll('.hero .h1-tagline .w');
71    var tl = gsap.timeline({ defaults: { ease: EASE } });
72    tl.fromTo('.hero-bg', { scale: 1.06 },
73        { scale: 1, duration: isMobile ? 1.2 : 1.8, ease: 'power2.out' }, 0)
74      .fromTo('.hero .h1-eyebrow', { opacity: 0, y: 12 },
75        { opacity: 1, y: 0, duration: 0.5 }, 0.15)
76      .to('.h1-rule', { scaleX: 1, duration: 0.5, ease: 'power2.inOut' }, 0.3)
77      .set('.hero .h1-tagline', { opacity: 1 }, 0.35)
78      .fromTo(words, { yPercent: 130 },
79        { yPercent: 0, duration: isMobile ? 0.7 : 1.0, stagger: 0.05 }, 0.35)
80      .fromTo(['.hero .h1-geo', '.hero-rating'], { opacity: 0, y: 16 },
81        { opacity: 1, y: 0, duration: 0.5, stagger: 0.08 }, isMobile ? 0.65 : 0.95)
82      .fromTo(['.hero-ctas', '.hero-proof'], { opacity: 0, y: 16 },
83        { opacity: 1, y: 0, duration: 0.5, stagger: 0.08 }, isMobile ? 0.75 : 1.1);
84  }
85
86  /* ── Shared reveal helpers ── */
87  function rise(targets, trigger, stagger, y) {
88    /* Never hide anything already in the viewport: late repaints become the
89       LCP and read as jank. Reveals apply only below the current viewport. */
90    var els = gsap.utils.toArray(targets).filter(function (el) {
91      return el.getBoundingClientRect().top > window.innerHeight * 0.9;
92    });
93    if (!els.length) return;
94    gsap.fromTo(els, { opacity: 0, y: y || (isMobile ? 18 : 28) },
95      { opacity: 1, y: 0, duration: 0.8, ease: EASE, stagger: stagger || 0,
96        scrollTrigger: { trigger: trigger || els[0], start: 'top 85%', once: true } });
97  }
98
99  function animateCount(el) {
100    var m = el.textContent.trim().match(/^(\$)?([\d][\d.,]*)([A-Za-z+%]*)$/);
101    if (!m) return;
102    var prefix = m[1] || '';
103    var clean = m[2].replace(/,/g, '');
104    var target = parseFloat(clean);
105    var suffix = m[3] || '';
106    var decimals = (clean.split('.')[1] || '').length;
107    var state = { v: 0 };
108    gsap.to(state, { v: target, duration: 1.2, ease: 'power1.out',
109      onUpdate: function () { el.textContent = prefix + state.v.toFixed(decimals) + suffix; },
110      onComplete: function () { el.textContent = prefix + m[2] + suffix; } });
111  }
112
113  function countOnEnter(el) {
114    ScrollTrigger.create({ trigger: el, start: 'top 88%', once: true,
115      onEnter: function () { animateCount(el); } });
116  }
117
118  function drawRules() {
119    /* Same viewport rule as reveals: never animate a rule already on screen. */
120    gsap.utils.toArray('.rule-draw').filter(function (el) {
121      return el.getBoundingClientRect().top > window.innerHeight * 0.9;
122    }
122).forEach(function (el) {
123      gsap.fromTo(el, { scaleX: 0 }, { scaleX: 1, duration: 0.6, ease: 'power2.inOut',
124        scrollTrigger: { trigger: el, start: 'top 88%', once: true } });
125    });
126  }
127
128  function featherChapter() {
129    var section = document.querySelector('.recent-result');
130    if (!section) return;
131
132    function compactReveal() {
133      rise('.recent-result-panel', '.recent-result', 0, 36);
134      if (document.querySelector('.recent-result-bg')) {
135        gsap.fromTo('.recent-result-bg', { yPercent: -6 }, { yPercent: 6, ease: 'none',
136          scrollTrigger: { trigger: '.recent-result', start: 'top bottom', end: 'bottom top', scrub: true } });
137      }
138    }
139
140    /* If the section is already in view at init (deep-scroll restore), never
141       pin or hide it: standing viewport rule. */
142    if (section.getBoundingClientRect().top <= window.innerHeight * 0.9) {
143      return;
144    }
145
146    /* Non-pinned, once-only reveal for all viewports. The previous desktop
147       path pinned + scrubbed this section; combined with Lenis smooth scroll
148       that pin could desync and trap the timeline at progress 0, leaving the
149       navy panel visible with its text stuck at opacity 0. The compact reveal
150       can't get stuck: the panel rises in once and stays visible. */
151    compactReveal();
152  }
153
154  /* ── Section choreography ── */
155  function sections() {
156    rise('.numbers-strip p', '.numbers-strip');
157    rise('.intro h2', '.intro');
158    rise('.intro-body p', '.intro-body', 0.12);
159    rise('.intro-stats > div', '.intro-stats', 0.1, 20);
160    document.querySelectorAll('.intro-stat-num').forEach(countOnEnter);
161
162    gsap.utils.toArray('.services-grid').forEach(function (grid) {
163      var cards = grid.querySelectorAll('.service-card');
164      gsap.fromTo(cards, { opacity: 0, y: 32 },
165        { opacity: 1, y: 0, duration: 0.8, ease: EASE, stagger: 0.12,
166          scrollTrigger: { trigger: grid, start: 'top 85%', once: true } });
167      gsap.fromTo(grid.querySelectorAll('.service-num'), { x: -20, opacity: 0 },
168        { x: 0, opacity: 1, duration: 0.6, ease: EASE, stagger: 0.12,
169          scrollTrigger: { trigger: grid, start: 'top 85%', once: true } });
170    });
171
172    rise('.hr-feature-inner > *', '.hr-feature', 0.1);
173    gsap.fromTo('.hr-feature .pillars span', { opacity: 0, y: 14 },
174      { opacity: 1, y: 0, duration: 0.5, ease: EASE, stagger: 0.08,
175        scrollTrigger: { trigger: '.hr-feature .pillars', start: 'top 90%', once: true } });
176    var hrBg = document.querySelector('.hr-feature');
177    if (hrBg && getComputedStyle(hrBg).backgroundImage !== 'none') {
178      gsap.fromTo(hrBg, { backgroundPositionY: '40%' }, { backgroundPositionY: '60%', ease: 'none',
179        scrollTrigger: { trigger: hrBg, start: 'top bottom', end: 'bottom top', scrub: true } });
180    }
181
182    /* Home Refresh compare slider: sweep the divider in on first view so
183       visitors discover it is draggable. home-cinema.js owns the control. */
184    var hrcFrame = document.querySelector('.hrc-frame');
185    if (hrcFrame && hrcFrame.getBoundingClientRect().top > window.innerHeight * 0.9) {
186      ScrollTrigger.create({ trigger: '.hr-compare', start: 'top 75%', once: true,
187        onEnter: function () {
188          if (!hrcFrame.__setPos) return;
189          var state = { v: 88 };
190          hrcFrame.__setPos(88);
191          gsap.to(state, { v: 50, duration: 1.4, ease: 'power2.inOut',
192            onUpdate: function () { hrcFrame.__setPos(state.v); } });
193        } });
194    }
195
196    /* How a sale unfolds */
197    rise('.sc-header > *', '.sale-chapters', 0.08);
198    var scCards = gsap.utils.toArray('.sc-card');
199    if (scCards.length) {
200      gsap.fromTo(scCards, { opacity: 0, y: 32 },
201        { opacity: 1, y: 0, duration: 0.8, ease: EASE, stagger: 0.1,
202          scrollTrigger: { trigger: '.sc-grid', start: 'top 85%', once: true } });
203      gsap.fromTo('.sc-num', { x: -16, opacity: 0 },
204        { x: 0, opacity: 1, duration: 0.6, ease: EASE, stagger: 0.1,
205          scrollTrigger: { trigger: '.sc-grid', start: 'top 85%', once: true } });
206    }
207    rise('.sc-cta > *', '.sc-cta', 0.08, 16);
208
209    /* Community switcher */
210    rise('.cs-item', '.community-switcher', 0.05, 18);
211    var csStage = document.querySelector('.cs-stage');
212    if (csStage && csStage.getBoundingClientRect().top > window.innerHeight * 0.9) {
213      gsap.fromTo(csStage, { opacity: 0, clipPath: 'inset(6% 6% 6% 6%)' },
214        { opacity: 1, clipPath: 'inset(0% 0% 0% 0%)', duration: 0.9, ease: EASE,
215          scrollTrigger: { trigger: '.community-switcher', start: 'top 80%', once: true } });
216    }
217
218    rise('.communities-header', '.communities');
219    var tiles = gsap.utils.toArray('.community-grid > *');
220    if (tiles.length) {
221      gsap.fromTo(tiles, { opacity: 0, clipPath: 'inset(10% 10% 10% 10%)' },
222        { opacity: 1, clipPath: 'inset(0% 0% 0% 0%)', duration: 0.8, ease: EASE, stagger: 0.07,
223          scrollTrigger: { trigger: '.community-grid', start: 'top 85%', once: true } });
224    }
225
226    rise('.market-widget-header', '.market-widget');
227    var mwHero = document.querySelector('[data-mw="hero"]');
228    if (mwHero) {
229      ScrollTrigger.create({ trigger: '#market-widget', start: 'top 80%', once: true,
230        onEnter: function () {
231          var els = mwHero.querySelectorAll('*');
232          for (var i = 0; i < els.length; i++) {
233            if (els[i].children.length === 0 && /^\$[\d.,]+[MK]?$/.test(els[i].textContent.trim())) {
234              animateCount(els[i]);
235              return;
236            }
237          }
238        } });
239    }
240
241    if (document.querySelector('.about-image')) {
242      gsap.fromTo('.about-image', { clipPath: 'inset(0 100% 0 0)' },
243        { clipPath: 'inset(0 0% 0 0)', duration: 1.0, ease: 'power3.inOut',
244          scrollTrigger: { trigger: '.about', start: 'top 80%', once: true } });
245    }
246    rise('.about-content > *', '.about-content', 0.08, 20);
247
248    gsap.fromTo('.brand-film-frame', { opacity: 0, scale: 0.97 },
249      { opacity: 1, scale: 1, duration: 0.9, ease: EASE,
250        scrollTrigger: { trigger: '.brand-film', start: 'top 80%', once: true } });
251    rise('.brand-film .intro-label, .brand-film-title', '.brand-film', 0.08);
252
253    rise('.testimonials-header', '.testimonials');
254    rise('.testi-col', '.testi-marquee', 0.12, 24);
255
256    rise('.blog-header', '.blog');
257    rise('.blog-card', '.blog-grid', 0.1, 30);
258
259    rise('.cta-inner > *', '.cta', 0.08, 20);
260  }
261
262  function init() {
263    drawRules();
264    featherChapter();
265    heroEntrance();
266    sections();
267    window.addEventListener('load', function () {
268      ScrollTrigger.refresh();
269      root.classList.add('wm-ready');
270    });
271    var lazyImg = document.querySelector('.recent-result-bg');
272    if (lazyImg && !lazyImg.complete) {
273      lazyImg.addEventListener('load', function () { ScrollTrigger.refresh(); }, { once: true });
274    }
275  }
276
277  if (document.readyState === 'loading') {
278    document.addEventListener('DOMContentLoaded', init);
279  } else {
280    init();
281  }
282})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.