@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
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 { 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;