javascriptcanvasgeometryhtml5-canvas360-degrees

Navigate according to degrees in circle


I try to build a 2d canvas game in javascript and nodejs. In the game the player is a ship that can navigate his way in the sea. The ship object is sprite sheet with 16 ship positions (22.5 degree for evrey frame) start from 0 to 360 degrees.

To navigate the ship i use NippleJS, its give me the degree in 0-360 (for example: 328.9051138238949).

Now, i need algorethem that check which sprite from the 16 should be display in evrey moment.

The ship must to move 1 sprite left or right in every check degree and its must be the best way (best way mean if ship degree is 0 and the degree from Nipplejs is 335 the ship should navigae right instead of left..).

I start to think about it and i understand that i need to display image frame if degree is between -(22.5/2) and (22.5/2) but i got alot of problems when i over 360 and more things..

To understand each other lets call ship degree ship.degree and the degree from Nipplejs we will call nipplejs.degree, and the ship sprite will represented by ship.sprite[0-16] (0 = right direction).

Please help.

circle 360 degrees


Solution

  • To turn along the smallest angle the function below will step the angle by the number of steps angleMoveSteps per 360 deg. Eg 90 steps will step in 4 deg steps.

    It will stop stepping if the angle difference is less than this step.

    const angleSteps = 16;
    const angleMoveSteps = 90;
    
    function getNextAngle(newDirection, currentDirection) {
        // normalize both directions to 360 
        newDirection %= 360;
        currentDirection %= 360;
    
        // if new direction is less than current move it ahead by 360
        newDirection = newDirection < currentDirection ? newDirection + 360 : newDirection;
    
        // get the difference (will always be positive)
        const dif = newDirection - currentDirection;
    
        // if the difference is greater than 180 and less than 360 - step
        // turn CCW
        if (dif > 360 / 2 && dif < 360 - (360 / angleMoveSteps)) {
             return currentDirection - (360 / angleMoveSteps);
        }
    
        // if the difference is greater than step and less than 180
        // turn CW
        if (dif > (360 / angleMoveSteps) && dif < 360 / 2){
            return currentDirection + (360 / angleMoveSteps);
        }
    
        // do nothing if within step angle of target
         return currentDirection
    }
    

    To convert from deg to image index the following function will do that. See demo for compact versions.

    function getStepAngle(dir) {  // dir in degrees
    
        // normalise the angle
        dir = (dir % 360 + 360) % 360;
    
        // scale to angleSteps 0 to 16
        dir /= 360 / angleSteps;
    
        // offset by half a step
        dir += 0.5;
    
        // floor the result to get integer
        dir = Math.floor(dir);
    
        // Get remainder to ensure value between 0 and 16 not including 16
        return dir % angleSteps;
    }
    

    Demo

    The demo shows the two functions being used. Click on the dial to set a new target direction and the current direction will move to the new direction, showing the moving angle (green) and the image index (blue) as it moves.

    const angleSteps = 16;
    const angleMoveSteps = 90;
    var currentDir = 0;
    var shipDir = 0;
    var targetAngle = 0;
    
    
    function getNextAngle(newDirection, currentDirection) {
        const step = 360 / angleMoveSteps
        newDirection %= 360;
        currentDirection %= 360;
        newDirection = newDirection < currentDirection ? newDirection + 360 : newDirection;
        const dif = newDirection - currentDirection;
    
        if (dif > 360 / 2 && dif < 360 - step) { return currentDirection - step }
        if (dif > step && dif < 360 / 2) { return currentDirection + step }
         return currentDirection
    }
    
    function getStepAngle(dir) {
        return Math.floor(((dir % 360 + 360) % 360) / (360 / angleSteps) + 0.5) % angleSteps;
    }
    
    
    
    
    
    
    /* Demo code from here down */
    Math.TAU = Math.PI * 2;
    const ctx = canvas.getContext("2d")
    const w = canvas.width;
    const h = canvas.height;
    var seeking = false;
    const speed = 100; // milliseconds
    
    update();
    canvas.addEventListener("click", event => {
       const bounds = canvas.getBoundingClientRect();
       const x = event.pageX - bounds.left - scrollX;
       const y = event.pageY - bounds.top  - scrollY;
       targetAngle = Math.atan2(y - w / 2, x - h / 2) * 180 / Math.PI;
       if(!seeking){ render() }
    });
    function render() {
       requestAnimationFrame(update);
       var newDir = getNextAngle(targetAngle, currentDir);
       if(newDir !== currentDir) {
           currentDir = newDir;
           seeking = true;
           setTimeout(render, speed);
       } else {
           currentDir = targetAngle;
            setTimeout(()=>requestAnimationFrame(update), speed);
           seeking = false;
       }
       
    }
    
    function update() {
       shipDir = getStepAngle(currentDir);
       clear();
       drawCompase();
       drawTargetAngle(targetAngle);
       drawCurrentAngle(currentDir);
       drawStepAngle(shipDir);
    
    }
    
    function clear() { ctx.clearRect(0,0,w,h) }
    
    function angleText(text,x,y,angle,size = 12, col = "#000") {
        const xAX = Math.cos(angle);
        const xAY = Math.sin(angle);
        ctx.fillStyle = col;
        ctx.font = size + "px arial";
        ctx.textAlign = "right";
        ctx.textBaseline = "middle";
        if(xAX < 0) {
            ctx.setTransform(-xAX, -xAY, xAY, -xAX, x, y);
            ctx.textAlign = "left";
        
        } else {
            ctx.setTransform(xAX, xAY, -xAY, xAX, x, y);
            ctx.textAlign = "right";
        }
        ctx.fillText(text,0,0);
    }
    function drawCompase() {
        var i;
        const rad = h * 0.4, rad1 = h * 0.395, rad2 = h * 0.41;
        ctx.lineWidth = 1;
        ctx.strokeStyle = "#000";
        ctx.beginPath();
        ctx.arc(w / 2, h / 2, rad, 0, Math.TAU);
        ctx.stroke();
    
        ctx.lineWidth = 2;
        ctx.beginPath();
        for (i = 0; i < 1; i += 1 / angleSteps) {
             const ang = i * Math.TAU;
             ctx.moveTo(Math.cos(ang) * rad1 + w / 2, Math.sin(ang) * rad1 + h / 2);
             ctx.lineTo(Math.cos(ang) * rad2 + w / 2, Math.sin(ang) * rad2 + h / 2);
        }
        ctx.stroke();
    
        for (i = 0; i < 1; i += 1 / angleSteps) {
             const ang = i * Math.TAU;
             angleText(
                 (ang * 180 / Math.PI).toFixed(1).replace(".0",""), 
                 Math.cos(ang) * (rad1 - 2) + w / 2, 
                 Math.sin(ang) * (rad1 - 2) + h / 2,
                 ang
             );
        }
        ctx.setTransform(1,0,0,1,0,0);
    }
    function drawTargetAngle(angle) { // angle in deg
        const rad = h * 0.30, rad1 = h * 0.1, rad2 = h * 0.34;
        const ang = angle * Math.PI / 180;
        const fromA = ang - Math.PI / (angleSteps * 4);
        
        const toA = ang + Math.PI / (angleSteps * 4);
        ctx.linewidth = 2;
        ctx.strokeStyle = "#F00";
        ctx.beginPath();       
        
        ctx.moveTo(Math.cos(fromA) * rad + w / 2, Math.sin(fromA) * rad + h / 2);
        ctx.lineTo(Math.cos(ang) * rad2 + w / 2, Math.sin(ang) * rad2 + h / 2);
        ctx.lineTo(Math.cos(toA) * rad + w / 2, Math.sin(toA) * rad + h / 2);       
        ctx.stroke();  
        angleText(
             angle.toFixed(1).replace(".0",""), 
             Math.cos(ang) * (rad - 4) + w / 2, 
             Math.sin(ang) * (rad - 4) + h / 2,
             ang,
             12, "#F00"
        );      
        ctx.setTransform(1,0,0,1,0,0);
    
    }
    function drawCurrentAngle(angle) { // angle in deg
        const rad = h * 0.14, rad2 = h * 0.17;
        const ang = angle * Math.PI / 180;
        const fromA = ang - Math.PI / (angleSteps * 2);
        
        const toA = ang + Math.PI / (angleSteps * 2);
        ctx.linewidth = 2;
        ctx.strokeStyle = "#0A0";
        ctx.beginPath();       
        
        ctx.moveTo(Math.cos(fromA) * rad + w / 2, Math.sin(fromA) * rad + h / 2);
        ctx.lineTo(Math.cos(ang) * rad2 + w / 2, Math.sin(ang) * rad2 + h / 2);
        ctx.lineTo(Math.cos(toA) * rad + w / 2, Math.sin(toA) * rad + h / 2);       
        ctx.stroke();  
        angleText(
             angle.toFixed(1).replace(".0",""), 
             Math.cos(ang) * (rad - 4) + w / 2, 
             Math.sin(ang) * (rad - 4) + h / 2,
             ang,
             12, "#0A0"
        );      
        ctx.setTransform(1,0,0,1,0,0);
    
    }
    function drawStepAngle(angle) { // ang 0 to angleSteps cyclic
        var ang = angle % angleSteps;
        ang *= Math.PI / angleSteps*2;
        const fromA = ang - Math.PI / angleSteps;
        const toA = ang + Math.PI / angleSteps;
        
        const rad = h * 0.4, rad1 = h * 0.35, rad2 = h * 0.44;
        const rad3 = h * 0.34, rad4 = h * 0.45;
        ctx.linewidth = 1;
        ctx.strokeStyle = "#08F";
        ctx.beginPath();   
        ctx.arc(w / 2, h / 2, rad1, fromA, toA);
        ctx.moveTo(w / 2 + Math.cos(fromA) * rad2, h / 2 + Math.sin(fromA) * rad2, 0, Math.TAU);
        ctx.arc(w / 2, h / 2, rad2,  fromA, toA);
        ctx.stroke();
        
        ctx.linewidth = 2;
        ctx.beginPath();       
        
         ctx.moveTo(Math.cos(fromA) * rad3 + w / 2, Math.sin(fromA) * rad3 + h / 2);
         ctx.lineTo(Math.cos(fromA) * rad4 + w / 2, Math.sin(fromA) * rad4 + h / 2);
         ctx.moveTo(Math.cos(toA) * rad3 + w / 2, Math.sin(toA) * rad3 + h / 2);
         ctx.lineTo(Math.cos(toA) * rad4 + w / 2, Math.sin(toA) * rad4 + h / 2);       
         ctx.stroke();     
         
         angleText(
             angle, 
             Math.cos(ang + 0.1) * (rad - 2) + w / 2, 
             Math.sin(ang + 0.1) * (rad - 2) + h / 2,
             ang,
             16, "#08F"
         );     
         
        ctx.setTransform(1,0,0,1,0,0);
    }
    body { font-family: arial }
    canvas {
     position: absolute;
     top: 0px;
     left: 0px;
    }
    <span> Click to set new target direction</span>
    <canvas id="canvas" width="400" height="400"></canvas>