Snowflake Cursor Effect
An interactive React component that adds a dynamic snowflake effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic snowflake effect, visually tracking cursor movement in real time.
1'use client';23import React, { useEffect, useRef } from 'react';45interface SnowflakeCursorOptions {6element?: HTMLElement;7zIndex?: number;8}910const SnowflakeCursor: React.FC<SnowflakeCursorOptions> = ({ element, zIndex }) => {11const canvasRef = useRef<HTMLCanvasElement | null>(null);12const particles = useRef<any[]>([]);13const canvImages = useRef<HTMLCanvasElement[]>([]);14const animationFrame = useRef<number | null>(null);15const possibleEmoji = ['❄️'];16const prefersReducedMotion = useRef<MediaQueryList | null>(null);1718useEffect(() => {19// Check if window is defined (to ensure code runs on client-side)20if (typeof window === 'undefined') return;2122prefersReducedMotion.current = window.matchMedia(23'(prefers-reduced-motion: reduce)'24);2526const canvas = document.createElement('canvas');27const context = canvas.getContext('2d');28if (!context) return;2930const targetElement = element || document.body;3132canvas.style.position = element ? 'absolute' : 'fixed';33canvas.style.top = '0';34canvas.style.left = '0';35canvas.style.pointerEvents = 'none';36canvas.style.zIndex = zIndex ? zIndex.toString() : '';3738targetElement.appendChild(canvas);39canvasRef.current = canvas;4041const setCanvasSize = () => {42canvas.width = element ? targetElement.clientWidth : window.innerWidth;43canvas.height = element ? targetElement.clientHeight : window.innerHeight;44};4546const createEmojiImages = () => {47context.font = '12px serif';48context.textBaseline = 'middle';49context.textAlign = 'center';5051possibleEmoji.forEach((emoji) => {52const measurements = context.measureText(emoji);53const bgCanvas = document.createElement('canvas');54const bgContext = bgCanvas.getContext('2d');55if (!bgContext) return;5657bgCanvas.width = measurements.width;58bgCanvas.height = measurements.actualBoundingBoxAscent * 2;5960bgContext.textAlign = 'center';61bgContext.font = '12px serif';62bgContext.textBaseline = 'middle';63bgContext.fillText(64emoji,65bgCanvas.width / 2,66measurements.actualBoundingBoxAscent67);6869canvImages.current.push(bgCanvas);70});71};7273const addParticle = (x: number, y: number) => {74const randomImage =75canvImages.current[76Math.floor(Math.random() * canvImages.current.length)77];78particles.current.push(new Particle(x, y, randomImage));79};8081const onMouseMove = (e: MouseEvent) => {82const x = element83? e.clientX - targetElement.getBoundingClientRect().left84: e.clientX;85const y = element86? e.clientY - targetElement.getBoundingClientRect().top87: e.clientY;88addParticle(x, y);89};9091const updateParticles = () => {92if (!context || !canvas) return;9394context.clearRect(0, 0, canvas.width, canvas.height);9596particles.current.forEach((particle, index) => {97particle.update(context);98if (particle.lifeSpan < 0) {99particles.current.splice(index, 1);100}101});102};103104const animationLoop = () => {105updateParticles();106animationFrame.current = requestAnimationFrame(animationLoop);107};108109const init = () => {110if (prefersReducedMotion.current?.matches) return;111112setCanvasSize();113createEmojiImages();114115targetElement.addEventListener('mousemove', onMouseMove);116window.addEventListener('resize', setCanvasSize);117118animationLoop();119};120121const destroy = () => {122if (canvasRef.current) {123canvasRef.current.remove();124}125if (animationFrame.current) {126cancelAnimationFrame(animationFrame.current);127}128targetElement.removeEventListener('mousemove', onMouseMove);129window.removeEventListener('resize', setCanvasSize);130};131132prefersReducedMotion.current.onchange = () => {133if (prefersReducedMotion.current?.matches) {134destroy();135} else {136init();137}138};139140init();141return () => destroy();142}, [element]);143144return null;145};146147/**148* Particle Class149*/150class Particle {151position: { x: number; y: number };152velocity: { x: number; y: number };153lifeSpan: number;154initialLifeSpan: number;155canv: HTMLCanvasElement;156157constructor(x: number, y: number, canvasItem: HTMLCanvasElement) {158this.position = { x, y };159this.velocity = {160x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 2),161y: 1 + Math.random(),162};163this.lifeSpan = Math.floor(Math.random() * 60 + 80);164this.initialLifeSpan = this.lifeSpan;165this.canv = canvasItem;166}167168update(context: CanvasRenderingContext2D) {169this.position.x += this.velocity.x;170this.position.y += this.velocity.y;171this.lifeSpan--;172173this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75;174this.velocity.y -= Math.random() / 300;175176const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0);177178context.save();179context.translate(this.position.x, this.position.y);180context.scale(scale, scale);181context.drawImage(this.canv, -this.canv.width / 2, -this.canv.height / 2);182context.restore();183}184}185186export default SnowflakeCursor;187