I want to animate a rectangle on html canvas. when the user will click the canvas, the rectangle will start it's animation, and go to the clicked position. I used delta x and y to add and subtract pixels from the x and y position of the rectangle.
But the problem with this solution is, I can't find a way to make the rectangle animate in a straight path.
My code:
'use strict'
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const ratio = Math.ceil(window.devicePixelRatio)
let height = window.innerHeight
let width = window.innerWidth
canvas.height = height * ratio
canvas.width = width * ratio
canvas.style.height = `${height}px`
canvas.style.width = `${width}px`
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
let position = {
x: 0,
y: 0,
deltaX: 0,
deltaY: 0,
size: 20
}
let move = {
x: 0,
y: 0,
}
function animate() {
if (position.x === move.x && position.y === move.y) {
cancelAnimationFrame()
}
if (position.x !== move.x) {
ctx.fillRect(position.x, position.y, position.size, position.size)
position.x += position.deltaX
}
if (position.y !== move.y) {
ctx.fillRect(position.x, position.y, position.size, position.size)
position.y += position.deltaY
}
requestAnimationFrame(animate)
}
function moveTo(x, y) {
move.x = x
move.y = y
position.deltaX = position.x > x ? -1 : 1
position.deltaY = position.y > y ? -1 : 1
animate()
}
canvas.addEventListener('click', (event) => {
moveTo(event.clientX, event.clientY)
})
ctx.fillRect(position.x, position.y, position.size, position.size)
<canvas id="canvas">
</canvas>
If you click in the canvas the rectangle will start moving but it'll go in a weird path, I can't find a way to properly go straight at the clicked position.
see demo at Github page
Here is the sample Math from something I did a while ago...
If there is something you don't understand there ask
In your code it was moving "weird" because your delta values where always 1 or -1, no fractions, that limits the way the object can travel, instead we do our calculations using the angle.
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let player = {
x: 0, y: 0, size: 10,
delta: { x: 0, y: 0 },
move: { x: 0, y: 0 }
}
function animate() {
let a = player.x - player.move.x;
let b = player.y - player.move.y;
if (Math.sqrt( a*a + b*b ) < 2) {
player.delta = { x: 0, y: 0 }
}
player.x += player.delta.x
player.y += player.delta.y
ctx.fillRect(player.x, player.y, player.size, player.size)
requestAnimationFrame(animate)
}
function moveTo(x, y) {
player.move.x = x
player.move.y = y
let angle = Math.atan2(y - player.y, x - player.x)
player.delta.x = Math.cos(angle)
player.delta.y = Math.sin(angle)
}
canvas.addEventListener('click', (event) => {
moveTo(event.clientX, event.clientY)
})
animate()
<canvas id="canvas"></canvas>
Related
In the following p5.js code I'm trying to create 2 separate methods.
centerInWindow() is meant to keep the image centered in the canvas while it's being scaled down after the user clicks on the canvas.
centerToClick() is meant to keep the image centered on the point the user clicked on, while it's being scaled up.
None of them work and I'm having trouble getting the logic right.
function centerInWindow(img) {
let currentSize = img.width * currentScale
imgX = (windowWidth / 2) - (currentSize / 2)
imgY = (windowHeight / 2) - (currentSize / 2)
}
function centerToClick() {
imgX = clickX * currentScale
imgY = clickY * currentScale
}
let minScale = 1
let maxScale = 5
let targetScale = minScale
let currentScale = targetScale
let clickX, clickY, imgX, imgY
let idx = 0
function setup() {
pixelDensity(1)
createCanvas(windowWidth, windowHeight)
preload(IMG_PATHS, IMGS)
frameRate(12)
}
function draw() {
clear()
if (currentScale < targetScale) {
currentScale += 0.05
if (currentScale > targetScale) {
currentScale = targetScale
}
centerToClick()
} else if (currentScale > targetScale) {
currentScale -= 0.05
if (currentScale < targetScale) {
currentScale = targetScale
}
centerInWindow(IMGS[idx])
} else {
centerInWindow(IMGS[idx])
}
scale(currentScale)
image(IMGS[idx], imgX, imgY)
idx++
if (idx === IMGS.length) {
idx = 0
}
}
window.addEventListener('click', function({ clientX, clientY }) {
targetScale = targetScale === maxScale ? minScale : maxScale
clickX = clientX
clickY = clientY
})
See it in action here.
Any help would be appreciated.
There are probably multiple ways to solve this problem, but here's one:
Imaging your viewport is an NxM rectangle, and you are drawing some portion of a scene in within that viewport. In order to zoom in and out you can shift the origin at which you draw that scene and increase or decrease the scale. The tricky part is to make it possible to zoom in and out centered on an arbitrary point within the currently visible portion of the scene, keeping that point in the scene locked to the current point in the viewport.
Given some center point, and a desired scale factor, it is possible to determine the necessary change in the offset of the scene to preserve the position of the center point after scaling.
There's probably some complicated trigonometric proof for how to calculate this, but conveniently it is a simple calculation based on the ratio of the offset of the mouse from the current top left of the scene, to the scaled height of the scene.
x_offset -= (x_center - x_offset) / (N * current_scale) * (N * new_scale - N * current_scale)
y_offset -= (y_center - y_offset) / (M * current_scale) * (M * new_scale - M * current_scale)
Conveniently it is possible to apply this repeatedly as your scale changes and regardless of whether scale is increasing or decreasing.
Here's a sample sketch demonstrating this:
const viewport = { width: 400, height: 300 };
let scaledView = { ...viewport, x: 0, y: 0 };
function setup() {
createCanvas(windowWidth, windowHeight);
viewport.x = (width - viewport.width) / 2;
viewport.y = (height - viewport.height) / 2;
}
function draw() {
background(255);
translate(viewport.x, viewport.y);
push();
translate(scaledView.x, scaledView.y);
scale(scaledView.width / viewport.width, scaledView.height / viewport.height);
// Draw scene
ellipseMode(CENTER);
noStroke();
fill(200);
rect(0, 0, viewport.width, viewport.height);
stroke('blue');
noFill();
strokeWeight(1);
translate(viewport.width / 2, viewport.height / 2);
circle(0, 0, 200);
arc(0, 0, 120, 120, PI * 0.25, PI * 0.75);
strokeWeight(4)
point(-40, -40);
point(40, -40);
pop();
noFill();
stroke(0);
rect(0, 0, viewport.width, viewport.height);
// viewport relative mouse position
let mousePos = { x: mouseX - viewport.x, y: mouseY - viewport.y };
if (mousePos.x >= 0 && mousePos.x <= viewport.width &&
mousePos.y >= 0 && mousePos.y <= viewport.height) {
line(scaledView.x, scaledView.y, mousePos.x, mousePos.y);
let updatedView = keyIsDown(SHIFT) ? getUnZoomedView(mousePos) : getZoomedView(mousePos);
line(scaledView.x, scaledView.y, updatedView.x, updatedView.y);
stroke('red');
rect(updatedView.x, updatedView.y, updatedView.width, updatedView.height);
}
}
function getZoomedView(center) {
return getScaledView(center, 1.1);
}
function getUnZoomedView(center) {
return getScaledView(center, 0.9);
}
function getScaledView(center, factor) {
// the center position relative to the scaled/shifted scene
let viewCenterPos = {
x: center.x - scaledView.x,
y: center.y - scaledView.y
};
// determine how much we will have to shift to keep the position centered
let shift = {
x: map(viewCenterPos.x, 0, scaledView.width, 0, 1),
y: map(viewCenterPos.y, 0, scaledView.height, 0, 1)
};
// calculate the new view dimensions
let updatedView = {
width: scaledView.width * factor,
height: scaledView.height * factor
};
// adjust the x and y offsets according to the shift
updatedView.x = scaledView.x + (updatedView.width - scaledView.width) * -shift.x;
updatedView.y = scaledView.y + (updatedView.height - scaledView.height) * -shift.y;
return updatedView;
}
function mouseClicked() {
// viewport relative mouse position
let mousePos = { x: mouseX - viewport.x, y: mouseY - viewport.y };
if (mousePos.x >= 0 && mousePos.x <= viewport.width &&
mousePos.y >= 0 && mousePos.y <= viewport.height) {
scaledView = keyIsDown(SHIFT) ? getUnZoomedView(mousePos) : getZoomedView(mousePos);
}
}
html, body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
We can actually simplify these equations because N and M can be factored out. The previous pseudo code becomes:
x_offset -= ((x_center - x_offset) * (new_scale - current_scale) * N) / (current_scale * N)
And because both the top and bottom are multiplied by N this becomes:
x_offset -= ((x_center - x_offset) * (new_scale - current_scale)) / (current_scale)
Here is an example using your image drawing code:
const IMG_PATHS = [
'https://storage.googleapis.com/www.paulwheeler.us/files/windows-95-desktop-background.jpg'
];
let IMGS = [];
const minScale = 1;
const maxScale = 5;
let targetScale = minScale;
let currentScale = targetScale;
let targetOrigin = {
x: 0,
y: 0
};
let currentOrigin = {
x: 0,
y: 0
};
let idx = 0;
function preloadHelper(pathsToImgs, imgs) {
for (let pathToImg of pathsToImgs) {
loadImage(pathToImg, img => {
imgs.push(img);
})
}
}
// Make sure load images happen from the actual preload() function. p5.js has special logic when these calls happen here to have the sketch wait to start until all the loadXXX class are complete
function preload() {
preloadHelper(IMG_PATHS, IMGS);
}
function setup() {
pixelDensity(1)
createCanvas(windowWidth, windowHeight)
frameRate(12)
}
function draw() {
clear();
/*
if (currentScale < targetScale) {
currentScale += 0.01
} else if (currentScale > targetScale) {
currentScale -= 0.01
} */
// By making all of the changing components part of a vector and normalizing it we can ensure that the we reach our target origin and scale at the same point
let offset = createVector(
targetOrigin.x - currentOrigin.x,
targetOrigin.y - currentOrigin.y,
// Give the change in scale more weight so that it happens at a similar rate to the translation. This is especially noticable when there is little to no offset required
(targetScale - currentScale) * 500
);
if (offset.magSq() > 0.01) {
// Multiplying by a larger number will move faster
offset.normalize().mult(8);
currentOrigin.x += offset.x;
currentOrigin.y += offset.y;
currentScale += offset.z / 500;
// We need to make sure we do not over shoot or targets
if (offset.x > 0 && currentOrigin.x > targetOrigin.x) {
currentOrigin.x = targetOrigin.x;
}
if (offset.x < 0 && currentOrigin.x < targetOrigin.x) {
currentOrigin.x = targetOrigin.x;
}
if (offset.y > 0 && currentOrigin.y > targetOrigin.y) {
currentOrigin.y = targetOrigin.y;
}
if (offset.y < 0 && currentOrigin.y < targetOrigin.y) {
currentOrigin.y = targetOrigin.y;
}
if (offset.z > 0 && currentScale > targetScale) {
currentScale = targetScale;
}
if (offset.z < 0 && currentScale < targetScale) {
currentScale = targetScale;
}
}
translate(currentOrigin.x, currentOrigin.y);
scale(currentScale);
image(IMGS[idx], 0, 0);
}
function mouseClicked() {
targetScale = constrain(
keyIsDown(SHIFT) ? currentScale * 0.9 : currentScale * 1.1,
minScale,
maxScale
);
targetOrigin = getScaledOrigin({
x: mouseX,
y: mouseY
},
currentScale,
targetScale
);
}
function getScaledOrigin(center, currentScale, newScale) {
// the center position relative to the scaled/shifted scene
let viewCenterPos = {
x: center.x - currentOrigin.x,
y: center.y - currentOrigin.y
};
// determine the new origin
let originShift = {
x: viewCenterPos.x / currentScale * (newScale - currentScale),
y: viewCenterPos.y / currentScale * (newScale - currentScale)
};
return {
x: currentOrigin.x - originShift.x,
y: currentOrigin.y - originShift.y
};
}
html,
body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
When the program runs the mouse is clicked creating a projectile from the center of the screen moving with every frame in the direction it was fired (mouse position on click).
When N+1 projectiles have fired all projectiles on-screen move to the new clicked location instead of continuing their path.
I am can not figure out why the current projectiles change direction when the New projectile's velocity should have no effect on prior projectiles.
index.html
<canvas></canvas>
<script src="./guns.js"></script>
<script src="./indexh.js"></script>
<script src="./runh.js"></script>
runh.html
const projectilesArray = [];
let frameCount = 0;
function animate() {
animationID = requestAnimationFrame(animate);
c.fillStyle = "rgba(0, 0, 0, 1)";
c.fillRect(0, 0, canvas.width, canvas.height);
projectilesArray.forEach((Projectile, pIndex) => {
Projectile.update();
console.log(Projectile)
if (
Projectile.x + Projectile.radius < 0 ||
Projectile.x - Projectile.radius > canvas.width ||
Projectile.y + Projectile.radius < 0 ||
Projectile.y - Projectile.radius > canvas.height
) {
setTimeout(() => {
projectilesArray.splice(pIndex, 1);
}, 0);
}
});
frameCount++;
if (frameCount > 150) {
}
}
var fire = 1;
let fireRate = 1;
const mouse = {
x: 0,
y: 0,
click: true,
};
canvas.addEventListener('mousedown', (event) => {
if (fire % fireRate == 0) {
if (mouse.click == true) {
mouse.x = event.x;
mouse.y = event.y;
const angle = Math.atan2(mouse.y - (canvas.height / 2), mouse.x - (canvas.width / 2));
const fireY = Math.sin(angle);
const fireX = Math.cos(angle);
//score -= 0;
//scoreL.innerHTML = score;
var weapon = new Projectile(cannon);
weapon.velocity.x = fireX * 9;
weapon.velocity.y = fireY * 9;
projectilesArray.push(weapon);
//var gun = object.constructor()
}
}
});
animate();
indexh.js
const canvas = document.querySelector("canvas");
const c = canvas.getContext("2d");
canvas.width = innerWidth;
canvas.height = innerHeight;
class Projectile {
constructor(config) {
this.color = config.color || "rgb(60, 179, 113)";
this.radius = config.radius || 1;
this.speed = config.speed || 5;
this.rounds = config.rounds || 2;
this.x = config.x || canvas.width / 2;
this.y = config.y || canvas.height /2;
this.velocity = config.velocity;
}
draw() {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
c.fillStyle = this.color;
c.fill();
}
update() {
this.draw();
this.x = this.x + this.velocity.x * this.speed;
this.y = this.y + this.velocity.y * this.speed;
}
}
gums.js
let pistol = {
color : "rgb(255, 0, 0)",
radius : 10,
speed : 1,
rounds : 1,
velocity : {
x: 1,
y: 1
}
}
let cannon = {
color : "rgb(0, 0, 255)",
radius : 30,
speed : .5,
rounds : 1,
velocity : {
x: 1,
y: 1
}
}
Thanks
The issue is that you have this cannon object used as a configuration object:
let cannon = {
color : "rgb(0, 0, 255)",
radius : 30,
speed : .5,
rounds : 1,
velocity : {
x: 1,
y: 1
}
}
And in your Projectile constructor you assign this.velocity = config.velocity; You are assigning this.velocity to be the same object instance for all Projectiles. To fix it, try copying the object, like:
this.velocity = {...config.velocity};
That will make a copy of the object rather than sharing the same object instance.
Also note that you have a bug that you didn't ask about in this line:
projectilesArray.splice(pIndex, 1);
If two timeouts are queued in the same loop, the array will shift when the first timeout fires, and the second timeout will be operating on the wrong index.
I have a bunch of sprites i want to render side by side.
let sprites = [sprite1, sprite2, sprite3];
sprites.forEach((_, i) => {
_.position.set(i * _.width, 0);
});
I want to move these sprites along the x axis controlled by tileX variable.
sprites.forEach((_, i) => {
_.position.set(tileX + i * _.width, 0);
});
Tricky part is when a sprite reaches the left or right edge of the screen I want to move it to the opposite edge so it is rendered again.
Assuming you don't want the sprites to pop on the left and right then you need to mod the position with the (displayAreaWidth + spriteWidth) and subtract that amount if the sprite is off the right side
It's not 100% clear what you're trying to do but if you just use x % displayWidth then sprites will pop in on the left. They need to start -width off the left.
sprites.forEach((_, i) => {
const scrollWidth = ctx.canvas.width + _.width;
let x = (tileX + i * _.width) % scrollWidth;
if (x < 0) x += scrollWidth;
if (x > ctx.canvas.width) x -= scrollWidth;
_.position.set(x, 0);
});
const ctx = document.querySelector('canvas').getContext('2d');
const sprites = [...Array(16).keys()].map(i => {
return {
id: String.fromCharCode(i + 65),
color: `hsl(${i / 26 * 360}, 100%, 50%)`,
width: 20,
height: 20,
position: {
x: 0,
y: 0,
set(x, y) {
this.x = x;
this.y = y;
},
},
};
});
requestAnimationFrame(animate);
function animate(time) {
time *= 0.001;
update(time);
render();
requestAnimationFrame(animate);
}
function update(time) {
tileX = Math.sin(time) * 100;
sprites.forEach((_, i) => {
const scrollWidth = ctx.canvas.width + _.width;
let x = (tileX + i * _.width) % scrollWidth;
if (x < 0) x += scrollWidth;
if (x > ctx.canvas.width) x -= scrollWidth;
_.position.set(x, 0);
});
}
function render() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.font = '28px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (const sprite of sprites) {
ctx.fillStyle = sprite.color;
ctx.fillRect(sprite.position.x, sprite.position.y, sprite.width, sprite.height);
ctx.fillStyle = 'black';
ctx.fillText(
sprite.id,
sprite.position.x + sprite.width / 2,
sprite.position.y + sprite.height / 2);
}
}
canvas { border: 1px solid black; }
<canvas></canvas>
First you need to find the size of the canvas or the screen, something like canvas.clientWidth. Then use the operator % (module) to find the position. As you said it could reach the left side of the screen, I assume this number can be negative. In that case, you need to add to the screen width.
var screenWidth = canvas.clientWidth;
var positionX = (tileX + i * _.width) % screenWidth;
if (positionX < 0) positionX += screenWidth;
I'm currently learning about the html5 canvas, and I was making a program that rotates a triangle to face wherever the mouse is. it partially works but skips half of it, and i was wondering if there was a better way to do it and also what is wrong with my current code
thanks!
const ctx = document.getElementById("canvas").getContext("2d")
ctx.canvas.style.backgroundColor = "#303030"
ctx.canvas.width = 500
ctx.canvas.height = 500
const w = ctx.canvas.width
const h = ctx.canvas.height
let x = 0;
let y = 0;
let degrees = 0;
triAngle = 60
ctx.canvas.addEventListener("mousemove", mouseMove, false)
function mouseMove(evt) {
x = evt.clientX
y = evt.clientY
let diffX = x - w / 2;
let diffY = y - h / 2;
console.log(diffX, diffY)
degrees = Math.floor(Math.atan(diffY / diffX) * 57.2958);
//Math.atan(diffY/ diffX)
console.log(degrees)
}
function draw() {
debugger ;
ctx.clearRect(0, 0, w, h)
ctx.fillStyle = "#fff";
ctx.save()
ctx.translate(w / 2, h / 2)
ctx.rotate(degree(degrees + triAngle / 2))
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(0, 100)
ctx.rotate(degree(triAngle))
ctx.lineTo(0, 100)
ctx.closePath()
ctx.fill()
ctx.restore()
requestAnimationFrame(draw)
}
function degree(input) {
return Math.PI / 180 * input
}
draw()
https://jsfiddle.net/tus5nxpb/
Math.atan2
The reason that Math.atan skips half the directions is because of the sign of the fraction. The circle has 4 quadrants, the lines from {x: 0, y: 0} to {x: 1, y: 1}, {x: -1, y: 1}, {x: -1, y: -1} and {x: 1, y: -1} result in only two values (1, and -1) if you divide y by x eg 1/1 === 1, 1/-1 === -1, -1/1 === -1, and -1/-1 === 1 thus there is no way to know which one of the 2 quadrants each value 1 and -1 is in.
You can use Math.atan2 to get the angle from a point to another point in radians. In the range -Math.PI to Math.PI (-180deg to 180deg)
BTW there is no need to convert radians to deg as all math functions in JavaScript use radians
requestAnimationFrame(mainLoop);
const ctx = canvas.getContext("2d")
canvas.height = canvas.width = 300;
canvas.style.backgroundColor = "#303030";
const mouse = {x: 0, y: 0};
canvas.addEventListener("mousemove", e => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
const shape = {
color: "lime",
x: 150,
y: 150,
size: 50,
path: [1, 0, -0.5, 0.7, -0.5, -0.7],
};
function draw(shape) {
var i = 0;
const s = shape.size, p = shape.path;
ctx.fillStyle = shape.color;
const rot = Math.atan2(mouse.y - shape.y, mouse.x - shape.x);
const xa = Math.cos(rot);
const ya = Math.sin(rot);
ctx.setTransform(xa, ya, -ya, xa, shape.x, shape.y);
ctx.beginPath();
while (i < p.length) { ctx.lineTo(p[i++] * s, p[i++] * s) }
ctx.fill();
}
function mainLoop() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // set default transform
ctx.clearRect(0, 0, canvas.width, canvas.height);
draw(shape);
requestAnimationFrame(mainLoop);
}
body { margin: 0px; }
canvas { position: absolute; top: 0px; left: 0px; }
<canvas id="canvas"></canvas>
I am trying to move a triangle in the direction which the triangle is rotated by. When I press the key to move it, it doesn't move, but when I rotate it, its center of rotation shifts because of the key I pressed to move it previously.
I tried checking the formulas to determine the direction to move the triangle, but those seemed correct, and the translation point to rotate it is not moving based on those formulas.
Expected results: On click of the up arrow key, triangle moves in rotation angle direction.
Actual results: On click of up arrow key, triangle doesn't move in the direction, but if I click up arrow key, then left or right arrow key to rotate, triangle rotates away from center of rotation.
Here's my code:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let ship_width = 20;
let ship_height = 20;
let angle = 0;
let ship_velocity_change = {
x: 0,
y: 0
}
//initial center coordinates of triangle
let ship_center = {
x: 450,
y: 300
};
let ship_points = [
//coordinates for vertices of triangle
{
x: ship_center.x - ship_width / 2,
y: ship_center.y +
ship_height / 2
},
{
x: ship_center.x + ship_width / 2,
y: ship_center.y +
ship_height / 2
},
{
x: ship_center.x,
y: ship_center.y - ship_height / 2
}
];
function drawRect(x, y, width, height, color) {
ctx.rect(x, y, width, height);
ctx.fillStyle = color;
ctx.fill();
}
//vertices for triangle as parameters
function drawTriangle(bottom_left, bottom_right, top, color) {
ctx.beginPath();
ctx.moveTo(top.x, top.y);
ctx.lineTo(bottom_left.x, bottom_left.y);
ctx.lineTo(bottom_right.x, bottom_right.y);
ctx.lineTo(top.x, top.y);
ctx.strokeStyle = color;
ctx.stroke();
}
//rotate triangle by an angle in degrees
function rotate(angle) {
ctx.translate(ship_center.x, ship_center.y);
ctx.rotate(Math.PI / 180 * angle);
ctx.translate(-ship_center.x, -ship_center.y);
drawTriangle(ship_points[2], ship_points[1], ship_points[0],
"white");
}
document.addEventListener('keydown', function(event) {
//rate of degree change per 10 milliseconds
if (event.keyCode === 37) {
angle = -2;
} else if (event.keyCode === 38) {
//move triangle by direction of angle
ship_center.x += Math.cos(Math.PI / 180 * angle) * 5;
ship_center.y += Math.sin(Math.PI / 180 * angle) * 5;
} else if (event.keyCode === 39) {
angle = 2;
}
});
document.addEventListener('keyup', function(event) {
if (event.keyCode === 37 || event.keyCode === 39) {
angle = 0;
}
});
function game() {
drawRect(0, 0, 900, 600, "black");
rotate(angle);
}
let gameLoop = setInterval(game, 10);
<canvas id="canvas" width="900" height="600"></canvas>
Nice attempt! This is one of those things that takes some experimentation to get right.
A few suggestions:
Encapsulate all relevant data and functions for the ship in one object. This keeps things organized and easy to understand and will simplify your logic and reduce bugs significantly. This makes entity state and rendering functions much easier to manage.
Avoid calling ship movement functions directly from the key event callback. Doing so can result in jerky, inconsistent behavior when the keyboard re-triggers. This callback should simply flip key flags, then let the event loop take care of calling the relevant functions based on those flags.
Use requestAnimationFrame instead of setInterval for most animations.
Something like angle = 2; is too rigid (this may have been temporary). We need to use += here and introduce a velocity variable (and, ideally, acceleration too) for realistic rotation.
Math.PI / 180 * angle for Math functions isn't strictly necessary. We can use angle directly in radians. If your game depends on setting angles with degrees, then you can add a conversion function.
Use variables for acceleration and velocity if you want the movement to feel realistic. In the sketch below, I've added a few of these, but it's not a definitive approach and is intended to be tweaked to taste.
Here's a simple example of all of this in action. There is much room for improvement, but it should be a decent starter.
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
canvas.width = 400;
canvas.height = 200;
const kbd = {
ArrowLeft: false,
ArrowUp: false,
ArrowRight: false
};
const ship = {
angle: 0,
color: "white",
x: canvas.width / 2,
y: canvas.height / 2,
width: 10,
height: 15,
drag: 0.9,
accSpeed: 0.04,
rotSpeed: 0.012,
rotv: 0,
ax: 0,
ay: 0,
vx: 0,
vy: 0,
rotateLeft() {
this.rotv -= this.rotSpeed;
},
rotateRight() {
this.rotv += this.rotSpeed;
},
accelerate() {
this.ax += this.accSpeed;
this.ay += this.accSpeed;
},
move() {
this.angle += this.rotv;
this.rotv *= this.drag;
this.vx += this.ax;
this.vy += this.ay;
this.ax *= this.drag;
this.ay *= this.drag;
this.vx *= this.drag;
this.vy *= this.drag;
this.x += Math.cos(this.angle) * this.vx;
this.y += Math.sin(this.angle) * this.vy;
},
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.beginPath();
ctx.moveTo(this.height, 0);
ctx.lineTo(-this.height, this.width);
ctx.lineTo(-this.height, -this.width);
ctx.closePath();
ctx.strokeStyle = this.color;
ctx.stroke();
ctx.restore();
}
};
document.addEventListener("keydown", event => {
if (event.code in kbd) {
event.preventDefault();
kbd[event.code] = true;
}
});
document.addEventListener("keyup", event => {
if (event.code in kbd) {
event.preventDefault();
kbd[event.code] = false;
}
});
(function update() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const shipActions = {
ArrowLeft: "rotateLeft",
ArrowUp: "accelerate",
ArrowRight: "rotateRight",
};
for (const key in shipActions) {
if (kbd[key]) {
ship[shipActions[key]]();
}
}
ship.move();
ship.draw(ctx);
requestAnimationFrame(update);
})();