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.