physics-playground / index.html
edtechdev's picture
Create an interactive physics simulation. Two cars move along a horizontal path at different initial speeds. Each car can be dragged by the user along the path to new positions or new velocities. As both cars move, 3 graphs are simultaneiously updated over time: a graph of the cars' position over time, a graph of velocity over time, and a graph of acceleration over time.
feed137 verified
raw
history blame
28.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Physics Playground</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
<style>
.car-container {
transition: transform 0.2s ease;
}
.car-dragging {
cursor: grabbing !important;
z-index: 10;
}
.track-line {
stroke: #4b5563;
stroke-width: 2;
stroke-dasharray: 5,5;
}
.car-shadow {
filter: drop-shadow(0 4px 3px rgba(0,0,0,0.07)) drop-shadow(0 2px 2px rgba(0,0,0,0.06));
}
</style>
</head>
<body class="bg-gradient-to-br from-indigo-50 to-purple-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="text-center mb-12">
<h1 class="text-4xl md:text-5xl font-bold text-gray-800 mb-3">Physics Playground 🚗</h1>
<p class="text-lg text-gray-600 max-w-2xl mx-auto">Drag the cars to change their positions or velocities. Watch as real-time graphs update to show position, velocity, and acceleration over time.</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
<!-- Simulation Canvas -->
<div class="lg:col-span-2 bg-white rounded-2xl shadow-xl p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800">Simulation</h2>
<div class="flex space-x-2">
<button id="resetBtn" class="px-4 py-2 bg-indigo-100 text-indigo-700 rounded-lg hover:bg-indigo-200 transition flex items-center">
<i data-feather="refresh-ccw" class="mr-2"></i> Reset
</button>
<button id="playPauseBtn" class="px-4 py-2 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition flex items-center">
<i data-feather="pause" class="mr-2"></i> Pause
</button>
</div>
</div>
<div id="simulationCanvas" class="relative h-64 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border-2 border-dashed border-gray-200 overflow-hidden">
<!-- Track -->
<div class="absolute top-1/2 left-0 right-0 h-1 bg-gray-300 transform -translate-y-1/2"></div>
<!-- Distance markers -->
<div class="absolute top-1/2 left-0 transform -translate-y-1/2 -translate-x-1/2">
<div class="text-xs text-gray-500">0m</div>
</div>
<div class="absolute top-1/2 left-1/4 transform -translate-y-1/2 -translate-x-1/2">
<div class="text-xs text-gray-500">25m</div>
</div>
<div class="absolute top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2">
<div class="text-xs text-gray-500">50m</div>
</div>
<div class="absolute top-1/2 left-3/4 transform -translate-y-1/2 -translate-x-1/2">
<div class="text-xs text-gray-500">75m</div>
</div>
<div class="absolute top-1/2 right-0 transform -translate-y-1/2 translate-x-1/2">
<div class="text-xs text-gray-500">100m</div>
</div>
<!-- Cars -->
<div id="car1" class="car-container absolute top-1/2 cursor-grab active:cursor-grabbing transform -translate-y-1/2" style="left: 20%;">
<div class="car-shadow">
<svg width="60" height="30" viewBox="0 0 60 30">
<rect x="5" y="8" width="50" height="14" rx="7" fill="#3b82f6"/>
<circle cx="15" cy="24" r="5" fill="#1e40af"/>
<circle cx="45" cy="24" r="5" fill="#1e40af"/>
<rect x="0" y="10" width="10" height="10" rx="2" fill="#3b82f6"/>
<rect x="50" y="10" width="10" height="10" rx="2" fill="#3b82f6"/>
</svg>
</div>
<div class="text-center mt-2 text-sm font-medium text-blue-600">Car 1</div>
</div>
<div id="car2" class="car-container absolute top-1/2 cursor-grab active:cursor-grabbing transform -translate-y-1/2" style="left: 40%;">
<div class="car-shadow">
<svg width="60" height="30" viewBox="0 0 60 30">
<rect x="5" y="8" width="50" height="14" rx="7" fill="#ef4444"/>
<circle cx="15" cy="24" r="5" fill="#b91c1c"/>
<circle cx="45" cy="24" r="5" fill="#b91c1c"/>
<rect x="0" y="10" width="10" height="10" rx="2" fill="#ef4444"/>
<rect x="50" y="10" width="10" height="10" rx="2" fill="#ef4444"/>
</svg>
</div>
<div class="text-center mt-2 text-sm font-medium text-red-600">Car 2</div>
</div>
</div>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-blue-50 p-4 rounded-lg">
<h3 class="font-bold text-blue-800 mb-2">Car 1 Controls</h3>
<div class="flex items-center space-x-4">
<div>
<label class="text-sm text-gray-600">Position (m)</label>
<input type="range" id="position1" min="0" max="100" value="20" class="w-full">
<div class="text-center text-blue-700 font-mono" id="position1Value">20m</div>
</div>
<div>
<label class="text-sm text-gray-600">Velocity (m/s)</label>
<input type="range" id="velocity1" min="-10" max="10" value="3" class="w-full">
<div class="text-center text-blue-700 font-mono" id="velocity1Value">3m/s</div>
</div>
</div>
</div>
<div class="bg-red-50 p-4 rounded-lg">
<h3 class="font-bold text-red-800 mb-2">Car 2 Controls</h3>
<div class="flex items-center space-x-4">
<div>
<label class="text-sm text-gray-600">Position (m)</label>
<input type="range" id="position2" min="0" max="100" value="40" class="w-full">
<div class="text-center text-red-700 font-mono" id="position2Value">40m</div>
</div>
<div>
<label class="text-sm text-gray-600">Velocity (m/s)</label>
<input type="range" id="velocity2" min="-10" max="10" value="-2" class="w-full">
<div class="text-center text-red-700 font-mono" id="velocity2Value">-2m/s</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Panel -->
<div class="bg-white rounded-2xl shadow-xl p-6">
<h2 class="text-xl font-bold text-gray-800 mb-4">Current Data</h2>
<div class="space-y-4">
<div class="bg-blue-50 p-4 rounded-lg">
<div class="flex justify-between items-center">
<h3 class="font-bold text-blue-800">Car 1</h3>
<div class="flex space-x-2">
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">v = <span id="car1Velocity">3</span> m/s</span>
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">a = <span id="car1Acceleration">0</span> m/s²</span>
</div>
</div>
<div class="mt-2">
<div class="text-sm text-gray-600">Position: <span id="car1Position" class="font-mono">20</span> m</div>
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
<div id="car1Progress" class="bg-blue-600 h-2 rounded-full" style="width: 20%"></div>
</div>
</div>
</div>
<div class="bg-red-50 p-4 rounded-lg">
<div class="flex justify-between items-center">
<h3 class="font-bold text-red-800">Car 2</h3>
<div class="flex space-x-2">
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">v = <span id="car2Velocity">-2</span> m/s</span>
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">a = <span id="car2Acceleration">0</span> m/s²</span>
</div>
</div>
<div class="mt-2">
<div class="text-sm text-gray-600">Position: <span id="car2Position" class="font-mono">40</span> m</div>
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
<div id="car2Progress" class="bg-red-600 h-2 rounded-full" style="width: 40%"></div>
</div>
</div>
</div>
</div>
<div class="mt-6 bg-gray-50 p-4 rounded-lg">
<h3 class="font-bold text-gray-800 mb-2">Simulation Controls</h3>
<div class="grid grid-cols-2 gap-3">
<button id="accelerate1" class="px-3 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition text-sm">Accelerate Car 1</button>
<button id="decelerate1" class="px-3 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition text-sm">Decelerate Car 1</button>
<button id="accelerate2" class="px-3 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200 transition text-sm">Accelerate Car 2</button>
<button id="decelerate2" class="px-3 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200 transition text-sm">Decelerate Car 2</button>
</div>
</div>
<div class="mt-6">
<h3 class="font-bold text-gray-800 mb-2">Physics Concepts</h3>
<ul class="text-sm text-gray-600 space-y-1">
<li class="flex items-start">
<span class="text-green-500 mr-2"></span>
<span>Position changes with velocity over time</span>
</li>
<li class="flex items-start">
<span class="text-green-500 mr-2"></span>
<span>Velocity changes with acceleration</span>
</li>
<li class="flex items-start">
<span class="text-green-500 mr-2"></span>
<span>Negative velocity means moving left</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Graphs Section -->
<div class="bg-white rounded-2xl shadow-xl p-6 mb-12">
<h2 class="text-xl font-bold text-gray-800 mb-6">Real-time Physics Graphs</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-bold text-gray-800 mb-4 text-center">Position vs Time</h3>
<canvas id="positionChart"></canvas>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-bold text-gray-800 mb-4 text-center">Velocity vs Time</h3>
<canvas id="velocityChart"></canvas>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-bold text-gray-800 mb-4 text-center">Acceleration vs Time</h3>
<canvas id="accelerationChart"></canvas>
</div>
</div>
</div>
<footer class="text-center text-gray-600 text-sm py-6">
<p>Drag cars to change their positions or use controls to adjust velocities. Watch how physics laws govern motion!</p>
</footer>
</div>
<script>
// Initialize Feather Icons
feather.replace();
// Simulation state
const state = {
cars: [
{ id: 'car1', position: 20, velocity: 3, acceleration: 0, color: '#3b82f6' },
{ id: 'car2', position: 40, velocity: -2, acceleration: 0, color: '#ef4444' }
],
isRunning: true,
lastTime: 0,
dataPoints: {
position: [[0, 20], [0, 40]],
velocity: [[0, 3], [0, -2]],
acceleration: [[0, 0], [0, 0]]
}
};
// DOM Elements
const elements = {
car1: document.getElementById('car1'),
car2: document.getElementById('car2'),
playPauseBtn: document.getElementById('playPauseBtn'),
resetBtn: document.getElementById('resetBtn'),
position1: document.getElementById('position1'),
position2: document.getElementById('position2'),
velocity1: document.getElementById('velocity1'),
velocity2: document.getElementById('velocity2'),
accelerate1: document.getElementById('accelerate1'),
decelerate1: document.getElementById('decelerate1'),
accelerate2: document.getElementById('accelerate2'),
decelerate2: document.getElementById('decelerate2')
};
// Initialize Charts
const positionCtx = document.getElementById('positionChart').getContext('2d');
const velocityCtx = document.getElementById('velocityChart').getContext('2d');
const accelerationCtx = document.getElementById('accelerationChart').getContext('2d');
const positionChart = new Chart(positionCtx, {
type: 'line',
data: {
labels: [0],
datasets: [{
label: 'Car 1',
data: [20],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: false
}, {
label: 'Car 2',
data: [40],
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
tension: 0.4,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { position: 'top' }
},
scales: {
x: { title: { display: true, text: 'Time (s)' } },
y: { title: { display: true, text: 'Position (m)' } }
}
}
});
const velocityChart = new Chart(velocityCtx, {
type: 'line',
data: {
labels: [0],
datasets: [{
label: 'Car 1',
data: [3],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: false
}, {
label: 'Car 2',
data: [-2],
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
tension: 0.4,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { position: 'top' }
},
scales: {
x: { title: { display: true, text: 'Time (s)' } },
y: { title: { display: true, text: 'Velocity (m/s)' } }
}
}
});
const accelerationChart = new Chart(accelerationCtx, {
type: 'line',
data: {
labels: [0],
datasets: [{
label: 'Car 1',
data: [0],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: false
}, {
label: 'Car 2',
data: [0],
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
tension: 0.4,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { position: 'top' }
},
scales: {
x: { title: { display: true, text: 'Time (s)' } },
y: { title: { display: true, text: 'Acceleration (m/s²)' } }
}
}
});
// Drag functionality for cars
function initDrag(carElement, carIndex) {
let isDragging = false;
let offsetX, offsetY;
carElement.addEventListener('mousedown', (e) => {
isDragging = true;
carElement.classList.add('car-dragging');
offsetX = e.clientX - carElement.getBoundingClientRect().left;
offsetY = e.clientY - carElement.getBoundingClientRect().top;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const canvasRect = document.getElementById('simulationCanvas').getBoundingClientRect();
const x = e.clientX - canvasRect.left - offsetX;
const position = Math.max(0, Math.min(100, (x / canvasRect.width) * 100));
state.cars[carIndex].position = position;
updateCarPosition(carIndex);
updateUI();
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
carElement.classList.remove('car-dragging');
}
});
}
// Update car position on canvas
function updateCarPosition(carIndex) {
const car = state.cars[carIndex];
const carElement = document.getElementById(car.id);
carElement.style.left = `${car.position}%`;
}
// Update UI elements
function updateUI() {
// Update car data displays
document.getElementById('car1Position').textContent = Math.round(state.cars[0].position);
document.getElementById('car1Velocity').textContent = state.cars[0].velocity.toFixed(1);
document.getElementById('car1Acceleration').textContent = state.cars[0].acceleration.toFixed(1);
document.getElementById('car1Progress').style.width = `${state.cars[0].position}%`;
document.getElementById('car2Position').textContent = Math.round(state.cars[1].position);
document.getElementById('car2Velocity').textContent = state.cars[1].velocity.toFixed(1);
document.getElementById('car2Acceleration').textContent = state.cars[1].acceleration.toFixed(1);
document.getElementById('car2Progress').style.width = `${state.cars[1].position}%`;
// Update sliders
elements.position1.value = state.cars[0].position;
elements.velocity1.value = state.cars[0].velocity;
elements.position2.value = state.cars[1].position;
elements.velocity2.value = state.cars[1].velocity;
// Update slider value displays
document.getElementById('position1Value').textContent = `${Math.round(state.cars[0].position)}m`;
document.getElementById('velocity1Value').textContent = `${state.cars[0].velocity}m/s`;
document.getElementById('position2Value').textContent = `${Math.round(state.cars[1].position)}m`;
document.getElementById('velocity2Value').textContent = `${state.cars[1].velocity}m/s`;
}
// Update charts with new data
function updateCharts(time) {
// Add new data points
positionChart.data.labels.push(time.toFixed(1));
velocityChart.data.labels.push(time.toFixed(1));
accelerationChart.data.labels.push(time.toFixed(1));
positionChart.data.datasets[0].data.push(state.cars[0].position);
positionChart.data.datasets[1].data.push(state.cars[1].position);
velocityChart.data.datasets[0].data.push(state.cars[0].velocity);
velocityChart.data.datasets[1].data.push(state.cars[1].velocity);
accelerationChart.data.datasets[0].data.push(state.cars[0].acceleration);
accelerationChart.data.datasets[1].data.push(state.cars[1].acceleration);
// Limit data points to last 20
if (positionChart.data.labels.length > 20) {
positionChart.data.labels.shift();
velocityChart.data.labels.shift();
accelerationChart.data.labels.shift();
positionChart.data.datasets[0].data.shift();
positionChart.data.datasets[1].data.shift();
velocityChart.data.datasets[0].data.shift();
velocityChart.data.datasets[1].data.shift();
accelerationChart.data.datasets[0].data.shift();
accelerationChart.data.datasets[1].data.shift();
}
// Update charts
positionChart.update();
velocityChart.update();
accelerationChart.update();
}
// Physics simulation
function simulatePhysics(deltaTime) {
if (!state.isRunning) return;
// Update positions based on velocity
state.cars.forEach(car => {
car.position += car.velocity * deltaTime;
// Boundary checks
if (car.position < 0) {
car.position = 0;
car.velocity = Math.abs(car.velocity) * 0.5; // Bounce with energy loss
} else if (car.position > 100) {
car.position = 100;
car.velocity = -Math.abs(car.velocity) * 0.5; // Bounce with energy loss
}
});
// Update UI
updateCarPosition(0);
updateCarPosition(1);
updateUI();
}
// Animation loop
function animationLoop(timestamp) {
if (!state.lastTime) state.lastTime = timestamp;
const deltaTime = (timestamp - state.lastTime) / 1000; // Convert to seconds
state.lastTime = timestamp;
simulatePhysics(deltaTime);
// Update charts every 0.5 seconds
if (timestamp % 500 < 20) { // Roughly every 0.5 seconds
const time = timestamp / 1000;
updateCharts(time);
}
requestAnimationFrame(animationLoop);
}
// Event Listeners
elements.playPauseBtn.addEventListener('click', () => {
state.isRunning = !state.isRunning;
const icon = elements.playPauseBtn.querySelector('i');
icon.setAttribute('data-feather', state.isRunning ? 'pause' : 'play');
feather.replace();
elements.playPauseBtn.innerHTML = elements.playPauseBtn.innerHTML.replace(
state.isRunning ? 'Play' : 'Pause',
state.isRunning ? 'Pause' : 'Play'
);
});
elements.resetBtn.addEventListener('click', () => {
state.cars[0].position = 20;
state.cars[0].velocity = 3;
state.cars[0].acceleration = 0;
state.cars[1].position = 40;
state.cars[1].velocity = -2;
state.cars[1].acceleration = 0;
state.lastTime = 0;
updateCarPosition(0);
updateCarPosition(1);
updateUI();
// Reset charts
positionChart.data.labels = [0];
velocityChart.data.labels = [0];
accelerationChart.data.labels = [0];
positionChart.data.datasets[0].data = [20];
positionChart.data.datasets[1].data = [40];
velocityChart.data.datasets[0].data = [3];
velocityChart.data.datasets[1].data = [-2];
accelerationChart.data.datasets[0].data = [0];
accelerationChart.data.datasets[1].data = [0];
positionChart.update();
velocityChart.update();
accelerationChart.update();
});
// Slider event listeners
elements.position1.addEventListener('input', () => {
state.cars[0].position = parseFloat(elements.position1.value);
updateCarPosition(0);
updateUI();
});
elements.velocity1.addEventListener('input', () => {
state.cars[0].velocity = parseFloat(elements.velocity1.value);
updateUI();
});
elements.position2.addEventListener('input', () => {
state.cars[1].position = parseFloat(elements.position2.value);
updateCarPosition(1);
updateUI();
});
elements.velocity2.addEventListener('input', () => {
state.cars[1].velocity = parseFloat(elements.velocity2.value);
updateUI();
});
// Acceleration controls
elements.accelerate1.addEventListener('click', () => {
state.cars[0].acceleration = 1;
state.cars[0].velocity += 0.5;
updateUI();
});
elements.decelerate1.addEventListener('click', () => {
state.cars[0].acceleration = -1;
state.cars[0].velocity -= 0.5;
updateUI();
});
elements.accelerate2.addEventListener('click', () => {
state.cars[1].acceleration = 1;
state.cars[1].velocity += 0.5;
updateUI();
});
elements.decelerate2.addEventListener('click', () => {
state.cars[1].acceleration = -1;
state.cars[1].velocity -= 0.5;
updateUI();
});
// Initialize drag functionality
initDrag(elements.car1, 0);
initDrag(elements.car2, 1);
// Start animation loop
requestAnimationFrame(animationLoop);
// Initial UI update
updateUI();
</script>
</body>
</html>