旋转图像和像素碰撞检测

Rotating Images & Pixel Collision Detection

我在这个 plunker.

里有这个游戏

当剑不旋转时,一切正常(您可以通过取消注释行 221 并注释掉 222-223 来检查)。当它们像上面的 plunker 一样旋转时,碰撞效果不佳。

我想那是因为 "getImageData" 记住了旧图像,但我认为一遍又一遍地重新计算是一件很昂贵的事情。

是否有更好的方法来旋转我的图像并使其正常工作?还是我必须重新计算他​​们的像素图?

罪魁祸首代码:

for (var i = 0; i < monsters.length; i++) {
    var monster = monsters[i];
    if (monster.ready) {
        if (imageCompletelyOutsideCanvas(monster, monster.monsterImage)) {
            monster.remove = true;
        }
        //else {
        //ctx.drawImage(monster.monsterImage, monster.x, monster.y);
        drawRotatedImage(monster.monsterImage, monster.x, monster.y, monster);
        monster.rotateCounter += 0.05;
        //}
    }
}

几何解

通过更快的几何解决方案来做到这一点。

最简单的解决方案是线段与圆相交算法。

线段.

一条线的起点和终点可以用多种方式描述。在这种情况下,我们将使用开始和结束坐标。

var line = {
    x1 : ?,
    y1 : ?,
    x2 : ?,
    y2 : ?,
}

圆形

圆是由它的位置和半径描述的

var circle = {
   x : ?,
   y : ?,
   r : ?,
}

圆线段相交

下面介绍我是如何测试圆线段碰撞的。我不知道是否有更好的方法(很可能有),但这对我很有帮助并且可靠,但需要注意的是线段必须有长度,圆必须有面积。如果您不能保证这一点,那么您必须在代码中添加检查以确保您不会被零除。

因此,为了测试一条线是否截取圆,我们首先找出线上最近的点有多远(注意线是无限长的,而线段有长度、起点和终点)

// a quick convertion of vars to make it easier to read.
var x1 = line.x1;
var y1 = line.y1;
var x2 = line.x2;
var y2 = line.y2;

var cx = circle.x;
var cy = circle.y;
var r = circle.r;

测试结果,如果发生碰撞,则为真。

var result; // the result of the test

将直线转换为向量。

var vx = x2 - x1;  // convert line to vector
var vy = y2 - y1;
var d2 = (vx * vx + vy * vy);  // get the length squared

获取直线上近点到圆的单位距离。单位距离是从 0 到 1(含)的数字,表示沿点向量的距离。如果该值小于 0 则该点在向量之前,如果大于 1 则该点超过末尾。

这个我是凭记忆知道的,忘记概念了。它是线向量与从线段起点到圆心的向量除以线向量长度平方的点积。

// dot product of two vectors is v1.x * v2.x + v1.y * v2.y over v1 length squared 
u =  ((cx - x1) * vx + (cy - y1) * vy) / d2;

现在使用单位位置,通过将线向量乘以单位距离加上线段起始位置,得到直线上最接近圆的点的实际坐标。

 // get the closest point
var  xx = x1 + vx * u;
var  yy = y1 + vy * u;

现在我们在直线上有一个点,我们使用毕达哥拉斯平方根计算与圆的距离。

// get the distance from the circle center
var d =  Math.hypot(xx - cx, yy - cy);    

现在,如果直线(不是线段)与圆相交,则距离将等于或小于圆半径。否则没有拦截。

if(d > r){ //is the distance greater than the radius
    result = false;  // no intercept
} else { // else we need some more calculations

要确定线段是否截取了圆,我们需要找到该线穿过圆周上的两个点。我们有半径和圆与直线的距离。由于与直线的距离始终成直角,因此我们有一个直角三角形,其中 hypot 是半径,一侧是找到的距离。

算出三角形缺失的长度。 UPDATE 在 "update" 下的答案底部查看代码的改进版本,它使用单位长度而不是归一化线向量。

// ld for line distance is the square root of the hyp subtract side squared
var ld = Math.sqrt(r * r - d * d);

现在将该距离添加到我们在直线 xx 上找到的点,yy 通过将线向量除以来对线向量进行归一化(使线向量成为一个单位长)它的长度,然后乘以上面找到的距离

var len = Math.sqrt(d2); // get the line vector length
var nx = (vx / len) * ld;      
var ny = (vy / len) * ld;      

有些人可能会看到我可以使用单位长度并跳过一些计算。是的,但我可能会因为重写演示而烦恼,所以将保持原样

现在通过将新向量加减到离圆最近的直线上的点来得到截点

ix1 = xx + nx; // the point furthest alone the line 
iy1 = xx + ny;
ix2 = xx - nx; // the point in the other direction
iy2 = xx - ny;

现在我们有了这两个点,我们可以计算出它们是否在线段中,但计算它们在原始线向量上的单位距离,使用点积除以平方距离。

    var u1 =  ((ix1 - x1) * vx + (iy1 - y1) * vy) / d2;
    var u2 =  ((ix2 - x1) * vx + (iy1 - y1) * vy) / d2; 

现在做一些简单的测试,看看这些点的单位postion是否在线段上

    if(u1 < 0){  // is the forward intercept befor the line segment start
        result = false;  // no intercept            
    }else
    if(u2 > 1){ // is the rear intercept after the line end
        result = false;  // no intercept            
    } else {
        // though the line segment may not have intercepted the circle
        // circumference if we have got to here it must meet the conditions
        // of touching some part of the circle.
        result = true;
    }
}

演示

一如既往,这里有一个展示逻辑的演示。圆圈以鼠标为中心。如果圆圈接触到一些测试线,它们会变红。它还将显示圆周确实穿过线的点。如果位于线段内,则点为红色;如果位于线段外,则点为绿色。这些点可用于添加效果或其他什么

我今天很懒,所以这是直接从我的图书馆里拿出来的。 注意,有机会我会post改进数学。

更新

我改进了算法,用单位长度计算圆的周长相交,减少了很多代码。我也把它添加到演示中了。

从直线距离小于圆半径的点开始

            // get the unit distance to the intercepts
            var ld = Math.sqrt(r * r - d * d) / Math.sqrt(d2);

            // get that points unit distance along the line
            var u1 =  u + ld; 
            var u2 =  u - ld; 
            if(u1 < 0){  // is the forward intercept befor the line
                result = false;  // no intercept
            }else
            if(u2 > 1){  // is the backward intercept past the end of the line
                result = false;  // no intercept
            }else{
                result = true;
            }
        }

var demo = function(){
    
    // the function described in the answer with extra stuff for the demo
    // at the bottom you will find the function being used to test circle intercepts.
    
    
    /** GeomDependancies.js begin **/
        
    // for speeding up calculations.
    // usage may vary from descriptions. See function for any special usage notes
    var data = {
        x:0,   // coordinate
        y:0,
        x1:0,   // 2nd coordinate if needed
        y1:0,
        u:0,   // unit length
        i:0,   // index
        d:0,   // distance
        d2:0,  // distance squared
        l:0,   // length
        nx:0,  // normal vector
        ny:0,
        result:false, // boolean result
    }
    // make sure hypot is suported
    if(typeof Math.hypot !== "function"){
        Math.hypot = function(x, y){ return Math.sqrt(x * x + y * y);};
    }
    /** GeomDependancies.js end **/
    
    /** LineSegCircleIntercept.js begin **/
    // use data properties
    // result  // intercept bool for intercept
    // x, y    // forward intercept point on line ** 
    // x1, y1  // backward intercept point on line
    // u       // unit distance of intercept mid point
    // d2      // line seg length squared
    // d       // distance of closest point on line from circle
    // i       // bit 0 on for forward intercept on segment 
    //         // bit 1 on for backward intercept
    // ** x = null id intercept points dont exist
    var lineSegCircleIntercept = function(ret, x1, y1, x2, y2, cx, cy, r){
    var vx, vy, u, u1, u2, d, ld, len, xx, yy;
        vx = x2 - x1;  // convert line to vector
        vy = y2 - y1;
        ret.d2 = (vx * vx + vy * vy);
        
        // get the unit distance of the near point on the line
        ret.u = u =  ((cx - x1) * vx + (cy - y1) * vy) / ret.d2;
        xx = x1 + vx * u; // get the closest point
        yy = y1 + vy * u;
        
        // get the distance from the circle center
        ret.d = d =  Math.hypot(xx - cx, yy - cy);    
        if(d <= r){ // line is inside circle
            // get the distance to the two intercept points
            ld = Math.sqrt(r * r - d * d) / Math.sqrt(ret.d2);

            // get that points unit distance along the line
            u1 =  u + ld; 
            if(u1 < 0){  // is the forward intercept befor the line
                ret.result = false;  // no intercept
                return ret;
            }
            u2 =  u - ld; 
            if(u2 > 1){  // is the backward intercept past the end of the line
                ret.result = false;  // no intercept
                return ret;
            }
            ret.i = 0;
            if(u1 <= 1){
                ret.i += 1;
                // get the forward point line intercepts the circle
                ret.x = x1 + vx * u1;  
                ret.y = y1 + vy * u1;
            }else{
                ret.x = x2;
                ret.y = y2;
                
            }
            if(u2 >= 0){
                ret.x1 = x1 + vx * u2;  
                ret.y1 = y1 + vy * u2;
                ret.i += 2;
            }else{
                ret.x1 = x1;
                ret.y1 = y1;
            }
            
            // tough the points of intercept may not be on the line seg
            // the closest point to the must be on the line segment
            ret.result = true;
            return ret;
            
        }
        ret.x = null; // flag that no intercept found at all;
        ret.result = false;  // no intercept
        return ret;
            
    }
    /** LineSegCircleIntercept.js end **/
    

    // mouse and canvas functions for this demo.

    /** fullScreenCanvas.js begin **/
    var canvas = (function(){
        var canvas = document.getElementById("canv");
        if(canvas !== null){
            document.body.removeChild(canvas);
        }
        // creates a blank image with 2d context
        canvas = document.createElement("canvas"); 
        canvas.id = "canv";    
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight; 
        canvas.style.position = "absolute";
        canvas.style.top = "0px";
        canvas.style.left = "0px";
        canvas.style.zIndex = 1000;
        canvas.ctx = canvas.getContext("2d"); 
        document.body.appendChild(canvas);
        return canvas;
    })();
    var ctx = canvas.ctx;
    
    /** fullScreenCanvas.js end **/
    /** MouseFull.js begin **/
    
    var canvasMouseCallBack = undefined;  // if needed
    var mouse = (function(){
        var mouse = {
            x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
            interfaceId : 0, buttonLastRaw : 0,  buttonRaw : 0,
            over : false,  // mouse is over the element
            bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
            getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
            startMouse:undefined,
        };
        function mouseMove(e) {
            var t = e.type, m = mouse;
            m.x = e.offsetX; m.y = e.offsetY;
            if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
            m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
            if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
            } else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
            } else if (t === "mouseover") { m.over = true;
            } else if (t === "mousewheel") { m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") { m.w = -e.detail;}
            if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
            e.preventDefault();
        }
        function startMouse(element){
            if(element === undefined){
                element = document;
            }
            "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
            function(n){element.addEventListener(n, mouseMove);});
            element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
        }
        mouse.mouseStart = startMouse;
        return mouse;
    })();
    if(typeof canvas === "undefined"){
        mouse.mouseStart(canvas);
    }else{
        mouse.mouseStart();
    }
    /** MouseFull.js end **/
    
    // helper function
    function drawCircle(ctx,x,y,r,col,col1,lWidth){
        if(col1){
            ctx.lineWidth = lWidth;
            ctx.strokeStyle = col1;
        }
        if(col){
            ctx.fillStyle = col;
        }
        
        ctx.beginPath();
        ctx.arc( x, y, r, 0, Math.PI*2);
        if(col){
            ctx.fill();
        }
        if(col1){
            ctx.stroke();
        }
    }
    
    // helper function
    function drawLine(ctx,x1,y1,x2,y2,col,lWidth){
        ctx.lineWidth = lWidth;
        ctx.strokeStyle = col;
        ctx.beginPath();
        ctx.moveTo(x1,y1);
        ctx.lineTo(x2,y2);
        ctx.stroke();
    }
    var h = canvas.height;
    var w = canvas.width;
    var unit = Math.ceil(Math.sqrt(Math.hypot(w, h)) / 32);
    const U80 = unit * 80;
    const U60 = unit * 60;
    const U40 = unit * 40;
    const U10 = unit * 10;
    var lines = [
        {x1 : U80, y1 : U80, x2 : w /2, y2 : h - U80},
        {x1 : w - U80, y1 : U80, x2 : w /2, y2 : h - U80},
        {x1 : w / 2 - U10, y1 : h / 2 - U40, x2 : w /2, y2 : h/2 + U10 * 2},
        {x1 : w / 2 + U10, y1 : h / 2 - U40, x2 : w /2, y2 : h/2 + U10 * 2},
    ];
    
    function update(){
        var i, l;
        ctx.clearRect(0, 0, w, h);
        
        drawCircle(ctx, mouse.x, mouse.y, U60, undefined, "black", unit * 3);
        drawCircle(ctx, mouse.x, mouse.y, U60, undefined, "yellow", unit * 2);
        for(i = 0; i < lines.length; i ++){
            l = lines[i]
            drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "black" , unit * 3)
            drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "yellow" , unit * 2)
            
            // test the lineSegment circle
            data = lineSegCircleIntercept(data,  l.x1, l.y1, l.x2, l.y2, mouse.x, mouse.y, U60);
            // if there is a result display the result
            if(data.result){
                drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "red" , unit * 2)
                if((data.i & 1) === 1){
                    drawCircle(ctx, data.x, data.y, unit * 4, "white", "red", unit );
                }else{
                    drawCircle(ctx, data.x, data.y, unit * 2, "white", "green", unit );
                }
                if((data.i & 2) === 2){
                    drawCircle(ctx, data.x1, data.y1, unit * 4, "white", "red", unit );
                }else{
                    drawCircle(ctx, data.x1, data.y1, unit * 2, "white", "green", unit );
                }
            }
        }
        requestAnimationFrame(update);
    }
    
    update();
}
// resize if needed by just starting again
window.addEventListener("resize",demo);

// start the demo
demo();

...这里是如何在移动和旋转剑时找到剑刃线

首先找到原始剑刃的顶点并将它们保存在一个数组中。

var pts=[{x:28,y:42},{x:69,y:3},{x:83,y:1},{x:83,y:19},{x:42,y:57}];

剑旋转时,每个剑刃顶点都会围绕旋转点旋转。在你的例子中,旋转点是图像的中心。

  • 灰色矩形是图像的矩形边框
  • 蓝点是一个剑尖(在刀尖)
  • 绿点在图像中心(==旋转点)
  • 绿线是图像中心到顶点的距离
  • 蓝色圆圈是刀尖旋转 360 度时所遵循的路径
  • 绿线会根据图像的旋转改变角度。

你可以像这样计算任意旋转角度的叶尖位置:

// [cx,cy] = the image centerpoint (== the rotation point)
// [vx,vy] = the coordinate position of the blade tip
// Calculate the distance and the angle between the 2 points 
var dx=vx-cx;
var dy=vy-cy;
var distance=Math.sqrt(dx*dx+dy*dy);
var originalAngle=Math.atan2(dy,dx);

// rotationAngle = the angle the image has been rotated expressed in radians
var rotatedX = cx + distance * Math.cos(originalAngle + rotationAngle);
var rotatedY = cy + distance * Math.sin(originalAngle + rotationAngle);

这是在移动和旋转时跟踪叶片顶点的示例代码和演示:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
  var BB=canvas.getBoundingClientRect();
  offsetX=BB.left;
  offsetY=BB.top;        
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }

var isDown=false;
var startX,startY;

var sword={
    img:null,
    rx:0,
    ry:0,
    angle:0,
    pts:[{x:28,y:42},{x:69,y:3},{x:83,y:1},{x:83,y:19},{x:42,y:57}],
    // precalculated properties -- for efficiency
    radii:[],
    angles:[],
    halfWidth:0,
    halfHeight:0,
    //
    initImg:function(img){
        var PI2=Math.PI*2;
        this.img=img;
        this.halfWidth=img.width/2;
        this.halfHeight=img.height/2;
        for(var i=0;i<this.pts.length;i++){
            var dx=this.halfWidth-this.pts[i].x;
            var dy=this.halfHeight-this.pts[i].y;
            this.radii[i]=Math.sqrt(dx*dx+dy*dy);
            this.angles[i]=((Math.atan2(dy,dx)+PI2)%PI2)-Math.PI;
        }
    },
    // draw sword with translation & rotation
    draw:function(){
        var img=this.img;
        var rx=this.rx;
        var ry=this.ry;
        var angle=this.angle;
        ctx.translate(rx,ry);
        ctx.rotate(angle);
        ctx.drawImage(img,-this.halfWidth,-this.halfHeight);
        ctx.rotate(-angle);
        ctx.translate(-rx,-ry);
    },
    // recalc this.pts after translation & rotation
    calcTrxPts:function(){
        var trxPts=[];
        for(var i=0;i<this.pts.length;i++){
            var r=this.radii[i];
            var ptangle=this.angles[i]+this.angle;
            trxPts[i]={
                x:this.rx+r*Math.cos(ptangle),
                y:this.ry+r*Math.sin(ptangle)
            };
        }
        return(trxPts);
    },
}

// load image & initialize sword object & draw scene
var img=new Image();
img.onload=function(){
    // set initial sword properties
    sword.initImg(img);
    sword.rx=150;
    sword.ry=75;
    sword.angle=0; //(Math.PI/8);

    // draw scene
    drawAll();

    // listen for mouse events
    $("#canvas").mousedown(function(e){handleMouseDown(e);});
    $("#canvas").mousemove(function(e){handleMouseMove(e);});
    $("#canvas").mouseup(function(e){handleMouseUpOut(e);});
    $("#canvas").mouseout(function(e){handleMouseUpOut(e);});

    // listen for mousewheel events
    $("#canvas").on('DOMMouseScroll mousewheel',function(e){
        e.preventDefault();
        e.stopPropagation();
        var e=e || window.event; // old IE support
        sign=((e.originalEvent.wheelDelta||e.originalEvent.detail*-1)>0)?1:-1;
        sword.angle+=Math.PI/45*sign;
        drawAll();
    });
}
img.src = "data:image/png;base64,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";


/////////////////////
// helper functions
/////////////////////

function drawAll(){
    ctx.clearRect(0,0,cw,ch);
    sword.draw();
    drawHitArea();
}

function drawHitArea(){
    // lines
    var trxPts=sword.calcTrxPts();
    ctx.beginPath();
    ctx.moveTo(trxPts[0].x,trxPts[0].y);
    for(var i=1;i<trxPts.length;i++){
        ctx.lineTo(trxPts[i].x,trxPts[i].y);
    }
    ctx.closePath();
    ctx.strokeStyle='red';
    ctx.stroke();
    // dots
    for(var i=0;i<trxPts.length;i++){
        ctx.beginPath();
        ctx.arc(trxPts[i].x,trxPts[i].y,3,0,Math.PI*2);
        ctx.closePath();
        ctx.fillStyle='blue';
        ctx.fill();
    }
}

function getClosestPointOnLineSegment(line,x,y) {
    //
    lerp=function(a,b,x){ return(a+x*(b-a)); };
    var dx=line.x1-line.x0;
    var dy=line.y1-line.y0;
    var t=((x-line.x0)*dx+(y-line.y0)*dy)/(dx*dx+dy*dy);
    var lineX=lerp(line.x0, line.x1, t);
    var lineY=lerp(line.y0, line.y1, t);
    return({x:lineX,y:lineY,isOnSegment:(t>=0 && t<=1)});
};


function handleMouseDown(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  
  startX=parseInt(e.clientX-offsetX);
  startY=parseInt(e.clientY-offsetY);

  // Put your mousedown stuff here
  isDown=true;
}

function handleMouseUpOut(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  // clear the isDragging flag
  isDown=false;
}

function handleMouseMove(e){
  if(!isDown){return;}
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();

  // calc distance moved since last drag
  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);
  var dx=mouseX-startX;
  var dy=mouseY-startY;
  startX=mouseX;
  startY=mouseY;

  // drag the sword to new position
  sword.rx+=dx;
  sword.ry+=dy;
  drawAll();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h6>Drag sword and<br>Rotate sword using mousewheel inside canvas<br>Red "collision" lines follow swords translation & rotation.</h6>
<h5></h5>
<canvas id="canvas" width=300 height=300></canvas>