Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion assets/scss/_scroll-cube.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
perspective: 1200px;
width: 50px;
height: 50px;
transform: translate(-50%, -50%);
animation: pieceIdle 3s ease-in-out infinite;
}

@keyframes pieceIdle {
0%, 100% { transform: translate(-50%, -50%); }
50% { transform: translate(-50%, calc(-50% - 7px)); }
}

.scroll-piece-body {
Expand Down Expand Up @@ -165,6 +170,7 @@
}

.reunited-float {
opacity: 0;
animation: reunitedFloat 7s ease-in-out infinite;
}

Expand Down
260 changes: 157 additions & 103 deletions static/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ const scrubEach = (elements, props, triggerEl, startBase, endBase, offsetPer) =>

// ── 5. Scroll Logo Pieces — 6 independent pieces that reunite in the browser section ──
const initScrollPieces = () => {
document.querySelectorAll('.scroll-piece-anchor').forEach((node) => node.remove());

// The 6 polygons of the Kanvas isometric logo
// Each piece: SVG polygon data, fill color, depth class, intro section
const pieces = [
Expand Down Expand Up @@ -129,6 +131,9 @@ const initScrollPieces = () => {
const anchor = document.createElement('div');
anchor.className = 'scroll-piece-anchor';
anchor.setAttribute('data-piece', i);
anchor.style.opacity = '0';
anchor.style.visibility = 'hidden';
anchor.style.zIndex = '-1';
anchor.innerHTML =
'<div class="scroll-piece-stage">' +
'<div class="scroll-piece-body scroll-piece-body--depth-' + p.depth + '">' +
Expand All @@ -139,6 +144,11 @@ const initScrollPieces = () => {
'<div class="scroll-piece-glow"></div>' +
'</div>';
document.body.appendChild(anchor);
const stage = anchor.querySelector('.scroll-piece-stage');
if (stage) {
stage.style.animationDuration = (2.4 + Math.random() * 1.4) + 's';
stage.style.animationDelay = (-Math.random() * 3) + 's';
}
anchors.push(anchor);
bodies.push(anchor.querySelector('.scroll-piece-body'));
});
Expand All @@ -160,9 +170,8 @@ const initScrollPieces = () => {
gsap.set(reunitedFloat, { opacity: 0 });
}

// ── Per-piece scroll journeys ──
// Store journey triggers so we can kill the scrubs when convergence fires
const journeyTriggers = [];
const pieceTriggers = [];
const journeyTimelines = [];

const browserSection = document.querySelector('.browser');
const sections = [
Expand All @@ -181,36 +190,46 @@ const initScrollPieces = () => {
const triggerEl = document.querySelector(section);
if (!triggerEl) return;

// Start at position, scale 1 (50px stage)
gsap.set(anchor, { x: startX, y: startY, scale: 1 });
gsap.set(body, { rotateY: rotation, rotateX: Math.random() * 910 - 5 });
gsap.set(body, { rotateY: rotation, rotateX: Math.random() * 10 - 5 });

// Fade-in as intro section enters viewport
gsap.to(anchor, {
const fadeTween = gsap.to(anchor, {
opacity: .8,
immediateRender: false,
scrollTrigger: {
trigger: triggerEl,
start: 'top 85%',
end: 'top 50%',
scrub: 1,
onEnter: () => {
anchor.style.visibility = 'visible';
anchor.style.display = 'block';
},
onEnterBack: () => {
anchor.style.visibility = 'visible';
anchor.style.display = 'block';
},
onLeaveBack: () => {
anchor.style.visibility = 'hidden';
anchor.style.display = 'none';
},
}
});
if (fadeTween.scrollTrigger) {
pieceTriggers.push(fadeTween.scrollTrigger);
}

// Journey: drift toward browser section center,
// growing from scale 1 (50px) to scale 5.6 (280px)
const sectionIndex = sections.indexOf(section);
const remainingSections = sections.slice(sectionIndex + 1);
const steps = remainingSections.length + 1;

// Each piece drifts to a unique orbit position around the browser area
// so they remain visibly spread out — ready to swoop in from distinct directions.
const orbitTargets = [
{ x: 35, y: 30 }, // piece 0: upper-left
{ x: 65, y: 30 }, // piece 1: upper-right
{ x: 70, y: 52 }, // piece 2: right
{ x: 60, y: 70 }, // piece 3: lower-right
{ x: 30, y: 52 }, // piece 4: left
{ x: 48, y: 22 }, // piece 5: top-center
{ x: 35, y: 30 },
{ x: 65, y: 30 },
{ x: 70, y: 52 },
{ x: 60, y: 70 },
{ x: 30, y: 52 },
{ x: 48, y: 22 },
];
const orbit = orbitTargets[pieceIdx];

Expand All @@ -220,10 +239,12 @@ const initScrollPieces = () => {
start: 'top 80%',
endTrigger: '.browser',
end: 'top 40%',
scrub: 2,
scrub: 1,
fastScrollEnd: true,
}
});
journeyTriggers.push(journey.scrollTrigger);
journeyTimelines.push(journey);
if (journey.scrollTrigger) pieceTriggers.push(journey.scrollTrigger);

for (let s = 0; s < steps; s++) {
const progress = (s + 1) / steps;
Expand All @@ -247,14 +268,15 @@ const initScrollPieces = () => {
}
});

// ── Step 1: Reveal — pieces rise from background as browser section approaches ──
if (browserSection) {
ScrollTrigger.create({
trigger: browserSection,
start: 'top 85%',
once: true,
onEnter: () => {
anchors.forEach(a => {
a.style.visibility = 'visible';
a.style.display = 'block';
a.style.zIndex = '10';
const svg = a.querySelector('svg');
if (svg) gsap.to(svg, { opacity: 0.85, duration: 0.6, overwrite: true });
Expand All @@ -263,101 +285,131 @@ const initScrollPieces = () => {
});
}

// ── Step 2: Convergence — pieces swoop from orbit into the browser screen ──
if (reunitedFloat && browserSection) {
let hasConverged = false;
const startConvergence = (instant = false) => {
if (hasConverged) return;
hasConverged = true;

if (instant) {
gsap.set(reunitedFloat, { opacity: 1 });
anchors.forEach(a => {
gsap.set(a, { autoAlpha: 0 });
a.style.zIndex = '-1';
a.style.visibility = 'hidden';
a.style.display = 'none';
});
return;
}

gsap.set(reunitedFloat, { opacity: 0 });

journeyTimelines.forEach(tl => {
tl.progress(1);
if (tl.scrollTrigger) tl.scrollTrigger.kill();
});
pieceTriggers.forEach(t => { if (t) t.kill(); });
anchors.forEach(a => gsap.killTweensOf(a));
bodies.forEach(b => gsap.killTweensOf(b));

requestAnimationFrame(() => {
anchors.forEach(a => {
a.style.display = 'block';
a.style.visibility = 'visible';
a.style.zIndex = '100';
gsap.set(a, { autoAlpha: 1 });
});

const logoEl = document.querySelector('.reunited-logo');
if (!logoEl) return;

const logoRect = logoEl.getBoundingClientRect();
const logoCenterX = logoRect.left + logoRect.width / 2;
const logoCenterY = logoRect.top + logoRect.height / 2 - 24;

const convergeTl = gsap.timeline({ defaults: { overwrite: true } });

introSchedule.forEach(({ pieceIdx }, order) => {
const anchor = anchors[pieceIdx];
const body = bodies[pieceIdx];
const svg = anchor.querySelector('svg');
const glow = anchor.querySelector('.scroll-piece-glow');
const t = order * 0.05;

if (glow) convergeTl.to(glow, { opacity: 0, duration: 0.12, ease: 'none' }, 0);
if (svg) gsap.set(svg, { opacity: 1 });

gsap.set(body, { rotateZ: (order % 2 === 0 ? 1 : -1) * (10 + order * 4) });

convergeTl.to(anchor, {
x: logoCenterX,
y: logoCenterY,
scale: 5.6,
opacity: 1,
duration: 0.46,
ease: 'power3.in',
}, t);

convergeTl.to(body, {
rotateX: 0, rotateY: 0, rotateZ: 0,
duration: 0.46,
ease: 'power2.inOut',
}, t);
});

const allLand = 5 * 0.05 + 0.46;

convergeTl
.to(anchors, { scale: 5.76, duration: 0.07, ease: 'power2.out' }, allLand)
.to(anchors, { scale: 5.6, duration: 0.14, ease: 'expo.out' })
.to(reunitedFloat, { opacity: 1, duration: 0.22, ease: 'power2.out' }, '-=0.1')
.to(anchors, { autoAlpha: 0, duration: 0.16, ease: 'power2.in' }, '<')
.add(() => {
anchors.forEach(a => {
a.style.zIndex = '-1';
a.style.visibility = 'hidden';
a.style.display = 'none';
a.style.opacity = '0';
});
});
});
};

const browserMockup = browserSection.querySelector('.browser-mockup') || browserSection;

ScrollTrigger.create({
trigger: browserMockup,
start: 'top 70%',
onEnter: startConvergence,
onLeaveBack: () => {
hasConverged = false;
}
});

ScrollTrigger.create({
trigger: browserSection,
start: 'top 30%',
start: 'bottom bottom',
once: true,
onEnter: () => {
requestAnimationFrame(() => {
// Kill scroll-driven journeys before taking over with time-based tweens
journeyTriggers.forEach(t => {
if (t) {
if (t.animation) t.animation.kill();
t.kill();
}
});
anchors.forEach(a => {
gsap.killTweensOf(a);
a.style.zIndex = '100'; // above everything during swoop
});
bodies.forEach(b => gsap.killTweensOf(b));

const logoEl = document.querySelector('.reunited-logo');
if (!logoEl) return;

const logoRect = logoEl.getBoundingClientRect();
const logoCenterX = logoRect.left + logoRect.width / 2;
const finalLogoYOffset = -24;
const logoCenterY = logoRect.top + logoRect.height / 2 + finalLogoYOffset;

let arrived = 0;
introSchedule.forEach(({ pieceIdx }, order) => {
const anchor = anchors[pieceIdx];
const body = bodies[pieceIdx];
const svg = anchor.querySelector('svg');
const glow = anchor.querySelector('.scroll-piece-glow');

if (glow) gsap.to(glow, { opacity: 0, duration: 0.15, overwrite: true });
if (svg) gsap.set(svg, { opacity: 1 });

gsap.to(anchor, {
x: logoCenterX,
y: logoCenterY,
scale: 5.6,
opacity: 1,
duration: 0.25,
delay: order * 0.045,
ease: 'power2.inOut',
overwrite: true,
onComplete: () => {
arrived++;
if (arrived === introSchedule.length) {
gsap.to(anchors, {
scale: 5.72, duration: 0.14, ease: 'sine.out',
overwrite: true,
onComplete: () => {
gsap.to(anchors, {
scale: 5.6, duration: 0.2, ease: 'sine.inOut',
onComplete: () => {
gsap.set(reunitedFloat, { opacity: 1 });
gsap.fromTo(logoEl,
{ y: finalLogoYOffset - 40, scale: 0.94 },
{
y: finalLogoYOffset,
scale: 1,
duration: 0.9,
ease: 'bounce.out',
overwrite: true,
}
);
anchors.forEach(a => {
gsap.set(a, { opacity: 0 });
a.style.zIndex = '-1';
});
}
});
}
});
}
}
});

gsap.to(body, {
rotateY: 0, rotateX: 0,
duration: 0.5, delay: order * 0.045, ease: 'sine.inOut',
overwrite: true,
});
});
if (!hasConverged) startConvergence(true);
anchors.forEach(a => {
gsap.set(a, { autoAlpha: 0 });
a.style.zIndex = '-1';
a.style.visibility = 'hidden';
a.style.display = 'none';
});
}
});

if (browserMockup.getBoundingClientRect().top <= window.innerHeight * 0.7) {
startConvergence(true);
}
}
};

const initScrollAnimations = () => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
if (!document.querySelector('.browser, #browserScene')) return;

// ── Background Ambient Overlay ──
const ambient = document.createElement('div');
Expand Down Expand Up @@ -577,7 +629,9 @@ const initVideoHandler = () => {

document.addEventListener("DOMContentLoaded", () => {
initMarquee();
initScrollAnimations();
if (document.querySelector('.browser, #browserScene')) {
initScrollAnimations();
}
initVideoHandler();

const header = document.querySelector(".site-header");
Expand Down
Loading