whats mildly cool

Check out this scroll

obligatory bottom sentence that looks stylistically familar to every site out there and sums sup what the page is sometimes

Scroll

Start spinning
Nyreummmm
Acceleration
swooosh *code actually contains no acceleration thats kinda hard and unnecessary*
Spinning
this is spinning
End
done spinning.







Timeline!
Live values ▲ hide

Alright here's some stuff ive made

Five techniques working together. Here's the code behind each one.

01

Tall scroll container

The outer .scroll-scene is set to 500vh — five times the viewport. This creates the scroll distance.

02

CSS sticky pinning

The inner .sticky-stage uses position: sticky; top: 0; height: 100vh, locking the visual while the page scrolls beneath.

03

Scroll → progress

A scroll listener converts scrollY into a 0–1 progress value. This is the number that drives everything else.

04

Progress → rotation

We map progress × 4π to get a target rotation in radians. The Three.js render loop lerps toward it each frame for smooth motion.

05

Label sync

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.

06

Three.js render loop

A requestAnimationFrame loop runs independently from scrolling. It lerps currentRot toward targetRot each tick, decoupling smoothness from scroll speed.

Step 1 + 2 — HTML structure (CSS sticky pin)
<!-- 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>
Step 3 + 4 — Scroll listener drives Three.js rotation
// 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();
Step 5 — Syncing text labels to scroll progress
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);
  });
}
Bonus — GSAP ScrollTrigger with Three.js (production approach)
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;
  }
});

That's the whole trick.

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.