| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Spring Jump - Hooke's Law Physics Game</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> |
| #gameCanvas { |
| background: linear-gradient(to bottom, #87CEEB, #E0F7FA); |
| border-radius: 10px; |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); |
| } |
| |
| .spring { |
| transition: height 0.1s ease-out; |
| } |
| |
| .platform { |
| background: linear-gradient(to right, #4CAF50, #8BC34A); |
| border-radius: 5px; |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
| } |
| |
| .character { |
| background: linear-gradient(to bottom, #FF5722, #FF9800); |
| border-radius: 50% 50% 40% 40%; |
| position: relative; |
| } |
| |
| .character::before { |
| content: ""; |
| position: absolute; |
| width: 10px; |
| height: 10px; |
| background: #333; |
| border-radius: 50%; |
| top: 25%; |
| left: 30%; |
| } |
| |
| .character::after { |
| content: ""; |
| position: absolute; |
| width: 10px; |
| height: 10px; |
| background: #333; |
| border-radius: 50%; |
| top: 25%; |
| right: 30%; |
| } |
| |
| .score-display { |
| background: rgba(255, 255, 255, 0.8); |
| border-radius: 20px; |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
| } |
| |
| .instructions { |
| background: rgba(255, 255, 255, 0.9); |
| border-radius: 10px; |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4"> |
| <div class="w-full max-w-4xl"> |
| <h1 class="text-4xl font-bold text-center text-orange-600 mb-2">Spring Jump</h1> |
| <p class="text-center text-gray-700 mb-6">Experience Hooke's Law in action! Hold space to compress the spring and release to jump.</p> |
| |
| <div class="relative"> |
| <canvas id="gameCanvas" width="800" height="500" class="w-full border-2 border-gray-300"></canvas> |
| |
| <div class="instructions absolute top-4 left-4 p-3 max-w-xs"> |
| <h3 class="font-bold text-gray-800 mb-2">How to Play:</h3> |
| <ul class="text-sm text-gray-700 list-disc pl-4"> |
| <li>Hold <span class="font-mono bg-gray-200 px-1">SPACE</span> to compress spring</li> |
| <li>Release to jump</li> |
| <li>Longer compression = higher jump</li> |
| <li>Reach platforms to score points</li> |
| </ul> |
| </div> |
| |
| <div class="score-display absolute top-4 right-4 px-4 py-2"> |
| <p class="text-lg font-bold text-gray-800">Score: <span id="score">0</span></p> |
| <p class="text-sm text-gray-600">High Score: <span id="highScore">0</span></p> |
| </div> |
| </div> |
| |
| <div class="mt-6 flex justify-between items-center"> |
| <div class="flex items-center"> |
| <div class="w-8 h-8 rounded-full bg-orange-500 mr-2"></div> |
| <span class="text-gray-700">Character</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="w-8 h-3 rounded bg-green-500 mr-2"></div> |
| <span class="text-gray-700">Platforms</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="w-2 h-8 bg-gray-800 mr-2 spring-icon"></div> |
| <span class="text-gray-700">Spring</span> |
| </div> |
| </div> |
| |
| <div class="mt-6 bg-white p-4 rounded-lg shadow"> |
| <h3 class="font-bold text-gray-800 mb-2">Physics Explained:</h3> |
| <p class="text-gray-700 mb-2">This game demonstrates <span class="font-bold">Hooke's Law</span> (F = -kx) where:</p> |
| <ul class="text-sm text-gray-700 list-disc pl-4 mb-3"> |
| <li>F = Force applied by the spring</li> |
| <li>k = Spring constant (stiffness)</li> |
| <li>x = Displacement from equilibrium</li> |
| </ul> |
| <p class="text-gray-700">The more you compress the spring (increase x), the more force it applies, resulting in higher jumps!</p> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const canvas = document.getElementById('gameCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const scoreElement = document.getElementById('score'); |
| const highScoreElement = document.getElementById('highScore'); |
| |
| |
| const { Engine, Render, Runner, Bodies, Composite, Body, Events, Mouse, MouseConstraint } = Matter; |
| |
| |
| const engine = Engine.create({ |
| gravity: { x: 0, y: 1 } |
| }); |
| |
| |
| let score = 0; |
| let highScore = localStorage.getItem('highScore') || 0; |
| let isCompressing = false; |
| let compressionStartTime = 0; |
| let maxCompression = 0; |
| const springConstant = 0.002; |
| const characterSize = 30; |
| let character, spring, ground; |
| let platforms = []; |
| let gameStarted = false; |
| |
| |
| function initGame() { |
| |
| Composite.clear(engine.world); |
| |
| |
| ground = Bodies.rectangle(canvas.width / 2, canvas.height - 10, canvas.width, 20, { |
| isStatic: true, |
| render: { |
| fillStyle: '#795548' |
| } |
| }); |
| |
| |
| character = Bodies.circle(canvas.width / 2, canvas.height - 60, characterSize, { |
| restitution: 0.3, |
| friction: 0.1, |
| render: { |
| fillStyle: '#FF5722' |
| } |
| }); |
| |
| |
| spring = { |
| x: canvas.width / 2, |
| y: canvas.height - 40, |
| width: 5, |
| height: 20, |
| naturalHeight: 20, |
| compressedHeight: 5 |
| }; |
| |
| |
| createPlatforms(); |
| |
| |
| Composite.add(engine.world, [ground, character, ...platforms]); |
| |
| |
| score = 0; |
| scoreElement.textContent = score; |
| highScoreElement.textContent = highScore; |
| |
| gameStarted = true; |
| } |
| |
| |
| function createPlatforms() { |
| platforms = []; |
| |
| |
| platforms.push( |
| Bodies.rectangle(canvas.width / 2, canvas.height - 30, 200, 20, { |
| isStatic: true, |
| render: { |
| fillStyle: '#4CAF50' |
| }, |
| label: 'platform' |
| }) |
| ); |
| |
| |
| const platformCount = 8; |
| const minHeight = canvas.height / 2; |
| const maxHeight = 100; |
| |
| for (let i = 0; i < platformCount; i++) { |
| const width = 100 + Math.random() * 100; |
| const x = 50 + Math.random() * (canvas.width - 100); |
| const y = minHeight - (i * ((minHeight - maxHeight) / platformCount)); |
| |
| platforms.push( |
| Bodies.rectangle(x, y, width, 15, { |
| isStatic: true, |
| render: { |
| fillStyle: '#8BC34A' |
| }, |
| label: 'platform' |
| }) |
| ); |
| } |
| } |
| |
| |
| function handleSpring() { |
| if (!gameStarted) return; |
| |
| const characterPos = character.position; |
| |
| |
| const isOnSurface = platforms.some(platform => { |
| return ( |
| characterPos.y + characterSize >= platform.position.y - 10 && |
| characterPos.y + characterSize <= platform.position.y + 10 && |
| characterPos.x >= platform.position.x - platform.bounds.max.x + platform.position.x && |
| characterPos.x <= platform.position.x + platform.bounds.max.x - platform.position.x |
| ); |
| }); |
| |
| if (isOnSurface) { |
| |
| spring.x = characterPos.x; |
| spring.y = characterPos.y + characterSize; |
| |
| |
| if (isCompressing) { |
| const compressionTime = Date.now() - compressionStartTime; |
| const compressionRatio = Math.min(compressionTime / 1000, 1); |
| |
| |
| spring.height = spring.naturalHeight - (compressionRatio * (spring.naturalHeight - spring.compressedHeight)); |
| |
| |
| maxCompression = Math.max(maxCompression, compressionRatio); |
| } else { |
| |
| spring.height = spring.naturalHeight; |
| } |
| |
| |
| if (!isCompressing && maxCompression > 0) { |
| |
| const jumpForce = -springConstant * (maxCompression * 1000); |
| |
| |
| Body.applyForce(character, character.position, { |
| x: 0, |
| y: jumpForce |
| }); |
| |
| maxCompression = 0; |
| } |
| } else { |
| |
| spring.height = 0; |
| } |
| } |
| |
| |
| function checkPlatformCollisions() { |
| if (!gameStarted) return; |
| |
| platforms.forEach(platform => { |
| if ( |
| character.position.y + characterSize >= platform.position.y - 15 && |
| character.position.y + characterSize <= platform.position.y + 5 && |
| character.position.x >= platform.position.x - (platform.bounds.max.x - platform.position.x) && |
| character.position.x <= platform.position.x + (platform.bounds.max.x - platform.position.x) && |
| character.velocity.y > 0 |
| ) { |
| |
| if (!platform.scored) { |
| score += 10; |
| scoreElement.textContent = score; |
| |
| if (score > highScore) { |
| highScore = score; |
| highScoreElement.textContent = highScore; |
| localStorage.setItem('highScore', highScore); |
| } |
| |
| platform.scored = true; |
| |
| |
| platforms.forEach(p => { |
| if (p !== platform) p.scored = false; |
| }); |
| } |
| } |
| }); |
| } |
| |
| |
| function gameLoop() { |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| ctx.fillStyle = '#795548'; |
| ctx.fillRect(0, canvas.height - 10, canvas.width, 20); |
| |
| |
| platforms.forEach(platform => { |
| ctx.fillStyle = platform.render.fillStyle; |
| ctx.fillRect( |
| platform.position.x - (platform.bounds.max.x - platform.position.x), |
| platform.position.y - (platform.bounds.max.y - platform.position.y), |
| platform.bounds.max.x - platform.bounds.min.x, |
| platform.bounds.max.y - platform.bounds.min.y |
| ); |
| }); |
| |
| |
| if (spring.height > 0) { |
| ctx.fillStyle = '#333'; |
| ctx.fillRect( |
| spring.x - spring.width / 2, |
| spring.y, |
| spring.width, |
| spring.height |
| ); |
| |
| |
| const coils = 4; |
| const coilHeight = spring.height / coils; |
| |
| for (let i = 0; i < coils; i++) { |
| ctx.beginPath(); |
| ctx.arc( |
| spring.x, |
| spring.y + (i * coilHeight) + (coilHeight / 2), |
| spring.width * 1.5, |
| 0, |
| Math.PI, |
| true |
| ); |
| ctx.strokeStyle = '#333'; |
| ctx.lineWidth = 2; |
| ctx.stroke(); |
| } |
| } |
| |
| |
| ctx.fillStyle = character.render.fillStyle; |
| ctx.beginPath(); |
| ctx.arc(character.position.x, character.position.y, characterSize, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| |
| ctx.fillStyle = '#333'; |
| ctx.beginPath(); |
| ctx.arc(character.position.x - 10, character.position.y - 5, 5, 0, Math.PI * 2); |
| ctx.arc(character.position.x + 10, character.position.y - 5, 5, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| if (isCompressing) { |
| ctx.moveTo(character.position.x - 10, character.position.y + 5); |
| ctx.lineTo(character.position.x + 10, character.position.y + 5); |
| } else { |
| ctx.arc(character.position.x, character.position.y + 5, 10, 0, Math.PI); |
| } |
| ctx.strokeStyle = '#333'; |
| ctx.lineWidth = 2; |
| ctx.stroke(); |
| |
| |
| handleSpring(); |
| |
| |
| checkPlatformCollisions(); |
| |
| |
| if (character.position.y > canvas.height + 100) { |
| gameStarted = false; |
| setTimeout(initGame, 1000); |
| } |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| |
| window.addEventListener('keydown', (e) => { |
| if (e.code === 'Space') { |
| if (!isCompressing && gameStarted) { |
| isCompressing = true; |
| compressionStartTime = Date.now(); |
| } |
| } |
| }); |
| |
| window.addEventListener('keyup', (e) => { |
| if (e.code === 'Space') { |
| isCompressing = false; |
| } |
| }); |
| |
| |
| canvas.addEventListener('touchstart', () => { |
| if (!isCompressing && gameStarted) { |
| isCompressing = true; |
| compressionStartTime = Date.now(); |
| } |
| }); |
| |
| canvas.addEventListener('touchend', () => { |
| isCompressing = false; |
| }); |
| |
| |
| initGame(); |
| gameLoop(); |
| |
| |
| const runner = Runner.create(); |
| Runner.run(runner, engine); |
| </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=arirajuns/space-2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |