Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Solar System Sandbox</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| #canvas-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100vh; | |
| background: radial-gradient(ellipse at center, #000000 0%, #1a1a2e 100%); | |
| overflow: hidden; | |
| } | |
| #controls { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 15px; | |
| border-radius: 10px; | |
| color: white; | |
| z-index: 100; | |
| max-width: 300px; | |
| } | |
| #playback-controls { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 10px; | |
| border-radius: 10px; | |
| color: white; | |
| z-index: 100; | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .planet { | |
| border-radius: 50%; | |
| position: absolute; | |
| box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); | |
| } | |
| .trail { | |
| position: absolute; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.2); | |
| pointer-events: none; | |
| } | |
| .info-panel { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 15px; | |
| border-radius: 10px; | |
| color: white; | |
| z-index: 100; | |
| max-width: 300px; | |
| } | |
| .gravity-line { | |
| position: absolute; | |
| background: rgba(255, 255, 255, 0.3); | |
| height: 1px; | |
| transform-origin: left center; | |
| pointer-events: none; | |
| } | |
| .playback-btn { | |
| background: rgba(255, 255, 255, 0.1); | |
| border: none; | |
| color: white; | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .playback-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| transform: scale(1.1); | |
| } | |
| .playback-btn.active { | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: scale(1.1); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white overflow-hidden"> | |
| <div id="canvas-container"> | |
| <div id="controls" class="space-y-4"> | |
| <h2 class="text-xl font-bold">Solar System Sandbox</h2> | |
| <div class="space-y-2"> | |
| <h3 class="font-semibold">Add Celestial Body</h3> | |
| <div class="flex space-x-2"> | |
| <button id="add-planet" class="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded">Planet</button> | |
| <button id="add-star" class="bg-yellow-600 hover:bg-yellow-700 px-3 py-1 rounded">Star</button> | |
| <button id="add-asteroid" class="bg-gray-600 hover:bg-gray-700 px-3 py-1 rounded">Asteroid</button> | |
| </div> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="font-semibold">Physics Settings</h3> | |
| <div class="flex items-center justify-between"> | |
| <label>Gravity:</label> | |
| <input id="gravity-slider" type="range" min="0" max="2" step="0.1" value="0.5" class="w-32"> | |
| <span id="gravity-value">0.5</span> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <label>Time Scale:</label> | |
| <input id="time-scale-slider" type="range" min="0.1" max="5" step="0.1" value="1" class="w-32"> | |
| <span id="time-scale-value">1.0</span> | |
| </div> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="font-semibold">Visual Effects</h3> | |
| <div class="flex items-center space-x-2"> | |
| <input id="show-trails" type="checkbox" checked> | |
| <label for="show-trails">Show Trails</label> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <input id="show-gravity" type="checkbox"> | |
| <label for="show-gravity">Show Gravity</label> | |
| </div> | |
| </div> | |
| <div> | |
| <button id="clear-all" class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded">Clear All</button> | |
| </div> | |
| </div> | |
| <div id="playback-controls"> | |
| <button id="rewind-btn" class="playback-btn" title="Rewind (0.5x)"> | |
| <i class="fas fa-backward"></i> | |
| </button> | |
| <button id="play-btn" class="playback-btn active" title="Play"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| <button id="pause-btn" class="playback-btn" title="Pause"> | |
| <i class="fas fa-pause"></i> | |
| </button> | |
| <button id="fastforward-btn" class="playback-btn" title="Fast Forward (2x)"> | |
| <i class="fas fa-forward"></i> | |
| </button> | |
| </div> | |
| <div id="info-panel" class="info-panel hidden"> | |
| <h3 class="font-semibold" id="selected-name">Selected Body</h3> | |
| <div class="grid grid-cols-2 gap-2 mt-2"> | |
| <div>Mass:</div> | |
| <div id="selected-mass">0</div> | |
| <div>Radius:</div> | |
| <div id="selected-radius">0</div> | |
| <div>Velocity:</div> | |
| <div id="selected-velocity">0</div> | |
| <div>Position:</div> | |
| <div id="selected-position">0, 0</div> | |
| </div> | |
| <div class="mt-2 flex space-x-2"> | |
| <button id="delete-body" class="bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm">Delete</button> | |
| <button id="freeze-body" class="bg-gray-600 hover:bg-gray-700 px-2 py-1 rounded text-sm">Freeze</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize Matter.js | |
| const { Engine, Render, World, Bodies, Body, Vector, Composite, Mouse, MouseConstraint } = Matter; | |
| // Create engine | |
| const engine = Engine.create({ | |
| gravity: { x: 0, y: 0 }, | |
| enableSleeping: true | |
| }); | |
| // Get container dimensions | |
| const container = document.getElementById('canvas-container'); | |
| const width = container.clientWidth; | |
| const height = container.clientHeight; | |
| // Store celestial bodies and their visual elements | |
| const bodies = []; | |
| const visualElements = {}; | |
| const trails = []; | |
| const gravityLines = []; | |
| // Physics settings | |
| let globalGravity = 0.5; | |
| let timeScale = 1.0; | |
| let showTrails = true; | |
| let showGravity = false; | |
| let selectedBody = null; | |
| // Playback state | |
| let isPlaying = true; | |
| let playbackSpeed = 1.0; | |
| let lastTimestamp = 0; | |
| // Colors for different body types | |
| const bodyColors = { | |
| star: '#FDB813', | |
| planet: '#4D8BFF', | |
| asteroid: '#AAAAAA' | |
| }; | |
| // Initialize UI controls | |
| document.getElementById('gravity-slider').addEventListener('input', (e) => { | |
| globalGravity = parseFloat(e.target.value); | |
| document.getElementById('gravity-value').textContent = globalGravity; | |
| }); | |
| document.getElementById('time-scale-slider').addEventListener('input', (e) => { | |
| timeScale = parseFloat(e.target.value); | |
| document.getElementById('time-scale-value').textContent = timeScale.toFixed(1); | |
| }); | |
| document.getElementById('show-trails').addEventListener('change', (e) => { | |
| showTrails = e.target.checked; | |
| document.querySelectorAll('.trail').forEach(trail => { | |
| trail.style.display = showTrails ? 'block' : 'none'; | |
| }); | |
| }); | |
| document.getElementById('show-gravity').addEventListener('change', (e) => { | |
| showGravity = e.target.checked; | |
| document.querySelectorAll('.gravity-line').forEach(line => { | |
| line.style.display = showGravity ? 'block' : 'none'; | |
| }); | |
| }); | |
| document.getElementById('clear-all').addEventListener('click', () => { | |
| // Remove all bodies | |
| bodies.forEach(body => { | |
| World.remove(engine.world, body); | |
| }); | |
| bodies.length = 0; | |
| // Remove all visual elements | |
| Object.keys(visualElements).forEach(id => { | |
| const element = visualElements[id]; | |
| if (element.parentNode) { | |
| element.parentNode.removeChild(element); | |
| } | |
| }); | |
| Object.keys(visualElements).forEach(key => delete visualElements[key]); | |
| // Clear trails | |
| trails.forEach(trail => { | |
| if (trail.parentNode) { | |
| trail.parentNode.removeChild(trail); | |
| } | |
| }); | |
| trails.length = 0; | |
| // Clear gravity lines | |
| gravityLines.forEach(line => { | |
| if (line.parentNode) { | |
| line.parentNode.removeChild(line); | |
| } | |
| }); | |
| gravityLines.length = 0; | |
| // Hide info panel | |
| document.getElementById('info-panel').classList.add('hidden'); | |
| selectedBody = null; | |
| }); | |
| // Playback controls | |
| document.getElementById('play-btn').addEventListener('click', () => { | |
| isPlaying = true; | |
| playbackSpeed = 1.0; | |
| updatePlaybackButtons(); | |
| lastTimestamp = performance.now(); // Reset timestamp when resuming | |
| }); | |
| document.getElementById('pause-btn').addEventListener('click', () => { | |
| isPlaying = false; | |
| updatePlaybackButtons(); | |
| }); | |
| document.getElementById('rewind-btn').addEventListener('click', () => { | |
| isPlaying = true; | |
| playbackSpeed = 0.5; | |
| updatePlaybackButtons(); | |
| lastTimestamp = performance.now(); // Reset timestamp when changing speed | |
| }); | |
| document.getElementById('fastforward-btn').addEventListener('click', () => { | |
| isPlaying = true; | |
| playbackSpeed = 2.0; | |
| updatePlaybackButtons(); | |
| lastTimestamp = performance.now(); // Reset timestamp when changing speed | |
| }); | |
| function updatePlaybackButtons() { | |
| // Reset all buttons | |
| document.querySelectorAll('.playback-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| // Activate the appropriate button | |
| if (!isPlaying) { | |
| document.getElementById('pause-btn').classList.add('active'); | |
| } else { | |
| if (playbackSpeed === 0.5) { | |
| document.getElementById('rewind-btn').classList.add('active'); | |
| } else if (playbackSpeed === 2.0) { | |
| document.getElementById('fastforward-btn').classList.add('active'); | |
| } else { | |
| document.getElementById('play-btn').classList.add('active'); | |
| } | |
| } | |
| } | |
| // Add body buttons | |
| document.getElementById('add-planet').addEventListener('click', () => { | |
| addCelestialBody('planet', width / 2, height / 2); | |
| }); | |
| document.getElementById('add-star').addEventListener('click', () => { | |
| addCelestialBody('star', width / 2, height / 2); | |
| }); | |
| document.getElementById('add-asteroid').addEventListener('click', () => { | |
| addCelestialBody('asteroid', width / 2, height / 2); | |
| }); | |
| // Body interaction buttons | |
| document.getElementById('delete-body').addEventListener('click', () => { | |
| if (selectedBody) { | |
| deleteBody(selectedBody); | |
| document.getElementById('info-panel').classList.add('hidden'); | |
| selectedBody = null; | |
| } | |
| }); | |
| document.getElementById('freeze-body').addEventListener('click', () => { | |
| if (selectedBody) { | |
| selectedBody.isStatic = !selectedBody.isStatic; | |
| Body.setStatic(selectedBody, selectedBody.isStatic); | |
| document.getElementById('freeze-body').textContent = | |
| selectedBody.isStatic ? 'Unfreeze' : 'Freeze'; | |
| } | |
| }); | |
| // Add a celestial body to the simulation | |
| function addCelestialBody(type, x, y) { | |
| let radius, mass, options = {}; | |
| switch (type) { | |
| case 'star': | |
| radius = 30 + Math.random() * 20; | |
| mass = radius * 100; | |
| options = { | |
| render: { | |
| fillStyle: bodyColors.star, | |
| strokeStyle: '#FFD700', | |
| lineWidth: 2 | |
| }, | |
| friction: 0, | |
| frictionAir: 0, | |
| frictionStatic: 0, | |
| restitution: 0.9 | |
| }; | |
| break; | |
| case 'planet': | |
| radius = 10 + Math.random() * 15; | |
| mass = radius * 20; | |
| options = { | |
| render: { | |
| fillStyle: bodyColors.planet, | |
| strokeStyle: '#7FB2FF', | |
| lineWidth: 1 | |
| }, | |
| friction: 0, | |
| frictionAir: 0.01, | |
| frictionStatic: 0, | |
| restitution: 0.7 | |
| }; | |
| break; | |
| case 'asteroid': | |
| radius = 3 + Math.random() * 7; | |
| mass = radius * 5; | |
| options = { | |
| render: { | |
| fillStyle: bodies.asteroid, | |
| strokeStyle: '#DDDDDD', | |
| lineWidth: 1 | |
| }, | |
| friction: 0, | |
| frictionAir: 0.02, | |
| frictionStatic: 0, | |
| restitution: 0.5 | |
| }; | |
| break; | |
| } | |
| // Create physics body | |
| const body = Bodies.circle(x, y, radius, options); | |
| body.mass = mass; | |
| body.type = type; | |
| body.name = `${type.charAt(0).toUpperCase() + type.slice(1)}-${bodies.length + 1}`; | |
| // Add some initial velocity if not a star | |
| if (type !== 'star') { | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 1 + Math.random() * 3; | |
| Body.setVelocity(body, { | |
| x: Math.cos(angle) * speed, | |
| y: Math.sin(angle) * speed | |
| }); | |
| } else { | |
| body.isStatic = true; | |
| } | |
| // Add to world | |
| World.add(engine.world, body); | |
| bodies.push(body); | |
| // Create visual element | |
| createVisualElement(body); | |
| return body; | |
| } | |
| // Create a visual representation of a body | |
| function createVisualElement(body) { | |
| const element = document.createElement('div'); | |
| element.className = 'planet'; | |
| element.style.width = `${body.circleRadius * 2}px`; | |
| element.style.height = `${body.circleRadius * 2}px`; | |
| element.style.left = `${body.position.x - body.circleRadius}px`; | |
| element.style.top = `${body.position.y - body.circleRadius}px`; | |
| // Set color based on type | |
| element.style.backgroundColor = bodyColors[body.type]; | |
| // Add glow effect for stars | |
| if (body.type === 'star') { | |
| element.style.boxShadow = `0 0 ${body.circleRadius * 2}px ${bodyColors.star}`; | |
| } | |
| // Add click event | |
| element.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| selectBody(body); | |
| }); | |
| // Add drag event | |
| element.addEventListener('mousedown', startDrag); | |
| container.appendChild(element); | |
| visualElements[body.id] = element; | |
| } | |
| // Select a body and show its info | |
| function selectBody(body) { | |
| selectedBody = body; | |
| // Update info panel | |
| document.getElementById('selected-name').textContent = body.name; | |
| document.getElementById('selected-mass').textContent = body.mass.toFixed(2); | |
| document.getElementById('selected-radius').textContent = body.circleRadius.toFixed(2); | |
| const velocity = Vector.magnitude(body.velocity); | |
| document.getElementById('selected-velocity').textContent = velocity.toFixed(2); | |
| document.getElementById('selected-position').textContent = | |
| `${body.position.x.toFixed(0)}, ${body.position.y.toFixed(0)}`; | |
| document.getElementById('freeze-body').textContent = | |
| body.isStatic ? 'Unfreeze' : 'Freeze'; | |
| document.getElementById('info-panel').classList.remove('hidden'); | |
| } | |
| // Delete a body | |
| function deleteBody(body) { | |
| // Remove from physics world | |
| World.remove(engine.world, body); | |
| // Remove from bodies array | |
| const index = bodies.indexOf(body); | |
| if (index > -1) { | |
| bodies.splice(index, 1); | |
| } | |
| // Remove visual element | |
| const element = visualElements[body.id]; | |
| if (element && element.parentNode) { | |
| element.parentNode.removeChild(element); | |
| } | |
| delete visualElements[body.id]; | |
| // Remove any trails associated with this body | |
| const bodyTrails = trails.filter(t => t.dataset.bodyId === body.id.toString()); | |
| bodyTrails.forEach(trail => { | |
| if (trail.parentNode) { | |
| trail.parentNode.removeChild(trail); | |
| } | |
| const trailIndex = trails.indexOf(trail); | |
| if (trailIndex > -1) { | |
| trails.splice(trailIndex, 1); | |
| } | |
| }); | |
| } | |
| // Start dragging a body | |
| function startDrag(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const element = e.target; | |
| const bodyId = Object.keys(visualElements).find(id => visualElements[id] === element); | |
| const body = bodies.find(b => b.id.toString() === bodyId); | |
| if (!body) return; | |
| selectBody(body); | |
| // Calculate offset from mouse to body center | |
| const rect = element.getBoundingClientRect(); | |
| const offsetX = e.clientX - rect.left - body.circleRadius; | |
| const offsetY = e.clientY - rect.top - body.circleRadius; | |
| // Set body to static while dragging | |
| const wasStatic = body.isStatic; | |
| Body.setStatic(body, true); | |
| function moveBody(e) { | |
| const x = e.clientX - offsetX; | |
| const y = e.clientY - offsetY; | |
| Body.setPosition(body, { x, y }); | |
| // Update visual element | |
| const visual = visualElements[body.id]; | |
| if (visual) { | |
| visual.style.left = `${x - body.circleRadius}px`; | |
| visual.style.top = `${y - body.circleRadius}px`; | |
| } | |
| } | |
| function endDrag(e) { | |
| document.removeEventListener('mousemove', moveBody); | |
| document.removeEventListener('mouseup', endDrag); | |
| // Restore static state | |
| Body.setStatic(body, wasStatic); | |
| // If not static, set velocity based on drag speed | |
| if (!wasStatic) { | |
| const x = e.clientX - offsetX; | |
| const y = e.clientY - offsetY; | |
| const deltaX = x - body.position.x; | |
| const deltaY = y - body.position.y; | |
| Body.setVelocity(body, { | |
| x: deltaX * 0.5, | |
| y: deltaY * 0.5 | |
| }); | |
| } | |
| } | |
| document.addEventListener('mousemove', moveBody); | |
| document.addEventListener('mouseup', endDrag); | |
| } | |
| // Click on empty space to deselect | |
| container.addEventListener('click', (e) => { | |
| if (e.target === container) { | |
| document.getElementById('info-panel').classList.add('hidden'); | |
| selectedBody = null; | |
| } | |
| }); | |
| // Add mouse control for creating gravity wells | |
| const mouse = Mouse.create(container); | |
| const mouseConstraint = MouseConstraint.create(engine, { | |
| mouse: mouse, | |
| constraint: { | |
| stiffness: 0.2, | |
| render: { | |
| visible: false | |
| } | |
| } | |
| }); | |
| World.add(engine.world, mouseConstraint); | |
| // Add asteroids on right click | |
| container.addEventListener('contextmenu', (e) => { | |
| e.preventDefault(); | |
| addCelestialBody('asteroid', e.clientX, e.clientY); | |
| }); | |
| // Main animation loop | |
| function run(timestamp) { | |
| if (!lastTimestamp) { | |
| lastTimestamp = timestamp; | |
| } | |
| const deltaTime = timestamp - lastTimestamp; | |
| lastTimestamp = timestamp; | |
| if (isPlaying) { | |
| // Calculate the time step based on playback speed and time scale | |
| const timeStep = deltaTime * 0.06 * playbackSpeed * timeScale; | |
| // Update physics | |
| Engine.update(engine, timeStep); | |
| // Apply custom gravity between bodies | |
| applyGravity(); | |
| // Update visual elements | |
| updateVisuals(); | |
| // Add trails | |
| if (showTrails) { | |
| addTrails(); | |
| } | |
| // Add gravity visualization | |
| if (showGravity) { | |
| visualizeGravity(); | |
| } | |
| } | |
| requestAnimationFrame(run); | |
| } | |
| // Apply gravity between all bodies | |
| function applyGravity() { | |
| for (let i = 0; i < bodies.length; i++) { | |
| const bodyA = bodies[i]; | |
| for (let j = i + 1; j < bodies.length; j++) { | |
| const bodyB = bodies[j]; | |
| // Skip if either body is static | |
| if (bodyA.isStatic && bodyB.isStatic) continue; | |
| // Calculate distance between bodies | |
| const direction = Vector.sub(bodyB.position, bodyA.position); | |
| const distance = Vector.magnitude(direction); | |
| const minDistance = bodyA.circleRadius + bodyB.circleRadius; | |
| // Skip if bodies are too close (to prevent extreme forces) | |
| if (distance < minDistance) continue; | |
| // Calculate gravitational force (Newton's law of universal gravitation) | |
| const forceMagnitude = globalGravity * bodyA.mass * bodyB.mass / (distance * distance); | |
| const force = Vector.normalise(direction); | |
| Vector.mult(force, forceMagnitude); | |
| // Apply forces to both bodies | |
| if (!bodyA.isStatic) { | |
| Body.applyForce(bodyA, bodyA.position, { | |
| x: force.x * 0.5, | |
| y: force.y * 0.5 | |
| }); | |
| } | |
| if (!bodyB.isStatic) { | |
| Body.applyForce(bodyB, bodyB.position, { | |
| x: -force.x * 0.5, | |
| y: -force.y * 0.5 | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| // Update positions of visual elements | |
| function updateVisuals() { | |
| bodies.forEach(body => { | |
| const element = visualElements[body.id]; | |
| if (element) { | |
| element.style.left = `${body.position.x - body.circleRadius}px`; | |
| element.style.top = `${body.position.y - body.circleRadius}px`; | |
| // Rotate planets slightly for visual effect | |
| if (body.type === 'planet') { | |
| const rotation = (body.angle * 180 / Math.PI) % 360; | |
| element.style.transform = `rotate(${rotation}deg)`; | |
| } | |
| } | |
| }); | |
| // Update info panel if a body is selected | |
| if (selectedBody) { | |
| document.getElementById('selected-velocity').textContent = | |
| Vector.magnitude(selectedBody.velocity).toFixed(2); | |
| document.getElementById('selected-position').textContent = | |
| `${selectedBody.position.x.toFixed(0)}, ${selectedBody.position.y.toFixed(0)}`; | |
| } | |
| } | |
| // Add motion trails behind moving bodies | |
| function addTrails() { | |
| bodies.forEach(body => { | |
| // Skip static bodies | |
| if (body.isStatic) return; | |
| // Skip if velocity is very low | |
| if (Vector.magnitude(body.velocity) < 0.1) return; | |
| // Create a trail element | |
| const trail = document.createElement('div'); | |
| trail.className = 'trail'; | |
| trail.style.width = `${body.circleRadius * 0.5}px`; | |
| trail.style.height = `${body.circleRadius * 0.5}px`; | |
| trail.style.left = `${body.position.x - body.circleRadius * 0.25}px`; | |
| trail.style.top = `${body.position.y - body.circleRadius * 0.25}px`; | |
| trail.dataset.bodyId = body.id; | |
| // Set color based on body type | |
| trail.style.backgroundColor = bodyColors[body.type]; | |
| container.appendChild(trail); | |
| trails.push(trail); | |
| // Fade out and remove old trails | |
| if (trails.length > 100) { | |
| const oldTrail = trails.shift(); | |
| if (oldTrail.parentNode) { | |
| oldTrail.parentNode.removeChild(oldTrail); | |
| } | |
| } | |
| }); | |
| } | |
| // Visualize gravity between bodies | |
| function visualizeGravity() { | |
| // Clear old gravity lines | |
| gravityLines.forEach(line => { | |
| if (line.parentNode) { | |
| line.parentNode.removeChild(line); | |
| } | |
| }); | |
| gravityLines.length = 0; | |
| // Create new gravity lines between close bodies | |
| for (let i = 0; i < bodies.length; i++) { | |
| const bodyA = bodies[i]; | |
| for (let j = i + 1; j < bodies.length; j++) { | |
| const bodyB = bodies[j]; | |
| // Calculate distance | |
| const direction = Vector.sub(bodyB.position, bodyA.position); | |
| const distance = Vector.magnitude(direction); | |
| const maxDistance = Math.min(width, height) * 0.4; | |
| // Only draw lines for relatively close bodies | |
| if (distance < maxDistance) { | |
| const line = document.createElement('div'); | |
| line.className = 'gravity-line'; | |
| line.style.width = `${distance}px`; | |
| line.style.left = `${bodyA.position.x}px`; | |
| line.style.top = `${bodyA.position.y}px`; | |
| // Calculate angle | |
| const angle = Math.atan2(bodyB.position.y - bodyA.position.y, | |
| bodyB.position.x - bodyA.position.x); | |
| line.style.transform = `rotate(${angle}rad)`; | |
| // Set opacity based on distance (closer = more opaque) | |
| const opacity = 1 - (distance / maxDistance); | |
| line.style.opacity = opacity * 0.5; | |
| container.appendChild(line); | |
| gravityLines.push(line); | |
| } | |
| } | |
| } | |
| } | |
| // Start with a simple solar system | |
| function initSolarSystem() { | |
| // Add a central star | |
| const sun = addCelestialBody('star', width / 2, height / 2); | |
| sun.mass = 10000; // Very massive sun | |
| // Add some planets | |
| for (let i = 0; i < 5; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const distance = 100 + Math.random() * 200; | |
| const planet = addCelestialBody('planet', | |
| width / 2 + Math.cos(angle) * distance, | |
| height / 2 + Math.sin(angle) * distance | |
| ); | |
| // Set initial velocity for orbit | |
| const orbitSpeed = Math.sqrt(sun.mass / distance) * 0.3; | |
| Body.setVelocity(planet, { | |
| x: Math.cos(angle + Math.PI/2) * orbitSpeed, | |
| y: Math.sin(angle + Math.PI/2) * orbitSpeed | |
| }); | |
| } | |
| // Add some asteroids | |
| for (let i = 0; i < 10; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const distance = 200 + Math.random() * 300; | |
| addCelestialBody('asteroid', | |
| width / 2 + Math.cos(angle) * distance, | |
| height / 2 + Math.sin(angle) * distance | |
| ); | |
| } | |
| } | |
| // Start the simulation | |
| initSolarSystem(); | |
| requestAnimationFrame(run); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=ItsMeDevRoland/se" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |