I am working on making a lightweight and simple 2d engine in HTML Canvas with JavaScript, and I am working on inheritance. For example, I have two structures, an entity class and a container class. (Inspired by PixiJS)
They both have draw function that takes four inputs, a reference to my graphics class (basic sprite renderer), rotation offset, x position, and y position. The container pretty much allows groups of entities to move together. How would I go about rotating them around the container's pivot?
Here's my code.
//Container Class
class container {
//List of objects (children)
objects = [];
//Position and rotation
x = 0;
y = 0;
rot = 0;
//Layer
layer = 0;
//Add a child to the container
addChild = function(child) {
//Add reference to array
this.objects.push(child);
//Sort by layer
this.objects = this.objects.sort((a, b) => { if (a.layer > b.layer) { return 1 } return -1})
}
//Draw
draw = function(gfx, rot, x, y) {
//Loop through all children
for(let i=0; i<this.objects.length; i++) {
let ent = this.objects[i]
//Call draw function from child and offset rotation, x, and y, by current x and y.
//Basically, the x and y from the input of the function offsets the container, and then we
//offset the children by the container's x and y.
//Allows for endless inheritance and offsets
ent.draw(gfx, this.rot + rot, this.x + x, this.y + y);
}
}
}
//Entity class
class entity {
//Sprite
sprite = null;
//Position and rotation
x = 0;
y = 0;
rot = 0;
//Layer
layer = 0;
//Create new sprite on creation (don't worry about this)
constructor() {
this.sprite = new sprite();
}
//Draw function
draw = function(gfx, rot, x, y) {
//Draws the sprite at this entities position offsetted by the input position and rotation
gfx.drawspr(this.sprite, this.x + x, this.y + y, this.rot + rot);
}
}
Keep in mind that the "scene" is also just a container that starts drawing at position (0,0) and rotation 0.
EDIT: DrawSpr..?
First, there is the sprite class. The entity contains the sprite.
//Sprite class
class sprite {
//Image
img = null;
width = 0;
height = 0;
//Loads image
load = async function(src) {
//New image object
this.img = new Image();
//Sets source to src
this.img.src = src
//Waits for image to load
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve("timeout");
}, 10000);
this.img.onload = function() {
resolve();
}
});
//Sets the width to the image
this.img.onload = null;
this.width = this.img.width;
this.height = this.img.height;
}
}
Here is the drawspr function in question.
//Draws sprite. (spr - sprite object, x - x position on screen to render, y - y position on screen to render
//rot - rotation to render the sprite
drawspr = function(spr, x, y, rot) {
//Save current 2d context transform
this.ctx.save();
if(rot!=0) {
//Rotate the screen to draw the image if its rotation is bigger than 0
this.ctx.rotate(rot*Math.PI/180);
}
//Draw the image at the x and y, and reset 2d context transform
this.ctx.drawImage(spr.img, x, y, spr.width, spr.height)
this.ctx.restore();
}
You will need to get farmiliar with matrix math to get a performat solution using the 2D API transformations.
You need to define a matrix for each level (object, world, player, gun, bullet)
The top level is the world or view matrix. It defines what part of the game world you are looking at. The scale, position, and rotation.
Each object then has its own transform (local matrix) that is used to position, rotate and scale it. The object is defined by its local coordinates, centered on 0,0 and pointing along its x axis.
If the object has parts that will be rotated and or scaled relative to itself then you need a matrix for that.
Each matrix is relative to the matrix above it.
To draw an object you multiply the world matrix by the local (object) matrix. To draw the sub object you multiply the world matrix by the local matrix by the sub object matrix.
The 2D API has the functionality to do all the matrix multiplication for you. You need only ensure that the current transform is the one you want. This is done using ctx.save()
and ctx.restore()
For example lets consider a world (view) containing tanks. Each tank has a position and rotation. On that tank is are two independent guns mounted on a turret, that move and rotate with the tank, but can be independently moved and rotated.
We define the world (view) matrix as
const viewMat = [1,0,0,1,0,0];
The tank matrix can be created using the function below
function Matrix(x, y, scale = 1, rotate = 0) {
const xAx = Math.cos(rotate) * scale;
const xAy = Math.sin(rotate) * scale;
return [xAx, xAy, -xAy, xAx, x, y]; /* Y Axis is 90 deg to x axis */
}
To create the matrix
const tankMat = Matrix(tank.pos.x, tank.pos.y, 1, tank.rotate);
The turret matrix is relative to the tank and set as
const turretMat = Matrix(tank.turret.pos.x, tank.turret.pos.y, 1, tank.turret.rotate);
Note ctx
is the 2D API in following code.
Note It is far more performant to use ctx.transform and ctx.setTransform creating (and reusing) the matrix array (via Math.cos
and Math.sin
) than using a combination of ctx.translate, ctx.rotate, ctx.scale as much of the complexity is eliminated as we assume that the y axis is at 90 degree to the x axis.
ctx.setTransform(...viewMat)
Note ctx.setTransform
sets (replaces) current transformctx.save()
ctx.transform(...tankMat)
Note ctx.transform
multiplies current transformctx.drawImage(tankImg, -tankImg.width * 0.5, -tankImg.height * 0.5, tankImg.width, tankImg.height);
ctx.save()
ctx.transform(...turretMat )
ctx.drawImage(gunImg, -10, -10, gunImg.width, gunImg.height); ctx.drawImage(gunImg, -10, 10, gunImg.width, gunImg.height);
ctx.restore()
ctx.restore()
requestAnimationFrame(gameLoop);
const ctx = canvas.getContext("2d");
/* Helper functions */
const V2 = (x = 0, y = 0) => ({x, y});
const Mat = (pos, scale = 1, rotate = 0) => {
const xAx = Math.cos(rotate) * scale;
const xAy = Math.sin(rotate) * scale;
return [xAx, xAy, -xAy, xAx, pos.x, pos.y];
};
const setMat = (mat, pos, scale = 1, rotate = 0) => {
const xAx = Math.cos(rotate) * scale;
const xAy = Math.sin(rotate) * scale;
mat[0] = xAx;
mat[1] = xAy;
mat[2] = -xAy;
mat[3] = xAx;
mat[4] = pos.x;
mat[5] = pos.y;
return mat;
};
const worldMat = Mat(V2());
const turret = { // relative to tank
pos: V2(-20, 0),
scale: 1,
rot: 0,
mat: Mat(V2(-20,0)),
gunPos: 0, /* move the guns in and out */
update() {
this.rot += 0.01;
this.gunPos = Math.sin(performance.now() * 0.01) * 20;
setMat(this.mat, this.pos, this.scale, this.rot); // update matrix
},
draw() {
ctx.save();
ctx.fillStyle = "#4A5";
ctx.strokeStyle = "#232";
ctx.lineWidth = 1;
ctx.transform(...this.mat);
/* draw left right guns */
ctx.fillRect(20 + this.gunPos, -30, 90, 20);
ctx.fillRect(20 + this.gunPos, 10, 90, 20);
/* draw turret */
ctx.fillStyle = "#8E9";
ctx.fillRect(-40, -40, 80, 80);
ctx.strokeRect(-40, -40, 80, 80);
ctx.restore();
}
};
const tank = { // relative to world
pos: V2(200, 50),
scale: 1,
rot: 0,
mat: Mat(V2(200,200)),
turret,
update() {
this.rot += 0.01;
this.pos.x += this.mat[0]; // move along x axis
this.pos.y += this.mat[1];
setMat(this.mat, this.pos, this.scale, this.rot); // update matrix
this.turret.update();
},
draw() {
ctx.save();
ctx.fillStyle = "#6C7";
ctx.strokeStyle = "#121";
ctx.lineWidth = 2;
ctx.transform(...this.mat);
/* draw tank body */
ctx.fillRect(-60, -50, 180, 100);
ctx.strokeRect(-60, -50, 180, 100);
this.turret.draw();
ctx.restore();
}
};
function gameLoop() {
ctx.setTransform(1,0,0,1,0,0); /* default transform */
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
tank.update();
ctx.setTransform(worldMat);
tank.draw();
requestAnimationFrame(gameLoop);
}
<canvas id="canvas" width="400" height="400"></canvas>
There is much more to learn in regards to matrices. They are also used in the 3D world in the same way.
Often you need to reverse the transform. IE locate where the mouse is on the transformed object. Or where a bullet hits your transformed tank Example answer.
It is best to become very familiar with matrices and how they are used in computer graphics. Remember that matrices only represent a set of multiplications and additions, there are often shortcuts you can leverage to get improved performance.