const canvas = document.getElementById('gameCanvas'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; canvas.style.backgroundColor = '#f0f0f0'; const ctx = canvas.getContext('2d'); (function(){ const COLORS = { player: '#2d2d2d', particles: '#2d2d2d', objects: '#2d2d2d', terrain: '#2d2d2d' }; const gravity = 1; const jumpForce = 14.75; const player = { x: 200, y: 0, vx: 16, vy: 16, vm: 32, jumpForce: gravity + jumpForce, onGround: false, width: 20, height: 20, distanceTraveled: 0 }; const terrain = { start: { x: 0, y: 0 }, end: { x: canvas.width, y: canvas.height }, height: 8 }; const objects = []; let lastObjectX = 0; const particles = []; const createObject = () => { const object = { x: lastObjectX + 10 * canvas.width / 10, width: Math.random() * 100, height: Math.random() * 22 }; let terrainY = ((terrain.end.y - terrain.start.y) / (terrain.end.x - terrain.start.x)) * (object.x - terrain.start.x) + terrain.start.y; object.y = terrainY - object.height; objects.push(object); lastObjectX = object.x; }; const isColliding = (obj1, obj2) => ( obj1.x < obj2.x + obj2.width && obj1.x + obj1.width > obj2.x && obj1.y < obj2.y + obj2.height && obj1.y + obj1.height > obj2.y ); const checkCollision = () => { objects.forEach(object => { if (isColliding(player, object)) { if (player.vx >= 1) { player.vx -= 0.5; player.vy -= 0.5; } } }); }; const updatePlayer = () => { player.vy += gravity; player.x += player.vx; player.y += player.vy; const terrainY = (terrain.end.y - terrain.start.y) / (terrain.end.x - terrain.start.x) * (player.x - terrain.start.x) + terrain.start.y; if (player.y + player.height > terrainY) { player.y = terrainY - player.height; player.vy = 0; } if (particlesEnabled) { particles.push({ x: player.x, y: player.y, vx: Math.random() * 2 - 1, vy: Math.random() * 2 - 1, lifespan: player.vx * 4 }); } }; const updateObjects = () => { for (let object of objects) { object.y += gravity; let terrainY = ((terrain.end.y - terrain.start.y) / (terrain.end.x - terrain.start.x)) * (object.x - terrain.start.x) + terrain.start.y; if (object.y + object.height > terrainY) { object.y = terrainY - object.height; } } }; const drawPlayer = () => { if (player.vx < 4) { ctx.fillStyle = COLORS.player; ctx.font = '22px Arial'; const text = 'Jump to build up speed!'; ctx.fillText(text, player.x, player.y - 12); } else { if (distanceMeterEnabled) { ctx.fillStyle = '#aaa'; ctx.font = '22px Arial'; const text = player.distanceTraveled.toFixed(0) + ' px'; ctx.fillText(text, player.x, player.y - 12); } } ctx.fillStyle = COLORS.player; ctx.fillRect(player.x, player.y, player.width, player.height); }; const drawObjects = () => { ctx.fillStyle = COLORS.objects; objects.forEach(object => { ctx.beginPath(); ctx.arc(object.x, object.y, object.width / 2, 0, Math.PI * 2, true); ctx.fill(); }); }; const drawTerrain = () => { ctx.beginPath(); ctx.moveTo(terrain.start.x, terrain.start.y); ctx.lineTo(terrain.end.x, terrain.end.y); ctx.strokeStyle = COLORS.terrain; ctx.lineWidth = terrain.height; ctx.stroke(); }; const drawParticles = () => { if (particlesEnabled) { ctx.fillStyle = COLORS.particles; particles.forEach((particle, index) => { ctx.fillRect(particle.x, particle.y, 5, 5); particle.x += particle.vx; particle.y += particle.vy; particle.lifespan--; if (particle.lifespan === 0) { particles.splice(index, 1); } }); } }; const draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(-player.x + canvas.width / 4, -player.y + canvas.height / 2); drawPlayer(); terrain.start.x = player.x - canvas.width; terrain.end.x = player.x + canvas.width; drawObjects(); drawTerrain(); drawParticles(); ctx.restore(); }; const update = (currentTime = 0) => { const deltaTime = currentTime - lastUpdateTime; player.distanceTraveled = Math.abs(player.x - 200); if (deltaTime >= frameDelay) { createObject(); updatePlayer(); updateObjects(); checkCollision(); lastUpdateTime = currentTime; } requestAnimationFrame(update); }; const initialize = () => { terrain.start = { x: 0, y: 0 }; terrain.end = { x: canvas.width, y: canvas.height }; }; const gameLoop = () => { initialize(); draw(); requestAnimationFrame(gameLoop); }; const playerDoJump = () => { const terrainY = (terrain.end.y - terrain.start.y) / (terrain.end.x - terrain.start.x) * (player.x - terrain.start.x) + terrain.start.y; if (player.y + player.height >= terrainY) { player.vy = player.jumpForce * -1; if (player.vx < player.vm) { player.vx += 0.5; } } }; const handleKeyDown = event => { if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowUp' || event.key === 'w' || event.key === 'W') { playerDoJump(); } }; canvas.addEventListener('click', playerDoJump); canvas.addEventListener('touchstart', playerDoJump); window.addEventListener('keydown', handleKeyDown); window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; draw(); }); const helpText = 'Controls:\n\nJump: space, up arrow, w or tap the screen on mobile\nFullscreen: f or the button in the upper-right\nParticles: p\nDistance: d\nHelp: h' document.addEventListener('keydown', function(event) { if (event.key === 'f') { if (canvas.requestFullscreen) { canvas.requestFullscreen(); } else if (canvas.mozRequestFullScreen) { canvas.mozRequestFullScreen(); } else if (canvas.webkitRequestFullscreen) { canvas.webkitRequestFullscreen(); } else if (canvas.msRequestFullscreen) { canvas.msRequestFullscreen(); } } else if (event.key === 'p' || event.key === 'P') { particlesEnabled = !particlesEnabled; } else if (event.key === 'h' || event.key === 'H') { alert(helpText); } else if (event.key === 'd' || event.key === 'D') { distanceMeterEnabled = !distanceMeterEnabled; } }); let particlesEnabled = true; let distanceMeterEnabled = false; const fullscreenButton = document.createElement('button'); fullscreenButton.style.position = 'absolute'; fullscreenButton.style.top = '10px'; fullscreenButton.style.right = '10px'; fullscreenButton.textContent = 'Fullscreen'; const distanceMeterButton = document.createElement('button'); distanceMeterButton.style.position = 'absolute'; distanceMeterButton.style.top = '40px'; distanceMeterButton.style.right = '10px'; distanceMeterButton.textContent = 'Distance'; const particlesButton = document.createElement('button'); particlesButton.style.position = 'absolute'; particlesButton.style.top = '70px'; particlesButton.style.right = '10px'; particlesButton.textContent = 'Particles'; const helpButton = document.createElement('button'); helpButton.style.position = 'absolute'; helpButton.style.top = '100px'; helpButton.style.right = '10px'; helpButton.textContent = 'Help'; fullscreenButton.addEventListener('click', function() { if (canvas.requestFullscreen) { canvas.requestFullscreen(); } else if (canvas.mozRequestFullScreen) { canvas.mozRequestFullScreen(); } else if (canvas.webkitRequestFullscreen) { canvas.webkitRequestFullscreen(); } else if (canvas.msRequestFullscreen) { canvas.msRequestFullscreen(); } }); distanceMeterButton.addEventListener('click', function() { distanceMeterEnabled = !distanceMeterEnabled; }); particlesButton.addEventListener('click', function() { particlesEnabled = !particlesEnabled; }); helpButton.addEventListener('click', function() { alert(helpText); }); canvas.parentNode.appendChild(fullscreenButton); canvas.parentNode.appendChild(distanceMeterButton); canvas.parentNode.appendChild(particlesButton); canvas.parentNode.appendChild(helpButton); let lastUpdateTime = 0; const fps = 60; const frameDelay = 1000 / fps; update(); gameLoop(); } )();