PageSourceSearch

https://download.lol/js/footer-parallax.js

js download.lol collected 2026-10-07 08:40:50 UTC 6,063 bytes, 143 lines download raw bytes

1/* ===========================================================================
2   FOOTER PARALLAX REVEAL (js/footer-parallax.js) — Osmo-style closer.
3   As the page scrolls to its end, the footer rises into view. Two scrubbed
4   tweens sell "surfacing from depth":
5     1) the footer's content is a slower layer — it lags the scroll (settling to
6        its natural position exactly when the footer fills the viewport), so the
7        foreground appears to lift off it;
8     2) a dark ink veil over the footer fades to nothing as it emerges.
9   Both are driven by ScrollTrigger's scrub, which is already fed by Lenis
10   (lenis.on("scroll", ScrollTrigger.update) in index.html) — so the motion
11   inherits the site's smooth-scroll easing and stays glassy.
12   =========================================================================== */
13(() => {
14  const gsap = window.gsap;
15  const ScrollTrigger = window.ScrollTrigger;
16  const footer = document.querySelector(".pfooter");
17  if (!footer || !gsap || !ScrollTrigger) return;
18
19  const inner = footer.querySelector(".pfooter__inner");
20  const veil = footer.querySelector(".pfooter__veil");
21  const wordmark = footer.querySelector(".pfooter__wordmark");
22
23  // Respect reduced-motion: land everything at rest, no reveal.
24  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
25    if (veil) gsap.set(veil, { opacity: 0 });
26    return;
27  }
28
29  gsap.registerPlugin(ScrollTrigger);
30
31  // Phones (≤820px): the footer is a content-height block (min-height:auto in
32  // footer.css), SHORTER than the viewport — so the desktop endpoints
33  // ("top top", "bottom bottom") sit PAST the page's maximum scroll and the
34  // scrub can never complete: the footer parks mid-reveal forever (wordmark
35  // sunk, columns high, veil half-dark — the "broken" mobile footer). "max"
36  // ends every scrub exactly at the absolute bottom of the page: always
37  // reachable, no viewport-height math, correct through iOS URL-bar resizes.
38  const narrow = window.matchMedia("(max-width: 820px)").matches;
39
40  // Phones: the document height drifts after load (late media, and on pages
41  // with content-visibility sections their estimated→real height swaps), so
42  // the scrub positions above go stale — the reveal then runs early, late, or
43  // never completes. Pages WITH the steady-glide system (pages.js) already
44  // watch for this and refresh at rest with a mid-glide guard; this covers
45  // the pages without it (about). Checked at fire time — script order and
46  // defer make load-time detection unreliable.
47  if (narrow && "ResizeObserver" in window) {
48    // Same safety rules as pages.js's observer: refresh only at scroll-rest
49    // and never inside a pinned intro (none on about, but the guard is cheap)
50    // — a mid-scroll refresh is a visible frame hitch.
51    let lastScrollTs = 0;
52    window.addEventListener("scroll", () => { lastScrollTs = performance.now(); }, { passive: true, capture: true });
53    let lastDocH = 0;
54    let t = 0;
55    const tryRefresh = () => {
56      const st = window.__introTL && window.__introTL.scrollTrigger;
57      const y = window.scrollY || document.documentElement.scrollTop;
58      if (performance.now() - lastScrollTs < 300 || (st && y < st.end + window.innerHeight * 0.5)) {
59        t = setTimeout(tryRefresh, 500); // retry until clear — deferred, not dropped
60        return;
61      }
62      ScrollTrigger.refresh();
63    };
64    new ResizeObserver(() => {
65      const h = document.documentElement.scrollHeight;
66      if (Math.abs(h - lastDocH) <= 4) return;
67      lastDocH = h;
68      if (window.__steadyHeight) return; // pages.js's guarded observer owns this page
69      clearTimeout(t);
70      t = setTimeout(tryRefresh, 250);
71    }).observe(document.body);
72  }
73
74  const build = () => {
75    // (1) Content lags the scroll. The inner starts pulled up (negative yPercent)
76    //     and eases to its natural position (0) exactly as the footer finishes
77    //     revealing — so net motion reads slower than the page above it.
78    gsap.fromTo(
79      inner,
80      { yPercent: -11 },
81      {
82        yPercent: 0,
83        ease: "none",
84        scrollTrigger: {
85          trigger: footer,
86          start: "top bottom", // footer's top enters at viewport bottom
87          end: narrow ? "max" : "top top", // fully shown (desktop: footer top hits viewport top)
88          scrub: true,
89          invalidateOnRefresh: true,
90        },
91      }
92    );
93
94    // (2) Ink veil fades off over the reveal, so the footer brightens as it
95    //     surfaces, then holds clean (power1.out front-loads the fade).
96    if (veil) {
97      gsap.fromTo(
98        veil,
99        { opacity: 0.5 },
100        {
101          opacity: 0,
102          ease: "power1.out",
103          scrollTrigger: {
104            trigger: footer,
105            start: "top bottom",
106            end: narrow ? "max" : "top 15%",
107            scrub: true,
108            invalidateOnRefresh: true,
109          },
110        }
111      );
112    }
113
114    // (3) The giant wordmark LIFTS UP into place — it rises vertically (no
115    //     sideways growth) as it clears the fold, the same "scrolls with you /
116    //     lifts up" feel as the content layer above it. Desktop triggers on the
117    //     wordmark itself; phones ride the footer trigger — the wordmark sits
118    //     so close to the page's end that its own start/end can fall beyond
119    //     the reachable scroll range.
120    if (wordmark) {
121      gsap.fromTo(
122        wordmark,
123        { yPercent: 55 }, // starts lower (pushed down, mostly below the fold)
124        {
125          yPercent: 0, // rises to rest as the footer finishes revealing
126          ease: "none",
127          scrollTrigger: {
128            trigger: narrow ? footer : wordmark,
129            start: "top bottom",
130            end: narrow ? "max" : "bottom bottom",
131            scrub: true,
132            invalidateOnRefresh: true,
133          },
134        }
135      );
136    }
137
138    ScrollTrigger.refresh();
139  };
140
141  if (document.readyState === "complete") build();
142  else window.addEventListener("load", build, { once: true });
143})();

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.