简体   繁体   中英

Rotate triangle in plain javascript

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.

Rotating a shape.

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

Defining the shape.

Local Space

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.

Pre-define the path

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.

Orientation

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

Example

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;
})();

Code complexity

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.

Transforming an object

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.

Two state render

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");

Demo

Putting it all together we get the following.

  • Using requestAnimationFrame to do the animation (Never use setInterval )
  • A generic function to create a path Path2D from a set of points
  • Defining the ship as an object to keep the data organised

UPDATE

Noticed 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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM