DeepSeek v4 pro天气卡片测试来了,结果有点意外(新增GPT5.5对比)

2026-04-29 10:152阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

跑了五分钟才生成完,可能现在并发高,有点慢
提示词:

你是 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还是一坨,希望只有前端拉了,不然玩完了