跑了五分多钟才出结果,现在并发可能太高了,有点慢。
用的提示词:
你是 Apple Inc 的顶级 UI 设计师,按 iOS 18 风格(毛玻璃、高斯模糊、动态渐变、细腻阴影)写一个单个HTML文件(包含完整CSS和JS)。做横版天气页面,要4个并排的动画天气卡片:
- 晴天(太阳光线、动态光晕)
- 大风(飘动的云、摇曳的树、风线)
- 暴雨(下落的雨滴、形成水洼、闪电)
- 暴雪(下落的雪花、堆积效果)
卡片用深色背景,加按钮切换天气状态,交互要流畅,微动效做好。代码必须能直接跑,美观优先。
HTML 代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天气卡片 - iOS 18 风格</title>
<style>
:root {
--bg-deep: #0a0a14;
--card-bg: rgba(22, 22, 38, 0.55);
--card-border: rgba(255, 255, 255, 0.12);
--card-border-hover: rgba(255, 255, 255, 0.22);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--text-tertiary: rgba(255, 255, 255, 0.45);
--shadow-card: 0 2px 16px rgba(0, 0, 0, 0.35), 0 8px 40px rgba(0, 0, 0, 0.25);
--shadow-card-hover: 0 4px 24px rgba(0, 0, 0, 0.45), 0 12px 56px rgba(0, 0, 0, 0.35),
0 0 0 1px rgba(255, 255, 255, 0.08) inset;
--radius-lg: 28px;
--radius-xl: 32px;
--transition-smooth: cubic-bezier(0.25, 0.1, 0.25, 1);
--glass-blur: 24px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', sans-serif;
background: #080810;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* 动态渐变背景 */
.bg-orchestra {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.bg-orchestra .orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.5;
animation: orbDrift 18s ease-in-out infinite;
will-change: transform;
}
.bg-orchestra .orb:nth-child(1) {
width: 620px;
height: 620px;
background: radial-gradient(circle, rgba(90, 60, 200, 0.7) 0%, transparent 70%);
top: -15%;
left: -10%;
animation-delay: 0s;
animation-duration: 20s;
}
.bg-orchestra .orb:nth-child(2) {
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(30, 120, 210, 0.65) 0%, transparent 70%);
bottom: -18%;
right: -8%;
animation-delay: -7s;
animation-duration: 22s;
}
.bg-orchestra .orb:nth-child(3) {
width: 440px;
height: 440px;
background: radial-gradient(circle, rgba(180, 50, 120, 0.5) 0%, transparent 70%);
top: 40%;
left: 50%;
animation-delay: -13s;
animation-duration: 24s;
}
.bg-orchestra .orb:nth-child(4) {
width: 350px;
height: 350px;
background: radial-gradient(circle, rgba(20, 160, 180, 0.55) 0%, transparent 70%);
bottom: 25%;
left: 20%;
animation-delay: -4s;
animation-duration: 19s;
}
@keyframes orbDrift {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(60px, -50px) scale(1.15);
}
50% {
transform: translate(-30px, 40px) scale(0.9);
}
75% {
transform: translate(-50px, -30px) scale(1.08);
}
}
/* 主容器 */
.main-container {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 36px;
padding: 28px 32px 36px;
width: 100%;
max-width: 1400px;
}
/* 标题 */
.page-title {
font-size: 1.6rem;
font-weight: 500;
letter-spacing: -0.02em;
color: rgba(255, 255, 255, 0.85);
text-align: center;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.page-title span {
font-weight: 300;
color: rgba(255, 255, 255, 0.5);
}
/* 卡片行 */
.cards-row {
display: flex;
gap: 20px;
width: 100%;
justify-content: center;
flex-wrap: nowrap;
}
/* 单张卡片 */
.weather-card {
position: relative;
flex: 1 1 260px;
max-width: 300px;
min-width: 240px;
height: 460px;
border-radius: var(--radius-xl);
background: var(--card-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--card-border);
box-shadow: var(--shadow-card);
overflow: hidden;
cursor: pointer;
transition: all 0.5s var(--transition-smooth);
isolation: isolate;
-webkit-transform: translateZ(0);
transform: translateZ(0);
}
.weather-card:hover {
border-color: var(--card-border-hover);
box-shadow: var(--shadow-card-hover);
transform: translateY(-6px);
transition: all 0.35s var(--transition-smooth);
}
.weather-card:active {
transform: scale(0.975);
transition: all 0.15s var(--transition-smooth);
}
/* 卡片内的光晕反射 (iOS风格顶部高光) */
.weather-card::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-xl);
pointer-events: none;
z-index: 10;
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.06) 0%,
rgba(255, 255, 255, 0.01) 30%,
transparent 55%,
rgba(0, 0, 0, 0.03) 100%);
transition: opacity 0.5s;
}
.weather-card:hover::after {
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.09) 0%,
rgba(255, 255, 255, 0.02) 30%,
transparent 55%,
rgba(0, 0, 0, 0.02) 100%);
}
/* Canvas层 */
.weather-card canvas {
position: absolute;
inset: 0;
z-index: 2;
border-radius: var(--radius-xl);
pointer-events: none;
}
/* 卡片内容覆盖层 */
.card-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 5;
padding: 20px 22px 22px;
display: flex;
flex-direction: column;
gap: 4px;
pointer-events: none;
background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.35) 55%, rgba(0, 0, 0, 0.6) 100%);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
}
.card-overlay .temp {
font-size: 3.4rem;
font-weight: 200;
letter-spacing: -0.04em;
color: #fff;
line-height: 1;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.card-overlay .desc {
font-size: 0.95rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.8);
letter-spacing: 0.01em;
}
.card-overlay .city {
font-size: 0.78rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.02em;
text-transform: uppercase;
}
/* 选中高亮指示 */
.weather-card.selected {
border-color: rgba(255, 255, 255, 0.35);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.18), 0 8px 40px rgba(0, 0, 0, 0.4),
0 0 80px rgba(120, 140, 255, 0.2);
transform: translateY(-4px);
}
.weather-card.selected::before {
content: '';
position: absolute;
inset: -3px;
border-radius: calc(var(--radius-xl) + 3px);
z-index: -1;
pointer-events: none;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.25) 0%,
rgba(255, 255, 255, 0.05) 40%,
rgba(255, 255, 255, 0.15) 100%);
animation: selectedGlow 3s ease-in-out infinite;
}
@keyframes selectedGlow {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* 底部控制栏 */
.controls-bar {
display: flex;
align-items: center;
gap: 10px;
background: rgba(30, 30, 48, 0.5);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
border: 1px solid rgba(255, 255, 255, 0.13);
border-radius: 40px;
padding: 6px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
z-index: 10;
}
.intensity-btn {
padding: 10px 22px;
border-radius: 34px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.65);
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 0.01em;
cursor: pointer;
transition: all 0.4s var(--transition-smooth);
font-family: inherit;
white-space: nowrap;
position: relative;
outline: none;
-webkit-tap-highlight-color: transparent;
}
.intensity-btn:hover {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.06);
}
.intensity-btn.active {
background: rgba(255, 255, 255, 0.15);
color: #fff;
font-weight: 600;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.2) inset;
letter-spacing: 0.02em;
}
.intensity-btn:active {
transform: scale(0.94);
transition: all 0.12s var(--transition-smooth);
}
.controls-label {
color: rgba(255, 255, 255, 0.4);
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.04em;
padding: 0 8px;
pointer-events: none;
white-space: nowrap;
}
/* 响应式 */
@media (max-width: 1100px) {
.cards-row {
flex-wrap: wrap;
gap: 16px;
}
.weather-card {
flex: 1 1 220px;
max-width: 48%;
min-width: 180px;
height: 400px;
border-radius: 24px;
}
.weather-card::after {
border-radius: 24px;
}
.weather-card canvas {
border-radius: 24px;
}
.card-overlay {
border-radius: 0 0 24px 24px;
padding: 16px 16px 18px;
}
.card-overlay .temp {
font-size: 2.6rem;
}
.weather-card.selected::before {
border-radius: 27px;
}
}
@media (max-width: 640px) {
.cards-row {
flex-wrap: wrap;
gap: 12px;
}
.weather-card {
flex: 1 1 44%;
max-width: 48%;
min-width: 150px;
height: 340px;
border-radius: 20px;
}
.weather-card::after,
.weather-card canvas {
border-radius: 20px;
}
.card-overlay {
border-radius: 0 0 20px 20px;
padding: 12px 14px 14px;
}
.card-overlay .temp {
font-size: 2rem;
}
.card-overlay .desc {
font-size: 0.8rem;
}
.card-overlay .city {
font-size: 0.7rem;
}
.weather-card.selected::before {
border-radius: 23px;
}
.controls-bar {
gap: 4px;
padding: 4px;
border-radius: 32px;
}
.intensity-btn {
padding: 8px 14px;
font-size: 0.78rem;
border-radius: 26px;
}
.controls-label {
font-size: 0.7rem;
padding: 0 4px;
}
.page-title {
font-size: 1.3rem;
}
}
</style>
</head>
<body>
<!-- 动态渐变背景 -->
<div class="bg-orchestra">
<div class="orb"></div>
<div class="orb"></div>
<div class="orb"></div>
<div class="orb"></div>
</div>
<!-- 主容器 -->
<div class="main-container">
<div class="page-title">天 气 <span>· Weather</span></div>
<!-- 卡片行 -->
<div class="cards-row" id="cardsRow">
<!-- 晴天卡片 -->
<div class="weather-card" data-weather="sunny" data-index="0">
<canvas id="canvasSunny" aria-hidden="true"></canvas>
<div class="card-overlay">
<div class="temp">28°</div>
<div class="desc">晴空万里</div>
<div class="city">加利福尼亚 · 库比蒂诺</div>
</div>
</div>
<!-- 大风卡片 -->
<div class="weather-card" data-weather="windy" data-index="1">
<canvas id="canvasWindy" aria-hidden="true"></canvas>
<div class="card-overlay">
<div class="temp">19°</div>
<div class="desc">大风呼啸</div>
<div class="city">伊利诺伊 · 芝加哥</div>
</div>
</div>
<!-- 暴雨卡片 -->
<div class="weather-card" data-weather="rain" data-index="2">
<canvas id="canvasRain" aria-hidden="true"></canvas>
<div class="card-overlay">
<div class="temp">14°</div>
<div class="desc">暴雨倾盆</div>
<div class="city">华盛顿 · 西雅图</div>
</div>
</div>
<!-- 暴雪卡片 -->
<div class="weather-card" data-weather="snow" data-index="3">
<canvas id="canvasSnow" aria-hidden="true"></canvas>
<div class="card-overlay">
<div class="temp">-8°</div>
<div class="desc">暴雪纷飞</div>
<div class="city">纽约 · 布法罗</div>
</div>
</div>
</div>
<!-- 强度控制栏 -->
<div class="controls-bar" id="controlsBar">
<span class="controls-label">强度</span>
<button class="intensity-btn" data-level="1" aria-label="轻度">🌤 柔和</button>
<button class="intensity-btn active" data-level="2" aria-label="适中">⛅ 适中</button>
<button class="intensity-btn" data-level="3" aria-label="强烈">🌩 强烈</button>
</div>
</div>
<script>
(function() {
// ============ 全局状态 ============
let globalIntensity = 2; // 1=柔和, 2=适中, 3=强烈
let selectedCardIndex = -1;
const cardElements = document.querySelectorAll('.weather-card');
const intensityButtons = document.querySelectorAll('.intensity-btn');
// ============ Canvas 初始化 ============
const canvasIds = ['canvasSunny', 'canvasWindy', 'canvasRain', 'canvasSnow'];
const weatherTypes = ['sunny', 'windy', 'rain', 'snow'];
const canvases = {};
const contexts = {};
const cardRects = {}; // 存储每个卡片canvas的绘制尺寸
function initCanvas(canvasId, cardEl) {
const canvas = document.getElementById(canvasId);
if (!canvas) return null;
const dpr = Math.min(window.devicePixelRatio || 1, 2); // 限制像素比以保证性能
const rect = cardEl.getBoundingClientRect();
const displayW = rect.width;
const displayH = rect.height;
if (displayW <= 0 || displayH <= 0) {
// 使用默认尺寸
const defaultW = 280;
const defaultH = 460;
canvas.width = defaultW * dpr;
canvas.height = defaultH * dpr;
canvas.style.width = defaultW + 'px';
canvas.style.height = defaultH + 'px';
return { width: defaultW, height: defaultH, dpr };
}
canvas.width = displayW * dpr;
canvas.height = displayH * dpr;
canvas.style.width = displayW + 'px';
canvas.style.height = displayH + 'px';
return { width: displayW, height: displayH, dpr };
}
function updateAllCanvasSizes() {
cardElements.forEach((cardEl, i) => {
const canvasId = canvasIds[i];
const info = initCanvas(canvasId, cardEl);
if (info) {
cardRects[canvasId] = info;
canvases[canvasId] = document.getElementById(canvasId);
contexts[canvasId] = canvases[canvasId]?.getContext('2d');
}
});
}
updateAllCanvasSizes();
// ============ 粒子系统 ============
// 每个天气的粒子数据
const particleData = {
sunny: { particles: [], lastEmit: 0 },
windy: { particles: [], clouds: [], windLines: [], treeSway: 0, lastEmit: 0 },
rain: { drops: [], puddleRipples: [], lightningTimer: 0, lightningActive: false,
lightningAlpha: 0, lastLightning: 0 },
snow: { flakes: [], snowAccum: 0, lastEmit: 0 },
};
function resetParticles(weatherType) {
const data = particleData[weatherType];
if (!data) return;
if (weatherType === 'sunny') {
data.particles = [];
} else if (weatherType === 'windy') {
data.particles = [];
data.clouds = [];
data.windLines = [];
data.treeSway = 0;
// 初始化云朵
const w = cardRects['canvasWindy']?.width || 280;
const h = cardRects['canvasWindy']?.height || 460;
for (let i = 0; i < 5; i++) {
data.clouds.push({
x: Math.random() * w * 1.3 - w * 0.15,
y: 30 + Math.random() * h * 0.3,
width: 60 + Math.random() * 100,
height: 25 + Math.random() * 40,
speed: 0.3 + Math.random() * 0.8,
opacity: 0.5 + Math.random() * 0.4,
});
}
for (let i = 0; i < 12; i++) {
data.windLines.push({
x: Math.random() * w,
y: Math.random() * h,
length: 30 + Math.random() * 80,
speed: 1.5 + Math.random() * 4,
opacity: 0.2 + Math.random() * 0.4,
thickness: 0.5 + Math.random() * 1.5,
});
}
} else if (weatherType === 'rain') {
data.drops = [];
data.puddleRipples = [];
data.lightningTimer = 0;
data.lightningActive = false;
data.lightningAlpha = 0;
data.lastLightning = performance.now();
const w = cardRects['canvasRain']?.width || 280;
const h = cardRects['canvasRain']?.height || 460;
// 预生成雨滴
const dropCount = 90;
for (let i = 0; i < dropCount; i++) {
data.drops.push({
x: Math.random() * w,
y: Math.random() * h,
speed: 5 + Math.random() * 10,
length: 10 + Math.random() * 22,
opacity: 0.35 + Math.random() * 0.5,
thickness: 1 + Math.random() * 1.8,
});
}
// 水洼位置
for (let i = 0; i < 4; i++) {
data.puddleRipples.push({
cx: 30 + Math.random() * (w - 60),
cy: h - 20 - Math.random() * 50,
radius: 15 + Math.random() * 30,
maxRadius: 35 + Math.random() * 50,
ripples: [],
});
}
} else if (weatherType === 'snow') {
data.flakes = [];
data.snowAccum = 0;
const w = cardRects['canvasSnow']?.width || 280;
const h = cardRects['canvasSnow']?.height || 460;
const flakeCount = 100;
for (let i = 0; i < flakeCount; i++) {
data.flakes.push({
x: Math.random() * w,
y: Math.random() * h,
radius: 1.2 + Math.random() * 3.5,
speed: 0.4 + Math.random() * 1.6,
windDrift: -0.3 + Math.random() * 0.6,
opacity: 0.5 + Math.random() * 0.5,
wobbleAmp: 0.3 + Math.random() * 1.2,
wobbleSpeed: 0.01 + Math.random() * 0.03,
wobbleOffset: Math.random() * Math.PI * 2,
});
}
}
}
// 初始化所有粒子
weatherTypes.forEach(wt => resetParticles(wt));
// ============ 绘制函数 ============
// --- 晴天 ---
function drawSunny(ctx, w, h, time, intensity) {
ctx.clearRect(0, 0, w, h);
const dpr = cardRects['canvasSunny']?.dpr || 2;
// 天空渐变
const skyGrad = ctx.createLinearGradient(0, 0, 0, h);
const topColor = [135, 195, 235]; // 浅蓝
const bottomColor = [245, 210, 140]; // 暖金
const tR = topColor[0] - intensity * 12;
const tG = topColor[1] - intensity * 10;
const tB = topColor[2] + intensity * 5;
const bR = bottomColor[0] + intensity * 8;
const bG = bottomColor[1] - intensity * 15;
const bB = bottomColor[2] - intensity * 20;
skyGrad.addColorStop(0, `rgb(${Math.max(80,tR)},${Math.max(130,tG)},${Math.min(255,tB)})`);
skyGrad.addColorStop(0.55, `rgb(${Math.max(160,bR)},${Math.max(170,bG)},${Math.max(100,bB)})`);
skyGrad.addColorStop(1, `rgb(${Math.max(200,bR+20)},${Math.max(185,bG+10)},${Math.max(120,bB+15)})`);
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, w, h);
// 太阳
const sunX = w * 0.72;
const sunY = h * 0.22;
const sunBaseRadius = 38 + intensity * 6;
const glowPulse = 1 + Math.sin(time * 0.0018) * 0.08 + Math.sin(time * 0.0027) * 0.05;
const sunRadius = sunBaseRadius * glowPulse;
// 外层大光晕
const outerGlow = ctx.createRadialGradient(sunX, sunY, sunRadius * 0.6, sunX, sunY, sunRadius * 3.5);
outerGlow.addColorStop(0, 'rgba(255,220,130,0.55)');
outerGlow.addColorStop(0.3, 'rgba(255,190,80,0.25)');
outerGlow.addColorStop(0.6, 'rgba(255,150,40,0.06)');
outerGlow.addColorStop(1, 'rgba(255,120,20,0)');
ctx.fillStyle = outerGlow;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius * 3.5, 0, Math.PI * 2);
ctx.fill();
// 中层光晕
const midGlow = ctx.createRadialGradient(sunX, sunY, sunRadius * 0.4, sunX, sunY, sunRadius * 2);
midGlow.addColorStop(0, 'rgba(255,240,180,0.8)');
midGlow.addColorStop(0.5, 'rgba(255,200,100,0.35)');
midGlow.addColorStop(1, 'rgba(255,160,50,0)');
ctx.fillStyle = midGlow;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius * 2, 0, Math.PI * 2);
ctx.fill();
// 太阳本体
const sunBody = ctx.createRadialGradient(sunX - sunRadius * 0.15, sunY - sunRadius * 0.15, sunRadius * 0.05,
sunX, sunY, sunRadius);
sunBody.addColorStop(0, 'rgba(255,255,245,1)');
sunBody.addColorStop(0.35, 'rgba(255,240,200,0.95)');
sunBody.addColorStop(0.7, 'rgba(255,200,100,0.7)');
sunBody.addColorStop(1, 'rgba(255,150,40,0)');
ctx.fillStyle = sunBody;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2);
ctx.fill();
// 光线
const rayCount = 16 + intensity * 4;
const rayMaxLen = sunRadius * 2.2;
for (let i = 0; i < rayCount; i++) {
const angle = (i / rayCount) * Math.PI * 2 + time * 0.0003;
const rayLen = rayMaxLen * (0.55 + 0.45 * Math.sin(time * 0.004 + i * 1.3));
const startR = sunRadius * 0.9;
const endR = startR + rayLen;
const sx = sunX + Math.cos(angle) * startR;
const sy = sunY + Math.sin(angle) * startR;
const ex = sunX + Math.cos(angle) * endR;
const ey = sunY + Math.sin(angle) * endR;
const rayGrad = ctx.createLinearGradient(sx, sy, ex, ey);
rayGrad.addColorStop(0, 'rgba(255,240,200,0.5)');
rayGrad.addColorStop(1, 'rgba(255,200,100,0)');
ctx.strokeStyle = rayGrad;
ctx.lineWidth = 1.5 + Math.random() * 0.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.stroke();
}
// 微粒子(阳光尘埃)
const data = particleData['sunny'];
if (data.particles.length < 25) {
data.particles.push({
x: Math.random() * w,
y: Math.random() * h,
radius: 0.6 + Math.random() * 1.8,
speed: 0.15 + Math.random() * 0.5,
opacity: 0.25 + Math.random() * 0.45,
phase: Math.random() * Math.PI * 2,
});
}
for (let p of data.particles) {
p.y -= p.speed;
p.x += Math.sin(time * 0.002 + p.phase) * 0.3;
if (p.y < -10) { p.y = h + 10;
p.x = Math.random() * w; }
if (p.x < -10) p.x = w + 10;
if (p.x > w + 10) p.x = -10;
ctx.fillStyle = `rgba(255,240,200,${p.opacity})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
}
// 限制粒子数
while (data.particles.length > 35) data.particles.shift();
}
// --- 大风 ---
function drawWindy(ctx, w, h, time, intensity) {
ctx.clearRect(0, 0, w, h);
const dpr = cardRects['canvasWindy']?.dpr || 2;
// 天空
const skyGrad = ctx.createLinearGradient(0, 0, 0, h);
skyGrad.addColorStop(0, 'rgb(110,130,155)');
skyGrad.addColorStop(0.5, 'rgb(145,160,175)');
skyGrad.addColorStop(1, 'rgb(170,180,190)');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, w, h);
const data = particleData['windy'];
const windMultiplier = 0.6 + intensity * 0.7;
// 风线
for (let wl of data.windLines) {
wl.x += wl.speed * windMultiplier;
if (wl.x > w + 40) { wl.x = -80;
wl.y = Math.random() * h; }
const grad = ctx.createLinearGradient(wl.x, wl.y, wl.x - wl.length, wl.y);
grad.addColorStop(0, `rgba(220,225,235,${wl.opacity})`);
grad.addColorStop(1, 'rgba(220,225,235,0)');
ctx.strokeStyle = grad;
ctx.lineWidth = wl.thickness;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(wl.x, wl.y);
ctx.lineTo(wl.x - wl.length, wl.y + Math.sin(time * 0.01 + wl.y * 0.05) * 3);
ctx.stroke();
}
// 云朵
for (let cloud of data.clouds) {
cloud.x += cloud.speed * windMultiplier;
if (cloud.x > w + 120) { cloud.x = -120;
cloud.y = 20 + Math.random() * h * 0.35; }
drawCloud(ctx, cloud.x, cloud.y, cloud.width, cloud.height, cloud.opacity);
}
// 摇曳的树
const treeBaseX = w * 0.35;
const treeBaseY = h * 0.78;
const swayAngle = Math.sin(time * 0.003) * (0.15 + intensity * 0.18) + Math.sin(time * 0.007) * 0.06;
drawTree(ctx, treeBaseX, treeBaseY, 55, swayAngle, intensity);
const tree2X = w * 0.62;
const tree2Y = h * 0.82;
const sway2 = Math.sin(time * 0.0035 + 1.5) * (0.12 + intensity * 0.16) + Math.sin(time * 0.006 + 2) * 0.05;
drawTree(ctx, tree2X, tree2Y, 42, sway2, intensity);
// 落叶
if (data.particles.length < 8 + intensity * 4) {
data.particles.push({
x: Math.random() * w,
y: -10 - Math.random() * 60,
radius: 2 + Math.random() * 3.5,
speedX: 1 + Math.random() * 3 * windMultiplier,
speedY: 0.6 + Math.random() * 1.5,
rotation: Math.random() * Math.PI * 2,
rotSpeed: 0.02 + Math.random() * 0.08,
opacity: 0.5 + Math.random() * 0.4,
color: ['#c49040', '#d4a050', '#b87830', '#e0b860'][Math.floor(Math.random() * 4)],
});
}
for (let p of data.particles) {
p.x += p.speedX;
p.y += p.speedY;
p.rotation += p.rotSpeed;
if (p.y > h + 20) { p.y = -20;
p.x = Math.random() * w; }
if (p.x > w + 30) p.x = -30;
if (p.x < -30) p.x = w + 30;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.fillStyle = p.color;
ctx.globalAlpha = p.opacity;
ctx.beginPath();
ctx.ellipse(0, 0, p.radius, p.radius * 0.55, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
while (data.particles.length > 16 + intensity * 6) data.particles.shift();
}
function drawCloud(ctx, x, y, w, h, opacity) {
ctx.fillStyle = `rgba(210,215,225,${opacity})`;
ctx.beginPath();
const cx = x + w / 2;
const cy = y + h / 2;
ctx.arc(cx - w * 0.25, cy, h * 0.5, 0, Math.PI * 2);
ctx.arc(cx + w * 0.1, cy - h * 0.15, h * 0.55, 0, Math.PI * 2);
ctx.arc(cx + w * 0.3, cy, h * 0.45, 0, Math.PI * 2);
ctx.arc(cx - w * 0.05, cy - h * 0.2, h * 0.5, 0, Math.PI * 2);
ctx.arc(cx + w * 0.18, cy + h * 0.05, h * 0.4, 0, Math.PI * 2);
ctx.fill();
}
function drawTree(ctx, baseX, baseY, height, swayAngle, intensity) {
const trunkHeight = height * 0.45;
const trunkTopX = baseX + Math.sin(swayAngle) * trunkHeight * 0.5;
const trunkTopY = baseY - trunkHeight;
// 树干
ctx.strokeStyle = '#8B7355';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(baseX, baseY);
ctx.lineTo(trunkTopX, trunkTopY);
ctx.stroke();
// 树枝和树冠
const crownCenterX = trunkTopX + Math.sin(swayAngle * 1.6) * height * 0.3;
const crownCenterY = trunkTopY - height * 0.3;
const crownRadius = height * 0.38;
// 多个圆形组成树冠
const crownParts = [
{ dx: 0, dy: 0, r: crownRadius },
{ dx: crownRadius * 0.5, dy: -crownRadius * 0.2, r: crownRadius * 0.75 },
{ dx: -crownRadius * 0.45, dy: -crownRadius * 0.1, r: crownRadius * 0.7 },
{ dx: crownRadius * 0.15, dy: -crownRadius * 0.55, r: crownRadius * 0.65 },
{ dx: -crownRadius * 0.15, dy: crownRadius * 0.3, r: crownRadius * 0.55 },
];
const greenBase = [80, 140, 70];
for (let part of crownParts) {
const shade = 0.75 + Math.random() * 0.25;
ctx.fillStyle =
`rgba(${Math.floor(greenBase[0]*shade)},${Math.floor(greenBase[1]*shade)},${Math.floor(greenBase[2]*shade)},0.85)`;
ctx.beginPath();
ctx.arc(crownCenterX + part.dx, crownCenterY + part.dy, part.r, 0, Math.PI * 2);
ctx.fill();
}
// 更亮的树冠高光
const highlightGrad = ctx.createRadialGradient(crownCenterX - crownRadius * 0.2, crownCenterY - crownRadius *
0.25, crownRadius * 0.15, crownCenterX, crownCenterY, crownRadius);
highlightGrad.addColorStop(0, 'rgba(160,210,130,0.45)');
highlightGrad.addColorStop(1, 'rgba(80,140,70,0)');
ctx.fillStyle = highlightGrad;
ctx.beginPath();
ctx.arc(crownCenterX, crownCenterY, crownRadius, 0, Math.PI * 2);
ctx.fill();
}
// --- 暴雨 ---
function drawRain(ctx, w, h, time, intensity) {
ctx.clearRect(0, 0, w, h);
const dpr = cardRects['canvasRain']?.dpr || 2;
// 暗沉天空
const skyGrad = ctx.createLinearGradient(0, 0, 0, h);
skyGrad.addColorStop(0, 'rgb(30,35,50)');
skyGrad.addColorStop(0.4, 'rgb(45,50,65)');
skyGrad.addColorStop(0.75, 'rgb(55,58,70)');
skyGrad.addColorStop(1, 'rgb(65,68,78)');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, w, h);
const data = particleData['rain'];
const dropMultiplier = 0.5 + intensity * 0.5;
const now = performance.now();
// 闪电
if (!data.lightningActive && now - data.lastLightning > 2500 - intensity * 600) {
if (Math.random() < 0.25 + intensity * 0.2) {
data.lightningActive = true;
data.lightningTimer = 0;
data.lightningAlpha = 1;
data.lastLightning = now;
data.lightningPath = generateLightningPath(w, h);
}
}
if (data.lightningActive) {
data.lightningTimer += 16;
if (data.lightningTimer < 60) {
data.lightningAlpha = 1;
} else if (data.lightningTimer < 180) {
data.lightningAlpha = Math.max(0, 1 - (data.lightningTimer - 60) / 120);
} else {
data.lightningActive = false;
data.lightningAlpha = 0;
}
// 绘制闪电闪光
if (data.lightningAlpha > 0.01) {
const flashGrad = ctx.createRadialGradient(w * 0.5, h * 0.35, 0, w * 0.5, h * 0.35, Math.max(w, h) * 0.8);
flashGrad.addColorStop(0, `rgba(255,255,240,${data.lightningAlpha*0.6})`);
flashGrad.addColorStop(0.3, `rgba(220,220,255,${data.lightningAlpha*0.3})`);
flashGrad.addColorStop(1, `rgba(200,200,240,0)`);
ctx.fillStyle = flashGrad;
ctx.fillRect(0, 0, w, h);
// 闪电线条
if (data.lightningPath && data.lightningAlpha > 0.3) {
ctx.strokeStyle = `rgba(255,255,240,${data.lightningAlpha})`;
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowColor = 'rgba(255,255,240,0.9)';
ctx.shadowBlur = 18;
ctx.beginPath();
ctx.moveTo(data.lightningPath[0].x, data.lightningPath[0].y);
for (let i = 1; i < data.lightningPath.length; i++) {
ctx.lineTo(data.lightningPath[i].x, data.lightningPath[i].y);
}
ctx.stroke();
ctx.shadowBlur = 0;
// 细分支
ctx.strokeStyle = `rgba(255,255,240,${data.lightningAlpha*0.55})`;
ctx.lineWidth = 1;
ctx.beginPath();
const mainPath = data.lightningPath;
const midIdx = Math.floor(mainPath.length / 2);
ctx.moveTo(mainPath[midIdx].x, mainPath[midIdx].y);
ctx.lineTo(mainPath[midIdx].x + 25, mainPath[midIdx].y + 30);
ctx.moveTo(mainPath[Math.floor(midIdx * 0.7)].x, mainPath[Math.floor(midIdx * 0.7)].y);
ctx.lineTo(mainPath[Math.floor(midIdx * 0.7)].x - 18, mainPath[Math.floor(midIdx * 0.7)].y + 22);
ctx.stroke();
}
}
}
// 雨滴
const targetDropCount = Math.floor((60 + intensity * 50) * dropMultiplier);
while (data.drops.length < targetDropCount) {
data.drops.push({
x: Math.random() * w * 1.15 - w * 0.075,
y: -Math.random() * h,
speed: 6 + Math.random() * 12 * dropMultiplier,
length: 10 + Math.random() * 24,
opacity: 0.3 + Math.random() * 0.55,
thickness: 1 + Math.random() * 2,
});
}
while (data.drops.length > targetDropCount + 10) data.drops.shift();
for (let drop of data.drops) {
drop.y += drop.speed;
if (drop.y > h + 20) {
drop.y = -15 - Math.random() * 30;
drop.x = Math.random() * w * 1.15 - w * 0.075;
// 溅起水花(水洼波纹)
if (drop.y > h - 60 && Math.random() < 0.25) {
const puddle = data.puddleRipples[Math.floor(Math.random() * data.puddleRipples.length)];
if (puddle) {
puddle.ripples.push({
radius: 2,
maxRadius: 14 + Math.random() * 24,
alpha: 0.7,
speed: 0.8 + Math.random() * 1.5,
});
}
}
}
ctx.strokeStyle = `rgba(180,195,220,${drop.opacity})`;
ctx.lineWidth = drop.thickness;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(drop.x, drop.y);
ctx.lineTo(drop.x - 1.5, drop.y + drop.length);
ctx.stroke();
}
// 水洼与波纹
for (let puddle of data.puddleRipples) {
// 水洼本体
const puddleGrad = ctx.createRadialGradient(puddle.cx, puddle.cy, puddle.radius * 0.3, puddle.cx, puddle.cy,
puddle.radius);
puddleGrad.addColorStop(0, 'rgba(140,155,180,0.5)');
puddleGrad.addColorStop(0.7, 'rgba(100,115,140,0.3)');
puddleGrad.addColorStop(1, 'rgba(80,95,120,0)');
ctx.fillStyle = puddleGrad;
ctx.beginPath();
ctx.ellipse(puddle.cx, puddle.cy, puddle.radius, puddle.radius * 0.45, 0, 0, Math.PI * 2);
ctx.fill();
// 波纹
for (let i = puddle.ripples.length - 1; i >= 0; i--) {
const rp = puddle.ripples[i];
rp.radius += rp.speed;
rp.alpha -= 0.025;
if (rp.alpha <= 0 || rp.radius > rp.maxRadius) {
puddle.ripples.splice(i, 1);
continue;
}
ctx.strokeStyle = `rgba(200,210,225,${rp.alpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(puddle.cx, puddle.cy, rp.radius, rp.radius * 0.45, 0, 0, Math.PI * 2);
ctx.stroke();
}
// 限制波纹数
while (puddle.ripples.length > 8) puddle.ripples.shift();
}
}
function generateLightningPath(w, h) {
const startX = w * 0.4 + Math.random() * w * 0.2;
const startY = h * 0.05;
const endX = w * 0.25 + Math.random() * w * 0.5;
const endY = h * 0.5 + Math.random() * h * 0.3;
const points = [{ x: startX, y: startY }];
const segments = 6 + Math.floor(Math.random() * 8);
const dx = (endX - startX) / segments;
const dy = (endY - startY) / segments;
for (let i = 1; i <= segments; i++) {
const jitterX = (Math.random() - 0.5) * w * 0.14;
const jitterY = (Math.random() - 0.5) * h * 0.08;
points.push({
x: startX + dx * i + jitterX,
y: startY + dy * i + jitterY,
});
}
points.push({ x: endX, y: endY });
return points;
}
// --- 暴雪 ---
function drawSnow(ctx, w, h, time, intensity) {
ctx.clearRect(0, 0, w, h);
const dpr = cardRects['canvasSnow']?.dpr || 2;
// 冷色调天空
const skyGrad = ctx.createLinearGradient(0, 0, 0, h);
skyGrad.addColorStop(0, 'rgb(140,155,175)');
skyGrad.addColorStop(0.5, 'rgb(160,172,190)');
skyGrad.addColorStop(1, 'rgb(185,195,205)');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, w, h);
const data = particleData['snow'];
const snowMultiplier = 0.5 + intensity * 0.5;
const targetFlakeCount = Math.floor((70 + intensity * 60) * snowMultiplier);
while (data.flakes.length < targetFlakeCount) {
data.flakes.push({
x: Math.random() * w,
y: -Math.random() * h * 0.6,
radius: 1.2 + Math.random() * 3.5,
speed: 0.4 + Math.random() * 1.6 * snowMultiplier,
windDrift: -0.3 + Math.random() * 0.6 + (intensity - 2) * 0.3,
opacity: 0.5 + Math.random() * 0.5,
wobbleAmp: 0.3 + Math.random() * 1.4,
wobbleSpeed: 0.01 + Math.random() * 0.03,
wobbleOffset: Math.random() * Math.PI * 2,
});
}
while (data.flakes.length > targetFlakeCount + 15) data.flakes.shift();
for (let flake of data.flakes) {
flake.y += flake.speed;
flake.x += flake.windDrift + Math.sin(time * flake.wobbleSpeed + flake.wobbleOffset) * flake.wobbleAmp;
if (flake.y > h + 8) {
flake.y = -8 - Math.random() * 20;
flake.x = Math.random() * w;
// 积雪增长
data.snowAccum += 0.015 * snowMultiplier;
}
if (flake.x > w + 10) flake.x = -10;
if (flake.x < -10) flake.x = w + 10;
ctx.fillStyle = `rgba(240,245,255,${flake.opacity})`;
ctx.beginPath();
ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
ctx.fill();
}
// 积雪堆积
const maxAccum = 18 + intensity * 22;
data.snowAccum = Math.min(data.snowAccum, maxAccum);
if (data.snowAccum > 0.3) {
const accumHeight = data.snowAccum;
// 起伏的积雪
ctx.fillStyle = 'rgba(235,240,248,0.85)';
ctx.beginPath();
ctx.moveTo(0, h);
for (let x = 0; x <= w; x += 8) {
const yOffset = Math.sin(x * 0.04 + time * 0.0004) * 5 + Math.sin(x * 0.09) * 3;
ctx.lineTo(x, h - accumHeight + yOffset);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
// 积雪高光
const snowHighlight = ctx.createLinearGradient(0, h - accumHeight - 6, 0, h);
snowHighlight.addColorStop(0, 'rgba(255,255,255,0.55)');
snowHighlight.addColorStop(0.4, 'rgba(240,245,255,0.3)');
snowHighlight.addColorStop(1, 'rgba(220,230,240,0)');
ctx.fillStyle = snowHighlight;
ctx.beginPath();
ctx.moveTo(0, h);
for (let x = 0; x <= w; x += 8) {
const yOffset = Math.sin(x * 0.04 + time * 0.0004) * 5 + Math.sin(x * 0.09) * 3;
ctx.lineTo(x, h - accumHeight + yOffset);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
}
// 重置积雪(定期融化一些)
if (data.snowAccum > maxAccum * 0.9) {
data.snowAccum *= 0.998;
}
}
// ============ 动画循环 ============
let animationId;
let lastTime = performance.now();
function animate(timestamp) {
const dt = Math.min(timestamp - lastTime, 50);
lastTime = timestamp;
weatherTypes.forEach((wt, i) => {
const canvasId = canvasIds[i];
const ctx = contexts[canvasId];
const rect = cardRects[canvasId];
if (!ctx || !rect || rect.width <= 0 || rect.height <= 0) return;
const w = rect.width;
const h = rect.height;
ctx.save();
// 根据天气类型绘制
switch (wt) {
case 'sunny':
drawSunny(ctx, w, h, timestamp, globalIntensity);
break;
case 'windy':
drawWindy(ctx, w, h, timestamp, globalIntensity);
break;
case 'rain':
drawRain(ctx, w, h, timestamp, globalIntensity);
break;
case 'snow':
drawSnow(ctx, w, h, timestamp, globalIntensity);
break;
}
ctx.restore();
});
animationId = requestAnimationFrame(animate);
}
// ============ 强度切换 ============
function setIntensity(level) {
globalIntensity = level;
// 更新按钮状态
intensityButtons.forEach(btn => {
const btnLevel = parseInt(btn.dataset.level);
if (btnLevel === level) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// 重置粒子系统以适应新强度
weatherTypes.forEach(wt => {
resetParticles(wt);
});
// 更新卡片描述文字
updateCardDescriptions(level);
}
function updateCardDescriptions(level) {
const overlays = document.querySelectorAll('.card-overlay .desc');
const sunnyDescs = ['晴空柔和', '晴空万里', '烈日当空'];
const windyDescs = ['微风轻拂', '大风呼啸', '狂风怒号'];
const rainDescs = ['细雨绵绵', '暴雨倾盆', '特大暴雨'];
const snowDescs = ['小雪飘落', '暴雪纷飞', '暴风雪肆虐'];
const allDescs = [sunnyDescs, windyDescs, rainDescs, snowDescs];
const tempAdjust = [
[27, 28, 30],
[20, 19, 17],
[16, 14, 11],
[-5, -8, -13]
];
overlays.forEach((descEl, i) => {
if (allDescs[i]) {
descEl.textContent = allDescs[i][level - 1] || allDescs[i][1];
}
});
// 更新温度
const tempEls = document.querySelectorAll('.card-overlay .temp');
tempEls.forEach((tempEl, i) => {
if (tempAdjust[i]) {
const newTemp = tempAdjust[i][level - 1] || tempAdjust[i][1];
tempEl.textContent = newTemp + '°';
}
});
}
intensityButtons.forEach(btn => {
btn.addEventListener('click', () => {
const level = parseInt(btn.dataset.level);
if (level === globalIntensity) return;
setIntensity(level);
// 按钮微动效
btn.style.transform = 'scale(0.9)';
setTimeout(() => { btn.style.transform = ''; }, 150);
});
});
// ============ 卡片点击选中 ============
cardElements.forEach(card => {
card.addEventListener('click', () => {
const index = parseInt(card.dataset.index);
if (selectedCardIndex === index) {
// 取消选中
card.classList.remove('selected');
selectedCardIndex = -1;
} else {
// 取消其他选中
cardElements.forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedCardIndex = index;
}
});
});
// ============ 窗口大小调整 ============
let resizeTimeout;
function handleResize() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
updateAllCanvasSizes();
weatherTypes.forEach(wt => resetParticles(wt));
}, 300);
}
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', () => {
setTimeout(handleResize, 500);
});
// ============ 启动 ============
function startAnimation() {
// 确保所有canvas尺寸正确
updateAllCanvasSizes();
weatherTypes.forEach(wt => resetParticles(wt));
updateCardDescriptions(globalIntensity);
// 设置初始活跃按钮
intensityButtons.forEach(btn => {
if (parseInt(btn.dataset.level) === globalIntensity) {
btn.classList.add('active');
}
});
// 启动动画循环
if (animationId) cancelAnimationFrame(animationId);
lastTime = performance.now();
animationId = requestAnimationFrame(animate);
}
// 页面加载完成后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startAnimation);
} else {
startAnimation();
}
// ============ 键盘导航 ============
document.addEventListener('keydown', (e) => {
if (e.key === '1' || e.key === '2' || e.key === '3') {
const level = parseInt(e.key);
setIntensity(level);
}
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
const dir = e.key === 'ArrowRight' ? 1 : -1;
let newIndex = selectedCardIndex + dir;
if (newIndex < 0) newIndex = cardElements.length - 1;
if (newIndex >= cardElements.length) newIndex = 0;
cardElements.forEach(c => c.classList.remove('selected'));
if (newIndex >= 0 && newIndex < cardElements.length) {
cardElements[newIndex].classList.add('selected');
cardElements[newIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
selectedCardIndex = newIndex;
}
}
if (e.key === 'Escape') {
cardElements.forEach(c => c.classList.remove('selected'));
selectedCardIndex = -1;
}
});
console.log('%c🌤 天气卡片已就绪 %c| %c强度等级: ' + globalIntensity +
' %c| %c键盘快捷键: 1-3切换强度, ←→切换卡片, Esc取消选中',
'font-size:1.1em;', '', 'font-weight:bold;color:#ffcc80;', '',
'color:rgba(255,255,255,0.5);');
console.log('%c💡 点击卡片可选中高亮,底部按钮切换天气强度', 'color:rgba(255,255,255,0.6);font-style:italic;');
})();
</script>
</body>
</html>
用同样的提示词跑了下GPT5.5做个对比,它的 thinking 思考进阶程度: