html5 canvas 绘制下雪效果
1.html5 canvas 绘制下雪效果
展示效果:

ai写代码为主,人工辅助修改:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas 下雪效果 - 透明背景</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; flex-direction: column; justify-content: center; align-items: center; min-height: 100vh; background: url(http://www.jnqianle.cn/upload/logo/content/202512/23/2303f83d-bb97-4153-8e0d-bc515722327d.jpg); font-family: 'Segoe UI', Arial, sans-serif; color: #e0e0ff; padding: 20px; overflow: hidden; } .container { display: flex; flex-direction: column; align-items: center; max-width: 1200px; width: 100%; gap: 30px; z-index: 10; } .header { text-align: center; padding: 25px 30px; background: rgba(15, 25, 60, 0.7); border-radius: 20px; width: 100%; max-width: 800px; border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px); } h1 { font-size: 2.8rem; margin-bottom: 15px; background: linear-gradient(90deg, #ffffff, #a8d0ff); -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: 0 2px 10px rgba(255, 255, 255, 0.2); } .subtitle { font-size: 1.2rem; opacity: 0.9; max-width: 700px; line-height: 1.6; margin: 0 auto; color: #b0c8ff; } .demo-area { display: flex; flex-wrap: wrap; gap: 30px; width: 100%; justify-content: center; } .canvas-container { position: relative; width: 800px; height: 500px; border-radius: 16px; overflow: hidden; /* box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4); border: 2px solid rgba(255, 255, 255, 0.15); */ } .canvas-wrapper { width: 100%; height: 100%; /* background: linear-gradient(rgba(30, 60, 114, 0.8), rgba(30, 60, 114, 0.8)), url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%231a1a2e"/><path d="M0,0 L100,100 M100,0 L0,100" stroke="%232a2a4a" stroke-width="1"/></svg>'); */ display: flex; justify-content: center; align-items: center; } canvas { display: block; width: 100%; height: 100%; } .controls { display: flex; flex-wrap: wrap; justify-content: center; gap: 25px; background: rgba(20, 30, 70, 0.8); padding: 25px; border-radius: 16px; width: 100%; max-width: 800px; border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(5px); } .control-group { display: flex; flex-direction: column; align-items: center; min-width: 150px; } .control-title { font-size: 1.1rem; margin-bottom: 12px; color: #a8c8ff; font-weight: 600; display: flex; align-items: center; gap: 8px; } .control-title i { color: #a8d0ff; } input[type="range"] { width: 180px; height: 8px; -webkit-appearance: none; background: rgba(30, 40, 80, 0.8); border-radius: 4px; outline: none; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; border-radius: 50%; background: #a8d0ff; cursor: pointer; box-shadow: 0 0 10px rgba(168, 208, 255, 0.8); border: 2px solid #ffffff; } .value-display { margin-top: 8px; font-size: 1.1rem; color: #a8d0ff; font-weight: bold; min-width: 60px; text-align: center; } .buttons { display: flex; justify-content: center; gap: 15px; width: 100%; margin-top: 10px; } button { padding: 14px 28px; background: linear-gradient(90deg, #4a7dff, #6ba8ff); border: none; border-radius: 10px; color: white; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s; box-shadow: 0 6px 20px rgba(74, 125, 255, 0.3); } button:hover { transform: translateY(-4px); box-shadow: 0 10px 25px rgba(74, 125, 255, 0.5); } button:active { transform: translateY(0); } .info { background: rgba(20, 30, 70, 0.8); border-radius: 16px; padding: 25px; margin-top: 10px; width: 100%; max-width: 800px; border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(5px); } .info-title { font-size: 1.3rem; color: #a8c8ff; margin-bottom: 15px; display: flex; align-items: center; gap: 10px; } .info-content { line-height: 1.7; color: #b0c8ff; } .transparent-demo { margin-top: 30px; padding: 20px; background: rgba(255, 255, 255, 0.1); border-radius: 12px; text-align: center; max-width: 800px; } .transparent-demo p { margin-bottom: 15px; color: #c8d8ff; } .footer { text-align: center; margin-top: 30px; color: #a8c8ff; font-size: 0.95rem; opacity: 0.8; max-width: 800px; } @media (max-width: 900px) { .canvas-container { width: 95%; height: 400px; } .controls { flex-direction: column; align-items: center; } h1 { font-size: 2.2rem; } } </style> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> </head> <body> <div class="container"> <div class="header"> <h1><i class="fas fa-snowflake"></i> Canvas 下雪效果</h1> <p class="subtitle">具有透明背景的缓慢下雪动画,雪花具有随机大小、速度和轻微摆动效果。Canvas背景完全透明,可叠加在任何背景上。</p> </div> <div class="demo-area"> <div class="canvas-container"> <div class="canvas-wrapper"> <canvas id="snowCanvas"></canvas> </div> </div> </div> <div class="controls"> <div class="control-group"> <div class="control-title"> <i class="fas fa-wind"></i> <span>雪花数量</span> </div> <input type="range" id="countSlider" min="50" max="500" step="10" value="200"> <div class="value-display" id="countValue">200</div> </div> <div class="control-group"> <div class="control-title"> <i class="fas fa-tachometer-alt"></i> <span>下落速度</span> </div> <input type="range" id="speedSlider" min="0.5" max="3" step="0.1" value="1.5"> <div class="value-display" id="speedValue">1.5</div> </div> <div class="control-group"> <div class="control-title"> <i class="fas fa-expand-alt"></i> <span>雪花大小</span> </div> <input type="range" id="sizeSlider" min="1" max="10" step="0.5" value="4"> <div class="value-display" id="sizeValue">4.0</div> </div> <div class="control-group"> <div class="control-title"> <i class="fas fa-random"></i> <span>摆动幅度</span> </div> <input type="range" id="swingSlider" min="0" max="5" step="0.2" value="1.5"> <div class="value-display" id="swingValue">1.5</div> </div> </div> <div class="buttons"> <button id="restartBtn"> <i class="fas fa-redo"></i> 重新开始 </button> <button id="pauseBtn"> <i class="fas fa-pause"></i> 暂停/继续 </button> <button id="windBtn"> <i class="fas fa-wind"></i> 风向切换 </button> </div> <div class="transparent-demo"> <p><strong>透明背景演示:</strong> Canvas背景是完全透明的,这里看到的背景是为了演示而添加的。在实际使用中,下雪效果可以叠加在任何背景上。</p> <button id="toggleBgBtn"> <i class="fas fa-eye"></i> 切换演示背景 </button> </div> <div class="info"> <div class="info-title"> <i class="fas fa-info-circle"></i> <span>下雪效果说明</span> </div> <div class="info-content"> <p>此Canvas动画模拟真实的下雪效果,具有以下特性:</p> <ul style="padding-left: 20px; margin-top: 10px;"> <li><strong>透明背景:</strong>Canvas背景完全透明,可以叠加在任何页面上。</li> <li><strong>随机参数:</strong>每个雪花的大小、下落速度和摆动幅度都是随机的。</li> <li><strong>自然运动:</strong>雪花不仅有垂直下落,还有轻微的左右摆动,模拟真实雪花飘落。</li> <li><strong>循环效果:</strong>雪花落到底部后会重置到顶部,形成连续下雪效果。</li> <li><strong>交互控制:</strong>可以实时调整雪花数量、速度、大小和摆动幅度。</li> </ul> </div> </div> <div class="footer"> <p>使用HTML5 Canvas实现 | 透明背景下雪效果 | 可自定义参数</p> </div> </div> <script> // 获取Canvas元素和上下文 const canvas = document.getElementById('snowCanvas'); const ctx = canvas.getContext('2d'); // 设置Canvas尺寸 function resizeCanvas() { canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; } // 初始设置 resizeCanvas(); window.addEventListener('resize', resizeCanvas); // 雪花数组 let snowflakes = []; // 动画参数 let animationParams = { snowflakeCount: 200, speed: 1.5, size: 4.0, swing: 1.5, isRunning: true, windDirection: 1, // 1表示轻微向右,-1表示轻微向左 windStrength: 0.5, backgroundColor: true // 演示背景开关 }; // 获取DOM元素 const countSlider = document.getElementById('countSlider'); const speedSlider = document.getElementById('speedSlider'); const sizeSlider = document.getElementById('sizeSlider'); const swingSlider = document.getElementById('swingSlider'); const restartBtn = document.getElementById('restartBtn'); const pauseBtn = document.getElementById('pauseBtn'); const windBtn = document.getElementById('windBtn'); const toggleBgBtn = document.getElementById('toggleBgBtn'); const countValue = document.getElementById('countValue'); const speedValue = document.getElementById('speedValue'); const sizeValue = document.getElementById('sizeValue'); const swingValue = document.getElementById('swingValue'); // 更新显示值 countSlider.addEventListener('input', function() { countValue.textContent = this.value; animationParams.snowflakeCount = parseInt(this.value); initSnowflakes(); }); speedSlider.addEventListener('input', function() { speedValue.textContent = this.value; animationParams.speed = parseFloat(this.value); }); sizeSlider.addEventListener('input', function() { sizeValue.textContent = this.value; animationParams.size = parseFloat(this.value); }); swingSlider.addEventListener('input', function() { swingValue.textContent = this.value; animationParams.swing = parseFloat(this.value); }); // 按钮事件 restartBtn.addEventListener('click', function() { initSnowflakes(); }); pauseBtn.addEventListener('click', function() { animationParams.isRunning = !animationParams.isRunning; if (animationParams.isRunning) { pauseBtn.innerHTML = '<i class="fas fa-pause"></i> 暂停动画'; } else { pauseBtn.innerHTML = '<i class="fas fa-play"></i> 继续动画'; } }); windBtn.addEventListener('click', function() { animationParams.windDirection *= -1; if (animationParams.windDirection === 1) { windBtn.innerHTML = '<i class="fas fa-wind"></i> 风向切换 (右)'; } else { windBtn.innerHTML = '<i class="fas fa-wind"></i> 风向切换 (左)'; } }); toggleBgBtn.addEventListener('click', function() { animationParams.backgroundColor = !animationParams.backgroundColor; const canvasWrapper = document.querySelector('.canvas-wrapper'); if (animationParams.backgroundColor) { canvasWrapper.style.background = 'linear-gradient(rgba(30, 60, 114, 0.8), rgba(30, 60, 114, 0.8)), ' + 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%231a1a2e"/><path d="M0,0 L100,100 M100,0 L0,100" stroke="%232a2a4a" stroke-width="1"/></svg>\')'; toggleBgBtn.innerHTML = '<i class="fas fa-eye"></i> 切换演示背景 (开)'; } else { canvasWrapper.style.background = 'transparent'; toggleBgBtn.innerHTML = '<i class="fas fa-eye"></i> 切换演示背景 (关)'; } }); // 雪花类 class Snowflake { constructor() { this.reset(); } reset() { // 随机位置 this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; // 随机大小 (基于基础大小) this.radius = animationParams.size * (0.5 + Math.random() * 0.5); // 随机速度 (基于基础速度) this.speed = animationParams.speed * (0.5 + Math.random() * 0.5); // 随机摆动幅度 this.swingAmplitude = animationParams.swing * (0.5 + Math.random() * 0.5); // 随机摆动速度 this.swingSpeed = 0.01 + Math.random() * 0.02; // 摆动相位 this.swingPhase = Math.random() * Math.PI * 2; // 随机透明度 this.alpha = 0.5 + Math.random() * 0.5; // 随机形状变化 this.shapeVariant = Math.floor(Math.random() * 3); } update() { // 垂直下落 this.y += this.speed; // 水平摆动 (正弦运动) this.swingPhase += this.swingSpeed; this.x += Math.sin(this.swingPhase) * this.swingAmplitude * 0.5; // 风向影响 this.x += animationParams.windDirection * animationParams.windStrength; // 如果雪花超出边界,重置到顶部 if (this.y > canvas.height + this.radius) { this.reset(); this.y = -this.radius; } // 水平边界处理 if (this.x > canvas.width + this.radius) { this.x = -this.radius; } else if (this.x < -this.radius) { this.x = canvas.width + this.radius; } } draw() { ctx.save(); ctx.globalAlpha = this.alpha; ctx.fillStyle = 'white'; // 绘制不同形状的雪花 if (this.shapeVariant === 0) { // 圆形雪花 ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); } else if (this.shapeVariant === 1) { // 六角形雪花 ctx.beginPath(); for (let i = 0; i < 6; i++) { const angle = (i * Math.PI) / 3; const px = this.x + Math.cos(angle) * this.radius; const py = this.y + Math.sin(angle) * this.radius; if (i === 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); } } ctx.closePath(); ctx.fill(); } else { // 星形雪花 ctx.beginPath(); for (let i = 0; i < 5; i++) { const angle = (i * 2 * Math.PI) / 5 - Math.PI / 2; const px = this.x + Math.cos(angle) * this.radius; const py = this.y + Math.sin(angle) * this.radius; if (i === 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); } // 内角 const innerAngle = angle + Math.PI / 5; const innerRadius = this.radius * 0.5; const innerPx = this.x + Math.cos(innerAngle) * innerRadius; const innerPy = this.y + Math.sin(innerAngle) * innerRadius; ctx.lineTo(innerPx, innerPy); } ctx.closePath(); ctx.fill(); } ctx.restore(); } } // 初始化雪花 function initSnowflakes() { snowflakes = []; for (let i = 0; i < animationParams.snowflakeCount; i++) { snowflakes.push(new Snowflake()); } } // 绘制下雪效果 function drawSnow() { // 清空Canvas - 使用透明色清除以实现透明背景 ctx.clearRect(0, 0, canvas.width, canvas.height); // 更新和绘制每个雪花 if (animationParams.isRunning) { for (let i = 0; i < snowflakes.length; i++) { snowflakes[i].update(); snowflakes[i].draw(); } } else { // 如果暂停,只绘制不更新 for (let i = 0; i < snowflakes.length; i++) { snowflakes[i].draw(); } } // 请求下一帧动画 requestAnimationFrame(drawSnow); } // 初始化并开始动画 initSnowflakes(); drawSnow(); // 添加键盘控制 document.addEventListener('keydown', function(e) { switch(e.key) { case ' ': // 空格键暂停/继续 animationParams.isRunning = !animationParams.isRunning; if (animationParams.isRunning) { pauseBtn.innerHTML = '<i class="fas fa-pause"></i> 暂停动画'; } else { pauseBtn.innerHTML = '<i class="fas fa-play"></i> 继续动画'; } break; case 'r': case 'R': // R键重新开始 initSnowflakes(); break; case 'w': case 'W': // W键切换风向 animationParams.windDirection *= -1; if (animationParams.windDirection === 1) { windBtn.innerHTML = '<i class="fas fa-wind"></i> 风向切换 (右)'; } else { windBtn.innerHTML = '<i class="fas fa-wind"></i> 风向切换 (左)'; } break; } }); </script> </body> </html>
更多:
浙公网安备 33010602011771号