<p>&nbsp;</p> <p><link href="https://fonts.googleapis.com" rel="preconnect" /> <link href="https://fonts.gstatic.com" rel="preconnect" crossorigin="" /> <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&amp;family=DM+Sans:wght@300;400;500&amp;display=swap" rel="stylesheet" /></p> <style> /* ============================================================ REPET TOFU CAT LITTER — Full-Screen Video Overlay Landing Page ============================================================ */ :root { --bg: #000000; --accent: #d4b896; --accent-gold: #c9a96e; --text-primary: #f0ece4; --text-muted: #7a7470; --text-dim: #3d3a38; --font-display: 'Cormorant Garamond', serif; --font-body: 'DM Sans', sans-serif; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { background: var(--bg); } body { background: var(--bg); color: var(--text-primary); font-family: var(--font-body); font-weight: 300; overflow-x: hidden; } a { color: inherit; text-decoration: none; } /* ---- Loader ---- */ #loader { position: fixed; inset: 0; z-index: 1000; background: var(--bg); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; transition: opacity 0.7s ease; } #loader.hidden { opacity: 0; pointer-events: none; } .loader-brand { font-family: var(--font-display); font-size: clamp(1.6rem, 4vw, 2.4rem); font-weight: 300; letter-spacing: 0.5em; color: var(--accent); text-transform: uppercase; } .loader-bar-wrap { width: clamp(200px, 28vw, 340px); height: 1px; background: var(--text-dim); overflow: hidden; } #loader-bar { height: 100%; width: 0%; background: var(--accent); transition: width 0.1s linear; } #loader-percent { font-size: 0.68rem; letter-spacing: 0.22em; color: var(--text-muted); } /* ---- Header ---- */ .site-header { position: fixed; top: 0; left: 0; right: 0; z-index: 200; padding: 1.4rem 3rem; mix-blend-mode: difference; } nav { display: flex; align-items: center; justify-content: space-between; } .nav-logo { font-family: var(--font-display); font-size: 1rem; font-weight: 600; letter-spacing: 0.45em; color: #fff; } .nav-links { display: flex; list-style: none; gap: 2rem; align-items: center; } .nav-links a { font-size: 0.7rem; font-weight: 400; letter-spacing: 0.15em; text-transform: uppercase; color: #fff; opacity: 0.55; transition: opacity 0.3s; } .nav-links a:hover { opacity: 1; } .nav-story { opacity: 0.75 !important; } .nav-cta { opacity: 1 !important; border: 1px solid rgba(255,255,255,0.45) !important; padding: 0.45rem 1.1rem !important; border-radius: 2px; transition: background 0.3s, color 0.3s !important; } .nav-cta:hover { background: #fff !important; color: #000 !important; } /* ---- Canvas — full-screen, visible from load ---- */ .canvas-wrap { position: fixed; inset: 0; z-index: 10; } canvas { display: block; width: 100%; height: 100%; } /* ---- Dark Overlay ---- */ #dark-overlay { position: fixed; inset: 0; z-index: 15; background: #000; opacity: 0; pointer-events: none; } /* ---- Marquee ---- */ .marquee-wrap { position: fixed; bottom: 2.5rem; left: 0; right: 0; z-index: 20; overflow: hidden; opacity: 0; pointer-events: none; } .marquee-text { font-family: var(--font-display); font-size: clamp(4rem, 8vw, 7.5rem); font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: transparent; -webkit-text-stroke: 1px rgba(212, 184, 150, 0.18); white-space: nowrap; will-change: transform; line-height: 1; } /* ---- Hero — full-screen transparent overlay, text left ---- */ .hero-standalone { position: relative; z-index: 100; width: 100%; height: 100vh; background: transparent; display: flex; flex-direction: column; justify-content: center; padding-left: 6vw; padding-right: 50vw; overflow: hidden; } /* Gradient so hero text is readable over any video frame */ .hero-standalone::before { content: ''; position: absolute; inset: 0; background: linear-gradient(to right, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.4) 50%, transparent 100%); z-index: 0; pointer-events: none; } .hero-content { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 1.6rem; } .hero-heading { font-family: var(--font-display); font-weight: 700; line-height: 0.88; color: var(--text-primary); display: flex; flex-direction: column; gap: 0.08em; } .hero-brand { font-size: clamp(5rem, 12vw, 11rem); letter-spacing: 0.06em; text-transform: uppercase; text-shadow: 0 2px 30px rgba(0,0,0,0.6); } .hero-sub { font-size: clamp(1.3rem, 3.2vw, 3.2rem); letter-spacing: 0.22em; font-weight: 300; color: var(--accent); text-transform: uppercase; } .hero-tagline { font-family: var(--font-display); font-style: italic; font-size: clamp(1.1rem, 2vw, 1.8rem); font-weight: 300; color: rgba(240,236,228,0.65); letter-spacing: 0.02em; line-height: 1.5; } .section-label { display: block; font-size: 0.64rem; font-weight: 400; letter-spacing: 0.24em; text-transform: uppercase; color: var(--text-muted); font-family: var(--font-body); } .hero-content .section-label { color: var(--accent-gold); opacity: 0.85; letter-spacing: 0.3em; } .scroll-indicator { position: absolute; bottom: 2.8rem; left: 6vw; display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; opacity: 0.35; animation: bob 2s ease-in-out infinite; z-index: 1; } .scroll-indicator span { font-size: 0.58rem; letter-spacing: 0.28em; text-transform: uppercase; } .scroll-indicator svg { width: 15px; height: 15px; } @keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(6px); } } /* ---- Scroll Container ---- */ #scroll-container { position: relative; height: 700vh; z-index: 20; } /* ---- Scroll Sections ---- */ .scroll-section { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); pointer-events: none; opacity: 0; visibility: hidden; z-index: 25; } .scroll-section.is-visible { opacity: 1; visibility: visible; pointer-events: auto; } /* Left-zone text with gradient backdrop for video contrast */ .align-left { padding-left: 6vw; padding-right: 50vw; /* Gradient fades left-to-transparent so text reads on any video frame */ background: linear-gradient(to right, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.45) 55%, transparent 100%); } .align-left .section-inner { max-width: 44vw; } .section-inner { display: flex; flex-direction: column; gap: 1.5rem; } .section-heading { font-family: var(--font-display); font-size: clamp(3.2rem, 6vw, 6.5rem); font-weight: 700; line-height: 0.95; color: var(--text-primary); letter-spacing: -0.01em; text-shadow: 0 2px 24px rgba(0,0,0,0.5); } .section-heading em { font-style: italic; color: var(--accent); font-weight: 300; } .section-body { font-size: clamp(0.9rem, 1.2vw, 1.05rem); line-height: 1.82; color: rgba(240,236,228,0.72); font-weight: 300; } /* Feature list styling */ .section-list { list-style: none; display: flex; flex-direction: column; gap: 0.7rem; padding: 0; } .section-list li { font-size: clamp(0.82rem, 1.1vw, 0.98rem); line-height: 1.6; color: rgba(240,236,228,0.68); font-weight: 300; padding-left: 1.2em; position: relative; } .section-list li::before { content: '—'; position: absolute; left: 0; color: var(--accent); font-weight: 400; } .section-list li strong { color: var(--text-primary); font-weight: 500; } /* ---- Stats Section ---- */ .section-stats { padding: 0 8vw; display: flex; align-items: center; justify-content: center; left: 0; right: 0; } .stats-grid { display: flex; gap: clamp(3rem, 8vw, 9rem); align-items: flex-end; } .stat { display: flex; flex-direction: column; align-items: center; gap: 0.65rem; } .stat-row { display: flex; align-items: flex-start; line-height: 1; } .stat-number { font-family: var(--font-display); font-size: clamp(4rem, 9vw, 9rem); font-weight: 700; color: var(--accent); line-height: 0.9; letter-spacing: -0.02em; } .stat-suffix { font-family: var(--font-display); font-size: clamp(1.4rem, 3vw, 3rem); font-weight: 300; color: var(--accent); padding-top: 0.35em; margin-left: 0.08em; opacity: 0.75; } .stat-label { font-size: 0.6rem; letter-spacing: 0.24em; text-transform: uppercase; color: var(--text-muted); } /* ---- CTA Section ---- */ .section-cta { left: 6vw; right: auto; width: auto; max-width: 46vw; padding: 0; text-align: left; transform: translateY(-50%); background: transparent; } .cta-inner { align-items: flex-start; text-align: left; } .cta-heading { font-size: clamp(3rem, 5.5vw, 6rem); } .cta-buttons { display: flex; gap: 1.2rem; align-items: center; margin-top: 0.4rem; flex-wrap: wrap; } .cta-button { display: inline-block; padding: 1.1rem 2.8rem; background: var(--accent); color: #000; font-family: var(--font-body); font-size: 0.78rem; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; border-radius: 2px; cursor: pointer; transition: background 0.3s, transform 0.2s; } .cta-button:hover { background: var(--accent-gold); transform: translateY(-2px); } .cta-button-ghost { display: inline-block; padding: 1.1rem 0; color: var(--accent); font-family: var(--font-body); font-size: 0.78rem; font-weight: 400; letter-spacing: 0.18em; text-transform: uppercase; opacity: 0.8; transition: opacity 0.3s, letter-spacing 0.3s; } .cta-button-ghost:hover { opacity: 1; letter-spacing: 0.25em; } .cta-note { font-size: 0.66rem; letter-spacing: 0.1em; color: var(--text-muted); margin-top: 0.3rem; } /* ---- Mobile ---- */ @media (max-width: 768px) { .site-header { padding: 1.2rem 1.5rem; } .nav-links li:nth-child(1), .nav-links li:nth-child(2) { display: none; } .hero-standalone { padding-left: 5vw; padding-right: 5vw; justify-content: flex-end; padding-bottom: 10vh; } .hero-standalone::before { background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.3) 60%, transparent 100%); } .hero-brand { font-size: clamp(3.5rem, 18vw, 7rem); } .hero-sub { font-size: clamp(1rem, 4.5vw, 1.8rem); } .align-left { padding-left: 5vw; padding-right: 5vw; background: rgba(0,0,0,0.78); } .align-left .section-inner { max-width: 100%; padding: 1.6rem; } #scroll-container { height: 520vh; } .stats-grid { gap: 2.5rem; flex-wrap: wrap; justify-content: center; } .section-cta { left: 5vw; max-width: 92vw; } .marquee-text { font-size: 14vw; } } </style> <!-- LOADER --> <div id="loader"> <div class="loader-brand">REPET</div> <div class="loader-bar-wrap"> <div id="loader-bar"></div> </div> <div id="loader-percent">0%</div> </div> <!-- FIXED HEADER --><header class="site-header"><nav><span class="nav-logo">REPET</span> <ul class="nav-links"> <li><a href="https://repet.store/pages/why-tofu.html">Why Tofu?</a></li> <li><a href="https://repet.store/pages/reviews.html">Reviews</a></li> <li><a class="nav-story" href="https://repet.store/pages/brand.html">Brand Story</a></li> <li><a class="nav-cta" href="https://repet.store/pages/product.html">Shop Now</a></li> </ul> </nav></header><!-- CANVAS — full-screen, visible immediately --> <div id="canvas-wrap" class="canvas-wrap"><canvas id="canvas"></canvas></div> <!-- DARK OVERLAY --> <div id="dark-overlay"></div> <!-- MARQUEE --> <div id="marquee-wrap" class="marquee-wrap" data-scroll-speed="-30"> <div class="marquee-text">PEA FIBERS &nbsp;&middot;&nbsp; CORN STARCH &nbsp;&middot;&nbsp; GUAR GUM &nbsp;&middot;&nbsp; 99.9% DUST FREE &nbsp;&middot;&nbsp; FLUSHABLE &nbsp;&middot;&nbsp; PEA FIBERS &nbsp;&middot;&nbsp; CORN STARCH &nbsp;&middot;&nbsp; GUAR GUM &nbsp;&middot;&nbsp; 99.9% DUST FREE &nbsp;&middot;&nbsp; FLUSHABLE &nbsp;&middot;&nbsp;</div> </div> <!-- HERO — transparent overlay, left-side text --> <section class="hero-standalone"> <div class="hero-content"><span class="section-label">TikTok's Top Pick &middot; Born in London</span> <h1 class="hero-heading"><span class="hero-word hero-brand">REPET</span> <span class="hero-word hero-sub">TOFU CAT LITTER</span></h1> <p class="hero-tagline">S-Grade purity.<br />Zero compromise.</p> </div> <div class="scroll-indicator">Scroll</div> </section> <!-- SCROLL CONTAINER --> <div id="scroll-container"><!-- SECTION 1: Raw Materials — slide-left, align-left --> <section class="scroll-section section-content align-left" data-enter="20" data-leave="36" data-animation="slide-left"> <div class="section-inner"><span class="section-label">001 / Raw Materials</span> <h2 class="section-heading">Food-grade<br /><em>all the way.</em></h2> <p class="section-body">Pea fibers, corn starch, guar gum. S-Grade natural ingredients held to food-safety standards &mdash; the same purity your kitchen demands. Zero clay. Zero silica. Zero toxins.</p> </div> </section> <!-- SECTION 2: Product Features — fade-up, align-left --> <section class="scroll-section section-content align-left" data-enter="38" data-leave="53" data-animation="fade-up"> <div class="section-inner"><span class="section-label">002 / Formula</span> <h2 class="section-heading">Five-way<br /><em>protection.</em></h2> <ul class="section-list"> <li><strong>Strong adsorption</strong> &mdash; clumps on contact, easy to scoop</li> <li><strong>Efficient deodorising</strong> &mdash; leaves no odor, fresher home</li> <li><strong>Safe &amp; hygienic</strong> &mdash; non-toxic, even food-safe if ingested</li> <li><strong>Low dust</strong> &mdash; professional process, clean air &amp; paws</li> <li><strong>Flushable</strong> &mdash; dissolves in water, pour straight down the toilet</li> </ul> </div> </section> <!-- SECTION 3: Stats — stagger-up, dark overlay --> <section class="scroll-section section-stats" data-enter="54" data-leave="69" data-animation="stagger-up"> <div class="stats-grid"> <div class="stat"> <div class="stat-row"><span class="stat-number" data-value="99" data-decimals="0">0</span> <span class="stat-suffix">.9%</span></div> <span class="stat-label">Dust-Free</span></div> <div class="stat"> <div class="stat-row"><span class="stat-number" data-value="7" data-decimals="0">0</span> <span class="stat-suffix">L</span></div> <span class="stat-label">Per Bag</span></div> <div class="stat"> <div class="stat-row"><span class="stat-number" data-value="2" data-decimals="0">0</span> <span class="stat-suffix">yr</span></div> <span class="stat-label">Shelf Life</span></div> </div> </section> <!-- SECTION 4: Eco / Flushable — clip-reveal, align-left --> <section class="scroll-section section-content align-left" data-enter="69" data-leave="82" data-animation="clip-reveal"> <div class="section-inner"><span class="section-label">003 / Eco-Friendly</span> <h2 class="section-heading">Scoop.<br /><em>Flush. Done.</em></h2> <p class="section-body">Lay 4&ndash;6 cm in a clean litter box. After use, scoop the clumps straight into the toilet &mdash; tofu litter dissolves completely. Replace fully once a month. No landfill. No plastic bags. No unnecessary waste.</p> </div> </section> <!-- CTA — scale-up, persist --> <section class="scroll-section section-cta" data-enter="83" data-leave="97" data-animation="scale-up" data-persist="true"> <div class="section-inner cta-inner"><span class="section-label">All-Natural &middot; Safe &middot; Flushable &middot; Born in London</span> <h2 class="section-heading cta-heading">Give your cat<br />the <em>pure</em> choice.</h2> <div class="cta-buttons"><a class="cta-button" href="https://repet.store/pages/product.html">Shop REPET Now</a> <a class="cta-button-ghost" href="https://repet.store/pages/brand.html">Our Story &rarr;</a></div> <p class="cta-note">Product Spec: 7L &middot; Shelf Life: 2 years &middot; Ultron Digital &amp; Technology Co., Ltd</p> </div> </section> </div> <!-- end #scroll-container --> <script src="https://cdn.jsdelivr.net/npm/lenis@1/dist/lenis.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/ScrollTrigger.min.js"></script> <script> /* ============================================================ TOFU CAT LITTER — Scroll-Driven Animation Engine ============================================================ */ const FRAME_COUNT = 151; const FRAME_SPEED = 1.0; // animation spans full scroll so video shows top to bottom const IMAGE_SCALE = 0.88; const TOTAL_HEIGHT = 700; // vh — shorter page, tighter pacing const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const canvasWrap = document.getElementById('canvas-wrap'); const overlay = document.getElementById('dark-overlay'); const loader = document.getElementById('loader'); const loaderBar = document.getElementById('loader-bar'); const loaderPct = document.getElementById('loader-percent'); const heroSection = document.querySelector('.hero-standalone'); const scrollCont = document.getElementById('scroll-container'); const marqueeWrap = document.getElementById('marquee-wrap'); const frames = new Array(FRAME_COUNT); let currentFrame = 0; let bgColor = '#000000'; let bgSampleCache = {}; // ---- Canvas DPR Setup ---- function resizeCanvas() { const dpr = window.devicePixelRatio || 1; canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // reset transform before applying DPR if (frames[currentFrame]) drawFrame(currentFrame); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); // ---- Background Color Sampler ---- function sampleBgColor(img) { const sampleCanvas = document.createElement('canvas'); sampleCanvas.width = img.naturalWidth; sampleCanvas.height = img.naturalHeight; const sCtx = sampleCanvas.getContext('2d'); sCtx.drawImage(img, 0, 0); const iw = img.naturalWidth; const ih = img.naturalHeight; const samples = []; const pts = [ [0, 0], [iw - 1, 0], [0, ih - 1], [iw - 1, ih - 1], [0, Math.floor(ih / 2)], [iw - 1, Math.floor(ih / 2)], [Math.floor(iw / 2), 0], [Math.floor(iw / 2), ih - 1] ]; pts.forEach(([x, y]) => { try { const d = sCtx.getImageData(x, y, 1, 1).data; samples.push([d[0], d[1], d[2]]); } catch (e) {} }); if (!samples.length) return '#000000'; const avg = samples.reduce((a, b) => [a[0]+b[0], a[1]+b[1], a[2]+b[2]], [0,0,0]) .map(v => Math.round(v / samples.length)); // darken slightly to help blend with pure-black UI const darken = 0.6; const r = Math.round(avg[0] * darken); const g = Math.round(avg[1] * darken); const b = Math.round(avg[2] * darken); return `rgb(${r},${g},${b})`; } // ---- Draw a single image onto canvas (full-screen cover, centered for 16:9 video) ---- function drawImageOnCanvas(img, alpha) { const cw = window.innerWidth; const ch = window.innerHeight; const iw = img.naturalWidth; const ih = img.naturalHeight; // Cover mode: fill entire viewport, keep aspect ratio, center the image const scale = Math.max(cw / iw, ch / ih); const dw = iw * scale; const dh = ih * scale; const dx = (cw - dw) / 2; const dy = (ch - dh) / 2; ctx.globalAlpha = alpha; ctx.drawImage(img, dx, dy, dw, dh); ctx.globalAlpha = 1.0; } // ---- Draw Frame ---- function drawFrame(index) { const img = frames[index]; if (!img || !img.complete) return; const cw = window.innerWidth; const ch = window.innerHeight; ctx.fillStyle = bgColor; ctx.fillRect(0, 0, cw, ch); drawImageOnCanvas(img, 1.0); } // ---- Two-Phase Frame Preloader ---- function loadFrames() { const FIRST_BATCH = 12; let loaded = 0; function updateLoader(n) { loaded = n; const pct = Math.round((loaded / FRAME_COUNT) * 100); loaderBar.style.width = pct + '%'; loaderPct.textContent = pct + '%'; } function frameUrl(i) { return `https://img-va.myshopline.com/image/store/1749217664136/frame-${String(i).padStart(4, '0')}.webp`; } function loadOne(i) { return new Promise(resolve => { const img = new Image(); img.onload = () => { frames[i - 1] = img; if ((i - 1) % 20 === 0 && !bgSampleCache[i]) { bgSampleCache[i] = true; bgColor = sampleBgColor(img); } resolve(); }; img.onerror = resolve; img.src = frameUrl(i); }); } // Phase 1: first batch — fast first paint const phase1 = Array.from({length: FIRST_BATCH}, (_, k) => loadOne(k + 1)); Promise.all(phase1).then(() => { updateLoader(FIRST_BATCH); drawFrame(0); // Phase 2: remaining frames in background let remaining = FIRST_BATCH + 1; function loadNext() { if (remaining > FRAME_COUNT) { // All done — hide loader updateLoader(FRAME_COUNT); setTimeout(() => { loader.classList.add('hidden'); setTimeout(() => { loader.style.display = 'none'; }, 700); initAll(); }, 300); return; } loadOne(remaining).then(() => { updateLoader(remaining); remaining++; loadNext(); }); } loadNext(); }); } // ---- Init All Systems After Load ---- function initAll() { initLenis(); initHeroEntrance(); initHeroTransition(); initFrameScroll(); initDarkOverlay(0.52, 0.70); initMarquee(); initSections(); initCounters(); } // ---- Hero Entrance (stagger after load, canvas visible immediately) ---- function initHeroEntrance() { // Canvas is visible from frame 1 — no circle-wipe needed canvasWrap.style.clipPath = 'none'; const words = document.querySelectorAll('.hero-word'); const label = document.querySelector('.hero-content .section-label'); const tagline = document.querySelector('.hero-tagline'); const scrollInd = document.querySelector('.scroll-indicator'); gsap.set([label, words, tagline, scrollInd], { opacity: 0, y: 24 }); const tl = gsap.timeline({ delay: 0.3 }); tl.to(label, { opacity: 1, y: 0, duration: 0.7, ease: 'power3.out' }) .to(words, { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out', stagger: 0.14 }, '-=0.3') .to(tagline, { opacity: 1, y: 0, duration: 0.7, ease: 'power3.out' }, '-=0.3') .to(scrollInd,{ opacity: 0.35, y: 0, duration: 0.6, ease: 'power2.out' }, '-=0.2'); } // ---- 6a. Lenis Smooth Scroll ---- function initLenis() { const lenis = new Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), smoothWheel: true }); lenis.on('scroll', ScrollTrigger.update); gsap.ticker.add((time) => lenis.raf(time * 1000)); gsap.ticker.lagSmoothing(0); } // ---- Hero Fade-Out on Scroll ---- function initHeroTransition() { ScrollTrigger.create({ trigger: scrollCont, start: 'top top', end: 'bottom bottom', scrub: true, onUpdate: (self) => { const p = self.progress; // Hero text fades out in first 12% of scroll heroSection.style.opacity = Math.max(0, 1 - p * 12); } }); } // ---- 6d. Frame-to-Scroll Binding ---- function initFrameScroll() { ScrollTrigger.create({ trigger: scrollCont, start: 'top top', end: 'bottom bottom', scrub: true, onUpdate: (self) => { const accelerated = Math.min(self.progress * FRAME_SPEED, 1); const index = Math.min(Math.floor(accelerated * FRAME_COUNT), FRAME_COUNT - 1); if (index !== currentFrame) { currentFrame = index; requestAnimationFrame(() => { if (frames[currentFrame]) { if (currentFrame % 20 === 0 && frames[currentFrame]) { const key = currentFrame + 1; if (!bgSampleCache[key]) { bgSampleCache[key] = true; bgColor = sampleBgColor(frames[currentFrame]); } } drawFrame(currentFrame); } }); } } }); } // ---- 6h. Dark Overlay ---- function initDarkOverlay(enter, leave) { const fadeRange = 0.04; ScrollTrigger.create({ trigger: scrollCont, start: 'top top', end: 'bottom bottom', scrub: true, onUpdate: (self) => { const p = self.progress; let opacity = 0; if (p >= enter - fadeRange && p <= enter) { opacity = (p - (enter - fadeRange)) / fadeRange; } else if (p > enter && p < leave) { opacity = 0.90; } else if (p >= leave && p <= leave + fadeRange) { opacity = 0.90 * (1 - (p - leave) / fadeRange); } overlay.style.opacity = opacity; } }); } // ---- 6g. Horizontal Marquee ---- function initMarquee() { const speed = parseFloat(marqueeWrap.dataset.scrollSpeed) || -28; // Fade marquee in when canvas is visible, out near end ScrollTrigger.create({ trigger: scrollCont, start: 'top top', end: 'bottom bottom', scrub: true, onUpdate: (self) => { const p = self.progress; let mOpacity = 0; if (p > 0.12 && p < 0.85) { mOpacity = Math.min(1, (p - 0.12) / 0.06) * Math.min(1, (0.85 - p) / 0.06); } marqueeWrap.style.opacity = mOpacity; } }); gsap.to(marqueeWrap.querySelector('.marquee-text'), { xPercent: speed, ease: 'none', scrollTrigger: { trigger: scrollCont, start: 'top top', end: 'bottom bottom', scrub: true } }); } // ---- 6e. Section Animation System ---- function initSections() { const sections = document.querySelectorAll('.scroll-section'); const containerHeight = scrollCont.offsetHeight; sections.forEach(section => { const enter = parseFloat(section.dataset.enter) / 100; const leave = parseFloat(section.dataset.leave) / 100; const type = section.dataset.animation; const persist = section.dataset.persist === 'true'; const mid = (enter + leave) / 2; // Position section at midpoint of its enter/leave range within 900vh container section.style.top = (mid * TOTAL_HEIGHT) + 'vh'; const children = section.querySelectorAll( '.section-label, .section-heading, .section-body, .section-note, .section-list li, .cta-button, .cta-button-ghost, .cta-note, .stat' ); // Build entrance timeline const tl = gsap.timeline({ paused: true }); switch (type) { case 'slide-left': gsap.set(children, { x: -80, opacity: 0 }); tl.to(children, { x: 0, opacity: 1, stagger: 0.14, duration: 0.9, ease: 'power3.out' }); break; case 'slide-right': gsap.set(children, { x: 80, opacity: 0 }); tl.to(children, { x: 0, opacity: 1, stagger: 0.14, duration: 0.9, ease: 'power3.out' }); break; case 'fade-up': gsap.set(children, { y: 50, opacity: 0 }); tl.to(children, { y: 0, opacity: 1, stagger: 0.12, duration: 0.9, ease: 'power3.out' }); break; case 'stagger-up': gsap.set(children, { y: 60, opacity: 0 }); tl.to(children, { y: 0, opacity: 1, stagger: 0.15, duration: 0.8, ease: 'power3.out' }); break; case 'scale-up': gsap.set(children, { scale: 0.85, opacity: 0 }); tl.to(children, { scale: 1, opacity: 1, stagger: 0.12, duration: 1.0, ease: 'power2.out' }); break; case 'clip-reveal': gsap.set(children, { clipPath: 'inset(100% 0 0 0)', opacity: 0 }); tl.to(children, { clipPath: 'inset(0% 0 0 0)', opacity: 1, stagger: 0.15, duration: 1.2, ease: 'power4.inOut' }); break; default: gsap.set(children, { y: 40, opacity: 0 }); tl.to(children, { y: 0, opacity: 1, stagger: 0.12, duration: 0.9, ease: 'power3.out' }); } let isVisible = false; let hasAnimated = false; ScrollTrigger.create({ trigger: scrollCont, start: 'top top', end: 'bottom bottom', scrub: false, onUpdate: (self) => { const p = self.progress; const inRange = p >= enter && p <= leave; if (inRange && !isVisible) { isVisible = true; section.classList.add('is-visible'); if (!hasAnimated) { hasAnimated = true; tl.play(); } else { tl.play(); } } else if (!inRange && isVisible) { if (!persist) { isVisible = false; section.classList.remove('is-visible'); tl.reverse(); } } } }); }); } // ---- 6f. Counter Animations ---- function initCounters() { const statSection = document.querySelector('.section-stats'); if (!statSection) return; const enter = parseFloat(statSection.dataset.enter) / 100; const leave = parseFloat(statSection.dataset.leave) / 100; let countersPlayed = false; let countersReversed = true; const counterEls = document.querySelectorAll('.stat-number'); function playCounters() { counterEls.forEach(el => { const target = parseFloat(el.dataset.value); const decimals = parseInt(el.dataset.decimals || '0'); gsap.fromTo(el, { textContent: 0 }, { textContent: target, duration: 2.2, ease: 'power1.out', snap: { textContent: decimals === 0 ? 1 : 0.01 }, onUpdate: function() { const v = parseFloat(this.targets()[0].textContent) || 0; el.textContent = decimals === 0 ? Math.round(v) : v.toFixed(decimals); } } ); }); } function resetCounters() { counterEls.forEach(el => { el.textContent = '0'; }); } ScrollTrigger.create({ trigger: scrollCont, start: 'top top', end: 'bottom bottom', scrub: false, onUpdate: (self) => { const p = self.progress; const inRange = p >= enter && p <= leave; if (inRange && !countersPlayed) { countersPlayed = true; countersReversed = false; playCounters(); } else if (!inRange && !countersReversed) { countersPlayed = false; countersReversed = true; resetCounters(); } } }); } // ---- Kick Off ---- loadFrames(); </script>