se / index.html
ItsMeDevRoland's picture
Add 2 files
be44044 verified
<!DOCTYPE html>
<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>