You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;会覆盖方向键的转向调整,导致转向控制失效。

具体修改步骤

  1. 恢复转向控制功能
    删除Car.render()函数开头的steering_angle = wheelbase;代码,让方向键可正常调整转向角度。

  2. 应用朝向角度绘制汽车
    修改汽车图片的绘制逻辑,利用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计算为逆时针增加,因此加负号让车辆朝向与行驶方向一致。

  3. 验证角度匹配
    当前代码中,圆周行驶时已通过heading += angularSpeed;更新朝向角度,直线行驶时也基于heading计算位移,直接应用即可让车辆朝向与轨迹切线方向一致。

效果说明

修改后,车辆在圆周行驶时会自动随弯道调整朝向,方向键可正常控制转向角度,油门键控制行驶速度,实现符合预期的转向效果。

内容的提问来源于stack exchange,提问作者Ramazan ŞAHİN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 21:20:52