|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
const canvas = document.getElementById('droneCanvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const osdElements = { |
|
|
altitude: document.getElementById('altitude'), |
|
|
speed: document.getElementById('speed'), |
|
|
battery: document.getElementById('battery') |
|
|
}; |
|
|
|
|
|
|
|
|
function resizeCanvas() { |
|
|
const container = canvas.parentElement; |
|
|
canvas.width = container.clientWidth; |
|
|
canvas.height = container.clientHeight; |
|
|
} |
|
|
window.addEventListener('resize', resizeCanvas); |
|
|
resizeCanvas(); |
|
|
|
|
|
|
|
|
const drone = { |
|
|
x: 0, |
|
|
y: 0, |
|
|
z: 0, |
|
|
rotationX: 0, |
|
|
rotationY: 0, |
|
|
rotationZ: 0, |
|
|
velocityX: 0, |
|
|
velocityY: 0, |
|
|
velocityZ: 0, |
|
|
battery: 100, |
|
|
isFlying: false, |
|
|
viewMode: 'fpv' |
|
|
}; |
|
|
|
|
|
|
|
|
const controls = { |
|
|
throttle: 0, |
|
|
yaw: 0, |
|
|
pitch: 0, |
|
|
roll: 0, |
|
|
windSpeed: 5, |
|
|
gravity: 9.8 |
|
|
}; |
|
|
|
|
|
|
|
|
const environment = { |
|
|
obstacles: [], |
|
|
terrain: [] |
|
|
}; |
|
|
|
|
|
|
|
|
document.getElementById('throttle').addEventListener('input', (e) => { |
|
|
controls.throttle = parseInt(e.target.value); |
|
|
}); |
|
|
document.getElementById('yaw').addEventListener('input', (e) => { |
|
|
controls.yaw = parseInt(e.target.value); |
|
|
}); |
|
|
document.getElementById('pitch').addEventListener('input', (e) => { |
|
|
controls.pitch = parseInt(e.target.value); |
|
|
}); |
|
|
document.getElementById('roll').addEventListener('input', (e) => { |
|
|
controls.roll = parseInt(e.target.value); |
|
|
}); |
|
|
document.getElementById('windSpeed').addEventListener('input', (e) => { |
|
|
controls.windSpeed = parseInt(e.target.value); |
|
|
}); |
|
|
document.getElementById('gravity').addEventListener('input', (e) => { |
|
|
controls.gravity = parseFloat(e.target.value); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('viewToggle').addEventListener('click', () => { |
|
|
drone.viewMode = drone.viewMode === 'fpv' ? 'map' : 'fpv'; |
|
|
document.getElementById('viewToggle').textContent = |
|
|
drone.viewMode === 'fpv' ? 'FPV View' : 'Map View'; |
|
|
}); |
|
|
document.getElementById('resetDrone').addEventListener('click', resetDrone); |
|
|
document.getElementById('takeoff').addEventListener('click', takeoff); |
|
|
document.getElementById('land').addEventListener('click', land); |
|
|
|
|
|
function resetDrone() { |
|
|
Object.assign(drone, { |
|
|
x: 0, |
|
|
y: 0, |
|
|
z: 0, |
|
|
rotationX: 0, |
|
|
rotationY: 0, |
|
|
rotationZ: 0, |
|
|
velocityX: 0, |
|
|
velocityY: 0, |
|
|
velocityZ: 0, |
|
|
battery: 100, |
|
|
isFlying: false |
|
|
}); |
|
|
} |
|
|
|
|
|
function takeoff() { |
|
|
if (!drone.isFlying) { |
|
|
drone.isFlying = true; |
|
|
drone.z = 1; |
|
|
} |
|
|
} |
|
|
|
|
|
function land() { |
|
|
if (drone.isFlying) { |
|
|
drone.isFlying = false; |
|
|
drone.z = 0; |
|
|
drone.velocityZ = 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updatePhysics(deltaTime) { |
|
|
if (!drone.isFlying) return; |
|
|
|
|
|
|
|
|
drone.rotationX += controls.pitch * 0.01 * deltaTime; |
|
|
drone.rotationY += controls.yaw * 0.01 * deltaTime; |
|
|
drone.rotationZ += controls.roll * 0.01 * deltaTime; |
|
|
|
|
|
|
|
|
drone.rotationX = Math.max(-Math.PI/4, Math.min(Math.PI/4, drone.rotationX)); |
|
|
drone.rotationZ = Math.max(-Math.PI/4, Math.min(Math.PI/4, drone.rotationZ)); |
|
|
|
|
|
|
|
|
const liftForce = controls.throttle / 10; |
|
|
const forwardForce = Math.sin(drone.rotationX) * liftForce; |
|
|
const sideForce = Math.sin(drone.rotationZ) * liftForce; |
|
|
|
|
|
|
|
|
drone.velocityX += forwardForce * deltaTime; |
|
|
drone.velocityY += sideForce * deltaTime; |
|
|
drone.velocityZ += (liftForce - controls.gravity) * deltaTime; |
|
|
|
|
|
|
|
|
drone.velocityX += (Math.random() - 0.5) * controls.windSpeed * 0.1 * deltaTime; |
|
|
drone.velocityY += (Math.random() - 0.5) * controls.windSpeed * 0.1 * deltaTime; |
|
|
|
|
|
|
|
|
const drag = 0.98; |
|
|
drone.velocityX *= drag; |
|
|
drone.velocityY *= drag; |
|
|
drone.velocityZ *= drag; |
|
|
|
|
|
|
|
|
drone.x += drone.velocityX * deltaTime; |
|
|
drone.y += drone.velocityY * deltaTime; |
|
|
drone.z += drone.velocityZ * deltaTime; |
|
|
|
|
|
|
|
|
if (drone.z < 0) { |
|
|
drone.z = 0; |
|
|
drone.velocityZ = 0; |
|
|
if (drone.isFlying && Math.abs(drone.velocityX) + Math.abs(drone.velocityY) < 0.5) { |
|
|
land(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
drone.battery -= 0.05 * deltaTime; |
|
|
if (drone.battery <= 0) { |
|
|
drone.battery = 0; |
|
|
land(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function render() { |
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
if (drone.viewMode === 'fpv') { |
|
|
renderFPV(); |
|
|
} else { |
|
|
renderMap(); |
|
|
} |
|
|
|
|
|
|
|
|
osdElements.altitude.textContent = drone.z.toFixed(1); |
|
|
osdElements.speed.textContent = Math.sqrt( |
|
|
drone.velocityX * drone.velocityX + |
|
|
drone.velocityY * drone.velocityY + |
|
|
drone.velocityZ * drone.velocityZ |
|
|
).toFixed(1); |
|
|
osdElements.battery.textContent = drone.battery.toFixed(0); |
|
|
} |
|
|
|
|
|
function renderFPV() { |
|
|
|
|
|
const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); |
|
|
skyGradient.addColorStop(0, '#1e3a8a'); |
|
|
skyGradient.addColorStop(1, '#0c4a6e'); |
|
|
ctx.fillStyle = skyGradient; |
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
|
|
|
const horizonY = canvas.height / 2 - drone.rotationX * 50; |
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; |
|
|
ctx.lineWidth = 2; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(0, horizonY); |
|
|
ctx.lineTo(canvas.width, horizonY); |
|
|
ctx.stroke(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#334155'; |
|
|
ctx.fillRect(0, horizonY, canvas.width, canvas.height - horizonY); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = '#10b981'; |
|
|
ctx.lineWidth = 2; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(canvas.width / 2 - 20, canvas.height / 2); |
|
|
ctx.lineTo(canvas.width / 2 + 20, canvas.height / 2); |
|
|
ctx.moveTo(canvas.width / 2, canvas.height / 2 - 20); |
|
|
ctx.lineTo(canvas.width / 2, canvas.height / 2 + 20); |
|
|
ctx.stroke(); |
|
|
} |
|
|
|
|
|
function renderMap() { |
|
|
|
|
|
ctx.fillStyle = '#1e293b'; |
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; |
|
|
ctx.lineWidth = 1; |
|
|
const gridSize = 50; |
|
|
for (let x = 0; x < canvas.width; x += gridSize) { |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(x, 0); |
|
|
ctx.lineTo(x, canvas.height); |
|
|
ctx.stroke(); |
|
|
} |
|
|
for (let y = 0; y < canvas.height; y += gridSize) { |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(0, y); |
|
|
ctx.lineTo(canvas.width, y); |
|
|
ctx.stroke(); |
|
|
} |
|
|
|
|
|
|
|
|
const centerX = canvas.width / 2; |
|
|
const centerY = canvas.height / 2; |
|
|
const droneX = centerX + drone.x * 10; |
|
|
const droneY = centerY - drone.y * 10; |
|
|
|
|
|
ctx.save(); |
|
|
ctx.translate(droneX, droneY); |
|
|
ctx.rotate(drone.rotationY); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#10b981'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(0, 0, 15, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = '#10b981'; |
|
|
ctx.lineWidth = 3; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(-20, 0); |
|
|
ctx.lineTo(20, 0); |
|
|
ctx.moveTo(0, -20); |
|
|
ctx.lineTo(0, 20); |
|
|
ctx.stroke(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#ffffff'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(-20, 0, 5, 0, Math.PI * 2); |
|
|
ctx.arc(20, 0, 5, 0, Math.PI * 2); |
|
|
ctx.arc(0, -20, 5, 0, Math.PI * 2); |
|
|
ctx.arc(0, 20, 5, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
ctx.restore(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; |
|
|
ctx.fillRect(10, 10, 20, 100); |
|
|
ctx.fillStyle = '#10b981'; |
|
|
const altHeight = Math.min(100, drone.z * 10); |
|
|
ctx.fillRect(10, 110 - altHeight, 20, altHeight); |
|
|
} |
|
|
|
|
|
|
|
|
let lastTime = 0; |
|
|
function gameLoop(timestamp) { |
|
|
const deltaTime = Math.min(100, timestamp - lastTime) / 1000; |
|
|
lastTime = timestamp; |
|
|
|
|
|
updatePhysics(deltaTime); |
|
|
render(); |
|
|
requestAnimationFrame(gameLoop); |
|
|
} |
|
|
requestAnimationFrame(gameLoop); |
|
|
}); |