PifPaf's picture
implement the simulator
6ff038a verified
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')
};
// Set canvas size
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Drone state
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' // 'fpv' or 'map'
};
// Controls
const controls = {
throttle: 0,
yaw: 0,
pitch: 0,
roll: 0,
windSpeed: 5,
gravity: 9.8
};
// Environment
const environment = {
obstacles: [],
terrain: []
};
// Setup controls
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);
});
// Buttons
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; // Start slightly above ground
}
}
function land() {
if (drone.isFlying) {
drone.isFlying = false;
drone.z = 0;
drone.velocityZ = 0;
}
}
// Physics update
function updatePhysics(deltaTime) {
if (!drone.isFlying) return;
// Apply controls
drone.rotationX += controls.pitch * 0.01 * deltaTime;
drone.rotationY += controls.yaw * 0.01 * deltaTime;
drone.rotationZ += controls.roll * 0.01 * deltaTime;
// Clamp rotations
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));
// Calculate forces
const liftForce = controls.throttle / 10;
const forwardForce = Math.sin(drone.rotationX) * liftForce;
const sideForce = Math.sin(drone.rotationZ) * liftForce;
// Apply forces
drone.velocityX += forwardForce * deltaTime;
drone.velocityY += sideForce * deltaTime;
drone.velocityZ += (liftForce - controls.gravity) * deltaTime;
// Apply wind
drone.velocityX += (Math.random() - 0.5) * controls.windSpeed * 0.1 * deltaTime;
drone.velocityY += (Math.random() - 0.5) * controls.windSpeed * 0.1 * deltaTime;
// Apply drag
const drag = 0.98;
drone.velocityX *= drag;
drone.velocityY *= drag;
drone.velocityZ *= drag;
// Update position
drone.x += drone.velocityX * deltaTime;
drone.y += drone.velocityY * deltaTime;
drone.z += drone.velocityZ * deltaTime;
// Ground collision
if (drone.z < 0) {
drone.z = 0;
drone.velocityZ = 0;
if (drone.isFlying && Math.abs(drone.velocityX) + Math.abs(drone.velocityY) < 0.5) {
land();
}
}
// Battery drain
drone.battery -= 0.05 * deltaTime;
if (drone.battery <= 0) {
drone.battery = 0;
land();
}
}
// Render
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (drone.viewMode === 'fpv') {
renderFPV();
} else {
renderMap();
}
// Update OSD
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() {
// Sky gradient
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);
// Horizon line
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();
// Ground
ctx.fillStyle = '#334155';
ctx.fillRect(0, horizonY, canvas.width, canvas.height - horizonY);
// Drone crosshair
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() {
// Background
ctx.fillStyle = '#1e293b';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Grid
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();
}
// Drone
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);
// Drone body
ctx.fillStyle = '#10b981';
ctx.beginPath();
ctx.arc(0, 0, 15, 0, Math.PI * 2);
ctx.fill();
// Drone arms
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();
// Propellers
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();
// Altitude indicator
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);
}
// Game loop
let lastTime = 0;
function gameLoop(timestamp) {
const deltaTime = Math.min(100, timestamp - lastTime) / 1000;
lastTime = timestamp;
updatePhysics(deltaTime);
render();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
});