如何用原生JS实现流畅的粒子运动悬浮效果?
问题
需要实现一个页面粒子系统,满足以下需求:
- 根据屏幕尺寸生成适量粒子,粒子始终悬浮在页面上方且保持在屏幕范围内
- 粒子使用预定义的随机图片,尽量不需要额外HTML代码
- 实现流畅的随机方向运动,避免当前固定间隔调整坐标导致的生硬瞬移效果
- 不预设粒子运动方向,也不限制粒子仅在屏幕边缘反弹
解决方案核心优化点
- 用
requestAnimationFrame替代setInterval:利用浏览器原生刷新机制,让动画帧率与屏幕刷新率同步,彻底解决卡顿瞬移问题 - 基于速度向量的运动逻辑:为每个粒子赋予持续的随机速度值,每一帧根据时间差计算位置变化,实现自然平滑的运动轨迹
- 动态适配的粒子数量:根据屏幕面积和粒子大小自动计算合适数量,确保在不同设备上显示效果一致
- 随机图片分配:预定义图片列表,生成粒子时随机选取,通过CSS遮罩实现图片显示
- 无缝边界循环:粒子超出屏幕边缘后直接从对侧重新进入,替代生硬的反弹效果
完整代码示例
JavaScript
(() => { const PARTICLE_SIZE = 20; // 粒子大小 const PARTICLE_SPEED_RANGE = [0.5, 2]; // 粒子速度范围(可调整) const PARTICLE_IMAGES = [ './icons/square.svg', './icons/circle.svg', './icons/star.svg' ]; // 替换为你的图片路径 let particles = []; let animationId = null; let lastFrameTime = Date.now(); // 自动创建粒子容器(无需手动添加HTML) let container = document.getElementById('particles'); if (!container) { container = document.createElement('div'); container.id = 'particles'; document.body.appendChild(container); } // 根据屏幕尺寸计算粒子数量 const calculateParticleCount = () => { const screenArea = window.innerWidth * window.innerHeight; const densityFactor = 50; // 数值越大粒子越少,可调整密度 const particleArea = PARTICLE_SIZE * PARTICLE_SIZE * densityFactor; return Math.floor(screenArea / particleArea); }; // 创建单个粒子 const createParticle = () => { const particle = document.createElement('span'); // 随机初始位置 particle.style.top = `${Math.random() * window.innerHeight}px`; particle.style.left = `${Math.random() * window.innerWidth}px`; // 随机分配图片(用CSS遮罩实现) const randomImg = PARTICLE_IMAGES[Math.floor(Math.random() * PARTICLE_IMAGES.length)]; particle.style.maskImage = `url(${randomImg})`; particle.style.webkitMaskImage = `url(${randomImg})`; // 兼容webkit内核浏览器 // 存储粒子的速度向量(x/y方向的随机速度) particle.speed = { x: (Math.random() - 0.5) * (PARTICLE_SPEED_RANGE[1] - PARTICLE_SPEED_RANGE[0]) + PARTICLE_SPEED_RANGE[0], y: (Math.random() - 0.5) * (PARTICLE_SPEED_RANGE[1] - PARTICLE_SPEED_RANGE[0]) + PARTICLE_SPEED_RANGE[0] }; container.appendChild(particle); return particle; }; // 生成所有粒子 const generateParticles = () => { const count = calculateParticleCount(); particles = Array.from({ length: count }, createParticle); }; // 每一帧更新粒子位置 const updateParticles = () => { const now = Date.now(); const deltaTime = (now - lastFrameTime) / 16; // 以60fps为基准的时间补偿系数 lastFrameTime = now; particles.forEach(particle => { let top = parseFloat(particle.style.top); let left = parseFloat(particle.style.left); // 根据速度和时间差更新位置 top += particle.speed.y * deltaTime; left += particle.speed.x * deltaTime; // 边界循环处理:超出屏幕后从对侧重新出现 if (top > window.innerHeight) top = -PARTICLE_SIZE; else if (top < -PARTICLE_SIZE) top = window.innerHeight; if (left > window.innerWidth) left = -PARTICLE_SIZE; else if (left < -PARTICLE_SIZE) left = window.innerWidth; particle.style.top = `${top}px`; particle.style.left = `${left}px`; }); animationId = requestAnimationFrame(updateParticles); }; // 窗口 resize 时重新生成粒子 const handleResize = () => { cancelAnimationFrame(animationId); particles.forEach(p => p.remove()); generateParticles(); lastFrameTime = Date.now(); updateParticles(); }; // 初始化启动 generateParticles(); updateParticles(); window.addEventListener('resize', handleResize); })();
CSS
#particles { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; /* 确保粒子不干扰页面其他元素的点击交互 */ opacity: 0.25; } #particles span { position: absolute; width: 20px; height: 20px; background-color: #333; /* 遮罩图片的基础颜色,可根据需求修改 */ mask-repeat: no-repeat; mask-size: contain; mask-position: center; /* 兼容webkit内核浏览器 */ -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; -webkit-mask-position: center; }
HTML
<!-- 无需手动添加容器,JS会自动创建 -->
内容的提问来源于stack exchange,提问作者RoyBlunk
相关产品推荐
相关产品推荐

