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.

345 lines (344 loc) 16.2 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 { useEffect, useRef, useState } from 'react'; 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; var canvasRef = useRef(null); var animationRef = useRef(null); var snowflakesRef = useRef([]); var _w = useState(false), imagesLoaded = _w[0], setImagesLoaded = _w[1]; var loadedImagesRef = useRef([]); var mouseRef = useRef({ x: 0, y: 0 }); var accumulationRef = useRef([]); var lastFrameTime = useRef(0); // Mouse tracking useEffect(function () { if (!followMouse) return; var handleMouseMove = function (e) { mouseRef.current = { x: e.clientX, y: e.clientY }; }; window.addEventListener('mousemove', handleMouseMove); return function () { return window.removeEventListener('mousemove', handleMouseMove); }; }, [followMouse]); // Load custom images useEffect(function () { if (images.length === 0) { setImagesLoaded(true); return; } var loadImages = 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: imagePromises = images.map(function (src) { return new Promise(function (resolve, reject) { var img = new Image(); img.onload = function () { return resolve(img); }; img.onerror = reject; img.src = src; }); }); _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); _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); setImagesLoaded(true); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; loadImages(); }, [images]); // Initialize snowflakes useEffect(function () { if (!imagesLoaded) return; var canvas = canvasRef.current; if (!canvas) return; var updateCanvasSize = function () { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }; updateCanvasSize(); window.addEventListener('resize', updateCanvasSize); // Create snowflakes snowflakesRef.current = Array.from({ length: snowflakeCount }, function () { return ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: Math.random() * (size.max - size.min) + size.min, speed: Math.random() * (speed.max - speed.min) + speed.min, wind: Math.random() * (wind.max - wind.min) + wind.min, opacity: Math.random() * (opacity.max - opacity.min) + opacity.min, rotation: Math.random() * 360, rotationSpeed: rotation.enabled ? Math.random() * (rotation.speed.max - rotation.speed.min) + rotation.speed.min : 0, color: colors[Math.floor(Math.random() * colors.length)], image: loadedImagesRef.current.length > 0 ? loadedImagesRef.current[Math.floor(Math.random() * loadedImagesRef.current.length)] : null, life: 1, velX: 0, velY: 0, landed: false, }); }); return function () { window.removeEventListener('resize', updateCanvasSize); }; }, [ imagesLoaded, snowflakeCount, speed, wind, size, opacity, rotation, colors, ]); // Draw different snowflake shapes var drawSnowflakeShape = function (ctx, snowflake) { ctx.fillStyle = snowflake.color; ctx.strokeStyle = snowflake.color; switch (snowflakeShape) { 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; } }; // Draw star shape var drawStar = function (ctx, cx, cy, spikes, outerRadius, innerRadius) { var rot = (Math.PI / 2) * 3; var x = cx; var y = cy; var step = Math.PI / spikes; ctx.beginPath(); ctx.moveTo(cx, cy - outerRadius); for (var i = 0; i < spikes; i++) { x = cx + Math.cos(rot) * outerRadius; y = cy + Math.sin(rot) * outerRadius; ctx.lineTo(x, y); rot += step; x = cx + Math.cos(rot) * innerRadius; y = cy + Math.sin(rot) * innerRadius; ctx.lineTo(x, y); rot += step; } ctx.lineTo(cx, cy - outerRadius); ctx.closePath(); ctx.fill(); }; // Animation loop 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; var animate = function (currentTime) { // FPS control if (currentTime - lastFrameTime.current < 1000 / fps) { animationRef.current = requestAnimationFrame(animate); return; } lastFrameTime.current = 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; // Mouse following effect if (followMouse) { var dx = mouseRef.current.x - snowflake.x; var dy = mouseRef.current.y - snowflake.y; var distance = Math.sqrt(dx * dx + dy * dy); var maxDistance = 150; if (distance < maxDistance) { var force = ((maxDistance - distance) / maxDistance) * 0.001; snowflake.velX += dx * force; snowflake.velY += dy * force; } snowflake.velX *= 0.98; // Friction snowflake.velY *= 0.98; } // Update position snowflake.y += snowflake.speed * gravity + snowflake.velY; snowflake.x += snowflake.wind + snowflake.velX; snowflake.rotation += snowflake.rotationSpeed; // Bounce effect if (bounce && snowflake.y > canvas.height - snowflake.size) { snowflake.y = canvas.height - snowflake.size; snowflake.speed *= -0.5; if (Math.abs(snowflake.speed) < 0.1) { snowflake.speed = 0; snowflake.landed = true; } } // Melting effect if (melt && snowflake.y > canvas.height - 50) { snowflake.life -= 0.02; if (snowflake.life <= 0) { snowflake.y = -snowflake.size; snowflake.x = Math.random() * canvas.width; snowflake.life = 1; } } // 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; } // Edge fading var edgeOpacity = snowflake.opacity; if (fadeEdges) { var edgeDistance = 100; var leftFade = Math.min(snowflake.x / edgeDistance, 1); var rightFade = Math.min((canvas.width - snowflake.x) / edgeDistance, 1); var topFade = Math.min(snowflake.y / edgeDistance, 1); edgeOpacity *= Math.min(leftFade, rightFade, topFade); } // Draw snowflake 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); } ctx.restore(); }); animationRef.current = requestAnimationFrame(animate); }; animate(0); return function () { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [ imagesLoaded, fps, accumulate, followMouse, bounce, melt, fadeEdges, gravity, rotation.enabled, snowflakeShape, ]); return (_jsx("canvas", { ref: canvasRef, className: "pointer-events-none fixed inset-0 ".concat(className), style: __assign({ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: zIndex }, style) })); }; export default Snowfall;