用 Canvas + CSS 打造“会呼吸”的天气卡片(附源码可复制)

前言

在前端视觉效果不断进化的今天,一个简单的天气卡片,早已不只是展示温度这么简单。本项目通过 Canvas 粒子系统 + CSS 毛玻璃 + 物理交互,实现了一个"有生命"的天气 UI------它会响应你的鼠标、会下雨下雪、甚至会结霜。

这篇文章带你完整拆解这个效果的实现思路。

一、整体设计思路

这个天气卡片的核心目标不是"功能",而是沉浸感,主要通过 4 个维度实现:

  1. 视觉层次
    • 背景渐变(晴 / 雨 / 雪)
    • 毛玻璃卡片(Glassmorphism)
    • 噪点纹理(提升真实感)
  2. 动态效果
    • 粒子系统(雨滴 / 雪花)
    • 雷电闪烁
    • 水波纹(雨滴打在卡片上)
  3. 交互反馈
    • 鼠标驱动 3D 旋转
    • 粒子"躲避鼠标"
    • 视差(parallax)
  4. 环境模拟
    • 雪天结霜
    • 雨天模糊增强(湿度感)
    • 阳光光斑

二、核心模块拆解

1. 毛玻璃卡片(Glassmorphism)

复制代码
.glass-card {
    background: rgba(255, 255, 255, 0.15);
    backdrop-filter: blur(16px);
    border: 1px solid rgba(255, 255, 255, 0.4);
}

📌 关键点:

  • backdrop-filter 是核心(注意 Safari 需要 -webkit- 前缀)
  • 半透明 + 边框 + 阴影 = 真实玻璃感

👉 坑点提醒:

  • 在部分 Windows 浏览器上性能较差
  • 不支持时会"失效"

2. Canvas 粒子系统(雨 / 雪)

核心类:

复制代码
class Particle {
    constructor(type) {
        this.type = type;
        this.reset();
    }

    reset() {
        this.x = Math.random() * w;
        this.y = Math.random() * -h;
    }

    update() {
        this.x += this.vx;
        this.y += this.vy;
    }

    draw() {
        // 根据类型绘制
    }
}

雨 vs 雪的区别

属性
速度
形状 线条 圆点
风影响 明显 柔和

3. 鼠标交互(粒子躲避)

这是本项目最"高级"的点之一👇

复制代码
const dx = this.x - mouseX;
const dy = this.y - mouseY;
const dist = Math.sqrt(dx*dx + dy*dy);

if (dist < radius) {
    const force = (radius - dist) / radius;
    this.x += Math.cos(angle) * force * 15;
    this.y += Math.sin(angle) * force * 15;
}

💡 本质:简单的力场模拟

👉 可以优化:

  • 用平方距离代替 sqrt(性能更好)
  • 引入 easing,让运动更丝滑

4. 3D 视差 + 卡片倾斜

复制代码
const rotateX = -((y - centerY) / centerY) * 10;
const rotateY = ((x - centerX) / centerX) * 10;

weatherCard.style.transform = `
  perspective(1000px)
  rotateX(${rotateX}deg)
  rotateY(${rotateY}deg)
`;

📌 核心点:

  • 鼠标位置 → 映射为旋转角度
  • perspective 才有立体感

5. 结霜效果(雪天专属)

复制代码
.frost-overlay {
    background: radial-gradient(
        circle at center,
        transparent 60%,
        rgba(255,255,255,0.4) 100%
    );
    mix-blend-mode: overlay;
}

💡 思路:

  • 用渐变模拟边缘霜
  • mix-blend-mode 融合背景

👉 更高级做法:

  • 使用噪声贴图(noise texture)
  • 或 WebGL shader(更真实)

6. 雷电闪烁

复制代码
function triggerLightning() {
    lightningLayer.style.background = 'rgba(255,255,255,0.8)';
    setTimeout(() => {
        lightningLayer.style.background = 'transparent';
    }, 80);
}

📌 技巧:

  • 快速闪 + 随机二次闪
  • 不用 Canvas,直接 DOM 更轻量

7. 水波纹(雨滴命中卡片)

复制代码
class Ripple {
    update() {
        this.radius += 1;
        this.opacity -= 0.03;
    }
}

💡 本质:

  • 半透明椭圆不断扩大 + 消失
  • 模拟水滴扩散

三、状态切换设计

复制代码
function setWeather(type) {
    if (type === 'rain') {
        initParticles('rain', 400);
    } else if (type === 'snow') {
        initParticles('snow', 200);
    }
}

📌 思路:

  • UI + 动效 + 数据统一切换
  • 每种天气都是一个"状态机"

完整代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>天气卡片</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap');

        body {
            font-family: 'Inter', sans-serif;
            margin: 0;
            padding: 0;
            overflow: hidden;
            transition: background 1s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            background-color: #1a1a1a;
        }

        /* 背景容器 */
        .bg-container {
            position: absolute;
            top: -10%;
            left: -10%;
            width: 120%;
            height: 120%;
            z-index: -2;
            transition: background 1s ease, transform 0.1s ease-out;
        }

        .bg-sunny { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
        .bg-rain { background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); }
        .bg-snow { background: linear-gradient(135deg, #83a4d4 0%, #b6fbff 100%); }

        /* 闪电层 */
        #lightningLayer {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(255, 255, 255, 0);
            z-index: -1; pointer-events: none;
            transition: background 0.1s;
        }

        /* 毛玻璃卡片 */
        .glass-card {
            position: relative;
            background: rgba(255, 255, 255, 0.15);
            backdrop-filter: blur(16px);
            -webkit-backdrop-filter: blur(16px);
            border: 1px solid rgba(255, 255, 255, 0.4);
            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
            transform-style: preserve-3d;
            will-change: transform;
            transition: backdrop-filter 1s ease; /* 模糊度过渡 */
        }

        /* 结霜特效层 (仅在下雪时显示) */
        .frost-overlay {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            pointer-events: none;
            z-index: 1;
            opacity: 0;
            transition: opacity 2s ease;
            /* 使用径向渐变模拟边缘结霜 */
            background: radial-gradient(circle at center, transparent 60%, rgba(255, 255, 255, 0.4) 100%);
            mix-blend-mode: overlay;
            border-radius: 1.5rem; /* 匹配卡片圆角 */
        }
        .frost-overlay.active { opacity: 1; }

        /* 噪点纹理 */
        .glass-noise {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            opacity: 0.06; z-index: 0; pointer-events: none;
            background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
        }

        /* 阳光光斑 */
        .sun-ray-effect {
            position: absolute;
            top: 10%; left: 10%; width: 80%; height: 80%;
            background: radial-gradient(circle at 70% 30%, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 60%);
            border-radius: 50%;
            opacity: 0; pointer-events: none;
            transition: opacity 1s ease;
            mix-blend-mode: overlay; filter: blur(20px);
        }
        .sun-ray-effect.active { opacity: 1; }

        /* 视差元素 */
        .sun {
            position: absolute; top: -60px; right: -60px; width: 150px; height: 150px;
            background: linear-gradient(to bottom right, #ffeb3b, #fbc02d);
            border-radius: 50%;
            box-shadow: 0 0 40px rgba(255, 235, 59, 0.6), 0 0 80px rgba(251, 192, 45, 0.4);
            z-index: 1; opacity: 0; transition: opacity 1s ease;
        }
        .sun.active { opacity: 1; }

        .cloud {
            position: absolute;
            background: linear-gradient(to bottom, rgba(255,255,255,0.9), rgba(255,255,255,0.7));
            border-radius: 50px; z-index: 2; opacity: 0;
            transition: opacity 1s ease;
            filter: drop-shadow(0 5px 15px rgba(0,0,0,0.1));
        }
        .cloud.active { opacity: 0.9; }
        .cloud::after, .cloud::before { content: ''; position: absolute; background: inherit; border-radius: 50%; }
        .cloud-1 { width: 120px; height: 45px; top: 30px; left: -20px; }
        .cloud-1::after { width: 50px; height: 50px; top: -25px; left: 20px; }
        .cloud-1::before { width: 40px; height: 40px; top: -15px; left: 60px; }
        .cloud-2 { width: 90px; height: 35px; top: 120px; left: 280px; }
        .cloud-2::after { width: 35px; height: 35px; top: -18px; left: 15px; }

        /* Canvas */
        #weatherCanvas, #cardEffectCanvas { position: absolute; top: 0; left: 0; pointer-events: none; }
        #weatherCanvas { width: 100%; height: 100%; z-index: 0; }
        #cardEffectCanvas { z-index: 4; }

        /* UI */
        .toggle-btn { transition: all 0.3s; position: relative; overflow: hidden; }
        .toggle-btn::after {
            content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(255,255,255,0.2); transform: translateX(-100%); transition: transform 0.3s;
        }
        .toggle-btn:hover::after { transform: translateX(0); }
        .toggle-btn.active { background: rgba(255,255,255,0.4); color: #000; font-weight: 700; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
        .text-shadow { text-shadow: 0 2px 8px rgba(0,0,0,0.15); }
        .card-content { transform: translateZ(20px); }
    </style>
</head>
<body class="relative">

    <div id="bgContainer" class="bg-container bg-sunny"></div>
    <div id="lightningLayer"></div>
    <canvas id="weatherCanvas"></canvas>

    <div id="cardWrapper" class="perspective-container">
        <div id="weatherCard" class="glass-card w-80 sm:w-96 rounded-3xl p-8 text-white overflow-hidden transform transition-transform duration-100 ease-out">
            
            <div class="glass-noise"></div>
            <div id="frostOverlay" class="frost-overlay"></div> <!-- 结霜层 -->
            
            <canvas id="cardEffectCanvas"></canvas>

            <div id="sun" class="sun active" data-parallax="0.05"></div>
            <div id="sunRayEffect" class="sun-ray-effect active"></div>
            <div id="cloud1" class="cloud cloud-1 active" data-parallax="0.08"></div>
            <div id="cloud2" class="cloud cloud-2 active" data-parallax="0.04"></div>

            <div class="card-content relative z-10">
                <div class="flex justify-between items-start mb-8">
                    <div>
                        <h2 class="text-lg font-bold tracking-wide flex items-center gap-2">
                            <i class="fas fa-location-arrow text-sm"></i> 上海
                        </h2>
                        <p id="dateText" class="text-xs text-white/80 mt-1 font-medium opacity-80">周二, 11月 19日</p>
                    </div>
                    <div class="text-right">
                        <i id="weatherIcon" class="fas fa-sun text-4xl text-yellow-300 drop-shadow-lg filter"></i>
                    </div>
                </div>

                <div class="flex flex-col items-center justify-center mb-10">
                    <div class="flex items-start relative">
                        <span id="tempValue" class="text-8xl font-thin tracking-tighter text-shadow">26</span>
                        <span class="text-4xl mt-4 font-light opacity-80">°</span>
                    </div>
                    <p id="weatherDesc" class="text-xl font-medium tracking-wider mt-2 opacity-90">晴朗舒适</p>
                </div>

                <div class="grid grid-cols-3 gap-4 border-t border-white/20 pt-6 mb-6">
                    <div class="flex flex-col items-center group cursor-default">
                        <i class="fas fa-wind mb-2 opacity-60 group-hover:scale-110 transition-transform"></i>
                        <span id="windValue" class="font-semibold text-sm">3 km/h</span>
                    </div>
                    <div class="flex flex-col items-center border-l border-r border-white/10 group cursor-default">
                        <i class="fas fa-droplet mb-2 opacity-60 group-hover:scale-110 transition-transform"></i>
                        <span id="humidValue" class="font-semibold text-sm">45%</span>
                    </div>
                    <div class="flex flex-col items-center group cursor-default">
                        <i class="fas fa-temperature-high mb-2 opacity-60 group-hover:scale-110 transition-transform"></i>
                        <span id="feelsLikeValue" class="font-semibold text-sm">28°</span>
                    </div>
                </div>

                <div class="flex justify-between bg-black/20 rounded-2xl p-1.5 backdrop-blur-md shadow-inner">
                    <button data-weather="sunny" class="toggle-btn active flex-1 py-2.5 rounded-xl text-xs text-white">
                        <i class="fas fa-sun mb-1 block text-sm"></i> 晴天
                    </button>
                    <button data-weather="rain" class="toggle-btn flex-1 py-2.5 rounded-xl text-xs text-white">
                        <i class="fas fa-bolt mb-1 block text-sm"></i> 雷雨
                    </button>
                    <button data-weather="snow" class="toggle-btn flex-1 py-2.5 rounded-xl text-xs text-white">
                        <i class="fas fa-snowflake mb-1 block text-sm"></i> 下雪
                    </button>
                </div>
            </div>
        </div>
    </div>

    <script>
        (function() {
            const dateElement = document.getElementById('dateText');
            const bgContainer = document.getElementById('bgContainer');
            const lightningLayer = document.getElementById('lightningLayer');
            const weatherCanvas = document.getElementById('weatherCanvas');
            const weatherCtx = weatherCanvas.getContext('2d');
            const cardEffectCanvas = document.getElementById('cardEffectCanvas');
            const cardEffectCtx = cardEffectCanvas.getContext('2d');
            const weatherCard = document.getElementById('weatherCard');
            const parallaxElements = document.querySelectorAll('[data-parallax]');
            const frostOverlay = document.getElementById('frostOverlay');

            const sun = document.getElementById('sun');
            const sunRayEffect = document.getElementById('sunRayEffect');
            const cloud1 = document.getElementById('cloud1');
            const cloud2 = document.getElementById('cloud2');
            const tempValue = document.getElementById('tempValue');
            const weatherDesc = document.getElementById('weatherDesc');
            const weatherIcon = document.getElementById('weatherIcon');
            const windValue = document.getElementById('windValue');
            const humidValue = document.getElementById('humidValue');
            const feelsLikeValue = document.getElementById('feelsLikeValue');
            const btns = document.querySelectorAll('.toggle-btn');

            const today = new Date();
            dateElement.innerText = today.toLocaleDateString('zh-CN', { weekday: 'long', month: 'short', day: 'numeric' });

            let w, h;
            let cardRect;
            let particles = [];
            let weatherType = 'sunny';
            let windSpeed = 0;
            let time = 0;
            
            // 鼠标位置跟踪 (用于交互)
            let mouseX = -1000;
            let mouseY = -1000;

            function resize() {
                w = weatherCanvas.width = window.innerWidth;
                h = weatherCanvas.height = window.innerHeight;
                
                cardRect = weatherCard.getBoundingClientRect();
                cardEffectCanvas.width = cardRect.width;
                cardEffectCanvas.height = cardRect.height;
            }
            window.addEventListener('resize', resize);
            resize();

            // --- 视差与交互核心 ---
            function handleMove(x, y) {
                mouseX = x;
                mouseY = y;

                const centerX = window.innerWidth / 2;
                const centerY = window.innerHeight / 2;
                const rotateX = -((y - centerY) / centerY) * 10;
                const rotateY = ((x - centerX) / centerX) * 10;

                weatherCard.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;

                parallaxElements.forEach(el => {
                    const speed = parseFloat(el.getAttribute('data-parallax'));
                    const offX = (x - centerX) * speed;
                    const offY = (y - centerY) * speed;
                    el.style.transform = `translate(${offX}px, ${offY}px)`;
                });

                bgContainer.style.transform = `translate(${-rotateY * 0.5}px, ${-rotateX * 0.5}px) scale(1.1)`;
            }

            // 鼠标移动
            document.addEventListener('mousemove', (e) => handleMove(e.clientX, e.clientY));
            
            // 触摸移动 (支持手机)
            document.addEventListener('touchmove', (e) => {
                handleMove(e.touches[0].clientX, e.touches[0].clientY);
            }, { passive: true });

            // 陀螺仪支持 (可选,部分设备需要权限)
            if (window.DeviceOrientationEvent) {
                window.addEventListener('deviceorientation', (e) => {
                    if (!e.beta || !e.gamma) return; // 某些桌面环境可能没有数据
                    // 简单的映射
                    const x = (e.gamma + 90) / 180 * window.innerWidth; 
                    const y = (e.beta + 90) / 180 * window.innerHeight;
                    // 这里不直接覆盖 handleMove,因为可能会冲突,实际项目通常会做一个输入源判断
                    // 仅做演示:如果鼠标不在屏幕内,使用陀螺仪
                    if (mouseX === -1000) { 
                        handleMove(x, y);
                    }
                });
            }

            // --- 粒子系统 ---
            class Particle {
                constructor(type) {
                    this.type = type;
                    this.reset();
                }

                reset() {
                    this.x = Math.random() * w;
                    this.y = Math.random() * -h;
                    
                    if (this.type === 'rain') {
                        this.vy = Math.random() * 15 + 15;
                        this.vxBase = Math.random() * 1 - 0.5; 
                        this.len = Math.random() * 20 + 10;
                        this.opacity = Math.random() * 0.4 + 0.2;
                    } else if (this.type === 'snow') {
                        this.vy = Math.random() * 2 + 1;
                        this.vxBase = Math.random() * 2 - 1;
                        this.size = Math.random() * 3 + 2;
                        this.opacity = Math.random() * 0.6 + 0.4;
                    }
                }

                update() {
                    // 风力
                    let currentVx = this.vxBase + windSpeed;

                    // --- 鼠标交互场 (Repel Effect) ---
                    const dx = this.x - mouseX;
                    const dy = this.y - mouseY;
                    const dist = Math.sqrt(dx*dx + dy*dy);
                    const interactRadius = 150; // 交互半径

                    if (dist < interactRadius) {
                        const force = (interactRadius - dist) / interactRadius; // 距离越近力度越大
                        const angle = Math.atan2(dy, dx);
                        const pushX = Math.cos(angle) * force * 15; // 推力
                        const pushY = Math.sin(angle) * force * 15;
                        
                        this.x += pushX;
                        this.y += pushY;
                    }
                    // ----------------------------------

                    this.x += currentVx;
                    this.y += this.vy;

                    if (this.type === 'rain') {
                        const relX = this.x - cardRect.left;
                        const relY = this.y - cardRect.top;
                        if (relX > 0 && relX < cardRect.width && relY > 0 && relY < cardRect.height) {
                            if (Math.random() < 0.05) createRipple(relX, relY);
                        }
                    }

                    if (this.y > h || this.x > w + 50 || this.x < -50) this.reset();
                }

                draw() {
                    weatherCtx.beginPath();
                    if (this.type === 'rain') {
                        weatherCtx.strokeStyle = `rgba(255, 255, 255, ${this.opacity})`;
                        weatherCtx.lineWidth = 1.5;
                        weatherCtx.moveTo(this.x, this.y);
                        weatherCtx.lineTo(this.x + (windSpeed * 2), this.y + this.len);
                        weatherCtx.stroke();
                    } else if (this.type === 'snow') {
                        weatherCtx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
                        weatherCtx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                        weatherCtx.fill();
                    }
                }
            }

            let ripples = [];
            class Ripple {
                constructor(x, y) {
                    this.x = x; this.y = y; this.radius = 0;
                    this.maxRadius = Math.random() * 15 + 5;
                    this.opacity = 0.8; this.speed = 1;
                }
                update() { this.radius += this.speed; this.opacity -= 0.03; }
                draw() {
                    cardEffectCtx.beginPath();
                    cardEffectCtx.strokeStyle = `rgba(255, 255, 255, ${this.opacity})`;
                    cardEffectCtx.lineWidth = 1.5;
                    cardEffectCtx.ellipse(this.x, this.y, this.radius, this.radius * 0.6, 0, 0, Math.PI * 2);
                    cardEffectCtx.stroke();
                }
            }
            function createRipple(x, y) { ripples.push(new Ripple(x, y)); }

            function initParticles(type, count) {
                particles = [];
                for(let i=0; i<count; i++) particles.push(new Particle(type));
            }

            function animate() {
                weatherCtx.clearRect(0, 0, w, h);
                cardEffectCtx.clearRect(0, 0, cardEffectCanvas.width, cardEffectCanvas.height);
                time += 0.01;
                windSpeed = Math.sin(time) * 2;

                if (weatherType !== 'sunny') {
                    particles.forEach(p => { p.update(); p.draw(); });
                }

                for (let i = ripples.length - 1; i >= 0; i--) {
                    let r = ripples[i];
                    r.update(); r.draw();
                    if (r.opacity <= 0) ripples.splice(i, 1);
                }

                if (weatherType === 'rain' && Math.random() < 0.005) triggerLightning();

                requestAnimationFrame(animate);
            }

            function triggerLightning() {
                lightningLayer.style.background = 'rgba(255, 255, 255, 0.8)';
                setTimeout(() => {
                    lightningLayer.style.background = 'rgba(255, 255, 255, 0)';
                    setTimeout(() => {
                        if(Math.random() > 0.5) {
                            lightningLayer.style.background = 'rgba(255, 255, 255, 0.4)';
                            setTimeout(() => lightningLayer.style.background = 'rgba(255, 255, 255, 0)', 50);
                        }
                    }, 100);
                }, 80);
            }

            function setWeather(type, btn) {
                weatherType = type;
                btns.forEach(b => b.classList.remove('active'));
                if(btn) btn.classList.add('active');
                bgContainer.className = 'bg-container';
                ripples = [];

                if (type === 'sunny') {
                    bgContainer.classList.add('bg-sunny');
                    sun.classList.add('active');
                    sunRayEffect.classList.add('active');
                    cloud1.classList.add('active');
                    cloud2.classList.add('active');
                    frostOverlay.classList.remove('active'); // 移除霜
                    weatherCard.style.backdropFilter = 'blur(16px)'; // 恢复默认模糊

                    tempValue.innerText = "26";
                    weatherDesc.innerText = "晴朗舒适";
                    weatherIcon.className = "fas fa-sun text-4xl text-yellow-300 drop-shadow-lg filter";
                    windValue.innerText = "3 km/h";
                    humidValue.innerText = "45%";
                    feelsLikeValue.innerText = "28°";
                    particles = [];

                } else if (type === 'rain') {
                    bgContainer.classList.add('bg-rain');
                    sun.classList.remove('active');
                    sunRayEffect.classList.remove('active');
                    cloud1.classList.add('active');
                    cloud2.classList.add('active');
                    frostOverlay.classList.remove('active');
                    weatherCard.style.backdropFilter = 'blur(24px)'; // 雨天玻璃更模糊(水汽)

                    tempValue.innerText = "19";
                    weatherDesc.innerText = "雷阵雨";
                    weatherIcon.className = "fas fa-bolt text-4xl text-yellow-200 drop-shadow-lg filter animate-pulse";
                    windValue.innerText = "18 km/h";
                    humidValue.innerText = "92%";
                    feelsLikeValue.innerText = "17°";
                    initParticles('rain', 400);

                } else if (type === 'snow') {
                    bgContainer.classList.add('bg-snow');
                    sun.classList.remove('active');
                    sunRayEffect.classList.remove('active');
                    cloud1.classList.remove('active');
                    cloud2.classList.remove('active');
                    frostOverlay.classList.add('active'); // 添加霜
                    weatherCard.style.backdropFilter = 'blur(20px)'; // 雪天稍微模糊

                    tempValue.innerText = "-4";
                    weatherDesc.innerText = "大雪纷飞";
                    weatherIcon.className = "fas fa-snowflake text-4xl text-white drop-shadow-lg filter";
                    windValue.innerText = "8 km/h";
                    humidValue.innerText = "88%";
                    feelsLikeValue.innerText = "-9°";
                    initParticles('snow', 200);
                }
            }

            btns.forEach(btn => {
                btn.addEventListener('click', function() {
                    setWeather(this.getAttribute('data-weather'), this);
                });
            });

            animate();
            setWeather('sunny', btns[0]);
        })();
    </script>
</body>
</html>
相关推荐
gechunlian884 小时前
SpringBoot3+Springdoc:v3api-docs可以访问,html无法访问的解决方法
前端·html
驾驭人生4 小时前
ASP.NET Core 实现 SSE 服务器推送|生产级实战教程(含跨域 / Nginx / 前端完整代码)
服务器·前端·nginx
酉鬼女又兒5 小时前
零基础快速入门前端ES6 核心特性详解:Set 数据结构与对象增强写法(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·es6
慧一居士5 小时前
Vue项目中,子组件调用父组件方法示例,以及如何传值示例,对比使用插槽和不使用插槽区别
前端·vue.js
我是伪码农5 小时前
HTML和CSS复习
前端·css·html
林恒smileZAZ5 小时前
前端实现进度条
前端
前端老石人5 小时前
邂逅前端开发:从基础到实践的全景指南
开发语言·前端·html
阿珊和她的猫5 小时前
以用户为中心的前端性能指标解析
前端·javascript·css
木心术15 小时前
OpenClaw网页前端开发与优化全流程指南
前端·人工智能
Amumu121385 小时前
HTML5的新特性
前端·html·html5