Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> | |
| <title>Pro Weather + NASA Portal | Jarvis Build</title> | |
| <!-- Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;400;600;700&family=Space+Grotesk:wght@300;500;700&display=swap" rel="stylesheet"> | |
| <!-- Leaflet CSS (Map) --> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> | |
| <!-- Three.js (3D) - Using r128 --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> | |
| <!-- Leaflet JS (Map) --> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> | |
| <style> | |
| /* ================= VARIABLES & RESET ================= */ | |
| :root { | |
| --bg-dark: #0a0e1a; | |
| --bg-panel: rgba(255, 255, 255, 0.03); | |
| --bg-panel-hover: rgba(255, 255, 255, 0.08); | |
| --border-light: rgba(255, 255, 255, 0.08); | |
| --accent-primary: #00f0ff; | |
| --accent-secondary: #a78bfa; | |
| --accent-tertiary: #ec4899; | |
| --accent-warning: #fbbf24; | |
| --accent-danger: #ef4444; | |
| --accent-success: #10b981; | |
| --text-main: #ffffff; | |
| --text-muted: rgba(255, 255, 255, 0.5); | |
| --font-main: 'Outfit', sans-serif; | |
| --font-display: 'Space Grotesk', sans-serif; | |
| --glass-blur: blur(20px); | |
| --radius-lg: 28px; | |
| --radius-md: 18px; | |
| --radius-sm: 10px; | |
| --shadow-glow: 0 0 40px rgba(0, 240, 255, 0.15); | |
| --shadow-card: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| } | |
| /* Light Theme */ | |
| body.light-mode { | |
| --bg-dark: #f5f7fa; | |
| --bg-panel: rgba(255, 255, 255, 0.8); | |
| --bg-panel-hover: rgba(255, 255, 255, 0.95); | |
| --border-light: rgba(0, 0, 0, 0.06); | |
| --text-main: #1a1c2e; | |
| --text-muted: rgba(0, 0, 0, 0.5); | |
| --shadow-glow: 0 0 40px rgba(167, 139, 250, 0.2); | |
| --shadow-card: 0 8px 32px rgba(0, 0, 0, 0.08); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; outline: none; } | |
| body { | |
| font-family: var(--font-main); | |
| background: var(--bg-dark); | |
| background-image: | |
| radial-gradient(circle at 15% 15%, rgba(167, 139, 250, 0.12) 0%, transparent 50%), | |
| radial-gradient(circle at 85% 85%, rgba(0, 240, 255, 0.08) 0%, transparent 50%), | |
| radial-gradient(circle at 50% 50%, rgba(236, 72, 153, 0.05) 0%, transparent 70%); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { width: 10px; height: 10px; } | |
| ::-webkit-scrollbar-track { background: rgba(0,0,0,0.05); } | |
| ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 5px; } | |
| ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); } | |
| /* ================= LAYOUT ================= */ | |
| .app-wrapper { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 24px; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 36px; | |
| padding: 16px 0; | |
| animation: slideDown 0.6s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .brand h1 { | |
| font-family: var(--font-display); | |
| font-size: 2rem; | |
| font-weight: 700; | |
| letter-spacing: -0.03em; | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .brand span { | |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary), var(--accent-tertiary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| animation: gradient-shift 3s ease infinite; | |
| background-size: 200% 200%; | |
| } | |
| .header-controls { | |
| display: flex; | |
| gap: 12px; | |
| align-items: center; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border-light); | |
| color: var(--text-main); | |
| padding: 12px 20px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| backdrop-filter: var(--glass-blur); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 0; | |
| height: 0; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.1); | |
| transform: translate(-50%, -50%); | |
| transition: width 0.4s, height 0.4s; | |
| } | |
| .btn:hover::before { | |
| width: 300px; | |
| height: 300px; | |
| } | |
| .btn:hover { | |
| background: var(--bg-panel-hover); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-card); | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); | |
| border: none; | |
| color: #000; | |
| font-weight: 700; | |
| box-shadow: var(--shadow-glow); | |
| } | |
| .btn-primary:hover { | |
| opacity: 0.9; | |
| transform: translateY(-2px) scale(1.02); | |
| box-shadow: 0 0 50px rgba(0, 240, 255, 0.3); | |
| } | |
| .btn-icon { | |
| padding: 12px; | |
| border-radius: 50%; | |
| width: 44px; | |
| height: 44px; | |
| justify-content: center; | |
| font-size: 1.2rem; | |
| } | |
| /* ================= SEARCH SECTION ================= */ | |
| .search-container { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 36px; | |
| background: var(--bg-panel); | |
| padding: 10px; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-light); | |
| backdrop-filter: var(--glass-blur); | |
| box-shadow: var(--shadow-card); | |
| animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.1s both; | |
| transition: all 0.3s ease; | |
| } | |
| .search-container:focus-within { | |
| border-color: var(--accent-primary); | |
| box-shadow: 0 0 30px rgba(0, 240, 255, 0.2); | |
| } | |
| .search-input { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-main); | |
| padding: 0 16px; | |
| font-size: 1.05rem; | |
| font-family: var(--font-main); | |
| } | |
| .search-input::placeholder { color: var(--text-muted); } | |
| /* ================= SECTION 1: WEATHER & MAP ================= */ | |
| .dashboard-grid { | |
| display: grid; | |
| grid-template-columns: 1.6fr 1fr; | |
| gap: 24px; | |
| margin-bottom: 36px; | |
| animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both; | |
| } | |
| .card { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-lg); | |
| padding: 28px; | |
| backdrop-filter: var(--glass-blur); | |
| overflow: hidden; | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| box-shadow: var(--shadow-card); | |
| position: relative; | |
| } | |
| .card::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| right: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: radial-gradient(circle, rgba(255,255,255,0.03) 0%, transparent 70%); | |
| opacity: 0; | |
| transition: opacity 0.4s ease; | |
| pointer-events: none; | |
| } | |
| .card:hover::before { | |
| opacity: 1; | |
| } | |
| .card:hover { | |
| transform: translateY(-4px); | |
| box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4); | |
| border-color: rgba(255, 255, 255, 0.12); | |
| } | |
| /* Weather Hero */ | |
| .weather-hero { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| position: relative; | |
| overflow: visible; | |
| } | |
| .weather-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 24px; | |
| } | |
| .location-tag { | |
| font-family: var(--font-display); | |
| font-size: 2.8rem; | |
| font-weight: 700; | |
| line-height: 1.1; | |
| background: linear-gradient(135deg, var(--text-main), var(--text-muted)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .location-date { | |
| color: var(--text-muted); | |
| font-size: 1rem; | |
| margin-top: 6px; | |
| font-weight: 300; | |
| } | |
| .weather-main { | |
| display: flex; | |
| align-items: center; | |
| gap: 28px; | |
| margin: 20px 0; | |
| } | |
| .temp-big { | |
| font-size: 6rem; | |
| font-weight: 700; | |
| font-family: var(--font-display); | |
| line-height: 0.9; | |
| background: linear-gradient(180deg, var(--text-main) 0%, var(--accent-primary) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 0 60px rgba(0, 240, 255, 0.3); | |
| } | |
| .weather-icon-lg { | |
| width: 140px; | |
| height: 140px; | |
| filter: drop-shadow(0 0 25px rgba(255,255,255,0.3)); | |
| animation: float 5s ease-in-out infinite; | |
| } | |
| .weather-condition { | |
| font-size: 1.6rem; | |
| text-transform: capitalize; | |
| color: var(--accent-primary); | |
| font-weight: 600; | |
| letter-spacing: -0.02em; | |
| } | |
| /* Weather Grid Stats */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 14px; | |
| margin-top: 28px; | |
| } | |
| .stat-box { | |
| background: rgba(255,255,255,0.02); | |
| padding: 16px; | |
| border-radius: var(--radius-md); | |
| text-align: center; | |
| border: 1px solid var(--border-light); | |
| transition: all 0.3s ease; | |
| } | |
| .stat-box:hover { | |
| background: rgba(255,255,255,0.05); | |
| transform: translateY(-2px); | |
| } | |
| .stat-label { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| margin-bottom: 6px; | |
| display: block; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| font-weight: 500; | |
| } | |
| .stat-val { | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| color: var(--text-main); | |
| } | |
| .aqi-badge { | |
| display: inline-block; | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| letter-spacing: 0.03em; | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| .aqi-good { background: rgba(16, 185, 129, 0.2); color: var(--accent-success); } | |
| .aqi-fair { background: rgba(251, 191, 36, 0.2); color: var(--accent-warning); } | |
| .aqi-poor { background: rgba(239, 68, 68, 0.2); color: var(--accent-danger); } | |
| /* Map Card */ | |
| .map-wrapper { | |
| height: 100%; | |
| min-height: 340px; | |
| border-radius: var(--radius-md); | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| #map { | |
| height: 100%; | |
| width: 100%; | |
| z-index: 1; | |
| filter: saturate(1.2) contrast(1.1); | |
| } | |
| /* Forecast Row */ | |
| .forecast-row { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 16px; | |
| margin-bottom: 48px; | |
| animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.3s both; | |
| } | |
| .forecast-card { | |
| background: var(--bg-panel); | |
| padding: 20px; | |
| border-radius: var(--radius-md); | |
| text-align: center; | |
| border: 1px solid var(--border-light); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| backdrop-filter: var(--glass-blur); | |
| } | |
| .forecast-card:hover { | |
| transform: translateY(-6px); | |
| box-shadow: var(--shadow-card); | |
| background: var(--bg-panel-hover); | |
| } | |
| .fc-day { | |
| font-size: 0.95rem; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .fc-icon { | |
| width: 50px; | |
| height: 50px; | |
| margin: 8px auto; | |
| display: block; | |
| filter: drop-shadow(0 2px 8px rgba(255,255,255,0.2)); | |
| } | |
| .fc-temp { | |
| font-weight: 700; | |
| margin-top: 12px; | |
| font-size: 1.3rem; | |
| color: var(--accent-primary); | |
| } | |
| /* ================= SECTION 2: NASA PORTAL ================= */ | |
| .section-title { | |
| font-family: var(--font-display); | |
| font-size: 1.8rem; | |
| margin-bottom: 24px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| } | |
| .section-title::before { | |
| content: ''; | |
| display: block; | |
| width: 5px; | |
| height: 32px; | |
| background: linear-gradient(180deg, var(--accent-secondary), var(--accent-tertiary)); | |
| border-radius: 3px; | |
| box-shadow: 0 0 15px var(--accent-secondary); | |
| } | |
| .nasa-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 24px; | |
| margin-bottom: 48px; | |
| animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.4s both; | |
| } | |
| /* APOD spans 2 columns */ | |
| .nasa-apod { | |
| grid-column: span 2; | |
| position: relative; | |
| height: 450px; | |
| cursor: pointer; | |
| overflow: hidden; | |
| } | |
| .apod-bg { | |
| position: absolute; | |
| inset: 0; | |
| background-size: cover; | |
| background-position: center; | |
| transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); | |
| filter: brightness(0.85); | |
| } | |
| .nasa-apod:hover .apod-bg { | |
| transform: scale(1.05); | |
| filter: brightness(1); | |
| } | |
| .apod-content { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| padding: 36px; | |
| background: linear-gradient(0deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.7) 60%, transparent); | |
| z-index: 2; | |
| transition: padding 0.3s ease; | |
| } | |
| .nasa-apod:hover .apod-content { | |
| padding-bottom: 42px; | |
| } | |
| .apod-tag { | |
| background: linear-gradient(135deg, var(--accent-secondary), var(--accent-tertiary)); | |
| padding: 6px 12px; | |
| border-radius: 6px; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| text-transform: uppercase; | |
| margin-bottom: 10px; | |
| display: inline-block; | |
| letter-spacing: 0.08em; | |
| } | |
| /* Lists */ | |
| .nasa-list-card { | |
| display: flex; | |
| flex-direction: column; | |
| height: 450px; | |
| } | |
| .nasa-list-header { | |
| font-weight: 700; | |
| border-bottom: 2px solid var(--border-light); | |
| padding-bottom: 14px; | |
| margin-bottom: 14px; | |
| font-size: 1.05rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .scroll-list { | |
| overflow-y: auto; | |
| flex: 1; | |
| padding-right: 6px; | |
| } | |
| .news-item { | |
| margin-bottom: 14px; | |
| padding-bottom: 14px; | |
| border-bottom: 1px solid var(--border-light); | |
| transition: all 0.2s ease; | |
| } | |
| .news-item:last-child { border: none; } | |
| .news-item:hover { | |
| padding-left: 8px; | |
| border-color: var(--accent-primary); | |
| } | |
| .news-title { | |
| font-size: 0.95rem; | |
| font-weight: 600; | |
| margin-bottom: 6px; | |
| color: var(--accent-primary); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| line-height: 1.4; | |
| } | |
| .news-title:hover { | |
| color: var(--accent-secondary); | |
| text-decoration: underline; | |
| } | |
| .news-meta { | |
| font-size: 0.78rem; | |
| color: var(--text-muted); | |
| font-weight: 400; | |
| } | |
| /* Space Weather */ | |
| .space-weather-card { | |
| background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(11, 13, 26, 0.5)); | |
| } | |
| .sw-badge { | |
| display: inline-block; | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| font-size: 0.75rem; | |
| font-weight: bold; | |
| margin-right: 8px; | |
| letter-spacing: 0.05em; | |
| } | |
| .bg-danger { background: rgba(239, 68, 68, 0.25); color: var(--accent-danger); } | |
| .bg-warn { background: rgba(251, 191, 36, 0.25); color: var(--accent-warning); } | |
| .bg-info { background: rgba(0, 240, 255, 0.2); color: var(--accent-primary); } | |
| /* Image Search Results */ | |
| .img-search-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); | |
| gap: 14px; | |
| margin-top: 18px; | |
| max-height: 340px; | |
| overflow-y: auto; | |
| } | |
| .img-result { | |
| aspect-ratio: 1; | |
| background: #000; | |
| border-radius: var(--radius-sm); | |
| overflow: hidden; | |
| position: relative; | |
| cursor: pointer; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .img-result img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .img-result:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); | |
| z-index: 10; | |
| } | |
| .img-result:hover img { | |
| transform: scale(1.15); | |
| } | |
| /* ================= 3D SOLAR SYSTEM MODAL ================= */ | |
| .solar-modal { | |
| position: fixed; | |
| inset: 0; | |
| background: #000; | |
| z-index: 9999; | |
| display: flex; | |
| flex-direction: column; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .solar-modal.active { | |
| opacity: 1; | |
| pointer-events: auto; | |
| } | |
| .solar-ui { | |
| position: absolute; | |
| top: 24px; | |
| left: 24px; | |
| right: 24px; | |
| display: flex; | |
| justify-content: space-between; | |
| z-index: 10; | |
| pointer-events: none; | |
| } | |
| .solar-ui button { pointer-events: auto; } | |
| .solar-canvas { width: 100%; height: 100%; } | |
| /* ================= UTILS ================= */ | |
| .hidden { display: none ; } | |
| .loader { | |
| border: 4px solid rgba(255,255,255,0.1); | |
| border-top: 4px solid var(--accent-primary); | |
| border-radius: 50%; | |
| width: 48px; | |
| height: 48px; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| .error-message { | |
| background: rgba(239, 68, 68, 0.15); | |
| border: 1px solid var(--accent-danger); | |
| color: var(--accent-danger); | |
| padding: 16px 20px; | |
| border-radius: var(--radius-md); | |
| margin: 20px 0; | |
| font-weight: 600; | |
| animation: shake 0.5s ease; | |
| } | |
| /* ================= ANIMATIONS ================= */ | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes fadeInUp { | |
| from { opacity: 0; transform: translateY(30px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideDown { | |
| from { opacity: 0; transform: translateY(-20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-15px); } | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.8; } | |
| } | |
| @keyframes gradient-shift { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-10px); } | |
| 75% { transform: translateX(10px); } | |
| } | |
| /* Mobile Responsive */ | |
| @media (max-width: 900px) { | |
| .app-wrapper { padding: 16px; } | |
| .dashboard-grid { grid-template-columns: 1fr; } | |
| .nasa-grid { grid-template-columns: 1fr; } | |
| .nasa-apod { grid-column: span 1; height: 320px; } | |
| .forecast-row { grid-template-columns: repeat(2, 1fr); } | |
| .forecast-card:nth-child(5) { grid-column: span 2; } | |
| .temp-big { font-size: 4rem; } | |
| .weather-main { flex-direction: column; text-align: center; } | |
| .weather-header { flex-direction: column; gap: 12px; } | |
| .stats-grid { grid-template-columns: repeat(2, 1fr); } | |
| .header-controls { gap: 8px; } | |
| .btn { padding: 10px 14px; font-size: 0.85rem; } | |
| .section-title { font-size: 1.4rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-wrapper"> | |
| <!-- Header --> | |
| <header> | |
| <div class="brand"> | |
| <h1>ProWeather <span>+</span></h1> | |
| </div> | |
| <div class="header-controls"> | |
| <button class="btn btn-primary" onclick="openSolarSystem()"> | |
| <span>🪐 3D Solar</span> | |
| </button> | |
| <button class="btn btn-icon" onclick="toggleTheme()" title="Toggle Theme"> | |
| 🌗 | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Search --> | |
| <div class="search-container"> | |
| <input type="text" id="cityInput" class="search-input" placeholder="Search city (e.g., Tokyo, New York, London)..." onkeypress="handleEnter(event)"> | |
| <button class="btn btn-primary" id="searchBtn" onclick="initApp()">Search</button> | |
| <button class="btn" onclick="getGeoLocation()">📍 Location</button> | |
| </div> | |
| <!-- Loading Indicator --> | |
| <div id="loadingState" class="hidden" style="text-align: center; padding: 60px;"> | |
| <div class="loader" style="margin: 0 auto 16px;"></div> | |
| <p style="color: var(--text-muted); font-size: 1.1rem; font-weight: 500;">Scanning Atmospheric Data...</p> | |
| </div> | |
| <!-- Error Message --> | |
| <div id="errorMessage" class="hidden error-message"></div> | |
| <!-- WEATHER SECTION --> | |
| <div id="weatherDashboard" class="hidden"> | |
| <div class="dashboard-grid"> | |
| <!-- Main Weather Card --> | |
| <div class="card weather-hero"> | |
| <div class="weather-header"> | |
| <div> | |
| <h2 class="location-tag" id="cityName">--</h2> | |
| <p class="location-date" id="currentDate">--</p> | |
| </div> | |
| <div id="aqiBadge" class="aqi-badge hidden">AQI: --</div> | |
| </div> | |
| <div class="weather-main"> | |
| <img id="weatherIcon" class="weather-icon-lg" src="" alt="Weather"> | |
| <div> | |
| <div class="temp-big" id="temperature">--°</div> | |
| <div class="weather-condition" id="description">--</div> | |
| </div> | |
| </div> | |
| <div class="stats-grid"> | |
| <div class="stat-box"> | |
| <span class="stat-label">Humidity</span> | |
| <div class="stat-val" id="humidity">--%</div> | |
| </div> | |
| <div class="stat-box"> | |
| <span class="stat-label">Wind</span> | |
| <div class="stat-val" id="wind">-- m/s</div> | |
| </div> | |
| <div class="stat-box"> | |
| <span class="stat-label">Pressure</span> | |
| <div class="stat-val" id="pressure">-- hPa</div> | |
| </div> | |
| <div class="stat-box"> | |
| <span class="stat-label">Feels Like</span> | |
| <div class="stat-val" id="feelslike">--°</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Map Card --> | |
| <div class="card" style="padding: 0; overflow: hidden;"> | |
| <div class="map-wrapper"> | |
| <div id="map"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Forecast Row --> | |
| <h3 class="section-title">5-Day Forecast</h3> | |
| <div id="forecastRow" class="forecast-row"></div> | |
| </div> | |
| <!-- NASA SECTION --> | |
| <div id="nasaSection" class="hidden"> | |
| <h3 class="section-title">NASA Intelligence Hub</h3> | |
| <div class="nasa-grid"> | |
| <!-- APOD --> | |
| <div class="card nasa-apod" id="apodCard"> | |
| <div class="apod-bg" id="apodBg"></div> | |
| <div class="apod-content"> | |
| <span class="apod-tag">Image of the Day</span> | |
| <h2 style="font-size: 1.5rem; margin-bottom: 8px; font-weight: 700;" id="apodTitle">Loading Space View...</h2> | |
| <p style="font-size: 0.9rem; opacity: 0.85; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;" id="apodDesc">Fetching latest imagery from deep space...</p> | |
| </div> | |
| </div> | |
| <!-- Space Weather --> | |
| <div class="card nasa-list-card space-weather-card"> | |
| <div class="nasa-list-header">⚡ Space Weather</div> | |
| <div class="scroll-list" id="spaceWeatherList"> | |
| <p style="padding: 12px; color: var(--text-muted); text-align: center;">Monitoring solar activity...</p> | |
| </div> | |
| </div> | |
| <!-- Earth Watch --> | |
| <div class="card nasa-list-card"> | |
| <div class="nasa-list-header">🌍 Earth Events</div> | |
| <div class="scroll-list" id="earthNewsList"> | |
| <p style="padding: 12px; color: var(--text-muted); text-align: center;">Scanning natural events...</p> | |
| </div> | |
| </div> | |
| <!-- NASA Image Search --> | |
| <div class="card" style="grid-column: 1 / -1;"> | |
| <div class="nasa-list-header">🔭 NASA Image Library</div> | |
| <div style="display: flex; gap: 12px;"> | |
| <input type="text" id="nasaSearchInput" class="search-input" style="background: rgba(0,0,0,0.15); font-size: 0.95rem; border: 1px solid var(--border-light); border-radius: var(--radius-sm); padding: 12px 16px;" placeholder="Search galaxies, stars, missions..." onkeypress="handleNasaSearchEnter(event)"> | |
| <button class="btn btn-primary" onclick="searchNasaImages()">Find</button> | |
| </div> | |
| <div id="nasaImageResults" class="img-search-grid"></div> | |
| </div> | |
| <!-- Tech Projects --> | |
| <div class="card nasa-list-card"> | |
| <div class="nasa-list-header">🚀 TechPort</div> | |
| <div class="scroll-list" id="techList"> | |
| <p style="padding: 12px; color: var(--text-muted); text-align: center;">Loading projects...</p> | |
| </div> | |
| </div> | |
| <!-- Asteroids --> | |
| <div class="card nasa-list-card"> | |
| <div class="nasa-list-header">☄️ Near-Earth Objects</div> | |
| <div class="scroll-list" id="neoList"> | |
| <p style="padding: 12px; color: var(--text-muted); text-align: center;">Scanning sky...</p> | |
| </div> | |
| </div> | |
| <!-- Mars Rover --> | |
| <div class="card" style="grid-column: span 1; height: 450px; position: relative; overflow: hidden;"> | |
| <div class="nasa-list-header" style="position: absolute; top: 20px; left: 20px; z-index: 2; background: rgba(0,0,0,0.7); padding: 6px 12px; border-radius: 8px; backdrop-filter: blur(10px);">🤖 Mars Rover</div> | |
| <img id="marsImg" src="" style="width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease;" alt="Mars" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 3D Solar System Modal --> | |
| <div id="solarModal" class="solar-modal"> | |
| <div class="solar-ui"> | |
| <h2 style="color: white; font-family: var(--font-display); font-size: 1.8rem;">Interactive Solar System</h2> | |
| <button class="btn" style="background: rgba(255,255,255,0.15); color: white; border: 1px solid rgba(255,255,255,0.2);" onclick="closeSolarSystem()">Close ✕</button> | |
| </div> | |
| <div id="solarContainer" class="solar-canvas"></div> | |
| </div> | |
| <script> | |
| /* ================= CONFIGURATION ================= */ | |
| const API_KEYS = { | |
| WEATHER: "7d9745a7db624a8a511e10c0b9890f71", | |
| NASA: "FaAyJO3S976lsFoLz6izgd06bTEikTvYmC0MflK4" | |
| }; | |
| let mapInstance = null; | |
| let mapMarker = null; | |
| const $ = id => document.getElementById(id); | |
| /* ================= ERROR HANDLING ================= */ | |
| function showError(message) { | |
| const errorEl = $("errorMessage"); | |
| errorEl.textContent = "⚠️ " + message; | |
| errorEl.classList.remove("hidden"); | |
| setTimeout(() => errorEl.classList.add("hidden"), 5000); | |
| } | |
| /* ================= APP INITIALIZATION ================= */ | |
| async function initApp() { | |
| const city = $("cityInput").value.trim(); | |
| if (!city) { | |
| showError("Please enter a city name"); | |
| return; | |
| } | |
| toggleLoading(true); | |
| $("errorMessage").classList.add("hidden"); | |
| try { | |
| const weatherData = await fetchWeather(city); | |
| if (weatherData) { | |
| renderWeather(weatherData); | |
| fetchForecast(weatherData.coord.lat, weatherData.coord.lon); | |
| fetchAQI(weatherData.coord.lat, weatherData.coord.lon); | |
| updateMap(weatherData.coord.lat, weatherData.coord.lon, weatherData.name); | |
| $("weatherDashboard").classList.remove("hidden"); | |
| $("nasaSection").classList.remove("hidden"); | |
| loadNASAData(); | |
| } | |
| } catch (error) { | |
| console.error("App Error:", error); | |
| showError(error.message || "Failed to fetch data. Please try again."); | |
| } finally { | |
| toggleLoading(false); | |
| } | |
| } | |
| function handleEnter(e) { | |
| if (e.key === 'Enter') initApp(); | |
| } | |
| function handleNasaSearchEnter(e) { | |
| if (e.key === 'Enter') searchNasaImages(); | |
| } | |
| function toggleLoading(show) { | |
| $("loadingState").classList.toggle("hidden", !show); | |
| if(show) { | |
| $("weatherDashboard").classList.add("hidden"); | |
| $("nasaSection").classList.add("hidden"); | |
| } | |
| } | |
| /* ================= WEATHER LOGIC ================= */ | |
| async function fetchWeather(city) { | |
| const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${API_KEYS.WEATHER}&units=metric`; | |
| const res = await fetch(url); | |
| if (!res.ok) { | |
| if (res.status === 404) throw new Error("City not found"); | |
| if (res.status === 401) throw new Error("Invalid API key"); | |
| throw new Error("Weather service unavailable"); | |
| } | |
| return await res.json(); | |
| } | |
| function renderWeather(data) { | |
| $("cityName").textContent = `${data.name}, ${data.sys.country}`; | |
| $("currentDate").textContent = new Date().toLocaleDateString('en-US', { | |
| weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' | |
| }); | |
| $("temperature").textContent = `${Math.round(data.main.temp)}°`; | |
| $("description").textContent = data.weather[0].description; | |
| $("humidity").textContent = `${data.main.humidity}%`; | |
| $("wind").textContent = `${data.wind.speed} m/s`; | |
| $("pressure").textContent = `${data.main.pressure} hPa`; | |
| $("feelslike").textContent = `${Math.round(data.main.feels_like)}°`; | |
| const iconCode = data.weather[0].icon; | |
| $("weatherIcon").src = `https://openweathermap.org/img/wn/${iconCode}@4x.png`; | |
| $("weatherIcon").alt = data.weather[0].description; | |
| } | |
| async function fetchAQI(lat, lon) { | |
| try { | |
| const url = `https://api.openweathermap.org/data/2.5/air_pollution?lat=${lat}&lon=${lon}&appid=${API_KEYS.WEATHER}`; | |
| const res = await fetch(url); | |
| if (!res.ok) throw new Error("AQI unavailable"); | |
| const data = await res.json(); | |
| const aqi = data.list[0].main.aqi; | |
| const badge = $("aqiBadge"); | |
| badge.classList.remove("hidden", "aqi-good", "aqi-fair", "aqi-poor"); | |
| let label = "Unknown"; | |
| if (aqi <= 2) { label = "Good"; badge.classList.add("aqi-good"); } | |
| else if (aqi <= 3) { label = "Fair"; badge.classList.add("aqi-fair"); } | |
| else { label = "Poor"; badge.classList.add("aqi-poor"); } | |
| badge.textContent = `Air Quality: ${label}`; | |
| } catch (e) { | |
| console.warn("AQI Error:", e); | |
| } | |
| } | |
| async function fetchForecast(lat, lon) { | |
| try { | |
| const url = `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${API_KEYS.WEATHER}&units=metric`; | |
| const res = await fetch(url); | |
| if (!res.ok) throw new Error("Forecast unavailable"); | |
| const data = await res.json(); | |
| const container = $("forecastRow"); | |
| container.innerHTML = ""; | |
| const daily = data.list.filter(item => item.dt_txt.includes("12:00:00")).slice(0, 5); | |
| daily.forEach(day => { | |
| const date = new Date(day.dt * 1000).toLocaleDateString("en-US", { weekday: 'short' }); | |
| const div = document.createElement("div"); | |
| div.className = "forecast-card"; | |
| div.innerHTML = ` | |
| <div class="fc-day">${date}</div> | |
| <img class="fc-icon" src="https://openweathermap.org/img/wn/${day.weather[0].icon}@2x.png" alt="${day.weather[0].description}"> | |
| <div class="fc-temp">${Math.round(day.main.temp)}°</div> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| } catch (e) { | |
| console.warn("Forecast Error:", e); | |
| } | |
| } | |
| function getGeoLocation() { | |
| if (!navigator.geolocation) { | |
| showError("Geolocation not supported by your browser"); | |
| return; | |
| } | |
| toggleLoading(true); | |
| navigator.geolocation.getCurrentPosition( | |
| async (pos) => { | |
| try { | |
| const { latitude, longitude } = pos.coords; | |
| const url = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEYS.WEATHER}&units=metric`; | |
| const res = await fetch(url); | |
| const data = await res.json(); | |
| $("cityInput").value = data.name; | |
| initApp(); | |
| } catch (e) { | |
| showError("Failed to get location data"); | |
| toggleLoading(false); | |
| } | |
| }, | |
| (error) => { | |
| showError("Location access denied"); | |
| toggleLoading(false); | |
| } | |
| ); | |
| } | |
| /* ================= MAP LOGIC ================= */ | |
| function updateMap(lat, lon, title) { | |
| try { | |
| if (!mapInstance) { | |
| mapInstance = L.map('map').setView([lat, lon], 11); | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { | |
| attribution: '© OpenStreetMap © CARTO', | |
| subdomains: 'abcd', | |
| maxZoom: 19 | |
| }).addTo(mapInstance); | |
| } else { | |
| mapInstance.setView([lat, lon], 11); | |
| } | |
| if (mapMarker) mapInstance.removeLayer(mapMarker); | |
| const customIcon = L.divIcon({ | |
| className: 'custom-pin', | |
| html: `<div style="background: var(--accent-primary); width: 16px; height: 16px; border-radius: 50%; box-shadow: 0 0 20px var(--accent-primary), 0 0 40px var(--accent-primary);"></div>` | |
| }); | |
| mapMarker = L.marker([lat, lon], { icon: customIcon }).addTo(mapInstance) | |
| .bindPopup(`<b style="font-family: var(--font-display);">${title}</b>`) | |
| .openPopup(); | |
| setTimeout(() => mapInstance.invalidateSize(), 400); | |
| } catch (e) { | |
| console.error("Map Error:", e); | |
| } | |
| } | |
| /* ================= NASA LOGIC ================= */ | |
| let nasaLoaded = false; | |
| async function loadNASAData() { | |
| if (nasaLoaded) return; | |
| nasaLoaded = true; | |
| // APOD | |
| fetch(`https://api.nasa.gov/planetary/apod?api_key=${API_KEYS.NASA}`) | |
| .then(r => r.json()) | |
| .then(data => { | |
| const imgUrl = data.media_type === "image" ? data.url : (data.thumbnail_url || "https://apod.nasa.gov/apod/image/2101/NGC1365_Hubble_960.jpg"); | |
| $("apodBg").style.backgroundImage = `url('${imgUrl}')`; | |
| $("apodTitle").textContent = data.title || "Astronomy Picture"; | |
| $("apodDesc").textContent = data.explanation || "Explore the cosmos"; | |
| }) | |
| .catch(e => console.warn("APOD Error:", e)); | |
| // TechPort | |
| fetch(`https://api.nasa.gov/techport/api/projects?api_key=${API_KEYS.NASA}`) | |
| .then(r => r.json()) | |
| .then(data => { | |
| const list = data.projects?.slice(0, 10) || []; | |
| const container = $("techList"); | |
| container.innerHTML = list.length ? "" : "<p style='padding:12px;text-align:center;color:var(--text-muted)'>No projects available</p>"; | |
| list.forEach(p => { | |
| const item = document.createElement("div"); | |
| item.className = "news-item"; | |
| item.innerHTML = ` | |
| <div class="news-title" onclick="window.open('https://techport.nasa.gov/view/${p.id}', '_blank')"> | |
| 🚀 ${p.title} | |
| </div> | |
| <div class="news-meta">Updated: ${new Date(p.lastUpdated).toLocaleDateString()}</div> | |
| `; | |
| container.appendChild(item); | |
| }); | |
| }) | |
| .catch(e => console.warn("TechPort Error:", e)); | |
| // NEO | |
| const today = new Date().toISOString().split("T")[0]; | |
| fetch(`https://api.nasa.gov/neo/rest/v1/feed?start_date=${today}&end_date=${today}&api_key=${API_KEYS.NASA}`) | |
| .then(r => r.json()) | |
| .then(data => { | |
| const asteroids = data.near_earth_objects[today] || []; | |
| const container = $("neoList"); | |
| container.innerHTML = asteroids.length ? "" : "<p style='padding:12px;text-align:center;color:var(--text-muted)'>No objects detected today</p>"; | |
| asteroids.slice(0, 8).forEach(a => { | |
| const size = Math.round(a.estimated_diameter.meters.estimated_diameter_max); | |
| const haz = a.is_potentially_hazardous_asteroid ? "⚠️" : "✅"; | |
| const item = document.createElement("div"); | |
| item.className = "news-item"; | |
| item.innerHTML = ` | |
| <div class="news-title">${haz} ${a.name}</div> | |
| <div class="news-meta">~${size}m • ${parseInt(a.close_approach_data[0].relative_velocity.kilometers_per_hour).toLocaleString()} km/h</div> | |
| `; | |
| container.appendChild(item); | |
| }); | |
| }) | |
| .catch(e => console.warn("NEO Error:", e)); | |
| // Mars Rover | |
| fetch(`https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/latest_photos?api_key=${API_KEYS.NASA}`) | |
| .then(r => r.json()) | |
| .then(data => { | |
| if(data.latest_photos && data.latest_photos.length > 0) { | |
| $("marsImg").src = data.latest_photos[0].img_src; | |
| } | |
| }) | |
| .catch(e => console.warn("Mars Error:", e)); | |
| loadSpaceWeather(); | |
| loadEarthEvents(); | |
| } | |
| /* ================= SPACE WEATHER ================= */ | |
| async function loadSpaceWeather() { | |
| const container = $("spaceWeatherList"); | |
| const endDate = new Date().toISOString().split("T")[0]; | |
| const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]; | |
| try { | |
| const res = await fetch(`https://api.nasa.gov/DONKI/FLR?startDate=${startDate}&endDate=${endDate}&api_key=${API_KEYS.NASA}`); | |
| if (!res.ok) throw new Error("DONKI unavailable"); | |
| const data = await res.json(); | |
| container.innerHTML = ""; | |
| if (data && data.length > 0) { | |
| data.slice(0, 6).forEach(f => { | |
| const item = document.createElement("div"); | |
| item.className = "news-item"; | |
| const power = f.classType || "Unk"; | |
| let badgeClass = "bg-info"; | |
| if(power.startsWith("M")) badgeClass = "bg-warn"; | |
| if(power.startsWith("X")) badgeClass = "bg-danger"; | |
| item.innerHTML = ` | |
| <div class="news-title"> | |
| <span class="sw-badge ${badgeClass}">${power}</span> Solar Flare Detected | |
| </div> | |
| <div class="news-meta">${new Date(f.beginTime).toLocaleString()}</div> | |
| `; | |
| container.appendChild(item); | |
| }); | |
| } else { | |
| container.innerHTML = "<p style='padding:12px;text-align:center;opacity:0.7'>No major solar activity this week</p>"; | |
| } | |
| } catch(e) { | |
| console.warn("DONKI Error:", e); | |
| container.innerHTML = "<p style='padding:12px;text-align:center;color:var(--accent-danger)'>Data unavailable</p>"; | |
| } | |
| } | |
| /* ================= EARTH EVENTS ================= */ | |
| async function loadEarthEvents() { | |
| const container = $("earthNewsList"); | |
| try { | |
| const res = await fetch("https://eonet.gsfc.nasa.gov/api/v2.1/events?limit=10"); | |
| if (!res.ok) throw new Error("EONET unavailable"); | |
| const data = await res.json(); | |
| container.innerHTML = ""; | |
| if (data.events && data.events.length > 0) { | |
| data.events.forEach(ev => { | |
| const item = document.createElement("div"); | |
| item.className = "news-item"; | |
| const cat = ev.categories[0]?.title || "Event"; | |
| item.innerHTML = ` | |
| <div class="news-title" onclick="window.open('${ev.sources[0]?.url || '#'}', '_blank')"> | |
| 🌍 ${ev.title} | |
| </div> | |
| <div class="news-meta">${cat} • ${new Date(ev.geometries[0].date).toLocaleDateString()}</div> | |
| `; | |
| container.appendChild(item); | |
| }); | |
| } else { | |
| container.innerHTML = "<p style='padding:12px;text-align:center;opacity:0.7'>No recent events</p>"; | |
| } | |
| } catch(e) { | |
| console.warn("EONET Error:", e); | |
| container.innerHTML = "<p style='padding:12px;text-align:center;color:var(--accent-danger)'>Data unavailable</p>"; | |
| } | |
| } | |
| /* ================= IMAGE SEARCH ================= */ | |
| async function searchNasaImages() { | |
| const query = $("nasaSearchInput").value.trim(); | |
| if(!query) { | |
| showError("Please enter a search term"); | |
| return; | |
| } | |
| const container = $("nasaImageResults"); | |
| container.innerHTML = "<p style='padding:16px;text-align:center;color:var(--text-muted)'>Searching NASA archives...</p>"; | |
| try { | |
| const res = await fetch(`https://images-api.nasa.gov/search?q=${encodeURIComponent(query)}&media_type=image`); | |
| if (!res.ok) throw new Error("Search failed"); | |
| const data = await res.json(); | |
| const items = data.collection?.items?.slice(0, 12) || []; | |
| container.innerHTML = ""; | |
| if(items.length === 0) { | |
| container.innerHTML = "<p style='padding:16px;text-align:center;color:var(--text-muted)'>No images found. Try different keywords.</p>"; | |
| return; | |
| } | |
| items.forEach(item => { | |
| const imgUrl = item.links?.[0]?.href; | |
| const title = item.data?.[0]?.title || "NASA Image"; | |
| if (!imgUrl) return; | |
| const div = document.createElement("div"); | |
| div.className = "img-result"; | |
| div.title = title; | |
| div.innerHTML = `<img src="${imgUrl}" alt="${title}" loading="lazy">`; | |
| div.onclick = () => window.open(imgUrl, '_blank'); | |
| container.appendChild(div); | |
| }); | |
| } catch(e) { | |
| console.error("Image Search Error:", e); | |
| container.innerHTML = "<p style='padding:16px;text-align:center;color:var(--accent-danger)'>Search failed. Please try again.</p>"; | |
| } | |
| } | |
| /* ================= 3D SOLAR SYSTEM ================= */ | |
| let scene, camera, renderer, animationId, solarControls; | |
| const planets = []; | |
| function openSolarSystem() { | |
| $("solarModal").classList.add("active"); | |
| if(!scene) initSolarSystem(); | |
| else animateSolar(); | |
| } | |
| function closeSolarSystem() { | |
| $("solarModal").classList.remove("active"); | |
| if(animationId) cancelAnimationFrame(animationId); | |
| } | |
| function initSolarSystem() { | |
| const container = $("solarContainer"); | |
| scene = new THREE.Scene(); | |
| camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 25, 110); | |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| container.appendChild(renderer.domElement); | |
| solarControls = new THREE.OrbitControls(camera, renderer.domElement); | |
| solarControls.enableDamping = true; | |
| solarControls.dampingFactor = 0.05; | |
| const ambientLight = new THREE.AmbientLight(0x404040, 0.8); | |
| scene.add(ambientLight); | |
| const sunLight = new THREE.PointLight(0xffffff, 2.5, 350); | |
| scene.add(sunLight); | |
| // Starfield | |
| const starGeo = new THREE.BufferGeometry(); | |
| const starCount = 3000; | |
| const posArray = new Float32Array(starCount * 3); | |
| for(let i=0; i<starCount*3; i++) { | |
| posArray[i] = (Math.random() - 0.5) * 1000; | |
| } | |
| starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); | |
| const starMat = new THREE.PointsMaterial({color: 0xffffff, size: 0.7}); | |
| const stars = new THREE.Points(starGeo, starMat); | |
| scene.add(stars); | |
| const textureLoader = new THREE.TextureLoader(); | |
| const createPlanet = (size, textureUrl, distance, speed) => { | |
| const geometry = new THREE.SphereGeometry(size, 32, 32); | |
| const material = new THREE.MeshStandardMaterial({ | |
| map: textureLoader.load(textureUrl), | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| const obj = new THREE.Object3D(); | |
| obj.add(mesh); | |
| mesh.position.x = distance; | |
| scene.add(obj); | |
| return { mesh, obj, speed, selfSpeed: Math.random() * 0.02 + 0.01 }; | |
| }; | |
| // Sun | |
| const sunGeo = new THREE.SphereGeometry(12, 32, 32); | |
| const sunMat = new THREE.MeshBasicMaterial({ | |
| map: textureLoader.load('https://threejs.org/examples/textures/planets/sun.jpg'), | |
| emissive: 0xffaa00, | |
| emissiveIntensity: 0.5 | |
| }); | |
| const sun = new THREE.Mesh(sunGeo, sunMat); | |
| scene.add(sun); | |
| // Planets | |
| planets.push(createPlanet(1.5, 'https://threejs.org/examples/textures/planets/mercury.jpg', 20, 0.024)); | |
| planets.push(createPlanet(2.8, 'https://threejs.org/examples/textures/planets/venus_atmos_2048.jpg', 32, 0.018)); | |
| planets.push(createPlanet(3.0, 'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg', 44, 0.012)); | |
| planets.push(createPlanet(2.2, 'https://threejs.org/examples/textures/planets/mars_1k_color.jpg', 56, 0.01)); | |
| planets.push(createPlanet(6.0, 'https://threejs.org/examples/textures/planets/jupiter2_1k.jpg', 80, 0.005)); | |
| window.addEventListener('resize', onWindowResize, false); | |
| animateSolar(); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function animateSolar() { | |
| animationId = requestAnimationFrame(animateSolar); | |
| planets.forEach(p => { | |
| p.obj.rotation.y += p.speed; | |
| p.mesh.rotation.y += p.selfSpeed; | |
| }); | |
| solarControls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| /* ================= THEME TOGGLE ================= */ | |
| function toggleTheme() { | |
| document.body.classList.toggle("light-mode"); | |
| } | |
| // Initialize on load | |
| window.addEventListener('DOMContentLoaded', () => { | |
| console.log("🚀 ProWeather + NASA Portal loaded successfully!"); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |