个人感觉酒馆RP效果好了点,文字自然很多
4 个赞
经典天气卡片,reasoner
提示词
创建一个包含 CSS 和 JavaScript 的单个 HTML 文件,以生成一个动画天气卡片。该卡片应通过独特的动画在视觉上呈现以下天气状况:
风:(例如,移动的云朵、摇曳的树木或风的线条)
雨:(例如,下落的雨滴、形成的水坑)
太阳:(例如,闪耀的光线、明亮的背景)
雪:(例如,飘落的雪花、积雪)
将所有天气卡片并排展示。卡片应具有深色背景。
在这个单个文件中提供所有的 HTML、CSS 和 JavaScript 代码。
JavaScript 代码应包含一种在不同天气状况之间切换的方式(例如,一个函数或一组按钮),
以展示每种天气状况的动画效果
此文本将被隐藏
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>
看来四月底稳了


