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.
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;
}
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>