NEMAWASHI LAB

Parallax

GSAP

Five layers of scattered shapes -- large translucent squares in the back, small bright circles in the front -- each scroll at a different fraction of the page speed (0.1x to 1.0x) via ScrollTrigger with scrub. Text headlines fade in from 60px below when they cross 80% of the viewport, then reverse out on scroll-up. The depth illusion comes from coupling size, opacity, and count: background layers have fewer, larger, dimmer shapes, foreground layers have many small bright ones.

Parallax

Depth

Layers

Motion

Space

Infinite

Source

ParallaxLayers.tsx150 lines
"use client";

import { useRef, useEffect, useState } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import ScrambleLink from "../ScrambleLink";

gsap.registerPlugin(ScrollTrigger);

const LAYERS = [
  { speed: 0.1, color: "rgba(255,255,255,0.03)", size: 200, count: 5 },
  { speed: 0.3, color: "rgba(255,255,255,0.05)", size: 120, count: 8 },
  { speed: 0.5, color: "rgba(255,255,255,0.08)", size: 80, count: 12 },
  { speed: 0.7, color: "rgba(255,255,255,0.12)", size: 50, count: 18 },
  { speed: 1.0, color: "rgba(255,255,255,0.2)", size: 24, count: 25 },
];

function seededRandom(seed: number) {
  const x = Math.sin(seed * 9301 + 49297) * 49297;
  return x - Math.floor(x);
}

export default function ParallaxLayers() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [dark] = useState(true);

  useEffect(() => {
    document.documentElement.classList.add("allow-scroll");
    return () => document.documentElement.classList.remove("allow-scroll");
  }, []);

  useEffect(() => {
    if (!containerRef.current) return;

    const ctx = gsap.context(() => {
      const layers = gsap.utils.toArray<HTMLElement>(".plx-layer");

      layers.forEach((layer) => {
        const speed = parseFloat(layer.dataset.speed || "0.5");
        gsap.to(layer, {
          yPercent: -50 * speed,
          ease: "none",
          scrollTrigger: {
            trigger: containerRef.current,
            start: "top top",
            end: "bottom top",
            scrub: true,
          },
        });
      });

      const texts = gsap.utils.toArray<HTMLElement>(".plx-text");
      texts.forEach((text) => {
        gsap.from(text, {
          opacity: 0,
          y: 60,
          duration: 0.8,
          ease: "power3.out",
          scrollTrigger: {
            trigger: text,
            start: "top 80%",
            toggleActions: "play none none reverse",
          },
        });
      });
    }, containerRef);

    return () => ctx.revert();
  }, []);

  return (
    <div
      ref={containerRef}
      className="relative"
      style={{ background: "#0a0a0a", minHeight: "400vh" }}
    >
      {/* Parallax layers */}
      {LAYERS.map((layer, li) => (
        <div
          key={li}
          className="plx-layer"
          data-speed={layer.speed}
          style={{
            position: "fixed",
            inset: 0,
            pointerEvents: "none",
            zIndex: li,
          }}
        >
          {Array.from({ length: layer.count }, (_, i) => (
            <div
              key={i}
              style={{
                position: "absolute",
                width: layer.size,
                height: layer.size,
                borderRadius: layer.size < 60 ? "50%" : 12,
                background: dark ? layer.color : layer.color.replace("255,255,255", "26,26,46"),
                left: `${seededRandom(li * 100 + i) * 100}%`,
                top: `${seededRandom(li * 100 + i + 50) * 120}%`,
              }}
            />
          ))}
        </div>
      ))}

      {/* Content sections */}
      <div style={{ position: "relative", zIndex: 10 }}>
        <div className="h-screen flex items-center justify-center">
          <p
            className="plx-text text-center"
            style={{
              fontFamily: "var(--font-flux), Georgia, serif",
              fontVariationSettings: "'wght' 600, 'SRIF' 500",
              fontSize: "clamp(32px, 6vw, 72px)",
              color: "#fff",
              lineHeight: 1.1,
            }}
          >
            Parallax
          </p>
        </div>

        {["Depth", "Layers", "Motion", "Space", "Infinite"].map((word, i) => (
          <div key={word} className="h-screen flex items-center justify-center">
            <p
              className="plx-text"
              style={{
                fontFamily: "var(--font-flux), Georgia, serif",
                fontVariationSettings: `'wght' ${200 + i * 100}, 'SRIF' ${i * 100}`,
                fontSize: "clamp(24px, 5vw, 56px)",
                color: `rgba(255,255,255,${0.3 + i * 0.15})`,
                letterSpacing: "0.08em",
                textTransform: "uppercase",
              }}
            >
              {word}
            </p>
          </div>
        ))}
      </div>

      {/* Nav */}
      <div className="fixed bottom-6 left-6" style={{ zIndex: 20 }}>
        <ScrambleLink from="PARALLAX" to="← LAB" href="/lab" className="text-[11px] tracking-[0.1em] text-foreground/30 uppercase hover:text-foreground/60 transition-colors" style={{ fontVariationSettings: "'wght' 400, 'SRIF' 100" }} />
      </div>
    </div>
  );
}

NEMAWASHI — Kotaro Abe