Search code examples
javascripthtml5-canvascollision-detectiongame-developmentcollision

How to check if a line is intersecting with a circle in JavaScript?


I'm creating a library so it's easier for me to create HTML5 canvas games. I'm currently working on the collision detection. This is the code I wrote for line/circle collisions is below. object1 is an object containing the circle's x, y, and radius. object2 is an object containing both points of a line segment.

const point1 = object2.point1;
const point2 = object2.point2;
        
let newPoint1X = point1.x - object1.x;
let newPoint1Y = point1.y - object1.y;
let newPoint2X = point2.x - object1.x;
let newPoint2Y = point2.y - object1.y;

let lineSlope = (newPoint2Y - newPoint1Y) / (newPoint2X - newPoint1X);
let circleSlope;
        
if (lineSlope != 0) circleSlope = lineSlope / -1;
else circleSlope = 65535;
        
let closestX = (newPoint1Y - lineSlope * newPoint1X) / (circleSlope - lineSlope);  
let closestY = closestX * circleSlope;

if ((closestX - newPoint1X) * (closestX - newPoint2X) >= 0 && (closestY - newPoint1Y) * (closestY - newPoint2Y) >= 0) {
    if ((closestX - newPoint1X) * (closestX - newPoint2X) > 0) { 
        if (Math.abs(closestX - newPoint1X) > Math.abs(closestX - newPoint2X)) {
            closestX = newPoint2X;
            closestY = newPoint2Y;
        }
        else {
            closestX = newPoint1X;
            closestY = newPoint1Y;
        }
    }
    else {
        if (Math.abs(closestY - newPoint1Y) > Math.abs(closestY - newPoint2Y)) {
            closestX = newPoint2X;
            closestY = newPoint2Y;
        }
        else {
            closestX = newPoint1X;
            closestY = newPoint1Y;
        }
    }
}

return closestX * closestX + closestY * closestY < object1.radius * object1.radius;

Here is an example of object1 and object2:

let object1 = {
    type: "circle",
    x: 100,
    y: 100,
    radius: 50,
    color: "#90fcff"
}
let object2 = {
    type: "line",
    point1: {
        x: 30,
        y: 20
    },
    point2: {
        x: 360,
        y: 310
    },
    color: "#000000",
    lineWidth: 1
}

I tested this code, and it doesn't detect intersections at the right points. Any help with this?


Solution

  • The given answer can be improved.

    Avoid the square root

    In the example below the function rayInterceptsCircle returns true or false depending on the intercept of a line segment (ray) and circle by using the distance from the line segment to the circle center.

    This is similar to the existing answer however it avoids the need to calculate a expensive square root

    On circle perimeter

    The function rayDist2Circle returns the distance along the line to the point where it intersects the circle, if there is no intercept then the distance is returned as Infinity. It does require up to 2 square roots.

    If you have many circles that you must test the line against this function can find the first circle the line intercepts by finding the minimum distance

    Demo

    Use mouse to move line segment endpoint. If line intercepts circle it is rendered in red to the point of intercept.

    const ctx = canvas.getContext("2d");
    const TAU = Math.PI * 2;
    requestAnimationFrame(renderLoop);
    var W = canvas.width, H = canvas.height;
    
    
    const Point = (x, y) => ({x, y}); 
    const Ray = (p1, p2) => ({p1, p2}); 
    const Circle = (p, radius) => ({x: p.x, y: p.y, radius});
    
    function drawRayLeng(ray, len) {
      ctx.beginPath();
      ctx.lineTo(ray.p1.x, ray.p1.y);
      if (len < Infinity) {
        const dx = ray.p2.x - ray.p1.x;
        const dy = ray.p2.y - ray.p1.y;
        const scale = len / Math.hypot(dx, dy);
        ctx.lineTo(ray.p1.x + dx * scale , ray.p1.y + dy  * scale);
      } else {
        ctx.lineTo(ray.p2.x, ray.p2.y);
      }
      ctx.stroke();
    }
    function drawRay(ray) {
      ctx.beginPath();
      ctx.lineTo(ray.p1.x, ray.p1.y);
      ctx.lineTo(ray.p2.x, ray.p2.y);
      ctx.stroke();
    }
    function drawCircle(circle) {
      ctx.beginPath();
      ctx.arc(circle.x, circle.y, circle.radius, 0, TAU);
      ctx.stroke();
    }
    function rayInterceptsCircle(ray, circle) {    
        const dx = ray.p2.x - ray.p1.x;
        const dy = ray.p2.y - ray.p1.y;
        const u = Math.min(1, Math.max(0, ((circle.x - ray.p1.x) * dx + (circle.y - ray.p1.y) * dy) / (dy * dy + dx * dx)));
        const nx = ray.p1.x + dx * u - circle.x;
        const ny = ray.p1.y + dy * u - circle.y;    
        return nx * nx + ny * ny < circle.radius * circle.radius;
    }
    function rayDist2Circle(ray, circle) {
        const dx = ray.p2.x - ray.p1.x;
        const dy = ray.p2.y - ray.p1.y;
        const vcx = ray.p1.x - circle.x; 
        const vcy = ray.p1.y - circle.y;
        var v =  (vcx * dx +  vcy * dy) * (-2 / Math.hypot(dx, dy));
        const dd = v * v - 4 * (vcx * vcx + vcy * vcy - circle.radius * circle.radius);
        if (dd <= 0) { return Infinity; }
        return  (v - Math.sqrt(dd)) / 2;
    }
    const mouse  = {x : 0, y : 0}
    function mouseEvents(e){
        mouse.x = e.pageX;
        mouse.y = e.pageY;
    }
    document.addEventListener("mousemove", mouseEvents);
    
    const c1 = Circle(Point(150, 120), 60);
    const r1 = Ray(Point(0, 50), Point(300, 50));
    
    
    function renderLoop(time) {
       ctx.clearRect(0, 0, W, H);
       r1.p1.x = c1.x + Math.cos(time / 5000) * 100;
       r1.p1.y = c1.y + Math.sin(time / 5000) * 100;
       r1.p2.x = mouse.x;
       r1.p2.y = mouse.y;
       
       ctx.lineWidth = 0.5;
       drawCircle(c1);
       drawRay(r1);
       ctx.lineWidth = 5;
       if (rayInterceptsCircle(r1, c1)) {
         ctx.strokeStyle = "red";
         drawRayLeng(r1, rayDist2Circle(r1, c1));
       } else {
         drawRay(r1);
       }
       
       ctx.strokeStyle = "black";
       requestAnimationFrame(renderLoop);
    }
    canvas {
      position: absolute;
      top: 0px;
      left: 0px;
    }
    <canvas id="canvas" width="300" height="250"></canvas>