Fairy Dust Cursor Effect
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
1'use client';2import React from 'react';3import FairyDustCursor from './FairyDustCursor';45function FairyDustIndex() {6return (7<>8<FairyDustCursor9colors={['#FF0000', '#00FF00', '#0000FF']}10characterSet={['✨', '⭐', '🌟']}11particleSize={24}12particleCount={2}13gravity={0.015}14fadeSpeed={0.97}15initialVelocity={{ min: 0.7, max: 2.0 }}16/>17</>18);19}2021export default FairyDustIndex;22
1'use client';2import React, { useEffect, useRef, useState } from 'react';34interface FairyDustCursorProps {5colors?: string[];6element?: HTMLElement;7characterSet?: string[];8particleSize?: number;9particleCount?: number;10gravity?: number;11fadeSpeed?: number;12initialVelocity?: {13min: number;14max: number;15};16}1718interface Particle {19x: number;20y: number;21character: string;22color: string;23velocity: {24x: number;25y: number;26};27lifeSpan: number;28initialLifeSpan: number;29scale: number;30}3132export const FairyDustCursor: React.FC<FairyDustCursorProps> = ({33colors = ['#D61C59', '#E7D84B', '#1B8798'],34element,35characterSet = ['✨', '⭐', '🌟', '★', '*'],36particleSize = 21,37particleCount = 1,38gravity = 0.02,39fadeSpeed = 0.98,40initialVelocity = { min: 0.5, max: 1.5 },41}) => {42const canvasRef = useRef<HTMLCanvasElement>(null);43const particlesRef = useRef<Particle[]>([]);44const cursorRef = useRef({ x: 0, y: 0 });45const lastPosRef = useRef({ x: 0, y: 0 });46const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });4748useEffect(() => {49const canvas = canvasRef.current;50if (!canvas) return;5152const targetElement = element || document.body;53const context = canvas.getContext('2d');54if (!context) return;5556const updateCanvasSize = () => {57const newWidth = element ? targetElement.clientWidth : window.innerWidth;58const newHeight = element59? targetElement.clientHeight60: window.innerHeight;61setCanvasSize({ width: newWidth, height: newHeight });62};6364updateCanvasSize();65window.addEventListener('resize', updateCanvasSize);6667// Animation frame setup68let animationFrameId: number;6970const createParticle = (x: number, y: number): Particle => {71const randomChar =72characterSet[Math.floor(Math.random() * characterSet.length)];73const randomColor = colors[Math.floor(Math.random() * colors.length)];74const velocityX =75(Math.random() < 0.5 ? -1 : 1) *76(Math.random() * (initialVelocity.max - initialVelocity.min) +77initialVelocity.min);78const velocityY = -(Math.random() * initialVelocity.max);7980return {81x,82y,83character: randomChar,84color: randomColor,85velocity: { x: velocityX, y: velocityY },86lifeSpan: 100,87initialLifeSpan: 100,88scale: 1,89};90};9192const updateParticles = () => {93if (!context) return;94context.clearRect(0, 0, canvasSize.width, canvasSize.height);9596// Update and draw particles97particlesRef.current.forEach((particle, index) => {98// Update position99particle.x += particle.velocity.x;100particle.y += particle.velocity.y;101102// Apply gravity103particle.velocity.y += gravity;104105// Update lifespan and scale106particle.lifeSpan *= fadeSpeed;107particle.scale = Math.max(108particle.lifeSpan / particle.initialLifeSpan,1090110);111112// Draw particle113context.save();114context.font = `${particleSize * particle.scale}px serif`;115context.fillStyle = particle.color;116context.globalAlpha = particle.scale;117context.fillText(particle.character, particle.x, particle.y);118context.restore();119});120121// Remove dead particles122particlesRef.current = particlesRef.current.filter(123(particle) => particle.lifeSpan > 0.1124);125};126127const animate = () => {128updateParticles();129animationFrameId = requestAnimationFrame(animate);130};131132const handleMouseMove = (e: MouseEvent) => {133const rect = element ? targetElement.getBoundingClientRect() : undefined;134const x = element ? e.clientX - rect!.left : e.clientX;135const y = element ? e.clientY - rect!.top : e.clientY;136137cursorRef.current = { x, y };138139const distance = Math.hypot(140cursorRef.current.x - lastPosRef.current.x,141cursorRef.current.y - lastPosRef.current.y142);143144if (distance > 2) {145for (let i = 0; i < particleCount; i++) {146particlesRef.current.push(147createParticle(cursorRef.current.x, cursorRef.current.y)148);149}150lastPosRef.current = { ...cursorRef.current };151}152};153154const handleTouchMove = (e: TouchEvent) => {155e.preventDefault();156const touch = e.touches[0];157const rect = element ? targetElement.getBoundingClientRect() : undefined;158const x = element ? touch.clientX - rect!.left : touch.clientX;159const y = element ? touch.clientY - rect!.top : touch.clientY;160161for (let i = 0; i < particleCount; i++) {162particlesRef.current.push(createParticle(x, y));163}164};165166targetElement.addEventListener('mousemove', handleMouseMove);167targetElement.addEventListener('touchmove', handleTouchMove, {168passive: false,169});170animate();171172return () => {173window.removeEventListener('resize', updateCanvasSize);174targetElement.removeEventListener('mousemove', handleMouseMove);175targetElement.removeEventListener('touchmove', handleTouchMove);176cancelAnimationFrame(animationFrameId);177};178}, [179colors,180element,181characterSet,182particleSize,183particleCount,184gravity,185fadeSpeed,186initialVelocity,187]);188189return (190<canvas191ref={canvasRef}192width={canvasSize.width}193height={canvasSize.height}194style={{195position: element ? 'absolute' : 'fixed',196top: 0,197left: 0,198pointerEvents: 'none',199zIndex: 9999,200}}201/>202);203};204205export default FairyDustCursor;
Prop | Type | Default | Description |
---|---|---|---|
colors | string[] | ['#D61C59', '#E7D84B', '#1B8798'] | Array of colors for the particles. |
element | HTMLElement | undefined | The HTML element where the cursor effect will be applied. If not specified, the effect applies to the document. |
characterSet | string[] | ['✨', '⭐', '🌟', '★', '*'] | Array of characters used for particles. |
particleSize | number | 21 | Size of the particles in pixels. |
particleCount | number | 1 | Number of particles generated per cursor movement event. |
gravity | number | 0.02 | Gravity effect applied to the particles. |
fadeSpeed | number | 0.98 | The fade-out speed of the particles (value between 0 and 1). |
initialVelocity | { min: number, max: number } | { min: 0.5, max: 1.5 } | The initial velocity range for particles. |