PageSourceSearch

https://circleofsanity.com/js/main.js?v=1.4.0

js circleofsanity.com collected 2026-09-29 06:56:20 UTC 11,215 bytes, 334 lines download raw bytes

1/* ============================================
2   CIRCLE OF SANITY — Main JS
3   Parallax + Scroll Animations + Transitions
4   ============================================ */
5
6document.addEventListener("DOMContentLoaded", () => {
7  const prefersReducedMotion = window.matchMedia(
8    "(prefers-reduced-motion: reduce)",
9  ).matches;
10
11  // --- Hero entrance animation ---
12  const hero = document.querySelector(".hero");
13  if (hero) {
14    requestAnimationFrame(() => {
15      setTimeout(() => hero.classList.add("loaded"), 200);
16    });
17  }
18
19  // --- Navbar scroll effect ---
20  const nav = document.querySelector(".nav");
21
22  // --- Keep hero clear of the fixed navbar at any nav height ---
23  const syncHeroOffset = () => {
24    if (nav && hero) hero.style.marginTop = nav.offsetHeight + "px";
25  };
26  syncHeroOffset();
27  window.addEventListener("resize", syncHeroOffset);
28
29  // --- Mobile menu ---
30  const toggle = document.querySelector(".nav-toggle");
31  const mobileMenu = document.querySelector(".mobile-menu");
32  const mobileClose = document.querySelector(".mobile-menu-close");
33  const mobileLinks = mobileMenu.querySelectorAll("a");
34
35  const openMenu = () => {
36    mobileMenu.classList.add("active");
37    document.body.style.overflow = "hidden";
38  };
39
40  const closeMenu = () => {
41    mobileMenu.classList.remove("active");
42    document.body.style.overflow = "";
43  };
44
45  toggle.addEventListener("click", openMenu);
46  mobileClose.addEventListener("click", closeMenu);
47  mobileLinks.forEach((link) => link.addEventListener("click", closeMenu));
48
49  // --- Scroll reveal animations (IntersectionObserver) ---
50  const revealSelectors =
51    ".reveal, .reveal-left, .reveal-right, .reveal-scale, .section-divider";
52  const revealElements = document.querySelectorAll(revealSelectors);
53
54  const revealObserver = new IntersectionObserver(
55    (entries) => {
56      entries.forEach((entry) => {
57        if (entry.isIntersecting) {
58          entry.target.classList.add("visible");
59          revealObserver.unobserve(entry.target);
60        }
61      });
62    },
63    {
64      threshold: 0.12,
65      rootMargin: "0px 0px -60px 0px",
66    },
67  );
68
69  revealElements.forEach((el) => revealObserver.observe(el));
70
71  // --- Parallax on scroll (requestAnimationFrame for performance) ---
72  const heroBg = document.querySelector(".hero-video");
73  const showsBg = document.querySelector(".shows-bg");
74  const heroTop = document.querySelector(".hero-top");
75  const heroBottom = document.querySelector(".hero-bottom");
76  const heroScroll = document.querySelector(".hero-scroll");
77
78  // Collect all parallax sections
79  const sections = document.querySelectorAll("section[id]");
80  const navLinks = document.querySelectorAll(".nav-links a");
81
82  let ticking = false;
83  let lastScrollY = 0;
84
85  const onScroll = () => {
86    lastScrollY = window.scrollY;
87    if (!ticking) {
88      requestAnimationFrame(updateOnScroll);
89      ticking = true;
90    }
91  };
92
93  const updateOnScroll = () => {
94    const scrollY = lastScrollY;
95
96    // Nav background
97    nav.classList.toggle("scrolled", scrollY > 60);
98
99    if (!prefersReducedMotion) {
100      // --- Hero parallax ---
101      if (hero && heroBg) {
102        const heroHeight = hero.offsetHeight;
103        if (scrollY < heroHeight) {
104          const progress = scrollY / heroHeight;
105          heroBg.style.transform = `translateY(${scrollY * 0.35}px) scale(${1 + progress * 0.05})`;
106          if (heroTop) {
107            heroTop.style.transform = `translateY(${scrollY * 0.15}px)`;
108            heroTop.style.opacity = 1 - progress * 1.5;
109          }
110          if (heroBottom) {
111            // A transform on this block turns the phone bottom-nav bar's
112            // position:fixed into scroll-along positioning — skip parallax
113            // entirely when the CTAs are the fixed bar.
114            if (window.matchMedia("(max-aspect-ratio: 3/5)").matches) {
115              heroBottom.style.transform = "";
116              heroBottom.style.opacity = "";
117            } else {
118              heroBottom.style.transform = `translateY(${scrollY * 0.25}px)`;
119              heroBottom.style.opacity = 1 - progress * 1.2;
120            }
121          }
122          if (heroScroll) {
123            heroScroll.style.opacity = Math.max(0, 1 - progress * 3);
124          }
125        }
126      }
127
128      // --- Shows section parallax bg ---
129      if (showsBg) {
130        const showsSection = showsBg.closest("section");
131        const rect = showsSection.getBoundingClientRect();
132        const viewH = window.innerHeight;
133        if (rect.top < viewH && rect.bottom > 0) {
134          const sectionProgress = (viewH - rect.top) / (viewH + rect.height);
135          showsBg.style.transform = `translateY(${(sectionProgress - 0.5) * -80}px)`;
136        }
137      }
138    }
139
140    // --- Active nav link ---
141    const scrollCheck = scrollY + 200;
142    sections.forEach((section) => {
143      const top = section.offsetTop;
144      const height = section.offsetHeight;
145      const id = section.getAttribute("id");
146      if (scrollCheck >= top && scrollCheck < top + height) {
147        navLinks.forEach((link) => {
148          link.style.color = "";
149          if (link.getAttribute("href") === `#${id}`) {
150            link.style.color = "#ffffff";
151          }
152        });
153      }
154    });
155
156    ticking = false;
157  };
158
159  window.addEventListener("scroll", onScroll, { passive: true });
160  onScroll(); // initial call
161
162  // --- Smooth scroll for anchor links ---
163  document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
164    anchor.addEventListener("click", (e) => {
165      const target = document.querySelector(anchor.getAttribute("href"));
166      if (target) {
167        e.preventDefault();
168        const offset = nav.offsetHeight;
169        const top =
170          target.getBoundingClientRect().top + window.scrollY - offset;
171        window.scrollTo({ top, behavior: "smooth" });
172      }
173    });
174  });
175
176  // --- Staggered children animation for grids ---
177  const staggerContainers = document.querySelectorAll("[data-stagger]");
178  const staggerObserver = new IntersectionObserver(
179    (entries) => {
180      entries.forEach((entry) => {
181        if (entry.isIntersecting) {
182          const children = entry.target.children;
183          Array.from(children).forEach((child, i) => {
184            child.style.transitionDelay = `${i * 0.12}s`;
185            child.classList.add("visible");
186          });
187          staggerObserver.unobserve(entry.target);
188        }
189      });
190    },
191    {
192      threshold: 0.1,
193      rootMargin: "0px 0px -40px 0px",
194    },
195  );
196
197  staggerContainers.forEach((el) => staggerObserver.observe(el));
198
199  // --- Staggered children animation for mobile-only containers ---
200  const isMobile = window.matchMedia("(max-width: 768px)").matches;
201  const mobileStaggerContainers = document.querySelectorAll(
202    "[data-stagger-mobile]",
203  );
204
205  if (isMobile) {
206    const mobileStaggerObserver = new IntersectionObserver(
207      (entries) => {
208        entries.forEach((entry) => {
209          if (entry.isIntersecting) {
210            const children = entry.target.children;
211            Array.from(children).forEach((child, i) => {
212              child.style.transitionDelay = `${i * 0.18}s`;
213              child.classList.add("visible");
214            });
215            mobileStaggerObserver.unobserve(entry.target);
216          }
217        });
218      },
219      {
220        threshold: 0.05,
221        rootMargin: "0px 0px -20px 0px",
222      },
223    );
224
225    mobileStaggerContainers.forEach((el) => mobileStaggerObserver.observe(el));
226  }
227
228  // --- Magnetic hover effect on buttons ---
229  if (!prefersReducedMotion) {
230    document.querySelectorAll(".btn").forEach((btn) => {
231      btn.addEventListener("mousemove", (e) => {
232        const rect = btn.getBoundingClientRect();
233        const x = e.clientX - rect.left - rect.width / 2;
234        const y = e.clientY - rect.top - rect.height / 2;
235        btn.style.transform = `translate(${x * 0.15}px, ${y * 0.15}px)`;
236      });
237
238      btn.addEventListener("mouseleave", () => {
239        btn.style.transform = "";
240      });
241    });
242  }
243
244  // --- Tilt effect on member cards ---
245  if (!prefersReducedMotion) {
246    document.querySelectorAll(".member-card").forEach((card) => {
247      card.addEventListener("mousemove", (e) => {
248        const rect = card.getBoundingClientRect();
249        const x = (e.clientX - rect.left) / rect.width;
250        const y = (e.clientY - rect.top) / rect.height;
251        const rotateX = (0.5 - y) * 10;
252        const rotateY = (x - 0.5) * 10;
253        card.style.transform = `perspective(600px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
254      });
255
256      card.addEventListener("mouseleave", () => {
257        card.style.transform = "";
258      });
259    });
260  }
261
262  // --- Counter animation for stats ---
263  const statNumbers = document.querySelectorAll(".about-stat h3");
264  const counterObserver = new IntersectionObserver(
265    (entries) => {
266      entries.forEach((entry) => {
267        if (entry.isIntersecting) {
268          const el = entry.target;
269          const text = el.textContent.trim();
270          const hasPlus = text.includes("+");
271          const target = parseInt(text.replace(/\D/g, ""));
272
273          if (isNaN(target)) return;
274
275          let current = 0;
276          const duration = 1500;
277          const step = target / (duration / 16);
278          const start = performance.now();
279
280          const animate = (now) => {
281            const elapsed = now - start;
282            const progress = Math.min(elapsed / duration, 1);
283            // Ease out cubic
284            const eased = 1 - Math.pow(1 - progress, 3);
285            current = Math.floor(eased * target);
286            el.textContent = current + (hasPlus ? "+" : "");
287            if (progress < 1) {
288              requestAnimationFrame(animate);
289            } else {
290              el.textContent = target + (hasPlus ? "+" : "");
291            }
292          };
293
294          requestAnimationFrame(animate);
295          counterObserver.unobserve(el);
296        }
297      });
298    },
299    { threshold: 0.5 },
300  );
301
302  statNumbers.forEach((el) => counterObserver.observe(el));
303
304  // --- Auto-update copyright year ---
305  const yearEl = document.getElementById("copyright-year");
306  if (yearEl) yearEl.textContent = new Date().getFullYear();
307
308  // --- Hero video toggle (press H to swap animation off/on, demo aid) ---
309  const heroVideoEl = document.querySelector(".hero-video");
310  if (heroVideoEl && heroVideoEl.tagName === "VIDEO") {
311    document.addEventListener("keydown", (e) => {
312      if (e.key.toLowerCase() !== "h") return;
313      if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
314      if (e.target.matches("input, textarea, select, [contenteditable]"))
315        return;
316      const off = heroVideoEl.classList.toggle("hero-video-off");
317      if (off) heroVideoEl.pause();
318      else heroVideoEl.play().catch(() => {});
319    });
320  }
321
322  // --- Back to top button ---
323  const backToTop = document.querySelector(".back-to-top");
324  if (backToTop) {
325    const toggleBackToTop = () => {
326      backToTop.classList.toggle("visible", window.scrollY > 400);
327    };
328    window.addEventListener("scroll", toggleBackToTop, { passive: true });
329    toggleBackToTop();
330    backToTop.addEventListener("click", () => {
331      window.scrollTo({ top: 0, behavior: "smooth" });
332    });
333  }
334});

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.