UNPKG

lightswind

Version:

A professionally designed animate react component library & templates market that brings together functionality, accessibility, and beautiful aesthetics for modern applications.

227 lines (226 loc) 10.6 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useEffect, useRef, useState, useCallback } from "react"; const QUANTITY = 25; const RADIUS = 70; const RADIUS_SCALE_MIN = 1; const RADIUS_SCALE_MAX = 1.5; const ParticleOrbitEffect = ({ className = "", style = {}, fullScreen = false, // Default to false for container-bound behavior }) => { const canvasRef = useRef(null); const animationRef = useRef(); // Explicitly define the type of particlesRef.current as an array of Particle const particlesRef = useRef([]); const mouseRef = useRef({ x: 0, // Initialize to 0, will be updated by handler or initial positioning y: 0, isDown: false, radiusScale: 1, // screenWidth and screenHeight will now refer to canvas width/height width: 0, height: 0, }); // State to track canvas dimensions if not full screen const [canvasDimensions, setCanvasDimensions] = useState({ width: 0, height: 0, }); // Helper: create all particles - now accepts initial x,y const createParticles = useCallback((initialX, initialY) => { // Explicitly define the type of the particles array const particles = []; for (let i = 0; i < QUANTITY; i++) { particles.push({ size: 1, position: { x: initialX, y: initialY }, offset: { x: 0, y: 0 }, shift: { x: initialX, y: initialY }, speed: 0.01 + Math.random() * 0.04, targetSize: 1, fillColor: "#" + ((Math.random() * 0x404040 + 0xaaaaaa) | 0) .toString(16) .padStart(6, "0"), orbit: RADIUS * 0.5 + RADIUS * 0.5 * Math.random(), }); } return particles; }, []); // No dependencies for createParticles itself, as it uses constants useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const context = canvas.getContext("2d"); if (!context) return; let parentContainer = null; let canvasRect; // To store the canvas's position and dimensions // Function to update canvas dimensions and mouse origin const updateCanvasDimensions = () => { if (fullScreen) { mouseRef.current.width = window.innerWidth; mouseRef.current.height = window.innerHeight; canvas.width = window.innerWidth; canvas.height = window.innerHeight; // For full screen, initial mouse position can be center of viewport mouseRef.current.x = window.innerWidth / 2; mouseRef.current.y = window.innerHeight / 2; } else { parentContainer = canvas.parentElement; if (parentContainer) { canvasRect = parentContainer.getBoundingClientRect(); mouseRef.current.width = canvasRect.width; mouseRef.current.height = canvasRect.height; canvas.width = canvasRect.width; canvas.height = canvasRect.height; // For container-bound, initial mouse position can be center of container mouseRef.current.x = canvasRect.width / 2; mouseRef.current.y = canvasRect.height / 2; } } setCanvasDimensions({ width: canvas.width, height: canvas.height }); // Update state for initial render // Re-create particles if dimensions change significantly, or just update their initial shift particlesRef.current = createParticles(mouseRef.current.x, mouseRef.current.y); }; // Event handlers for mouse/touch const handleEvent = (event) => { let clientX; let clientY; if ("touches" in event) { // Touch event if (event.touches.length === 0) return; // No touch points clientX = event.touches[0].clientX; clientY = event.touches[0].clientY; } else { // Mouse event clientX = event.clientX; clientY = event.clientY; } // Calculate relative position ONLY if not full screen if (!fullScreen && parentContainer) { canvasRect = parentContainer.getBoundingClientRect(); // Get latest rect mouseRef.current.x = clientX - canvasRect.left; mouseRef.current.y = clientY - canvasRect.top; } else { // For full screen, direct clientX/Y is fine mouseRef.current.x = clientX; mouseRef.current.y = clientY; } // Handle mouse/touch down/up states if (event.type === "mousedown" || event.type === "touchstart") { mouseRef.current.isDown = true; } else if (event.type === "mouseup" || event.type === "touchend") { mouseRef.current.isDown = false; } }; // Animation loop const draw = () => { // Animate RADIUS SCALE if (mouseRef.current.isDown) { mouseRef.current.radiusScale += (RADIUS_SCALE_MAX - mouseRef.current.radiusScale) * 0.02; } else { mouseRef.current.radiusScale -= (mouseRef.current.radiusScale - RADIUS_SCALE_MIN) * 0.02; } mouseRef.current.radiusScale = Math.min(mouseRef.current.radiusScale, RADIUS_SCALE_MAX); // Background fade for trailing context.fillStyle = "rgba(0,0,0,0.05)"; context.fillRect(0, 0, mouseRef.current.width, // Use canvas's current width mouseRef.current.height // Use canvas's current height ); // Particles update/draw for (let i = 0, len = particlesRef.current.length; i < len; i++) { const particle = particlesRef.current[i]; const lp = { x: particle.position.x, y: particle.position.y }; particle.offset.x += particle.speed; particle.offset.y += particle.speed; particle.shift.x += (mouseRef.current.x - particle.shift.x) * particle.speed; particle.shift.y += (mouseRef.current.y - particle.shift.y) * particle.speed; particle.position.x = particle.shift.x + Math.cos(i + particle.offset.x) * (particle.orbit * mouseRef.current.radiusScale); particle.position.y = particle.shift.y + Math.sin(i + particle.offset.y) * (particle.orbit * mouseRef.current.radiusScale); // Keep inside canvas bounds particle.position.x = Math.max(Math.min(particle.position.x, mouseRef.current.width), 0); particle.position.y = Math.max(Math.min(particle.position.y, mouseRef.current.height), 0); // Particle size animation particle.size += (particle.targetSize - particle.size) * 0.05; if (Math.round(particle.size) === Math.round(particle.targetSize)) { particle.targetSize = 1 + Math.random() * 7; } context.beginPath(); context.fillStyle = particle.fillColor; context.strokeStyle = particle.fillColor; context.lineWidth = particle.size; context.moveTo(lp.x, lp.y); context.lineTo(particle.position.x, particle.position.y); context.stroke(); context.arc(particle.position.x, particle.position.y, particle.size / 2, 0, Math.PI * 2, true); context.fill(); } animationRef.current = requestAnimationFrame(draw); }; // Initial setup updateCanvasDimensions(); // Call once on mount // Event listeners window.addEventListener("resize", updateCanvasDimensions); canvas.addEventListener("mousemove", handleEvent); // Listen on canvas directly canvas.addEventListener("mousedown", handleEvent); canvas.addEventListener("mouseup", handleEvent); canvas.addEventListener("touchstart", handleEvent, { passive: false }); canvas.addEventListener("touchmove", handleEvent, { passive: false }); canvas.addEventListener("touchend", handleEvent); // Start animation animationRef.current = requestAnimationFrame(draw); // Cleanup function return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current); window.removeEventListener("resize", updateCanvasDimensions); if (canvas) { // Check if canvas still exists for cleanup canvas.removeEventListener("mousemove", handleEvent); canvas.removeEventListener("mousedown", handleEvent); canvas.removeEventListener("mouseup", handleEvent); canvas.removeEventListener("touchstart", handleEvent); canvas.removeEventListener("touchmove", handleEvent); canvas.removeEventListener("touchend", handleEvent); } }; }, [fullScreen, createParticles]); // Re-run effect if fullScreen prop changes // Dynamic style based on fullScreen prop const canvasStyle = fullScreen ? { position: "fixed", top: 0, left: 0, width: "100vw", height: "100vh", zIndex: -1, display: "block", ...style, } : { position: "absolute", // Use absolute to position within relative parent top: 0, left: 0, width: "100%", // Take full width of parent height: "100%", // Take full height of parent display: "block", ...style, }; return (_jsx("canvas", { ref: canvasRef, className: className, style: canvasStyle, width: canvasDimensions.width, height: canvasDimensions.height, "aria-hidden": "true", tabIndex: -1 })); }; export default ParticleOrbitEffect;