@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
JavaScript
'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;