【Canvas实现3D雪花飞舞与积雪特效】_canvas 结冰
Canvas实现3D雪花飞舞与积雪特效
冬天来了,想为您的网站或个人项目增添一份浪漫的冬日氛围吗?今天,我们将一起探索如何利用 HTML5 Canvas 绘制出逼真的 3D 雪花飞舞效果,并且更酷的是,这些雪花还会真实地在屏幕底部累积成一片片积雪!
这个特效不仅模拟了雪花的自然下落和风力影响,还通过简单的物理逻辑实现了积雪的动态增长,让您的网页瞬间拥有沉浸式的冬日体验。
特效亮点
- 3D 视差效果: 通过
z
轴深度模拟,实现雪花近大远小、近快远慢的视觉效果。 - 逼真雪花形状: 不再是简单的圆点,而是模拟了雪花晶体的六瓣形状,并带有随机旋转。
- 动态风力模拟: 雪花会受到风力影响,并带有轻微的波动,使其飘落路径更自然。
- 真实积雪效果: 雪花落地后会在屏幕底部逐渐累积,形成高度不一的积雪,且积雪量可以动态调整。
- 与背景完美融合: Canvas 设为透明,可搭配任何背景图片,轻松融入您的设计。
效果展示
核心代码解析
我们将主要聚焦于 JavaScript 部分,这是实现所有动态效果的关键。
1. 初始化与雪花创建 (init
& createSnowflake
)
init
函数负责设置 Canvas 的尺寸,并初始化雪花数组和积雪数组。每次窗口大小改变时,都会重置这些数据,确保效果始终适应屏幕。
createSnowflake
函数定义了每个雪花的初始属性:
function createSnowflake() { const x = Math.random() * width; const y = Math.random() * height; // 初始Y可以从顶部开始 const z = Math.random() * 10 + 1; // 深度, 用于模拟3D效果,避免z为0 const size = Math.random() * 3 + 1; // 雪花大小 const speed = Math.random() * 1 + 0.5; // 下落速度 const opacity = Math.random() * 0.7 + 0.3; // 透明度 return { x: x, y: y, z: z, // z值越大,表示雪花越“远” size: size, speed: speed, // z值越大,下落速度相对越慢(除以z) opacity: opacity, };}
这里 z
属性是实现 3D 效果的关键。我们通过调整雪花的 size
和 speed
与 z
值挂钩,模拟“近大远快,远小慢”的透视感。
2. 绘制雪花 (drawSnowflake
)
drawSnowflake
函数负责将单个雪花绘制到 Canvas 上。为了让雪花看起来更真实,我们不再简单地绘制一个圆点,而是绘制了一个简单的六瓣星型,并加入了随机旋转:
function drawSnowflake(snowflake) { ctx.beginPath(); const flakeSize = snowflake.size * (1 + snowflake.z * 0.05); // 根据Z值调整大小 const flakeX = snowflake.x; const flakeY = snowflake.y; ctx.save(); // 保存当前绘图状态 ctx.translate(flakeX, flakeY); // 将坐标原点移动到雪花位置 ctx.rotate(Math.random() * Math.PI * 2); // 随机旋转 // 绘制六瓣雪花形状的核心逻辑 // ... (代码略,详见完整代码) ctx.closePath(); ctx.restore(); // 恢复之前保存的绘图状态 ctx.fillStyle = `rgba(255, 255, 255, ${snowflake.opacity})`; ctx.fill();}
ctx.save()
和 ctx.restore()
配合 ctx.translate()
和 ctx.rotate()
是 Canvas 绘图中的常用技巧,可以独立地对每个雪花进行定位和旋转,而不会影响其他雪花或整体画布。
3. 更新雪花状态与积雪逻辑 (updateSnowflake
)
这是整个动画的核心,它处理了雪花的移动、与地面的碰撞、积雪的累积以及雪花的重置。
function updateSnowflake(snowflake) { // 1. 雪花移动:y轴下落,x轴受风力影响 snowflake.y += snowflake.speed / snowflake.z; // z越大,下落速度越慢,模拟透视 snowflake.x += windForce * snowflake.z + Math.sin(snowflake.y * 0.01 + snowflake.x * 0.005) * 0.5; // 添加正弦波动的风力 // 2. 获取当前雪花位置对应的积雪高度 const pileIndex = Math.min(Math.floor(snowflake.x / snowResolution), snowPiles.length - 1); const groundLevel = height - (snowPiles[pileIndex] || 0); // 计算当前地面高度 // 3. 碰撞检测与积雪累积 if (snowflake.y > groundLevel) { const influenceRadius = 5; // 影响周围积雪的范围 for (let i = -influenceRadius; i <= influenceRadius; i++) { const currentPileIndex = pileIndex + i; if (currentPileIndex >= 0 && currentPileIndex < snowPiles.length) { const falloff = 1 - (Math.abs(i) / influenceRadius); // 越靠近中心影响越大 snowPiles[currentPileIndex] = Math.min(maxSnowHeight, snowPiles[currentPileIndex] + snowAccumulationRate * falloff); } } // 4. 重置雪花到顶部,继续飘落 snowflake.y = -snowflake.size; snowflake.x = Math.random() * width; snowflake.z = Math.random() * 10 + 1; snowflake.speed = Math.random() * 1 + 0.5; snowflake.opacity = Math.random() * 0.7 + 0.3; snowflake.size = Math.random() * 3 + 1; } // 5. 左右边界循环:雪花从一侧飞出,从另一侧进入 if (snowflake.x < -snowflake.size) { snowflake.x = width + snowflake.size; } if (snowflake.x > width + snowflake.size) { snowflake.x = -snowflake.size; }}
- 积雪逻辑详解:
snowPiles
数组存储了屏幕底部每一小段(由snowResolution
定义宽度)的积雪高度。当雪花snowflake.y
超过当前位置的groundLevel
(即碰撞到地面或现有积雪)时:- 我们不仅在雪花落点增加积雪,还会根据
influenceRadius
和falloff
让其周围的积雪也略微增加,模拟雪堆的扩散和自然坡度。 snowAccumulationRate
控制每次落地增加的量,maxSnowHeight
限制积雪的最高高度。
- 我们不仅在雪花落点增加积雪,还会根据
4. 绘制积雪 (drawSnowPiles
)
积雪并非由无数小方块组成,而是通过 Canvas
的路径绘制功能,将所有积雪高度点连接起来,然后填充成一个实心区域:
function drawSnowPiles() { ctx.fillStyle = \'white\'; // 积雪填充色 ctx.beginPath(); ctx.moveTo(0, height); // 从左下角开始 // 遍历积雪数组,连接每个积雪柱的顶部 for (let i = 0; i < snowPiles.length; i++) { const x = i * snowResolution; const y = Math.max(0, height - snowPiles[i]); // Y坐标是画布高度减去积雪高度 ctx.lineTo(x, y); } ctx.lineTo(width, height); // 连接到右下角 ctx.closePath(); // 闭合路径 ctx.fill(); // 填充整个路径}
这种绘制方式非常高效,即使积雪高度不断变化,也能平滑地渲染出来。
5. 动画循环 (draw
)
draw
函数是动画的主循环。它在每一帧中:
function draw() { ctx.clearRect(0, 0, width, height); // 清空画布 drawSnowPiles(); // 先绘制积雪,确保雪花落在积雪上 for (let i = 0; i < snowflakes.length; i++) { const snowflake = snowflakes[i]; updateSnowflake(snowflake); // 更新雪花位置和状态 drawSnowflake(snowflake); // 绘制雪花 } requestAnimationFrame(draw); // 请求浏览器在下一次重绘时调用 draw 函数,形成循环}
requestAnimationFrame
是进行网页动画的最佳实践,它会根据浏览器的刷新频率来执行动画,确保动画流畅且省电。
完整代码
将以下代码保存为一个 .html
文件(例如 snow.html
),然后用浏览器打开即可运行。
<!DOCTYPE html><html><head> <meta charset=\"UTF-8\"> <title>3D 雪花飞舞特效与积雪</title> <style>body {margin: 0;overflow: hidden;/* 背景图:一个夜晚雪景或森林图片会非常棒。 这里提供一个示例URL,建议替换成您自己的图片或直接下载到本地。 图片来源:Unsplash等免费图库,搜索“snowy night forest” */background-image: url(\'https://images.unsplash.com/photo-1549487926-0e1b6f9a0c7c?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\');background-size: cover; /* 覆盖整个区域 */background-position: center; /* 居中显示 */background-repeat: no-repeat; /* 不重复 */background-color: #1a2a3a; /* 背景图加载失败时的备用颜色 */}canvas {display: block;/* 确保 canvas 是透明的,以便看到背景图 */background-color: transparent;} </style></head><body><canvas id=\"snowCanvas\"></canvas><script>const canvas = document.getElementById(\'snowCanvas\');const ctx = canvas.getContext(\'2d\');let width, height;let snowflakes = [];const numSnowflakes = 350; // **调整:雪花数量增加,让积雪更快显现**const windForce = 0.08; // 风力强度, 影响雪花水平移动const maxSnowHeight = 200; // **调整:积雪最高高度增加,允许更多积雪**const snowAccumulationRate = 0.15; // **调整:每次雪花落地的积雪增加量增加,让积雪更明显**const snowResolution = 2; // 积雪颗粒的宽度(像素),值越小积雪越细腻但性能开销越大let snowPiles = []; // 存储每列的积雪高度function init() {width = window.innerWidth;height = window.innerHeight;canvas.width = width;canvas.height = height;snowflakes = [];for (let i = 0; i < numSnowflakes; i++) {snowflakes.push(createSnowflake());}// 初始化积雪数组,每次窗口大小改变时重置积雪snowPiles = new Array(Math.ceil(width / snowResolution)).fill(0);}function createSnowflake() {const x = Math.random() * width;const y = Math.random() * height; // 初始Y可以从顶部开始const z = Math.random() * 10 + 1; //深度, 用于模拟3D效果,避免z为0const size = Math.random() * 3 + 1; //雪花大小const speed = Math.random() * 1 + 0.5; //下落速度const opacity = Math.random() * 0.7 + 0.3; //透明度return {x: x,y: y,z: z,size: size,speed: speed,opacity: opacity,};}function drawSnowflake(snowflake) {ctx.beginPath();// 模拟雪花形状,不再是简单的圆点,更像真实的雪花晶体// flakeSize 根据Z值调整,模拟近大远小const flakeSize = snowflake.size * (1 + snowflake.z * 0.05);const flakeX = snowflake.x;const flakeY = snowflake.y;// 使用线段和旋转绘制一个简单的星型雪花ctx.save(); //保存当前绘图状态ctx.translate(flakeX, flakeY); // 将坐标原点移动到雪花位置ctx.rotate(Math.random() * Math.PI * 2); // 随机旋转画布,增加雪花的随机性// 绘制一个简单的六瓣雪花形状const numPoints = 6;const innerRadius = flakeSize * 0.3;const outerRadius = flakeSize;for (let i = 0; i < numPoints * 2; i++) {const radius = (i % 2 === 0) ? outerRadius : innerRadius;const angle = (Math.PI / numPoints) * i;const currentX = Math.cos(angle) * radius;const currentY = Math.sin(angle) * radius;if (i === 0) {ctx.moveTo(currentX, currentY);} else {ctx.lineTo(currentX, currentY);}}ctx.closePath(); // 闭合路径ctx.restore(); // 恢复之前保存的绘图状态ctx.fillStyle = `rgba(255, 255, 255, ${snowflake.opacity})`; // 白色雪花,带透明度ctx.fill();}function updateSnowflake(snowflake) {// z越大,下落速度越慢,模拟透视snowflake.y += snowflake.speed / snowflake.z;// z越大,受风力影响越大snowflake.x += windForce * snowflake.z + Math.sin(snowflake.y * 0.01 + snowflake.x * 0.005) * 0.5; // 添加一些正弦波动的风力,更自然// 获取当前雪花位置对应的积雪高度// 确保索引在有效范围内,防止X坐标超出画布时索引计算错误const pileIndex = Math.min(Math.floor(snowflake.x / snowResolution), snowPiles.length - 1);const groundLevel = height - (snowPiles[pileIndex] || 0); // 如果pileIndex越界,默认为0// 如果雪花超出屏幕底部或撞到积雪,重置雪花位置并增加积雪if (snowflake.y > groundLevel) {// 增加积雪const influenceRadius = 5; // 影响周围积雪的范围 (以snowResolution为单位)for (let i = -influenceRadius; i <= influenceRadius; i++) {const currentPileIndex = pileIndex + i;if (currentPileIndex >= 0 && currentPileIndex < snowPiles.length) {// 越靠近中心影响越大,边缘递减const falloff = 1 - (Math.abs(i) / influenceRadius);snowPiles[currentPileIndex] = Math.min(maxSnowHeight, snowPiles[currentPileIndex] + snowAccumulationRate * falloff);}}// 重置雪花到顶部,以便继续飘落snowflake.y = -snowflake.size; // 从屏幕上方开始snowflake.x = Math.random() * width; // 随机X位置snowflake.z = Math.random() * 10 + 1; // 重新随机深度snowflake.speed = Math.random() * 1 + 0.5; // 重新随机速度snowflake.opacity = Math.random() * 0.7 + 0.3; // 重新随机透明度snowflake.size = Math.random() * 3 + 1; // 重新随机大小}// 如果雪花超出屏幕左右边界,重置雪花位置(模拟循环,让雪花从另一侧出现)if (snowflake.x < -snowflake.size) { // 考虑到雪花大小snowflake.x = width + snowflake.size;}if (snowflake.x > width + snowflake.size) { // 考虑到雪花大小snowflake.x = -snowflake.size;}}function drawSnowPiles() {// **调整:将积雪填充色改为完全不透明的白色,使其更显眼**ctx.fillStyle = \'white\';ctx.beginPath();ctx.moveTo(0, height); // 左下角// 绘制积雪的上边缘for (let i = 0; i < snowPiles.length; i++) {const x = i * snowResolution;// 确保Y坐标不会低于0(积雪不会超出屏幕顶部)const y = Math.max(0, height - snowPiles[i]);ctx.lineTo(x, y);}ctx.lineTo(width, height); // 右下角ctx.closePath(); // 闭合路径ctx.fill();}function draw() {ctx.clearRect(0, 0, width, height); // 清空画布// 先绘制积雪,这样雪花会落在积雪上面drawSnowPiles();// 然后绘制雪花for (let i = 0; i < snowflakes.length; i++) {const snowflake = snowflakes[i];updateSnowflake(snowflake);drawSnowflake(snowflake);}requestAnimationFrame(draw); // 请求下一次动画帧}// 初始化和启动动画init();draw();// 监听窗口大小改变,重新初始化window.addEventListener(\'resize\', init);</script></body></html>
温馨提示: body
样式中的 background-image
URL 是一个示例,您可以将其替换为您自己的雪景、森林或其他任何您喜欢的背景图片。如果网络不佳或图片加载失败,background-color
会作为备用背景色。
优化与扩展
- 性能优化: 如果雪花数量很多导致性能下降,可以尝试减少
numSnowflakes
,或者在绘制时进行性能分析,只绘制屏幕内的雪花。 - 更多雪花形状: 可以使用更多的 Canvas 绘图 API(如二次贝塞尔曲线、三次贝塞尔曲线)来绘制更复杂的雪花晶体图案。
- 交互式风力: 增加鼠标交互,例如当鼠标移动时改变
windForce
的大小和方向,让用户可以“吹动”雪花。 - 积雪纹理: 而不是纯白色,可以考虑给积雪添加一些噪声或纹理,使其看起来更像真实的雪堆。
- 动态天气: 引入天气变量,比如下雪密度(
numSnowflakes
)、风力大小(windForce
)等,让雪景更加多变。
总结
通过 HTML5 Canvas 和 JavaScript,我们不仅能创造出赏心悦目的视觉效果,还能模拟一些有趣的物理现象,如雪花的飘落和积雪的累积。这个项目提供了一个很好的起点,您可以根据自己的创意和需求,进一步拓展和优化,打造出独一无二的冬日网页体验!
希望这篇博客文章对您有所帮助!快去动手尝试,让您的网页也下起一场浪漫的冬雪吧!