156 lines
6.4 KiB
JavaScript
156 lines
6.4 KiB
JavaScript
/* =============================================
|
|
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();
|
|
}
|
|
});
|