console
class VehiclePhysics {
constructor(config) {
this.config = {
mass: 1500,
power: 100000,
wheelBase: 2.5,
maxSteer: Math.PI/4,
dragCoeff: 0.3,
...config
};
this.state = {
x: 0, y: 0,
v: 0,
theta: 0,
delta: 0,
throttle: 0
};
}
update(dt, controls) {
const { mass, power, wheelBase, dragCoeff } = this.config;
const { throttle, delta } = controls;
const tractionForce = Math.min(throttle * power / Math.max(this.state.v, 1), 5000);
const dragForce = 0.5*dragCoeff*1.225*2.5*this.state.v**2;
const acceleration = (tractionForce - dragForce) / mass;
this.state.v += acceleration * dt;
const beta = Math.atan(Math.tan(delta) / 2);
this.state.x += this.state.v * Math.cos(this.state.theta + beta) * dt;
this.state.y += this.state.v * Math.sin(this.state.theta + beta) * dt;
this.state.theta += this.state.v * Math.tan(delta) * Math.cos(beta) / wheelBase * dt;
this.state.delta = Math.max(-this.config.maxSteer,
Math.min(this.config.maxSteer, delta));
}
}
class ControlSystem {
constructor() {
this.controls = {
throttle: 0,
delta: 0,
autoPilot: true
};
this.keyState = {};
this.setupControls();
}
setupControls() {
document.addEventListener('keydown', (e) => this.handleKey(e, true));
document.addEventListener('keyup', (e) => this.handleKey(e, false));
}
handleKey(e, isDown) {
this.keyState[e.key] = isDown;
if(isDown) this.controls.autoPilot = false;
}
update(targetPoint) {
if(this.controls.autoPilot) {
this.autoControl(targetPoint);
} else {
this.manualControl();
}
return this.controls;
}
manualControl() {
if(this.keyState['ArrowUp'] || this.keyState['w']) {
this.controls.throttle = Math.min(this.controls.throttle + 0.1, 1);
} else if(this.keyState['ArrowDown'] || this.keyState['s']) {
this.controls.throttle = Math.max(this.controls.throttle - 0.1, 0);
}
if(this.keyState['ArrowLeft'] || this.keyState['a']) {
this.controls.delta = Math.max(this.controls.delta - 0.1, -Math.PI/4);
} else if(this.keyState['ArrowRight'] || this.keyState['d']) {
this.controls.delta = Math.min(this.controls.delta + 0.1, Math.PI/4);
}
}
autoControl(target) {
const dx = target.x - this.controls.currentX;
const dy = target.y - this.controls.currentY;
const targetAngle = Math.atan2(dy, dx);
const angleDiff = targetAngle - this.controls.currentTheta;
this.controls.delta = Math.sin(angleDiff) * 0.5;
this.controls.throttle = 0.3 + Math.random() * 0.2;
}
}
class DriftCarSim {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.initDOM();
this.initSimulation();
this.setupEventListeners();
this.startLoop();
}
initDOM() {
this.container.innerHTML = `
<div class="container">
<canvas></canvas>
<div class="controls"></div>
</div>
`;
this.canvas = this.container.querySelector('canvas');
this.ctx = this.canvas.getContext('2d');
this.resizeCanvas();
this.initControls();
}
initSimulation() {
this.vehicle = new VehiclePhysics({
wheelBase: 2.5,
maxSteer: Math.PI/4
});
this.controlSystem = new ControlSystem();
this.target = { x: 100, y: 100 };
this.state = {
history: [],
targets: [],
showParams: true,
showTrail: true,
showTargetLine: true
};
}
initControls() {
const controls = this.container.querySelector('.controls');
controls.innerHTML = `
<div style="position:absolute; right:10px; top:10px; background:rgba(255,255,255,0.8); padding:8px; border-radius:4px;">
<label><input type="checkbox" checked> 显示轨迹</label><br>
<label><input type="checkbox" checked> 显示目标线</label><br>
<label><input type="checkbox"> 自动模式</label><br>
<label><input type="checkbox" checked> 显示参数</label>
</div>
`;
controls.querySelectorAll('input').forEach((input, i) => {
input.onchange = () => this.handleControlChange(i, input.checked);
});
}
handleControlChange(index, checked) {
switch(index) {
case 0: this.state.showTrail = checked; break;
case 1: this.state.showTargetLine = checked; break;
case 2: this.controlSystem.controls.autoPilot = checked; break;
case 3: this.state.showParams = checked; break;
}
}
resizeCanvas() {
this.canvas.width = this.container.offsetWidth;
this.canvas.height = this.container.offsetHeight;
}
setupEventListeners() {
window.addEventListener('resize', () => this.resizeCanvas());
this.canvas.addEventListener('mousemove', e => this.handleMouse(e));
}
handleMouse(e) {
if(!this.controlSystem.controls.autoPilot) return;
const rect = this.canvas.getBoundingClientRect();
this.target = {
x: (e.clientX - rect.left) / 10,
y: (e.clientY - rect.top) / 10
};
}
startLoop() {
const frame = () => {
this.update();
this.draw();
requestAnimationFrame(frame);
};
frame();
}
update() {
const controls = this.controlSystem.update(this.target);
this.vehicle.update(0.016, controls);
this.state.history.push({...this.vehicle.state});
if(this.state.history.length > 100) this.state.history.shift();
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.setTransform(1,0,0,1,0,0);
this.ctx.translate(
this.canvas.width/2 - this.vehicle.state.x * 10,
this.canvas.height/2 - this.vehicle.state.y * 10
);
if(this.state.showTargetLine) {
this.drawPath(this.state.targets, '#FF9800');
}
if(this.state.showTrail) {
this.drawPath(this.state.history, '#E91E63');
}
this.drawCar();
if(this.state.showParams) {
this.drawParams();
}
}
drawPath(points, color) {
if(points.length < 2) return;
this.ctx.beginPath();
this.ctx.moveTo(points[0].x * 10, points[0].y * 10);
points.forEach(p => this.ctx.lineTo(p.x * 10, p.y * 10));
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 2;
this.ctx.stroke();
}
drawCar() {
this.ctx.save();
this.ctx.translate(this.vehicle.state.x * 10, this.vehicle.state.y * 10);
this.ctx.rotate(this.vehicle.state.theta);
this.ctx.fillStyle = '#2196F3';
this.ctx.fillRect(-15, -10, 30, 20);
this.ctx.fillStyle = '#666';
this.ctx.fillRect(10, -8, 5, 16);
this.ctx.fillRect(10, -8 + this.vehicle.state.delta * 30, 5, 16);
this.ctx.restore();
}
drawParams() {
this.ctx.setTransform(1,0,0,1,0,0);
this.ctx.fillStyle = 'black';
this.ctx.font = '14px Arial';
const params = [
`速度: ${(this.vehicle.state.v * 3.6).toFixed(1)} km/h`,
`油门: ${(this.controlSystem.controls.throttle * 100).toFixed(0)}%`,
`转向: ${(this.vehicle.state.delta * 180/Math.PI).toFixed(1)}°`,
`模式: ${this.controlSystem.controls.autoPilot ? '自动' : '手动'}`
];
params.forEach((text, i) => {
this.ctx.fillText(text, 10, 30 + i * 20);
});
}
}
new DriftCarSim('simContainer');
<div id="simContainer" style="width:500px;height:500px"></div>
.container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#canvas {
background: #f0f0f0;
}
.controls {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
}