Copy URL or Code
Paste to your AI coding assistant and say:
“Add this ghost effect to my 404 page”
Done. Your AI handles the rest.
Fully customizable. Ask your AI to adjust the glow color, follow speed, ghost size, or eye color.
Cursor replacement. “Make my mouse change to this ghost when hovering over the hero section.”
/*
================================================================================
AI COMPONENT: GhostFollow
================================================================================
SETUP:
1. Install: npm install three @types/three
2. Create file: components/GhostFollow.tsx
3. Copy this entire code block into that file
4. Import: import GhostFollow from "@/components/GhostFollow"
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default | Description |
|-------------------|--------|-----------|-----------------------------------|
| bodyColor | string | #0f2027 | Ghost body color |
| glowColor | string | #ff4500 | Glow color (orange) |
| eyeGlowColor | string | #00ff00 | Eye glow color (green) |
| ghostOpacity | number | 0.88 | Ghost body opacity |
| ghostScale | number | 2.4 | Ghost size scale |
| emissiveIntensity | number | 5.8 | Glow brightness |
| followSpeed | number | 0.075 | How fast ghost follows cursor |
| wobbleAmount | number | 0.35 | Body wobble intensity |
| floatSpeed | number | 1.6 | Floating animation speed |
| movementThreshold | number | 0.07 | Movement needed for eye glow |
| width | number | 600 | Container width |
| height | number | 400 | Container height |
| className | string | "" | Additional CSS classes |
================================================================================
COLOR IDEAS
================================================================================
SPOOKY (default):
glowColor="#ff4500" eyeGlowColor="#00ff00"
ETHEREAL:
glowColor="#00ffff" eyeGlowColor="#ff00ff"
FRIENDLY:
glowColor="#ffff00" eyeGlowColor="#00ff00"
DARK:
glowColor="#9400d3" eyeGlowColor="#ff0040"
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useEffect, useRef } from "react";
export interface GhostFollowProps {
bodyColor?: string;
glowColor?: string;
eyeGlowColor?: string;
ghostOpacity?: number;
ghostScale?: number;
emissiveIntensity?: number;
followSpeed?: number;
wobbleAmount?: number;
floatSpeed?: number;
movementThreshold?: number;
width?: number;
height?: number;
className?: string;
}
export function GhostFollow({
bodyColor = "#0f2027",
glowColor = "#ff4500",
eyeGlowColor = "#00ff00",
ghostOpacity = 0.88,
ghostScale = 2.4,
emissiveIntensity = 5.8,
followSpeed = 0.075,
wobbleAmount = 0.35,
floatSpeed = 1.6,
movementThreshold = 0.07,
width = 600,
height = 400,
className = "",
}: GhostFollowProps) {
const containerRef = useRef<HTMLDivElement>(null);
const isInitializedRef = useRef(false);
const cleanupRef = useRef<(() => void) | null>(null);
useEffect(() => {
if (!containerRef.current || isInitializedRef.current) return;
const initScene = async () => {
try {
const THREE = await import("three");
if (!containerRef.current) return;
const container = containerRef.current;
const scene = new THREE.Scene();
scene.background = null;
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.z = 20;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, premultipliedAlpha: false });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x000000, 0);
container.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0x0a0a2e, 0.08);
scene.add(ambientLight);
const ghostGroup = new THREE.Group();
ghostGroup.scale.set(ghostScale, ghostScale, ghostScale);
scene.add(ghostGroup);
// Ghost geometry with wavy bottom
const ghostGeometry = new THREE.SphereGeometry(2, 40, 40);
const positions = ghostGeometry.getAttribute("position").array as Float32Array;
for (let i = 0; i < positions.length; i += 3) {
if (positions[i + 1] < -0.2) {
const x = positions[i], z = positions[i + 2];
positions[i + 1] = -2.0 + Math.sin(x * 5) * 0.35 + Math.cos(z * 4) * 0.25 + Math.sin((x + z) * 3) * 0.15;
}
}
ghostGeometry.computeVertexNormals();
const ghostMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(bodyColor),
transparent: true,
opacity: ghostOpacity,
emissive: new THREE.Color(glowColor),
emissiveIntensity,
roughness: 0.02,
metalness: 0.0,
side: THREE.DoubleSide,
alphaTest: 0.1,
});
const ghostBody = new THREE.Mesh(ghostGeometry, ghostMaterial);
ghostGroup.add(ghostBody);
// Rim lights
const rimLight1 = new THREE.DirectionalLight(0x4a90e2, 1.8);
rimLight1.position.set(-8, 6, -4);
scene.add(rimLight1);
const rimLight2 = new THREE.DirectionalLight(0x50e3c2, 1.26);
rimLight2.position.set(8, -4, -6);
scene.add(rimLight2);
// Eyes
const eyeGroup = new THREE.Group();
ghostGroup.add(eyeGroup);
const socketGeometry = new THREE.SphereGeometry(0.45, 16, 16);
const socketMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const leftSocket = new THREE.Mesh(socketGeometry, socketMaterial);
leftSocket.position.set(-0.7, 0.6, 1.9);
leftSocket.scale.set(1.1, 1.0, 0.6);
eyeGroup.add(leftSocket);
const rightSocket = leftSocket.clone();
rightSocket.position.set(0.7, 0.6, 1.9);
eyeGroup.add(rightSocket);
const eyeGeometry = new THREE.SphereGeometry(0.3, 12, 12);
const eyeColor = new THREE.Color(eyeGlowColor);
const leftEyeMaterial = new THREE.MeshBasicMaterial({ color: eyeColor, transparent: true, opacity: 0 });
const leftEye = new THREE.Mesh(eyeGeometry, leftEyeMaterial);
leftEye.position.set(-0.7, 0.6, 2.0);
eyeGroup.add(leftEye);
const rightEyeMaterial = new THREE.MeshBasicMaterial({ color: eyeColor, transparent: true, opacity: 0 });
const rightEye = new THREE.Mesh(eyeGeometry, rightEyeMaterial);
rightEye.position.set(0.7, 0.6, 2.0);
eyeGroup.add(rightEye);
const outerGlowGeometry = new THREE.SphereGeometry(0.525, 12, 12);
const leftOuterGlowMaterial = new THREE.MeshBasicMaterial({ color: eyeColor, transparent: true, opacity: 0, side: THREE.BackSide });
const leftOuterGlow = new THREE.Mesh(outerGlowGeometry, leftOuterGlowMaterial);
leftOuterGlow.position.set(-0.7, 0.6, 1.95);
eyeGroup.add(leftOuterGlow);
const rightOuterGlowMaterial = new THREE.MeshBasicMaterial({ color: eyeColor, transparent: true, opacity: 0, side: THREE.BackSide });
const rightOuterGlow = new THREE.Mesh(outerGlowGeometry, rightOuterGlowMaterial);
rightOuterGlow.position.set(0.7, 0.6, 1.95);
eyeGroup.add(rightOuterGlow);
const mouse = { x: 0, y: 0 };
let currentMovement = 0, time = 0, animationId: number;
const handleMouseMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / height) * 2 + 1;
};
container.addEventListener("mousemove", handleMouseMove);
const animate = () => {
animationId = requestAnimationFrame(animate);
time += 0.01;
const targetX = mouse.x * 11, targetY = mouse.y * 7;
const prevX = ghostGroup.position.x, prevY = ghostGroup.position.y;
ghostGroup.position.x += (targetX - ghostGroup.position.x) * followSpeed;
ghostGroup.position.y += (targetY - ghostGroup.position.y) * followSpeed;
const dx = ghostGroup.position.x - prevX, dy = ghostGroup.position.y - prevY;
currentMovement = currentMovement * 0.95 + Math.sqrt(dx * dx + dy * dy) * 0.05;
ghostGroup.position.y += Math.sin(time * floatSpeed * 1.5) * 0.03 + Math.cos(time * floatSpeed * 0.7) * 0.018;
ghostMaterial.emissiveIntensity = emissiveIntensity + Math.sin(time * 1.6) * 0.6;
const mouseDir = { x: targetX - ghostGroup.position.x, y: targetY - ghostGroup.position.y };
const len = Math.sqrt(mouseDir.x * mouseDir.x + mouseDir.y * mouseDir.y) || 1;
mouseDir.x /= len; mouseDir.y /= len;
ghostBody.rotation.z = ghostBody.rotation.z * 0.95 - mouseDir.x * 0.1 * wobbleAmount * 0.05;
ghostBody.rotation.x = ghostBody.rotation.x * 0.95 + mouseDir.y * 0.1 * wobbleAmount * 0.05;
ghostBody.rotation.y = Math.sin(time * 1.4) * 0.05 * wobbleAmount;
const scale = (1 + Math.sin(time * 2.1) * 0.025 * wobbleAmount) * (1 + Math.sin(time * 0.8) * 0.012);
ghostBody.scale.set(scale, scale, scale);
const isMoving = currentMovement > movementThreshold;
const targetGlow = isMoving ? 1.0 : 0.0;
const newOpacity = leftEyeMaterial.opacity + (targetGlow - leftEyeMaterial.opacity) * (isMoving ? 0.62 : 0.31);
leftEyeMaterial.opacity = rightEyeMaterial.opacity = newOpacity;
leftOuterGlowMaterial.opacity = rightOuterGlowMaterial.opacity = newOpacity * 0.3;
renderer.render(scene, camera);
};
animate();
isInitializedRef.current = true;
cleanupRef.current = () => {
cancelAnimationFrame(animationId);
container.removeEventListener("mousemove", handleMouseMove);
renderer.dispose();
ghostGeometry.dispose();
ghostMaterial.dispose();
if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement);
};
} catch (error) {
console.error("Failed to initialize GhostFollow:", error);
}
};
initScene();
return () => { cleanupRef.current?.(); cleanupRef.current = null; isInitializedRef.current = false; };
}, [bodyColor, glowColor, eyeGlowColor, ghostOpacity, ghostScale, emissiveIntensity, followSpeed, wobbleAmount, floatSpeed, movementThreshold, width, height]);
return <div ref={containerRef} className={`ghost-follow-container ${className}`} style={{ width, height, position: "relative", overflow: "hidden", borderRadius: 16 }} />;
}
export default GhostFollow;import GhostFollow from "@/components/GhostFollow";
// Basic usage
export default function MyPage() {
return (
<div className="flex items-center justify-center h-screen bg-gray-100">
<GhostFollow />
</div>
);
}
// Custom colors
export function EtherealGhost() {
return (
<GhostFollow
glowColor="#00ffff"
eyeGlowColor="#ff00ff"
width={800}
height={500}
/>
);
}
// Faster, more responsive ghost
export function QuickGhost() {
return (
<GhostFollow
followSpeed={0.15}
wobbleAmount={0.5}
/>
);
}
// Subtle ghost
export function SubtleGhost() {
return (
<GhostFollow
ghostOpacity={0.6}
emissiveIntensity={3}
ghostScale={1.5}
/>
);
}