Reputation: 317
I created a rotation function to rotate a triangle I drew, the function parameter is the amount of degrees I want to rotate the shape by. The rotation function still wont rotate.
I tried calling the function to draw the triangle in different lines within the rotation function.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
//central coordinates, width and height of triangle
let ship_center = {x: 450, y: 300};
let ship_width = 20;
let ship_height = 20;
//coordinates of the points of the vertices for my triangle
let ship_points = [
//top vertex
{x: 450 - ship_width/2, y: ship_center.y + ship_height/2},
//bottom right vertex
{x: 450 + ship_width/2, y: ship_center.y + ship_height/2},
//bottom left vertex
{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();
}
//bottom left vertices, bottom right verices and top vertices
//as parameters in drawTriangle
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();
ctx.closePath();
}
//rotation function
function rotate(angle){
ctx.save();
//draw the triangle
drawTriangle(ship_points[2], ship_points[1], ship_points[0],
"white");
ctx.translate(ship_center.x, ship_center.y);
ctx.rotate(Math.PI/180 * angle);
ctx.restore();
}
function game(){
drawRect(0, 0, 900, 600, "black");
//rotate 10 degrees
rotate(10);
}
let gameLoop = setInterval(game, 10);
<canvas id="canvas">
no support
</canvas>
Expected result: Triangle rotated by 10 degrees to the left. Actual result: Normal triangle without any rotation.
Upvotes: 1
Views: 1999
Reputation: 54039
maybe more than you asked for but you are making some common mistakes in your code that will negatively effect the final code and take some of the fun out of writing a game
When you have a rotating, moving, and maybe scaling (zoom) shape it is best to define its shape centered on its own origin (local space) (as pointed out in the comments) so that transforming it to appear on the canvas (world space) does not require a the complexity of moving it to local space and back if you create the object in world coordinates.
Rather than create the ships path each time you render it use a Path2D to define the shape. This avoid some of the computational overhead of creating the path by moving the computations to startup.
The natural orientation (forward) of the canvas transform is along the X axis. When building object that move in world space its best to have the front point along the same axis. You have the ship point up along the y axis in the negative direction.
ctx.closePath
It is a very common mistake to think that ctx.closePath
is akin to ctx.beginPath
. closePath
has nothing to do with beginPath
it is rather more like lineTo
and creates an additional line from the last path point to the previous moveTo
The code defines the ship as a 2D path with the front pointing along the x axis.
const shipShape = (() => {
const width = 20, height = 20;
const ship = new Path2D()
ship.lineTo(-width / 2, -height / 2);
ship.lineTo(width / 2, 0);
ship.lineTo(-width / 2, height / 2);
ship.closePath();
return ship;
})();
You are working your way to overly complicated code. As your game grows this complexity will start to make harder and hard to make changes and manage bugs. Always strive to keep it as simple as possible.
There are many ways to transform an object to be rendered. The most common way is as you have done it. However this method requires many GPU state changes (the exchange of data between the CPU and GPU). State change can be very slow (especially on low end devices)
The next snippet of your code marks the state changes
ctx.save();
// the ctx.stroke in the following function is a state change
drawTriangle(ship_points[2], ship_points[1], ship_points[0],"white");
// All 3 of the following calls are state changes.
ctx.translate(ship_center.x, ship_center.y);
ctx.rotate(Math.PI/180 * angle);
ctx.restore(); // Depending on the saved state this can be very time expensive
The worst state change is ctx.restore
which is highly dependent on the saved state and the changes made to the state between save and restore. You should avoid using save and restore at all costs if you need performance code.
The next example will render a 2D shape in the minimum number of state changes possible and the fastest way to render transformed content using the 2D API. It does however leave the state as is so you must be aware of this in subsequent renders. It is more efficient to fully define each state as needed rather than use save and restore.
Note I added scale as you may need is some time.
function strokeShape(shape, pos, rotate = 0, scale = 1, style = ctx.strokeStyle) {
const xAx = Math.cos(rotate) * scale;
const xAy = Math.sin(rotate) * scale;
ctx.setTransform(xAx, xAy, -xAy, xAx, pos.x, pos.y); // set rotate scale and position
// in one state change
ctx.strokeStyle = style;
ctx.stroke(shape);
}
To draw the ship then just needs the line
strokeShape(shipShape, {x:450, y:300}, rotate, 1, "white");
Putting it all together we get the following.
requestAnimationFrame
to do the animation (Never use setInterval
)Path2D
from a set of pointsNoticed your second question regarding movement. As the question was answered I though I would just extend this demo a little to give some hints to how to move the ship and some other game related stuff. Click to start Up to thrust, Left right turns.
var started = false;
canvas.addEventListener("click",() => {
if (!started) {
requestAnimationFrame(updateFrame);
started = true;
}
})
const ctx = canvas.getContext("2d", {aplha:false});// aplha:false to avoid unneeded composition
ctx.font = "16px arial";
ctx.textAlign = "center";
fillBackground();
ctx.fillStyle = "white"
ctx.fillText("Click to Start", ctx.canvas.width / 2, ctx.canvas.height / 2);
document.addEventListener("keydown", keyboardEvent);
document.addEventListener("keyup", keyboardEvent);
const keys = {ArrowUp: false, ArrowLeft: false, ArrowRight: false}
function keyboardEvent(event) {
if(keys[event.code] !== undefined) {
event.preventDefault();
keys[event.code] = event.type === "keydown";
}
}
const width = 20, height = 20;
const TURN_RATE = 0.01; // in radians
const MAX_TURN_RATE = 0.1; // in radians
const REACTOR_WINDUP_RATE = 0.01; // in power units per frame
const REACTOR_MAX_POWER = 0.1; // in pixels per frame (frame = 1/60th sec)
const SPACE_QUANTUM_FLUX = 0.015; // drains ship moment per frame
const DEFLUXING_CONVERTER = 0.8; // How dirty the thruster is
const SHIP_HULL = [-width*(1/3), -height/2, width*(2/3), 0, -width*(1/3), height/2,"close"];
const SHIP_PORT = [width*(1/6), -height/8, width*(1/3), 0, width*(1/6), height/8,"close"];
const thrustParticlePool = [];
const thrustParticle = {
get pos() { return {x:0, y:0} },
get vel() { return {x:0, y:0} },
shape: createPath([-0.5,0,0.5,0]),
style: "#FFF",
rotate: 0,
pool: thrustParticlePool,
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
this.vel.x *= 0.996;
this.vel.y *= 0.996;
this.life -= 1;
},
init(x,y,direction, speed) {
const offCenter = Math.random()**2 * (Math.random() < 0.5 ? -1 : 1);
const offCenterA = Math.random()**2 * (Math.random() < 0.5 ? -1 : 1);
speed += speed * offCenterA;
speed **= 2.5;
this.pos.x = x + Math.cos(direction) * width * (2/3) - Math.sin(direction) * height * (1/6) * offCenter;
this.pos.y = y + Math.sin(direction) * width * (2/3) + Math.cos(direction) * height * (1/6) * offCenter;
direction += direction * 0.1 * offCenter;
this.rotate = direction;
this.vel.x = Math.cos(direction) * speed;
this.vel.y = Math.sin(direction) * speed;
this.life = 100;
},
};
const particles = Object.assign([],{
add(type,...args) {
var p;
if(type.pool.length) {
p = type.pool.pop();
} else {
p = Object.assign({}, type);
}
p.init(...args);
this.push(p);
},
updateDraw() {
var i = 0
while(i < this.length) {
const p = this[i];
p.update();
if (p.life <= 0) {
this.splice(i--,1)[0];
if (p.pool) { p.pool.push(p) }
} else {
strokeShape(p.shape, p.pos, p.rotate, 1, p.style);
}
i++;
}
}
});
function createPath(...paths) {
var i, path = new Path2D;
for(const points of paths) {
i = 0;
path.moveTo(points[i++],points[i++])
while (i < points.length -1) { path.lineTo(points[i++],points[i++]) }
points[i] === "close" && path.closePath();
}
return path;
}
const ship = {
shapes: {
normal: createPath(SHIP_HULL, SHIP_PORT),
thrustingA: createPath(SHIP_HULL, SHIP_PORT,
[-width*(1/3), -height/4, -width*(1/3)-height/4,0, -width*(1/3), height/4]
),
thrustingB: createPath(SHIP_HULL, SHIP_PORT,
[-width*(1/3), -height/3.5, -width*(1/3)-height/2.4,0, -width*(1/3), height/3.5]
),
},
shape: null,
rotate: 0, // point left to right along x axis
deltaRotate: 0,
pos: {x : 200, y: 100},
vel: {x : 0, y: 0},
power: 0,
style: "#FFF", // named colours take about 10% longer to set than Hex colours
update() {
if (keys.ArrowUp) {
this.shape = this.shapes.thrustingA === this.shape ? this.shapes.thrustingB : this.shapes.thrustingA;
this.power = this.power < REACTOR_MAX_POWER ? this.power + REACTOR_WINDUP_RATE : REACTOR_MAX_POWER;
if (Math.random() < DEFLUXING_CONVERTER) {
particles.add(
thrustParticle,
this.pos.x, this.pos.y,
this.rotate + Math.PI,
this.power * 8,
);
}
} else {
this.shape = this.shapes.normal;
this.power = 0;
}
var dr = this.deltaRotate;
dr *= 0.95;
dr = keys.ArrowLeft ? dr - TURN_RATE : dr;
dr = keys.ArrowRight ? dr + TURN_RATE : dr;
dr = Math.abs(dr) > MAX_TURN_RATE ? MAX_TURN_RATE * Math.sign(dr) : dr;
this.rotate += (this.deltaRotate = dr);
this.vel.x += Math.cos(this.rotate) * this.power;
this.vel.y += Math.sin(this.rotate) * this.power;
const speed = (this.vel.x * this.vel.x + this.vel.y * this.vel.y)**4;
if (speed > 0.0) {
this.vel.x = this.vel.x * (speed / (speed * (1+SPACE_QUANTUM_FLUX)));
this.vel.y = this.vel.y * (speed / (speed * (1+SPACE_QUANTUM_FLUX)));
}
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
this.pos.x = (this.pos.x + ctx.canvas.width * 2) % ctx.canvas.width;
this.pos.y = (this.pos.y + ctx.canvas.height * 2) % ctx.canvas.height;
},
draw() {
strokeShape(ship.shape, ship.pos, ship.rotate, 1, ship.style);
}
};
function strokeShape(shape, pos, rotate = 0, scale = 1, style = ctx.strokeStyle) {
const xAx = Math.cos(rotate) * scale; // direction and size of the top of a
const xAy = Math.sin(rotate) * scale; // single pixel
ctx.setTransform(xAx, xAy, -xAy, xAx, pos.x, pos.y); // one state change
ctx.strokeStyle = style;
ctx.stroke(shape);
}
function fillBackground() {
ctx.fillStyle = "#000";
ctx.setTransform(1,0,0,1,0,0); //ensure that the GPU Transform state is correct
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function updateFrame(time) {
fillBackground();
ship.update();
particles.updateDraw();
ship.draw();
requestAnimationFrame(updateFrame);
}
<canvas id="canvas" width="400" height="200"></canvas>
Upvotes: 4