Canvas圆周行驶汽车:朝向未随弯道转向问题求助
Canvas汽车圆周行驶转向问题解决
问题描述
已实现汽车沿圆周轨迹正确行驶,但车辆始终保持同一朝向,无法随弯道转向。已知需要通过车辆在圆周位置的切线确定其应朝向的Y轴旋转角度,但不清楚如何在Canvas中实现该逻辑,也不知道如何利用切线调整车辆方向。程序通过方向键控制,当前车辆未沿旋转方向移动,期望实现车辆随弯道转向的效果。
问题代码
<style type="text/css"> canvas{ background: #f1f1f1; border:1px solid #eeeeee; } </style> <canvas height="900" width="900" id="canvas" style="float:left;margin-right:10px;"></canvas> <script type="text/javascript"> var canvas = document.getElementById('canvas'); var ctx = canvas.getContext("2d"); var xpos = 300; var ypos = 300; var steering_angle = 0; var motor = 0.0; var heading = 0; var track = 70; var wheelbase = 110; var turn_radius = 0; var targetX = 0; var targetY = 0; var xOffset = 0; var yOffset = 0; var ack = 0; function lineToAngle(ctx, x1, y1, length, angle) { angle *= Math.PI / 180; var x2 = x1 + length * Math.cos(angle), y2 = y1 + length * Math.sin(angle); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); return {x: x2, y: y2}; } var test = 0; function Car(){ this.render = function(){ steering_angle = wheelbase; // Turn Radius if(turn_radius < 0){ turn_radius = wheelbase / Math.sin(steering_angle * Math.PI/180) - (track / 2); // Turning radius }else{ turn_radius = wheelbase / Math.sin(steering_angle * Math.PI/180) + (track / 2); // Turning radius } xOffset = Math.sin(heading) * wheelbase/2; yOffset = Math.cos(heading) * wheelbase/2; ack = { x: xpos + turn_radius * Math.cos(-heading) + wheelbase/2 * Math.sin(-heading), y: ypos + turn_radius * Math.sin(-heading) - wheelbase/2 * Math.cos(-heading) }; var dCenterAck = Math.sqrt(wheelbase/2 * wheelbase/2 + turn_radius * turn_radius); if(Math.abs(turn_radius) > 0 && Math.abs(turn_radius) < 1000){ var angularSpeed = 0; if(turn_radius < 0){ angularSpeed = -(motor / (Math.PI * 2.0 * dCenterAck)) * Math.PI * 2; }else{ angularSpeed = (motor / (Math.PI * 2.0 * dCenterAck)) * Math.PI * 2; } targetX = ack.x + dCenterAck * Math.sin(Math.atan2((xpos - ack.x), (ypos - ack.y)) + angularSpeed); targetY = ack.y + dCenterAck * Math.cos(Math.atan2((xpos - ack.x), (ypos - ack.y)) + angularSpeed); heading += angularSpeed; xpos = targetX; ypos = targetY; }else{ xpos += motor * Math.sin(heading); ypos += motor * Math.cos(heading); } // Arac Ön Orta ve Arka Pozasiyon var CarCenterCoord = {x:xpos, y:ypos}; var CarFrontCoord = {x:xpos - xOffset, y:ypos - yOffset}; var CarBackCoord = {x:xOffset + xpos, y:yOffset + ypos}; //var a = getPosition(-100, -turn_radius, heading); // Arac Tekerlek Pozisyonları var BackRightWheel = { x: xpos + -track/2 * Math.cos(-heading) + wheelbase/2 * Math.sin(-heading), y: ypos + -track/2 * Math.sin(-heading) - wheelbase/2 * Math.cos(-heading) }; var BackLeftWheel = { x: xpos + track/2 * Math.cos(-heading) + wheelbase/2 * Math.sin(-heading), y: ypos + track/2 * Math.sin(-heading) - wheelbase/2 * Math.cos(-heading) }; var FrontRightWheel = { x: xpos + -track/2 * Math.cos(-heading) - wheelbase/2 * Math.sin(-heading), y: ypos + -track/2 * Math.sin(-heading) + wheelbase/2 * Math.cos(-heading) }; var FrontLeftWheel = { x: xpos + track/2 * Math.cos(-heading) - wheelbase/2 * Math.sin(-heading), y: ypos + track/2 * Math.sin(-heading) + wheelbase/2 * Math.cos(-heading) }; // Slip Angle //var FrontAckermannAngleLeft = 0; //var FrontAckermannAngleRight = 0; var BackAckermannAngleLeft = 0; var BackAckermannAngleRight = 0; if(turn_radius > 0){ //BackAckermannAngleLeft = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius + (track / 2))) + Math.sign(wheelbase); //BackAckermannAngleRight = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius - (track / 2))) - Math.sign(wheelbase); FrontAckermannAngleLeft = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius + (track / 2))); FrontAckermannAngleRight = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius - (track / 2))); }else if(turn_radius < 0){ //BackAckermannAngleLeft = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius + (track / 2))) - Math.sign(wheelbase); //BackAckermannAngleRight = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius - (track / 2))) + Math.sign(wheelbase); FrontAckermannAngleLeft = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius - (track / 2))); FrontAckermannAngleRight = (360 / (Math.PI * 2)) * Math.atan(wheelbase / (turn_radius + (track / 2))); }else{ FrontAckermannAngleLeft = 0; FrontAckermannAngleRight = 0; } ctx.fillStyle = "blue"; ctx.beginPath(); ctx.fillRect(BackRightWheel.x, BackRightWheel.y, 5, 5); ctx.fillRect(BackLeftWheel.x, BackLeftWheel.y, 5, 5); ctx.fillRect(FrontRightWheel.x, FrontRightWheel.y, 5, 5); ctx.fillRect(FrontLeftWheel.x, FrontLeftWheel.y, 5, 5); ctx.stroke(); var aa = {x:xpos+FrontAckermannAngleLeft, y:ypos+30}; ctx.lineWidth = 0.5 ctx.strokeStyle = "blue"; ctx.beginPath(); //lineToAngle(ctx, FrontLeftWheel.x, FrontLeftWheel.y, 50, 90-steering_angle); ctx.moveTo(xpos, ypos); ctx.lineTo(aa.x, aa.y); ctx.stroke(); // debug ctx.fillStyle = "red"; ctx.strokeStyle = "blue"; ctx.lineWidth = 0.5 ctx.beginPath(); ctx.fillRect(ack.x, ack.y, 5, 5); //ctx.arc(ack.x, ack.y, Math.abs(turn_radius), 0, 2 * Math.PI, false); ctx.arc(ack.x, ack.y, dCenterAck, 0, 2 * Math.PI, false); //ctx.arc(ack.x, ack.y, 115, 0, 2 * Math.PI, false); //ctx.arc(ack.x, ack.y, 150, 0, 2 * Math.PI, false); //ctx.arc(ack.x, ack.y, 185, 0, 2 * Math.PI, false); //ctx.arc(ack.x, ack.y, 215, 0, 2 * Math.PI, false); ctx.stroke(); // arac arka nokta ctx.fillStyle = "blue"; ctx.beginPath(); ctx.fillRect(CarFrontCoord.x, CarFrontCoord.y, 5, 5); ctx.stroke(); ctx.lineWidth = 0.5 ctx.strokeStyle = "blue"; ctx.beginPath(); ctx.moveTo(ack.x, ack.y); ctx.lineTo(CarFrontCoord.x, CarFrontCoord.y); ctx.stroke(); // arac orta nokta ctx.fillStyle = "red"; ctx.beginPath(); ctx.fillRect(CarCenterCoord.x, CarCenterCoord.y, 5, 5); ctx.stroke(); // arac on nokta ctx.fillStyle = "purple"; ctx.beginPath(); ctx.fillRect(CarBackCoord.x, CarBackCoord.y, 5, 5); ctx.stroke(); ctx.lineWidth = 0.5 ctx.strokeStyle = "purple"; ctx.beginPath(); ctx.moveTo(ack.x, ack.y); ctx.lineTo(FrontRightWheel.x, FrontRightWheel.y); ctx.stroke(); ctx.lineWidth = 0.5 ctx.strokeStyle = "purple"; ctx.beginPath(); ctx.moveTo(ack.x, ack.y); ctx.lineTo(FrontLeftWheel.x, FrontLeftWheel.y); ctx.stroke(); var img = new Image(); img.src = 'https://i.ibb.co/2y2wz7b/car.png'; ctx.save(); ctx.translate(xpos+100/2, ypos); //ctx.rotate(motor * -steering_angle * Math.PI / 360); ctx.translate(-xpos-100/2, -ypos-100/2); ctx.drawImage(img, xpos-100/2, ypos-100/2); ctx.restore(); test += 1; } } var car = new Car(); function renderx(tickTime){ // Time in milisec per frame, to set FPS to 60 = 1000/60 => 16.6, 24 fps => 41 var targetFrameTime = 40; var date = new Date(); var frameStart = date.getMilliseconds(); var endDate = new Date(); var frameStop = endDate.getMilliseconds(); var frameTime = frameStop-frameStart; var timeout = targetFrameTime - frameTime - tickTime; setTimeout(function(){ tick(); }, timeout); ctx.clearRect(-10000, -10000, 10000*canvas.width, 10000*canvas.height); car.render(); } // Key Control var pressedKeys = {}; document.onkeydown = function(e){ e = e || window.event; pressedKeys[e.keyCode] = true; } window.document.onkeyup = function(e){ e = e || window.event; delete pressedKeys[e.keyCode]; } setInterval( function(){ if(pressedKeys[39]){ steering_angle -= 2; } if(pressedKeys[37]){ steering_angle += 2; } if(pressedKeys[38]){ motor += 0.1; } if(pressedKeys[40]){ motor -= 0.1; } }, 30 ); // Delta Time function tick(){ var date = new Date(); var tickStart = date.getMilliseconds(); // Delta.Time var date = new Date(); var tickEnd = date.getMilliseconds(); renderx( tickEnd - tickStart ); } // FPS var times = []; var fps; function refreshLoop(){ window.requestAnimationFrame(function() { const now = performance.now(); while (times.length > 0 && times[0] <= now - 1000) { times.shift(); } times.push(now); fps = times.length; refreshLoop(); }); } refreshLoop(); tick(); </script>
解决方案
核心问题分析
代码中已通过heading变量正确计算车辆行驶方向的角度,但绘制汽车图片时未应用该角度,导致朝向固定;同时steering_angle = wheelbase;会覆盖方向键的转向调整,导致转向控制失效。
具体修改步骤
恢复转向控制功能
删除Car.render()函数开头的steering_angle = wheelbase;代码,让方向键可正常调整转向角度。应用朝向角度绘制汽车
修改汽车图片的绘制逻辑,利用heading实现旋转对齐:var img = new Image(); img.src = 'https://i.ibb.co/2y2wz7b/car.png'; ctx.save(); // 平移到车辆中心作为旋转原点 ctx.translate(xpos, ypos); // 应用heading旋转(负号适配Canvas顺时针旋转的坐标系) ctx.rotate(-heading); // 平移到图片左上角的绘制位置(假设图片为100x100) ctx.translate(-50, -50); ctx.drawImage(img, 0, 0); ctx.restore();注:Canvas默认顺时针为正旋转方向,
heading计算为逆时针增加,因此加负号让车辆朝向与行驶方向一致。验证角度匹配
当前代码中,圆周行驶时已通过heading += angularSpeed;更新朝向角度,直线行驶时也基于heading计算位移,直接应用即可让车辆朝向与轨迹切线方向一致。
效果说明
修改后,车辆在圆周行驶时会自动随弯道调整朝向,方向键可正常控制转向角度,油门键控制行驶速度,实现符合预期的转向效果。
内容的提问来源于stack exchange,提问作者Ramazan ŞAHİN
相关产品推荐
相关产品推荐

