简介:网页彩点背景是一个使用 JavaScript 实现的动态背景源码压缩包,适合前端初学者、网页设计师以及希望在页面中快速加入动态视觉效果的开发者。压缩包内共 2 个文件,包含一个 HTML 页面与一个 JavaScript 脚本,整体仅 2KB,结构精简,便于直接阅读和修改。该实现覆盖 CSS3 @keyframes 动画、setInterval 定时器、Math.random 随机数生成、createElement 与 style 操作等核心知识点,也用到事件监听与响应式适配思路,可作为理解浏览器端动画原理的入门范例。彩点位置、大小、颜色如何随机生成,定时更新与动画循环如何协同工作,以及 requestAnimationFrame 优化渲染性能、媒体查询适配不同屏幕等提升思路,对从静态页面过渡到动态交互很有参考价值。目前已有 3142 人学习下载,适合在个人站点、活动页或博客中引入动态彩点背景时借鉴复用。
1. 网页彩点背景.zip:把花哨背景变成可复用组件,到底值不值得折腾
打开任何有点审美的开发者站点,你可能都见过那种彩色小圆点缓缓漂浮的背景,比纯白底高级,又比视频背景轻。很多第一次看到“网页彩点背景.zip”的人以为里面是一张动态图或现成插件,装上就能用。实际上,它是一整套用 Canvas 实现的动态背景方案,包含绘制逻辑、参数配置和打包好的资源,目的是让你把这种花哨效果真正复用到自己的页面上。这套东西解决的核心问题不是“画几个圆点”,而是怎么在浏览器里稳定跑几十上百个粒子还不卡、不糊、不挡住页面交互。适合前端开发者、独立开发者,以及想给个人站点或活动页快速加点质感的从业者。下面我直接把它拆开:先探清原理,再给能跑的最小代码,最后把参数和坑一次讲明白。
2. 彩点背景的实现方案:为什么选 Canvas 而不是 CSS 动画
2.1 两种路径的对比:DOM 动画最后都会卡,Canvas 是正路
很多人第一次做彩点背景会先试 CSS 动画,用几个 span 设置不同背景色、圆角,再加 animation 让它们移动。20 个点以内效果还行,一旦想做到 60 个、100 个,页面就开始发虚,滚轮滚动时明显掉帧,因为浏览器要把几十个独立 DOM 节点持续做样式重算和合成。另一个致命问题是,点与点之间做不了碰撞或连线,因为 CSS 动画没有全局坐标概念,每个节点都是黑匣子。
相比之下,Canvas 是一整块画布,所有圆点的位置、颜色、速度都维护在一个数组里,每次刷新只调用一次重绘 API。100 个点也只有一个 canvas 节点,性能开销可控。常见做法是将点的坐标和向量放在普通 JavaScript 对象里,不频繁创建和销毁对象,而是复用数组项。选 Canvas 的另一个理由是可以自由控制绘制顺序:先画底层大点、再画上层小点,透明度叠加出来的层次感是 CSS 很难模拟的。这是我的第一推荐,后面的所有代码都基于 Canvas。
2.2 最小可运行代码:先跑起来再谈调参
下面是一份最精简的彩点背景,打开浏览器就能看到一个黑色背景上随机漂浮 50 个彩色圆点。它只有绘制循环和随机初始化,没有交互,没有响应式,但足以让你看清核心逻辑。
const canvas = document.getElementById('dot-bg-canvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const dots = []; const COUNT = 50; const COLORS = ['#f94144', '#f3722c', '#f8961e', '#90be6d', '#43aa8b', '#577590']; function random(min, max) { return Math.random() * (max - min) + min; } for (let i = 0; i < COUNT; i++) { dots.push({ x: random(0, canvas.width), y: random(0, canvas.height), vx: random(-0.5, 0.5), vy: random(-0.5, 0.5), radius: random(2, 6), color: COLORS[Math.floor(Math.random() * COLORS.length)] }); } function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (const dot of dots) { dot.x += dot.vx; dot.y += dot.vy; if (dot.x < 0 || dot.x > canvas.width) dot.vx *= -1; if (dot.y < 0 || dot.y > canvas.height) dot.vy *= -1; ctx.beginPath(); ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2); ctx.fillStyle = dot.color; ctx.fill(); } requestAnimationFrame(tick); } tick();逻辑很直白:数组里每个点存当前坐标、速度向量、半径和颜色;每帧清屏后遍历数组,把 x 和 y 加上速度,碰到边界就把速度反向;再用 arc 画一个实心圆。requestAnimationFrame代替 setInterval,浏览器会按屏幕刷新率回调,不会出现两个定时器叠加的问题。这里有两个重要参数:COUNT控制点数量,vx/vy的随机范围控制移动速度。如果你觉得点动得太慢,把随机范围从 -0.5~0.5 提到 -1~1 就行。这个最小版本最大的问题是窗口缩放后画布尺寸不变,以及高分屏上会发糊,后面我会专门讲怎么修。
2.3 坐标系统与绘制循环的底层逻辑
Canvas 的坐标原点在左上角,x 向右增加,y 向下增加,所以vy为正时点在往下走。很多人调速度时老觉得方向反了,不是反了,是你心里用了数学课的向上 y 正方向。绘制循环里最标准的顺序是:清屏、更新状态、绘制。顺序不能乱,先清屏可避免上一帧的残影。如果你想做拖尾效果,可以直接把clearRect去掉,改成用半透明黑色重新覆盖全屏,就能产生视觉残留,但这里做的是干净背景,必须全清。
每帧都重算坐标还牵扯到时间差的问题:requestAnimationFrame的回调时间间隔在不同显示器上不一样,常见的 60Hz 屏是 16.7ms,120Hz 屏是 8.3ms。同一套速度参数在 120Hz 屏上会跑快一倍,这就是很多人说“demo 在自己电脑上正常,到别人电脑上像开挂”的原因。稳妥的修法是用两帧的时间戳差做系数,把位置增量乘以deltaTime。最小版没写,是因为我默认你先跑通流程,理解变量含义后再加物理时间修正。下面调参阶段我会给出完整写法,那才是真正能作为组件的版本。
3. 把彩点调成你真正想要的样子:核心参数与交互设计
3.1 参数表:数量、半径、透明度、色板与速度的推荐区间
彩点背景好不好看,其实不是代码决定的,是参数决定的。我整理了一张常用参数表和调参方向,你可以直接用,也可以根据自己的场景微调。数量不是越多越好:个人博客建议 30~60,品牌首页建议 80~120,超过 150 就必须考虑性能降级。半径建议在 2~8 之间浮动,太小看不清颜色,太大容易糊成一片。透明度上,canvas 的globalAlpha建议控制在 0.6~0.9,低一点会显得有通透感。色板不要直接用纯红纯蓝,HSL 模式微调饱和度和亮度更容易做出高级感。速度上,漂浮类点位移每帧 0.3~1.2 比较温和,想要活泼可以到 1.5,但超过 2 就有点干扰阅读了。
| 参数 | 推荐区间 | 说明 |
|---|---|---|
| 点数量 | 50~120 | 数量越大越热闹,但低端设备会卡 |
| 点半径 | 2~8 | 混合大小比统一大小好看 |
| 全局透明度 | 0.6~0.9 | 降低透明度能做出景深错觉 |
| 移动速度 | 0.3~1.5 | 单位是像素/帧,需要根据刷新率修正 |
| 色板 | HSL 饱和度 60%~80% | 避免荧光感 |
| 呼吸幅度 | 0.1~0.3 | 让半径或透明度周期变化 |
3.2 从静态到“呼吸”效果:半径与透明度的正弦扰动
纯漂浮的彩点只是平移,看久了还是会腻。常见做法是给每个点加一个相位,让半径或透明度随正弦函数波动,产生一伸一缩的呼吸感。实现成本极低,思路是每个点记录一个随机起始相位phase,每帧让相位累加,绘制半径时用radius * (0.8 + 0.2 * Math.sin(phase))。相位差会让每个点呼吸节奏错开,画面就活起来了。透明度和半径同理,你可以给透明度也来一条正弦曲线,但要注意不要让透明度降到 0,那会造成闪烁。
const dots = []; const COUNT = 80; const BASE_OPACITY = 0.75; for (let i = 0; i < COUNT; i++) { dots.push({ x: random(0, canvas.width), y: random(0, canvas.height), vx: (Math.random() - 0.5) * 1.2, vy: (Math.random() - 0.5) * 1.2, radius: random(2, 7), phase: Math.random() * Math.PI * 2, speed: 0.02 + Math.random() * 0.03, color: COLORS[Math.floor(Math.random() * COLORS.length)] }); } // 每帧更新 for (const dot of dots) { dot.x += dot.vx; dot.y += dot.vy; dot.phase += dot.speed; const breath = 0.8 + 0.2 * Math.sin(dot.phase); const r = dot.radius * breath; ctx.beginPath(); ctx.arc(dot.x, dot.y, r, 0, Math.PI * 2); ctx.fillStyle = dot.color; ctx.globalAlpha = BASE_OPACITY * (0.85 + 0.15 * Math.sin(dot.phase * 1.5)); ctx.fill(); } ctx.globalAlpha = 1;这段代码里有几个设计点。phase是一个 0~2π 的随机值,breath最终落在 0.6~1.0 之间,所以点不会缩小到消失。透明度用了另一个正弦频率phase * 1.5,视觉上半径和透明度不完全同步,看起来更自然。别忘了每帧最后把globalAlpha重置为 1,否则会影响后面画布上其他内容。这里也能看到,一个点的状态对象里存了所有的动态属性,这是 Canvas 粒子系统的通用思路。如果你要调整呼吸快慢,改speed,推荐 0.01~0.05,太快会让人头晕。
3.3 鼠标跟随与点击散开:交互让背景不死板
静态漂移毕竟是背景,加了鼠标交互才有“活的网页”的体感。简单且出效果的做法有两种:一种是鼠标附近一定范围内的点被推开,产生斥力;另一种是鼠标移动时持续生成新的彩点短尾迹。我项目里常选斥力式,因为不会意外地往画布里堆大量新对象。实现思路是在每帧更新坐标之前,先检查鼠标位置与点的距离,如果小于设定半径(比如 120px),就按反方向给点一个加速度。
let mouse = { x: -9999, y: -9999 }; window.addEventListener('mousemove', (e) => { mouse.x = e.clientX; mouse.y = e.clientY; }); function updateDot(dot) { const dx = dot.x - mouse.x; const dy = dot.y - mouse.y; const dist = Math.sqrt(dx * dx + dy * dy); const PUSH_RADIUS = 120; if (dist < PUSH_RADIUS && dist > 0) { const force = (PUSH_RADIUS - dist) / PUSH_RADIUS; dot.vx += (dx / dist) * force * 0.6; dot.vy += (dy / dist) * force * 0.6; } dot.x += dot.vx; dot.y += dot.vy; dot.phase += dot.speed; // 限速,防止点被推得飞出去 const maxSpeed = 2; const speed = Math.hypot(dot.vx, dot.vy); if (speed > maxSpeed) { dot.vx = (dot.vx / speed) * maxSpeed; dot.vy = (dot.vy / speed) * maxSpeed; } }这里force在靠近鼠标时趋近 1,远处趋近 0,所以点离鼠标越近被推开越快。0.6 是加速度系数,实际调的时候如果你觉得点飞太猛,就降到 0.3。限速是个容易被忽略的细节,因为单帧加速度叠加后速度可能无限增长,点会一下弹走再也回不来。鼠标初始值放-9999是为了页面上还没有鼠标移动事件时,点不会集体往左上角飞。加了这层交互后,彩点背景就不再是纯装饰,而是能给访客带来“页面有反应”的正面体验。
4. 彩点背景常见问题排查:卡顿、模糊、事件穿透与电量消耗
4.1 高分屏上画面发虚:canvas 没处理 devicePixelRatio
现象:在自己电脑上点看起来圆润清晰,放到一台 2K 或 Retina 屏上,圆点边缘发糊,线条粗细不均。
原因:Canvas 的物理像素大小和 CSS 逻辑像素大小不一致。canvas.width = window.innerWidth设置的是画布的物理像素宽度,但在 CSS 里如果写width: 100%; height: 100%;,画布会被拉伸。高分屏的 devicePixelRatio 通常是 2 或更高,一个逻辑像素对应两个物理像素,导致每个圆点被放大两倍,边缘自然模糊。
解决:在设置 canvas 宽高时乘上window.devicePixelRatio,并且给 canvas 样式固定为视口尺寸。常见做法是封装一个 resize 函数:
function resizeCanvas(canvas) { const dpr = window.devicePixelRatio || 1; const rect = document.body.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); }红字要点是ctx.scale(dpr, dpr)。如果只调大 canvas 尺寸而不做缩放,绘制坐标会错乱,因为在 dpr=2 的屏幕上半径 5px 实际画成 10px。先 scale 后,你后续所有坐标和半径都不需要再乘 dpr。每次 resize 时还要清空旧状态,并重新给每个点做坐标约束,否则旧点可能在 canvas 范围外。
4.2 滚动页面时卡成 PPT:点数量不是唯一凶手
现象:页面内容很多,滚动时彩点背景跟页面一起滚动,卡顿明显;或是不滚动时单独看动画很流畅,一开其他动效就掉帧。
原因:如果是固定定位的背景,想不卡主要是减少每帧 CPU 计算。除了点数量过多外,很多人会在每帧调用ctx.shadowBlur来加发光效果,这个属性性能开销极大,一个带阴影的圆点可能需要几十次 GPU 调用。另外,resize 事件里没有做防抖,每次窗口变化触发几十次重绘,也会卡。
解决:先去掉所有 shadow 相关属性,改用径向渐变模拟光晕。其次给 resize 加防抖,我习惯用 150ms 的延迟。第三是控制总绘制面积,点半径总和越大越慢。最后可以按设备性能自动减点:页面加载后先跑 2 秒,如果 FPS 低于 45,就把点数量减半并降低呼吸频率。这属于性能兜底逻辑,虽然每个点少画几笔,但用户感知不明显,换来了整体流畅。
4.3 canvas 挡住了页面上的按钮:pointer-events 默认不是 none
现象:背景铺满全屏后,页面上的链接和按钮点击失效,或者鼠标移上去时浏览器不显示手型光标,但在地址栏刷一刷又好了。
原因:canvas 元素默认是一个普通 DOM 节点,会接收鼠标事件。当它覆盖在按钮上方时,按钮当然收不到点击。这个坑特别常见,因为很多人把 canvas z-index 设为 0,但忘了按钮的 z-index 没设,或者 canvas 本身高度占了全屏。
解决:给 canvas 加 CSS 样式pointer-events: none,它就能让鼠标事件穿透到下方元素。但要注意,如果你做了鼠标交互效果,这个样式会让 canvas 上的 mousemove 监听失效。正确做法是把鼠标监听挂在 window 或 document 上,而不是 canvas 上,这样穿透和交互两不误。还有个细节是移动端,touch 事件也要同样处理,否则有 onclick 的元素会被背景拦截。
4.4 手机电量消耗过快:后台或锁屏时动画没停
现象:用户切到其他标签页或锁屏后,返回页面发现手机发烫,或者动画继续跑,CPU 占用很高。
原因:requestAnimationFrame在标签页不可见时会自动暂停,但前提是浏览器没有触发强制后台运行。更常见的问题是,你在循环里同时用了音频播放、定时器或 WebSocket 心跳,它们会把页面唤醒,动画也会跟着继续跑。另一个被忽略的点是,动画中每帧都创建了新的数组或对象,导致 GC 频繁触发,耗电明显。
解决:监听document.visibilitychange事件,页面隐藏时用cancelAnimationFrame停止循环,可见时重新启动。这已经是所有粒子类组件的基操。在移动端,可以进一步把点半径和数量降低,很多高端机型 GPU 很强,但 CPU 省电模式下微弱的性能差别都会让耗电流量翻倍。我做这套背景时还会避免在构造函数里直接跑 while 循环,所有数据初始化一次,循环里只做加法,为的就是减少垃圾回收。
4.5 颜色太艳或者整体太脏:色板和透明度的锅
现象:点单个看很漂亮,混在一起变成一团“荧光泥”,或者浅色背景上某些颜色看不清,深色背景上又刺眼。
原因:用随机 RGB 生成颜色,很容易出现饱和度接近满格的组合;颜色之间没有主次关系,每个点都想跳出来,视觉就乱了。
解决:固定色板,从 5~7 个精心选过的颜色里随机选,饱和度控制在 50%~75%,明度控制在 60%~85%。如果想要更高级的渐变呼吸感,可以给每个点按 HSV 插值取色。透明度方面,整体建议 0.7~0.9,但不要全部一样,随机 0.2 的透明度差能增加层次。如果你用的是深色背景,把饱和度适当提高到 80%,亮度提高到 80%;浅色背景则相反,亮度太高会看不清。这些经验不是公式,但能避开 90% 的翻车情况。
5. 把彩点背景做成 zip 资源:文件结构、打包与集成方式
5.1 一个干净可复用的文件结构
当你把彩点背景从一个 demo 页面变成可分发资源,就需要规划文件结构,而不是把代码都塞在index.html里。我一般会这样组织:
dot-bg/ |-- index.html |-- css/ | `-- dot-bg.css |-- js/ | `-- dot-bg.js `-- README.mdindex.html是演示页,用来直接预览效果;dot-bg.js是核心逻辑,里面写一个DotBg类或工厂函数,包括初始化、动画循环、resize 监听和销毁方法;dot-bg.css负责把 canvas 定位到fixed并设pointer-events: none;README.md写清用法和参数。这样做的好处是,别人拿到 zip 后能在一分钟内用起来,不需要理解任何内部实现。如果你要把这个 zip 共享给同事或交付给甲方,这四样东西是底线。
5.2 通过配置项初始化:不要改源码
很多初学者的组件没法用,就是因为参数写死在循环里,每次都要去源码里找数字改。成为一个靠谱的组件,必须暴露配置入口。我会在dot-bg.js里写一个initDotBg(options)函数,用Object.assign把默认值和用户传入的 options 合并,而不是直接options || {},这样能避免用户少传参数时出现 undefined 错误。
function initDotBg(options) { const defaults = { count: 80, colors: ['#f94144', '#f3722c', '#f8961e', '#90be6d', '#43aa8b'], minRadius: 2, maxRadius: 7, speed: 1, breathing: true, mousePush: true }; const settings = Object.assign({}, defaults, options); const canvas = document.createElement('canvas'); canvas.id = 'dot-bg-canvas'; document.body.appendChild(canvas); // 关键逻辑:从 settings 读取所有参数 const dots = []; for (let i = 0; i < settings.count; i++) { dots.push({ x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, vx: (Math.random() - 0.5) * settings.speed, vy: (Math.random() - 0.5) * settings.speed, radius: settings.minRadius + Math.random() * (settings.maxRadius - settings.minRadius), color: settings.colors[Math.floor(Math.random() * settings.colors.length)], phase: Math.random() * Math.PI * 2 }); } return { destroy() { cancelAnimationFrame(animationId); canvas.remove(); window.removeEventListener('resize', onResize); } }; }注意settings.speed是速度系数,我把它直接乘到速度向量上,用户传 0.5 就是慢半拍,传 2 就是快一倍。destroy方法很重要,它让组件可以在单页应用里被安全卸载。很多人只做初始化,从不做清理,导致路由切换后 canvas 还留在 DOM 里,动画还在跑,这是内存泄漏的经典来源。把destroy写在返回对象里,谁拿到实例谁就负责销毁,符合最小约定。
5.3 集成到 Vue 或 React 项目时的生命周期触碰
现代前端项目基本不会再整个复制 HTML 文件,而是把逻辑封装进组件。如果你用的是 Vue,会在mounted里调用initDotBg,在beforeUnmount里调用destroy。React 则是在useEffect的清理函数里做同样的事。这里最容易犯的错是在初始化时读取window.innerWidth,但组件挂载时 CSS 可能还没渲染完成,获取的宽度是旧的。稳妥做法是在resize事件里统一更新 canvas 尺寸,初始化时先强制一次 resize。
function mountDotBg() { const bg = initDotBg({ count: 100 }); // 路由离开时 return () => bg.destroy(); }打包发布时,为了不污染全局,我会把initDotBg挂在一个命名空间下,比如window.DotBg.init,这样使用者不用 import 也能用。如果你要考虑多页面共存,每个页面实例必须独立,不能共享同一个 canvas。我见过同一个函数在多个页面同时调用后,后面的覆盖前面的 Canvas 对象,动画速度翻倍,最后所有页面都乱了。这里的最佳实践是:一个组件实例对应一个 canvas,互不干扰。若是用微前端,还需要考虑主应用和子应用的 body 是否共享,建议把 canvas 挂在当前页面的根节点下,而不是 body 下。
6. 进阶玩法与验证方法:做一个能拿得出手的彩点背景
当你把基础版彩点跑顺,可以继续往上加东西。我最常用也最推荐的一个技巧是“自动降级”,它能让组件在生产环境里真正敢用:页面加载后自动跑 2 秒,手动记录这一时间段内的平均帧率,如果低于 45,就把count降到原来的一半,并关闭呼吸效果;如果还低于 30,就直接关闭动画,展示静态渐变背景。这个兜底策略比任何“高性能优化”都管用,因为真实用户机器差异太大,你不可能只在开发机上流畅。
验证方法也很简单,用requestAnimationFrame记录时间戳,每 10 帧计算一次 FPS,显示在页面角落。注意 FPS 不等于绝对性能,同一台机器背景透明度和半径都会影响结果,你要以“正常使用场景”为准。还有一个习惯是,在控制台里手动执行performance.memory观察内存趋势,如果反复进出页面后内存只涨不降,大概率是 destroy 没清理干净。我一般会在演示页放一个切换按钮,来回切十次看内存占用,这比任何静态审查都直接。
给想上手的人一个收尾建议:不要一开始就追求“完美”,先把最小版跑通,再加呼吸感,再加鼠标交互,再加性能降级。每加一层,都用 FPS 工具验证一次,别等全都写完了再去查卡顿。这样每一步都能看到明确变化,出问题也知道是哪一步引入的。我见过不少同学一上来就加连线、加图片粒子、加拖拽,最后代码几百行,连自己都说不清哪里卡,最后只能重写。保持简单,把参数暴露给使用者,让它在不同屏幕上都能自适应,这比花里胡哨重要得多。
最后说个我的血泪经验:把背景做出来只是第一步,真正难的是“像空气一样存在”。用户感知不到背景卡、糊、遮挡,才算合格。希望这篇笔记能让你少踩几个坑,做出自己满意的彩点背景。希望帮到你。
本文还有配套的精品资源,点击获取