VibeCoderzVibeCoderz
Telegram
All Prompts
animationhover effect

Ribbon

Интерактивный эффект ленты (ribbon) с плавным движением, следуя за курсором. Минималистичный дизайн, монохромная эстетика, анимация при наведении.

by Zhou JasonLive Preview

Prompt

# Ribbon

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

~~~/README.md
# RibbonShowcase

A premium minimalist showcase of an interactive ribbon effect using OGL (WebGL). This component features fluid, spring-based motion that follows mouse or touch movements, creating an atmospheric and high-end visual experience.

## Dependencies

- `ogl`: ^0.0.117
- `framer-motion`: ^11.11.17
- `react`: ^18.3.1
- `react-dom`: ^18.3.1

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `colors` | `string[]` | `['#ff9346', '#7cff67', '#ffee51', '#5227FF']` | Array of hex color strings for the ribbons |
| `baseSpring` | `number` | `0.03` | Spring tension for movement (lower = more fluid) |
| `baseFriction` | `number` | `0.9` | Friction for movement (lower = more drift) |
| `baseThickness` | `number` | `30` | Thickness of the ribbons in pixels |
| `offsetFactor` | `number` | `0.05` | Horizontal offset factor between multiple ribbons |
| `maxAge` | `number` | `500` | Max lifetime/length of the trail |
| `pointCount` | `number` | `50` | Number of points defining each ribbon curve |
| `speedMultiplier` | `number` | `0.6` | Speed multiplier for movement interpolation |
| `enableFade` | `boolean` | `false` | Enable transparency fade at the end of the ribbon |
| `enableShaderEffect` | `boolean` | `false` | Enable wave-like shader distortion effect |
| `effectAmplitude` | `number` | `2` | Amplitude of the shader distortion |
| `backgroundColor` | `number[]` | `[0, 0, 0, 0]` | Background clear color [r, g, b, a] |

## Usage Example

```tsx
import { Ribbons } from '@/sd-components/b8472475-8adc-4656-b93e-725ce5e30657';

export default function MyComponent() {
  return (
    <div style={{ height: '500px', width: '100%', position: 'relative' }}>
      <Ribbons
        baseThickness={40}
        colors={['#1A1A1B']}
        speedMultiplier={0.5}
        maxAge={600}
        enableFade={true}
        enableShaderEffect={true}
        effectAmplitude={1.5}
      />
    </div>
  );
}
```
~~~

~~~/src/App.tsx
/**
 * RibbonShowcase Demo
 * Displays the Ribbons component in a premium, minimalist framing.
 * Adheres to the 'Minimalist Showcase' style guide:
 * - Off-white background (#F9F9F9)
 * - Ample whitespace
 * - Floating soft-shadow container
 * - Monochrome palette with a single brand accent
 */

import React, { useState } from 'react';
import { Ribbons } from './Component';
import { motion, AnimatePresence } from 'framer-motion';

export default function App() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div className="min-h-screen bg-[#F9F9F9] flex items-center justify-center p-20 font-sans">
      <motion.div 
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.6, ease: [0.19, 1, 0.22, 1] }}
        className="relative w-full max-w-4xl aspect-video bg-white rounded-[2rem] shadow-[0_40px_80px_rgba(0,0,0,0.05)] overflow-hidden border-none"
        onMouseEnter={() => setIsHovered(true)}
        onMouseLeave={() => setIsHovered(false)}
      >
        {/* The Animated Element */}
        <div className="absolute inset-0 z-0">
          <Ribbons
            baseThickness={40}
            colors={['#1A1A1B']} // Monochrome base
            speedMultiplier={0.5}
            maxAge={600}
            enableFade={true}
            enableShaderEffect={true}
            effectAmplitude={1.5}
            backgroundColor={[0, 0, 0, 0]}
          />
        </div>

        {/* Content Overlay */}
        <div className="absolute inset-0 z-10 flex flex-col items-center justify-center pointer-events-none p-12 text-center">
          <motion.h1 
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.4, duration: 0.8 }}
            className="text-[2.5rem] font-medium tracking-tight text-[#1A1A1B] mb-2"
          >
            Fluid Motion
          </motion.h1>
          <motion.p
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.6, duration: 0.8 }}
            className="text-[#888888] font-normal"
          >
            A Minimalist WebGL Ribbon Showcase
          </motion.p>
        </div>

        {/* Reply Button (Required by prompt for animations) */}
        <AnimatePresence>
          {isHovered && (
            <motion.button
              initial={{ opacity: 0, scale: 0.9, y: 10 }}
              animate={{ opacity: 1, scale: 1, y: 0 }}
              exit={{ opacity: 0, scale: 0.9, y: 10 }}
              transition={{ duration: 0.3, ease: [0.19, 1, 0.22, 1] }}
              onClick={() => window.location.reload()}
              className="absolute bottom-8 right-8 z-20 px-6 py-3 bg-[#1A1A1B] text-white rounded-full text-sm font-medium shadow-lg hover:bg-black transition-colors pointer-events-auto flex items-center gap-2 group"
            >
              <svg 
                width="16" 
                height="16" 
                viewBox="0 0 24 24" 
                fill="none" 
                stroke="currentColor" 
                strokeWidth="2.5" 
                strokeLinecap="round" 
                strokeLinejoin="round"
                className="group-hover:rotate-[-180deg] transition-transform duration-500"
              >
                <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
                <path d="M3 3v5h5" />
                <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
                <path d="M16 16h5v5" />
              </svg>
              Reply Animation
            </motion.button>
          )}
        </AnimatePresence>

        {/* Interactive Highlight (Single brand accent) */}
        <motion.div 
          className="absolute bottom-0 left-0 h-1 bg-black w-full origin-left"
          initial={{ scaleX: 0 }}
          animate={{ scaleX: isHovered ? 1 : 0 }}
          transition={{ duration: 0.6, ease: [0.19, 1, 0.22, 1] }}
        />
      </motion.div>
    </div>
  );
}
~~~

~~~/package.json
{
  "name": "ribbon-showcase",
  "description": "A premium WebGL interactive ribbon effect showcase.",
  "dependencies": {
    "ogl": "^0.0.117",
    "framer-motion": "^11.11.17",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "lucide-react": "^0.460.0"
  }
}
~~~

~~~/src/Component.tsx
/**
 * Ribbons Component
 * A WebGL-powered interactive ribbon effect using the OGL library.
 * Features fluid, spring-based motion that follows the mouse/touch position.
 * Optimized for performance with customizable thickness, speed, and shader effects.
 */

import React, { useEffect, useRef } from 'react';
import { Renderer, Transform, Vec3, Color, Polyline } from 'ogl';

interface RibbonsProps {
  /** Array of hex color strings for the ribbons */
  colors?: string[];
  /** Spring tension for the movement (lower = more fluid) */
  baseSpring?: number;
  /** Friction for the movement (lower = more drift) */
  baseFriction?: number;
  /** Thickness of the ribbons in pixels */
  baseThickness?: number;
  /** Horizontal offset factor between multiple ribbons */
  offsetFactor?: number;
  /** Max lifetime/length of the trail */
  maxAge?: number;
  /** Number of points defining each ribbon curve */
  pointCount?: number;
  /** Speed multiplier for movement interpolation */
  speedMultiplier?: number;
  /** Enable transparency fade at the end of the ribbon */
  enableFade?: boolean;
  /** Enable wave-like shader distortion effect */
  enableShaderEffect?: boolean;
  /** Amplitude of the shader distortion */
  effectAmplitude?: number;
  /** Background clear color [r, g, b, a] */
  backgroundColor?: number[];
}

export const Ribbons: React.FC<RibbonsProps> = ({
  colors = ['#ff9346', '#7cff67', '#ffee51', '#5227FF'],
  baseSpring = 0.03,
  baseFriction = 0.9,
  baseThickness = 30,
  offsetFactor = 0.05,
  maxAge = 500,
  pointCount = 50,
  speedMultiplier = 0.6,
  enableFade = false,
  enableShaderEffect = false,
  effectAmplitude = 2,
  backgroundColor = [0, 0, 0, 0]
}) => {
  const containerRef = useRef<HTMLDivElement>(null);

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

    const renderer = new Renderer({ dpr: window.devicePixelRatio || 2, alpha: true });
    const gl = renderer.gl;

    if (Array.isArray(backgroundColor) && backgroundColor.length === 4) {
      gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], backgroundColor[3]);
    } else {
      gl.clearColor(0, 0, 0, 0);
    }

    gl.canvas.style.position = 'absolute';
    gl.canvas.style.top = '0';
    gl.canvas.style.left = '0';
    gl.canvas.style.width = '100%';
    gl.canvas.style.height = '100%';
    container.appendChild(gl.canvas);

    const scene = new Transform();
    const lines: {
      spring: number;
      friction: number;
      mouseVelocity: Vec3;
      mouseOffset: Vec3;
      points: Vec3[];
      polyline: Polyline;
    }[] = [];

    const vertex = `
      precision highp float;
      
      attribute vec3 position;
      attribute vec3 next;
      attribute vec3 prev;
      attribute vec2 uv;
      attribute float side;
      
      uniform vec2 uResolution;
      uniform float uDPR;
      uniform float uThickness;
      uniform float uTime;
      uniform float uEnableShaderEffect;
      uniform float uEffectAmplitude;
      
      varying vec2 vUV;
      
      vec4 getPosition() {
          vec4 current = vec4(position, 1.0);
          vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);
          vec2 nextScreen = next.xy * aspect;
          vec2 prevScreen = prev.xy * aspect;
          vec2 tangent = normalize(nextScreen - prevScreen);
          vec2 normal = vec2(-tangent.y, tangent.x);
          normal /= aspect;
          normal *= mix(1.0, 0.1, pow(abs(uv.y - 0.5) * 2.0, 2.0));
          float dist = length(nextScreen - prevScreen);
          normal *= smoothstep(0.0, 0.02, dist);
          float pixelWidthRatio = 1.0 / (uResolution.y / uDPR);
          float pixelWidth = current.w * pixelWidthRatio;
          normal *= pixelWidth * uThickness;
          current.xy -= normal * side;
          if(uEnableShaderEffect > 0.5) {
            current.xy += normal * sin(uTime + current.x * 10.0) * uEffectAmplitude;
          }
          return current;
      }
      
      void main() {
          vUV = uv;
          gl_Position = getPosition();
      }
    `;

    const fragment = `
      precision highp float;
      uniform vec3 uColor;
      uniform float uOpacity;
      uniform float uEnableFade;
      varying vec2 vUV;
      void main() {
          float fadeFactor = 1.0;
          if(uEnableFade > 0.5) {
              fadeFactor = 1.0 - smoothstep(0.0, 1.0, vUV.y);
          }
          gl_FragColor = vec4(uColor, uOpacity * fadeFactor);
      }
    `;

    function resize() {
      if (!container) return;
      const width = container.clientWidth;
      const height = container.clientHeight;
      renderer.setSize(width, height);
      lines.forEach(line => line.polyline.resize());
    }

    window.addEventListener('resize', resize);

    const center = (colors.length - 1) / 2;
    colors.forEach((color, index) => {
      const spring = baseSpring + (Math.random() - 0.5) * 0.05;
      const friction = baseFriction + (Math.random() - 0.5) * 0.05;
      const thickness = baseThickness + (Math.random() - 0.5) * 3;
      const mouseOffset = new Vec3(
        (index - center) * offsetFactor + (Math.random() - 0.5) * 0.01,
        (Math.random() - 0.5) * 0.1,
        0
      );

      const line = {
        spring,
        friction,
        mouseVelocity: new Vec3(),
        mouseOffset,
        points: [] as Vec3[],
        polyline: {} as Polyline
      };

      const count = pointCount;
      const points: Vec3[] = [];
      for (let i = 0; i < count; i++) {
        points.push(new Vec3());
      }
      line.points = points;
      line.polyline = new Polyline(gl, {
        points,
        vertex,
        fragment,
        uniforms: {
          uColor: { value: new Color(color) },
          uThickness: { value: thickness },
          uOpacity: { value: 1.0 },
          uTime: { value: 0.0 },
          uEnableShaderEffect: { value: enableShaderEffect ? 1.0 : 0.0 },
          uEffectAmplitude: { value: effectAmplitude },
          uEnableFade: { value: enableFade ? 1.0 : 0.0 }
        }
      });
      line.polyline.mesh.setParent(scene);
      lines.push(line);
    });

    resize();

    const mouse = new Vec3();
    function updateMouse(e: MouseEvent | TouchEvent) {
      let x: number, y: number;
      if (!container) return;
      const rect = container.getBoundingClientRect();
      if ('changedTouches' in e && e.changedTouches.length) {
        x = e.changedTouches[0].clientX - rect.left;
        y = e.changedTouches[0].clientY - rect.top;
      } else if (e instanceof MouseEvent) {
        x = e.clientX - rect.left;
        y = e.clientY - rect.top;
      } else {
        x = 0;
        y = 0;
      }
      const width = container.clientWidth;
      const height = container.clientHeight;
      mouse.set((x / width) * 2 - 1, (y / height) * -2 + 1, 0);
    }

    container.addEventListener('mousemove', updateMouse);
    container.addEventListener('touchstart', updateMouse, { passive: false });
    container.addEventListener('touchmove', updateMouse, { passive: false });

    let frameId: number;
    let lastTime = performance.now();

    function update() {
      frameId = requestAnimationFrame(update);
      const currentTime = performance.now();
      const dt = currentTime - lastTime;
      lastTime = currentTime;

      lines.forEach(line => {
        const tmp = new Vec3();
        tmp.copy(mouse).add(line.mouseOffset).sub(line.points[0]).multiply(line.spring);
        line.mouseVelocity.add(tmp).multiply(line.friction);
        line.points[0].add(line.mouseVelocity);

        for (let i = 1; i < line.points.length; i++) {
          if (isFinite(maxAge) && maxAge > 0) {
            const segmentDelay = maxAge / (line.points.length - 1);
            const alpha = Math.min(1, (dt * speedMultiplier) / segmentDelay);
            line.points[i].lerp(line.points[i - 1], alpha);
          } else {
            line.points[i].lerp(line.points[i - 1], 0.9);
          }
        }

        if (line.polyline.mesh.program.uniforms.uTime) {
          line.polyline.mesh.program.uniforms.uTime.value = currentTime * 0.001;
        }
        line.polyline.updateGeometry();
      });

      renderer.render({ scene });
    }

    update();

    return () => {
      window.removeEventListener('resize', resize);
      container.removeEventListener('mousemove', updateMouse);
      container.removeEventListener('touchstart', updateMouse);
      container.removeEventListener('touchmove', updateMouse);
      cancelAnimationFrame(frameId);
      if (gl.canvas && gl.canvas.parentNode === container) {
        container.removeChild(gl.canvas);
      }
    };
  }, [
    colors,
    baseSpring,
    baseFriction,
    baseThickness,
    offsetFactor,
    maxAge,
    pointCount,
    speedMultiplier,
    enableFade,
    enableShaderEffect,
    effectAmplitude,
    backgroundColor
  ]);

  return <div ref={containerRef} className="relative w-full h-full cursor-none" />;
};

export default Ribbons;
~~~

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