Harshit

Experiments

A scratchpad for testing interactions, playing with physics, and pushing the boundaries of web components.

harshit
experiments

Gooey Mask Physics

A reactive SVG filter with spring physics and mouse repulsion.

Inspired by the hero section at schaum.cc
Code
"use client";

import React, { useEffect, useRef } from 'react';

interface GooeyMaskProps {
    imageUrl?: string;
}

export function GooeyMask({
    imageUrl = "https://images.unsplash.com/photo-1743710426934-89887ca897d8?q=80&w=1238&auto=format&fit=crop"
}: GooeyMaskProps) {
    const containerRef = useRef<HTMLDivElement>(null);
    const cursorRef = useRef<SVGCircleElement>(null);
    const nodesRefs = useRef<(SVGCircleElement | null)[]>([]);
    const requestRef = useRef<number>(0);

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

        const MathPI2 = Math.PI * 2;
        const CURSOR_RADIUS = 90;
        const REPULSION_FORCE = 1.8;
        const REPULSION_RADIUS = 300;
        const SPRING_STRENGTH = 0.005;
        const FRICTION = 0.88;

        let width = container.clientWidth;
        let height = container.clientHeight;
        
        // Initial center
        let mouseX = width / 2;
        let mouseY = height / 2;
        let cursorX = mouseX;
        let cursorY = mouseY;

        // Add a ResizeObserver to keep dimensions updated
        const resizeObserver = new ResizeObserver((entries) => {
            for (const entry of entries) {
                width = entry.contentRect.width;
                height = entry.contentRect.height;
            }
        });
        resizeObserver.observe(container);

        const nodes = Array.from({ length: 8 }).map((_, i) => ({
            el: nodesRefs.current[i],
            x: width / 2 + (Math.random() - 0.5) * width * 1.5,
            y: height / 2 + (Math.random() - 0.5) * height * 1.5,
            vx: 0,
            vy: 0,
            baseRadius: i === 0 ? 240 : 100 + Math.random() * 120,
            angle: Math.random() * MathPI2,
            orbitSpeed: (Math.random() - 0.5) * 0.015,
            spreadDistance: i === 0 ? 0 : 120 + Math.random() * 200,
            pulseSpeed: 0.001 + Math.random() * 0.002,
        }));

        if (cursorRef.current) {
            cursorRef.current.setAttribute('r', String(CURSOR_RADIUS));
        }

        const handleMouseMove = (e: MouseEvent) => {
            const rect = container.getBoundingClientRect();
            mouseX = e.clientX - rect.left;
            mouseY = e.clientY - rect.top;
        };

        const handleTouchMove = (e: TouchEvent) => {
            const rect = container.getBoundingClientRect();
            mouseX = e.touches[0].clientX - rect.left;
            mouseY = e.touches[0].clientY - rect.top;
        };

        container.addEventListener('mousemove', handleMouseMove);
        container.addEventListener('touchmove', handleTouchMove, { passive: false });
        
        // If mouse leaves the container, smoothly return to center
        const handleMouseLeave = () => {
            mouseX = width / 2;
            mouseY = height / 2;
        };
        container.addEventListener('mouseleave', handleMouseLeave);

        const animate = (time: number) => {
            cursorX += (mouseX - cursorX) * 0.15;
            cursorY += (mouseY - cursorY) * 0.15;

            if (cursorRef.current) {
                cursorRef.current.setAttribute('cx', String(cursorX));
                cursorRef.current.setAttribute('cy', String(cursorY));
            }

            const globalTargetX = width / 2 +
                Math.sin(time * 0.0003) * (width * 0.4) +
                Math.cos(time * 0.0005) * (width * 0.15);

            const globalTargetY = height / 2 +
                Math.cos(time * 0.0004) * (height * 0.4) +
                Math.sin(time * 0.0006) * (height * 0.15);

            const timeSec = time * 0.001;
            let spreadModifier = 1;

            if (timeSec < 3) {
                spreadModifier = 3.5 - timeSec * 0.9;
            } else {
                spreadModifier = 0.3 + (Math.sin((timeSec - 3) * 0.5) + 1) * 0.2;
            }

            nodes.forEach((node) => {
                if (!node.el) return;

                const currentRadius = node.baseRadius + Math.sin(time * node.pulseSpeed * 2) * 50;
                node.angle += node.orbitSpeed;
                const currentSpread = (node.spreadDistance + Math.sin(time * node.pulseSpeed) * 80) * spreadModifier;

                const targetX = globalTargetX + Math.cos(node.angle) * currentSpread;
                const targetY = globalTargetY + Math.sin(node.angle) * currentSpread;

                node.vx += (targetX - node.x) * SPRING_STRENGTH;
                node.vy += (targetY - node.y) * SPRING_STRENGTH;

                const dx = node.x - cursorX;
                const dy = node.y - cursorY;
                const dist = Math.sqrt(dx * dx + dy * dy);

                if (dist < currentRadius + REPULSION_RADIUS) {
                    const force = (currentRadius + REPULSION_RADIUS - dist) / (currentRadius + REPULSION_RADIUS);
                    node.vx += (dx / dist) * force * REPULSION_FORCE;
                    node.vy += (dy / dist) * force * REPULSION_FORCE;
                }

                node.vx *= FRICTION;
                node.vy *= FRICTION;
                node.x += node.vx;
                node.y += node.vy;

                node.el.setAttribute('cx', String(node.x));
                node.el.setAttribute('cy', String(node.y));
                node.el.setAttribute('r', String(currentRadius));
            });

            requestRef.current = requestAnimationFrame(animate);
        };

        requestRef.current = requestAnimationFrame(animate);

        return () => {
            container.removeEventListener('mousemove', handleMouseMove);
            container.removeEventListener('touchmove', handleTouchMove);
            container.removeEventListener('mouseleave', handleMouseLeave);
            resizeObserver.disconnect();
            if (requestRef.current) cancelAnimationFrame(requestRef.current);
        };
    }, []);

    return (
        <div ref={containerRef} className="w-full h-full relative overflow-hidden bg-[#EAE5D9]">
            <svg
                style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}
                preserveAspectRatio="xMidYMid slice"
            >
                <defs>
                    <filter id="goo">
                        <feGaussianBlur in="SourceGraphic" stdDeviation="40" result="blur" />
                        <feColorMatrix in="blur" mode="matrix" values="
                            1 0 0 0 0  
                            0 1 0 0 0  
                            0 0 1 0 0  
                            0 0 0 65 -30" result="goo"
                        />
                    </filter>

                    <mask id="blob-mask">
                        <g filter="url(#goo)">
                            {Array.from({ length: 8 }).map((_, i) => (
                                <circle
                                    key={i}
                                    ref={(el) => { nodesRefs.current[i] = el; }}
                                    fill="white"
                                />
                            ))}
                            <circle ref={cursorRef} fill="white" />
                        </g>
                    </mask>
                </defs>

                <image
                    href={imageUrl}
                    width="100%"
                    height="100%"
                    preserveAspectRatio="xMidYMid slice"
                    mask="url(#blob-mask)"
                />
            </svg>
        </div>
    );
}
Tap to expand
HARSHIT
HellYeahh
Hover Me

Smooth Text Morph

A critically damped, scattered spring physics text morpher.

Code
"use client";

import React, { useState, useMemo } from "react";
import { motion } from "framer-motion";

interface CharData {
  id: string;
  char: string;
  isSpace: boolean;
  x: number;
  y: number;
  r: number;
}

interface SmoothTextMorphProps {
  primaryText?: string;
  secondaryText?: string;
}

const generateScatterData = (text: string): CharData[] => {
  return text.split("").map((char, i) => {
    if (char === " ") {
      return { id: `space-${i}`, char, isSpace: true, x: 0, y: 0, r: 0 };
    }
    const angle = Math.random() * Math.PI * 2;
    const distance = 100 + Math.random() * 100;

    return {
      id: `${char}-${i}`,
      char,
      isSpace: false,
      x: Math.cos(angle) * distance,
      y: Math.sin(angle) * distance - 20,
      r: (Math.random() - 0.5) * 60,
    };
  });
};

export function SmoothTextMorph({
  primaryText = "HARSHIT",
  secondaryText = "Web Developer",
}: SmoothTextMorphProps) {
  const [isHovered, setIsHovered] = useState(false);

  const primaryCharData = useMemo(() => generateScatterData(primaryText), [primaryText]);
  const secondaryCharData = useMemo(() => generateScatterData(secondaryText), [secondaryText]);

  return (
    <div 
      className="relative flex h-full w-full cursor-pointer items-center justify-center overflow-hidden bg-[#121212] font-['Plus_Jakarta_Sans',sans-serif] antialiased"
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      onClick={() => setIsHovered(!isHovered)}
    >
      <div className="pointer-events-none absolute flex items-center justify-center">
        {primaryCharData.map((item, i) => {
          if (item.isSpace) {
            return <span key={item.id} className="w-[0.4em]" />;
          }
          return (
            <motion.span
              key={item.id}
              className="inline-block origin-center text-4xl md:text-6xl font-extrabold text-white tracking-[4px] will-change-transform"
              initial={false}
              animate={
                isHovered
                  ? { x: item.x, y: item.y, rotate: item.r, scale: 0.4, opacity: 0 }
                  : { x: 0, y: 0, rotate: 0, scale: 1, opacity: 1 }
              }
              transition={{
                type: "spring",
                bounce: 0,
                duration: isHovered ? 0.7 : 0.9,
                delay: isHovered ? i * 0.02 : 0.1 + i * 0.02,
              }}
            >
              {item.char}
            </motion.span>
          );
        })}
      </div>

      <div className="pointer-events-none absolute flex items-center justify-center">
        {secondaryCharData.map((item, i) => {
          if (item.isSpace) {
            return <span key={item.id} className="w-[0.4em]" />;
          }
          return (
            <motion.span
              key={item.id}
              className="inline-block origin-center text-3xl md:text-5xl font-medium text-zinc-400 tracking-tight will-change-transform pt-2"
              initial={false}
              animate={
                isHovered
                  ? { x: 0, y: 0, rotate: 0, scale: 1, opacity: 1 }
                  : { x: item.x, y: item.y, rotate: item.r, scale: 0.4, opacity: 0 }
              }
              transition={{
                type: "spring",
                bounce: 0,
                duration: isHovered ? 0.9 : 0.6,
                delay: isHovered ? 0.1 + i * 0.02 : i * 0.015,
              }}
            >
              {item.char}
            </motion.span>
          );
        })}
      </div>

      <motion.div
        className="pointer-events-none absolute bottom-10 text-xs font-extrabold uppercase tracking-[2px] text-zinc-600"
        animate={{ opacity: isHovered ? 0 : 1 }}
        transition={{ duration: 0.4, ease: "easeInOut" }}
      >
        Hover Me
      </motion.div>
    </div>
  );
}
Tap to expand
Card 1
Card 2
Card 3
Card 4
Swipe to explore

Ambient Deck

Tinder-style card swiper with ambient dynamic background lighting mapping dominant image colors.

Code
"use client";

import React, { useState, useEffect } from "react";
import { motion, PanInfo, useMotionValue, useTransform } from "framer-motion";

// --- Types & Data ---
interface CardData {
  id: number;
  url: string;
}

interface RGB {
  r: number;
  g: number;
  b: number;
}

const initialCards: CardData[] = [
  { id: 1, url: "https://images.unsplash.com/photo-1485841890310-6a055c88698a?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80" },
  { id: 2, url: "https://images.unsplash.com/photo-1518791841217-8f162f1e1131?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80" },
  { id: 3, url: "https://images.unsplash.com/photo-1507146426996-ef05306b995a?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80" },
  { id: 4, url: "https://images.unsplash.com/photo-1511447333015-45b65e60f6d5?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80" }
];

// Cache to avoid recalculating colors for images we've already processed
const colorCache: Record<string, RGB> = {};

// Helper to extract the dominant ambient color from an image URL using a hidden Canvas
const getAverageColor = (url: string): Promise<RGB> => {
  return new Promise((resolve) => {
    if (colorCache[url]) return resolve(colorCache[url]);

    const img = new Image();
    img.crossOrigin = "Anonymous";
    img.src = url;
    img.onload = () => {
      const canvas = document.createElement("canvas");
      canvas.width = 64;
      canvas.height = 64;
      const ctx = canvas.getContext("2d");
      if (!ctx) return resolve({ r: 50, g: 50, b: 80 });

      ctx.drawImage(img, 0, 0, 64, 64);
      const data = ctx.getImageData(0, 0, 64, 64).data;
      let r = 0, g = 0, b = 0, count = 0;

      // Sample every 16th pixel for performance
      for (let i = 0; i < data.length; i += 16) {
        r += data[i];
        g += data[i + 1];
        b += data[i + 2];
        count++;
      }

      const color = {
        r: Math.round(r / count),
        g: Math.round(g / count),
        b: Math.round(b / count)
      };

      colorCache[url] = color;
      resolve(color);
    };
    img.onerror = () => resolve({ r: 50, g: 50, b: 80 }); // Fallback on CORS error
  });
};

export default function AmbientDeck() {
  const [cards, setCards] = useState<CardData[]>(initialCards);
  const [ambient1, setAmbient1] = useState<RGB>({ r: 56, g: 31, b: 122 });
  const [ambient2, setAmbient2] = useState<RGB>({ r: 19, g: 78, b: 112 });

  // Motion values for Tinder-like swipe
  const x = useMotionValue(0);
  const rotateDrag = useTransform(x, [-200, 200], [-18, 18]);
  const likeOpacity = useTransform(x, [20, 100], [0, 1]);
  const nopeOpacity = useTransform(x, [-20, -100], [0, 1]);

  // Update ambient background colors whenever the top cards change
  useEffect(() => {
    const fetchColors = async () => {
      const topColor = await getAverageColor(cards[0].url);
      const nextColor = await getAverageColor(cards[1].url);
      setAmbient1(topColor);
      setAmbient2(nextColor);
    };
    fetchColors();
  }, [cards]);

  // Handle Drag logic
  const handleDragEnd = (event: any, info: PanInfo) => {
    const slideThreshold = 100; // How far to swipe before it shuffles

    // If dragged far enough on the X axis, shuffle
    if (Math.abs(info.offset.x) > slideThreshold) {
      x.set(0); // Instantly reset motion value so next top card starts at 0
      setCards((prev) => {
        const newArray = [...prev];
        const topCard = newArray.shift();
        if (topCard) newArray.push(topCard); // Move top card to back
        return newArray;
      });
    }
  };

  return (
    <div
      className="relative flex items-center justify-center h-full w-full overflow-hidden font-sans"
      style={{ backgroundColor: "#09090b" }}
    >
      {/* Dynamic Ambient Background Layer */}
      <motion.div
        className="absolute inset-0 pointer-events-none"
        animate={{
          background: `
            radial-gradient(circle at 20% 20%, rgba(${ambient1.r}, ${ambient1.g}, ${ambient1.b}, 0.5) 0%, transparent 60%),
            radial-gradient(circle at 80% 80%, rgba(${ambient2.r}, ${ambient2.g}, ${ambient2.b}, 0.3) 0%, transparent 60%),
            radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.02) 0px, transparent 100%)
          `
        }}
        transition={{ duration: 1.2, ease: "easeInOut" }}
      />

      {/* Deck Container */}
      <div className="relative w-[260px] h-[360px]">
        {cards.map((card, index) => {
          const isTop = index === 0;

          // Modern layout variables
          const xOffset = index * 8;
          const yOffset = index * -10;
          const scale = 1 - index * 0.04;
          const rotate = index % 2 === 0 ? index * 2 : index * -2;
          const zIndex = cards.length - index;

          return (
            <motion.div
              key={card.id}
              // "layout" prop tells Framer to smoothly animate array re-ordering
              layout
              className="absolute top-0 left-0 w-full h-full rounded-[24px] overflow-hidden"
              style={{
                x: isTop ? x : undefined,
                rotate: isTop ? rotateDrag : undefined,
                zIndex,
                transformOrigin: "center center",
                backgroundColor: "#18181b",
                border: "1px solid rgba(255, 255, 255, 0.15)",
                boxShadow: `
                  0 30px 60px -12px rgba(0, 0, 0, 0.6), 
                  0 18px 36px -18px rgba(0, 0, 0, 0.5),
                  inset 0 1px 1px rgba(255, 255, 255, 0.2)
                `,
              }}
              // 1. Initial/Resting State
              animate={{
                x: isTop ? 0 : xOffset,
                y: yOffset,
                scale: scale,
                rotate: isTop ? 0 : rotate,
              }}
              // Framer Motion Spring physics
              transition={{
                type: "spring",
                stiffness: 300,
                damping: 22,
                bounce: 0.4,
              }}
              // 2. Drag Interactivity (Only applied to the top card)
              drag={isTop ? "x" : false}
              dragConstraints={{ left: 0, right: 0 }} // Snap back to origin if not swiped hard enough
              dragElastic={0.8} // Very elastic for Tinder drag feel
              onDragEnd={handleDragEnd}
              // 3. Hover & Active States
              whileHover={
                isTop
                  ? { y: -15, scale: 1.02, cursor: "grab" }
                  : {}
              }
              whileTap={
                isTop
                  ? {
                      scale: 1.05, 
                      cursor: "grabbing",
                      boxShadow: `
                        0 40px 70px -15px rgba(0,0,0,0.8), 
                        inset 0 1px 1px rgba(255,255,255,0.3)
                      `,
                    }
                  : {}
              }
            >
              <img
                src={card.url}
                alt={`Card ${card.id}`}
                draggable={false} // Disable native HTML image dragging
                className="w-full h-full object-cover pointer-events-none transition-all duration-300 ease-in-out hover:brightness-110 hover:contrast-110"
                style={{
                  filter: isTop
                    ? "brightness(1) contrast(1.1)"
                    : "brightness(0.85) contrast(1.1)",
                }}
              />
              
              {/* Tinder Stamps */}
              {isTop && (
                <>
                  <motion.div
                    style={{ opacity: likeOpacity }}
                    className="absolute top-8 left-6 border-4 border-green-400 text-green-400 rounded-lg px-4 py-1 text-3xl font-black uppercase tracking-widest rotate-[-15deg] z-10 bg-black/20 backdrop-blur-sm shadow-[0_0_15px_rgba(74,222,128,0.2)]"
                  >
                    LIKE
                  </motion.div>
                  <motion.div
                    style={{ opacity: nopeOpacity }}
                    className="absolute top-8 right-6 border-4 border-red-500 text-red-500 rounded-lg px-4 py-1 text-3xl font-black uppercase tracking-widest rotate-[15deg] z-10 bg-black/20 backdrop-blur-sm shadow-[0_0_15px_rgba(239,68,68,0.2)]"
                  >
                    NOPE
                  </motion.div>
                </>
              )}
            </motion.div>
          );
        })}

        {/* Minimalist modern hint */}
        <div className="absolute -bottom-[50px] left-1/2 -translate-x-1/2 flex items-center gap-2 text-[rgba(255,255,255,0.4)] text-[0.85rem] font-medium tracking-[0.02em]">
          <span>Swipe to explore</span>
        </div>
      </div>
    </div>
  );
}
Tap to expand
The New Standard for WebGL

A physics-driven fluid interface designed to blur the line between digital canvas and interactive reality.

Fluid Landing

A physics-driven fluid interface with WebGL and magnetic canvas interactions.

Code
"use client";
import React, { useEffect, useRef } from 'react';

// --- Types & Interfaces ---
interface MagnetState {
  x: number;
  y: number;
}

interface MouseState {
  x: number;
  y: number;
  r: number;
}

// --- Physics Class ---
class PhysicsBlob {
  x: number;
  y: number;
  vx: number;
  vy: number;
  r: number;
  tx: number;
  ty: number;

  constructor(x: number, y: number, r: number) {
    this.x = x;
    this.y = y;
    this.vx = 0;
    this.vy = 0;
    this.r = r;
    // Idle floating anchors
    this.tx = (Math.random() - 0.5) * 500;
    this.ty = (Math.random() - 0.5) * 300;
  }

  update(
    width: number,
    height: number,
    mouse: MouseState,
    activeMagnet: MagnetState | null
  ) {
    let cx, cy, pullStrength;
    if (activeMagnet) {
      cx = activeMagnet.x + (Math.random() - 0.5) * 30;
      cy = activeMagnet.y + (Math.random() - 0.5) * 30;
      pullStrength = 0.006;
    } else {
      cx = width / 2 + this.tx;
      cy = height / 2 - 50 + this.ty;
      pullStrength = 0.0006;
    }

    // Spring forces
    this.vx += (cx - this.x) * pullStrength;
    this.vy += (cy - this.y) * pullStrength;

    // Mouse Pull
    if (!activeMagnet) {
      const dxM = mouse.x - this.x;
      const dyM = mouse.y - this.y;
      const distM = Math.sqrt(dxM * dxM + dyM * dyM);
      if (distM < 400) {
        this.vx += (dxM / distM) * 0.25;
        this.vy += (dyM / distM) * 0.25;
      }
    }

    // Friction
    this.vx *= 0.92;
    this.vy *= 0.92;
    this.x += this.vx;
    this.y += this.vy;
  }

  draw(ctx: CanvasRenderingContext2D) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    ctx.fill();
  }
}

// --- Component ---
export const FluidLanding: React.FC = () => {
  const glCanvasRef = useRef<HTMLCanvasElement>(null);
  const physicsCanvasRef = useRef<HTMLCanvasElement>(null);

  // Mutable state held in refs to avoid React re-renders during the 60fps loop
  const state = useRef({
    width: 0,
    height: 0,
    mouse: { x: -1000, y: -1000, r: 80 } as MouseState,
    targetMouse: { x: -1000, y: -1000, r: 80 } as MouseState,
    activeMagnet: null as MagnetState | null,
    blobs: [] as PhysicsBlob[],
  });

  // Event Handlers for Magnetic UI
  const handleMagnetEnter = (e: React.MouseEvent<HTMLElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    state.current.activeMagnet = {
      x: rect.left + rect.width / 2,
      y: rect.top + rect.height / 2,
    };
    state.current.targetMouse.r = 220;
  };

  const handleMagnetLeave = () => {
    state.current.activeMagnet = null;
    state.current.targetMouse.r = 80;
  };

  useEffect(() => {
    const glCanvas = glCanvasRef.current;
    const pCanvas = physicsCanvasRef.current;
    if (!glCanvas || !pCanvas) return;

    const gl = glCanvas.getContext('webgl2');
    const ctx = pCanvas.getContext('2d');
    if (!gl || !ctx) return;

    let glFrameId: number;
    let physicsFrameId: number;

    // --- Interaction Listeners ---
    const updateMouse = (x: number, y: number) => {
      // Initialize if offscreen
      if (state.current.mouse.x === -1000) {
        state.current.mouse.x = x;
        state.current.mouse.y = y;
      }
      state.current.targetMouse.x = x;
      state.current.targetMouse.y = y;
    };

    const onMouseMove = (e: MouseEvent) => updateMouse(e.clientX, e.clientY);
    const onTouchMove = (e: TouchEvent) =>
      updateMouse(e.touches[0].clientX, e.touches[0].clientY);

    const triggerShockwave = () => {
      state.current.targetMouse.r = 30; // Shrink cursor sharply
      const { mouse, blobs } = state.current;
      blobs.forEach((b) => {
        const dx = b.x - mouse.x;
        const dy = b.y - mouse.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < 600) {
          const force = (600 - dist) * 0.2;
          b.vx += (dx / dist) * force;
          b.vy += (dy / dist) * force;
        }
      });
    };

    const restoreCursor = () => {
      state.current.targetMouse.r = state.current.activeMagnet ? 220 : 80;
    };

    window.addEventListener('mousemove', onMouseMove);
    window.addEventListener('touchmove', onTouchMove);
    window.addEventListener('mousedown', triggerShockwave);
    window.addEventListener('touchstart', triggerShockwave);
    window.addEventListener('mouseup', restoreCursor);
    window.addEventListener('touchend', restoreCursor);

    // --- Initialization & Resizing ---
    const initPhysics = () => {
      const { width, height } = state.current;
      state.current.blobs = Array.from({ length: 30 }, () => {
        const r = 20 + Math.random() * 80;
        return new PhysicsBlob(
          width / 2 + (Math.random() - 0.5) * 300,
          height / 2 + (Math.random() - 0.5) * 300,
          r
        );
      });
    };

    const resize = () => {
      const width = window.innerWidth;
      const height = window.innerHeight;
      state.current.width = width;
      state.current.height = height;

      pCanvas.width = width;
      pCanvas.height = height;
      glCanvas.width = width;
      glCanvas.height = height;
      
      gl.viewport(0, 0, width, height);
      if (resLoc) gl.uniform2f(resLoc, width, height);

      initPhysics();
    };

    // --- WebGL Shader Setup ---
    const vsSource = `#version 300 es
      in vec4 aPosition;
      void main() { gl_Position = aPosition; }
    `;

    const fsSource = `#version 300 es
      precision highp float;
      out vec4 fragColor;
      uniform vec2 u_resolution;
      uniform float u_time;
      uniform vec2 u_mouse;

      float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
      float noise(vec2 p) {
          vec2 i = floor(p); vec2 f = fract(p);
          vec2 u = f * f * (3.0 - 2.0 * f);
          return mix(mix(hash(i + vec2(0.0,0.0)), hash(i + vec2(1.0,0.0)), u.x),
                     mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), u.x), u.y);
      }
      float fbm(vec2 p) {
          float v = 0.0; float a = 0.5;
          mat2 rot = mat2(cos(0.5), -sin(0.5), sin(0.5), cos(0.5));
          for (int i = 0; i < 5; ++i) {
              v += a * noise(p); p = rot * p * 2.0 + vec2(100.0); a *= 0.5;
          }
          return v;
      }

      void main() {
          vec2 st = gl_FragCoord.xy / u_resolution.xy * 2.5;
          st.x *= u_resolution.x / u_resolution.y;
          
          vec2 m = u_mouse / u_resolution.xy;
          m.y = 1.0 - m.y; m *= 2.5; m.x *= u_resolution.x / u_resolution.y;

          float t = u_time * 0.25; 

          // Mouse Push Interaction
          float distToMouse = distance(st, m);
          st += normalize(st - m) * exp(-distToMouse * 3.0) * 0.3;

          // Domain Warping
          vec2 q = vec2(0.0);
          q.x = fbm(st + vec2(0.0));
          q.y = fbm(st + vec2(1.0));

          vec2 r = vec2(0.0);
          r.x = fbm(st + 1.0 * q + vec2(1.7, 9.2) + 0.15 * t);
          r.y = fbm(st + 1.0 * q + vec2(8.3, 2.8) + 0.12 * t);

          float f = fbm(st + r);

          // Elevate Palette
          vec3 colBg = vec3(0.02, 0.0, 0.05);
          vec3 col1  = vec3(1.0, 0.15, 0.3);
          vec3 col2  = vec3(0.0, 0.6, 1.0);
          vec3 col3  = vec3(0.6, 0.0, 1.0);

          vec3 color = mix(colBg, col1, clamp((f * f) * 2.5, 0.0, 1.0));
          color = mix(color, col2, clamp(length(q) * 0.9, 0.0, 1.0));
          color = mix(color, col3, clamp(length(r.x) * 1.8, 0.0, 1.0));

          float mouseGlow = exp(-distToMouse * 3.5) * 1.0;
          color += vec3(0.3, 0.6, 1.0) * mouseGlow;

          vec2 center = gl_FragCoord.xy / u_resolution.xy - 0.5;
          float vignette = 1.0 - dot(center, center) * 1.2;
          
          vec3 finalColor = (f * f + 0.5 * f) * color * 2.5;
          finalColor *= smoothstep(0.0, 1.0, vignette);

          fragColor = vec4(finalColor, 1.0);
      }
    `;

    const compileShader = (type: number, source: string) => {
      const shader = gl.createShader(type);
      if (!shader) return null;
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error(gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
      }
      return shader;
    };

    const program = gl.createProgram();
    const vShader = compileShader(gl.VERTEX_SHADER, vsSource);
    const fShader = compileShader(gl.FRAGMENT_SHADER, fsSource);
    if (!program || !vShader || !fShader) return;

    gl.attachShader(program, vShader);
    gl.attachShader(program, fShader);
    gl.linkProgram(program);
    gl.useProgram(program);

    const posBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
      gl.STATIC_DRAW
    );

    const posLoc = gl.getAttribLocation(program, 'aPosition');
    gl.enableVertexAttribArray(posLoc);
    gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

    const timeLoc = gl.getUniformLocation(program, 'u_time');
    const resLoc = gl.getUniformLocation(program, 'u_resolution');
    const mouseLoc = gl.getUniformLocation(program, 'u_mouse');

    window.addEventListener('resize', resize);
    resize(); // Initial sizing

    // --- Render Loops ---
    const renderWebGL = (time: number) => {
      gl.uniform1f(timeLoc, time * 0.001);
      gl.uniform2f(mouseLoc, state.current.mouse.x, state.current.mouse.y);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
      glFrameId = requestAnimationFrame(renderWebGL);
    };

    const clamp = (min: number, val: number, max: number) =>
      Math.max(min, Math.min(max, val));

    const renderPhysics = () => {
      const { width, height, mouse, targetMouse, activeMagnet, blobs } = state.current;

      mouse.x += (targetMouse.x - mouse.x) * 0.15;
      mouse.y += (targetMouse.y - mouse.y) * 0.15;
      mouse.r += (targetMouse.r - mouse.r) * 0.12;

      // Trail Fade
      ctx.globalCompositeOperation = 'destination-out';
      ctx.fillStyle = `rgba(0, 0, 0, 0.25)`;
      ctx.fillRect(0, 0, width, height);

      ctx.globalCompositeOperation = 'source-over';
      ctx.fillStyle = 'white';

      // Typography
      ctx.font = `800 ${clamp(80, width * 0.15, 250)}px 'Syne', sans-serif`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.letterSpacing = '-0.04em';
      ctx.fillText('SHAPE', width / 2, height / 2 - 60);

      // Resolve soft collisions
      for (let i = 0; i < blobs.length; i++) {
        for (let j = i + 1; j < blobs.length; j++) {
          const b1 = blobs[i];
          const b2 = blobs[j];
          const dx = b1.x - b2.x;
          const dy = b1.y - b2.y;
          const dist = Math.sqrt(dx * dx + dy * dy);
          const minDist = b1.r + b2.r;
          if (dist < minDist && dist > 0) {
            const force = (minDist - dist) * 0.04;
            const nx = dx / dist;
            const ny = dy / dist;
            b1.vx += nx * force;
            b1.vy += ny * force;
            b2.vx -= nx * force;
            b2.vy -= ny * force;
          }
        }
      }

      blobs.forEach((blob) => {
        blob.update(width, height, mouse, activeMagnet);
        blob.draw(ctx);
      });

      // Draw active cursor
      ctx.beginPath();
      ctx.arc(mouse.x, mouse.y, mouse.r, 0, Math.PI * 2);
      ctx.fill();

      physicsFrameId = requestAnimationFrame(renderPhysics);
    };

    glFrameId = requestAnimationFrame(renderWebGL);
    physicsFrameId = requestAnimationFrame(renderPhysics);

    // Cleanup
    return () => {
      cancelAnimationFrame(glFrameId);
      cancelAnimationFrame(physicsFrameId);
      window.removeEventListener('resize', resize);
      window.removeEventListener('mousemove', onMouseMove);
      window.removeEventListener('touchmove', onTouchMove);
      window.removeEventListener('mousedown', triggerShockwave);
      window.removeEventListener('touchstart', triggerShockwave);
      window.removeEventListener('mouseup', restoreCursor);
      window.removeEventListener('touchend', restoreCursor);
    };
  }, []);

  return (
    <div className="fluid-landing-container">
      {/* Dynamic Styles Specific to the Component */}
      <style>{`
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&family=Syne:wght@600;700;800&display=swap');

        .fluid-landing-container {
          --bg-color: #020203;
          --text-light: #ffffff;
          --text-muted: #888890;
          --accent: rgba(255, 255, 255, 0.08);
          --border: rgba(255, 255, 255, 0.12);
          
          position: relative;
          width: 100vw;
          height: 100vh;
          overflow: hidden;
          background-color: var(--bg-color);
          font-family: 'Inter', sans-serif;
          color: var(--text-light);
          user-select: none;
          -webkit-tap-highlight-color: transparent;
        }

        .gl-canvas {
          position: absolute;
          top: 0; left: 0;
          width: 100%; height: 100%;
          z-index: 1;
        }

        .mask-container {
          position: absolute;
          top: 0; left: 0;
          width: 100%; height: 100%;
          z-index: 2;
          background-color: var(--bg-color);
          mix-blend-mode: multiply;
          pointer-events: none;
        }

        .physics-canvas {
          width: 100%; height: 100%;
          filter: url(#gooey-filter);
        }

        .texture-overlay {
          position: absolute; top: 0; left: 0;
          width: 100%; height: 100%; z-index: 3;
          pointer-events: none;
          background-image: 
              linear-gradient(var(--border) 1px, transparent 1px),
              linear-gradient(90deg, var(--border) 1px, transparent 1px),
              url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
          background-size: 100px 100px, 100px 100px, 100px 100px;
          background-position: center center;
          opacity: 0.15;
          mask-image: radial-gradient(circle at center, black 20%, transparent 80%);
          -webkit-mask-image: radial-gradient(circle at center, black 20%, transparent 80%);
        }

        .ui-layer {
          position: absolute; top: 0; left: 0;
          width: 100%; height: 100%;
          z-index: 10;
          display: flex; flex-direction: column;
          pointer-events: none;
        }

        .interactive { pointer-events: auto; }

        .landing-nav {
          display: flex; justify-content: space-between; align-items: center;
          padding: 2rem 4rem;
        }

        .landing-logo {
          font-family: 'Syne', sans-serif;
          font-weight: 800; font-size: 1.8rem;
          letter-spacing: -1px; cursor: pointer;
          transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
        }
        .landing-logo:hover { transform: scale(1.08) rotate(-2deg); }

        .landing-nav-links {
          display: flex; gap: 2.5rem;
          background: rgba(0,0,0,0.4);
          padding: 0.75rem 2rem;
          border-radius: 100px;
          border: 1px solid var(--border);
          backdrop-filter: blur(12px);
        }

        .landing-nav-links a {
          color: var(--text-light); text-decoration: none;
          font-size: 0.85rem; font-weight: 500;
          opacity: 0.6; transition: all 0.3s;
          text-transform: uppercase; letter-spacing: 1px;
        }
        .landing-nav-links a:hover { opacity: 1; transform: translateY(-1px); }

        .landing-nav-btn {
          background: #fff; color: #000;
          border: none; padding: 0.75rem 1.8rem;
          border-radius: 100px; font-weight: 600; font-size: 0.85rem;
          cursor: pointer; transition: all 0.3s;
        }
        .landing-nav-btn:hover {
          transform: translateY(-2px);
          box-shadow: 0 10px 25px rgba(255, 255, 255, 0.2);
        }

        .landing-hero {
          flex: 1; display: flex; flex-direction: column;
          justify-content: space-between; align-items: center;
          text-align: center; padding: 4rem 2rem;
        }

        .landing-badge {
          background: var(--accent);
          border: 1px solid var(--border);
          padding: 0.5rem 1.2rem; border-radius: 50px;
          font-size: 0.75rem; font-weight: 500;
          letter-spacing: 2px; text-transform: uppercase;
          backdrop-filter: blur(5px);
          color: var(--text-muted);
        }

        .landing-hero-spacer { height: clamp(100px, 20vw, 300px); }

        .landing-hero-copy {
          max-width: 600px;
          display: flex; flex-direction: column; align-items: center; gap: 2rem;
        }

        .landing-subtitle {
          font-size: clamp(1rem, 1.5vw, 1.2rem);
          font-weight: 300; color: var(--text-muted);
          line-height: 1.6; margin: 0;
        }

        .landing-cta-btn {
          background: transparent; color: #fff;
          border: 1px solid rgba(255,255,255,0.3);
          padding: 1.2rem 3rem; border-radius: 100px;
          font-family: 'Syne', sans-serif;
          font-size: 1.1rem; font-weight: 700;
          cursor: pointer; position: relative; overflow: hidden;
          transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
        }
        .landing-cta-btn:hover {
          border-color: #fff;
          background: rgba(255,255,255,0.1);
          transform: scale(1.05);
          letter-spacing: 2px;
        }

        .hidden-svg { position: absolute; width: 0; height: 0; pointer-events: none; }
      `}</style>

      {/* SVG Gooey Filter */}
      <svg className="hidden-svg">
        <defs>
          <filter id="gooey-filter" x="-20%" y="-20%" width="140%" height="140%">
            <feGaussianBlur in="SourceGraphic" stdDeviation="25" result="blur" />
            <feColorMatrix
              in="blur"
              mode="matrix"
              values="
                1 0 0 0 0  
                0 1 0 0 0  
                0 0 1 0 0  
                0 0 0 70 -30"
              result="gooey"
            />
            <feComposite in="SourceGraphic" in2="gooey" operator="atop" />
          </filter>
        </defs>
      </svg>

      {/* Canvases */}
      <canvas ref={glCanvasRef} className="gl-canvas" />
      
      <div className="mask-container">
        <canvas ref={physicsCanvasRef} className="physics-canvas" />
      </div>

      <div className="texture-overlay" />

      {/* User Interface */}
      <div className="ui-layer">
        <nav className="landing-nav">
          <div
            className="landing-logo interactive"
            onMouseEnter={handleMagnetEnter}
            onMouseLeave={handleMagnetLeave}
          >
            OASIS.
          </div>
          <div className="landing-nav-links interactive">
            {['Engine', 'Showcase', 'Pricing'].map((item) => (
              <a
                key={item}
                href={`#${item.toLowerCase()}`}
                onMouseEnter={handleMagnetEnter}
                onMouseLeave={handleMagnetLeave}
              >
                {item}
              </a>
            ))}
          </div>
          <button
            className="landing-nav-btn interactive"
            onMouseEnter={handleMagnetEnter}
            onMouseLeave={handleMagnetLeave}
          >
            Get Access
          </button>
        </nav>

        <div className="landing-hero">
          <div className="landing-badge">The New Standard for WebGL</div>
          <div className="landing-hero-spacer" />
          <div className="landing-hero-copy">
            <p className="landing-subtitle">
              A physics-driven fluid interface designed to blur the line between
              digital canvas and interactive reality.
            </p>
            <button
              className="landing-cta-btn interactive"
              onMouseEnter={handleMagnetEnter}
              onMouseLeave={handleMagnetLeave}
            >
              Deploy Now
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default FluidLanding;
Tap to expand