DS api 上V4 lite了,有没有人试试代码效果

个人感觉酒馆RP效果好了点,文字自然很多

4 个赞

经典天气卡片,reasoner

提示词

创建一个包含 CSS 和 JavaScript 的单个 HTML 文件,以生成一个动画天气卡片。该卡片应通过独特的动画在视觉上呈现以下天气状况:
风:(例如,移动的云朵、摇曳的树木或风的线条)
雨:(例如,下落的雨滴、形成的水坑)
太阳:(例如,闪耀的光线、明亮的背景)
雪:(例如,飘落的雪花、积雪)
将所有天气卡片并排展示。卡片应具有深色背景。
在这个单个文件中提供所有的 HTML、CSS 和 JavaScript 代码。
JavaScript 代码应包含一种在不同天气状况之间切换的方式(例如,一个函数或一组按钮),
以展示每种天气状况的动画效果

此文本将被隐藏

1 个赞

思考过程还是很好玩的

1 个赞
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>🌦️ 动画天气卡片</title>
    <style>
        /* ----- 全局重置 & 深色背景 ----- */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            min-height: 100vh;
            background: #0b0e1a;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
            padding: 20px;
        }

        .container {
            width: 100%;
            max-width: 1280px;
            background: #141829;
            border-radius: 32px;
            padding: 30px 24px 28px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8);
            border: 1px solid rgba(255, 255, 255, 0.04);
        }

        /* ----- 标题 & 全局控制 ----- */
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 16px;
            margin-bottom: 24px;
            padding: 0 4px;
        }

        .header h1 {
            color: #e8edf5;
            font-weight: 600;
            font-size: 1.7rem;
            letter-spacing: 0.5px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .header h1 span {
            background: rgba(255, 255, 255, 0.06);
            padding: 2px 14px;
            border-radius: 40px;
            font-size: 0.85rem;
            font-weight: 400;
            color: #8899bb;
        }

        .global-controls {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .global-controls button {
            padding: 8px 20px;
            border: none;
            border-radius: 40px;
            font-size: 0.9rem;
            font-weight: 500;
            cursor: pointer;
            transition: 0.25s ease;
            background: rgba(255, 255, 255, 0.06);
            color: #aabbdd;
            backdrop-filter: blur(4px);
            border: 1px solid rgba(255, 255, 255, 0.06);
            letter-spacing: 0.3px;
            display: flex;
            align-items: center;
            gap: 6px;
        }
        .global-controls button:hover {
            background: rgba(255, 255, 255, 0.14);
            color: #fff;
            transform: translateY(-2px);
        }
        .global-controls button.active-wind {
            background: #4fc3f7;
            color: #0b0e1a;
            border-color: #4fc3f7;
            box-shadow: 0 0 24px rgba(79, 195, 247, 0.25);
        }
        .global-controls button.active-rain {
            background: #42a5f5;
            color: #0b0e1a;
            border-color: #42a5f5;
            box-shadow: 0 0 24px rgba(66, 165, 245, 0.25);
        }
        .global-controls button.active-sun {
            background: #ffb74d;
            color: #0b0e1a;
            border-color: #ffb74d;
            box-shadow: 0 0 24px rgba(255, 183, 77, 0.25);
        }
        .global-controls button.active-snow {
            background: #e0e8f0;
            color: #0b0e1a;
            border-color: #e0e8f0;
            box-shadow: 0 0 24px rgba(224, 232, 240, 0.2);
        }

        /* ----- 卡片网格 ----- */
        .cards-grid {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 18px;
        }

        /* ----- 单张卡片 ----- */
        .card {
            background: #1c2238;
            border-radius: 20px;
            overflow: hidden;
            border: 1px solid rgba(255, 255, 255, 0.05);
            transition: transform 0.3s ease, box-shadow 0.3s ease;
            display: flex;
            flex-direction: column;
        }
        .card:hover {
            transform: translateY(-4px);
            box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
        }

        .card-header {
            padding: 14px 18px 8px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .card-header .card-title {
            color: #d0daea;
            font-weight: 600;
            font-size: 1.05rem;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .card-header .card-title .icon {
            font-size: 1.3rem;
        }
        .card-header .weather-badge {
            font-size: 0.7rem;
            padding: 2px 12px;
            border-radius: 40px;
            background: rgba(255, 255, 255, 0.06);
            color: #8899bb;
            letter-spacing: 0.3px;
        }

        .card-canvas-wrap {
            position: relative;
            width: 100%;
            padding-top: 75%;
            /* 4:3 比例 */
            background: #0f1322;
            flex-shrink: 0;
        }
        .card-canvas-wrap canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: block;
            border-radius: 4px;
        }

        /* ----- 卡片内控制按钮 ----- */
        .card-controls {
            display: flex;
            gap: 6px;
            padding: 12px 16px 14px;
            justify-content: center;
            background: rgba(0, 0, 0, 0.15);
            border-top: 1px solid rgba(255, 255, 255, 0.03);
            flex-wrap: wrap;
        }
        .card-controls .ctrl-btn {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            border: none;
            cursor: pointer;
            font-size: 1.1rem;
            transition: 0.25s ease;
            background: rgba(255, 255, 255, 0.06);
            color: #667799;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
        }
        .card-controls .ctrl-btn:hover {
            background: rgba(255, 255, 255, 0.15);
            transform: scale(1.12);
            color: #fff;
        }
        .card-controls .ctrl-btn.active {
            transform: scale(1.1);
            box-shadow: 0 0 20px rgba(255, 255, 255, 0.08);
        }
        .card-controls .ctrl-btn.active-wind {
            background: #4fc3f7;
            color: #0b0e1a;
        }
        .card-controls .ctrl-btn.active-rain {
            background: #42a5f5;
            color: #0b0e1a;
        }
        .card-controls .ctrl-btn.active-sun {
            background: #ffb74d;
            color: #0b0e1a;
        }
        .card-controls .ctrl-btn.active-snow {
            background: #e0e8f0;
            color: #0b0e1a;
        }

        /* ----- 响应式 ----- */
        @media (max-width: 900px) {
            .cards-grid {
                grid-template-columns: repeat(2, 1fr);
                gap: 14px;
            }
            .header h1 {
                font-size: 1.3rem;
            }
        }
        @media (max-width: 520px) {
            .cards-grid {
                grid-template-columns: 1fr;
                gap: 18px;
            }
            .container {
                padding: 16px;
            }
            .header {
                flex-direction: column;
                align-items: stretch;
                gap: 12px;
            }
            .global-controls {
                justify-content: center;
            }
        }

        /* 工具类 */
        .hidden {
            display: none !important;
        }
    </style>
</head>
<body>

    <div class="container">
        <!-- 头部 -->
        <div class="header">
            <h1>
                🌦️ 天气动画
                <span>4 季</span>
            </h1>
            <div class="global-controls" id="globalControls">
                <button data-weather="wind">💨 风</button>
                <button data-weather="rain">🌧️ 雨</button>
                <button data-weather="sun">☀️ 太阳</button>
                <button data-weather="snow">❄️ 雪</button>
            </div>
        </div>

        <!-- 卡片网格 -->
        <div class="cards-grid" id="cardsGrid">
            <!-- 四个卡片将由 JavaScript 动态生成 -->
        </div>
    </div>

    <script>
        // ================================================================
        //  1. 工具 & 常量
        // ================================================================
        const WEATHERS = ['wind', 'rain', 'sun', 'snow'];
        const WEATHER_LABELS = {
            wind: { label: '风', icon: '💨' },
            rain: { label: '雨', icon: '🌧️' },
            sun: { label: '太阳', icon: '☀️' },
            snow: { label: '雪', icon: '❄️' },
        };
        const CARD_NAMES = ['清新微风', '绵绵细雨', '灿烂阳光', '浪漫飘雪'];
        const CARD_INITIAL = ['wind', 'rain', 'sun', 'snow'];

        // 颜色方案
        const COLORS = {
            wind: { primary: '#4fc3f7', secondary: '#81d4fa', particle: '#b3e5fc', cloud: 'rgba(255,255,255,0.5)' },
            rain: { primary: '#42a5f5', secondary: '#64b5f6', drop: '#90caf9', ripple: 'rgba(144,202,249,0.3)' },
            sun: { primary: '#ffb74d', secondary: '#ffcc80', glow: '#fff3e0', ray: '#ffe082' },
            snow: { primary: '#e0e8f0', secondary: '#f0f4f8', flake: '#ffffff', ground: 'rgba(255,255,255,0.15)' },
        };

        // ================================================================
        //  2. 天气粒子系统 (每个卡片独立)
        // ================================================================
        class WeatherScene {
            constructor(canvas, weatherType) {
                this.canvas = canvas;
                this.ctx = canvas.getContext('2d');
                this.weather = weatherType || 'wind';
                this.particles = [];
                this.width = canvas.width;
                this.height = canvas.height;
                this.time = 0;
                this.clouds = [];
                this.ripples = [];
                this.snowGround = 0;
                this.sunAngle = 0;
                this.windLines = [];

                this._initParticles();
                this._initScene();
            }

            resize(w, h) {
                this.width = w;
                this.height = h;
                this.canvas.width = w;
                this.canvas.height = h;
                this._initParticles();
                this._initScene();
            }

            setWeather(type) {
                if (type === this.weather) return;
                this.weather = type;
                this._initParticles();
                this._initScene();
            }

            // ---------- 初始化 ----------
            _initParticles() {
                this.particles = [];
                this.clouds = [];
                this.ripples = [];
                this.windLines = [];
                const w = this.width,
                    h = this.height;
                if (!w || !h) return;

                switch (this.weather) {
                    case 'wind':
                        // 风粒子 (流动线条)
                        for (let i = 0; i < 60; i++) {
                            this.particles.push({
                                x: Math.random() * w * 1.4 - w * 0.2,
                                y: Math.random() * h,
                                vx: 1.2 + Math.random() * 2.5,
                                vy: (Math.random() - 0.5) * 0.4,
                                len: 20 + Math.random() * 40,
                                alpha: 0.15 + Math.random() * 0.35,
                                width: 1.2 + Math.random() * 2.5,
                            });
                        }
                        // 云朵
                        for (let i = 0; i < 4; i++) {
                            this.clouds.push({
                                x: Math.random() * w * 1.6 - w * 0.3,
                                y: 0.08 * h + Math.random() * 0.35 * h,
                                rw: 50 + Math.random() * 90,
                                rh: 24 + Math.random() * 36,
                                speed: 0.4 + Math.random() * 0.8,
                                alpha: 0.25 + Math.random() * 0.25,
                            });
                        }
                        break;

                    case 'rain':
                        // 雨滴
                        for (let i = 0; i < 160; i++) {
                            this.particles.push({
                                x: Math.random() * w * 1.2 - w * 0.1,
                                y: Math.random() * h * 1.3 - h * 0.15,
                                vy: 5 + Math.random() * 6,
                                len: 12 + Math.random() * 18,
                                alpha: 0.3 + Math.random() * 0.5,
                                speed: 5 + Math.random() * 6,
                            });
                        }
                        // 涟漪 (初始几个)
                        for (let i = 0; i < 3; i++) {
                            this.ripples.push({
                                x: Math.random() * w,
                                y: h * (0.82 + Math.random() * 0.14),
                                r: 2 + Math.random() * 6,
                                maxR: 20 + Math.random() * 30,
                                alpha: 0.5,
                                speed: 0.4 + Math.random() * 0.6,
                            });
                        }
                        break;

                    case 'sun':
                        // 闪烁光点
                        for (let i = 0; i < 50; i++) {
                            this.particles.push({
                                x: Math.random() * w,
                                y: Math.random() * h,
                                size: 1.5 + Math.random() * 4,
                                alpha: 0.1 + Math.random() * 0.5,
                                speed: 0.005 + Math.random() * 0.015,
                                phase: Math.random() * Math.PI * 2,
                                baseAlpha: 0.1 + Math.random() * 0.5,
                            });
                        }
                        break;

                    case 'snow':
                        // 雪花
                        for (let i = 0; i < 120; i++) {
                            this.particles.push({
                                x: Math.random() * w * 1.3 - w * 0.15,
                                y: Math.random() * h * 1.3 - h * 0.15,
                                r: 1.5 + Math.random() * 3.5,
                                vy: 0.4 + Math.random() * 0.9,
                                vx: (Math.random() - 0.5) * 0.3,
                                swing: 0.2 + Math.random() * 0.6,
                                swingSpeed: 0.008 + Math.random() * 0.015,
                                phase: Math.random() * Math.PI * 2,
                                alpha: 0.5 + Math.random() * 0.5,
                            });
                        }
                        this.snowGround = 0;
                        break;
                }
            }

            _initScene() {
                if (this.weather === 'sun') {
                    this.sunAngle = 0;
                }
                if (this.weather === 'snow') {
                    this.snowGround = 0;
                }
                // 风线条
                if (this.weather === 'wind') {
                    this.windLines = [];
                    for (let i = 0; i < 8; i++) {
                        this.windLines.push({
                            y: 0.1 + Math.random() * 0.8,
                            phase: Math.random() * Math.PI * 2,
                            amp: 6 + Math.random() * 16,
                            freq: 0.008 + Math.random() * 0.015,
                            speed: 0.6 + Math.random() * 1.2,
                        });
                    }
                }
            }

            // ---------- 更新 ----------
            update() {
                this.time += 0.016;
                const w = this.width,
                    h = this.height;
                if (!w || !h) return;

                switch (this.weather) {
                    case 'wind':
                        this._updateWind(w, h);
                        break;
                    case 'rain':
                        this._updateRain(w, h);
                        break;
                    case 'sun':
                        this._updateSun(w, h);
                        break;
                    case 'snow':
                        this._updateSnow(w, h);
                        break;
                }
            }

            _updateWind(w, h) {
                // 粒子
                for (const p of this.particles) {
                    p.x += p.vx;
                    p.y += p.vy + Math.sin(this.time * 0.02 + p.x * 0.01) * 0.08;
                    if (p.x > w + 60) {
                        p.x = -60 - Math.random() * 40;
                        p.y = Math.random() * h;
                        p.alpha = 0.15 + Math.random() * 0.35;
                    }
                }
                // 云朵
                for (const c of this.clouds) {
                    c.x += c.speed;
                    if (c.x > w + c.rw + 40) {
                        c.x = -c.rw - 40 - Math.random() * 60;
                        c.y = 0.08 * h + Math.random() * 0.35 * h;
                        c.alpha = 0.25 + Math.random() * 0.25;
                    }
                }
            }

            _updateRain(w, h) {
                for (const p of this.particles) {
                    p.y += p.vy;
                    p.x += Math.sin(this.time * 0.01 + p.y * 0.02) * 0.06;
                    if (p.y > h + 20) {
                        // 落地 → 产生涟漪
                        if (Math.random() < 0.25) {
                            this.ripples.push({
                                x: p.x,
                                y: h * (0.82 + Math.random() * 0.14),
                                r: 2,
                                maxR: 16 + Math.random() * 28,
                                alpha: 0.5 + Math.random() * 0.3,
                                speed: 0.5 + Math.random() * 0.8,
                            });
                        }
                        p.y = -20 - Math.random() * 30;
                        p.x = Math.random() * w * 1.2 - w * 0.1;
                        p.vy = 5 + Math.random() * 6;
                    }
                }
                // 涟漪
                for (let i = this.ripples.length - 1; i >= 0; i--) {
                    const r = this.ripples[i];
                    r.r += r.speed;
                    r.alpha -= 0.008;
                    if (r.alpha <= 0 || r.r > r.maxR) {
                        this.ripples.splice(i, 1);
                        continue;
                    }
                }
                // 控制涟漪数量
                while (this.ripples.length > 60) this.ripples.shift();
            }

            _updateSun(w, h) {
                this.sunAngle += 0.012;
                for (const p of this.particles) {
                    p.alpha = p.baseAlpha * (0.5 + 0.5 * Math.sin(this.time * p.speed + p.phase));
                }
            }

            _updateSnow(w, h) {
                for (const p of this.particles) {
                    p.x += p.vx + Math.sin(this.time * p.swingSpeed + p.phase) * p.swing;
                    p.y += p.vy;
                    if (p.y > h - 8 - Math.random() * 6) {
                        // 落地堆积
                        p.y = h - 6 - Math.random() * 4;
                        p.vy = 0;
                        p.vx *= 0.9;
                        if (this.snowGround < h * 0.35) {
                            this.snowGround += 0.06;
                        }
                        if (Math.random() < 0.005) {
                            p.y = -10 - Math.random() * 20;
                            p.x = Math.random() * w * 1.3 - w * 0.15;
                            p.vy = 0.4 + Math.random() * 0.9;
                        }
                    }
                    if (p.x < -20) p.x = w + 20;
                    if (p.x > w + 20) p.x = -20;
                }
                // 积雪上限
                if (this.snowGround > h * 0.30) this.snowGround = h * 0.30;
            }

            // ---------- 绘制 ----------
            draw() {
                const ctx = this.ctx;
                const w = this.width,
                    h = this.height;
                if (!w || !h) return;

                ctx.clearRect(0, 0, w, h);

                // 每种天气的背景
                this._drawBackground(ctx, w, h);

                switch (this.weather) {
                    case 'wind':
                        this._drawWind(ctx, w, h);
                        break;
                    case 'rain':
                        this._drawRain(ctx, w, h);
                        break;
                    case 'sun':
                        this._drawSun(ctx, w, h);
                        break;
                    case 'snow':
                        this._drawSnow(ctx, w, h);
                        break;
                }
            }

            _drawBackground(ctx, w, h) {
                // 深色基底 (卡片自身背景已深,这里再叠一层氛围)
                const grad = ctx.createRadialGradient(w * 0.5, h * 0.5, 0, w * 0.5, h * 0.5, w * 0.7);
                switch (this.weather) {
                    case 'wind':
                        grad.addColorStop(0, '#1a2340');
                        grad.addColorStop(1, '#0f1322');
                        break;
                    case 'rain':
                        grad.addColorStop(0, '#152238');
                        grad.addColorStop(1, '#0b1120');
                        break;
                    case 'sun':
                        grad.addColorStop(0, '#2a1f10');
                        grad.addColorStop(0.5, '#1e1828');
                        grad.addColorStop(1, '#0f0e1a');
                        break;
                    case 'snow':
                        grad.addColorStop(0, '#1c2238');
                        grad.addColorStop(1, '#10162a');
                        break;
                    default:
                        grad.addColorStop(0, '#1a1a2e');
                        grad.addColorStop(1, '#0f0f1a');
                }
                ctx.fillStyle = grad;
                ctx.fillRect(0, 0, w, h);

                // 薄雾
                ctx.fillStyle = 'rgba(255,255,255,0.015)';
                ctx.fillRect(0, 0, w, h);
            }

            // ---- 风 ----
            _drawWind(ctx, w, h) {
                // 风线条 (曲线)
                ctx.lineCap = 'round';
                for (const line of this.windLines) {
                    ctx.beginPath();
                    const baseY = line.y * h;
                    let x = -10;
                    ctx.moveTo(x, baseY + Math.sin(this.time * line.speed + line.phase) * line.amp);
                    for (x = 0; x <= w + 20; x += 6) {
                        const yy = baseY + Math.sin(x * line.freq + this.time * line.speed + line.phase) * line.amp;
                        ctx.lineTo(x, yy);
                    }
                    ctx.strokeStyle = `rgba(179,229,252,${0.08 + 0.06 * Math.sin(this.time * 0.5 + line.phase)})`;
                    ctx.lineWidth = 1.8 + Math.sin(this.time * 0.3 + line.phase) * 0.6;
                    ctx.stroke();
                }

                // 云朵
                for (const c of this.clouds) {
                    ctx.save();
                    ctx.globalAlpha = c.alpha;
                    const cx = c.x,
                        cy = c.y;
                    const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, c.rw);
                    grad.addColorStop(0, 'rgba(255,255,255,0.35)');
                    grad.addColorStop(0.5, 'rgba(200,220,255,0.20)');
                    grad.addColorStop(1, 'rgba(200,220,255,0.02)');
                    ctx.fillStyle = grad;
                    ctx.beginPath();
                    ctx.ellipse(cx, cy, c.rw, c.rh, 0, 0, Math.PI * 2);
                    ctx.fill();
                    // 云朵高光
                    ctx.fillStyle = 'rgba(255,255,255,0.10)';
                    ctx.beginPath();
                    ctx.ellipse(cx - c.rw * 0.2, cy - c.rh * 0.3, c.rw * 0.5, c.rh * 0.5, 0, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.restore();
                }

                // 粒子 (流动短线)
                ctx.lineCap = 'round';
                for (const p of this.particles) {
                    ctx.beginPath();
                    const ex = p.x + p.len;
                    const ey = p.y + p.vy * 2;
                    ctx.moveTo(p.x, p.y);
                    ctx.lineTo(ex, ey);
                    ctx.strokeStyle = `rgba(179,229,252,${p.alpha})`;
                    ctx.lineWidth = p.width;
                    ctx.stroke();
                    // 尾部淡出
                    ctx.beginPath();
                    ctx.moveTo(p.x - p.len * 0.2, p.y - p.vy * 0.5);
                    ctx.lineTo(p.x, p.y);
                    ctx.strokeStyle = `rgba(179,229,252,${p.alpha * 0.3})`;
                    ctx.lineWidth = p.width * 0.6;
                    ctx.stroke();
                }
            }

            // ---- 雨 ----
            _drawRain(ctx, w, h) {
                // 雨滴
                ctx.lineCap = 'round';
                for (const p of this.particles) {
                    ctx.beginPath();
                    ctx.moveTo(p.x, p.y);
                    ctx.lineTo(p.x - 0.6, p.y - p.len);
                    ctx.strokeStyle = `rgba(144,202,249,${p.alpha})`;
                    ctx.lineWidth = 1.6;
                    ctx.stroke();
                    // 微光
                    ctx.beginPath();
                    ctx.moveTo(p.x + 0.4, p.y - p.len * 0.3);
                    ctx.lineTo(p.x + 0.4, p.y - p.len * 0.7);
                    ctx.strokeStyle = `rgba(255,255,255,${p.alpha * 0.15})`;
                    ctx.lineWidth = 0.8;
                    ctx.stroke();
                }

                // 涟漪
                for (const r of this.ripples) {
                    ctx.beginPath();
                    ctx.ellipse(r.x, r.y, r.r, r.r * 0.3, 0, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(144,202,249,${r.alpha * 0.6})`;
                    ctx.lineWidth = 1.2;
                    ctx.stroke();
                    // 内圈
                    ctx.beginPath();
                    ctx.ellipse(r.x, r.y, r.r * 0.5, r.r * 0.15, 0, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(255,255,255,${r.alpha * 0.15})`;
                    ctx.lineWidth = 0.6;
                    ctx.stroke();
                }

                // 地面水渍 (底部反光)
                const grad = ctx.createLinearGradient(0, h * 0.88, 0, h);
                grad.addColorStop(0, 'rgba(100,150,220,0.0)');
                grad.addColorStop(0.4, 'rgba(100,150,220,0.04)');
                grad.addColorStop(1, 'rgba(100,150,220,0.08)');
                ctx.fillStyle = grad;
                ctx.fillRect(0, h * 0.88, w, h * 0.12);
            }

            // ---- 太阳 ----
            _drawSun(ctx, w, h) {
                const cx = w * 0.5,
                    cy = h * 0.45;
                const maxR = Math.min(w, h) * 0.32;

                // 1. 外层大光晕
                const grad1 = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxR * 1.6);
                grad1.addColorStop(0, 'rgba(255,224,130,0.20)');
                grad1.addColorStop(0.3, 'rgba(255,200,100,0.10)');
                grad1.addColorStop(0.7, 'rgba(255,180,80,0.04)');
                grad1.addColorStop(1, 'rgba(255,180,80,0)');
                ctx.fillStyle = grad1;
                ctx.beginPath();
                ctx.arc(cx, cy, maxR * 1.6, 0, Math.PI * 2);
                ctx.fill();

                // 2. 光芒射线 (旋转)
                ctx.save();
                ctx.translate(cx, cy);
                ctx.rotate(this.sunAngle);
                for (let i = 0; i < 16; i++) {
                    const angle = (i / 16) * Math.PI * 2;
                    const len = maxR * (0.9 + 0.3 * Math.sin(this.time * 0.6 + i * 0.8));
                    ctx.save();
                    ctx.rotate(angle);
                    const gradR = ctx.createLinearGradient(0, 0, len, 0);
                    gradR.addColorStop(0, 'rgba(255,235,150,0.5)');
                    gradR.addColorStop(0.5, 'rgba(255,200,100,0.15)');
                    gradR.addColorStop(1, 'rgba(255,200,100,0)');
                    ctx.fillStyle = gradR;
                    ctx.beginPath();
                    ctx.moveTo(0, -2.5);
                    ctx.lineTo(len, -0.5);
                    ctx.lineTo(len, 0.5);
                    ctx.lineTo(0, 2.5);
                    ctx.closePath();
                    ctx.fill();
                    ctx.restore();
                }
                ctx.restore();

                // 3. 太阳本体
                const grad2 = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxR * 0.6);
                grad2.addColorStop(0, '#fff8e8');
                grad2.addColorStop(0.2, '#ffecb3');
                grad2.addColorStop(0.5, '#ffcc80');
                grad2.addColorStop(0.8, '#ffb74d');
                grad2.addColorStop(1, 'rgba(255,183,77,0.4)');
                ctx.fillStyle = grad2;
                ctx.beginPath();
                ctx.arc(cx, cy, maxR * 0.6, 0, Math.PI * 2);
                ctx.fill();

                // 4. 核心高光
                const grad3 = ctx.createRadialGradient(cx - maxR * 0.1, cy - maxR * 0.1, 0, cx, cy, maxR * 0.3);
                grad3.addColorStop(0, 'rgba(255,255,255,0.8)');
                grad3.addColorStop(0.5, 'rgba(255,240,200,0.2)');
                grad3.addColorStop(1, 'rgba(255,240,200,0)');
                ctx.fillStyle = grad3;
                ctx.beginPath();
                ctx.arc(cx, cy, maxR * 0.3, 0, Math.PI * 2);
                ctx.fill();

                // 5. 闪烁粒子
                for (const p of this.particles) {
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.size * 0.5, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(255,255,220,${p.alpha * 0.5})`;
                    ctx.fill();
                    // 光晕
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.size * 1.2, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(255,235,180,${p.alpha * 0.1})`;
                    ctx.fill();
                }

                // 6. 环境光 (底部)
                const grad4 = ctx.createLinearGradient(0, h * 0.7, 0, h);
                grad4.addColorStop(0, 'rgba(255,200,100,0.0)');
                grad4.addColorStop(0.5, 'rgba(255,200,100,0.03)');
                grad4.addColorStop(1, 'rgba(255,200,100,0.06)');
                ctx.fillStyle = grad4;
                ctx.fillRect(0, h * 0.7, w, h * 0.3);
            }

            // ---- 雪 ----
            _drawSnow(ctx, w, h) {
                // 积雪
                if (this.snowGround > 0.5) {
                    const gh = Math.min(this.snowGround, h * 0.30);
                    const grad = ctx.createLinearGradient(0, h - gh * 2, 0, h);
                    grad.addColorStop(0, 'rgba(255,255,255,0.0)');
                    grad.addColorStop(0.3, 'rgba(255,255,255,0.04)');
                    grad.addColorStop(0.7, 'rgba(255,255,255,0.10)');
                    grad.addColorStop(1, 'rgba(255,255,255,0.18)');
                    ctx.fillStyle = grad;
                    ctx.beginPath();
                    ctx.moveTo(0, h - gh);
                    for (let x = 0; x <= w; x += 6) {
                        const yy = h - gh + Math.sin(x * 0.03 + this.time * 0.1) * 3;
                        ctx.lineTo(x, yy);
                    }
                    ctx.lineTo(w, h);
                    ctx.lineTo(0, h);
                    ctx.closePath();
                    ctx.fill();

                    // 积雪高光
                    ctx.fillStyle = 'rgba(255,255,255,0.04)';
                    ctx.beginPath();
                    for (let x = 0; x <= w; x += 8) {
                        const yy = h - gh + Math.sin(x * 0.03 + this.time * 0.1) * 3 - 2;
                        if (x === 0) ctx.moveTo(x, yy);
                        else ctx.lineTo(x, yy);
                    }
                    ctx.lineTo(w, h - gh + 6);
                    ctx.lineTo(0, h - gh + 6);
                    ctx.closePath();
                    ctx.fill();
                }

                // 雪花
                for (const p of this.particles) {
                    if (p.vy === 0 && p.y > h - 20) {
                        // 已落地的雪花:微弱闪烁
                        ctx.beginPath();
                        ctx.arc(p.x, p.y, p.r * 0.4, 0, Math.PI * 2);
                        ctx.fillStyle = `rgba(255,255,255,${0.08 + 0.06 * Math.sin(this.time * 0.5 + p.phase)})`;
                        ctx.fill();
                        continue;
                    }
                    // 飘落雪花
                    ctx.save();
                    ctx.shadowColor = 'rgba(255,255,255,0.2)';
                    ctx.shadowBlur = 6;
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(255,255,255,${p.alpha * 0.8})`;
                    ctx.fill();
                    // 雪花高光
                    ctx.shadowBlur = 0;
                    ctx.beginPath();
                    ctx.arc(p.x - p.r * 0.2, p.y - p.r * 0.2, p.r * 0.3, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(255,255,255,${p.alpha * 0.3})`;
                    ctx.fill();
                    ctx.restore();

                    // 六角形装饰 (大雪花)
                    if (p.r > 2.8) {
                        ctx.save();
                        ctx.translate(p.x, p.y);
                        ctx.rotate(this.time * 0.2 + p.phase);
                        ctx.strokeStyle = `rgba(255,255,255,${p.alpha * 0.15})`;
                        ctx.lineWidth = 0.6;
                        for (let i = 0; i < 6; i++) {
                            const a = (i / 6) * Math.PI * 2;
                            ctx.beginPath();
                            ctx.moveTo(0, 0);
                            ctx.lineTo(Math.cos(a) * p.r * 1.6, Math.sin(a) * p.r * 1.6);
                            ctx.stroke();
                        }
                        ctx.restore();
                    }
                }

                // 飘落的微光
                ctx.fillStyle = 'rgba(200,220,255,0.015)';
                for (let i = 0; i < 15; i++) {
                    const x = ((i * 137 + this.time * 20) % w);
                    const y = ((i * 251 + this.time * 8) % h);
                    ctx.beginPath();
                    ctx.arc(x, y, 1 + Math.sin(this.time + i) * 0.5, 0, Math.PI * 2);
                    ctx.fill();
                }
            }

            // ---------- 全帧 ----------
            frame() {
                this.update();
                this.draw();
            }
        }

        // ================================================================
        //  3. 卡片管理器
        // ================================================================
        class WeatherCard {
            constructor(container, name, initialWeather) {
                this.container = container;
                this.name = name;
                this.weather = initialWeather;

                this._buildDOM();
                this._initCanvas();
                this._bindControls();

                this.scene = new WeatherScene(this.canvas, this.weather);
                this._startLoop();
            }

            _buildDOM() {
                const card = document.createElement('div');
                card.className = 'card';

                const info = WEATHER_LABELS[this.weather];
                card.innerHTML = `
                    <div class="card-header">
                        <div class="card-title">
                            <span class="icon">${info.icon}</span>
                            ${this.name}
                        </div>
                        <span class="weather-badge">${info.label}</span>
                    </div>
                    <div class="card-canvas-wrap">
                        <canvas></canvas>
                    </div>
                    <div class="card-controls" data-card-controls>
                        <button class="ctrl-btn" data-w="wind" title="风">💨</button>
                        <button class="ctrl-btn" data-w="rain" title="雨">🌧️</button>
                        <button class="ctrl-btn" data-w="sun" title="太阳">☀️</button>
                        <button class="ctrl-btn" data-w="snow" title="雪">❄️</button>
                    </div>
                `;

                this.container.appendChild(card);
                this.el = card;
                this.canvas = card.querySelector('canvas');
                this.canvasWrap = card.querySelector('.card-canvas-wrap');
                this.controls = card.querySelector('[data-card-controls]');
                this.badge = card.querySelector('.weather-badge');
                this.iconEl = card.querySelector('.icon');
                this.titleEl = card.querySelector('.card-title');
            }

            _initCanvas() {
                this._resizeCanvas();
                // 监听尺寸变化
                this._resizeObserver = new ResizeObserver(() => this._resizeCanvas());
                this._resizeObserver.observe(this.canvasWrap);
            }

            _resizeCanvas() {
                const rect = this.canvasWrap.getBoundingClientRect();
                const w = Math.floor(rect.width);
                const h = Math.floor(rect.height);
                if (w > 0 && h > 0 && (this.canvas.width !== w || this.canvas.height !== h)) {
                    this.canvas.width = w;
                    this.canvas.height = h;
                    if (this.scene) {
                        this.scene.resize(w, h);
                    }
                }
            }

            _bindControls() {
                const btns = this.controls.querySelectorAll('.ctrl-btn');
                for (const btn of btns) {
                    btn.addEventListener('click', () => {
                        const w = btn.dataset.w;
                        this.setWeather(w);
                    });
                }
            }

            setWeather(type) {
                if (type === this.weather) return;
                this.weather = type;
                const info = WEATHER_LABELS[type];
                this.badge.textContent = info.label;
                this.iconEl.textContent = info.icon;

                // 更新按钮高亮
                const btns = this.controls.querySelectorAll('.ctrl-btn');
                for (const btn of btns) {
                    btn.classList.remove('active', 'active-wind', 'active-rain', 'active-sun', 'active-snow');
                    if (btn.dataset.w === type) {
                        btn.classList.add('active', `active-${type}`);
                    }
                }

                this.scene.setWeather(type);
            }

            _startLoop() {
                const loop = () => {
                    if (this.scene) {
                        this.scene.frame();
                    }
                    this._raf = requestAnimationFrame(loop);
                };
                this._raf = requestAnimationFrame(loop);
            }

            destroy() {
                if (this._raf) cancelAnimationFrame(this._raf);
                if (this._resizeObserver) this._resizeObserver.disconnect();
                this.el.remove();
            }
        }

        // ================================================================
        //  4. 初始化应用
        // ================================================================
        (function init() {
            const grid = document.getElementById('cardsGrid');
            const cards = [];

            // 创建四个卡片
            for (let i = 0; i < 4; i++) {
                const card = new WeatherCard(grid, CARD_NAMES[i], CARD_INITIAL[i]);
                cards.push(card);
            }

            // 全局控制
            const globalBtns = document.querySelectorAll('#globalControls button');
            for (const btn of globalBtns) {
                btn.addEventListener('click', () => {
                    const weather = btn.dataset.weather;
                    // 更新全局按钮状态
                    for (const b of globalBtns) {
                        b.classList.remove('active-wind', 'active-rain', 'active-sun', 'active-snow');
                    }
                    btn.classList.add(`active-${weather}`);
                    // 切换所有卡片
                    for (const card of cards) {
                        card.setWeather(weather);
                    }
                });
            }

            // 默认高亮第一个全局按钮 (wind)
            if (globalBtns.length) {
                globalBtns[0].classList.add('active-wind');
            }

            // 窗口变化时重新调整所有 canvas
            window.addEventListener('resize', () => {
                for (const card of cards) {
                    card._resizeCanvas();
                }
            });

            // 暴露到全局以便调试
            window.__weatherCards = cards;
        })();
    </script>

</body>
</html>

看来四月底稳了