用普通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度。实际结果:法线三角形不旋转。

盲人67

旋转形状。

也许比您要的要多,但是您在代码中犯了一些常见错误,这会对最终代码产生负面影响,并从编写游戏中获得一些乐趣

定义形状。

当地空间

当您具有旋转,移动以及可能的缩放(缩放)形状时,最好以其自身的原点(局部空间)为中心定义其形状(如注释中所指出),以便将其转换为在画布上显示(世界空间),如果您在世界坐标系中创建对象,则不需要将其移至局部空间然后再移回的复杂性。

预先定义路径

无需每次渲染时都创建飞船路径,而是使用Path2D定义形状。这样可以避免通过将计算移至启动位置来创建路径的一些计算开销。

方向

canvas变换的自然方向(正向)沿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>

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章