Привет друг ! В этой статье попробуем создать эффект падающих снежинок поверх веб-контента, используя React и HTML5 Canvas. Этот эффект добавит праздничное настроение твоему сайту или приложению 🎅.
С моими любыми комментариями в коде 🤭.
Чего мы хотим добиться? тут 🥸.
И так - поехали !🛼
1. Создаём компоненту CircleCanvas
Создаём компоненту CircleCanvas.tsx и добавим в него следующий код:
2. Интерфейс снежинки
interface Circle {
x: number;
y: number;
radius: number;
speedY: number;
speedX: number;
offsetX: number;
}
export type Nullable<T> = T | null;
Этот интерфейс описывает параметры одной снежинки. Мы будем использовать их для управления движением и анимацией.
3. Настройка Canvas
Внутри нашего компонента создаем ссылку на canvas и функцию для автоматического изменения его размеров при изменении окна:
// Инициализация и обновление размеров canvas
const canvasRef = useRef<Nullable<HTMLCanvasElement>>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, []);
4. Функция создания снежинки
Каждая снежинка создается со случайным положением, скоростью и размером:
const createCircle = (): Circle => {
return {
x: Math.random() * window.innerWidth, // случайная позиция по ширине
y: -50, // начинаем выше экрана
radius: Math.random() * 10 + 5, // радиус от 5 до 15
speedY: Math.random() * 2 - 1, // скорость падения
speedX: Math.random() * 2 - 1, // горизонтальная скорость (S-образное движение)
offsetX: 0, // смещение по X для S-образного движения
};
};
5. Анимация снежинок
Используем requestAnimationFrame для плавной анимации снежинок:
const animate = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
// Очищаем canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Обновляем и рисуем снежинки
circles.current.forEach((circle) => {
circle.y += circle.speedY;
circle.offsetX += circle.speedX;
circle.x += Math.sin(circle.offsetX / 50) * 2; // S-образное движение
// Рисуем снежинку
context.beginPath();
context.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
context.fillStyle = 'rgba(173,184,197,0.7)'; // Цвет снежинки
context.fill();
context.closePath();
});
// Удаляем снежинки, которые вышли за экран
circles.current = circles.current.filter(circle => circle.y < window.innerHeight + 50);
// Добавляем новую снежинку с некоторой вероятностью
if (Math.random() < 0.02) {
circles.current.push(createCircle());
}
// Продолжаем анимацию
animationFrameId.current = requestAnimationFrame(animate);
};
6. Запуск анимации
Подключаем анимацию при монтировании компонента:
useEffect(() => {
animationFrameId.current = requestAnimationFrame(animate);
return () => {
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
};
}, []);
7. Рендер Canvas
Canvas располагается поверх контента, не мешая пользователю взаимодействовать с остальной страницей:
<canvas
ref={canvasRef}
style={{
position: 'fixed',
inset: 0,
zIndex: 100,
display: 'block',
pointerEvents: 'none',
}}
/>
8. Итоговый код
Теперь у вас есть готовый компонент CircleCanvas, который добавляет эффект снежинок:
import React, { useEffect, useRef } from 'react';
interface Circle {
x: number;
y: number;
radius: number;
speedY: number;
speedX: number;
offsetX: number;
}
export type Nullable<T> = T | null;
export const CircleCanvas: React.FC = () => {
const canvasRef = useRef<Nullable<HTMLCanvasElement>>(null);
const circles = useRef<Circle[]>([]);
const animationFrameId = useRef<Nullable<number>>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, []);
const createCircle = (): Circle => {
return {
x: Math.random() * window.innerWidth,
y: -50,
radius: Math.random() * 10 + 5,
speedY: Math.random() * 2 - 1,
speedX: Math.random() * 2 - 1,
offsetX: 0,
};
};
const animate = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
context.clearRect(0, 0, canvas.width, canvas.height);
circles.current.forEach((circle) => {
circle.y += circle.speedY;
circle.offsetX += circle.speedX;
circle.x += Math.sin(circle.offsetX / 50) * 2;
context.beginPath();
context.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
context.fillStyle = 'rgba(173,184,197,0.7)';
context.fill();
context.closePath();
});
circles.current = circles.current.filter(circle => circle.y < window.innerHeight + 50);
if (Math.random() < 0.02) {
circles.current.push(createCircle());
}
animationFrameId.current = requestAnimationFrame(animate);
};
useEffect(() => {
animationFrameId.current = requestAnimationFrame(animate);
return () => {
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
inset: 0,
zIndex: 100,
display: 'block',
pointerEvents: 'none',
}}
/>
);
};
Теперь мы можем использовать CircleCanvas в любом месте нашего приложения! Да и вообще мы молодцы 🧸!
Спасибо за внимание! До Новых Встреч!🤗🤗🤗