Spaces:
Running
Running
| // Solar System Simulation | |
| class SolarSystem { | |
| constructor() { | |
| this.canvas = document.getElementById('solarCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.centerX = 0; | |
| this.centerY = 0; | |
| this.zoom = 1; | |
| this.speed = 1; | |
| this.isPaused = false; | |
| this.time = 0; | |
| this.fps = 60; | |
| this.lastFrameTime = performance.now(); | |
| this.startDate = new Date(); | |
| this.currentDate = new Date(); | |
| this.timeLapseSpeed = 1; // days per frame | |
| this.planets = [ | |
| { | |
| name: 'Mercury', | |
| type: 'Terrestrial', | |
| radius: 4, | |
| distance: 60, | |
| orbitalPeriod: 87.97, // days | |
| color: '#9CA3AF', | |
| info: { | |
| diameter: '4,879 km', | |
| moons: 0, | |
| orbitalPeriod: '87.97 days' | |
| }, | |
| eccentricity: 0.2056, | |
| inclination: 7.0 | |
| }, | |
| { | |
| name: 'Venus', | |
| type: 'Terrestrial', | |
| radius: 7, | |
| distance: 85, | |
| orbitalPeriod: 224.70, | |
| color: '#FEF3C7', | |
| info: { | |
| diameter: '12,104 km', | |
| moons: 0, | |
| orbitalPeriod: '224.70 days' | |
| }, | |
| eccentricity: 0.0067, | |
| inclination: 3.39 | |
| }, | |
| { | |
| name: 'Earth', | |
| type: 'Terrestrial', | |
| radius: 8, | |
| distance: 110, | |
| orbitalPeriod: 365.26, | |
| color: '#3B82F6', | |
| info: { | |
| diameter: '12,742 km', | |
| moons: 1, | |
| orbitalPeriod: '365.26 days' | |
| }, | |
| moons: [{ | |
| radius: 2, | |
| distance: 15, | |
| orbitalPeriod: 27.32, // days | |
| color: '#E5E7EB' | |
| }], | |
| eccentricity: 0.0167, | |
| inclination: 0.0 | |
| }, | |
| { | |
| name: 'Mars', | |
| type: 'Terrestrial', | |
| radius: 5, | |
| distance: 140, | |
| orbitalPeriod: 686.98, | |
| color: '#EF4444', | |
| info: { | |
| diameter: '6,779 km', | |
| moons: 2, | |
| orbitalPeriod: '686.98 days' | |
| }, | |
| moons: [ | |
| { | |
| radius: 1, | |
| distance: 10, | |
| orbitalPeriod: 0.32, // days | |
| color: '#F3F4F6' | |
| }, | |
| { | |
| radius: 1, | |
| distance: 14, | |
| orbitalPeriod: 1.26, // days | |
| color: '#D1D5DB' | |
| } | |
| ], | |
| eccentricity: 0.0935, | |
| inclination: 1.85 | |
| }, | |
| { | |
| name: 'Jupiter', | |
| type: 'Gas Giant', | |
| radius: 20, | |
| distance: 200, | |
| orbitalPeriod: 4332.59, | |
| color: '#FB923C', | |
| info: { | |
| diameter: '139,820 km', | |
| moons: 79, | |
| orbitalPeriod: '11.86 years' | |
| }, | |
| moons: [ | |
| { | |
| radius: 2, | |
| distance: 28, | |
| orbitalPeriod: 1.77, // days | |
| color: '#FCD34D' | |
| }, | |
| { | |
| radius: 2, | |
| distance: 35, | |
| orbitalPeriod: 3.55, // days | |
| color: '#FDE68A' | |
| } | |
| ], | |
| eccentricity: 0.0489, | |
| inclination: 1.31 | |
| }, | |
| { | |
| name: 'Saturn', | |
| type: 'Gas Giant', | |
| radius: 17, | |
| distance: 260, | |
| orbitalPeriod: 10759.22, | |
| color: '#F59E0B', | |
| hasRings: true, | |
| info: { | |
| diameter: '116,460 km', | |
| moons: 82, | |
| orbitalPeriod: '29.46 years' | |
| }, | |
| moons: [ | |
| { | |
| radius: 3, | |
| distance: 30, | |
| orbitalPeriod: 2.74, // days | |
| color: '#FBBF24' | |
| } | |
| ], | |
| eccentricity: 0.0565, | |
| inclination: 2.49 | |
| }, | |
| { | |
| name: 'Uranus', | |
| type: 'Ice Giant', | |
| radius: 12, | |
| distance: 320, | |
| orbitalPeriod: 30688.5, | |
| color: '#06B6D4', | |
| info: { | |
| diameter: '50,724 km', | |
| moons: 27, | |
| orbitalPeriod: '84.01 years' | |
| }, | |
| eccentricity: 0.0457, | |
| inclination: 0.77 | |
| }, | |
| { | |
| name: 'Neptune', | |
| type: 'Ice Giant', | |
| radius: 11, | |
| distance: 370, | |
| orbitalPeriod: 60182, | |
| color: '#1E40AF', | |
| info: { | |
| diameter: '49,244 km', | |
| moons: 14, | |
| orbitalPeriod: '164.79 years' | |
| }, | |
| eccentricity: 0.0113, | |
| inclination: 1.77 | |
| } | |
| ]; | |
| this.init(); | |
| } | |
| init() { | |
| this.resize(); | |
| window.addEventListener('resize', () => this.resize()); | |
| this.setupEventListeners(); | |
| this.createStars(); | |
| this.animate(); | |
| this.updateDate(); | |
| this.updateLocation(); | |
| this.updateTimeDisplay(); | |
| setInterval(() => this.updateDate(), 1000); | |
| setInterval(() => this.updateLocation(), 30000); | |
| } | |
| resize() { | |
| this.canvas.width = window.innerWidth; | |
| this.canvas.height = window.innerHeight; | |
| this.centerX = this.canvas.width / 2; | |
| this.centerY = this.canvas.height / 2; | |
| } | |
| createStars() { | |
| const starsContainer = document.getElementById('starsContainer'); | |
| for (let i = 0; i < 200; i++) { | |
| const star = document.createElement('div'); | |
| star.className = 'star'; | |
| star.style.width = Math.random() * 3 + 'px'; | |
| star.style.height = star.style.width; | |
| star.style.left = Math.random() * 100 + '%'; | |
| star.style.top = Math.random() * 100 + '%'; | |
| star.style.animationDelay = Math.random() * 3 + 's'; | |
| starsContainer.appendChild(star); | |
| } | |
| } | |
| setupEventListeners() { | |
| // Speed control | |
| const speedSlider = document.getElementById('speedSlider'); | |
| speedSlider.addEventListener('input', (e) => { | |
| this.speed = parseFloat(e.target.value); | |
| this.timeLapseSpeed = this.speed * 0.1; // Convert to days per frame | |
| document.getElementById('speedValue').textContent = this.speed.toFixed(1) + 'x'; | |
| }); | |
| // Zoom control | |
| const zoomSlider = document.getElementById('zoomSlider'); | |
| zoomSlider.addEventListener('input', (e) => { | |
| this.zoom = parseFloat(e.target.value); | |
| document.getElementById('zoomValue').textContent = this.zoom.toFixed(1) + 'x'; | |
| }); | |
| // Pause button | |
| const pauseBtn = document.getElementById('pauseBtn'); | |
| pauseBtn.addEventListener('click', () => { | |
| this.isPaused = !this.isPaused; | |
| pauseBtn.innerHTML = this.isPaused ? | |
| '<i data-feather="play" class="w-4 h-4"></i><span>Play</span>' : | |
| '<i data-feather="pause" class="w-4 h-4"></i><span>Pause</span>'; | |
| feather.replace(); | |
| }); | |
| // Reset button | |
| document.getElementById('resetBtn').addEventListener('click', () => { | |
| this.time = 0; | |
| this.currentDate = new Date(); | |
| this.zoom = 1; | |
| this.speed = 1; | |
| this.timeLapseSpeed = 0.1; | |
| document.getElementById('zoomSlider').value = 1; | |
| document.getElementById('speedSlider').value = 1; | |
| document.getElementById('zoomValue').textContent = '1.0x'; | |
| document.getElementById('speedValue').textContent = '1.0x'; | |
| this.updateTimeDisplay(); | |
| }); | |
| // Fullscreen button | |
| document.getElementById('fullscreenBtn').addEventListener('click', () => { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen(); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| }); | |
| // Info button | |
| document.getElementById('infoBtn').addEventListener('click', () => { | |
| const planetInfo = document.getElementById('planetInfo'); | |
| planetInfo.classList.toggle('hidden'); | |
| }); | |
| // Location refresh on click | |
| document.getElementById('locationDisplay').addEventListener('click', () => { | |
| document.getElementById('locationDisplay').textContent = 'Updating...'; | |
| this.updateLocation(); | |
| }); | |
| // Close info panel | |
| document.getElementById('closeInfo').addEventListener('click', () => { | |
| document.getElementById('planetInfo').classList.add('hidden'); | |
| }); | |
| // Time jump controls | |
| document.querySelectorAll('.time-unit').forEach(unit => { | |
| unit.addEventListener('click', (e) => { | |
| const jumpDays = parseInt(e.target.dataset.time); | |
| this.time += jumpDays / this.timeLapseSpeed; | |
| this.updateTimeDisplay(); | |
| this.updateDate(); | |
| }); | |
| }); | |
| // Canvas click for planet info | |
| this.canvas.addEventListener('click', (e) => { | |
| const rect = this.canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left - this.centerX; | |
| const y = e.clientY - rect.top - this.centerY; | |
| for (const planet of this.planets) { | |
| const angle = this.time * planet.speed * this.speed * 0.01; | |
| const px = Math.cos(angle) * planet.distance * this.zoom; | |
| const py = Math.sin(angle) * planet.distance * this.zoom; | |
| const distance = Math.sqrt((x - px) ** 2 + (y - py) ** 2); | |
| if (distance < planet.radius * this.zoom + 5) { | |
| this.showPlanetInfo(planet); | |
| break; | |
| } | |
| } | |
| }); | |
| } | |
| showPlanetInfo(planet) { | |
| const info = document.getElementById('planetInfo'); | |
| info.classList.remove('hidden'); | |
| document.getElementById('planetName').textContent = planet.name; | |
| document.getElementById('planetType').textContent = planet.type; | |
| document.getElementById('planetDistance').textContent = planet.distance + ' AU'; | |
| document.getElementById('planetPeriod').textContent = planet.info.orbitalPeriod; | |
| document.getElementById('planetDiameter').textContent = planet.info.diameter; | |
| document.getElementById('planetMoons').textContent = planet.info.moons || 'None'; | |
| } | |
| updateTimeDisplay() { | |
| const timeDisplay = document.getElementById('timeDisplay'); | |
| if (timeDisplay) { | |
| const days = Math.floor(this.timeLapseSpeed * this.time); | |
| const years = Math.floor(days / 365.26); | |
| const remainingDays = days % 365.26; | |
| const months = Math.floor(remainingDays / 30.44); | |
| const remainingDays2 = Math.floor(remainingDays % 30.44); | |
| let timeString = ''; | |
| if (years > 0) timeString += `${years}Y `; | |
| if (months > 0) timeString += `${months}M `; | |
| if (remainingDays2 > 0 || timeString === '') timeString += `${remainingDays2}D`; | |
| timeDisplay.textContent = `Time Lapse: ${timeString}`; | |
| } | |
| } | |
| calculatePlanetPosition(planet, elapsedTime) { | |
| const meanAnomaly = (2 * Math.PI * elapsedTime) / planet.orbitalPeriod; | |
| const eccentricity = planet.eccentricity || 0; | |
| // Solve Kepler's equation (simplified) | |
| let E = meanAnomaly; | |
| for (let i = 0; i < 5; i++) { | |
| E = meanAnomaly + eccentricity * Math.sin(E); | |
| } | |
| // True anomaly | |
| const trueAnomaly = 2 * Math.atan2( | |
| Math.sqrt(1 + eccentricity) * Math.sin(E / 2), | |
| Math.sqrt(1 - eccentricity) * Math.cos(E / 2) | |
| ); | |
| // Distance from sun | |
| const r = planet.distance * (1 - eccentricity * Math.cos(E)); | |
| // Position | |
| const x = r * Math.cos(trueAnomaly); | |
| const y = r * Math.sin(trueAnomaly); | |
| // Apply inclination | |
| const inclination = (planet.inclination || 0) * Math.PI / 180; | |
| const yInclined = y * Math.cos(inclination); | |
| return { x, y: yInclined, r }; | |
| } | |
| updateDate() { | |
| const now = new Date(); | |
| document.getElementById('dateDisplay').textContent = now.toLocaleDateString('en-US', { | |
| weekday: 'long', | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }); | |
| // Update current date display | |
| const currentDateDisplay = document.getElementById('currentDateDisplay'); | |
| if (currentDateDisplay) { | |
| currentDateDisplay.textContent = this.currentDate.toLocaleDateString('en-US', { | |
| month: 'short', | |
| day: 'numeric', | |
| year: 'numeric' | |
| }); | |
| } | |
| } | |
| async updateLocation() { | |
| const locationDisplay = document.getElementById('locationDisplay'); | |
| if (!navigator.geolocation) { | |
| locationDisplay.textContent = 'Location not supported'; | |
| return; | |
| } | |
| navigator.geolocation.getCurrentPosition( | |
| async (position) => { | |
| const { latitude, longitude } = position.coords; | |
| try { | |
| // Using Nominatim reverse geocoding API | |
| const response = await fetch( | |
| `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=10`, | |
| { | |
| headers: { | |
| 'User-Agent': 'CosmicExplorer/1.0' | |
| } | |
| } | |
| ); | |
| if (!response.ok) throw new Error('Location fetch failed'); | |
| const data = await response.json(); | |
| const city = data.address?.city || data.address?.town || data.address?.village || 'Unknown'; | |
| const country = data.address?.country || ''; | |
| locationDisplay.textContent = country ? `${city}, ${country}` : city; | |
| } catch (error) { | |
| // Fallback to coordinates | |
| locationDisplay.textContent = `${latitude.toFixed(2)}°, ${longitude.toFixed(2)}°`; | |
| } | |
| }, | |
| (error) => { | |
| locationDisplay.textContent = 'Location denied'; | |
| } | |
| ); | |
| } | |
| drawSun() { | |
| // Simplified sun for performance | |
| this.ctx.fillStyle = '#FBBF24'; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(this.centerX, this.centerY, 25 * this.zoom, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| // Simple glow effect | |
| this.ctx.fillStyle = 'rgba(251, 191, 36, 0.3)'; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(this.centerX, this.centerY, 35 * this.zoom, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| } | |
| drawPlanet(planet) { | |
| const elapsedTime = this.timeLapseSpeed * this.time; | |
| const position = this.calculatePlanetPosition(planet, elapsedTime); | |
| const x = this.centerX + position.x * this.zoom; | |
| const y = this.centerY + position.y * this.zoom; | |
| // Draw orbit | |
| this.ctx.strokeStyle = 'rgba(147, 51, 234, 0.2)'; | |
| this.ctx.lineWidth = 1; | |
| this.ctx.beginPath(); | |
| this.ctx.ellipse(this.centerX, this.centerY, planet.distance * this.zoom, planet.distance * this.zoom, 0, 0, Math.PI * 2); | |
| this.ctx.stroke(); | |
| // Draw rings for Saturn | |
| if (planet.hasRings) { | |
| this.ctx.strokeStyle = 'rgba(245, 158, 11, 0.5)'; | |
| this.ctx.lineWidth = 3 * this.zoom; | |
| this.ctx.beginPath(); | |
| this.ctx.ellipse(x, y, planet.radius * this.zoom * 2, planet.radius * this.zoom * 0.7, 0, 0, Math.PI * 2); | |
| this.ctx.stroke(); | |
| } | |
| // Draw planet | |
| this.ctx.fillStyle = planet.color; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(x, y, planet.radius * this.zoom, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| // Draw moons | |
| if (planet.moons) { | |
| planet.moons.forEach(moon => { | |
| const moonElapsedTime = this.timeLapseSpeed * this.time; | |
| const moonAngle = (2 * Math.PI * moonElapsedTime) / moon.orbitalPeriod; | |
| const moonX = x + Math.cos(moonAngle) * moon.distance * this.zoom; | |
| const moonY = y + Math.sin(moonAngle) * moon.distance * this.zoom; | |
| this.ctx.fillStyle = moon.color; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(moonX, moonY, moon.radius * this.zoom, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| }); | |
| } | |
| // Draw planet name | |
| if (this.zoom > 0.8) { | |
| this.ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; | |
| this.ctx.font = `${Math.max(10, 10 * this.zoom)}px Arial`; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText(planet.name, x, y - planet.radius * this.zoom - 5); | |
| } | |
| return { x, y, position }; | |
| } | |
| adjustBrightness(color, amount) { | |
| const num = parseInt(color.replace('#', ''), 16); | |
| const r = Math.max(0, Math.min(255, (num >> 16) + amount)); | |
| const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount)); | |
| const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount)); | |
| return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`; | |
| } | |
| animate() { | |
| // Clear canvas completely for better performance | |
| this.ctx.fillStyle = '#000'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| // Draw sun | |
| this.drawSun(); | |
| // Draw planets | |
| this.planets.forEach(planet => this.drawPlanet(planet)); | |
| // Update time | |
| if (!this.isPaused) { | |
| this.time++; | |
| this.currentDate = new Date(this.startDate.getTime() + this.timeLapseSpeed * this.time * 24 * 60 * 60 * 1000); | |
| this.updateTimeDisplay(); | |
| } | |
| // Update stats less frequently | |
| if (this.time % 10 === 0) { | |
| this.updateStats(); | |
| } | |
| // Calculate FPS | |
| const now = performance.now(); | |
| const delta = now - this.lastFrameTime; | |
| this.fps = Math.round(1000 / delta); | |
| this.lastFrameTime = now; | |
| requestAnimationFrame(() => this.animate()); | |
| } | |
| updateStats() { | |
| document.getElementById('fpsDisplay').textContent = this.fps; | |
| const totalDays = Math.floor(this.timeLapseSpeed * this.time); | |
| document.getElementById('daysDisplay').textContent = totalDays; | |
| document.getElementById('yearsDisplay').textContent = (totalDays / 365.26).toFixed(2); | |
| } | |
| } | |
| // Initialize the solar system | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new SolarSystem(); | |
| }); | |