/* ============================================ NolHitam — Software Engineering JS: GSAP ScrollTrigger + Lenis ============================================ */ // --- Wait for DOM --- document.addEventListener("DOMContentLoaded", () => { initLenis(); initNav(); initHero(); initServices(); initProcess(); initCaseStudies(); initCTA(); }); // ============================================ // LENIS (Smooth Scroll) // ============================================ function initLenis() { const lenis = new Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), orientation: "vertical", smoothWheel: true, wheelMultiplier: 1, touchMultiplier: 1.5, }); lenis.on("scroll", ScrollTrigger.update); gsap.ticker.add((time) => { lenis.raf(time * 1000); }); gsap.ticker.lagSmoothing(0); window.__lenis = lenis; } // ============================================ // NAV // ============================================ function initNav() { const nav = document.getElementById("nav"); const toggle = document.querySelector(".nav__toggle"); const links = document.querySelector(".nav__links"); const closeBtn = document.querySelector(".nav__close"); const navLinks = document.querySelectorAll(".nav__link"); ScrollTrigger.create({ start: "top -80", onUpdate: (self) => { if (self.progress > 0) { nav.classList.add("nav--scrolled"); } else { nav.classList.remove("nav--scrolled"); } }, }); function toggleMenu(open) { const isOpen = open !== undefined ? open : !links.classList.contains("nav__links--open"); links.classList.toggle("nav__links--open", isOpen); nav.classList.toggle("nav--menu-open", isOpen); document.body.style.overflow = isOpen ? "hidden" : ""; document.body.style.height = isOpen ? "100%" : ""; } toggle.addEventListener("click", (e) => { e.stopPropagation(); toggleMenu(); }); // Close button if (closeBtn) { closeBtn.addEventListener("click", () => toggleMenu(false)); } // Close menu when clicking overlay background links.addEventListener("click", (e) => { if (e.target === links) { toggleMenu(false); } }); // Smooth scroll to sections using Lenis function scrollToSection(href) { const target = document.querySelector(href); if (target && window.__lenis) { window.__lenis.scrollTo(target, { duration: 1.4, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), }); } } navLinks.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const href = link.getAttribute("href"); toggleMenu(false); // Small delay to let menu close animation play setTimeout(() => scrollToSection(href), 100); }); }); // Also handle hero action buttons document.querySelectorAll(".hero__actions .btn").forEach((btn) => { btn.addEventListener("click", (e) => { const href = btn.getAttribute("href"); if (href && href.startsWith("#")) { e.preventDefault(); scrollToSection(href); } }); }); } // ============================================ // HERO // ============================================ function initHero() { const lines = document.querySelectorAll(".hero__line"); const subtitle = document.querySelector(".hero__subtitle"); const label = document.querySelector(".hero__label"); const actions = document.querySelector(".hero__actions"); const scroll = document.querySelector(".hero__scroll"); const stats = document.querySelector(".hero__stats"); const statNums = document.querySelectorAll(".hero__stat-num"); const glow = document.querySelector(".hero__glow"); const glowSecondary = document.querySelector(".hero__glow-secondary"); // Initial state gsap.set([label, subtitle, actions, scroll, stats], { opacity: 0, y: 24, }); gsap.set([glow, glowSecondary], { opacity: 0 }); // Split headline into individual words for a more dramatic reveal const title = document.querySelector(".hero__title"); const wordSpans = []; // Get each hero__line, split its words, and rebuild with word spans const originalLines = gsap.utils.toArray(".hero__line"); title.innerHTML = ""; originalLines.forEach((lineEl, li) => { const lineWords = lineEl.textContent.trim().split(/\s+/); lineWords.forEach((word, wi) => { const span = document.createElement("span"); span.className = "hero__word"; span.textContent = word; span.style.display = "inline-block"; span.style.opacity = "0"; span.style.transform = "translateY(30px) scale(0.9)"; span.style.filter = "blur(8px)"; title.appendChild(span); wordSpans.push(span); if (wi < lineWords.length - 1) { title.appendChild(document.createTextNode(" ")); } }); // Add line break after each original line except the last if (li < originalLines.length - 1) { title.appendChild(document.createElement("br")); } }); // Master timeline const tl = gsap.timeline({ defaults: { ease: "power3.out" } }); // Dramatic word-by-word reveal tl.to(wordSpans, { opacity: 1, y: 0, scale: 1, filter: "blur(0px)", duration: 0.7, stagger: 0.06, ease: "power4.out", }) // Glow fade in .to(glow, { opacity: 1, duration: 1.2, ease: "power2.out" }, "-=0.4") .to( glowSecondary, { opacity: 1, duration: 1.5, ease: "power2.out" }, "-=0.6", ) // Label .to(label, { opacity: 1, y: 0, duration: 0.6 }, "-=0.2") // Subtitle .to(subtitle, { opacity: 1, y: 0, duration: 0.6 }, "-=0.1") // Actions .to(actions, { opacity: 1, y: 0, duration: 0.6 }, "+=0.05") // Stats .to(stats, { opacity: 1, y: 0, duration: 0.6 }, "-=0.1") // Scroll cue .to(scroll, { opacity: 0.4, y: 0, duration: 0.6 }, "-=0.1"); // Animated counter for stats statNums.forEach((el) => { const target = parseInt(el.dataset.count); const duration = 2; ScrollTrigger.create({ trigger: el, start: "top 90%", once: true, onEnter: () => { gsap.fromTo( el, { textContent: 0 }, { textContent: target, duration: duration, ease: "power2.out", snap: { textContent: 1 }, onUpdate: () => { // Add + suffix for display el.textContent = Math.round(parseFloat(el.textContent)) + "+"; }, }, ); }, }); }); // Continuous subtle glow pulse after reveal tl.to( glow, { scale: 1.05, opacity: 0.7, duration: 3, repeat: -1, yoyo: true, ease: "sine.inOut", }, "+=0.5", ); tl.to( glowSecondary, { scale: 1.08, opacity: 0.6, duration: 4, repeat: -1, yoyo: true, ease: "sine.inOut", }, "+=0.5", ); // Subtle parallax on grid gsap.to(".hero__grid", { y: 80, ease: "none", scrollTrigger: { trigger: ".hero", start: "top top", end: "bottom top", scrub: 1, }, }); } // ============================================ // SERVICES // ============================================ function initServices() { const cards = gsap.utils.toArray(".service-card"); const sectionHeader = document.querySelector("#services .section__header"); // Section header reveal gsap.fromTo( sectionHeader, { opacity: 0, y: 40 }, { opacity: 1, y: 0, duration: 0.8, ease: "power3.out", scrollTrigger: { trigger: sectionHeader, start: "top 80%", toggleActions: "play none none reverse", }, }, ); // Cards staggered reveal with scale cards.forEach((card, i) => { const delay = parseFloat(card.dataset.delay) || 0; gsap.fromTo( card, { opacity: 0, y: 50, scale: 0.97 }, { opacity: 1, y: 0, scale: 1, duration: 0.7, delay: delay, ease: "power3.out", scrollTrigger: { trigger: card, start: "top 85%", toggleActions: "play none none reverse", }, }, ); }); } // ============================================ // PROCESS (Timeline) // ============================================ function initProcess() { const steps = gsap.utils.toArray(".process-step"); const progressEl = document.getElementById("processProgress"); const sectionHeader = document.querySelector("#process .section__header"); if (!steps.length) return; // Section header reveal gsap.fromTo( sectionHeader, { opacity: 0, y: 40 }, { opacity: 1, y: 0, duration: 0.8, ease: "power3.out", scrollTrigger: { trigger: sectionHeader, start: "top 80%", toggleActions: "play none none reverse", }, }, ); // Progress bar + active step tracking ScrollTrigger.create({ trigger: ".process__track", start: "top 60%", end: "bottom 40%", onUpdate: (self) => { const p = self.progress; progressEl.style.setProperty("height", p * 100 + "%"); steps.forEach((step, i) => { const stepStart = i / steps.length; const stepEnd = (i + 1) / steps.length; if (p >= stepStart && p < stepEnd) { step.classList.add("process-step--active"); } else { step.classList.remove("process-step--active"); } }); }, }); // Animate each step on enter with direction-aware entrance steps.forEach((step, i) => { const isEven = i % 2 === 0; const xFrom = isEven ? -30 : 30; // Content side const contentSide = step.querySelector( isEven ? ".process-step__side--left" : ".process-step__side--right", ); const visualSide = step.querySelector( isEven ? ".process-step__side--right" : ".process-step__side--left", ); if (contentSide) { gsap.fromTo( contentSide, { opacity: 0, x: xFrom }, { opacity: 1, x: 0, duration: 0.8, ease: "power3.out", scrollTrigger: { trigger: step, start: "top 75%", toggleActions: "play none none reverse", }, }, ); } if (visualSide) { gsap.fromTo( visualSide, { opacity: 0, x: -xFrom, scale: 0.9 }, { opacity: 1, x: 0, scale: 1, duration: 0.8, ease: "power3.out", scrollTrigger: { trigger: step, start: "top 75%", toggleActions: "play none none reverse", }, }, ); } }); } // ============================================ // CASE STUDIES // ============================================ function initCaseStudies() { const track = document.getElementById("workTrack"); const prevBtn = document.getElementById("workPrev"); const nextBtn = document.getElementById("workNext"); const dotsContainer = document.getElementById("workDots"); const cards = gsap.utils.toArray(".work-card"); const sectionHeader = document.querySelector("#work .section__header"); if (!track || !cards.length) return; // Section header reveal gsap.fromTo( sectionHeader, { opacity: 0, y: 40 }, { opacity: 1, y: 0, duration: 0.8, ease: "power3.out", scrollTrigger: { trigger: sectionHeader, start: "top 80%", toggleActions: "play none none reverse", }, }, ); let currentIndex = 0; const totalCards = cards.length; // Create dots cards.forEach((_, i) => { const dot = document.createElement("button"); dot.className = "work__dot" + (i === 0 ? " work__dot--active" : ""); dot.setAttribute("aria-label", `Go to project ${i + 1}`); dot.addEventListener("click", () => goTo(i)); dotsContainer.appendChild(dot); }); const dots = gsap.utils.toArray(".work__dot"); function goTo(index) { if (index < 0) index = 0; if (index >= totalCards) index = totalCards - 1; currentIndex = index; const card = cards[currentIndex]; const scrollLeft = card.offsetLeft - (track.parentElement.offsetWidth - card.offsetWidth) / 2; window.__lenis.scrollTo(track, { offset: -track.getBoundingClientRect().left + scrollLeft, duration: 0.8, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), }); dots.forEach((d, i) => { d.classList.toggle("work__dot--active", i === currentIndex); }); } // Sync dots on manual scroll/swipe track.addEventListener("scroll", () => { const trackRect = track.getBoundingClientRect(); const center = trackRect.left + trackRect.width / 2; let closestIndex = 0; let closestDist = Infinity; cards.forEach((card, i) => { const cardRect = card.getBoundingClientRect(); const cardCenter = cardRect.left + cardRect.width / 2; const dist = Math.abs(cardCenter - center); if (dist < closestDist) { closestDist = dist; closestIndex = i; } }); if (closestIndex !== currentIndex) { currentIndex = closestIndex; dots.forEach((d, i) => { d.classList.toggle("work__dot--active", i === currentIndex); }); } }); prevBtn.addEventListener("click", () => goTo(currentIndex - 1)); nextBtn.addEventListener("click", () => goTo(currentIndex + 1)); // Animate cards on scroll with clip reveal effect cards.forEach((card, i) => { gsap.fromTo( card, { opacity: 0, y: 40, scale: 0.96 }, { opacity: 1, y: 0, scale: 1, duration: 0.8, delay: i * 0.1, ease: "power3.out", scrollTrigger: { trigger: card, start: "top 85%", toggleActions: "play none none reverse", }, }, ); }); } // ============================================ // CTA // ============================================ function initCTA() { const title = document.querySelector(".cta__title"); const subtitle = document.querySelector(".cta__subtitle"); const btn = document.querySelector(".cta .btn"); const ctaSection = document.querySelector(".cta"); // Section entrance gsap.fromTo( [title, subtitle, btn], { opacity: 0, y: 40 }, { opacity: 1, y: 0, duration: 0.8, stagger: 0.15, ease: "power3.out", scrollTrigger: { trigger: ".cta__inner", start: "top 80%", toggleActions: "play none none reverse", }, }, ); // Subtle background glow pulse on scroll gsap.to(ctaSection, { backgroundPosition: "50% 100%", ease: "none", scrollTrigger: { trigger: ctaSection, start: "top bottom", end: "bottom top", scrub: 1, }, }); }