首页 > 解决方案 > 用普通的javascript旋转三角形

问题描述

我创建了一个旋转函数来旋转我画的三角形,函数参数是我想要旋转形状的度数。旋转功能仍然不会旋转。

我尝试调用该函数在旋转函数中以不同的线绘制三角形。

 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>

预期结果:向左旋转 10 度的三角形。实际结果:没有任何旋转的正常三角形。

标签: javascripthtml5-canvas

解决方案


旋转一个形状。

可能超出了您的要求,但您在代码中犯了一些常见错误,这些错误将对最终代码产生负面影响,并从编写游戏中获得一些乐趣

定义形状。

局部空间

当您有一个旋转、移动和缩放(缩放)形状时,最好定义以它自己的原点(局部空间)为中心的形状(如评论中指出的那样),以便将其转换为出现在画布上(世界如果您在世界坐标中创建对象,则不需要将其移动到本地空间并返回的复杂性。

预定义路径

而不是每次渲染时都创建船只路径,而是使用Path2D来定义形状。这避免了通过将计算移动到启动来创建路径的一些计算开销。

方向

画布变换的自然方向(向前)沿 X 轴。当构建在世界空间中移动的对象时,最好让前点沿同一轴。您将船沿 y 轴指向负方向。

ctx.closePath

认为这ctx.closePath类似于. 这是一个非常常见的错误ctx.beginPathclosePath与它无关,beginPath更像是lineTo并创建一条从最后一个路径点到上一个路径点的附加线moveTo

例子

代码将船定义为 2D 路径,其前端指向 x 轴。

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

代码复杂度

您正在努力编写过于复杂的代码。随着游戏的发展,这种复杂性将开始变得越来越难进行更改和管理错误。始终努力使其尽可能简单。

变换对象

有很多方法可以转换要渲染的对象。最常见的方法是你已经完成了。然而,这种方法需要许多 GPU 状态更改(CPU 和 GPU 之间的数据交换)。状态变化可能非常缓慢(尤其是在低端设备上)

下一段代码标记状态更改

 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

最糟糕的状态变化ctx.restore高度依赖于保存的状态以及在保存和恢复之间对状态所做的更改。如果您需要性能代码,则应不惜一切代价避免使用保存和恢复。

两种状态渲染

下一个示例将以尽可能少的状态更改次数和使用 2D API 呈现转换内容的最快方式呈现 2D 形状。但是,它确实会保持原样,因此您必须在后续渲染中意识到这一点。根据需要完全定义每个状态比使用保存和恢复更有效。

注意我添加了规模,因为您可能需要一些时间。

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

画船然后只需要这条线

strokeShape(shipShape, {x:450, y:300}, rotate, 1, "white");

演示

把它们放在一起,我们得到以下结果。

  • requestAnimationFrame用来做动画(从不使用setInterval
  • Path2D从一组点创建路径的通用函数
  • 将船定义为对象以保持数据井井有条

更新

注意到你关于运动的第二个问题。当问题得到回答时,我虽然只是稍微扩展了这个演示,以提供一些关于如何移动船和其他一些与游戏相关的东西的提示。点击开始向上推,左转右转。

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>


推荐阅读