OPUS 4.6 天气卡测试

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Premium Weather Cards</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=SF+Pro+Display:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap');

  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  :root {
    --bg-primary: #0a0a0f;
    --bg-secondary: #12121a;
    --text-primary: #ffffff;
    --text-secondary: rgba(255,255,255,0.6);
    --glass-bg: rgba(255,255,255,0.05);
    --glass-border: rgba(255,255,255,0.08);
    --card-radius: 28px;
    --sunny-warm: #ff9500;
    --rain-blue: #5ac8fa;
    --snow-white: #e8e8ed;
    --wind-gray: #8e8e93;
  }

  body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;
    background: var(--bg-primary);
    min-height: 100vh;
    color: var(--text-primary);
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
  }

  /* ============ Ambient Background ============ */
  .ambient-bg {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
  }

  .ambient-orb {
    position: absolute;
    border-radius: 50%;
    filter: blur(120px);
    opacity: 0.15;
    animation: orbFloat 20s ease-in-out infinite;
  }

  .ambient-orb:nth-child(1) {
    width: 600px; height: 600px;
    background: radial-gradient(circle, #5856d6, transparent);
    top: -200px; left: -100px;
    animation-delay: 0s;
  }

  .ambient-orb:nth-child(2) {
    width: 500px; height: 500px;
    background: radial-gradient(circle, #ff9500, transparent);
    bottom: -200px; right: -100px;
    animation-delay: -7s;
  }

  .ambient-orb:nth-child(3) {
    width: 400px; height: 400px;
    background: radial-gradient(circle, #30d158, transparent);
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    animation-delay: -14s;
  }

  @keyframes orbFloat {
    0%, 100% { transform: translate(0, 0) scale(1); }
    25% { transform: translate(40px, -30px) scale(1.1); }
    50% { transform: translate(-20px, 20px) scale(0.95); }
    75% { transform: translate(30px, 40px) scale(1.05); }
  }

  /* ============ Layout ============ */
  .app-container {
    position: relative;
    z-index: 1;
    max-width: 1400px;
    margin: 0 auto;
    padding: 40px 24px 60px;
  }

  /* ============ Header ============ */
  .header {
    text-align: center;
    margin-bottom: 20px;
  }

  .header-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 16px;
    border-radius: 100px;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.08);
    font-size: 12px;
    font-weight: 500;
    color: var(--text-secondary);
    letter-spacing: 0.5px;
    text-transform: uppercase;
    margin-bottom: 20px;
    backdrop-filter: blur(10px);
  }

  .header-badge .dot {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: #30d158;
    box-shadow: 0 0 8px #30d158;
    animation: pulse 2s ease-in-out infinite;
  }

  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
  }

  .header h1 {
    font-size: clamp(32px, 5vw, 52px);
    font-weight: 700;
    letter-spacing: -1.5px;
    line-height: 1.1;
    margin-bottom: 12px;
    background: linear-gradient(135deg, #fff 0%, rgba(255,255,255,0.7) 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
  }

  .header p {
    font-size: 16px;
    color: var(--text-secondary);
    font-weight: 400;
    max-width: 500px;
    margin: 0 auto;
    line-height: 1.5;
  }

  /* ============ Time & Location Bar ============ */
  .info-bar {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 24px;
    margin: 28px 0 36px;
    flex-wrap: wrap;
  }

  .info-item {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    color: var(--text-secondary);
  }

  .info-item svg {
    width: 16px; height: 16px;
    opacity: 0.5;
  }

  /* ============ Control Strip ============ */
  .control-strip {
    display: flex;
    justify-content: center;
    gap: 8px;
    margin-bottom: 40px;
    flex-wrap: wrap;
  }

  .ctrl-btn {
    padding: 10px 22px;
    border-radius: 100px;
    border: 1px solid rgba(255,255,255,0.1);
    background: rgba(255,255,255,0.04);
    color: var(--text-secondary);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    backdrop-filter: blur(10px);
    font-family: inherit;
    letter-spacing: 0.3px;
    position: relative;
    overflow: hidden;
  }

  .ctrl-btn::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    opacity: 0;
    transition: opacity 0.4s;
  }

  .ctrl-btn:hover {
    background: rgba(255,255,255,0.1);
    border-color: rgba(255,255,255,0.2);
    color: #fff;
    transform: translateY(-1px);
  }

  .ctrl-btn.active {
    background: rgba(255,255,255,0.15);
    border-color: rgba(255,255,255,0.25);
    color: #fff;
    box-shadow: 0 4px 20px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1);
  }

  .ctrl-btn.active[data-weather="sunny"] {
    background: rgba(255,149,0,0.2);
    border-color: rgba(255,149,0,0.4);
    color: #ffb340;
  }

  .ctrl-btn.active[data-weather="rainy"] {
    background: rgba(90,200,250,0.15);
    border-color: rgba(90,200,250,0.3);
    color: #5ac8fa;
  }

  .ctrl-btn.active[data-weather="snowy"] {
    background: rgba(200,200,220,0.15);
    border-color: rgba(200,200,220,0.3);
    color: #d1d1d6;
  }

  .ctrl-btn.active[data-weather="windy"] {
    background: rgba(142,142,147,0.2);
    border-color: rgba(142,142,147,0.3);
    color: #aeaeb2;
  }

  /* ============ Cards Grid ============ */
  .cards-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 20px;
  }

  @media (max-width: 1100px) {
    .cards-grid { grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 600px) {
    .cards-grid { grid-template-columns: 1fr; }
  }

  /* ============ Weather Card ============ */
  .weather-card {
    position: relative;
    border-radius: var(--card-radius);
    overflow: hidden;
    height: 480px;
    cursor: pointer;
    transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
                box-shadow 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    will-change: transform;
  }

  .weather-card:hover {
    transform: translateY(-8px) scale(1.02);
    box-shadow: 0 30px 80px -20px rgba(0,0,0,0.6);
  }

  .weather-card::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    border: 1px solid var(--glass-border);
    z-index: 5;
    pointer-events: none;
  }

  .weather-card::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    z-index: 4;
    pointer-events: none;
    background: linear-gradient(180deg,
      transparent 40%,
      rgba(0,0,0,0.5) 100%);
  }

  /* Card Backgrounds */
  .card-bg {
    position: absolute;
    inset: 0;
    z-index: 0;
    transition: opacity 1s;
  }

  /* Sunny BG */
  .card-sunny .card-bg {
    background: linear-gradient(160deg, #1c1402 0%, #2d1e06 30%, #1a1205 70%, #0f0e09 100%);
  }

  /* Rainy BG */
  .card-rainy .card-bg {
    background: linear-gradient(160deg, #0a1628 0%, #0d1a2d 30%, #091420 70%, #060d15 100%);
  }

  /* Snowy BG */
  .card-snowy .card-bg {
    background: linear-gradient(160deg, #151520 0%, #1a1a2e 30%, #141425 70%, #0e0e1a 100%);
  }

  /* Windy BG */
  .card-windy .card-bg {
    background: linear-gradient(160deg, #12161a 0%, #181d22 30%, #13171b 70%, #0c0e11 100%);
  }

  /* ============ Canvas Layer ============ */
  .weather-canvas {
    position: absolute;
    inset: 0;
    z-index: 2;
    width: 100%;
    height: 100%;
  }

  /* ============ Card Content ============ */
  .card-content {
    position: relative;
    z-index: 6;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 28px 24px;
  }

  .card-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
  }

  .card-location {
    font-size: 13px;
    font-weight: 500;
    color: rgba(255,255,255,0.5);
    letter-spacing: 0.5px;
  }

  .card-icon-badge {
    width: 40px; height: 40px;
    border-radius: 50%;
    background: rgba(255,255,255,0.1);
    backdrop-filter: blur(10px);
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(255,255,255,0.1);
  }

  .card-icon-badge svg {
    width: 20px; height: 20px;
  }

  .card-bottom {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  .card-temp {
    font-size: 72px;
    font-weight: 200;
    letter-spacing: -4px;
    line-height: 1;
    font-variant-numeric: tabular-nums;
  }

  .card-temp .degree {
    font-size: 32px;
    font-weight: 300;
    vertical-align: super;
    letter-spacing: 0;
    opacity: 0.6;
  }

  .card-condition {
    font-size: 20px;
    font-weight: 500;
    margin-bottom: 4px;
    letter-spacing: -0.3px;
  }

  .card-detail-row {
    display: flex;
    gap: 16px;
    margin-top: 8px;
  }

  .card-detail {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 12px;
    color: rgba(255,255,255,0.45);
    font-weight: 400;
  }

  .card-detail svg {
    width: 14px; height: 14px;
    opacity: 0.5;
  }

  /* ============ Sunny Specific ============ */
  .sun-glow {
    position: absolute;
    top: -60px;
    right: -60px;
    width: 260px;
    height: 260px;
    border-radius: 50%;
    background: radial-gradient(circle,
      rgba(255,180,0,0.35) 0%,
      rgba(255,149,0,0.15) 30%,
      rgba(255,100,0,0.05) 60%,
      transparent 70%);
    z-index: 1;
    animation: sunPulse 4s ease-in-out infinite;
  }

  .sun-core {
    position: absolute;
    top: -15px;
    right: -15px;
    width: 140px;
    height: 140px;
    border-radius: 50%;
    background: radial-gradient(circle,
      rgba(255,214,100,0.8) 0%,
      rgba(255,180,0,0.4) 40%,
      transparent 70%);
    z-index: 1;
    animation: sunPulse 4s ease-in-out infinite;
    filter: blur(2px);
  }

  @keyframes sunPulse {
    0%, 100% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.08); opacity: 0.85; }
  }

  .sun-rays {
    position: absolute;
    top: 15px;
    right: 15px;
    width: 100px;
    height: 100px;
    z-index: 1;
    animation: sunRotate 30s linear infinite;
  }

  @keyframes sunRotate {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }

  .sun-ray {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 2px;
    height: 45px;
    background: linear-gradient(to bottom, rgba(255,200,50,0.5), transparent);
    transform-origin: top center;
    border-radius: 1px;
  }

  /* Light streaks for sunny */
  .light-streak {
    position: absolute;
    z-index: 1;
    background: linear-gradient(90deg, transparent, rgba(255,200,60,0.06), transparent);
    height: 1px;
    animation: streakMove 8s ease-in-out infinite;
  }

  @keyframes streakMove {
    0%, 100% { opacity: 0; transform: translateX(-50px); }
    50% { opacity: 1; transform: translateX(50px); }
  }

  /* ============ Rainy Specific ============ */
  .rain-cloud {
    position: absolute;
    z-index: 1;
    border-radius: 50%;
    background: rgba(30,50,80,0.5);
    filter: blur(15px);
  }

  .puddle {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 60px;
    z-index: 3;
    background: linear-gradient(to top,
      rgba(30,80,140,0.15) 0%,
      transparent 100%);
  }

  .puddle-ripple {
    position: absolute;
    border-radius: 50%;
    border: 1px solid rgba(90,200,250,0.15);
    animation: ripple 2s ease-out infinite;
    z-index: 3;
  }

  @keyframes ripple {
    0% { width: 0; height: 0; opacity: 0.6; }
    100% { width: 40px; height: 12px; opacity: 0; }
  }

  /* ============ Snowy Specific ============ */
  .snow-ground {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 3;
    pointer-events: none;
  }

  .snow-layer {
    position: absolute;
    bottom: 0;
    left: -5%;
    right: -5%;
    background: white;
    border-radius: 50% 50% 0 0;
  }

  .snow-layer:nth-child(1) {
    height: 45px;
    background: rgba(220,220,235,0.25);
    bottom: 20px;
    filter: blur(2px);
  }

  .snow-layer:nth-child(2) {
    height: 30px;
    background: rgba(230,230,240,0.2);
    bottom: 5px;
    border-radius: 40% 60% 0 0;
  }

  .snow-layer:nth-child(3) {
    height: 25px;
    background: rgba(240,240,250,0.15);
    bottom: 0px;
    border-radius: 55% 45% 0 0;
  }

  .frost-overlay {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: radial-gradient(ellipse at bottom,
      rgba(200,200,230,0.05) 0%,
      transparent 60%);
    pointer-events: none;
  }

  /* ============ Windy Specific ============ */
  .wind-line {
    position: absolute;
    z-index: 1;
    height: 1.5px;
    border-radius: 1px;
    background: linear-gradient(90deg,
      transparent 0%,
      rgba(180,180,190,0.2) 30%,
      rgba(180,180,190,0.1) 70%,
      transparent 100%);
    animation: windSweep 3s ease-in-out infinite;
  }

  @keyframes windSweep {
    0% { transform: translateX(-120%) scaleX(0.5); opacity: 0; }
    30% { opacity: 1; scaleX: 1; }
    100% { transform: translateX(120%) scaleX(0.8); opacity: 0; }
  }

  .cloud-shape {
    position: absolute;
    z-index: 1;
    background: rgba(100,110,120,0.08);
    border-radius: 50px;
    filter: blur(8px);
    animation: cloudDrift 12s ease-in-out infinite;
  }

  @keyframes cloudDrift {
    0%, 100% { transform: translateX(0); }
    50% { transform: translateX(30px); }
  }

  /* Swaying tree */
  .tree-container {
    position: absolute;
    bottom: 20px;
    z-index: 3;
  }

  .tree-trunk {
    width: 3px;
    height: 40px;
    background: rgba(120,100,80,0.4);
    margin: 0 auto;
    border-radius: 2px;
    transform-origin: bottom center;
    animation: treeSway 3s ease-in-out infinite;
  }

  .tree-crown {
    width: 30px;
    height: 25px;
    background: rgba(60,80,60,0.3);
    border-radius: 50% 50% 40% 40%;
    margin: -5px auto 0;
    transform-origin: bottom center;
    animation: treeSway 3s ease-in-out infinite;
    animation-delay: 0.2s;
  }

  @keyframes treeSway {
    0%, 100% { transform: rotate(-4deg); }
    50% { transform: rotate(6deg); }
  }

  /* ============ Status indicator ============ */
  .status-dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    margin-right: 2px;
    vertical-align: middle;
  }

  .card-sunny .status-dot { background: var(--sunny-warm); box-shadow: 0 0 6px var(--sunny-warm); }
  .card-rainy .status-dot { background: var(--rain-blue); box-shadow: 0 0 6px var(--rain-blue); }
  .card-snowy .status-dot { background: var(--snow-white); box-shadow: 0 0 6px var(--snow-white); }
  .card-windy .status-dot { background: var(--wind-gray); box-shadow: 0 0 6px var(--wind-gray); }

  /* ============ Footer Info ============ */
  .footer-info {
    text-align: center;
    margin-top: 40px;
    padding: 24px;
  }

  .footer-info p {
    font-size: 12px;
    color: rgba(255,255,255,0.2);
    letter-spacing: 0.5px;
  }

  /* ============ Smooth transitions on all cards ============ */
  .weather-card, .weather-card * {
    -webkit-backface-visibility: hidden;
  }

  /* Highlighted card state */
  .weather-card.highlighted {
    z-index: 10;
  }

  .weather-card.dimmed {
    opacity: 0.35;
    transform: scale(0.97);
    filter: brightness(0.6);
    transition: all 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }

  .weather-card.highlighted {
    transform: translateY(-12px) scale(1.03);
    box-shadow: 0 40px 100px -20px rgba(0,0,0,0.7);
  }

  /* ============ Particle Mist for transitions ============ */
  .golden-particles, .blue-mist, .snow-mist, .wind-dust {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
  }

  /* Card shimmer effect on hover */
  .card-shimmer {
    position: absolute;
    inset: 0;
    z-index: 5;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.5s;
    background: linear-gradient(
      105deg,
      transparent 40%,
      rgba(255,255,255,0.03) 45%,
      rgba(255,255,255,0.06) 50%,
      rgba(255,255,255,0.03) 55%,
      transparent 60%
    );
    background-size: 200% 100%;
    animation: shimmerSlide 3s ease-in-out infinite;
  }

  .weather-card:hover .card-shimmer { opacity: 1; }

  @keyframes shimmerSlide {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }

  /* ============ Enhanced card inner glow ============ */
  .card-sunny .card-inner-glow {
    position: absolute;
    top: -50%;
    right: -30%;
    width: 80%;
    height: 80%;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255,180,0,0.08), transparent 70%);
    z-index: 1;
  }

  .card-rainy .card-inner-glow {
    position: absolute;
    top: -30%;
    left: 10%;
    width: 80%;
    height: 60%;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(40,80,140,0.12), transparent 70%);
    z-index: 1;
  }

  .card-snowy .card-inner-glow {
    position: absolute;
    top: -20%;
    left: 20%;
    width: 60%;
    height: 60%;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(180,180,220,0.06), transparent 70%);
    z-index: 1;
  }

  .card-windy .card-inner-glow {
    position: absolute;
    top: 20%;
    right: -20%;
    width: 60%;
    height: 50%;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(120,130,140,0.06), transparent 70%);
    z-index: 1;
  }

  /* ============ Micro-interactions ============ */
  .card-sunny .card-temp { color: #ffd060; }
  .card-rainy .card-temp { color: #8dd4f0; }
  .card-snowy .card-temp { color: #d0d0e0; }
  .card-windy .card-temp { color: #b0b8c0; }

  .card-sunny .card-condition { color: rgba(255,200,80,0.9); }
  .card-rainy .card-condition { color: rgba(130,200,240,0.9); }
  .card-snowy .card-condition { color: rgba(200,200,230,0.9); }
  .card-windy .card-condition { color: rgba(170,180,190,0.9); }

  /* ============ Lightning Flash ============ */
  .lightning-flash {
    position: absolute;
    inset: 0;
    z-index: 3;
    background: rgba(200,220,255,0.15);
    opacity: 0;
    pointer-events: none;
    border-radius: inherit;
  }

  @keyframes lightningFlash {
    0% { opacity: 0; }
    5% { opacity: 1; }
    10% { opacity: 0; }
    12% { opacity: 0.7; }
    15% { opacity: 0; }
    100% { opacity: 0; }
  }
</style>
</head>
<body>

<div class="ambient-bg">
  <div class="ambient-orb"></div>
  <div class="ambient-orb"></div>
  <div class="ambient-orb"></div>
</div>

<div class="app-container">
  <header class="header">
    <div class="header-badge">
      <span class="dot"></span>
      实时天气 · LIVE
    </div>
    <h1>Weather Animations</h1>
    <p>精致沉浸式天气可视化 · 动态粒子渲染引擎</p>
  </header>

  <div class="info-bar">
    <div class="info-item">
      <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"/><path d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"/></svg>
      <span id="locationText">上海, 中国</span>
    </div>
    <div class="info-item">
      <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
      <span id="timeText"></span>
    </div>
    <div class="info-item">
      <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"/></svg>
      <span>AQI 42 · 优</span>
    </div>
  </div>

  <div class="control-strip">
    <button class="ctrl-btn" data-weather="all" onclick="highlightWeather('all')">
      ✦ 全部展示
    </button>
    <button class="ctrl-btn" data-weather="sunny" onclick="highlightWeather('sunny')">
      ☀ 晴天
    </button>
    <button class="ctrl-btn" data-weather="rainy" onclick="highlightWeather('rainy')">
      🌧 降雨
    </button>
    <button class="ctrl-btn" data-weather="snowy" onclick="highlightWeather('snowy')">
      ❄ 降雪
    </button>
    <button class="ctrl-btn" data-weather="windy" onclick="highlightWeather('windy')">
      💨 大风
    </button>
  </div>

  <div class="cards-grid" id="cardsGrid">

    <!-- ===== SUNNY CARD ===== -->
    <div class="weather-card card-sunny" id="card-sunny" data-type="sunny">
      <div class="card-bg"></div>
      <div class="card-inner-glow"></div>
      <div class="sun-glow"></div>
      <div class="sun-core"></div>
      <div class="sun-rays" id="sunRays"></div>
      <canvas class="weather-canvas" id="sunnyCanvas"></canvas>
      <div class="card-shimmer"></div>
      <div class="card-content">
        <div class="card-top">
          <span class="card-location">上海 · 陆家嘴</span>
          <div class="card-icon-badge">
            <svg fill="none" stroke="#ffb340" stroke-width="1.5" viewBox="0 0 24 24">
              <path d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"/>
            </svg>
          </div>
        </div>
        <div class="card-bottom">
          <div class="card-temp">32<span class="degree">°</span></div>
          <div class="card-condition"><span class="status-dot"></span> 晴朗</div>
          <div class="card-detail-row">
            <div class="card-detail">
              <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"/></svg>
              UV 极强
            </div>
            <div class="card-detail">
              <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M12 6v6h4.5"/></svg>
              体感 36°
            </div>
            <div class="card-detail">湿度 45%</div>
          </div>
        </div>
      </div>
    </div>

    <!-- ===== RAINY CARD ===== -->
    <div class="weather-card card-rainy" id="card-rainy" data-type="rainy">
      <div class="card-bg"></div>
      <div class="card-inner-glow"></div>
      <div class="rain-cloud" style="top:-20px;left:10%;width:160px;height:60px;"></div>
      <div class="rain-cloud" style="top:10px;left:40%;width:200px;height:50px;opacity:0.6;"></div>
      <canvas class="weather-canvas" id="rainyCanvas"></canvas>
      <div class="lightning-flash" id="lightningFlash"></div>
      <div class="puddle"></div>
      <div class="card-shimmer"></div>
      <div class="card-content">
        <div class="card-top">
          <span class="card-location">上海 · 外滩</span>
          <div class="card-icon-badge">
            <svg fill="none" stroke="#5ac8fa" stroke-width="1.5" viewBox="0 0 24 24">
              <path d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"/>
            </svg>
          </div>
        </div>
        <div class="card-bottom">
          <div class="card-temp">18<span class="degree">°</span></div>
          <div class="card-condition"><span class="status-dot"></span> 暴雨</div>
          <div class="card-detail-row">
            <div class="card-detail">
              <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"/></svg>
              降水 85%
            </div>
            <div class="card-detail">风速 24km/h</div>
            <div class="card-detail">湿度 92%</div>
          </div>
        </div>
      </div>
    </div>

    <!-- ===== SNOWY CARD ===== -->
    <div class="weather-card card-snowy" id="card-snowy" data-type="snowy">
      <div class="card-bg"></div>
      <div class="card-inner-glow"></div>
      <div class="frost-overlay"></div>
      <canvas class="weather-canvas" id="snowyCanvas"></canvas>
      <div class="snow-ground">
        <div class="snow-layer"></div>
        <div class="snow-layer"></div>
        <div class="snow-layer"></div>
      </div>
      <div class="card-shimmer"></div>
      <div class="card-content">
        <div class="card-top">
          <span class="card-location">哈尔滨 · 冰雪大世界</span>
          <div class="card-icon-badge">
            <svg fill="none" stroke="#d0d0e0" stroke-width="1.5" viewBox="0 0 24 24">
              <path d="M12 3v18m-6-6l6 6 6-6m-9-3l3 3 3-3M6 9l6-6 6 6"/>
            </svg>
          </div>
        </div>
        <div class="card-bottom">
          <div class="card-temp">-8<span class="degree">°</span></div>
          <div class="card-condition"><span class="status-dot"></span> 大雪</div>
          <div class="card-detail-row">
            <div class="card-detail">积雪 15cm</div>
            <div class="card-detail">能见度 200m</div>
            <div class="card-detail">湿度 78%</div>
          </div>
        </div>
      </div>
    </div>

    <!-- ===== WINDY CARD ===== -->
    <div class="weather-card card-windy" id="card-windy" data-type="windy">
      <div class="card-bg"></div>
      <div class="card-inner-glow"></div>
      <div class="cloud-shape" style="top:60px;left:-20px;width:180px;height:40px;animation-duration:10s;"></div>
      <div class="cloud-shape" style="top:120px;right:-30px;width:150px;height:35px;animation-duration:14s;animation-delay:-4s;"></div>
      <div class="cloud-shape" style="top:200px;left:20px;width:120px;height:30px;animation-duration:12s;animation-delay:-8s;opacity:0.5;"></div>
      <canvas class="weather-canvas" id="windyCanvas"></canvas>
      <div class="card-shimmer"></div>
      <div class="card-content">
        <div class="card-top">
          <span class="card-location">大连 · 星海广场</span>
          <div class="card-icon-badge">
            <svg fill="none" stroke="#aeaeb2" stroke-width="1.5" viewBox="0 0 24 24">
              <path d="M3.75 13.5h10.5a2.25 2.25 0 002.25-2.25v0a2.25 2.25 0 00-2.25-2.25H12M3.75 17.25h14.25a2.25 2.25 0 002.25-2.25v0a2.25 2.25 0 00-2.25-2.25h-1.5M3.75 9h7.5a2.25 2.25 0 002.25-2.25v0A2.25 2.25 0 0011.25 4.5H9"/>
            </svg>
          </div>
        </div>
        <div class="card-bottom">
          <div class="card-temp">14<span class="degree">°</span></div>
          <div class="card-condition"><span class="status-dot"></span> 大风</div>
          <div class="card-detail-row">
            <div class="card-detail">风速 68km/h</div>
            <div class="card-detail">阵风 8级</div>
            <div class="card-detail">西北风</div>
          </div>
        </div>
      </div>
    </div>

  </div>

  <div class="footer-info">
    <p>WEATHER VISUALIZATION ENGINE v2.0 · CANVAS PARTICLE RENDERING · 60FPS</p>
  </div>
</div>

<script>
// ============================================
//  Premium Weather Animation Engine
// ============================================

// --- Utility ---
const dpr = Math.min(window.devicePixelRatio || 1, 2);

function setupCanvas(canvas) {
  const rect = canvas.parentElement.getBoundingClientRect();
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  canvas.style.width = rect.width + 'px';
  canvas.style.height = rect.height + 'px';
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  return { ctx, w: rect.width, h: rect.height };
}

function rand(min, max) { return Math.random() * (max - min) + min; }
function lerp(a, b, t) { return a + (b - a) * t; }

// --- Sun Rays Setup ---
(function setupSunRays() {
  const container = document.getElementById('sunRays');
  for (let i = 0; i < 12; i++) {
    const ray = document.createElement('div');
    ray.className = 'sun-ray';
    ray.style.transform = `rotate(${i * 30}deg)`;
    ray.style.opacity = 0.3 + Math.random() * 0.4;
    container.appendChild(ray);
  }
})();

// ============================================
//  SUNNY – Golden floating particles + lens flares
// ============================================
(function initSunny() {
  const canvas = document.getElementById('sunnyCanvas');
  let { ctx, w, h } = setupCanvas(canvas);

  class SunParticle {
    constructor() { this.reset(); }
    reset() {
      this.x = rand(0, w);
      this.y = rand(0, h);
      this.size = rand(1, 3);
      this.vx = rand(-0.15, 0.15);
      this.vy = rand(-0.3, -0.05);
      this.alpha = rand(0.1, 0.5);
      this.alphaSpeed = rand(0.003, 0.01);
      this.alphaDir = 1;
      this.life = rand(100, 300);
      this.age = 0;
      this.color = `rgba(255,${Math.floor(rand(180,220))},${Math.floor(rand(50,100))},`;
    }
    update() {
      this.x += this.vx;
      this.y += this.vy;
      this.alpha += this.alphaSpeed * this.alphaDir;
      if (this.alpha >= 0.6 || this.alpha <= 0.05) this.alphaDir *= -1;
      this.age++;
      if (this.age > this.life || this.y < -10 || this.x < -10 || this.x > w + 10) this.reset();
    }
    draw(ctx) {
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
      ctx.fillStyle = this.color + this.alpha + ')';
      ctx.fill();
    }
  }

  // Lens flare streaks
  class LensFlare {
    constructor() { this.reset(); }
    reset() {
      this.x = rand(w * 0.3, w);
      this.y = rand(0, h * 0.5);
      this.len = rand(30, 80);
      this.angle = rand(-0.5, 0.5);
      this.alpha = 0;
      this.maxAlpha = rand(0.03, 0.08);
      this.speed = rand(0.002, 0.005);
      this.phase = rand(0, Math.PI * 2);
    }
    update(t) {
      this.alpha = this.maxAlpha * (0.5 + 0.5 * Math.sin(t * this.speed + this.phase));
    }
    draw(ctx) {
      ctx.save();
      ctx.translate(this.x, this.y);
      ctx.rotate(this.angle);
      const grad = ctx.createLinearGradient(-this.len/2, 0, this.len/2, 0);
      grad.addColorStop(0, `rgba(255,200,60,0)`);
      grad.addColorStop(0.5, `rgba(255,200,60,${this.alpha})`);
      grad.addColorStop(1, `rgba(255,200,60,0)`);
      ctx.strokeStyle = grad;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(-this.len/2, 0);
      ctx.lineTo(this.len/2, 0);
      ctx.stroke();
      ctx.restore();
    }
  }

  const particles = Array.from({ length: 60 }, () => new SunParticle());
  const flares = Array.from({ length: 8 }, () => new LensFlare());
  let frame = 0;

  function animate() {
    ctx.clearRect(0, 0, w, h);
    frame++;
    particles.forEach(p => { p.update(); p.draw(ctx); });
    flares.forEach(f => { f.update(frame); f.draw(ctx); });
    requestAnimationFrame(animate);
  }

  animate();

  window.addEventListener('resize', () => {
    ({ ctx, w, h } = setupCanvas(canvas));
  });
})();

// ============================================
//  RAINY – Rain drops + splashes + mist + lightning
// ============================================
(function initRainy() {
  const canvas = document.getElementById('rainyCanvas');
  let { ctx, w, h } = setupCanvas(canvas);

  class RainDrop {
    constructor() { this.reset(true); }
    reset(initial) {
      this.x = rand(-20, w + 20);
      this.y = initial ? rand(-h, 0) : rand(-50, -10);
      this.len = rand(15, 35);
      this.speed = rand(12, 22);
      this.alpha = rand(0.15, 0.4);
      this.wind = rand(1, 3);
      this.thickness = rand(0.8, 1.8);
    }
    update() {
      this.y += this.speed;
      this.x += this.wind;
      if (this.y > h) {
        splashes.push(new Splash(this.x, h - rand(10, 60)));
        this.reset(false);
      }
    }
    draw(ctx) {
      ctx.beginPath();
      ctx.moveTo(this.x, this.y);
      ctx.lineTo(this.x + this.wind * 1.5, this.y + this.len);
      ctx.strokeStyle = `rgba(150,200,255,${this.alpha})`;
      ctx.lineWidth = this.thickness;
      ctx.lineCap = 'round';
      ctx.stroke();
    }
  }

  class Splash {
    constructor(x, y) {
      this.x = x; this.y = y;
      this.particles = [];
      for (let i = 0; i < rand(3, 6); i++) {
        this.particles.push({
          vx: rand(-2, 2),
          vy: rand(-3, -0.5),
          alpha: rand(0.3, 0.6),
          size: rand(1, 2),
          life: 0
        });
      }
      this.maxLife = 15;
    }
    update() {
      this.particles.forEach(p => {
        p.vx *= 0.96;
        p.vy += 0.15;
        p.alpha *= 0.92;
        p.life++;
      });
    }
    draw(ctx) {
      this.particles.forEach(p => {
        const px = this.x + p.vx * p.life;
        const py = this.y + p.vy * p.life + 0.5 * 0.15 * p.life * p.life;
        ctx.beginPath();
        ctx.arc(px, py, p.size, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(150,200,255,${p.alpha})`;
        ctx.fill();
      });
    }
    isDead() { return this.particles[0].life > this.maxLife; }
  }

  // Mist/fog layer
  class MistLayer {
    constructor() { this.reset(); }
    reset() {
      this.x = rand(-100, w);
      this.y = rand(h * 0.4, h);
      this.width = rand(150, 350);
      this.height = rand(20, 50);
      this.speed = rand(0.2, 0.6);
      this.alpha = rand(0.02, 0.06);
    }
    update() {
      this.x += this.speed;
      if (this.x > w + 100) this.reset();
    }
    draw(ctx) {
      ctx.save();
      ctx.globalAlpha = this.alpha;
      ctx.fillStyle = 'rgba(100,140,180,1)';
      ctx.filter = 'blur(20px)';
      ctx.fillRect(this.x, this.y, this.width, this.height);
      ctx.restore();
      ctx.filter = 'none';
    }
  }

  const drops = Array.from({ length: 120 }, () => new RainDrop());
  let splashes = [];
  const mists = Array.from({ length: 5 }, () => new MistLayer());

  // Ripple effects at bottom
  class Ripple {
    constructor() { this.reset(); }
    reset() {
      this.x = rand(0, w);
      this.y = rand(h - 70, h - 10);
      this.maxR = rand(8, 20);
      this.r = 0;
      this.alpha = 0.4;
      this.speed = rand(0.3, 0.8);
      this.delay = rand(0, 120);
      this.age = 0;
    }
    update() {
      this.age++;
      if (this.age < this.delay) return;
      this.r += this.speed;
      this.alpha = 0.4 * (1 - this.r / this.maxR);
      if (this.r >= this.maxR) this.reset();
    }
    draw(ctx) {
      if (this.age < this.delay) return;
      ctx.beginPath();
      ctx.ellipse(this.x, this.y, this.r, this.r * 0.3, 0, 0, Math.PI * 2);
      ctx.strokeStyle = `rgba(100,180,250,${this.alpha})`;
      ctx.lineWidth = 0.8;
      ctx.stroke();
    }
  }

  const ripples = Array.from({ length: 15 }, () => new Ripple());

  // Lightning
  const flashEl = document.getElementById('lightningFlash');
  let lightningTimer = 0;

  function triggerLightning() {
    flashEl.style.animation = 'none';
    void flashEl.offsetWidth;
    flashEl.style.animation = 'lightningFlash 0.8s ease-out forwards';
    lightningTimer = rand(200, 600);
  }

  lightningTimer = rand(180, 400);

  function animate() {
    ctx.clearRect(0, 0, w, h);

    mists.forEach(m => { m.update(); m.draw(ctx); });
    drops.forEach(d => { d.update(); d.draw(ctx); });
    splashes.forEach(s => { s.update(); s.draw(ctx); });
    splashes = splashes.filter(s => !s.isDead());
    ripples.forEach(r => { r.update(); r.draw(ctx); });

    lightningTimer--;
    if (lightningTimer <= 0) triggerLightning();

    requestAnimationFrame(animate);
  }

  animate();

  window.addEventListener('resize', () => {
    ({ ctx, w, h } = setupCanvas(canvas));
  });
})();

// ============================================
//  SNOWY – Snowflakes + wind drift + sparkle
// ============================================
(function initSnowy() {
  const canvas = document.getElementById('snowyCanvas');
  let { ctx, w, h } = setupCanvas(canvas);

  class Snowflake {
    constructor() { this.reset(true); }
    reset(initial) {
      this.x = rand(-20, w + 20);
      this.y = initial ? rand(-h, 0) : rand(-40, -5);
      this.size = rand(1.5, 5);
      this.speed = this.size * 0.4 + rand(0.2, 0.8);
      this.wind = rand(-0.3, 0.3);
      this.wobbleAmp = rand(0.5, 2);
      this.wobbleSpeed = rand(0.02, 0.06);
      this.phase = rand(0, Math.PI * 2);
      this.alpha = rand(0.3, 0.9);
      this.rotation = rand(0, Math.PI * 2);
      this.rotSpeed = rand(-0.02, 0.02);
      this.isStar = this.size > 3.5 && Math.random() > 0.5;
      this.sparkle = 0;
      this.sparkleSpeed = rand(0.03, 0.08);
    }
    update(frame) {
      this.y += this.speed;
      this.x += Math.sin(frame * this.wobbleSpeed + this.phase) * this.wobbleAmp * 0.3 + this.wind;
      this.rotation += this.rotSpeed;
      this.sparkle = 0.5 + 0.5 * Math.sin(frame * this.sparkleSpeed + this.phase);
      if (this.y > h - 40) {
        this.alpha *= 0.95;
        if (this.alpha < 0.01) this.reset(false);
      }
      if (this.x < -30 || this.x > w + 30) this.reset(false);
    }
    draw(ctx, frame) {
      ctx.save();
      ctx.translate(this.x, this.y);
      ctx.rotate(this.rotation);
      ctx.globalAlpha = this.alpha;

      if (this.isStar) {
        // Draw crystalline snowflake
        const s = this.size;
        ctx.strokeStyle = `rgba(220,220,240,${0.6 + this.sparkle * 0.4})`;
        ctx.lineWidth = 0.8;
        for (let i = 0; i < 6; i++) {
          ctx.save();
          ctx.rotate(i * Math.PI / 3);
          ctx.beginPath();
          ctx.moveTo(0, 0);
          ctx.lineTo(0, -s);
          // small branches
          ctx.moveTo(0, -s * 0.5);
          ctx.lineTo(-s * 0.3, -s * 0.7);
          ctx.moveTo(0, -s * 0.5);
          ctx.lineTo(s * 0.3, -s * 0.7);
          ctx.stroke();
          ctx.restore();
        }
        // Center glow
        if (this.sparkle > 0.7) {
          ctx.beginPath();
          ctx.arc(0, 0, s * 0.3, 0, Math.PI * 2);
          ctx.fillStyle = `rgba(220,230,255,${(this.sparkle - 0.7) * 0.5})`;
          ctx.fill();
        }
      } else {
        // Simple round snowflake
        const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size);
        gradient.addColorStop(0, `rgba(240,240,255,${0.8 + this.sparkle * 0.2})`);
        gradient.addColorStop(0.5, `rgba(220,220,240,${0.4 + this.sparkle * 0.2})`);
        gradient.addColorStop(1, 'rgba(200,200,230,0)');
        ctx.beginPath();
        ctx.arc(0, 0, this.size, 0, Math.PI * 2);
        ctx.fillStyle = gradient;
        ctx.fill();
      }

      ctx.restore();
    }
  }

  const flakes = Array.from({ length: 100 }, () => new Snowflake());
  let frame = 0;

  function animate() {
    ctx.clearRect(0, 0, w, h);
    frame++;
    flakes.forEach(f => {
      f.update(frame);
      f.draw(ctx, frame);
    });
    requestAnimationFrame(animate);
  }

  animate();

  window.addEventListener('resize', () => {
    ({ ctx, w, h } = setupCanvas(canvas));
  });
})();

// ============================================
//  WINDY – Wind streaks + particles + leaves
// ============================================
(function initWindy() {
  const canvas = document.getElementById('windyCanvas');
  let { ctx, w, h } = setupCanvas(canvas);

  class WindStreak {
    constructor() { this.reset(); }
    reset() {
      this.x = rand(-w * 0.5, 0);
      this.y = rand(0, h);
      this.len = rand(40, 150);
      this.speed = rand(4, 12);
      this.alpha = rand(0.03, 0.12);
      this.thickness = rand(0.5, 1.5);
      this.curve = rand(-0.3, 0.3);
    }
    update() {
      this.x += this.speed;
      this.y += this.curve;
      if (this.x > w + 50) this.reset();
    }
    draw(ctx) {
      ctx.beginPath();
      ctx.moveTo(this.x, this.y);
      ctx.quadraticCurveTo(
        this.x + this.len * 0.5, this.y + this.curve * 10,
        this.x + this.len, this.y + this.curve * 5
      );
      ctx.strokeStyle = `rgba(180,190,200,${this.alpha})`;
      ctx.lineWidth = this.thickness;
      ctx.lineCap = 'round';
      ctx.stroke();
    }
  }

  class Leaf {
    constructor() { this.reset(); }
    reset() {
      this.x = rand(-50, -10);
      this.y = rand(h * 0.1, h * 0.9);
      this.size = rand(3, 7);
      this.speed = rand(3, 8);
      this.vy = rand(-1, 1);
      this.rotation = rand(0, Math.PI * 2);
      this.rotSpeed = rand(0.05, 0.15) * (Math.random() > 0.5 ? 1 : -1);
      this.alpha = rand(0.2, 0.5);
      this.wobbleAmp = rand(1, 3);
      this.wobbleSpeed = rand(0.03, 0.08);
      this.phase = rand(0, Math.PI * 2);
      this.color = [
        'rgba(100,130,80,',
        'rgba(140,120,60,',
        'rgba(120,100,50,',
        'rgba(80,110,70,',
      ][Math.floor(rand(0, 4))];
    }
    update(frame) {
      this.x += this.speed;
      this.y += this.vy + Math.sin(frame * this.wobbleSpeed + this.phase) * this.wobbleAmp * 0.5;
      this.rotation += this.rotSpeed;
      if (this.x > w + 30) this.reset();
    }
    draw(ctx) {
      ctx.save();
      ctx.translate(this.x, this.y);
      ctx.rotate(this.rotation);
      ctx.globalAlpha = this.alpha;
      ctx.beginPath();
      ctx.ellipse(0, 0, this.size, this.size * 0.4, 0, 0, Math.PI * 2);
      ctx.fillStyle = this.color + '1)';
      ctx.fill();
      // Leaf vein
      ctx.beginPath();
      ctx.moveTo(-this.size * 0.8, 0);
      ctx.lineTo(this.size * 0.8, 0);
      ctx.strokeStyle = this.color + '0.5)';
      ctx.lineWidth = 0.5;
      ctx.stroke();
      ctx.restore();
    }
  }

  class DustParticle {
    constructor() { this.reset(); }
    reset() {
      this.x = rand(-20, 0);
      this.y = rand(0, h);
      this.size = rand(0.5, 2);
      this.speed = rand(2, 6);
      this.alpha = rand(0.05, 0.2);
      this.vy = rand(-0.5, 0.5);
    }
    update() {
      this.x += this.speed;
      this.y += this.vy;
      if (this.x > w + 10) this.reset();
    }
    draw(ctx) {
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
      ctx.fillStyle = `rgba(160,170,180,${this.alpha})`;
      ctx.fill();
    }
  }

  const streaks = Array.from({ length: 30 }, () => new WindStreak());
  const leaves = Array.from({ length: 8 }, () => new Leaf());
  const dust = Array.from({ length: 40 }, () => new DustParticle());
  let frame = 0;

  function animate() {
    ctx.clearRect(0, 0, w, h);
    frame++;
    streaks.forEach(s => { s.update(); s.draw(ctx); });
    dust.forEach(d => { d.update(); d.draw(ctx); });
    leaves.forEach(l => { l.update(frame); l.draw(ctx); });
    requestAnimationFrame(animate);
  }

  animate();

  window.addEventListener('resize', () => {
    ({ ctx, w, h } = setupCanvas(canvas));
  });
})();

// ============================================
//  HIGHLIGHT / FILTER CONTROLS
// ============================================
function highlightWeather(type) {
  // Update buttons
  document.querySelectorAll('.ctrl-btn').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.weather === type);
  });

  const cards = document.querySelectorAll('.weather-card');

  if (type === 'all') {
    cards.forEach(card => {
      card.classList.remove('dimmed', 'highlighted');
    });
    return;
  }

  cards.forEach(card => {
    if (card.dataset.type === type) {
      card.classList.add('highlighted');
      card.classList.remove('dimmed');
    } else {
      card.classList.add('dimmed');
      card.classList.remove('highlighted');
    }
  });
}

// Initialize "all" as active
highlightWeather('all');
document.querySelector('.ctrl-btn[data-weather="all"]').classList.add('active');

// ============================================
//  LIVE CLOCK
// ============================================
function updateTime() {
  const now = new Date();
  const options = {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false
  };
  document.getElementById('timeText').textContent =
    now.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' }) +
    ' ' + now.toLocaleTimeString('zh-CN', options);
}
updateTime();
setInterval(updateTime, 1000);

// ============================================
//  Card tilt effect (subtle 3D)
// ============================================
document.querySelectorAll('.weather-card').forEach(card => {
  card.addEventListener('mousemove', e => {
    const rect = card.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width;
    const y = (e.clientY - rect.top) / rect.height;
    const rotY = (x - 0.5) * 8;
    const rotX = (0.5 - y) * 8;
    card.style.transform = `perspective(800px) rotateX(${rotX}deg) rotateY(${rotY}deg) translateY(-8px) scale(1.02)`;
  });

  card.addEventListener('mouseleave', () => {
    card.style.transform = '';
  });
});

// ============================================
//  Auto-cycle demonstration
// ============================================
let autoCycleTimer = null;
let cycleIndex = 0;
const weatherTypes = ['sunny', 'rainy', 'snowy', 'windy', 'all'];

function startAutoCycle() {
  stopAutoCycle();
  autoCycleTimer = setInterval(() => {
    cycleIndex = (cycleIndex + 1) % weatherTypes.length;
    highlightWeather(weatherTypes[cycleIndex]);
  }, 4000);
}

function stopAutoCycle() {
  if (autoCycleTimer) clearInterval(autoCycleTimer);
}

// Stop auto-cycle when user interacts
document.querySelectorAll('.ctrl-btn').forEach(btn => {
  btn.addEventListener('click', stopAutoCycle);
});

// Start auto-cycle after 10 seconds of no interaction
let idleTimer = setTimeout(startAutoCycle, 10000);

document.addEventListener('click', () => {
  clearTimeout(idleTimer);
  stopAutoCycle();
  idleTimer = setTimeout(startAutoCycle, 15000);
});

console.log('%c🌤 Weather Engine Initialized', 'color: #ffb340; font-size: 14px; font-weight: bold;');
console.log('%c4 animated weather cards · Canvas particle rendering · 60fps', 'color: #888; font-size: 11px;');
</script>

</body>
</html>
17 Likes

感觉还可以啊 :distorted_face:

有Gemini那味儿了

还不错啊

1 Like

不赖啊我感觉

1 Like

怎么感觉没有经典的渐变紫了

雷雨天气还带闪电

1 Like

看起来gemini的前端打不过claude了,TPU难不成是前端增幅器?:rofl:

2 Likes

还是好看的
横看成gemini侧成claude

比隔壁 gpt-5.3-codex 审美好些,不知道啥时候能用上让俺试试

试一下codex的呢

很不错啊

1 Like

比隔壁的glm5好看 :distorted_face:

1 Like

佬,提示词能分享一下吗 :face_blowing_a_kiss:

你是一位就职于苹果公司的顶级前端工程师。请创建一个包含CSS和JavaScript的HTML文件,用于生成动画天气卡片。卡片需要以不同动画效果直观展示以下天气状况:

风力(如:飘动的云朵、摇曳的树木或风线)
降雨(如:下落的雨滴、形成的水洼)
晴天(如:闪耀的光线、明亮的背景)
下雪(如:飘落的雪花、积雪效果)

所有天气卡片需要并排显示,背景采用深色设计。所有HTML、CSS和JavaScript代码都需包含在这个单一文件中。JavaScript部分需包含切换不同天气状态的功能(例如通过函数或按钮组),以演示每种天气的动画效果。

将前端显示效果优化得更精致流畅,打造出价值20元/月的精品天气网页既视感。
2 Likes

还行吧~

谢谢佬友

审美好独特