All Prompts
All Prompts

GhostScrollText
GhostScrollText: UI-компонент текста с эффектом призрачного слоя, искажающегося за основным текстом при прокрутке. Создает эффект призрака.
by Zhou JasonLive Preview
Prompt
# GhostScrollText
You are given a task to integrate an existing React component in the codebase
~~~/README.md
# GhostScrollText
A high-performance scroll interaction component where text generates "ectoplasm" ghost trails based on scroll velocity. The faster you scroll, the more the ghost layers separate, blur, and skew.
## Features
- **Scroll Velocity Driven**: Animation intensity is directly linked to how fast the user scrolls.
- **Performance Optimized**: Uses GSAP `quickSetter` (via context) and optimized render loops for 60fps performance.
- **Dynamic Distortion**: Applies skew and blur filters dynamically.
- **Customizable**: Control sensitivity, ghost count, and snap-back duration.
## Usage
```tsx
import { GhostScrollText } from '@/sd-components/86929e8c-8c61-4b68-989f-12792e27a2d5';
function MyPage() {
return (
<div className="h-[200vh]"> {/* Needs scrollable area */}
<div className="sticky top-0 h-screen flex items-center justify-center bg-slate-900">
<GhostScrollText
text="VELOCITY"
ghostCount={5}
sensitivity={0.1}
/>
</div>
</div>
);
}
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `text` | `string` | - | The text to display. |
| `className` | `string` | - | Additional classes for the container. |
| `ghostCount` | `number` | `4` | Number of trailing ghost layers. |
| `sensitivity` | `number` | `0.05` | Multiplier for how far ghosts move per unit of scroll velocity. |
| `snapDuration` | `number` | `0.5` | Duration (in seconds) for ghosts to return to origin when scrolling stops. |
## Dependencies
- `gsap`: Core animation engine.
- `clsx`, `tailwind-merge`: Class utility handling.
~~~
~~~/src/App.tsx
import React from 'react';
import { GhostScrollText } from './Component';
import { RefreshCcw } from 'lucide-react';
export default function App() {
const handleReplay = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<div className="min-h-screen bg-[#1A1A1B] font-sans text-slate-100 selection:bg-slate-700 overflow-x-hidden">
{/* Tall Scroll Container to allow generating velocity */}
<div className="h-[400vh] relative">
{/* Sticky viewport wrapper */}
<div className="sticky top-0 h-screen w-full flex flex-col items-center justify-center overflow-hidden">
<div className="absolute top-8 left-8 z-50">
<span className="text-xs font-medium uppercase tracking-widest opacity-50">Ghost Scroll Velocity</span>
</div>
<div className="relative w-full max-w-6xl flex items-center justify-center">
<GhostScrollText
text="GHOST"
ghostCount={5}
sensitivity={0.08}
/>
</div>
<div className="absolute bottom-12 flex flex-col items-center gap-4 opacity-40">
<div className="w-px h-12 bg-gradient-to-b from-transparent via-white to-transparent" />
<span className="text-xs uppercase tracking-[0.2em]">Scroll Fast</span>
</div>
<button
onClick={handleReplay}
className="absolute bottom-8 right-8 p-3 rounded-full bg-white/5 hover:bg-white/10 transition-colors backdrop-blur-sm group z-50"
aria-label="Scroll to top"
>
<RefreshCcw className="w-4 h-4 text-white/70 group-hover:rotate-180 transition-transform duration-500" />
</button>
</div>
</div>
{/* Scroll indicator content blocks to give sense of movement */}
<div className="fixed top-0 left-0 w-full h-full pointer-events-none opacity-[0.03] mix-blend-overlay">
<div className="w-full h-full" style={{ backgroundImage: 'radial-gradient(circle at 50% 50%, #ffffff 1px, transparent 1px)', backgroundSize: '40px 40px' }}></div>
</div>
</div>
);
}
~~~
~~~/package.json
{
"name": "ghost-scroll-text",
"version": "1.0.0",
"description": "A typography component with trailing ghost layers driven by scroll velocity",
"main": "src/Component.tsx",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"gsap": "^3.12.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0",
"lucide-react": "^0.292.0"
}
}
~~~
~~~/src/utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
~~~
~~~/src/Component.tsx
import React, { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { cn } from './utils';
gsap.registerPlugin(ScrollTrigger);
interface GhostScrollTextProps {
text: string;
className?: string;
ghostCount?: number;
sensitivity?: number; // How much they move per unit of velocity
snapDuration?: number; // Time to return to center
}
export function GhostScrollText({
text,
className,
ghostCount = 4,
sensitivity = 0.05,
snapDuration = 0.5,
}: GhostScrollTextProps) {
const containerRef = useRef<HTMLDivElement>(null);
const ghostsRef = useRef<(HTMLSpanElement | null)[]>([]);
useEffect(() => {
const ghosts = ghostsRef.current;
// Create a proxy object to hold the current velocity value for smooth interpolation
const tracker = { velocity: 0 };
const ctx = gsap.context(() => {
ScrollTrigger.create({
trigger: document.body, // Track global scroll
start: "top top",
end: "bottom bottom",
onUpdate: (self) => {
// Smoothly interpolate the velocity to avoid jitter
gsap.to(tracker, {
velocity: self.getVelocity(),
duration: 0.1,
overwrite: true,
onUpdate: () => {
const vel = tracker.velocity;
ghosts.forEach((ghost, i) => {
if (!ghost) return;
// Calculate lag based on index
// The deeper the ghost (higher index), the more it moves
const indexFactor = i + 1;
const moveY = -vel * sensitivity * indexFactor * 0.5;
const skewAngle = vel * 0.02; // Skew based on speed
// Opacity increases with speed, up to a limit
const speed = Math.abs(vel);
const opacity = Math.min(speed * 0.0005 * (1 - i * 0.15), 0.6);
// Blur increases with speed
const blurAmount = Math.min(speed * 0.01, 10);
gsap.set(ghost, {
y: moveY,
skewX: -skewAngle, // Skew opposite to motion for drag effect
opacity: opacity,
filter: `blur(${blurAmount}px)`,
});
});
}
});
},
});
// Cleanup mechanism: When scroll stops, ScrollTrigger doesn't fire updates with 0 velocity immediately in all cases
// So we use a check to snap back if needed, though the tween above handles most of it.
// We can also add a listener for scroll end to ensure clean snap back
ScrollTrigger.addEventListener("scrollEnd", () => {
gsap.to(ghosts, {
y: 0,
skewX: 0,
opacity: 0,
filter: "blur(0px)",
duration: snapDuration,
ease: "power2.out"
});
});
}, containerRef);
return () => ctx.revert();
}, [text, ghostCount, sensitivity, snapDuration]);
return (
<div
ref={containerRef}
className={cn("relative flex items-center justify-center p-8 overflow-visible", className)}
>
{/* Main Anchor Text - Outlined */}
<h1 className="relative z-20 text-9xl font-black tracking-tighter text-transparent select-none transition-colors duration-300 pointer-events-none">
<span
className="block leading-none"
style={{
WebkitTextStroke: '2px rgba(255,255,255,0.8)',
textStroke: '2px rgba(255,255,255,0.8)'
}}
>
{text}
</span>
</h1>
{/* Ghost Layers */}
{Array.from({ length: ghostCount }).map((_, i) => (
<span
key={i}
ref={(el) => (ghostsRef.current[i] = el)}
className="absolute z-10 text-9xl font-black tracking-tighter text-white select-none pointer-events-none top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
style={{
opacity: 0,
willChange: 'transform, opacity, filter',
// Default styling
}}
>
<span className="block leading-none">
{text}
</span>
</span>
))}
</div>
);
}
export default GhostScrollText;
~~~
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