whats mildly cool
obligatory bottom sentence that looks stylistically familar to every site out there and sums sup what the page is sometimes
Five techniques working together. Here's the code behind each one.
The outer .scroll-scene is set to 500vh — five times the viewport. This creates the scroll distance.
The inner .sticky-stage uses position: sticky; top: 0; height: 100vh, locking the visual while the page scrolls beneath.
A scroll listener converts scrollY into a 0–1 progress value. This is the number that drives everything else.
We map progress × 4π to get a target rotation in radians. The Three.js render loop lerps toward it each frame for smooth motion.
Each text label has a scroll window (e.g. 0.2–0.4). We interpolate opacity from the progress value to fade in and out.
A requestAnimationFrame loop runs independently from scrolling. It lerps currentRot toward targetRot each tick, decoupling smoothness from scroll speed.
<!-- Tall outer container creates scroll distance --> <section class="scroll-scene" style="height: 500vh"> <!-- Sticky inner locks visual to viewport --> <div class="sticky-stage" style="position:sticky; top:0; height:100vh"> <video id="v" muted preload="auto"></video> </div> </section>
// Three.js scene already set up with a staplerGroup let targetRotY = 0; let currentRotY = 0; window.addEventListener('scroll', () => { const { top, height } = scene.getBoundingClientRect(); // 0→1 as the scene scrolls through const progress = Math.max(0, Math.min(1, -top / (height - window.innerHeight) )); // 2 full rotations = 4π radians total targetRotY = progress * Math.PI * 4; }); // rAF loop: lerp toward target → smooth regardless of scroll speed function animate() { requestAnimationFrame(animate); currentRotY += (targetRotY - currentRotY) * 0.1; staplerGroup.rotation.y = currentRotY; renderer.render(scene, camera); } animate();
const steps = [ { el: document.querySelector('#label-0'), start: 0, end: 0.22 }, { el: document.querySelector('#label-1'), start: 0.18, end: 0.42 }, // …etc ]; function updateLabels(progress) { steps.forEach(({ el, start, end }) => { const mid = (start + end) / 2; const range = (end - start) / 2; // Tent function: ramps up then down within the window const dist = Math.abs(progress - mid) / range; el.style.opacity = Math.max(0, 1 - dist).toFixed(3); }); }
gsap.registerPlugin(ScrollTrigger); const state = { rot: 0 }; gsap.to(state, { rot: Math.PI * 4, ease: 'none', scrollTrigger: { trigger: '.scroll-scene', start: 'top top', end: 'bottom bottom', scrub: 0.8, // ← lerp lag in seconds }, onUpdate: () => { // GSAP handles the lerp — assign directly staplerGroup.rotation.y = state.rot; } });
A tall container. A sticky child. A scroll listener that sets a target rotation. And a Three.js render loop that lerps toward it. Everything else layers on top of those four ideas.