UNPKG

@namnguyenthanhwork/react-snowfall-effect

Version:

Create stunning, customizable snowfall animations for your React applications with ease. Perfect for winter themes, Christmas websites, or any application that needs beautiful falling particle effects.

380 lines (379 loc) 17.9 kB
'use client'; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; import { jsx as _jsx } from "react/jsx-runtime"; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // Constants for performance optimization var MOUSE_INFLUENCE_DISTANCE = 150; var FRICTION = 0.98; var EDGE_FADE_DISTANCE = 100; var MELTING_RATE = 0.02; var BOUNCE_DAMPING = 0.5; var MIN_BOUNCE_SPEED = 0.1; // Utility functions for better code organization var getRandomInRange = function (min, max) { return Math.random() * (max - min) + min; }; var calculateDistance = function (x1, y1, x2, y2) { return Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2)); }; var clamp = function (value, min, max) { return Math.min(Math.max(value, min), max); }; var createSnowflake = function (canvasWidth, canvasHeight, speed, wind, size, opacity, rotation, colors, loadedImages) { return ({ x: Math.random() * canvasWidth, y: Math.random() * canvasHeight, size: getRandomInRange(size.min, size.max), speed: getRandomInRange(speed.min, speed.max), wind: getRandomInRange(wind.min, wind.max), opacity: getRandomInRange(opacity.min, opacity.max), rotation: Math.random() * 360, rotationSpeed: rotation.enabled ? getRandomInRange(rotation.speed.min, rotation.speed.max) : 0, color: colors[Math.floor(Math.random() * colors.length)], image: loadedImages.length > 0 ? loadedImages[Math.floor(Math.random() * loadedImages.length)] : null, life: 1, velX: 0, velY: 0, landed: false, }); }; var drawStar = function (ctx, cx, cy, spikes, outerRadius, innerRadius) { var rot = (Math.PI / 2) * 3; var step = Math.PI / spikes; ctx.beginPath(); ctx.moveTo(cx, cy - outerRadius); for (var i = 0; i < spikes; i++) { var x1 = cx + Math.cos(rot) * outerRadius; var y1 = cy + Math.sin(rot) * outerRadius; ctx.lineTo(x1, y1); rot += step; var x2 = cx + Math.cos(rot) * innerRadius; var y2 = cy + Math.sin(rot) * innerRadius; ctx.lineTo(x2, y2); rot += step; } ctx.lineTo(cx, cy - outerRadius); ctx.closePath(); ctx.fill(); }; var drawSnowflakeShape = function (ctx, snowflake, shape) { ctx.fillStyle = snowflake.color; ctx.strokeStyle = snowflake.color; switch (shape) { case 'star': drawStar(ctx, 0, 0, 5, snowflake.size / 2, snowflake.size / 4); break; case 'dot': ctx.beginPath(); ctx.arc(0, 0, snowflake.size / 4, 0, Math.PI * 2); ctx.fill(); break; case 'circle': default: ctx.beginPath(); ctx.arc(0, 0, snowflake.size / 2, 0, Math.PI * 2); ctx.fill(); // Add sparkle effect ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, -snowflake.size / 3); ctx.lineTo(0, snowflake.size / 3); ctx.moveTo(-snowflake.size / 3, 0); ctx.lineTo(snowflake.size / 3, 0); ctx.stroke(); break; } }; var Snowfall = function (_a) { var _b = _a.snowflakeCount, snowflakeCount = _b === void 0 ? 50 : _b, _c = _a.images, images = _c === void 0 ? [] : _c, _d = _a.speed, speed = _d === void 0 ? { min: 1, max: 3 } : _d, _e = _a.wind, wind = _e === void 0 ? { min: -0.5, max: 0.5 } : _e, _f = _a.size, size = _f === void 0 ? { min: 10, max: 30 } : _f, _g = _a.opacity, opacity = _g === void 0 ? { min: 0.1, max: 0.8 } : _g, _h = _a.rotation, rotation = _h === void 0 ? { enabled: true, speed: { min: -2, max: 2 } } : _h, _j = _a.colors, colors = _j === void 0 ? ['#ffffff'] : _j, _k = _a.className, className = _k === void 0 ? '' : _k, _l = _a.style, style = _l === void 0 ? {} : _l, _m = _a.zIndex, zIndex = _m === void 0 ? 1000 : _m, _o = _a.fps, fps = _o === void 0 ? 60 : _o, _p = _a.snowflakeShape, snowflakeShape = _p === void 0 ? 'circle' : _p, _q = _a.fadeEdges, fadeEdges = _q === void 0 ? true : _q, _r = _a.followMouse, followMouse = _r === void 0 ? false : _r, _s = _a.gravity, gravity = _s === void 0 ? 1 : _s, _t = _a.bounce, bounce = _t === void 0 ? false : _t, _u = _a.melt, melt = _u === void 0 ? false : _u, _v = _a.accumulate, accumulate = _v === void 0 ? false : _v; // Refs for performance var canvasRef = useRef(null); var animationRef = useRef(null); var snowflakesRef = useRef([]); var loadedImagesRef = useRef([]); var mouseRef = useRef({ x: 0, y: 0 }); var accumulationRef = useRef([]); var animationStateRef = useRef({ isRunning: false, lastFrameTime: 0, }); // State var _w = useState(false), imagesLoaded = _w[0], setImagesLoaded = _w[1]; // Memoized values for performance var frameInterval = useMemo(function () { return 1000 / fps; }, [fps]); var canvasStyle = useMemo(function () { return (__assign({ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: zIndex }, style)); }, [style, zIndex]); // Mouse tracking with useCallback for performance var handleMouseMove = useCallback(function (e) { mouseRef.current = { x: e.clientX, y: e.clientY }; }, []); // Mouse tracking useEffect(function () { if (!followMouse) return; window.addEventListener('mousemove', handleMouseMove); return function () { return window.removeEventListener('mousemove', handleMouseMove); }; }, [followMouse, handleMouseMove]); // Optimized image loading with better error handling var loadImages = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { var imagePromises, _a, error_1; return __generator(this, function (_b) { switch (_b.label) { case 0: if (images.length === 0) { setImagesLoaded(true); return [2 /*return*/]; } _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); imagePromises = images.map(function (src) { return new Promise(function (resolve, reject) { var img = new Image(); img.onload = function () { return resolve(img); }; img.onerror = function () { return reject(new Error("Failed to load image: ".concat(src))); }; img.src = src; }); }); _a = loadedImagesRef; return [4 /*yield*/, Promise.all(imagePromises)]; case 2: _a.current = _b.sent(); setImagesLoaded(true); return [3 /*break*/, 4]; case 3: error_1 = _b.sent(); console.warn('Failed to load some snowflake images:', error_1); loadedImagesRef.current = []; // Clear any partially loaded images setImagesLoaded(true); // Continue with default shapes return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }, [images]); // Load custom images useEffect(function () { loadImages(); }, [loadImages]); // Canvas resize handler with useCallback var updateCanvasSize = useCallback(function () { var canvas = canvasRef.current; if (!canvas) return; canvas.width = window.innerWidth; canvas.height = window.innerHeight; }, []); // Initialize snowflakes with better performance useEffect(function () { if (!imagesLoaded) return; var canvas = canvasRef.current; if (!canvas) return; updateCanvasSize(); window.addEventListener('resize', updateCanvasSize); // Create snowflakes using the utility function snowflakesRef.current = Array.from({ length: snowflakeCount }, function () { return createSnowflake(canvas.width, canvas.height, speed, wind, size, opacity, rotation, colors, loadedImagesRef.current); }); return function () { window.removeEventListener('resize', updateCanvasSize); }; }, [ imagesLoaded, snowflakeCount, speed, wind, size, opacity, rotation, colors, updateCanvasSize, ]); // Optimized animation loop with proper frame timing useEffect(function () { if (!imagesLoaded) return; var canvas = canvasRef.current; var ctx = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('2d'); if (!canvas || !ctx) return; animationStateRef.current.isRunning = true; var animate = function (currentTime) { if (!animationStateRef.current.isRunning) return; // FPS control with more efficient timing if (currentTime - animationStateRef.current.lastFrameTime < frameInterval) { animationRef.current = requestAnimationFrame(animate); return; } animationStateRef.current.lastFrameTime = currentTime; ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw accumulation layer if (accumulate) { ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; accumulationRef.current.forEach(function (acc) { ctx.beginPath(); ctx.arc(acc.x, acc.y, acc.size, 0, Math.PI * 2); ctx.fill(); }); } snowflakesRef.current.forEach(function (snowflake) { if (snowflake.landed && accumulate) return; // Optimized mouse following effect if (followMouse) { var dx = mouseRef.current.x - snowflake.x; var dy = mouseRef.current.y - snowflake.y; var distance = calculateDistance(snowflake.x, snowflake.y, mouseRef.current.x, mouseRef.current.y); if (distance < MOUSE_INFLUENCE_DISTANCE) { var force = ((MOUSE_INFLUENCE_DISTANCE - distance) / MOUSE_INFLUENCE_DISTANCE) * 0.001; snowflake.velX += dx * force; snowflake.velY += dy * force; } snowflake.velX *= FRICTION; snowflake.velY *= FRICTION; } // Update position snowflake.y += snowflake.speed * gravity + snowflake.velY; snowflake.x += snowflake.wind + snowflake.velX; snowflake.rotation += snowflake.rotationSpeed; // Optimized bounce effect if (bounce && snowflake.y > canvas.height - snowflake.size) { snowflake.y = canvas.height - snowflake.size; snowflake.speed *= -BOUNCE_DAMPING; if (Math.abs(snowflake.speed) < MIN_BOUNCE_SPEED) { snowflake.speed = 0; snowflake.landed = true; } } // Optimized melting effect if (melt && snowflake.y > canvas.height - 50) { snowflake.life -= MELTING_RATE; if (snowflake.life <= 0) { snowflake.y = -snowflake.size; snowflake.x = Math.random() * canvas.width; snowflake.life = 1; } } // Optimized accumulation effect if (accumulate && snowflake.y > canvas.height - snowflake.size) { accumulationRef.current.push({ x: snowflake.x, y: canvas.height - snowflake.size / 2, size: snowflake.size / 2, }); snowflake.landed = true; snowflake.y = -snowflake.size; snowflake.x = Math.random() * canvas.width; } // Reset snowflake if it goes off screen if (snowflake.y > canvas.height && !bounce && !accumulate) { snowflake.y = -snowflake.size; snowflake.x = Math.random() * canvas.width; snowflake.life = 1; } if (snowflake.x > canvas.width + snowflake.size) { snowflake.x = -snowflake.size; } else if (snowflake.x < -snowflake.size) { snowflake.x = canvas.width + snowflake.size; } // Optimized edge fading var edgeOpacity = snowflake.opacity; if (fadeEdges) { var leftFade = clamp(snowflake.x / EDGE_FADE_DISTANCE, 0, 1); var rightFade = clamp((canvas.width - snowflake.x) / EDGE_FADE_DISTANCE, 0, 1); var topFade = clamp(snowflake.y / EDGE_FADE_DISTANCE, 0, 1); edgeOpacity *= Math.min(leftFade, rightFade, topFade); } // Draw snowflake with better performance ctx.save(); ctx.globalAlpha = edgeOpacity * snowflake.life; ctx.translate(snowflake.x, snowflake.y); if (rotation.enabled) { ctx.rotate((snowflake.rotation * Math.PI) / 180); } if (snowflake.image) { // Draw custom image ctx.drawImage(snowflake.image, -snowflake.size / 2, -snowflake.size / 2, snowflake.size, snowflake.size); } else { // Draw default snowflake shape drawSnowflakeShape(ctx, snowflake, snowflakeShape); } ctx.restore(); }); animationRef.current = requestAnimationFrame(animate); }; animate(0); return function () { animationStateRef.current.isRunning = false; if (animationRef.current) { cancelAnimationFrame(animationRef.current); animationRef.current = null; } }; }, [ imagesLoaded, frameInterval, accumulate, followMouse, bounce, melt, fadeEdges, gravity, rotation.enabled, snowflakeShape, ]); return (_jsx("canvas", { ref: canvasRef, className: "pointer-events-none fixed inset-0 ".concat(className), style: canvasStyle })); }; export default Snowfall;