DeepSeek v4 pro天气卡片测试来了,结果有点意外(新增GPT5.5对比)
- 内容介绍
- 文章标签
- 相关推荐
跑了五分钟才生成完,可能现在并发高,有点慢
提示词:
你是 Apple Inc 的顶级 UI 设计师,以 iOS 18 的设计风格(毛玻璃效果、高斯模糊、动态渐变、细腻阴影)创建一个单个HTML文件(包含完整CSS和JavaScript)。实现横板天气页面,包含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的对比,5.5 thinking 思考程度进阶:
GPT 5.5 天气卡片测试1920×1064 220 KB
--【壹】--:
的确是差了一点点点,等晚上的时候再测试一下?
--【贰】--:
突破了历史最差纪录,没看过这么差的 ,确定测试没问题吗
--【叁】--:
感觉还行啊,https://chat.deepseek.com/share/e84p9vfyxnl20oposn 还是快速模式
--【肆】--:
官方说了,官网上就是 V4。只是不知道官网上的 Pro think 模式 是不是 Pro-max。
--【伍】--:
我在官网专家模式,开 think 测试的。看文档描述,专家模式就是 Pro。
不知道用 API 测会不会好一点。
--【陆】--:
你这个看起来还不错,看来我的抽卡失败了。
--【柒】--:
v4不是原生多模态模型,可能审美这块确实差点,感觉明显有差距的
--【捌】--:
有点抽象,等了这么多天不会拉依托大的吧,希望只是偶发现象
--【玖】--:
感觉审美这一块确实差一点,也可能抽卡没抽好。
--【拾】--:
这坐标都不对认真的吗,太阳的建模倒是不错
--【拾壹】--:
已经更换了录屏动图,貌似就是这样设计的。
--【拾贰】--:
有一说一吧差点意思 不过还是很期待在cc里使用ds编程的
--【拾叁】--:
啊,这是认真的吗?是图片有问题还是特意如此设计啊?
image1920×1248 184 KB
--【拾肆】--:
V4还没上线官网啊,只有API有,目前官网用的还是3.2
--【拾伍】--:
同样的提示词,我的很好看啊
image1920×1063 134 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather – iOS 18</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--bg: #0a0a0f;
--card-bg: rgba(28, 28, 32, 0.72);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.56);
--text-tertiary: rgba(255, 255, 255, 0.36);
--sun-glow: #ff9f43;
--rain-blue: #74b9ff;
--snow-white: #dfe6e9;
--wind-teal: #55efc4;
--lightning: #ffeaa7;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* Dynamic ambient background */
.ambient-bg {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
.ambient-bg .orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0;
transition: opacity 1.2s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.orb-sun { width: 460px; height: 460px; background: radial-gradient(circle, #ff9f43 0%, transparent 70%); top: -160px; left: 2%; }
.orb-wind { width: 400px; height: 400px; background: radial-gradient(circle, #55efc4 0%, transparent 70%); top: 40px; left: 22%; }
.orb-rain { width: 420px; height: 420px; background: radial-gradient(circle, #74b9ff 0%, transparent 70%); top: -100px; left: 46%; }
.orb-snow { width: 440px; height: 440px; background: radial-gradient(circle, #a29bfe 0%, transparent 70%); top: -60px; left: 68%; }
.orb.active { opacity: 0.28; }
/* Main container */
.container {
position: relative;
z-index: 1;
width: 96vw;
max-width: 1500px;
display: flex;
gap: 18px;
padding: 20px 0;
}
/* Weather card */
.weather-card {
flex: 1;
min-width: 0;
height: 520px;
background: var(--card-bg);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--card-border);
border-radius: 32px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.55s cubic-bezier(0.25, 0.1, 0.25, 1);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.24),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 28px 24px 24px;
}
.weather-card:hover {
transform: translateY(-4px);
border-color: rgba(255, 255, 255, 0.16);
box-shadow:
0 20px 48px rgba(0, 0, 0, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.weather-card:active {
transform: scale(0.985);
transition: all 0.15s cubic-bezier(0.25, 0.1, 0.25, 1);
}
/* Card header */
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
z-index: 2;
position: relative;
}
.card-label {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-tertiary);
}
.card-icon {
font-size: 22px;
opacity: 0.7;
}
/* Card footer */
.card-footer {
z-index: 2;
position: relative;
}
.card-temp {
font-size: 52px;
font-weight: 300;
color: var(--text-primary);
line-height: 1;
letter-spacing: -0.02em;
}
.card-desc {
font-size: 15px;
font-weight: 500;
color: var(--text-secondary);
margin-top: 6px;
}
.card-detail {
display: flex;
gap: 16px;
margin-top: 12px;
}
.detail-item {
font-size: 12px;
font-weight: 500;
color: var(--text-tertiary);
display: flex;
align-items: center;
gap: 4px;
}
/* Canvas for animations */
.card-canvas {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
/* Toggle buttons */
.controls {
position: relative;
z-index: 2;
display: flex;
gap: 10px;
margin-bottom: 4px;
}
.ctrl-btn {
padding: 10px 20px;
border-radius: 100px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(28, 28, 32, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: var(--text-secondary);
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
letter-spacing: 0.02em;
}
.ctrl-btn:hover {
background: rgba(44, 44, 50, 0.8);
color: var(--text-primary);
border-color: rgba(255, 255, 255, 0.2);
}
.ctrl-btn.active {
background: rgba(255, 255, 255, 0.12);
color: var(--text-primary);
border-color: rgba(255, 255, 255, 0.24);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
}
/* Lightning flash overlay */
.lightning-flash {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.06);
opacity: 0;
pointer-events: none;
z-index: 0;
transition: opacity 0.08s;
border-radius: 32px;
}
.lightning-flash.strike { opacity: 1; }
@media (max-width: 960px) {
.container {
flex-wrap: wrap;
}
.weather-card {
flex: 1 1 calc(50% - 9px);
min-width: 240px;
height: 380px;
}
}
@media (max-width: 560px) {
.weather-card {
flex: 1 1 100%;
height: 340px;
}
.card-temp { font-size: 40px; }
}
</style>
</head>
<body>
<div class="ambient-bg">
<div class="orb orb-sun active" id="orb-sun"></div>
<div class="orb orb-wind active" id="orb-wind"></div>
<div class="orb orb-rain active" id="orb-rain"></div>
<div class="orb orb-snow active" id="orb-snow"></div>
</div>
<div class="controls">
<button class="ctrl-btn active" data-mode="all">All Active</button>
<button class="ctrl-btn" data-mode="sun">Sunny</button>
<button class="ctrl-btn" data-mode="wind">Windy</button>
<button class="ctrl-btn" data-mode="rain">Rainstorm</button>
<button class="ctrl-btn" data-mode="snow">Blizzard</button>
</div>
<div class="container">
<!-- SUNNY -->
<div class="weather-card" data-weather="sunny">
<div class="card-header">
<span class="card-label">Sunny</span>
<span class="card-icon">☀️</span>
</div>
<canvas class="card-canvas" id="canvas-sunny"></canvas>
<div class="card-footer">
<div class="card-temp">28°</div>
<div class="card-desc">Clear Sky</div>
<div class="card-detail">
<span class="detail-item">UV 6</span>
<span class="detail-item">H 32°</span>
<span class="detail-item">L 21°</span>
</div>
</div>
</div>
<!-- WINDY -->
<div class="weather-card" data-weather="windy">
<div class="card-header">
<span class="card-label">Windy</span>
<span class="card-icon">💨</span>
</div>
<canvas class="card-canvas" id="canvas-windy"></canvas>
<div class="card-footer">
<div class="card-temp">18°</div>
<div class="card-desc">Gusty Winds</div>
<div class="card-detail">
<span class="detail-item">32 km/h</span>
<span class="detail-item">Gust 48</span>
</div>
</div>
</div>
<!-- RAIN -->
<div class="weather-card" data-weather="rainy">
<div class="card-header">
<span class="card-label">Rainstorm</span>
<span class="card-icon">⛈️</span>
</div>
<canvas class="card-canvas" id="canvas-rainy"></canvas>
<div class="lightning-flash" id="flash-rainy"></div>
<div class="card-footer">
<div class="card-temp">16°</div>
<div class="card-desc">Heavy Rain</div>
<div class="card-detail">
<span class="detail-item">95%</span>
<span class="detail-item">24 mm</span>
</div>
</div>
</div>
<!-- SNOW -->
<div class="weather-card" data-weather="snowy">
<div class="card-header">
<span class="card-label">Blizzard</span>
<span class="card-icon">❄️</span>
</div>
<canvas class="card-canvas" id="canvas-snowy"></canvas>
<div class="card-footer">
<div class="card-temp">-8°</div>
<div class="card-desc">Heavy Snow</div>
<div class="card-detail">
<span class="detail-item">18 cm</span>
<span class="detail-item">-12°</span>
</div>
</div>
</div>
</div>
<script>
(function() {
// ── Canvas setup ──────────────────────────────────────────────
function setupCanvas(id) {
const canvas = document.getElementById(id);
const card = canvas.closest('.weather-card');
const dpr = Math.min(window.devicePixelRatio || 1, 2);
function resize() {
const r = card.getBoundingClientRect();
canvas.width = r.width * dpr;
canvas.height = r.height * dpr;
canvas.style.width = r.width + 'px';
canvas.style.height = r.height + 'px';
}
resize();
window.addEventListener('resize', resize);
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return { canvas, ctx, getW: () => canvas.width / dpr, getH: () => canvas.height / dpr, resize };
}
// ── SUNNY ────────────────────────────────────────────────────
function animateSunny() {
const { ctx, getW, getH } = setupCanvas('canvas-sunny');
const particles = [];
const RAY_COUNT = 18;
for (let i = 0; i < RAY_COUNT; i++) {
particles.push({
angle: (i / RAY_COUNT) * Math.PI * 2,
length: 50 + Math.random() * 60,
speed: 0.002 + Math.random() * 0.006,
width: 1 + Math.random() * 1.8,
offset: Math.random() * Math.PI * 2
});
}
// Floating dust motes
const motes = [];
for (let i = 0; i < 30; i++) {
motes.push({
x: Math.random(), y: Math.random(),
size: 0.5 + Math.random() * 2,
speed: 0.0003 + Math.random() * 0.001,
drift: (Math.random() - 0.5) * 0.3,
opacity: 0.2 + Math.random() * 0.5
});
}
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
const cx = w * 0.46, cy = h * 0.38;
// Outer glow — pulsating
const glowPulse = 1 + Math.sin(t * 0.001) * 0.15;
const glow = ctx.createRadialGradient(cx, cy, 20, cx, cy, 120 * glowPulse);
glow.addColorStop(0, 'rgba(255, 170, 50, 0.28)');
glow.addColorStop(0.5, 'rgba(255, 140, 30, 0.08)');
glow.addColorStop(1, 'rgba(255, 100, 20, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(cx, cy, 120 * glowPulse, 0, Math.PI * 2);
ctx.fill();
// Medium glow
const glow2 = ctx.createRadialGradient(cx, cy, 14, cx, cy, 55);
glow2.addColorStop(0, 'rgba(255, 220, 140, 0.45)');
glow2.addColorStop(0.7, 'rgba(255, 160, 50, 0.08)');
glow2.addColorStop(1, 'rgba(255, 120, 30, 0)');
ctx.fillStyle = glow2;
ctx.beginPath();
ctx.arc(cx, cy, 55, 0, Math.PI * 2);
ctx.fill();
// Sun body
const sunGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 28);
sunGrad.addColorStop(0, 'rgba(255, 250, 210, 1)');
sunGrad.addColorStop(0.3, 'rgba(255, 210, 80, 0.95)');
sunGrad.addColorStop(0.7, 'rgba(255, 160, 30, 0.5)');
sunGrad.addColorStop(1, 'rgba(255, 120, 20, 0)');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(cx, cy, 28, 0, Math.PI * 2);
ctx.fill();
// Rays
particles.forEach(p => {
const ang = p.angle + Math.sin(t * 0.0008 + p.offset) * 0.12;
const len = p.length * (0.8 + Math.sin(t * p.speed + p.offset) * 0.25);
const x1 = cx + Math.cos(ang) * 32;
const y1 = cy + Math.sin(ang) * 32;
const x2 = cx + Math.cos(ang) * (32 + len);
const y2 = cy + Math.sin(ang) * (32 + len);
const rayGrad = ctx.createLinearGradient(x1, y1, x2, y2);
rayGrad.addColorStop(0, 'rgba(255, 210, 100, 0.7)');
rayGrad.addColorStop(1, 'rgba(255, 160, 60, 0)');
ctx.strokeStyle = rayGrad;
ctx.lineWidth = p.width;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
});
// Dust motes
motes.forEach(m => {
m.y -= m.speed;
m.x += m.drift * 0.1;
if (m.y < 0) { m.y = 1; m.x = Math.random(); }
if (m.x > 1) m.x = 0;
if (m.x < 0) m.x = 1;
ctx.fillStyle = `rgba(255,220,150,${m.opacity})`;
ctx.beginPath();
ctx.arc(m.x * w, m.y * h, m.size, 0, Math.PI * 2);
ctx.fill();
});
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── WINDY ────────────────────────────────────────────────────
function animateWindy() {
const { ctx, getW, getH } = setupCanvas('canvas-windy');
const clouds = [];
for (let i = 0; i < 5; i++) {
clouds.push({
x: Math.random() * 1.2 - 0.1,
y: 0.08 + Math.random() * 0.35,
speed: 0.0003 + Math.random() * 0.0008,
scale: 0.6 + Math.random() * 0.9,
opacity: 0.3 + Math.random() * 0.4
});
}
const windLines = [];
for (let i = 0; i < 18; i++) {
windLines.push({
x: Math.random(),
y: 0.15 + Math.random() * 0.65,
len: 20 + Math.random() * 50,
speed: 0.002 + Math.random() * 0.008,
opacity: 0.1 + Math.random() * 0.35,
phase: Math.random() * Math.PI * 2
});
}
// Tree data — approximate a simple pine
const treeX = 0.62, treeBaseY = 0.78;
const branches = [];
for (let i = 0; i < 8; i++) {
branches.push({
yOff: 0.04 + i * 0.06,
swayAmp: (0.03 + i * 0.018),
swaySpeed: 0.002 + Math.random() * 0.003,
phase: Math.random() * Math.PI * 2
});
}
// Leaves blowing
const leaves = [];
for (let i = 0; i < 12; i++) {
leaves.push({
x: Math.random(),
y: Math.random(),
size: 1.5 + Math.random() * 3,
speedX: 0.004 + Math.random() * 0.012,
speedY: -0.002 + Math.random() * 0.004,
rot: Math.random() * Math.PI * 2,
rotSpeed: (Math.random() - 0.5) * 0.08,
opacity: 0.2 + Math.random() * 0.4
});
}
function drawCloud(ctx, x, y, scale, opacity) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.fillStyle = `rgba(200,220,240,${opacity})`;
ctx.beginPath();
ctx.arc(0, 0, 22, 0, Math.PI * 2);
ctx.arc(20, -6, 18, 0, Math.PI * 2);
ctx.arc(36, 2, 20, 0, Math.PI * 2);
ctx.arc(16, 8, 16, 0, Math.PI * 2);
ctx.arc(-14, 4, 14, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
// Clouds
clouds.forEach(c => {
c.x += c.speed;
if (c.x > 1.2) c.x = -0.2;
drawCloud(ctx, c.x * w, c.y * h, c.scale, c.opacity);
});
// Wind lines
ctx.lineCap = 'round';
windLines.forEach(l => {
l.x += l.speed;
if (l.x > 1.05) l.x = -0.05;
const alpha = l.opacity * (0.6 + 0.4 * Math.sin(t * 0.003 + l.phase));
const sx = l.x * w;
const sy = l.y * h;
ctx.strokeStyle = `rgba(180,210,240,${alpha})`;
ctx.lineWidth = 0.8 + Math.random() * 0.4;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx - l.len * (0.7 + 0.3 * Math.sin(t * 0.005 + l.phase)), sy + (Math.sin(t * 0.004 + l.phase) * 6));
ctx.stroke();
});
// Tree trunk
const tx = treeX * w, tbY = treeBaseY * h;
ctx.fillStyle = 'rgba(120,90,60,0.45)';
ctx.fillRect(tx - 4, tbY - 55, 8, 55);
// Tree branches (pine layers)
branches.forEach(b => {
const sway = Math.sin(t * b.swaySpeed + b.phase) * b.swayAmp * w;
const by = tbY - b.yOff * h;
const layerWidth = (0.12 - b.yOff * 0.08) * w;
ctx.fillStyle = 'rgba(60,140,80,0.5)';
ctx.beginPath();
ctx.moveTo(tx + sway, by - layerWidth * 0.55);
ctx.lineTo(tx + sway + layerWidth * 0.5, by + layerWidth * 0.25);
ctx.lineTo(tx + sway - layerWidth * 0.5, by + layerWidth * 0.25);
ctx.closePath();
ctx.fill();
});
// Ground
ctx.fillStyle = 'rgba(80,130,70,0.18)';
ctx.fillRect(0, tbY, w, h - tbY);
// Leaves
leaves.forEach(l => {
l.x += l.speedX;
l.y += l.speedY;
l.rot += l.rotSpeed;
if (l.x > 1.1) { l.x = -0.1; l.y = Math.random(); }
if (l.y < 0) l.y = 1;
if (l.y > 1) l.y = 0;
ctx.save();
ctx.translate(l.x * w, l.y * h);
ctx.rotate(l.rot);
ctx.fillStyle = `rgba(140,190,80,${l.opacity})`;
ctx.beginPath();
ctx.ellipse(0, 0, l.size, l.size * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── RAIN ─────────────────────────────────────────────────────
function animateRainy() {
const { ctx, getW, getH } = setupCanvas('canvas-rainy');
const flashEl = document.getElementById('flash-rainy');
const drops = [];
for (let i = 0; i < 140; i++) {
drops.push({
x: Math.random(),
y: Math.random(),
speed: 0.006 + Math.random() * 0.018,
len: 8 + Math.random() * 18,
opacity: 0.15 + Math.random() * 0.5,
width: 0.6 + Math.random() * 1.2
});
}
// Puddle ripples
const ripples = [];
for (let i = 0; i < 6; i++) {
ripples.push({
x: 0.15 + Math.random() * 0.7,
y: 0.72 + Math.random() * 0.18,
radius: 0,
maxRadius: 8 + Math.random() * 14,
speed: 0.3 + Math.random() * 0.6,
opacity: 0,
delay: Math.random() * 3000,
lastSpawn: 0
});
}
// Lightning timer
let nextLightning = 2000 + Math.random() * 4000;
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
// Dark storm clouds at top
const cloudGrad = ctx.createLinearGradient(0, 0, 0, h * 0.35);
cloudGrad.addColorStop(0, 'rgba(30,35,50,0.7)');
cloudGrad.addColorStop(0.5, 'rgba(40,48,65,0.4)');
cloudGrad.addColorStop(1, 'rgba(40,48,65,0)');
ctx.fillStyle = cloudGrad;
ctx.fillRect(0, 0, w, h * 0.35);
// Cloud bumps
ctx.fillStyle = 'rgba(35,40,58,0.6)';
const bumpYs = [-8, 4, -2, 10, -6, 2, 8];
for (let i = 0; i < 7; i++) {
ctx.beginPath();
ctx.arc(w * (i / 6.5), bumpYs[i], w * 0.14, 0, Math.PI * 2);
ctx.fill();
}
// Rain drops
ctx.lineCap = 'round';
drops.forEach(d => {
d.y += d.speed;
if (d.y > 1.02) { d.y = -0.05; d.x = Math.random(); }
const sx = d.x * w;
const sy = d.y * h;
const grad = ctx.createLinearGradient(sx, sy, sx - 2, sy + d.len);
grad.addColorStop(0, `rgba(160,200,255,${d.opacity})`);
grad.addColorStop(1, `rgba(120,170,240,${d.opacity * 0.3})`);
ctx.strokeStyle = grad;
ctx.lineWidth = d.width;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx - 2, sy + d.len);
ctx.stroke();
});
// Puddles / ground
const puddleY = h * 0.78;
ctx.fillStyle = 'rgba(30,50,80,0.25)';
ctx.fillRect(0, puddleY, w, h - puddleY);
// Ripples
ripples.forEach(r => {
if (t - r.lastSpawn > r.delay) {
r.radius = 0;
r.opacity = 0.5;
r.lastSpawn = t;
r.delay = 800 + Math.random() * 2500;
r.x = 0.1 + Math.random() * 0.8;
r.y = 0.76 + Math.random() * 0.16;
r.maxRadius = 6 + Math.random() * 16;
}
if (r.opacity > 0.001) {
r.radius += r.speed;
r.opacity -= 0.015;
ctx.strokeStyle = `rgba(140,190,240,${Math.max(0, r.opacity)})`;
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.arc(r.x * w, r.y * h, r.radius, 0, Math.PI * 2);
ctx.stroke();
}
});
// Lightning
if (t > nextLightning) {
const lx = w * (0.25 + Math.random() * 0.5);
const ly = h * 0.02;
ctx.strokeStyle = 'rgba(255,245,200,0.8)';
ctx.lineWidth = 2;
ctx.shadowColor = 'rgba(255,240,180,0.6)';
ctx.shadowBlur = 18;
let x = lx, y = ly;
ctx.beginPath();
ctx.moveTo(x, y);
for (let i = 0; i < 5; i++) {
x += (Math.random() - 0.5) * 40;
y += h * 0.12 + Math.random() * h * 0.08;
ctx.lineTo(x, y);
}
ctx.stroke();
ctx.shadowBlur = 0;
// Flash
flashEl.classList.add('strike');
setTimeout(() => flashEl.classList.remove('strike'), 120);
nextLightning = t + 2000 + Math.random() * 5000;
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── SNOW ─────────────────────────────────────────────────────
function animateSnowy() {
const { ctx, getW, getH } = setupCanvas('canvas-snowy');
const flakes = [];
for (let i = 0; i < 100; i++) {
flakes.push({
x: Math.random(),
y: Math.random(),
size: 1 + Math.random() * 3.5,
speed: 0.0003 + Math.random() * 0.0015,
drift: (Math.random() - 0.5) * 0.8,
sway: Math.random() * Math.PI * 2,
swaySpeed: 0.01 + Math.random() * 0.03,
opacity: 0.3 + Math.random() * 0.55
});
}
// Snow accumulation on bottom
const accumHeights = [];
const segments = 50;
for (let i = 0; i < segments; i++) accumHeights.push(8 + Math.random() * 5);
function drawSnowflake(ctx, x, y, size, opacity) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = `rgba(230,240,255,${opacity})`;
// Six-pointed snowflake simplified
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate((i / 6) * Math.PI * 2);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -size);
ctx.lineWidth = Math.max(0.5, size * 0.3);
ctx.strokeStyle = `rgba(230,240,255,${opacity})`;
ctx.stroke();
// Small branch
ctx.beginPath();
ctx.moveTo(0, -size * 0.5);
ctx.lineTo(size * 0.35, -size * 0.7);
ctx.moveTo(0, -size * 0.5);
ctx.lineTo(-size * 0.35, -size * 0.7);
ctx.stroke();
ctx.restore();
}
// Center dot
ctx.fillStyle = `rgba(240,245,255,${Math.min(1, opacity + 0.2)})`;
ctx.beginPath();
ctx.arc(0, 0, size * 0.22, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
// Soft top gradient
const topGrad = ctx.createLinearGradient(0, 0, 0, h * 0.25);
topGrad.addColorStop(0, 'rgba(25,28,40,0.5)');
topGrad.addColorStop(1, 'rgba(25,28,40,0)');
ctx.fillStyle = topGrad;
ctx.fillRect(0, 0, w, h * 0.25);
// Snowflakes
flakes.forEach(f => {
f.y += f.speed;
f.sway += f.swaySpeed;
f.x += Math.sin(f.sway) * f.drift * 0.002;
if (f.y > 1.02) { f.y = -0.02; f.x = Math.random(); }
if (f.x > 1.02) f.x = -0.02;
if (f.x < -0.02) f.x = 1.02;
drawSnowflake(ctx, f.x * w, f.y * h, f.size, f.opacity);
});
// Snow accumulation at bottom
const baseY = h * 0.82;
ctx.fillStyle = 'rgba(210,225,245,0.35)';
ctx.beginPath();
ctx.moveTo(0, h);
for (let i = 0; i <= segments; i++) {
const x = (i / segments) * w;
const idx = i % accumHeights.length;
accumHeights[idx] += 0.02;
if (accumHeights[idx] > 28) accumHeights[idx] = 28;
const bump = Math.sin(i * 0.6 + t * 0.0005) * 4 + accumHeights[idx];
ctx.lineTo(x, baseY + bump);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
// Subtle mist
const mistGrad = ctx.createLinearGradient(0, baseY - 20, 0, baseY + 10);
mistGrad.addColorStop(0, 'rgba(200,215,235,0)');
mistGrad.addColorStop(0.5, 'rgba(200,215,235,0.12)');
mistGrad.addColorStop(1, 'rgba(200,215,235,0)');
ctx.fillStyle = mistGrad;
ctx.fillRect(0, baseY - 20, w, 30);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── Start all animations ─────────────────────────────────────
animateSunny();
animateWindy();
animateRainy();
animateSnowy();
// ── Mode switching ───────────────────────────────────────────
const cards = document.querySelectorAll('.weather-card');
const orbs = {
sunny: document.getElementById('orb-sun'),
windy: document.getElementById('orb-wind'),
rainy: document.getElementById('orb-rainy'),
snowy: document.getElementById('orb-snow'),
};
const btns = document.querySelectorAll('.ctrl-btn');
function showAll() {
cards.forEach(c => {
c.style.opacity = '1';
c.style.transform = '';
c.style.pointerEvents = 'auto';
c.style.filter = 'none';
});
Object.values(orbs).forEach(o => o.classList.add('active'));
}
function showOnly(weather) {
Object.entries(orbs).forEach(([key, orb]) => {
if (key === weather) orb.classList.add('active');
else orb.classList.remove('active');
});
cards.forEach(c => {
if (c.dataset.weather === weather) {
c.style.opacity = '1';
c.style.transform = 'scale(1.02)';
c.style.pointerEvents = 'auto';
c.style.filter = 'none';
c.style.zIndex = '3';
} else {
c.style.opacity = '0.35';
c.style.transform = 'scale(0.96)';
c.style.pointerEvents = 'none';
c.style.filter = 'blur(3px)';
c.style.zIndex = '1';
}
});
}
btns.forEach(btn => {
btn.addEventListener('click', () => {
btns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const mode = btn.dataset.mode;
if (mode === 'all') showAll();
else if (mode === 'sun') showOnly('sunny');
else if (mode === 'wind') showOnly('windy');
else if (mode === 'rain') showOnly('rainy');
else if (mode === 'snow') showOnly('snowy');
});
});
})();
</script>
</body>
</html>
--【拾陆】--:
应该就这样了。看了其他佬的测试结果,感觉确实一般。
--【拾柒】--:
怎么感觉差了一点点呢
下雨和下雪的卡片背景不够突出
--【拾捌】--:
看了好几个佬的天气卡片测试都一般般啊,比较普通,难道只是这方面短板?
--【拾玖】--:
完蛋,国模末日,开了thinking还是一坨,希望只有前端拉了,不然玩完了
跑了五分钟才生成完,可能现在并发高,有点慢
提示词:
你是 Apple Inc 的顶级 UI 设计师,以 iOS 18 的设计风格(毛玻璃效果、高斯模糊、动态渐变、细腻阴影)创建一个单个HTML文件(包含完整CSS和JavaScript)。实现横板天气页面,包含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的对比,5.5 thinking 思考程度进阶:
GPT 5.5 天气卡片测试1920×1064 220 KB
--【壹】--:
的确是差了一点点点,等晚上的时候再测试一下?
--【贰】--:
突破了历史最差纪录,没看过这么差的 ,确定测试没问题吗
--【叁】--:
感觉还行啊,https://chat.deepseek.com/share/e84p9vfyxnl20oposn 还是快速模式
--【肆】--:
官方说了,官网上就是 V4。只是不知道官网上的 Pro think 模式 是不是 Pro-max。
--【伍】--:
我在官网专家模式,开 think 测试的。看文档描述,专家模式就是 Pro。
不知道用 API 测会不会好一点。
--【陆】--:
你这个看起来还不错,看来我的抽卡失败了。
--【柒】--:
v4不是原生多模态模型,可能审美这块确实差点,感觉明显有差距的
--【捌】--:
有点抽象,等了这么多天不会拉依托大的吧,希望只是偶发现象
--【玖】--:
感觉审美这一块确实差一点,也可能抽卡没抽好。
--【拾】--:
这坐标都不对认真的吗,太阳的建模倒是不错
--【拾壹】--:
已经更换了录屏动图,貌似就是这样设计的。
--【拾贰】--:
有一说一吧差点意思 不过还是很期待在cc里使用ds编程的
--【拾叁】--:
啊,这是认真的吗?是图片有问题还是特意如此设计啊?
image1920×1248 184 KB
--【拾肆】--:
V4还没上线官网啊,只有API有,目前官网用的还是3.2
--【拾伍】--:
同样的提示词,我的很好看啊
image1920×1063 134 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather – iOS 18</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--bg: #0a0a0f;
--card-bg: rgba(28, 28, 32, 0.72);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.56);
--text-tertiary: rgba(255, 255, 255, 0.36);
--sun-glow: #ff9f43;
--rain-blue: #74b9ff;
--snow-white: #dfe6e9;
--wind-teal: #55efc4;
--lightning: #ffeaa7;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* Dynamic ambient background */
.ambient-bg {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
.ambient-bg .orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0;
transition: opacity 1.2s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.orb-sun { width: 460px; height: 460px; background: radial-gradient(circle, #ff9f43 0%, transparent 70%); top: -160px; left: 2%; }
.orb-wind { width: 400px; height: 400px; background: radial-gradient(circle, #55efc4 0%, transparent 70%); top: 40px; left: 22%; }
.orb-rain { width: 420px; height: 420px; background: radial-gradient(circle, #74b9ff 0%, transparent 70%); top: -100px; left: 46%; }
.orb-snow { width: 440px; height: 440px; background: radial-gradient(circle, #a29bfe 0%, transparent 70%); top: -60px; left: 68%; }
.orb.active { opacity: 0.28; }
/* Main container */
.container {
position: relative;
z-index: 1;
width: 96vw;
max-width: 1500px;
display: flex;
gap: 18px;
padding: 20px 0;
}
/* Weather card */
.weather-card {
flex: 1;
min-width: 0;
height: 520px;
background: var(--card-bg);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--card-border);
border-radius: 32px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.55s cubic-bezier(0.25, 0.1, 0.25, 1);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.24),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 28px 24px 24px;
}
.weather-card:hover {
transform: translateY(-4px);
border-color: rgba(255, 255, 255, 0.16);
box-shadow:
0 20px 48px rgba(0, 0, 0, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.weather-card:active {
transform: scale(0.985);
transition: all 0.15s cubic-bezier(0.25, 0.1, 0.25, 1);
}
/* Card header */
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
z-index: 2;
position: relative;
}
.card-label {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-tertiary);
}
.card-icon {
font-size: 22px;
opacity: 0.7;
}
/* Card footer */
.card-footer {
z-index: 2;
position: relative;
}
.card-temp {
font-size: 52px;
font-weight: 300;
color: var(--text-primary);
line-height: 1;
letter-spacing: -0.02em;
}
.card-desc {
font-size: 15px;
font-weight: 500;
color: var(--text-secondary);
margin-top: 6px;
}
.card-detail {
display: flex;
gap: 16px;
margin-top: 12px;
}
.detail-item {
font-size: 12px;
font-weight: 500;
color: var(--text-tertiary);
display: flex;
align-items: center;
gap: 4px;
}
/* Canvas for animations */
.card-canvas {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
/* Toggle buttons */
.controls {
position: relative;
z-index: 2;
display: flex;
gap: 10px;
margin-bottom: 4px;
}
.ctrl-btn {
padding: 10px 20px;
border-radius: 100px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(28, 28, 32, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: var(--text-secondary);
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
letter-spacing: 0.02em;
}
.ctrl-btn:hover {
background: rgba(44, 44, 50, 0.8);
color: var(--text-primary);
border-color: rgba(255, 255, 255, 0.2);
}
.ctrl-btn.active {
background: rgba(255, 255, 255, 0.12);
color: var(--text-primary);
border-color: rgba(255, 255, 255, 0.24);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
}
/* Lightning flash overlay */
.lightning-flash {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.06);
opacity: 0;
pointer-events: none;
z-index: 0;
transition: opacity 0.08s;
border-radius: 32px;
}
.lightning-flash.strike { opacity: 1; }
@media (max-width: 960px) {
.container {
flex-wrap: wrap;
}
.weather-card {
flex: 1 1 calc(50% - 9px);
min-width: 240px;
height: 380px;
}
}
@media (max-width: 560px) {
.weather-card {
flex: 1 1 100%;
height: 340px;
}
.card-temp { font-size: 40px; }
}
</style>
</head>
<body>
<div class="ambient-bg">
<div class="orb orb-sun active" id="orb-sun"></div>
<div class="orb orb-wind active" id="orb-wind"></div>
<div class="orb orb-rain active" id="orb-rain"></div>
<div class="orb orb-snow active" id="orb-snow"></div>
</div>
<div class="controls">
<button class="ctrl-btn active" data-mode="all">All Active</button>
<button class="ctrl-btn" data-mode="sun">Sunny</button>
<button class="ctrl-btn" data-mode="wind">Windy</button>
<button class="ctrl-btn" data-mode="rain">Rainstorm</button>
<button class="ctrl-btn" data-mode="snow">Blizzard</button>
</div>
<div class="container">
<!-- SUNNY -->
<div class="weather-card" data-weather="sunny">
<div class="card-header">
<span class="card-label">Sunny</span>
<span class="card-icon">☀️</span>
</div>
<canvas class="card-canvas" id="canvas-sunny"></canvas>
<div class="card-footer">
<div class="card-temp">28°</div>
<div class="card-desc">Clear Sky</div>
<div class="card-detail">
<span class="detail-item">UV 6</span>
<span class="detail-item">H 32°</span>
<span class="detail-item">L 21°</span>
</div>
</div>
</div>
<!-- WINDY -->
<div class="weather-card" data-weather="windy">
<div class="card-header">
<span class="card-label">Windy</span>
<span class="card-icon">💨</span>
</div>
<canvas class="card-canvas" id="canvas-windy"></canvas>
<div class="card-footer">
<div class="card-temp">18°</div>
<div class="card-desc">Gusty Winds</div>
<div class="card-detail">
<span class="detail-item">32 km/h</span>
<span class="detail-item">Gust 48</span>
</div>
</div>
</div>
<!-- RAIN -->
<div class="weather-card" data-weather="rainy">
<div class="card-header">
<span class="card-label">Rainstorm</span>
<span class="card-icon">⛈️</span>
</div>
<canvas class="card-canvas" id="canvas-rainy"></canvas>
<div class="lightning-flash" id="flash-rainy"></div>
<div class="card-footer">
<div class="card-temp">16°</div>
<div class="card-desc">Heavy Rain</div>
<div class="card-detail">
<span class="detail-item">95%</span>
<span class="detail-item">24 mm</span>
</div>
</div>
</div>
<!-- SNOW -->
<div class="weather-card" data-weather="snowy">
<div class="card-header">
<span class="card-label">Blizzard</span>
<span class="card-icon">❄️</span>
</div>
<canvas class="card-canvas" id="canvas-snowy"></canvas>
<div class="card-footer">
<div class="card-temp">-8°</div>
<div class="card-desc">Heavy Snow</div>
<div class="card-detail">
<span class="detail-item">18 cm</span>
<span class="detail-item">-12°</span>
</div>
</div>
</div>
</div>
<script>
(function() {
// ── Canvas setup ──────────────────────────────────────────────
function setupCanvas(id) {
const canvas = document.getElementById(id);
const card = canvas.closest('.weather-card');
const dpr = Math.min(window.devicePixelRatio || 1, 2);
function resize() {
const r = card.getBoundingClientRect();
canvas.width = r.width * dpr;
canvas.height = r.height * dpr;
canvas.style.width = r.width + 'px';
canvas.style.height = r.height + 'px';
}
resize();
window.addEventListener('resize', resize);
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return { canvas, ctx, getW: () => canvas.width / dpr, getH: () => canvas.height / dpr, resize };
}
// ── SUNNY ────────────────────────────────────────────────────
function animateSunny() {
const { ctx, getW, getH } = setupCanvas('canvas-sunny');
const particles = [];
const RAY_COUNT = 18;
for (let i = 0; i < RAY_COUNT; i++) {
particles.push({
angle: (i / RAY_COUNT) * Math.PI * 2,
length: 50 + Math.random() * 60,
speed: 0.002 + Math.random() * 0.006,
width: 1 + Math.random() * 1.8,
offset: Math.random() * Math.PI * 2
});
}
// Floating dust motes
const motes = [];
for (let i = 0; i < 30; i++) {
motes.push({
x: Math.random(), y: Math.random(),
size: 0.5 + Math.random() * 2,
speed: 0.0003 + Math.random() * 0.001,
drift: (Math.random() - 0.5) * 0.3,
opacity: 0.2 + Math.random() * 0.5
});
}
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
const cx = w * 0.46, cy = h * 0.38;
// Outer glow — pulsating
const glowPulse = 1 + Math.sin(t * 0.001) * 0.15;
const glow = ctx.createRadialGradient(cx, cy, 20, cx, cy, 120 * glowPulse);
glow.addColorStop(0, 'rgba(255, 170, 50, 0.28)');
glow.addColorStop(0.5, 'rgba(255, 140, 30, 0.08)');
glow.addColorStop(1, 'rgba(255, 100, 20, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(cx, cy, 120 * glowPulse, 0, Math.PI * 2);
ctx.fill();
// Medium glow
const glow2 = ctx.createRadialGradient(cx, cy, 14, cx, cy, 55);
glow2.addColorStop(0, 'rgba(255, 220, 140, 0.45)');
glow2.addColorStop(0.7, 'rgba(255, 160, 50, 0.08)');
glow2.addColorStop(1, 'rgba(255, 120, 30, 0)');
ctx.fillStyle = glow2;
ctx.beginPath();
ctx.arc(cx, cy, 55, 0, Math.PI * 2);
ctx.fill();
// Sun body
const sunGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 28);
sunGrad.addColorStop(0, 'rgba(255, 250, 210, 1)');
sunGrad.addColorStop(0.3, 'rgba(255, 210, 80, 0.95)');
sunGrad.addColorStop(0.7, 'rgba(255, 160, 30, 0.5)');
sunGrad.addColorStop(1, 'rgba(255, 120, 20, 0)');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(cx, cy, 28, 0, Math.PI * 2);
ctx.fill();
// Rays
particles.forEach(p => {
const ang = p.angle + Math.sin(t * 0.0008 + p.offset) * 0.12;
const len = p.length * (0.8 + Math.sin(t * p.speed + p.offset) * 0.25);
const x1 = cx + Math.cos(ang) * 32;
const y1 = cy + Math.sin(ang) * 32;
const x2 = cx + Math.cos(ang) * (32 + len);
const y2 = cy + Math.sin(ang) * (32 + len);
const rayGrad = ctx.createLinearGradient(x1, y1, x2, y2);
rayGrad.addColorStop(0, 'rgba(255, 210, 100, 0.7)');
rayGrad.addColorStop(1, 'rgba(255, 160, 60, 0)');
ctx.strokeStyle = rayGrad;
ctx.lineWidth = p.width;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
});
// Dust motes
motes.forEach(m => {
m.y -= m.speed;
m.x += m.drift * 0.1;
if (m.y < 0) { m.y = 1; m.x = Math.random(); }
if (m.x > 1) m.x = 0;
if (m.x < 0) m.x = 1;
ctx.fillStyle = `rgba(255,220,150,${m.opacity})`;
ctx.beginPath();
ctx.arc(m.x * w, m.y * h, m.size, 0, Math.PI * 2);
ctx.fill();
});
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── WINDY ────────────────────────────────────────────────────
function animateWindy() {
const { ctx, getW, getH } = setupCanvas('canvas-windy');
const clouds = [];
for (let i = 0; i < 5; i++) {
clouds.push({
x: Math.random() * 1.2 - 0.1,
y: 0.08 + Math.random() * 0.35,
speed: 0.0003 + Math.random() * 0.0008,
scale: 0.6 + Math.random() * 0.9,
opacity: 0.3 + Math.random() * 0.4
});
}
const windLines = [];
for (let i = 0; i < 18; i++) {
windLines.push({
x: Math.random(),
y: 0.15 + Math.random() * 0.65,
len: 20 + Math.random() * 50,
speed: 0.002 + Math.random() * 0.008,
opacity: 0.1 + Math.random() * 0.35,
phase: Math.random() * Math.PI * 2
});
}
// Tree data — approximate a simple pine
const treeX = 0.62, treeBaseY = 0.78;
const branches = [];
for (let i = 0; i < 8; i++) {
branches.push({
yOff: 0.04 + i * 0.06,
swayAmp: (0.03 + i * 0.018),
swaySpeed: 0.002 + Math.random() * 0.003,
phase: Math.random() * Math.PI * 2
});
}
// Leaves blowing
const leaves = [];
for (let i = 0; i < 12; i++) {
leaves.push({
x: Math.random(),
y: Math.random(),
size: 1.5 + Math.random() * 3,
speedX: 0.004 + Math.random() * 0.012,
speedY: -0.002 + Math.random() * 0.004,
rot: Math.random() * Math.PI * 2,
rotSpeed: (Math.random() - 0.5) * 0.08,
opacity: 0.2 + Math.random() * 0.4
});
}
function drawCloud(ctx, x, y, scale, opacity) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.fillStyle = `rgba(200,220,240,${opacity})`;
ctx.beginPath();
ctx.arc(0, 0, 22, 0, Math.PI * 2);
ctx.arc(20, -6, 18, 0, Math.PI * 2);
ctx.arc(36, 2, 20, 0, Math.PI * 2);
ctx.arc(16, 8, 16, 0, Math.PI * 2);
ctx.arc(-14, 4, 14, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
// Clouds
clouds.forEach(c => {
c.x += c.speed;
if (c.x > 1.2) c.x = -0.2;
drawCloud(ctx, c.x * w, c.y * h, c.scale, c.opacity);
});
// Wind lines
ctx.lineCap = 'round';
windLines.forEach(l => {
l.x += l.speed;
if (l.x > 1.05) l.x = -0.05;
const alpha = l.opacity * (0.6 + 0.4 * Math.sin(t * 0.003 + l.phase));
const sx = l.x * w;
const sy = l.y * h;
ctx.strokeStyle = `rgba(180,210,240,${alpha})`;
ctx.lineWidth = 0.8 + Math.random() * 0.4;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx - l.len * (0.7 + 0.3 * Math.sin(t * 0.005 + l.phase)), sy + (Math.sin(t * 0.004 + l.phase) * 6));
ctx.stroke();
});
// Tree trunk
const tx = treeX * w, tbY = treeBaseY * h;
ctx.fillStyle = 'rgba(120,90,60,0.45)';
ctx.fillRect(tx - 4, tbY - 55, 8, 55);
// Tree branches (pine layers)
branches.forEach(b => {
const sway = Math.sin(t * b.swaySpeed + b.phase) * b.swayAmp * w;
const by = tbY - b.yOff * h;
const layerWidth = (0.12 - b.yOff * 0.08) * w;
ctx.fillStyle = 'rgba(60,140,80,0.5)';
ctx.beginPath();
ctx.moveTo(tx + sway, by - layerWidth * 0.55);
ctx.lineTo(tx + sway + layerWidth * 0.5, by + layerWidth * 0.25);
ctx.lineTo(tx + sway - layerWidth * 0.5, by + layerWidth * 0.25);
ctx.closePath();
ctx.fill();
});
// Ground
ctx.fillStyle = 'rgba(80,130,70,0.18)';
ctx.fillRect(0, tbY, w, h - tbY);
// Leaves
leaves.forEach(l => {
l.x += l.speedX;
l.y += l.speedY;
l.rot += l.rotSpeed;
if (l.x > 1.1) { l.x = -0.1; l.y = Math.random(); }
if (l.y < 0) l.y = 1;
if (l.y > 1) l.y = 0;
ctx.save();
ctx.translate(l.x * w, l.y * h);
ctx.rotate(l.rot);
ctx.fillStyle = `rgba(140,190,80,${l.opacity})`;
ctx.beginPath();
ctx.ellipse(0, 0, l.size, l.size * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── RAIN ─────────────────────────────────────────────────────
function animateRainy() {
const { ctx, getW, getH } = setupCanvas('canvas-rainy');
const flashEl = document.getElementById('flash-rainy');
const drops = [];
for (let i = 0; i < 140; i++) {
drops.push({
x: Math.random(),
y: Math.random(),
speed: 0.006 + Math.random() * 0.018,
len: 8 + Math.random() * 18,
opacity: 0.15 + Math.random() * 0.5,
width: 0.6 + Math.random() * 1.2
});
}
// Puddle ripples
const ripples = [];
for (let i = 0; i < 6; i++) {
ripples.push({
x: 0.15 + Math.random() * 0.7,
y: 0.72 + Math.random() * 0.18,
radius: 0,
maxRadius: 8 + Math.random() * 14,
speed: 0.3 + Math.random() * 0.6,
opacity: 0,
delay: Math.random() * 3000,
lastSpawn: 0
});
}
// Lightning timer
let nextLightning = 2000 + Math.random() * 4000;
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
// Dark storm clouds at top
const cloudGrad = ctx.createLinearGradient(0, 0, 0, h * 0.35);
cloudGrad.addColorStop(0, 'rgba(30,35,50,0.7)');
cloudGrad.addColorStop(0.5, 'rgba(40,48,65,0.4)');
cloudGrad.addColorStop(1, 'rgba(40,48,65,0)');
ctx.fillStyle = cloudGrad;
ctx.fillRect(0, 0, w, h * 0.35);
// Cloud bumps
ctx.fillStyle = 'rgba(35,40,58,0.6)';
const bumpYs = [-8, 4, -2, 10, -6, 2, 8];
for (let i = 0; i < 7; i++) {
ctx.beginPath();
ctx.arc(w * (i / 6.5), bumpYs[i], w * 0.14, 0, Math.PI * 2);
ctx.fill();
}
// Rain drops
ctx.lineCap = 'round';
drops.forEach(d => {
d.y += d.speed;
if (d.y > 1.02) { d.y = -0.05; d.x = Math.random(); }
const sx = d.x * w;
const sy = d.y * h;
const grad = ctx.createLinearGradient(sx, sy, sx - 2, sy + d.len);
grad.addColorStop(0, `rgba(160,200,255,${d.opacity})`);
grad.addColorStop(1, `rgba(120,170,240,${d.opacity * 0.3})`);
ctx.strokeStyle = grad;
ctx.lineWidth = d.width;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx - 2, sy + d.len);
ctx.stroke();
});
// Puddles / ground
const puddleY = h * 0.78;
ctx.fillStyle = 'rgba(30,50,80,0.25)';
ctx.fillRect(0, puddleY, w, h - puddleY);
// Ripples
ripples.forEach(r => {
if (t - r.lastSpawn > r.delay) {
r.radius = 0;
r.opacity = 0.5;
r.lastSpawn = t;
r.delay = 800 + Math.random() * 2500;
r.x = 0.1 + Math.random() * 0.8;
r.y = 0.76 + Math.random() * 0.16;
r.maxRadius = 6 + Math.random() * 16;
}
if (r.opacity > 0.001) {
r.radius += r.speed;
r.opacity -= 0.015;
ctx.strokeStyle = `rgba(140,190,240,${Math.max(0, r.opacity)})`;
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.arc(r.x * w, r.y * h, r.radius, 0, Math.PI * 2);
ctx.stroke();
}
});
// Lightning
if (t > nextLightning) {
const lx = w * (0.25 + Math.random() * 0.5);
const ly = h * 0.02;
ctx.strokeStyle = 'rgba(255,245,200,0.8)';
ctx.lineWidth = 2;
ctx.shadowColor = 'rgba(255,240,180,0.6)';
ctx.shadowBlur = 18;
let x = lx, y = ly;
ctx.beginPath();
ctx.moveTo(x, y);
for (let i = 0; i < 5; i++) {
x += (Math.random() - 0.5) * 40;
y += h * 0.12 + Math.random() * h * 0.08;
ctx.lineTo(x, y);
}
ctx.stroke();
ctx.shadowBlur = 0;
// Flash
flashEl.classList.add('strike');
setTimeout(() => flashEl.classList.remove('strike'), 120);
nextLightning = t + 2000 + Math.random() * 5000;
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── SNOW ─────────────────────────────────────────────────────
function animateSnowy() {
const { ctx, getW, getH } = setupCanvas('canvas-snowy');
const flakes = [];
for (let i = 0; i < 100; i++) {
flakes.push({
x: Math.random(),
y: Math.random(),
size: 1 + Math.random() * 3.5,
speed: 0.0003 + Math.random() * 0.0015,
drift: (Math.random() - 0.5) * 0.8,
sway: Math.random() * Math.PI * 2,
swaySpeed: 0.01 + Math.random() * 0.03,
opacity: 0.3 + Math.random() * 0.55
});
}
// Snow accumulation on bottom
const accumHeights = [];
const segments = 50;
for (let i = 0; i < segments; i++) accumHeights.push(8 + Math.random() * 5);
function drawSnowflake(ctx, x, y, size, opacity) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = `rgba(230,240,255,${opacity})`;
// Six-pointed snowflake simplified
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate((i / 6) * Math.PI * 2);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -size);
ctx.lineWidth = Math.max(0.5, size * 0.3);
ctx.strokeStyle = `rgba(230,240,255,${opacity})`;
ctx.stroke();
// Small branch
ctx.beginPath();
ctx.moveTo(0, -size * 0.5);
ctx.lineTo(size * 0.35, -size * 0.7);
ctx.moveTo(0, -size * 0.5);
ctx.lineTo(-size * 0.35, -size * 0.7);
ctx.stroke();
ctx.restore();
}
// Center dot
ctx.fillStyle = `rgba(240,245,255,${Math.min(1, opacity + 0.2)})`;
ctx.beginPath();
ctx.arc(0, 0, size * 0.22, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function draw(t) {
const w = getW(), h = getH();
ctx.clearRect(0, 0, w, h);
// Soft top gradient
const topGrad = ctx.createLinearGradient(0, 0, 0, h * 0.25);
topGrad.addColorStop(0, 'rgba(25,28,40,0.5)');
topGrad.addColorStop(1, 'rgba(25,28,40,0)');
ctx.fillStyle = topGrad;
ctx.fillRect(0, 0, w, h * 0.25);
// Snowflakes
flakes.forEach(f => {
f.y += f.speed;
f.sway += f.swaySpeed;
f.x += Math.sin(f.sway) * f.drift * 0.002;
if (f.y > 1.02) { f.y = -0.02; f.x = Math.random(); }
if (f.x > 1.02) f.x = -0.02;
if (f.x < -0.02) f.x = 1.02;
drawSnowflake(ctx, f.x * w, f.y * h, f.size, f.opacity);
});
// Snow accumulation at bottom
const baseY = h * 0.82;
ctx.fillStyle = 'rgba(210,225,245,0.35)';
ctx.beginPath();
ctx.moveTo(0, h);
for (let i = 0; i <= segments; i++) {
const x = (i / segments) * w;
const idx = i % accumHeights.length;
accumHeights[idx] += 0.02;
if (accumHeights[idx] > 28) accumHeights[idx] = 28;
const bump = Math.sin(i * 0.6 + t * 0.0005) * 4 + accumHeights[idx];
ctx.lineTo(x, baseY + bump);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
// Subtle mist
const mistGrad = ctx.createLinearGradient(0, baseY - 20, 0, baseY + 10);
mistGrad.addColorStop(0, 'rgba(200,215,235,0)');
mistGrad.addColorStop(0.5, 'rgba(200,215,235,0.12)');
mistGrad.addColorStop(1, 'rgba(200,215,235,0)');
ctx.fillStyle = mistGrad;
ctx.fillRect(0, baseY - 20, w, 30);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ── Start all animations ─────────────────────────────────────
animateSunny();
animateWindy();
animateRainy();
animateSnowy();
// ── Mode switching ───────────────────────────────────────────
const cards = document.querySelectorAll('.weather-card');
const orbs = {
sunny: document.getElementById('orb-sun'),
windy: document.getElementById('orb-wind'),
rainy: document.getElementById('orb-rainy'),
snowy: document.getElementById('orb-snow'),
};
const btns = document.querySelectorAll('.ctrl-btn');
function showAll() {
cards.forEach(c => {
c.style.opacity = '1';
c.style.transform = '';
c.style.pointerEvents = 'auto';
c.style.filter = 'none';
});
Object.values(orbs).forEach(o => o.classList.add('active'));
}
function showOnly(weather) {
Object.entries(orbs).forEach(([key, orb]) => {
if (key === weather) orb.classList.add('active');
else orb.classList.remove('active');
});
cards.forEach(c => {
if (c.dataset.weather === weather) {
c.style.opacity = '1';
c.style.transform = 'scale(1.02)';
c.style.pointerEvents = 'auto';
c.style.filter = 'none';
c.style.zIndex = '3';
} else {
c.style.opacity = '0.35';
c.style.transform = 'scale(0.96)';
c.style.pointerEvents = 'none';
c.style.filter = 'blur(3px)';
c.style.zIndex = '1';
}
});
}
btns.forEach(btn => {
btn.addEventListener('click', () => {
btns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const mode = btn.dataset.mode;
if (mode === 'all') showAll();
else if (mode === 'sun') showOnly('sunny');
else if (mode === 'wind') showOnly('windy');
else if (mode === 'rain') showOnly('rainy');
else if (mode === 'snow') showOnly('snowy');
});
});
})();
</script>
</body>
</html>
--【拾陆】--:
应该就这样了。看了其他佬的测试结果,感觉确实一般。
--【拾柒】--:
怎么感觉差了一点点呢
下雨和下雪的卡片背景不够突出
--【拾捌】--:
看了好几个佬的天气卡片测试都一般般啊,比较普通,难道只是这方面短板?
--【拾玖】--:
完蛋,国模末日,开了thinking还是一坨,希望只有前端拉了,不然玩完了

