/* ============================================= Skiply.ai — Connected Systems section Scan-the-shoe → distribute-to-tree motion sequence ============================================= */ 'use strict'; document.addEventListener('DOMContentLoaded', () => { const section = document.getElementById('connected-systems'); const shoeStage = document.getElementById('shoeStage'); const horizon = document.getElementById('scanHorizon'); const flowLine = document.querySelector('.connected__flow-line'); const flowDot = document.querySelector('.connected__flow-dot'); const mesh = document.getElementById('shoeMesh'); const meshClip = document.getElementById('meshClip'); const particlesHost = document.getElementById('scanParticles'); const labels = Array.from(document.querySelectorAll('.scan-label')); const treeSvg = document.getElementById('treeSvg'); const branchGroups = Array.from(document.querySelectorAll('.connected__branch-group')); const nodes = Array.from(document.querySelectorAll('.connected__node')); const routes = Array.from(document.querySelectorAll('.route')); if (!section || !shoeStage || typeof gsap === 'undefined') return; const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ── Build scan particles ───────────────────── */ const PARTICLE_COUNT = 14; const particles = []; for (let i = 0; i < PARTICLE_COUNT; i++) { const p = document.createElement('span'); p.className = 'scan-particle'; const left = 8 + Math.random() * 84; const top = 6 + Math.random() * 88; const size = 2 + Math.random() * 3; p.style.left = left + '%'; p.style.top = top + '%'; p.style.width = size + 'px'; p.style.height = size + 'px'; particlesHost.appendChild(p); particles.push({ el: p, left }); } /* ── Ambient dust: persistent, independent of the scroll timeline ── */ const ambientHost = document.getElementById('ambientParticles'); const AMBIENT_COUNT = 22; for (let i = 0; i < AMBIENT_COUNT; i++) { const p = document.createElement('span'); p.className = 'ambient-particle'; const size = 1.5 + Math.random() * 2; p.style.left = Math.random() * 100 + '%'; p.style.top = Math.random() * 100 + '%'; p.style.width = size + 'px'; p.style.height = size + 'px'; p.style.setProperty('--dur', (2.5 + Math.random() * 3).toFixed(2) + 's'); p.style.setProperty('--delay', (Math.random() * 4).toFixed(2) + 's'); p.style.setProperty('--dx', (Math.random() * 20 - 10).toFixed(1) + 'px'); p.style.setProperty('--dy', (-6 - Math.random() * 16).toFixed(1) + 'px'); ambientHost.appendChild(p); } if (reduceMotion) { gsap.set(shoeStage, { opacity: 1 }); gsap.set(mesh, { opacity: 0.75 }); gsap.set(meshClip, { '--reveal': '115%' }); gsap.set(labels, { opacity: 1, y: 0, scale: 1 }); gsap.set(particles.map((p) => p.el), { opacity: 0.6 }); gsap.set(horizon, { opacity: 0.85, scaleX: 1 }); gsap.set(flowLine, { opacity: 0.7, scaleY: 1 }); gsap.set(treeSvg, { opacity: 1 }); gsap.set(branchGroups, { clipPath: 'inset(0% 0% 0% 0%)' }); gsap.set(nodes, { opacity: 1, scale: 1 }); routes.forEach((el) => el.classList.add('is-lit')); return; } gsap.set(shoeStage, { opacity: 0 }); gsap.set(labels, { opacity: 0, y: 10, scale: 0.9, transformOrigin: '50% 50%' }); function playIntro() { const tl = gsap.timeline({ defaults: { ease: 'power2.out' } }); // 1. Stage fades up, ambient glow already pulsing via CSS tl.to(shoeStage, { opacity: 1, duration: 0.5 }, 0); // 2. The horizon line is the whole scan effect: it lights up and // grows outward from center, passing through the shoe and // extending past its silhouette on both sides. The wireframe // mesh reveals in step with it. tl.to(horizon, { opacity: 0.85, duration: 0.2 }, 0.3) .to(horizon, { scaleX: 1, duration: 0.6, ease: 'power2.out' }, 0.3) .to(mesh, { opacity: 0.75, duration: 0.2 }, 0.3) .to(meshClip, { '--reveal': '115%', duration: 0.6, ease: 'power2.out' }, 0.3); // 3. Particles twinkle in across the same window the horizon grows particles.forEach(({ el, left }) => { const t = 0.3 + (left / 100) * 0.6; tl.to(el, { opacity: 1, duration: 0.25 }, t) .to(el, { opacity: 0.45, y: -4, duration: 0.5 }, t + 0.25); }); // 4. Annotation labels appear one at a time, each a distinct beat labels.forEach((label, i) => { tl.to(label, { opacity: 1, y: 0, scale: 1, duration: 0.4, ease: 'back.out(1.7)' }, 0.45 + i * 0.32); }); // 5. The horizon line hands off into a connector that carries the // light down from the shoe stage into the tree's attachment point. tl.to(flowLine, { opacity: 0.7, scaleY: 1, duration: 0.4, ease: 'power1.out' }, 1.1) .fromTo(flowDot, { opacity: 0, top: '0%' }, { opacity: 1, top: '100%', duration: 0.4, ease: 'power1.in' }, 1.1) .to(flowDot, { opacity: 0, duration: 0.15 }, 1.5); // 6. The tree becomes visible, then each branch draws itself in // center-out: the middle line first, then the two adjacent, then // the two outer ones — each followed by its node flashing and // its pill lighting up as the light "arrives". const centerIndex = Math.floor(branchGroups.length / 2); const STEP = 0.22; const BRANCH_DUR = 0.9; tl.addLabel('branches', 1.5) .to(treeSvg, { opacity: 1, duration: 0.3 }, 'branches'); branchGroups.forEach((group, i) => { const pos = `branches+=${(Math.abs(i - centerIndex) * STEP).toFixed(2)}`; tl.fromTo(group, { clipPath: 'inset(0% 0% 100% 0%)' }, { clipPath: 'inset(0% 0% 0% 0%)', duration: BRANCH_DUR, ease: 'power2.out' }, pos ); // Opacity reveal is permanent; the scale punch alone yoyos back down. tl.to(nodes[i], { opacity: 1, duration: 0.15 }, `${pos}+=${BRANCH_DUR - 0.15}`); tl.fromTo(nodes[i], { scale: 1 }, { scale: 1.5, duration: 0.18, yoyo: true, repeat: 1, ease: 'power2.out' }, `${pos}+=${BRANCH_DUR - 0.15}` ); tl.call(() => routes[i].classList.add('is-lit'), null, `${pos}+=${(BRANCH_DUR - 0.05).toFixed(2)}`); }); return tl; } if (typeof ScrollTrigger !== 'undefined') { gsap.registerPlugin(ScrollTrigger); ScrollTrigger.create({ trigger: section, start: 'top 70%', once: true, onEnter: playIntro, }); } else { playIntro(); } });