From 09407de91b34e9ede33e69706053205e4394fdc3 Mon Sep 17 00:00:00 2001 From: Oluwatunmise-olat Date: Sun, 15 Mar 2026 01:34:36 +0100 Subject: [PATCH 1/5] [ui] Refine ending logo animation timing and convergence flow Signed-off-by: Oluwatunmise-olat --- static/scripts/main.js | 85 ++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/static/scripts/main.js b/static/scripts/main.js index d27f8096..4e471ccc 100644 --- a/static/scripts/main.js +++ b/static/scripts/main.js @@ -183,7 +183,7 @@ const initScrollPieces = () => { // 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, { @@ -270,17 +270,17 @@ const initScrollPieces = () => { start: 'top 30%', once: true, onEnter: () => { + journeyTriggers.forEach(t => { + if (t) { + if (t.animation) t.animation.progress(1); + t.kill(); + } + }); + 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 + a.style.zIndex = '100'; }); bodies.forEach(b => gsap.killTweensOf(b)); @@ -292,63 +292,39 @@ const initScrollPieces = () => { const finalLogoYOffset = -24; const logoCenterY = logoRect.top + logoRect.height / 2 + finalLogoYOffset; - let arrived = 0; + 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 pieceDelay = order * 0.04; if (glow) gsap.to(glow, { opacity: 0, duration: 0.15, overwrite: true }); if (svg) gsap.set(svg, { opacity: 1 }); - gsap.to(anchor, { + convergeTl.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, { + duration: 0.5, + ease: 'power3.inOut', + }, pieceDelay); + + convergeTl.to(body, { rotateY: 0, rotateX: 0, - duration: 0.5, delay: order * 0.045, ease: 'sine.inOut', - overwrite: true, - }); + duration: 0.5, + ease: 'power2.inOut', + }, pieceDelay); + }); + + convergeTl + .to(reunitedFloat, { opacity: 1, duration: 0.15, ease: 'none' }) + .to(anchors, { opacity: 0, duration: 0.15, ease: 'none' }, '<') + .add(() => { + anchors.forEach(a => { a.style.zIndex = '-1'; }); }); }); } @@ -358,6 +334,7 @@ const initScrollPieces = () => { 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'); @@ -577,7 +554,9 @@ const initVideoHandler = () => { document.addEventListener("DOMContentLoaded", () => { initMarquee(); - initScrollAnimations(); + if (document.querySelector('.browser, #browserScene')) { + initScrollAnimations(); + } initVideoHandler(); const header = document.querySelector(".site-header"); From f039d23a7498677ef4979885fef818cd1f36db1b Mon Sep 17 00:00:00 2001 From: Oluwatunmise-olat Date: Sun, 15 Mar 2026 03:23:54 +0100 Subject: [PATCH 2/5] attempt stabilization of scroll pieces Signed-off-by: Oluwatunmise-olat --- assets/scss/_scroll-cube.scss | 1 + static/scripts/main.js | 210 ++++++++++++++++++++++------------ 2 files changed, 135 insertions(+), 76 deletions(-) diff --git a/assets/scss/_scroll-cube.scss b/assets/scss/_scroll-cube.scss index 79da480b..6b685863 100644 --- a/assets/scss/_scroll-cube.scss +++ b/assets/scss/_scroll-cube.scss @@ -165,6 +165,7 @@ } .reunited-float { + opacity: 0; animation: reunitedFloat 7s ease-in-out infinite; } diff --git a/static/scripts/main.js b/static/scripts/main.js index 4e471ccc..09a516e5 100644 --- a/static/scripts/main.js +++ b/static/scripts/main.js @@ -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 = [ @@ -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 = '
' + '
' + @@ -160,9 +165,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 = [ @@ -181,36 +185,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() * 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]; @@ -220,10 +234,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; @@ -247,7 +263,6 @@ const initScrollPieces = () => { } }); - // ── Step 1: Reveal — pieces rise from background as browser section approaches ── if (browserSection) { ScrollTrigger.create({ trigger: browserSection, @@ -255,6 +270,8 @@ const initScrollPieces = () => { 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 }); @@ -263,72 +280,113 @@ const initScrollPieces = () => { }); } - // ── Step 2: Convergence — pieces swoop from orbit into the browser screen ── if (reunitedFloat && browserSection) { - ScrollTrigger.create({ - trigger: browserSection, - start: 'top 30%', - once: true, - onEnter: () => { - journeyTriggers.forEach(t => { - if (t) { - if (t.animation) t.animation.progress(1); - t.kill(); - } + 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; + } + + 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 finalLogoYOffset = -24; + const logoCenterY = logoRect.top + logoRect.height / 2 + finalLogoYOffset; + + 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 pieceDelay = order * 0.04; + + if (glow) gsap.to(glow, { opacity: 0, duration: 0.15, overwrite: true }); + if (svg) gsap.set(svg, { opacity: 1 }); + + convergeTl.to(anchor, { + x: logoCenterX, + y: logoCenterY, + scale: 5.6, + opacity: 1, + duration: 0.5, + ease: 'power3.inOut', + }, pieceDelay); + + convergeTl.to(body, { + rotateY: 0, rotateX: 0, + duration: 0.5, + ease: 'power2.inOut', + }, pieceDelay); }); - requestAnimationFrame(() => { + convergeTl + .to(reunitedFloat, { opacity: 1, duration: 0.2, ease: 'sine.out' }) + .to(anchors, { autoAlpha: 0, duration: 0.2, ease: 'sine.out' }, '<') + .add(() => { anchors.forEach(a => { - gsap.killTweensOf(a); - a.style.zIndex = '100'; - }); - 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; - - 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 pieceDelay = order * 0.04; - - if (glow) gsap.to(glow, { opacity: 0, duration: 0.15, overwrite: true }); - if (svg) gsap.set(svg, { opacity: 1 }); - - convergeTl.to(anchor, { - x: logoCenterX, - y: logoCenterY, - scale: 5.6, - opacity: 1, - duration: 0.5, - ease: 'power3.inOut', - }, pieceDelay); - - convergeTl.to(body, { - rotateY: 0, rotateX: 0, - duration: 0.5, - ease: 'power2.inOut', - }, pieceDelay); + a.style.zIndex = '-1'; + a.style.visibility = 'hidden'; + a.style.display = 'none'; + a.style.opacity = '0'; }); + }); + }); + }; - convergeTl - .to(reunitedFloat, { opacity: 1, duration: 0.15, ease: 'none' }) - .to(anchors, { opacity: 0, duration: 0.15, ease: 'none' }, '<') - .add(() => { - anchors.forEach(a => { a.style.zIndex = '-1'; }); - }); + ScrollTrigger.create({ + trigger: browserSection, + start: 'top 40%', + once: true, + onEnter: startConvergence + }); + + ScrollTrigger.create({ + trigger: browserSection, + start: 'bottom bottom', + onEnter: () => { + 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 (browserSection.getBoundingClientRect().top <= window.innerHeight * 0.4) { + startConvergence(true); + } } }; From e6491f5042b5a0bff397bd5f0ab2d0d5ce4a027c Mon Sep 17 00:00:00 2001 From: Oluwatunmise-olat Date: Sun, 15 Mar 2026 13:21:09 +0100 Subject: [PATCH 3/5] attempt stabilization of scroll pieces Signed-off-by: Oluwatunmise-olat --- static/scripts/main.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/static/scripts/main.js b/static/scripts/main.js index 09a516e5..dd94276f 100644 --- a/static/scripts/main.js +++ b/static/scripts/main.js @@ -297,6 +297,8 @@ const initScrollPieces = () => { return; } + gsap.set(reunitedFloat, { opacity: 0 }); + journeyTimelines.forEach(tl => { tl.progress(1); if (tl.scrollTrigger) tl.scrollTrigger.kill(); @@ -366,13 +368,16 @@ const initScrollPieces = () => { ScrollTrigger.create({ trigger: browserSection, start: 'top 40%', - once: true, - onEnter: startConvergence + onEnter: startConvergence, + onLeaveBack: () => { + hasConverged = false; + } }); ScrollTrigger.create({ trigger: browserSection, start: 'bottom bottom', + once: true, onEnter: () => { if (!hasConverged) startConvergence(true); anchors.forEach(a => { From 2c3819866b5672cea61a928481af097286cec8de Mon Sep 17 00:00:00 2001 From: Oluwatunmise-olat Date: Sun, 15 Mar 2026 14:02:27 +0100 Subject: [PATCH 4/5] smooth out ending logo animation Signed-off-by: Oluwatunmise-olat --- assets/scss/_scroll-cube.scss | 7 ++++- static/scripts/main.js | 48 +++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/assets/scss/_scroll-cube.scss b/assets/scss/_scroll-cube.scss index 6b685863..6eff58c5 100644 --- a/assets/scss/_scroll-cube.scss +++ b/assets/scss/_scroll-cube.scss @@ -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 { diff --git a/static/scripts/main.js b/static/scripts/main.js index dd94276f..a9221c52 100644 --- a/static/scripts/main.js +++ b/static/scripts/main.js @@ -144,6 +144,11 @@ const initScrollPieces = () => { '
' + '
'; 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')); }); @@ -320,8 +325,7 @@ const initScrollPieces = () => { const logoRect = logoEl.getBoundingClientRect(); const logoCenterX = logoRect.left + logoRect.width / 2; - const finalLogoYOffset = -24; - const logoCenterY = logoRect.top + logoRect.height / 2 + finalLogoYOffset; + const logoCenterY = logoRect.top + logoRect.height / 2 - 24; const convergeTl = gsap.timeline({ defaults: { overwrite: true } }); @@ -330,38 +334,44 @@ const initScrollPieces = () => { const body = bodies[pieceIdx]; const svg = anchor.querySelector('svg'); const glow = anchor.querySelector('.scroll-piece-glow'); - const pieceDelay = order * 0.04; + const t = order * 0.05; - if (glow) gsap.to(glow, { opacity: 0, duration: 0.15, overwrite: true }); + 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.5, - ease: 'power3.inOut', - }, pieceDelay); + duration: 0.46, + ease: 'power3.in', + }, t); convergeTl.to(body, { - rotateY: 0, rotateX: 0, - duration: 0.5, + rotateX: 0, rotateY: 0, rotateZ: 0, + duration: 0.46, ease: 'power2.inOut', - }, pieceDelay); + }, t); }); + const allLand = 5 * 0.05 + 0.46; + convergeTl - .to(reunitedFloat, { opacity: 1, duration: 0.2, ease: 'sine.out' }) - .to(anchors, { autoAlpha: 0, duration: 0.2, ease: 'sine.out' }, '<') - .add(() => { - anchors.forEach(a => { - a.style.zIndex = '-1'; - a.style.visibility = 'hidden'; - a.style.display = 'none'; - a.style.opacity = '0'; + .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'; + }); }); - }); }); }; From fe729c72f34a4ce0a812b279d214ba6b38853a27 Mon Sep 17 00:00:00 2001 From: Oluwatunmise-olat Date: Sun, 15 Mar 2026 14:29:30 +0100 Subject: [PATCH 5/5] fix piece visibility issues Signed-off-by: Oluwatunmise-olat --- static/scripts/main.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/static/scripts/main.js b/static/scripts/main.js index a9221c52..6481d3fc 100644 --- a/static/scripts/main.js +++ b/static/scripts/main.js @@ -375,9 +375,11 @@ const initScrollPieces = () => { }); }; + const browserMockup = browserSection.querySelector('.browser-mockup') || browserSection; + ScrollTrigger.create({ - trigger: browserSection, - start: 'top 40%', + trigger: browserMockup, + start: 'top 70%', onEnter: startConvergence, onLeaveBack: () => { hasConverged = false; @@ -399,7 +401,7 @@ const initScrollPieces = () => { } }); - if (browserSection.getBoundingClientRect().top <= window.innerHeight * 0.4) { + if (browserMockup.getBoundingClientRect().top <= window.innerHeight * 0.7) { startConvergence(true); } }