实现一个炫酷的漂浮粒子星系动画

369 阅读6分钟

说在前面

还记得之前好像在哪个网站看到过类似这样的一个动画效果,当时就感觉挺炫酷的。

动画效果是这样的:页面上有很多粒子漂浮自由移动,靠近的粒子会连线形成星系,鼠标靠近粒子的时候会有一个排斥效果,今天我们一起来实现一下这个炫酷的动画效果吧。

在线体验

码上掘金

codePen

codepen.io/yongtaozhen…

实现代码

系统配置

整体配置,所有视觉和交互参数都集中在这里,通过参数修改就能快速调整动画整体效果。

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

gitee.com/zheng_yongt…

github

github.com/yongtaozhen…


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。