简介:一款基于HTML5、CSS3与jQuery实现的3D雪花飘落背景动画源码,适合网页前端开发者借鉴和复用,可直接嵌入个人网站、活动专题页或节日氛围页面,增强背景的立体感与冬日意境。压缩包共4个文件,包含2个JavaScript脚本、1个HTML页面和1个CSS样式表,整体仅10KB;JavaScript脚本负责雪花对象的生成、坐标更新、飘落路径以及Canvas逐帧重绘,CSS样式表则利用transform、animation与keyframes规则定义雪花在三维空间中的旋转、缩放和渐变下落效果,代码结构清晰,方便按需调整雪花数量、尺寸、速度与透明度。目前已有66人学习下载。仔细研读源码可以掌握Canvas动态绘图与requestAnimationFrame动画循环的协作方式,理解CSS3 3D变换和关键帧动画的实际落地写法,同时学会借助jQuery简化DOM操作与事件绑定;对于想要入门Web特效,或希望快速获取现成雪景动画素材的开发者而言,这是一份小巧实用的参考资料。
1. 当页面需要“3D雪花”时,技术选型不只是引擎问题
“H5大雪纷飞3D雪花背景动画”这个标题看起来像是一个特效资源包,实际上涵盖了三件事:H5页面如何在移动端渲染大量3D视觉粒子、如何让背景层不挡住业务操作,以及打包成zip后如何快速验证。这种效果通常用在品牌活动页、倒计时首页、直播礼物氛围和企业年终首屏;一旦掉帧,用户对页面的第一印象会立刻变差。适合读这篇的人,是前端开发、H5混合开发或独立开发者,不一定需要WebGL基础,但需要能在现有项目里独立改造并维护一个可改参数的雪花背景。下面给出纯Canvas 2D和Three.js两套代码,再讲参数设置与落地时的坑。
2. 3D雪花背景的核心原理与选型:先搞懂“3D”是怎么画出来
在动手写代码之前,得先分清这个标题里的“3D”是哪种3D。我见到的zip包里,至少有一半是用Canvas 2D仿三维视差,不是WebGL空间投射。两种方案的代码结构、调参手段和兼容性差别很大,但最关键的是你能看懂它,才能改参数。
2.1 粒子系统:雪花背后的最小物理模型
雪不是一个图片在屏上飘,而是一组粒子。每个雪花保存自己的x、y、z坐标和速度vx、vy、vz,然后在每一帧里执行两步:更新位置,把三维坐标投影成屏幕上的二维坐标。公式通常长这样:
const scale = perspective / (perspective + z); const screenX = centerX + (x - centerX) * scale; const screenY = y * scale; const size = baseSize * scale;逻辑说明:当z增大时scale减小,雪花就变小并移向观察方向;z减小则靠近镜头变大。这就是最便宜的“3D感”。由于没有真实深度缓冲,每一帧绘制顺序并不严格按深度排序,但雪花粒子互相重叠不敏感,所以视觉上不会露馅。
参数说明:perspective取值越大,透视越平缓;取值在200到600之间比较自然。z的取值范围决定雪花从远到近的跨度,跨度越大视差越明显,同时也意味着有时雪花会突然变大显得“冲脸”。你可以把z的变化率也当成速度的一个维度,用来制造“近处下得快、远处下得慢”的视差。
真正做沉浸式雪花背景,不只是让一个粒子匀速下落,还要控制它的亮度与雾化效果。近处雪花应更大、更亮,远处应更小、更透明,这比三维坐标本身更影响观感。透明度建议写成alpha = baseAlpha * (z / maxZ),当z小(近)时透明度高,z大(远)时偏暗。不过Canvas的globalAlpha结合粒子绘制容易重影,所以更多是把透明度和颜色一起写进填充样式。
2.2 渲染方案对比:CSS动画、Canvas 2D、WebGL/Three.js
| 方案 | 粒子数上限 | 3D程度 | 包体成本 | 典型适用 |
|---|---|---|---|---|
| CSS transform/keyframes | 30~60 | 伪3D,靠缩放和透明度 | 无 | 极简H5,背景不追求交互 |
| Canvas 2D | 150~300 | 伪3D,z轴缩放与视差 | 无 | 活动页通用,性能稳定 |
| Canvas + Sprite 粒子系统 | 300~1000 | 可做3D投影,带贴图 | 贴图资源 | 需照片级雪景 |
| Three.js / WebGL | 1000~10000 | 真3D空间,可转镜头 | three.min.js或gzip约150k | 强氛围首屏、重交互 |
注意表格中“包体成本”不是0,程序本身也有成本。前三项没有外部库依赖,这是“H5大雪纷飞3D雪花背景动画.zip”这类资源包最常见的做法,因为它解压之后直接双击就能看到效果,不依赖“运行环境必须联网加载CDN”。如果上来就用Three.js,反而会让工单群进到“为什么file://下没效果”的问题。
我一般会这样选型:如果项目在微信公众号内部浏览器、小程序内嵌H5或低端安卓机上运行,采用纯Canvas 2D,把粒子数控制在160~220,避免JavaScript垃圾回收抖动。如果营销页面需要割裂出“近景大雪花扫过屏幕”的冲击感,再考虑引入Three.js Points。这里的取舍不是“WebGL更牛就选WebGL”,而是“zip包能不能在移动端开箱即用”。
2.3 为什么CSS雪景替代不了“3D雪花背景”
CSS动画做雪,是给每个DOM节点配置transform: translate3d和animation-delay。节点少时看着干净,但30个以上在低端安卓上就开始卡顿,因为每一个节点都触发独立的动画合成层;更重要的是CSS动效拿不到粒子坐标,后续想“风吹手滑、点击炸雪花”都很难扩展。
反过来用Canvas,把所有雪画在一个画布里,只有画布这一个合成层,粒子数量多一倍也不见得慢,而且随时能读取粒子对象数组做交互。把多个z平面的粒子按照不同速度平移,就是“3D背景”最核心的纵深感。当你在性能面板里看到帧率从30fps升到55fps,自然明白为什么大多数zip包里是Canvas脚本而不是动画库。
3. 用 Canvas 2D 写一个可直接运行的 H5 3D 雪花背景
这一章给出一份无外部依赖的HTML文件,保存为index.html即可打开;也是很多zip包里常见的首页文件框架。
3.1 最小可运行实现:透视投影与循环雪池
下面这串代码是完整页面,不考虑加载任何库,直接复制到空HTML里可运行。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>H5 3D雪花背景动画</title> <style> html, body { margin: 0; height: 100%; overflow: hidden; background: linear-gradient(180deg, #0a0e27, #1b2040); } .snow-canvas { position: fixed; inset: 0; z-index: 0; pointer-events: none; } .page { position: relative; z-index: 1; color: #fff; text-align: center; padding-top: 28vh; font-family: system-ui; } .page h1 { font-size: 3rem; margin: 0 0 .5rem; text-shadow: 0 2px 10px rgba(0,0,0,.5); } .page p { opacity: .8; } </style> </head> <body> <canvas class="snow-canvas" id="snowCanvas"></canvas> <div class="page"> <h1>大雪纷飞</h1> <p>H5 3D雪花背景动画</p> </div> <script> const canvas = document.getElementById('snowCanvas'); const ctx = canvas.getContext('2d'); let width, height; const cfg = { count: 180, windX: 0.25, fallSpeed: 1.1, minSize: 1, maxSize: 3.5, perspective: 300, zMin: 1, zMax: 4, baseAlpha: 0.75, resetZ: 0.2 }; let snow = []; function rand(min, max) { return min + Math.random() * (max - min); } function createParticle(initial) { const p = { x: rand(0, width), y: initial ? rand(0, height) : -4, z: rand(cfg.zMin, cfg.zMax), vx: cfg.windX + rand(-0.15, 0.15), vy: cfg.fallSpeed + rand(0, 0.4), size: rand(cfg.minSize, cfg.maxSize) }; return p; } function resize() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; } function init() { resize(); snow = Array.from({ length: cfg.count }, () => createParticle(true)); window.addEventListener('resize', resize); } let rafId = null; function frame() { update(); draw(); rafId = requestAnimationFrame(frame); } function update() { const depth = 0.003; for (let i = 0; i < snow.length; i++) { const p = snow[i]; p.x += p.vx; p.y += p.vy; p.z -= depth; if (p.z < cfg.resetZ || p.y > height + 6) { snow[i] = createParticle(false); } if (p.x < -6) p.x = width + 6; if (p.x > width + 6) p.x = -6; } } function draw() { ctx.clearRect(0, 0, width, height); const centerX = width / 2; for (const p of snow) { const scale = cfg.perspective / (cfg.perspective + p.z); const x = centerX + (p.x - centerX) * scale; const y = p.y * scale; const alpha = cfg.baseAlpha * (0.4 + 0.6 * (p.z - cfg.zMin) / (cfg.zMax - cfg.zMin)); ctx.fillStyle = '#fff'; ctx.globalAlpha = alpha; ctx.beginPath(); ctx.arc(x, y, p.size * scale, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; } init(); frame(); </script> </body> </html>逻辑说明:
update里每一帧把p.z减小,相当于雪花逐渐靠近镜头,视觉上就会成为一个“从远到近压过来”的效果;重置到resetZ的时候雪花又回到远处。这个“循环雪池”避免了不断创建和销毁对象,也让GC不要稳定几十毫秒一次。scale决定每个雪花在屏幕上的投影位置和直径;z越大scale越小,投影位置也越靠近视口中线,这正是中心透视与正交滚动的差别。
参数说明:count是粒子总数,控制在120~240;windX加random之后形成风摆,fallSpeed是基础下落速度;perspective建议不要低于200,否则近处雪片放得太大,屏幕边缘会出现明显径向拉伸。globalAlpha需要在每帧画完后恢复为1,避免下一帧继续影响背景清除。如果你把页面背景改成纯白,那就把雪的填充颜色改成#eef或加阴影,否则白底白字会看不出来。
3.2 嵌入业务页面:z-index、pointer-events 与页面手势
上面代码里canvas使用position: fixed; inset: 0; z-index: 0,同时pointer-events: none,目的是让背景不接收任何鼠标或触摸事件。业务内容一般放在z-index: 1的容器里。如果不加pointer-events: none,页面里的按钮、列表滚动很容易被canvas挡住,尤其在iOS Safari下点击区域会被那一层不可见的画布吃掉。
在uniapp嵌入微信公众号里的H5页面场景中,你通常不会把这个canvas写进每个page,而是把它挂在全局App.vue或index.html里,让背景层常驻。要注意页面容器如果设了overflow: auto,fixed定位依然是相对viewport,不受滚动影响;但有些老机型对inset: 0的CSS支持不够完善,可以改成left:0; top:0; width:100%; height:100%。
另外一个到处踩的问题:H5页面里如果有横向轮播图或tab切换,雪花的vx随机偏移可能会被误认为是页面漂移。若想要风雪更贴合默认阅读动线,可以把windX调低到0.15,再把随机偏移控制在±0.1以内。别小看这个参数,它决定了用户对首屏稳定性的第一感受。
3.3 让“3D感”更强:分组景深、模糊与近景大雪花
纯z缩放已经能看出空间感,但为了让视觉效果更像3D场景,我一般会把粒子分成两组:远场景(z大于2.5)和近场景(z小于等于2.5)。近景组里额外加几个大粒子,半径是普通雪花的2到3倍,下落速度也加快一些;远景组则降低透明度和半径,并在draw里用ctx.filter = 'blur(1px)'做一次轻模糊。注意ctx.filter在移动端部分浏览器上开销较大,不要每帧对全部雪花开滤镜,只对远景组开启就行。
如果你需要雪花有真实冰晶的棱角,不要用圆形ctx.arc,可以改用生成一个六角星形顶点数组,然后按同一路径填充。但六边形消耗比圆高,粒子数大于150时建议用圆形加径向渐变模拟冰晶的高光。很多人把“3D雪花”误解成“每个雪花必须有六角形状”,实际上3D感来自深度和运动视差,而不是贴图是否逼真。
4. 用 Three.js 实现真正的 H5 3D 雪花场景(可选增强)
当纯2D投影不能满足“需要镜头转动”的设计时,我们再上Three.js。这一章专注用Points实现大量雪粒子的3D渲染,而不是把每片雪花建模成Mesh,那样性能会立刻崩。
4.1 为什么选 Points(点精灵)而不是 Mesh
Three.js里渲染1000个BoxGeometry也会让draw call破千,移动端很难吃消。Point是GPU粒子的最简入口,所有雪花只占一个draw call,每个粒子通过Position坐标和尺寸来控制外观。配合BufferGeometry,可以直接在CPU里挪动坐标,再把needsUpdate置为true即可。如果你的雪花还需要不同的大小/透明度,可以用PointsMaterial的单一size,或者用ShaderMaterial对gl_PointSize和varying alpha做更细粒度的控制;后者更专业,但调试成本也高。
这一章给出的方案,把Three.js构建成离线本地文件,放进zip包,避免外链CDN在活动当天挂掉。
4.2 离线可用的 Three.js 雪花场景最小代码
这个示例假设你已经在assets/js/three.min.js放下了Three.js发行版。不需要外部纹理图,雪花用CanvasTexture动态生成。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>H5 3D雪花背景 - Three.js</title> <style> body { margin: 0; overflow: hidden; background: #0a0e27; } canvas { display: block; } </style> </head> <body> <script src="./assets/js/three.min.js"></script> <script> const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100); camera.position.set(0, 0, 5.2); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(innerWidth, innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const COUNT = 1400; const positions = new Float32Array(COUNT * 3); for (let i = 0; i < COUNT; i++) { positions[i * 3] = (Math.random() - 0.5) * 8; positions[i * 3 + 1] = (Math.random() - 0.5) * 8; positions[i * 3 + 2] = (Math.random() - 0.5) * 4; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); function createSnowTexture() { const c = document.createElement('canvas'); c.width = 64; c.height = 64; const ctx = c.getContext('2d'); const grad = ctx.createRadialGradient(32, 32, 0, 32, 32, 32); grad.addColorStop(0, 'rgba(255,255,255,1)'); grad.addColorStop(0.3, 'rgba(255,255,255,0.85)'); grad.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = grad; ctx.fillRect(0, 0, 64, 64); return new THREE.CanvasTexture(c); } const material = new THREE.PointsMaterial({ color: 0xffffff, size: 0.12, map: createSnowTexture(), transparent: true, depthWrite: false, alphaTest: 0.01, }); const points = new THREE.Points(geometry, material); scene.add(points); let last = performance.now(); function animate(now) { requestAnimationFrame(animate); const delta = Math.min((now - last) / 16.67, 3); last = now; const pos = geometry.attributes.position.array; for (let i = 0; i < COUNT; i++) { pos[i * 3 + 1] -= 0.008 * delta; pos[i * 3] += Math.sin(now * 0.001 + i) * 0.0008; if (pos[i * 3 + 1] < -4) pos[i * 3 + 1] = 4; } geometry.attributes.position.needsUpdate = true; points.rotation.y = Math.sin(now * 0.0001) * 0.03; renderer.render(scene, camera); } requestAnimationFrame(animate); window.addEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); }); </script> </body> </html>逻辑说明:粒子位置围绕原点分布,Y轴负方向为下,x轴加入sin做横向摆动;当粒子跌破-4就重置到+4,形成一个无限循环。整体旋转points.rotation.y在±0.03弧度之间摆动,这会给静止背景增加非常轻微的镜头感,但也意味着你需要在业务页上注意方向错觉,活动页有箭头类导航时建议关掉rotation。
参数说明:size是Three.js世界单位中的点大小,会受相机距离影响,0.12约在5.2相机距离下显示为3-4像素;COUNT 1400在多数中端机型可流畅,如果打开后掉帧,先调小到800而不是去调size。depthWrite: false避免雪花互相遮挡导致半透明排序问题,alphaTest: 0.01去掉边缘透明度较低的死像素。如果你用的是真3D场景,建议把sizeAttenuation保留默认true,否则距离远近不会改变粒子大小,3D感反而消失。
4.3 从“背景”到“交互”:鼠标与触摸影响雪场
在3D背景上增加交互,是让整个首屏不显得像静态壁纸的关键。常见做法是监听pointermove,把归一化坐标告诉摄像机,让镜头围绕原点小幅旋转,而不是动粒子自身:
let pointerX = 0, pointerY = 0; document.addEventListener('pointermove', (e) => { pointerX = (e.clientX / innerWidth) * 2 - 1; pointerY = (e.clientY / innerHeight) * 2 - 1; }); // animate函数里更新相机位置 const targetX = pointerX * 0.6; const targetY = -pointerY * 0.4; camera.position.x += (targetX - camera.position.x) * 0.03; camera.position.y += (targetY - camera.position.y) * 0.03; camera.lookAt(scene.position);这段逻辑说明:使用指数衰减让摄像机平滑朝目标移动,0.03是阻尼系数,系数越小跟手越柔和。pointermove在移动端触发的频次低于鼠标,但在微信浏览器里够用;要注意不要在touchend后继续让镜头产生偏移,否则用户点击完按钮会发现背景还在缓慢旋转,观感会有点晕。除了镜头交互,也可以在点击位置附近生成一圈加快下落的大雪花,这种额外粒子要单独维护一个临时数组,用setTimeout在1500ms后移除。
5. 参数验证与 zip 包上线:让雪花动画不在线上掉链子
把你写好的代码压缩成zip之前,下面这些事会直接影响线上效果。
5.1 目录结构与本地服务
常见的zip包结构一般是这样:
snow-background/ ├── index.html ├── assets/ │ ├── js/ │ │ ├── three.min.js │ │ └── main.js │ └── images/ │ └── snow.png ├── README.md建议开发时就把main.js拆开而不是全写死在index.html,因为活动页通常需要配合A/B测试或者按环境注入配置。压缩时要避免套一层外层同名目录,否则解压后路径容易变成双层,部署时还要再拖一层。本地验证不要双击打开index.html,因为file://协议下Canvas没问题,但Three.js版里纹理和外部脚本会被拦。先进项目目录,开一个静态服务:
cd snow-background python3 -m http.server 8080然后用手机和电脑分别访问http://localhost:8080。线上注意资源请求是否404、WebGL是否被禁用。Android微信里WebGL大概率可用,但部分WebView开发者选项会关闭硬件加速,需要做一次降级判断:WebGLRenderingContext不存在时就回退到第3章的Canvas 2D版。
5.2 性能调参与后台暂停
调参上最关键的三个开关是粒子数量、设备像素比和页面隐藏时的暂停。把renderer.setPixelRatio(Math.min(devicePixelRatio, 2))加上,否则iPhone的高倍渲染会直接把GPU塞满;页面隐藏时也应及时暂停动画,避免微信后台常驻掉电:
let running = true; document.addEventListener('visibilitychange', () => { if (document.hidden) { running = false; cancelAnimationFrame(rafId); } else { running = true; rafId = requestAnimationFrame(frame); } });逻辑说明:这个监听器在页面切到后台时取消下一帧,切回来重新发起requestAnimationFrame。如果你在Three.js版里使用,把回调里的frame换成animate即可。
5.3 上线前检查
上线前我用三个指标自检:第一,Chrome Rendering面板打开“Paint flashing”,确认每次刷新只重绘画布区域;第二,Performance面板记录5秒,拖动页面观察FPS是否出现断崖;第三,在微信开发者工具里模拟中低端机型,把count从180降到120再跑一次,记录首屏加载耗时。三个指标都通过,这套3D雪花背景参数就可以随业务一起发布。
本文还有配套的精品资源,点击获取