| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Physics Box Animations</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> |
| <style> |
| .physics-container { |
| position: relative; |
| overflow: hidden; |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
| } |
| |
| .box { |
| position: absolute; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: bold; |
| user-select: none; |
| } |
| |
| .controls-panel { |
| backdrop-filter: blur(10px); |
| background-color: rgba(255, 255, 255, 0.8); |
| } |
| |
| .toggle-btn { |
| transition: all 0.3s ease; |
| } |
| |
| .toggle-btn.active { |
| background-color: #3b82f6; |
| color: white; |
| } |
| |
| #gravityValue::after { |
| content: "↓"; |
| position: absolute; |
| right: -20px; |
| } |
| |
| #gravityValue.negative::after { |
| content: "↑"; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="text-center mb-8"> |
| <h1 class="text-4xl font-bold text-blue-800 mb-2">Physics Box Playground</h1> |
| <p class="text-lg text-gray-600">Interactive demonstrations of physics principles using boxes</p> |
| </header> |
| |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> |
| |
| <div class="physics-container rounded-xl shadow-xl h-96 w-full" id="physicsCanvas"> |
| |
| </div> |
| |
| |
| <div class="controls-panel rounded-xl shadow-xl p-6 h-96 overflow-y-auto"> |
| <h2 class="text-2xl font-semibold text-gray-800 mb-4">Simulation Controls</h2> |
| |
| <div class="mb-6"> |
| <h3 class="text-lg font-medium text-gray-700 mb-2">Simulation Type</h3> |
| <div class="flex flex-wrap gap-2 mb-4"> |
| <button id="freeFallBtn" class="toggle-btn active px-4 py-2 bg-blue-100 text-blue-800 rounded-lg">Free Fall</button> |
| <button id="collisionBtn" class="toggle-btn px-4 py-2 bg-blue-100 text-blue-800 rounded-lg">Collisions</button> |
| <button id="stackBtn" class="toggle-btn px-4 py-2 bg-blue-100 text-blue-800 rounded-lg">Stacking</button> |
| <button id="chainBtn" class="toggle-btn px-4 py-2 bg-blue-100 text-blue-800 rounded-lg">Chain Reaction</button> |
| </div> |
| </div> |
| |
| <div class="mb-6"> |
| <h3 class="text-lg font-medium text-gray-700 mb-2">Environment</h3> |
| <div class="grid grid-cols-2 gap-4"> |
| <div> |
| <label for="gravitySlider" class="block text-sm font-medium text-gray-700 mb-1">Gravity</label> |
| <div class="flex items-center"> |
| <input type="range" id="gravitySlider" min="-2" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| <span id="gravityValue" class="ml-3 w-12 text-center font-medium relative">1.0</span> |
| </div> |
| </div> |
| <div> |
| <label for="frictionSlider" class="block text-sm font-medium text-gray-700 mb-1">Friction</label> |
| <div class="flex items-center"> |
| <input type="range" id="frictionSlider" min="0" max="1" step="0.05" value="0.1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| <span id="frictionValue" class="ml-3 w-12 text-center font-medium">0.1</span> |
| </div> |
| </div> |
| <div> |
| <label for="restitutionSlider" class="block text-sm font-medium text-gray-700 mb-1">Bounciness</label> |
| <div class="flex items-center"> |
| <input type="range" id="restitutionSlider" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| <span id="restitutionValue" class="ml-3 w-12 text-center font-medium">0.5</span> |
| </div> |
| </div> |
| <div> |
| <label for="boxCount" class="block text-sm font-medium text-gray-700 mb-1">Box Count</label> |
| <select id="boxCount" class="w-full rounded-lg border-gray-300 text-gray-700"> |
| <option value="5">5 Boxes</option> |
| <option value="10" selected>10 Boxes</option> |
| <option value="20">20 Boxes</option> |
| <option value="50">50 Boxes</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| |
| <div class="flex justify-between"> |
| <button id="resetBtn" class="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition">Reset</button> |
| <button id="addBoxBtn" class="px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition">Add Box</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-white rounded-xl shadow-xl p-6 mb-8"> |
| <h2 class="text-2xl font-semibold text-gray-800 mb-4">Physics Principles Demonstrated</h2> |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| <div class="bg-blue-50 p-4 rounded-lg"> |
| <h3 class="font-medium text-blue-800 mb-2">Newton's Laws</h3> |
| <p class="text-gray-700">Objects in motion stay in motion unless acted upon by an external force. Force equals mass times acceleration.</p> |
| </div> |
| <div class="bg-purple-50 p-4 rounded-lg"> |
| <h3 class="font-medium text-purple-800 mb-2">Gravity</h3> |
| <p class="text-gray-700">Objects accelerate downward at 9.8 m/s² (adjustable in the simulation).</p> |
| </div> |
| <div class="bg-green-50 p-4 rounded-lg"> |
| <h3 class="font-medium text-green-800 mb-2">Collisions</h3> |
| <p class="text-gray-700">Momentum is conserved during collisions between objects.</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const Engine = Matter.Engine, |
| Render = Matter.Render, |
| Runner = Matter.Runner, |
| Bodies = Matter.Bodies, |
| Body = Matter.Body, |
| Composite = Matter.Composite, |
| Mouse = Matter.Mouse, |
| MouseConstraint = Matter.MouseConstraint; |
| |
| |
| const engine = Engine.create({ |
| gravity: { x: 0, y: 1 } |
| }); |
| |
| const container = document.getElementById('physicsCanvas'); |
| const render = Render.create({ |
| element: container, |
| engine: engine, |
| options: { |
| width: container.clientWidth, |
| height: container.clientHeight, |
| wireframes: false, |
| background: 'transparent' |
| } |
| }); |
| |
| |
| const runner = Runner.create(); |
| Runner.run(runner, engine); |
| Render.run(render); |
| |
| |
| const mouse = Mouse.create(render.canvas); |
| const mouseConstraint = MouseConstraint.create(engine, { |
| mouse: mouse, |
| constraint: { |
| stiffness: 0.2, |
| render: { |
| visible: false |
| } |
| } |
| }); |
| Composite.add(engine.world, mouseConstraint); |
| render.mouse = mouse; |
| |
| |
| let boxes = []; |
| let walls = []; |
| let currentSimulation = 'freeFall'; |
| |
| |
| const boxColors = [ |
| '#FF5252', '#FF4081', '#E040FB', '#7C4DFF', '#536DFE', |
| '#448AFF', '#40C4FF', '#18FFFF', '#64FFDA', '#69F0AE', |
| '#B2FF59', '#EEFF41', '#FFFF00', '#FFD740', '#FFAB40' |
| ]; |
| |
| |
| function initSimulation() { |
| |
| Composite.clear(engine.world); |
| boxes = []; |
| walls = []; |
| |
| |
| Composite.add(engine.world, mouseConstraint); |
| |
| |
| const wallOptions = { isStatic: true, render: { visible: false } }; |
| const wallThickness = 50; |
| |
| walls.push( |
| Bodies.rectangle(container.clientWidth / 2, -wallThickness / 2, container.clientWidth, wallThickness, wallOptions), |
| Bodies.rectangle(container.clientWidth / 2, container.clientHeight + wallThickness / 2, container.clientWidth, wallThickness, wallOptions), |
| Bodies.rectangle(-wallThickness / 2, container.clientHeight / 2, wallThickness, container.clientHeight, wallOptions), |
| Bodies.rectangle(container.clientWidth + wallThickness / 2, container.clientHeight / 2, wallThickness, container.clientHeight, wallOptions) |
| ); |
| |
| Composite.add(engine.world, walls); |
| |
| |
| const boxCount = parseInt(document.getElementById('boxCount').value); |
| |
| switch(currentSimulation) { |
| case 'freeFall': |
| createFreeFallBoxes(boxCount); |
| break; |
| case 'collision': |
| createCollisionBoxes(boxCount); |
| break; |
| case 'stack': |
| createStackedBoxes(boxCount); |
| break; |
| case 'chain': |
| createChainReaction(boxCount); |
| break; |
| } |
| } |
| |
| |
| function createFreeFallBoxes(count) { |
| for (let i = 0; i < count; i++) { |
| const size = Math.random() * 30 + 20; |
| const x = Math.random() * (container.clientWidth - size * 2) + size; |
| const y = Math.random() * 100; |
| const color = boxColors[Math.floor(Math.random() * boxColors.length)]; |
| |
| const box = Bodies.rectangle(x, y, size, size, { |
| restitution: parseFloat(document.getElementById('restitutionSlider').value), |
| friction: parseFloat(document.getElementById('frictionSlider').value), |
| render: { |
| fillStyle: color, |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| boxes.push(box); |
| } |
| |
| Composite.add(engine.world, boxes); |
| } |
| |
| |
| function createCollisionBoxes(count) { |
| |
| const group1Count = Math.floor(count / 2); |
| const group2Count = count - group1Count; |
| |
| |
| for (let i = 0; i < group1Count; i++) { |
| const size = Math.random() * 20 + 20; |
| const x = Math.random() * 100 + 50; |
| const y = Math.random() * (container.clientHeight - 100) + 50; |
| const color = '#448AFF'; |
| |
| const box = Bodies.rectangle(x, y, size, size, { |
| restitution: parseFloat(document.getElementById('restitutionSlider').value), |
| friction: parseFloat(document.getElementById('frictionSlider').value), |
| render: { |
| fillStyle: color, |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| |
| Body.setVelocity(box, { x: 5, y: 0 }); |
| |
| boxes.push(box); |
| } |
| |
| |
| for (let i = 0; i < group2Count; i++) { |
| const size = Math.random() * 20 + 20; |
| const x = Math.random() * 100 + (container.clientWidth - 200); |
| const y = Math.random() * (container.clientHeight - 100) + 50; |
| const color = '#FF5252'; |
| |
| const box = Bodies.rectangle(x, y, size, size, { |
| restitution: parseFloat(document.getElementById('restitutionSlider').value), |
| friction: parseFloat(document.getElementById('frictionSlider').value), |
| render: { |
| fillStyle: color, |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| |
| Body.setVelocity(box, { x: -5, y: 0 }); |
| |
| boxes.push(box); |
| } |
| |
| Composite.add(engine.world, boxes); |
| } |
| |
| |
| function createStackedBoxes(count) { |
| const baseWidth = 200; |
| const baseHeight = 30; |
| const baseX = container.clientWidth / 2; |
| const baseY = container.clientHeight - 50; |
| |
| |
| const base = Bodies.rectangle(baseX, baseY, baseWidth, baseHeight, { |
| isStatic: true, |
| render: { |
| fillStyle: '#795548', |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| Composite.add(engine.world, base); |
| |
| |
| const maxPerRow = 5; |
| const boxSize = 30; |
| const rows = Math.ceil(count / maxPerRow); |
| |
| for (let row = 0; row < rows; row++) { |
| const boxesInRow = Math.min(count - row * maxPerRow, maxPerRow); |
| const rowWidth = boxesInRow * boxSize; |
| const startX = baseX - rowWidth / 2 + boxSize / 2; |
| |
| for (let i = 0; i < boxesInRow; i++) { |
| const x = startX + i * boxSize; |
| const y = baseY - (row + 0.5) * boxSize - 5; |
| const color = boxColors[(row * maxPerRow + i) % boxColors.length]; |
| |
| const box = Bodies.rectangle(x, y, boxSize - 2, boxSize - 2, { |
| restitution: parseFloat(document.getElementById('restitutionSlider').value), |
| friction: parseFloat(document.getElementById('frictionSlider').value), |
| render: { |
| fillStyle: color, |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| boxes.push(box); |
| } |
| } |
| |
| Composite.add(engine.world, boxes); |
| } |
| |
| |
| function createChainReaction(count) { |
| |
| const floor = Bodies.rectangle( |
| container.clientWidth / 2, |
| container.clientHeight - 10, |
| container.clientWidth, |
| 20, |
| { |
| isStatic: true, |
| render: { |
| fillStyle: '#795548', |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| } |
| ); |
| |
| Composite.add(engine.world, floor); |
| |
| |
| const dominoWidth = 15; |
| const dominoHeight = 60; |
| const spacing = 20; |
| const startX = 100; |
| const startY = container.clientHeight - 20 - dominoHeight/2; |
| |
| |
| const ball = Bodies.circle(50, startY - 30, 15, { |
| restitution: 0.9, |
| density: 0.005, |
| render: { |
| fillStyle: '#FF5252', |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| |
| Body.setVelocity(ball, { x: 15, y: 0 }); |
| |
| boxes.push(ball); |
| |
| |
| for (let i = 0; i < count; i++) { |
| const x = startX + i * (dominoWidth + spacing); |
| const color = boxColors[i % boxColors.length]; |
| |
| const domino = Bodies.rectangle(x, startY, dominoWidth, dominoHeight, { |
| chamfer: { radius: 2 }, |
| friction: 0.3, |
| restitution: 0.2, |
| render: { |
| fillStyle: color, |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| |
| Body.setAngle(domino, -0.1); |
| |
| boxes.push(domino); |
| } |
| |
| Composite.add(engine.world, boxes); |
| |
| |
| const endWall = Bodies.rectangle( |
| startX + count * (dominoWidth + spacing) + 50, |
| container.clientHeight / 2, |
| 20, |
| container.clientHeight, |
| { |
| isStatic: true, |
| render: { |
| visible: false |
| } |
| } |
| ); |
| |
| Composite.add(engine.world, endWall); |
| } |
| |
| |
| function addBox() { |
| const size = Math.random() * 30 + 20; |
| const x = Math.random() * (container.clientWidth - size * 2) + size; |
| const y = -50; |
| const color = boxColors[Math.floor(Math.random() * boxColors.length)]; |
| |
| const box = Bodies.rectangle(x, y, size, size, { |
| restitution: parseFloat(document.getElementById('restitutionSlider').value), |
| friction: parseFloat(document.getElementById('frictionSlider').value), |
| render: { |
| fillStyle: color, |
| strokeStyle: '#000', |
| lineWidth: 1 |
| } |
| }); |
| |
| boxes.push(box); |
| Composite.add(engine.world, box); |
| } |
| |
| |
| document.getElementById('freeFallBtn').addEventListener('click', function() { |
| currentSimulation = 'freeFall'; |
| updateActiveButton(this); |
| initSimulation(); |
| }); |
| |
| document.getElementById('collisionBtn').addEventListener('click', function() { |
| currentSimulation = 'collision'; |
| updateActiveButton(this); |
| initSimulation(); |
| }); |
| |
| document.getElementById('stackBtn').addEventListener('click', function() { |
| currentSimulation = 'stack'; |
| updateActiveButton(this); |
| initSimulation(); |
| }); |
| |
| document.getElementById('chainBtn').addEventListener('click', function() { |
| currentSimulation = 'chain'; |
| updateActiveButton(this); |
| initSimulation(); |
| }); |
| |
| document.getElementById('gravitySlider').addEventListener('input', function() { |
| const value = parseFloat(this.value); |
| engine.gravity.y = value; |
| document.getElementById('gravityValue').textContent = value.toFixed(1); |
| |
| if (value < 0) { |
| document.getElementById('gravityValue').classList.add('negative'); |
| } else { |
| document.getElementById('gravityValue').classList.remove('negative'); |
| } |
| }); |
| |
| document.getElementById('frictionSlider').addEventListener('input', function() { |
| const value = parseFloat(this.value); |
| document.getElementById('frictionValue').textContent = value.toFixed(2); |
| |
| |
| boxes.forEach(box => { |
| box.friction = value; |
| box.frictionStatic = value * 1.5; |
| }); |
| }); |
| |
| document.getElementById('restitutionSlider').addEventListener('input', function() { |
| const value = parseFloat(this.value); |
| document.getElementById('restitutionValue').textContent = value.toFixed(2); |
| |
| |
| boxes.forEach(box => { |
| box.restitution = value; |
| }); |
| }); |
| |
| document.getElementById('boxCount').addEventListener('change', function() { |
| initSimulation(); |
| }); |
| |
| document.getElementById('resetBtn').addEventListener('click', function() { |
| initSimulation(); |
| }); |
| |
| document.getElementById('addBoxBtn').addEventListener('click', function() { |
| addBox(); |
| }); |
| |
| |
| function updateActiveButton(activeBtn) { |
| document.querySelectorAll('.toggle-btn').forEach(btn => { |
| btn.classList.remove('active'); |
| }); |
| activeBtn.classList.add('active'); |
| } |
| |
| |
| window.addEventListener('resize', function() { |
| render.options.width = container.clientWidth; |
| render.options.height = container.clientHeight; |
| Render.setPixelRatio(render, window.devicePixelRatio); |
| |
| |
| if (walls.length >= 4) { |
| Body.setPosition(walls[0], { x: container.clientWidth / 2, y: -25 }); |
| Body.setPosition(walls[1], { x: container.clientWidth / 2, y: container.clientHeight + 25 }); |
| Body.setPosition(walls[2], { x: -25, y: container.clientHeight / 2 }); |
| Body.setPosition(walls[3], { x: container.clientWidth + 25, y: container.clientHeight / 2 }); |
| } |
| }); |
| |
| |
| initSimulation(); |
| </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=leos123122/simple-physic-simulator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |