VibeCoderzVibeCoderz
Telegram
All Prompts
animationbackground

Ballpit Background

3D интерактивный фон с физикой и анимированными шарами. Идеально для создания уникального пользовательского опыта.

by Zhou JasonLive Preview

Prompt

# Ballpit Background

You are given a task to integrate an existing React component in the codebase

~~~/README.md
# BallpitBackground

A high-performance interactive 3D background featuring physics-based spheres that react to gravity, friction, and user pointer movements.

## Features
- **3D Physics**: Real-time sphere-to-sphere and sphere-to-wall collisions.
- **Instanced Rendering**: Optimized for high performance even with hundreds of objects.
- **Interactive**: Spheres follow or react to the user's cursor/touch.
- **Custom Shaders**: Enhanced physical material for subsurface scattering-like visuals.
- **Responsive**: Automatically adjusts physics boundaries to container size.

## Dependencies
- `three`: ^0.170.0
- `gsap`: ^3.12.5
- `lucide-react`: ^0.454.0

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `count` | `number` | `200` | Number of spheres to render. |
| `colors` | `string[]` | `['#ffffff', '#888888', '#444444']` | Array of colors for the spheres. |
| `gravity` | `number` | `0.5` | Strength of the downward pull. |
| `friction` | `number` | `0.9975` | Velocity decay factor (1 = no friction). |
| `wallBounce` | `number` | `0.95` | Energy conservation factor on wall hit. |
| `followCursor` | `boolean` | `true` | Whether a lead sphere should track the pointer. |
| `minSize` | `number` | `0.5` | Minimum radius of spheres. |
| `maxSize` | `number` | `1.0` | Maximum radius of spheres. |
| `lightIntensity` | `number` | `200` | Intensity of the interactive point light. |

## Usage

```tsx
import { BallpitBackground } from '@/sd-components/6f929b17-114f-4ebe-9b32-6443ea884cd2';

function Example() {
  return (
    <div className="h-screen w-full">
      <BallpitBackground 
        count={100}
        gravity={0.3}
        colors={['#ff0000', '#00ff00', '#0000ff']}
      />
    </div>
  );
}
```
~~~

~~~/src/App.tsx
import React, { useState } from 'react';
import { BallpitBackground } from './Component';
import { RefreshCcw } from 'lucide-react';

/**
 * App Demo
 * Showcases the BallpitBackground component in a minimalist environment.
 * Includes a "Reply/Reset" button as requested.
 */
export default function App() {
  const [resetKey, setResetKey] = useState(0);

  const handleReset = () => {
    setResetKey(prev => prev + 1);
  };

  return (
    <div className="relative w-full h-screen bg-[#F9F9F9] flex items-center justify-center p-20">
      {/* Premium Floating Container */}
      <div className="relative w-full h-full bg-white rounded-[40px] overflow-hidden shadow-[0_40px_100px_rgba(0,0,0,0.05)] border-none">
        
        {/* The Background Component */}
        <BallpitBackground 
          key={resetKey}
          count={150}
          colors={['#1A1A1B', '#3B82F6', '#94A3B8']}
          gravity={0.4}
          friction={0.998}
          followCursor={true}
        />

        {/* Minimalist Overlay */}
        <div className="absolute inset-0 pointer-events-none flex flex-col items-center justify-center">
          <h1 className="text-[#1A1A1B] text-4xl font-medium tracking-tight mb-2 opacity-80">
            Ballpit Background
          </h1>
          <p className="text-[#94A3B8] text-sm tracking-wide uppercase font-normal opacity-60">
            3D Physics Interaction
          </p>
        </div>

        {/* Reply/Action Button */}
        <button 
          onClick={handleReset}
          className="absolute bottom-10 right-10 p-4 bg-[#1A1A1B] text-white rounded-full shadow-lg hover:scale-105 transition-transform active:scale-95 flex items-center justify-center group"
          aria-label="Reset Animation"
        >
          <RefreshCcw className="w-5 h-5 group-active:rotate-180 transition-transform duration-500" />
        </button>
      </div>
    </div>
  );
}
~~~

~~~/package.json
{
  "name": "ballpit-background",
  "description": "Interactive 3D physics-based background with bouncing spheres.",
  "dependencies": {
    "three": "^0.170.0",
    "gsap": "^3.12.5",
    "lucide-react": "^0.454.0",
    "framer-motion": "^11.11.11",
    "clsx": "^2.1.1",
    "tailwind-merge": "^2.5.4"
  }
}
~~~

~~~/src/Component.tsx
/**
 * BallpitBackground Component
 * 
 * A high-performance 3D background featuring interactive spheres that react to gravity,
 * friction, and user interaction. Uses Three.js InstancedMesh for rendering efficiency
 * and a custom physical material for advanced lighting effects.
 */

import React, { useRef, useEffect, useMemo } from 'react';
import * as THREE from 'three';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
import { gsap } from 'gsap';
import { Observer } from 'gsap/Observer';

gsap.registerPlugin(Observer);

// --- Custom Material for Subsurface Scattering-like effect ---
class PhysicalScatteringMaterial extends THREE.MeshPhysicalMaterial {
  uniforms: { [key: string]: { value: any } } = {
    thicknessDistortion: { value: 0.1 },
    thicknessAmbient: { value: 0 },
    thicknessAttenuation: { value: 0.1 },
    thicknessPower: { value: 2 },
    thicknessScale: { value: 10 }
  };

  constructor(params: THREE.MeshPhysicalMaterialParameters) {
    super(params);
    this.defines = { USE_UV: '' };
    this.onBeforeCompile = (shader) => {
      Object.assign(shader.uniforms, this.uniforms);
      shader.fragmentShader = `
        uniform float thicknessPower;
        uniform float thicknessScale;
        uniform float thicknessDistortion;
        uniform float thicknessAmbient;
        uniform float thicknessAttenuation;
        ${shader.fragmentShader}
      `;
      
      shader.fragmentShader = shader.fragmentShader.replace(
        'void main() {',
        `
        void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {
          vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));
          float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;
          #ifdef USE_COLOR
            vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;
          #else
            vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;
          #endif
          reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;
        }
        void main() {
        `
      );

      const lightsChunk = THREE.ShaderChunk.lights_fragment_begin.replaceAll(
        'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',
        `
          RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
          RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);
        `
      );
      shader.fragmentShader = shader.fragmentShader.replace('#include <lights_fragment_begin>', lightsChunk);
    };
  }
}

// --- Physics Engine ---
class PhysicsWorld {
  config: BallpitProps;
  positionData: Float32Array;
  velocityData: Float32Array;
  sizeData: Float32Array;
  center: THREE.Vector3 = new THREE.Vector3();

  constructor(config: BallpitProps) {
    this.config = config;
    const count = config.count || 200;
    this.positionData = new Float32Array(3 * count).fill(0);
    this.velocityData = new Float32Array(3 * count).fill(0);
    this.sizeData = new Float32Array(count).fill(1);
    this.initialize();
  }

  initialize() {
    const count = this.config.count || 200;
    const maxX = this.config.maxX || 5;
    const maxY = this.config.maxY || 5;
    const maxZ = this.config.maxZ || 2;
    const minSize = this.config.minSize || 0.5;
    const maxSize = this.config.maxSize || 1;

    for (let i = 0; i < count; i++) {
      const idx = 3 * i;
      this.positionData[idx] = THREE.MathUtils.randFloatSpread(2 * maxX);
      this.positionData[idx + 1] = THREE.MathUtils.randFloatSpread(2 * maxY);
      this.positionData[idx + 2] = THREE.MathUtils.randFloatSpread(2 * maxZ);
      this.sizeData[i] = THREE.MathUtils.randFloat(minSize, maxSize);
    }
  }

  update(delta: number) {
    const count = this.config.count || 200;
    const gravity = this.config.gravity ?? 0.5;
    const friction = this.config.friction ?? 0.9975;
    const wallBounce = this.config.wallBounce ?? 0.95;
    const maxVelocity = this.config.maxVelocity ?? 0.15;
    const maxX = this.config.maxX || 5;
    const maxY = this.config.maxY || 5;
    const maxZ = this.config.maxZ || 2;
    const followCursor = this.config.followCursor ?? true;

    // First sphere (index 0) is the "follower" if enabled
    let startIdx = 0;
    if (followCursor) {
      startIdx = 1;
      const firstPos = new THREE.Vector3().fromArray(this.positionData, 0);
      firstPos.lerp(this.center, 0.1).toArray(this.positionData, 0);
      new THREE.Vector3(0, 0, 0).toArray(this.velocityData, 0);
    }

    for (let i = startIdx; i < count; i++) {
      const base = 3 * i;
      const pos = new THREE.Vector3().fromArray(this.positionData, base);
      const vel = new THREE.Vector3().fromArray(this.velocityData, base);
      const radius = this.sizeData[i];

      // Gravity & Velocity
      vel.y -= delta * gravity * radius;
      vel.multiplyScalar(friction);
      vel.clampLength(0, maxVelocity);
      pos.add(vel);

      // Collisions with other spheres (simplified)
      for (let j = i + 1; j < count; j++) {
        const otherBase = 3 * j;
        const otherPos = new THREE.Vector3().fromArray(this.positionData, otherBase);
        const diff = new THREE.Vector3().copy(otherPos).sub(pos);
        const dist = diff.length();
        const sumRadius = radius + this.sizeData[j];

        if (dist < sumRadius) {
          const overlap = sumRadius - dist;
          const correction = diff.normalize().multiplyScalar(0.5 * overlap);
          pos.sub(correction);
          otherPos.add(correction);
          
          // Simple impulse transfer
          const relVel = new THREE.Vector3().fromArray(this.velocityData, otherBase).sub(vel);
          const impulse = correction.clone().multiplyScalar(relVel.dot(correction.normalize()));
          vel.add(impulse);
          
          otherPos.toArray(this.positionData, otherBase);
        }
      }

      // Special interaction with follower sphere
      if (followCursor) {
        const followerPos = new THREE.Vector3().fromArray(this.positionData, 0);
        const diff = new THREE.Vector3().copy(followerPos).sub(pos);
        const d = diff.length();
        const sumRadius = radius + this.sizeData[0];
        if (d < sumRadius) {
          const correction = diff.normalize().multiplyScalar(sumRadius - d);
          pos.sub(correction);
          vel.sub(correction.multiplyScalar(0.2));
        }
      }

      // Boundary Collisions
      if (Math.abs(pos.x) + radius > maxX) {
        pos.x = Math.sign(pos.x) * (maxX - radius);
        vel.x *= -wallBounce;
      }
      if (pos.y - radius < -maxY) {
        pos.y = -maxY + radius;
        vel.y *= -wallBounce;
      } else if (gravity === 0 && pos.y + radius > maxY) {
         pos.y = maxY - radius;
         vel.y *= -wallBounce;
      }
      if (Math.abs(pos.z) + radius > maxZ) {
        pos.z = Math.sign(pos.z) * (maxZ - radius);
        vel.z *= -wallBounce;
      }

      pos.toArray(this.positionData, base);
      vel.toArray(this.velocityData, base);
    }
  }
}

// --- Main Component ---
export interface BallpitProps {
  count?: number;
  colors?: string[];
  ambientColor?: string;
  ambientIntensity?: number;
  lightIntensity?: number;
  minSize?: number;
  maxSize?: number;
  gravity?: number;
  friction?: number;
  wallBounce?: number;
  maxVelocity?: number;
  maxX?: number;
  maxY?: number;
  maxZ?: number;
  followCursor?: boolean;
  className?: string;
}

export const BallpitBackground: React.FC<BallpitProps> = ({
  count = 200,
  colors = ['#ffffff', '#888888', '#444444'],
  ambientColor = '#ffffff',
  ambientIntensity = 1,
  lightIntensity = 200,
  minSize = 0.5,
  maxSize = 1,
  gravity = 0.5,
  friction = 0.9975,
  wallBounce = 0.95,
  maxVelocity = 0.15,
  followCursor = true,
  className = ""
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  
  useEffect(() => {
    if (!canvasRef.current || !containerRef.current) return;

    const canvas = canvasRef.current;
    const parent = containerRef.current;
    
    // Setup Renderer
    const renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: true,
      alpha: true,
      powerPreference: 'high-performance'
    });
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

    // Scene & Environment
    const scene = new THREE.Scene();
    const roomEnv = new RoomEnvironment();
    const pmrem = new THREE.PMREMGenerator(renderer);
    const envTexture = pmrem.fromScene(roomEnv).texture;

    // Camera
    const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 1000);
    camera.position.z = 20;

    // Geometry & Material
    const geometry = new THREE.SphereGeometry(1, 32, 32);
    const material = new PhysicalScatteringMaterial({
      envMap: envTexture,
      metalness: 0.5,
      roughness: 0.5,
      clearcoat: 1,
      clearcoatRoughness: 0.15
    });

    // Instanced Mesh
    const imesh = new THREE.InstancedMesh(geometry, material, count);
    scene.add(imesh);

    // Lights
    const ambient = new THREE.AmbientLight(ambientColor, ambientIntensity);
    scene.add(ambient);

    const pointLight = new THREE.PointLight(colors[0], lightIntensity);
    scene.add(pointLight);

    // Physics
    const config = {
      count, minSize, maxSize, gravity, friction, wallBounce, 
      maxVelocity, followCursor, maxX: 5, maxY: 5, maxZ: 2
    };
    const physics = new PhysicsWorld(config);

    // Set Instance Colors
    const threeColors = colors.map(c => new THREE.Color(c));
    for (let i = 0; i < count; i++) {
      const color = threeColors[i % threeColors.length];
      imesh.setColorAt(i, color);
    }
    imesh.instanceColor!.needsUpdate = true;

    // Raycasting for Interaction
    const raycaster = new THREE.Raycaster();
    const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
    const intersection = new THREE.Vector3();
    const pointer = new THREE.Vector2();

    const updatePointer = (e: MouseEvent | TouchEvent) => {
      const x = 'touches' in e ? e.touches[0].clientX : e.clientX;
      const y = 'touches' in e ? e.touches[0].clientY : e.clientY;
      const rect = canvas.getBoundingClientRect();
      pointer.x = ((x - rect.left) / rect.width) * 2 - 1;
      pointer.y = -((y - rect.top) / rect.height) * 2 + 1;
      
      raycaster.setFromCamera(pointer, camera);
      raycaster.ray.intersectPlane(plane, intersection);
      physics.center.copy(intersection);
    };

    window.addEventListener('mousemove', updatePointer);
    window.addEventListener('touchstart', updatePointer);
    window.addEventListener('touchmove', updatePointer);

    // Resize Logic
    const resize = () => {
      const w = parent.offsetWidth;
      const h = parent.offsetHeight;
      renderer.setSize(w, h);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();

      // Update World Boundaries
      const fovRad = (camera.fov * Math.PI) / 180;
      const wHeight = 2 * Math.tan(fovRad / 2) * camera.position.z;
      const wWidth = wHeight * camera.aspect;
      physics.config.maxX = wWidth / 2;
      physics.config.maxY = wHeight / 2;
    };
    
    const resizeObserver = new ResizeObserver(resize);
    resizeObserver.observe(parent);
    resize();

    // Animation Loop
    let animationFrameId: number;
    const clock = new THREE.Clock();
    const dummy = new THREE.Object3D();

    const animate = () => {
      const delta = clock.getDelta();
      physics.update(Math.min(delta, 0.1));

      for (let i = 0; i < count; i++) {
        dummy.position.fromArray(physics.positionData, i * 3);
        const s = physics.sizeData[i];
        
        // Hide follower if not needed
        if (i === 0 && !followCursor) {
          dummy.scale.setScalar(0);
        } else {
          dummy.scale.setScalar(s);
        }
        
        dummy.updateMatrix();
        imesh.setMatrixAt(i, dummy.matrix);
        
        if (i === 0) pointLight.position.copy(dummy.position);
      }
      imesh.instanceMatrix.needsUpdate = true;

      renderer.render(scene, camera);
      animationFrameId = requestAnimationFrame(animate);
    };
    animate();

    return () => {
      window.removeEventListener('mousemove', updatePointer);
      window.removeEventListener('touchstart', updatePointer);
      window.removeEventListener('touchmove', updatePointer);
      resizeObserver.disconnect();
      cancelAnimationFrame(animationFrameId);
      renderer.dispose();
      geometry.dispose();
      material.dispose();
      pmrem.dispose();
      roomEnv.dispose();
    };
  }, [count, colors, ambientColor, ambientIntensity, lightIntensity, minSize, maxSize, gravity, friction, wallBounce, maxVelocity, followCursor]);

  return (
    <div ref={containerRef} className={`relative w-full h-full overflow-hidden ${className}`}>
      <canvas ref={canvasRef} className="block w-full h-full outline-none" />
    </div>
  );
};

export default BallpitBackground;
~~~

Implementation Guidelines

1. Analyze the component structure, styling, animation implementations
2. Review the component's arguments and state
3. Think through what is the best place to adopt this component/style into the design we are doing
4. Then adopt the component/design to our current system

Help me integrate this into my design
All Prompts