说在前面
还记得之前好像在哪个网站看到过类似这样的一个动画效果,当时就感觉挺炫酷的。
动画效果是这样的:
页面上有很多粒子漂浮自由移动,靠近的粒子会连线形成星系,鼠标靠近粒子的时候会有一个排斥效果,今天我们一起来实现一下这个炫酷的动画效果吧。
在线体验
码上掘金
codePen
实现代码
系统配置
整体配置,所有视觉和交互参数都集中在这里,通过参数修改就能快速调整动画整体效果。
const config = {
particleDensity: 9000, // 粒子密度(数值越大,粒子越少)
mouseRadius: 150, // 鼠标交互影响半径
particleSize: { min: 1, max: 3 }, // 粒子大小范围
particleSpeed: { min: -0.4, max: 0.4 }, // 粒子漂浮速度范围
particleColor: '173, 216, 230', // 粒子颜色(RGB值,便于调整透明度)
lineOpacityFactor: 20000, // 连线透明度衰减系数
lineWidth: 0.5, // 连线宽度
connectionDistanceFactor: 7 // 粒子连线距离系数
};
将参数与逻辑分离,比如想让粒子更密集,只需减小particleDensity;想让粒子飘得更快,可扩大particleSpeed的数值范围,降低后续维护成本。
基础准备
- Canvas设置:获取页面中的
canvas元素,设置其宽高为窗口大小,确保全屏覆盖; - 鼠标追踪:用
mouse对象存储鼠标坐标和交互半径,为后续粒子避让逻辑做准备; - 事件监听:
mousemove/mouseout:实时更新鼠标位置,鼠标离开时重置位置(避免粒子持续避让);resize:窗口缩放时触发画布重置,搭配debounce防抖函数(避免频繁触发导致卡顿)。
const canvas = document.getElementById('particle-canvas');
const ctx = canvas.getContext('2d');
let width, height, particlesArray, connectDistance;
const mouse = { x: null, y: null, radius: config.mouseRadius };
window.addEventListener('resize', debounce(setupCanvas, 250));
Particle粒子类
1.初始化粒子属性
class Particle {
constructor(x, y, directionX, directionY, size, color) {
this.x = x; // 粒子X坐标
this.y = y; // 粒子Y坐标
this.directionX = directionX; // X轴漂浮方向(速度)
this.directionY = directionY; // Y轴漂浮方向(速度)
this.size = size; // 粒子大小
this.color = color; // 粒子颜色(含透明度)
this.density = (Math.random() * 30) + 10; // 粒子「密度」(影响鼠标交互力度)
}
}
density属性为每个粒子赋予不同的「重量」——密度越大的粒子,鼠标对它的避让影响越强,让交互效果更有层次感。
2.绘制单个粒子
用canvas的arc()方法绘制圆形粒子,搭配半透明颜色。
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2, false); // 画圆(x,y,半径,起始角,结束角)
ctx.fillStyle = this.color; // 粒子颜色(从配置中动态生成,含透明度)
ctx.fill();
}
3.实现粒子的动态行为
update()方法负责处理三大核心逻辑:鼠标交互避让、自然漂浮和边界碰撞:
(1)鼠标交互避让
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < mouse.radius) {
const forceDirectionX = dx / distance;
const forceDirectionY = dy / distance;
//计算避让力度(距离越近,力度越大:(最大距离-当前距离)/最大距离)
const maxDistance = mouse.radius;
const force = (maxDistance - distance) / maxDistance;
//计算最终避让位移(方向 × 力度 × 粒子密度:密度越大,位移越明显)
const directionX = forceDirectionX * force * this.density;
const directionY = forceDirectionY * force * this.density;
this.x -= directionX;
this.y -= directionY;
}
forceDirectionX = dx / distance是「归一化」—— 比如鼠标在粒子右侧 100px(dx=100),distance=100,归一化后方向为 1(正方向,即粒子需向左移);若 dx=-50(鼠标在左侧),归一化后为 - 1(负方向,粒子需向右移),确保方向始终正确。
(2)自然漂浮 —— 粒子持续移动
无论有无鼠标交互,粒子都会按初始的 directionX/Y 持续移动。
this.x += this.directionX; // X轴位置 += X轴速度(正负控制方向)
this.y += this.directionY; // Y轴位置 += Y轴速度
(3)边界碰撞检测
当粒子碰到画布边缘时,反转其移动方向,确保粒子始终在屏幕内。
// X轴边界:粒子右边缘超过画布宽度 或 左边缘小于0
if (this.x + this.size > width || this.x - this.size < 0) {
this.directionX = -this.directionX;
}
// Y轴边界:粒子下边缘超过画布高度 或 上边缘小于0
if (this.y + this.size > height || this.y - this.size < 0) {
this.directionY = -this.directionY;
}
粒子初始化与连线
有了粒子类,还需要批量生成粒子,并实现粒子间的智能连线。
1.批量创建粒子
根据屏幕大小和配置的粒子密度,计算需要生成的粒子数量,循环创建Particle实例并加入数组。
function init() {
particlesArray = [];
// 粒子数量 = 屏幕面积 / 密度(密度越大,粒子越少)
const numberOfParticles = Math.floor((width * height) / config.particleDensity);
for (let i = 0; i < numberOfParticles; i++) {
const size = Math.random() * (config.particleSize.max - config.particleSize.min) + config.particleSize.min;
// 粒子初始位置:避免贴边(留出粒子大小的距离)
const x = Math.random() * (width - size * 2) + size;
const y = Math.random() * (height - size * 2) + size;
// 随机漂浮方向(在配置的速度范围内)
const directionX = Math.random() * (config.particleSpeed.max - config.particleSpeed.min) + config.particleSpeed.min;
const directionY = Math.random() * (config.particleSpeed.max - config.particleSpeed.min) + config.particleSpeed.min;
// 随机透明度(0.3~0.8):让粒子更有层次感
const color = `rgba(${config.particleColor}, ${Math.random() * 0.5 + 0.3})`;
particlesArray.push(new Particle(x, y, directionX, directionY, size, color));
}
}
2.粒子间连线
遍历所有粒子,计算粒子间的距离,当距离小于阈值时,用半透明线条连接它们,形成动态星系。
function connect() {
let opacityValue;
// 双重循环:遍历所有粒子对(a从0开始,b从a开始,避免重复计算)
for (let a = 0; a < particlesArray.length; a++) {
for (let b = a; b < particlesArray.length; b++) {
// 计算粒子a和b的距离(用平方距离替代开方,减少性能消耗)
const distance = (particlesArray[a].x - particlesArray[b].x) ** 2 + (particlesArray[a].y - particlesArray[b].y) ** 2;
// 若距离小于连线阈值,绘制连线
if (distance < connectDistance) {
// 连线透明度:距离越远,透明度越低
opacityValue = 1 - (distance / config.lineOpacityFactor);
ctx.strokeStyle = `rgba(${config.particleColor}, ${opacityValue})`;
ctx.lineWidth = config.lineWidth;
ctx.beginPath();
ctx.moveTo(particlesArray[a].x, particlesArray[a].y); // 起点:粒子a
ctx.lineTo(particlesArray[b].x, particlesArray[b].y); // 终点:粒子b
ctx.stroke();
}
}
}
}
动画循环
最后,通过requestAnimationFrame实现动画循环,每帧清空画布、更新所有粒子、绘制粒子连线。
// 动画循环
function animate() {
requestAnimationFrame(animate); // 浏览器原生动画API,确保60帧/秒
ctx.clearRect(0, 0, width, height); // 清空画布(避免粒子拖影)
// 更新所有粒子状态
for (const particle of particlesArray) {
particle.update();
}
// 绘制粒子连线
connect();
}
// 初始化画布并启动动画
function setupCanvas() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
// 计算粒子连线阈值(随屏幕大小自适应)
connectDistance = (width / config.connectionDistanceFactor) * (height / config.connectionDistanceFactor);
init(); // 初始化粒子
}
setupCanvas();
animate();
源码地址
gitee
github
- 🌟 觉得有帮助的可以点个 star~
- 🖊 有什么问题或错误可以指出,欢迎 pr~
- 📬 有什么想要实现的功能或想法可以联系我~
公众号
关注公众号『 前端也能这么有趣 』,获取更多有趣内容。
发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『
前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。