Snowflake 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';23import React, { useEffect, useRef } from 'react';45interface SnowflakeCursorOptions {6element?: HTMLElement;7}89const SnowflakeCursor: React.FC<SnowflakeCursorOptions> = ({ element }) => {10const canvasRef = useRef<HTMLCanvasElement | null>(null);11const particles = useRef<any[]>([]);12const canvImages = useRef<HTMLCanvasElement[]>([]);13const animationFrame = useRef<number | null>(null);14const possibleEmoji = ['❄️'];15const prefersReducedMotion = useRef<MediaQueryList | null>(null);1617useEffect(() => {18// Check if window is defined (to ensure code runs on client-side)19if (typeof window === 'undefined') return;2021prefersReducedMotion.current = window.matchMedia(22'(prefers-reduced-motion: reduce)'23);2425const canvas = document.createElement('canvas');26const context = canvas.getContext('2d');27if (!context) return;2829const targetElement = element || document.body;3031canvas.style.position = element ? 'absolute' : 'fixed';32canvas.style.top = '0';33canvas.style.left = '0';34canvas.style.pointerEvents = 'none';3536targetElement.appendChild(canvas);37canvasRef.current = canvas;3839const setCanvasSize = () => {40canvas.width = element ? targetElement.clientWidth : window.innerWidth;41canvas.height = element ? targetElement.clientHeight : window.innerHeight;42};4344const createEmojiImages = () => {45context.font = '12px serif';46context.textBaseline = 'middle';47context.textAlign = 'center';4849possibleEmoji.forEach((emoji) => {50const measurements = context.measureText(emoji);51const bgCanvas = document.createElement('canvas');52const bgContext = bgCanvas.getContext('2d');53if (!bgContext) return;5455bgCanvas.width = measurements.width;56bgCanvas.height = measurements.actualBoundingBoxAscent * 2;5758bgContext.textAlign = 'center';59bgContext.font = '12px serif';60bgContext.textBaseline = 'middle';61bgContext.fillText(62emoji,63bgCanvas.width / 2,64measurements.actualBoundingBoxAscent65);6667canvImages.current.push(bgCanvas);68});69};7071const addParticle = (x: number, y: number) => {72const randomImage =73canvImages.current[74Math.floor(Math.random() * canvImages.current.length)75];76particles.current.push(new Particle(x, y, randomImage));77};7879const onMouseMove = (e: MouseEvent) => {80const x = element81? e.clientX - targetElement.getBoundingClientRect().left82: e.clientX;83const y = element84? e.clientY - targetElement.getBoundingClientRect().top85: e.clientY;86addParticle(x, y);87};8889const updateParticles = () => {90if (!context || !canvas) return;9192context.clearRect(0, 0, canvas.width, canvas.height);9394particles.current.forEach((particle, index) => {95particle.update(context);96if (particle.lifeSpan < 0) {97particles.current.splice(index, 1);98}99});100};101102const animationLoop = () => {103updateParticles();104animationFrame.current = requestAnimationFrame(animationLoop);105};106107const init = () => {108if (prefersReducedMotion.current?.matches) return;109110setCanvasSize();111createEmojiImages();112113targetElement.addEventListener('mousemove', onMouseMove);114window.addEventListener('resize', setCanvasSize);115116animationLoop();117};118119const destroy = () => {120if (canvasRef.current) {121canvasRef.current.remove();122}123if (animationFrame.current) {124cancelAnimationFrame(animationFrame.current);125}126targetElement.removeEventListener('mousemove', onMouseMove);127window.removeEventListener('resize', setCanvasSize);128};129130prefersReducedMotion.current.onchange = () => {131if (prefersReducedMotion.current?.matches) {132destroy();133} else {134init();135}136};137138init();139return () => destroy();140}, [element]);141142return null;143};144145/**146* Particle Class147*/148class Particle {149position: { x: number; y: number };150velocity: { x: number; y: number };151lifeSpan: number;152initialLifeSpan: number;153canv: HTMLCanvasElement;154155constructor(x: number, y: number, canvasItem: HTMLCanvasElement) {156this.position = { x, y };157this.velocity = {158x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 2),159y: 1 + Math.random(),160};161this.lifeSpan = Math.floor(Math.random() * 60 + 80);162this.initialLifeSpan = this.lifeSpan;163this.canv = canvasItem;164}165166update(context: CanvasRenderingContext2D) {167this.position.x += this.velocity.x;168this.position.y += this.velocity.y;169this.lifeSpan--;170171this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75;172this.velocity.y -= Math.random() / 300;173174const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0);175176context.save();177context.translate(this.position.x, this.position.y);178context.scale(scale, scale);179context.drawImage(this.canv, -this.canv.width / 2, -this.canv.height / 2);180context.restore();181}182}183184export default SnowflakeCursor;185