资讯动态

Canvas粒子动画实现动态星空科技感登录页:完整代码与调优

发布时间:2026/10/15 19:16:33 来源:尧图企业网站定制
每次打开各种产品官网登录页往往是第一个被注意到的“门面”。很多朋友看到网上那些飘着动态粒子线条、背景深邃如星空一样的登录页第一反应都是“好看是好看但代码肯定很复杂”。其实这类效果并没有想象中那么高不可攀一个懂点Canvas基础的前端开发者完全可以通过一个百来行的HTML文件把它做出来。今天要拆解的就是这样一个“动态星空科技感粒子登录页”标题里“附代码复制即可用”是实话把核心文件完整保留下来你直接粘贴到本地浏览器里运行效果就能出来。这个页面适合产品展示、个人官网、后台系统、企业Saas平台等在用户入口处拉高第一眼质感的场景也适合想快速掌握Canvas粒子动画思路的前端初学者。这个项目本身解决的是一个很实际的问题普通的登录框放在白底页面上太单调如果加静态图片背景又容易和表单抢视觉焦点。粒子动画刚好处于“有动效但不干扰操作”的中间态——它自带科技感和未来感同时因为粒子大多分布在四周、颜色偏暗中间正好留出适合放置登陆表单的区域。我会把这套东西从原理、代码到参数调优再到实际落地过程中容易踩的坑一次性讲清楚。你不需要从零设计算法只需要理解关键逻辑然后把代码拿过去按需求改一改。1. 拆解项目这套“科技感”到底由什么构成1.1 从效果层面看这个页面包含几个层次先从用户能直观看到的部分说起。“动态星空科技感粒子登录页”虽然听起来是个整体但把它往下拆其实是三个互相叠加的视觉层底层是深邃的渐变背景一般从深蓝到紫黑模拟夜空氛围是整个页面科技感的基调中间层是跳动且缓慢飘移的粒子这些粒子之间根据距离互相连线形成类似星座或者星云的网状结构顶层是半透明的毛玻璃登录卡片承载用户名输入框、密码输入框和登录按钮。这种层次关系很重要。我在实际做的过程中发现很多人在第一步就翻车原因是“粒子画出来了表单看不见”——不是粒子太亮就是粒子挤在中间把输入区域挡了个严实。正确做法是让粒子整体偏向左右两侧和上缘中心区域保持稀疏同时表单卡片使用半透明的深色背景常见的是rgba(0, 0, 0, 0.45)配合backdrop-filter: blur(20px)这样即使粒子从卡片后面飘过也不会影响文字可读性。1.2 粒子系统的概念别被专业术语吓住“粒子系统”听起来像是很高级的图形学概念其实就是“一堆带坐标、速度、颜色的小点通过循环不断更新位置并绘制到画布上”。你可以把它想象成一群小鱼在鱼缸里游每条鱼有自己游动的方向和速度每帧刷新时它们的坐标发生微调画布上重新绘制一次视觉上就成了连续的动画。这个登录页里粒子之间还会连线所以比单纯的小点飘动复杂一点点。连线的逻辑是“如果两个粒子的直线距离小于某个阈值就在它们之间画一条透明度很低的线段”。这个阈值通常介于120到180像素。距离越近的粒子连线颜色越亮距离越远则越暗最后形成那种层层叠叠的网状视觉。1.3 为什么选Canvas而不是其他方案这是我在动手之前反复问过自己的问题。如果用CSS写粒子效果严格来说也能做到比如创建大量div元素然后用transform移动但粒子数量一旦超过100个页面在低端设备上就会明显卡顿。用DOM元素做连线更是灾难——每一条线都是一个元素数量一多布局和渲染性能都扛不住。Canvas的优势在于它是“一张画布”而不是“一堆元素”。整张画布上发生的变化都通过像素级绘制来处理浏览器在渲染时不需要逐个管理DOM节点的布局和样式的重绘性能表现稳定得多。粒子数量控制在150个以内、同时执行动画循环时CPU和内存占用都处于相当温和的范围。对登录页这种只需要持续运行几分钟到几十分钟的场景来说完全够用。2. 核心实现思路与代码细节从零手写“粒子星空”2.1 基础设施画布初始化与全屏适配代码的第一步是准备画布。这里有一个新手极易踩的坑直接给Canvas设置了width: 100%; height: 100%的CSS样式但Canvas内部实际绘制区域还是默认的300x150像素。这样粒子会全部挤在左上角的一个小画面里完全看不出“铺满星空”的效果。正确方式是把Canvas的宽高设置为浏览器可视区域的物理像素值。另外现代屏幕普遍是高清屏物理像素和CSS像素存在比例关系像素比devicePixelRatio常见值是2为了让画布在高清屏上不发虚需要把物理像素设置为CSS像素 * devicePixelRatio然后用scale()方法把绘制坐标系同步放大。const canvas document.getElementById(starCanvas); const ctx canvas.getContext(2d); let width window.innerWidth; let height window.innerHeight; function initCanvas() { const ratio window.devicePixelRatio || 1; canvas.width width * ratio; canvas.height height * ratio; canvas.style.width width px; canvas.style.height height px; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); } initCanvas();窗口大小变化时也需要重新调用initCanvas()并重新生成粒子坐标否则会出现画布内容被拉伸或缩小的奇怪表现。注意window.innerWidth和window.innerHeight获取的是可视区域大小不是document.body的完整宽度。如果页面有滚动条粒子画布固定为可视区域高度背景不会跟着页面滚动这也是登录页通常的处理方式。2.2 粒子对象的生成与运动更新定义粒子是一个比较直接的过程每个对象包含x、y坐标vx、vy速度分量一个半径radius和透明度opacity。坐标初始值在整个屏幕范围内随机分布速度通常设置在-0.5到0.5之间这样粒子移动速度不快有“缓慢漂移”的星空感而不是“飞蚊症”那种快速乱窜的感觉。const particles []; const PARTICLE_COUNT 120; function createParticle() { return { x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.8, vy: (Math.random() - 0.5) * 0.8, radius: Math.random() * 1.5 0.5, opacity: Math.random() * 0.5 0.3, }; } for (let i 0; i PARTICLE_COUNT; i) { particles.push(createParticle()); }每一帧动画中粒子坐标都要和速度相加。粒子抵达屏幕边缘时需要反弹否则飘出屏幕后画面就会越来越空。反弹的处理是当x小于0或大于width时让vx取反y的处理同理。如果想做“边缘回流”而不是硬邦邦反弹也可以让坐标越界后重新从对侧出现两种方案各有视觉差异硬反弹更适合有科技感线条的项目。2.3 粒子间连线的距离判定与透明度渐变连线是整个粒子星空最出效果的部分。代码上每帧都需要两两计算粒子之间的距离这就带来了性能隐患——粒子数量为n时两两组合数量是n^2/2。按150个粒子来算每帧要做1万多组距离计算虽然数值不大但需要养成随时关注这个计算量的意识。function drawLines() { for (let i 0; i particles.length; i) { for (let j i 1; j particles.length; j) { const dx particles[i].x - particles[j].x; const dy particles[i].y - particles[j].y; const distance Math.sqrt(dx * dx dy * dy); const maxDistance 140; if (distance maxDistance) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); const opacity (1 - distance / maxDistance) * 0.4; ctx.strokeStyle rgba(255, 255, 255, ${opacity}); ctx.lineWidth 0.6; ctx.stroke(); } } } }这里的maxDistance是需要反复调节的阈值。调大了线条多整体显得拥挤杂乱调小了线条稀少星际感不足。我实测下来屏幕宽度在1920px左右时maxDistance140配合120个粒子达到了饱满网络和留空间隙的平衡点。如果你在笔记本上设计可能需要把阈值缩到110~130之间。2.4 鼠标交互让用户感觉“画面回应了我”这一层是这个登录页体验的加分项。粒子星空如果能感知鼠标位置形成一个小范围的“避让”或者“连线”整个登录页的科技感会提升一个层次。实现方式有很多种最简洁且不会影响登录框操作的是鼠标位置一定范围比如160像素内的粒子会产生一个向外推的偏移量。用代码实现就是const mouse { x: null, y: null }; window.addEventListener(mousemove, (e) { mouse.x e.clientX; mouse.y e.clientY; }); function repelMouser(particle) { if (mouse.x null) return; const dx particle.x - mouse.x; const dy particle.y - mouse.y; const distance Math.sqrt(dx * dx dy * dy); const influenceRadius 160; if (distance influenceRadius distance 0.1) { const force (influenceRadius - distance) / influenceRadius; particle.x (dx / distance) * force * 2; particle.y (dy / distance) * force * 2; } }这种“斥力”会让鼠标滑过时粒子像避开指尖一样散开松开后粒子又会自然飘回来。注意不要让粒子的运动过于剧烈否则画面看起来会像慌乱蚂蚁窝。这里设置的force * 2是一个相对保守的量体现在视觉上是“被轻轻推开”而不是“被炸开”。2.5 登录卡片与表单的设计要点粒子画布是整个页面的底层登录卡片需要以绝对定位悬浮在上面。卡片宽度建议在380~420px之间高度自适应用transform: translate(-50%, -50%)配合top: 50%; left: 50%实现居中。这种居中方式在桌面端适配得最好比用flex布局更稳定因为卡片不会受粒子Canvas布局影响。登录表单的结构并不复杂需要保证的是输入框样式和整体科技感统一。常见方案是输入框背景用半透明白色rgba(255, 255, 255, 0.08)边框是淡白色1px solid rgba(255, 255, 255, 0.2)聚焦时边框变亮底色轻微提亮。因为有毛玻璃效果打在背景上输入框会自带一种“悬浮在星空中”的视觉感。3. 完整可复用的登录页代码在这部分直接给出一个可直接运行的核心版本。因为很多读者可能只想要“拿过去就能用”我把HTML、CSS和JavaScript都放在同一个文件里。这个文件不需要构建工具不需要安装依赖新建一个login.html粘贴保存双击打开就能看到效果。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title动态星空粒子登录/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; font-family: Microsoft YaHei, PingFang SC, sans-serif; background: linear-gradient(135deg, #0b0b1a 0%, #101536 45%, #1b1140 100%); } #starCanvas { position: fixed; top: 0; left: 0; z-index: 0; } .login-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 400px; padding: 40px 32px; border-radius: 16px; background: rgba(12, 15, 35, 0.65); border: 1px solid rgba(255, 255, 255, 0.15); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); z-index: 1; } .login-panel h1 { text-align: center; color: #e8eaff; font-size: 22px; font-weight: 500; letter-spacing: 2px; margin-bottom: 28px; } .field { margin-bottom: 18px; } .field input { width: 100%; height: 44px; padding: 0 14px; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; background: rgba(255, 255, 255, 0.06); color: #f0f2ff; font-size: 14px; outline: none; transition: border-color 0.2s, background 0.2s; } .field input::placeholder { color: rgba(255, 255, 255, 0.4); } .field input:focus { border-color: rgba(130, 200, 255, 0.8); background: rgba(255, 255, 255, 0.1); } .login-btn { width: 100%; height: 44px; margin-top: 10px; border: none; border-radius: 8px; background: linear-gradient(135deg, #3a6ef0, #7b4dff); color: #ffffff; font-size: 15px; letter-spacing: 1px; cursor: pointer; box-shadow: 0 10px 30px rgba(64, 110, 240, 0.25); transition: transform 0.15s, box-shadow 0.15s; } .login-btn:hover { transform: translateY(-2px); box-shadow: 0 14px 34px rgba(64, 110, 240, 0.35); } .login-btn:active { transform: translateY(0); } .tips { margin-top: 16px; text-align: center; color: rgba(255, 255, 255, 0.35); font-size: 12px; } /style /head body canvas idstarCanvas/canvas div classlogin-panel h1星舰控制台/h1 div classfield input idusername typetext placeholder用户名 / 邮箱 autocompleteusername/ /div div classfield input idpassword typepassword placeholder密码 autocompletecurrent-password/ /div button classlogin-btn idloginBtn登 录/button div classtips© 动态星空粒子演示/div /div script const canvas document.getElementById(starCanvas); const ctx canvas.getContext(2d); let width window.innerWidth; let height window.innerHeight; let particles []; const mouse { x: null, y: null }; const PARTICLE_COUNT 120; const MAX_DISTANCE 140; const MOUSE_RADIUS 160; function initCanvas() { const ratio window.devicePixelRatio || 1; canvas.width width * ratio; canvas.height height * ratio; canvas.style.width width px; canvas.style.height height px; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); } function createParticle() { return { x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.6, vy: (Math.random() - 0.5) * 0.6, radius: Math.random() * 1.5 0.5, opacity: Math.random() * 0.5 0.3, }; } function initParticles() { particles []; for (let i 0; i PARTICLE_COUNT; i) { particles.push(createParticle()); } } function drawParticles() { ctx.clearRect(0, 0, width, height); for (let i 0; i particles.length; i) { const p particles[i]; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); ctx.fillStyle rgba(255, 255, 255, ${p.opacity}); ctx.fill(); } } function drawLines() { for (let i 0; i particles.length; i) { for (let j i 1; j particles.length; j) { const p1 particles[i]; const p2 particles[j]; const dx p1.x - p2.x; const dy p1.y - p2.y; const distance Math.sqrt(dx * dx dy * dy); if (distance MAX_DISTANCE) { const opacity (1 - distance / MAX_DISTANCE) * 0.4; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.strokeStyle rgba(255, 255, 255, ${opacity}); ctx.lineWidth 0.6; ctx.stroke(); } } } } function applyMouseRepel(p) { if (mouse.x null) return; const dx p.x - mouse.x; const dy p.y - mouse.y; const distance Math.sqrt(dx * dx dy * dy); if (distance MOUSE_RADIUS distance 0.1) { const force (MOUSE_RADIUS - distance) / MOUSE_RADIUS; p.x (dx / distance) * force * 2; p.y (dy / distance) * force * 2; } } function updateParticles() { for (let i 0; i particles.length; i) { const p particles[i]; p.x p.vx; p.y p.vy; if (p.x 0 || p.x width) p.vx * -1; if (p.y 0 || p.y height) p.vy * -1; applyMouseRepel(p); } } function animate() { drawParticles(); drawLines(); updateParticles(); requestAnimationFrame(animate); } window.addEventListener(mousemove, (e) { mouse.x e.clientX; mouse.y e.clientY; }); window.addEventListener(resize, () { width window.innerWidth; height window.innerHeight; initCanvas(); initParticles(); }); document.getElementById(loginBtn).addEventListener(click, () { const username document.getElementById(username).value.trim(); const password document.getElementById(password).value.trim(); if (!username || !password) { alert(请输入用户名和密码); return; } // 这里只是演示交互实际使用时应把数据交给后端接口处理 console.log(提交登录请求, { username, password }); alert(演示环境登录流程已触发); }); initCanvas(); initParticles(); animate(); /script /body /html以上是一份完整的实现。复制保存后本地打开就可以看到效果。但有一点必须提醒这个文件里的登录按钮只是演示不支持真实校验。真实项目的登录按钮应把数据通过HTTPS请求发送到后端这里为了保持文件“零依赖”只把交互逻辑留了口子。4. 性能调优和适配细节让它在各种设备上都丝滑4.1 粒子数量不是越多越好我第一次做这类页面时为了让效果“酷炫”把粒子数量加到了400多个。打开桌面浏览器还行一放到集成显卡的笔记本上风扇立刻起飞。粒子数量的增加不只是让绘制粒子本身变慢drawLines()里的两两距离计算量更是呈平方级别增长400个粒子意味着每帧大概8万次距离运算。所以性能优化的第一条原则就是粒子数量控制在60到180之间。低于60画面太空高于180性价比大幅降低。如果确实希望画面更饱满可以增加粒子半径和提高连线阈值从观感上弥补数量不足带来的稀疏感而不必堆粒子数量。4.2 降低计算量减少平方根操作距离计算中Math.sqrt()是一个相对耗资源的操作。实际上在判断“是否小于阈值”时可以把两边都做平方计算dx*dx dy*dy与MAX_DISTANCE * MAX_DISTANCE比较。只有满足距离条件后再用一次Math.sqrt()来算线段的透明度。这个优化在粒子数量大时性能差别明显。虽然当前量级下这微小的改动可能感知不到但作为习惯值得在代码中体现。4.3 省电模式与页面失焦处理登录页打开后通常摆在页面上等用户操作这里有一个隐藏的耗电风险即使页面失去焦点requestAnimationFrame依然在继续循环。移动端上更严重可能后台挂着这个页面导致手机发热。推荐的通用处理方式是监听document.visibilitychange事件页面隐藏时暂停动画重新可见时再恢复。let isRunning true; document.addEventListener(visibilitychange, () { if (document.hidden) { isRunning false; } else { isRunning true; animate(); } }); function animate() { if (!isRunning) return; drawParticles(); drawLines(); updateParticles(); requestAnimationFrame(animate); }这样改造后当用户切到其他标签页或锁屏时动画自动挂起回来时无缝恢复。4.4 移动端适配手势与尺寸移动端没有“鼠标移动”touchmove事件要实现类似效果可以把首个触摸点当作鼠标位置。连续的触摸滑动会让粒子产生跟随避让体验也很好。构建响应式布局时登录卡片的宽度在小于480px的设备上应调整为calc(100% - 40px)并且登录框高度需要增加到48px左右以满足触控建议尺寸。4.5 低端设备降级策略如果产品用户中有大量中低端Android设备建议加一道检测逻辑用navigator.hardwareConcurrency判断CPU核心数如果低于4直接把粒子数量降到60个或者用navigator.deviceMemory如果浏览器支持检测内存低于4GB时关闭连线功能。这些降级逻辑不会让高配设备上的视觉效果打折扣但能显著改善低端机上的流畅度。5. 常见问题与排查实录这份代码移植时最容易掉的坑5.1 粒子全部堆在左上角这是最经典的问题现象是整个画布只有左上角巴掌大的区域显示粒子其他区域全是背景色。原因就是前面提到的Canvas的绘图缓冲区和CSS显示尺寸不一致。浏览器默认Canvas缓冲区宽高是300x150如果只设width: 100%这种CSS尺寸实际绘图坐标还是按缓冲区原始尺寸来的。解决办法就是显式设置canvas.width和canvas.height属性并且一定要在setTransform中乘上devicePixelRatio。5.2 粒子动了但连线不出现如果粒子正常飘动线却没有绘制出来基本可以确定是drawLines()里遍历逻辑而不是绘制函数的问题。常见原因有三种particles.length为0初始化粒子的代码没有执行需要检查initParticles()是否在animate()之前被调用阈值MAX_DISTANCE设得太小比如小于30粒子平均间距大于阈值永远满足不了连线条件。用鼠标不断移动把粒子聚拢后看线条是否出现就能定位strokeStyle写成rgba(255, 255, 255, 1)但线段透明度正常但lineWidth设置为0会让线宽变成不可见某些浏览器实现里lineWidth0可能不绘制。检查lineWidth不要小于0.5。5.3 登录卡片无法点击粒子Canvas和登录卡片如果都设置了position: fixed或absolute需要考虑层叠顺序。粒子Canvas应该设置z-index: 0登录卡片设置z-index: 1。如果登录卡片还是点不了检查一下是否给粒子Canvas额外加了pointer-events: none。从交互角度考虑Canvas本身不需要接收任何点击事件加这个CSS属性可以让鼠标事件直接穿透Canvas落到底层元素上是推荐做法。5.4 页面尺寸变化时动画出现残影或错位利益相关场景常见于用户旋转手机或者拖动浏览器窗口。残影问题一般是clearRect()没有清除干净以及清除时使用了旧的width和height。resize事件监听器里一定先更新width和height变量再调用initCanvas()最后重新生成粒子。否则粒子坐标基于新的窗口尺寸画布本身却还是旧尺寸画面就会错乱。5.5 代码粘贴到Vue或React项目中发现闭包问题有些朋友不是用原生HTML而是想把这份逻辑集成到现代前端框架中。此时会遇到的主要问题window.addEventListener(resize, handleResize)在组件销毁时没有解绑导致切换路由后事件仍触发报canvas为null的错误。迁移到框架里时必须在unmount或beforeUnmount钩子中移除事件监听。同时requestAnimationFrame保存一个animationId组件卸载时用cancelAnimationFrame取消循环。6. 扩展创意让“粒子星空”不止于登录页6.1 把粒子背景抽成可复用组件既然已经写好了粒子主逻辑接下来可以把它封装成一个不依赖外部库的小组件。爆几个接口粒子数量、连线阈值、粒子颜色、鼠标响应开关。做成一个构造函数或类通过传入配置项生成不同氛围的背景。深蓝星空适合登录页换成青绿渐变的“极光”风格适合品牌宣传页金色粒子配合暗红背景适合年末活动页。这个改造的收益是持续性的以后每个新项目都能一行代码启用。6.2 添加声音反馈或者点击波纹如果产品是面向年轻群体的创意展示页可以给粒子加上点击效果——比如用户点击画布时在点击位置向外扩散一圈涟漪涟漪经过的粒子会短暂变亮然后恢复。这类效果会让用户感受到页面是“活”的。难点在于涟漪数据结构和粒子变亮逻辑的状态管理但核心思路和粒子避让类似多加一个状态数组就行。6.3 从二维粒子升级为三维星空在Canvas中模拟三维粒子也不难——粒子引入z坐标绘制时使用透视投影公式将三维坐标映射到二维平面上。近处的粒子大而且亮远处的粒子小且暗。粒子的运动逻辑从平面漂移改为围绕一个中心点旋转或者做云团状扩散。这个视觉升级会让登录页的“科技感”再上一个台阶但代价是代码复杂度增加不少如果当前只想要一个轻量高颜值的登录页二维方案已经足够。这段时间反复调试下来我的个人感受是想要做出真正成功的粒子登录页重点根本不在粒子有多炫而在于做出“背景在动但不抢内容”的平衡感。粒子数量、透明度、速度三个变量一旦配合得当整张页面自然就有高级感。如果你第一次调参时觉得不好看不必急着推翻重来先把粒子数降到80速度调慢一半看看整体氛围是否反而舒服多了——这个页面的设计逻辑里“克制”带来的收益往往比“堆叠”更多。登录页如此很多前端的活儿也一样技术点到为止比什么都往上加更能赢得用户好感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑