Search code examples
javascriptkineticjstrigonometryellipse

Converting a Circle to Ellipse so it calculates the Distance of a Point from an Ellipse Border


I'm looking for the equation to convert a circle to an ellipse so that I can find the shortest distance from a point to an ellipse border. I have found the equation for the distance between a circle and a point but cant figure out how to convert it to work with a ellipse.

px and py are the points and x and y are the circle origin and ray is the radius

closestCirclePoint: function(px, py, x, y, ray) {
    var tg = (x += ray, y += ray, 0);
    return function(x, y, x0, y0) {
        return Math.sqrt((x -= x0) * x + (y -= y0) * y);
    }(px, py, x, y) > ray
      ? {x: Math.cos(tg = Math.atan2(py - y, px - x)) * ray + x,
         y: Math.sin(tg) * ray + y}
      : {x: px, y: py};
}

Solution

  • [ Addition to answer: How to approximate the nearest point on the ellipse]

    If you are willing to sacrifice perfection for practicality…

    Here is a way to calculate an ellipse point that is “near-ish” to your targeted point.

    enter image description here

    The method:

    • Determine which quadrant of the ellipse your target point is in.
    • Calculate the beginning and ending radian angles of that quadrant.
    • Calculate points along that ellipse quadrant (“walk the ellipse”).
    • For each calculated ellipse point, calc the distance to the target point.
    • Save the ellipse point with the shortest distance to the target.

    Cons:

    • The result is approximate.
    • It's less elegant than the mathematically perfect calculation—uses a brute force method.
    • (but an efficient brute force method).

    Pros:

    • The approximated result is pretty good.
    • Performance is pretty good.
    • The calculations are much simpler.
    • The calculations are (probably) faster than the mathematically perfect calculation.
    • (costs about 20 trig calculations plus some addition/subtraction)
    • If you need greater accuracy, you just change 1 variable
    • (greater accuracy costs more calculations, of course)

    Performance note:

    • You could pre-calculate all the "walking points" on the ellipse for even better performance.

    Here’s the code for this method:

        // calc a point on the ellipse that is "near-ish" the target point
        // uses "brute force"
        function getEllipsePt(targetPtX,targetPtY){
    
            // calculate which ellipse quadrant the targetPt is in
            var q;
            if(targetPtX>cx){
                q=(targetPtY>cy)?0:3;
            }else{
                q=(targetPtY>cy)?1:2;
            }
    
            // calc beginning and ending radian angles to check
            var r1=q*halfPI;
            var r2=(q+1)*halfPI;
            var dr=halfPI/steps;
            var minLengthSquared=200000000;
            var minX,minY;
    
            // walk the ellipse quadrant and find a near-point
            for(var r=r1;r<r2;r+=dr){
    
                // get a point on the ellipse at radian angle == r
                var ellipseX=cx+radiusX*Math.cos(r);
                var ellipseY=cy+radiusY*Math.sin(r);
    
                // calc distance from ellipsePt to targetPt
                var dx=targetPtX-ellipseX;
                var dy=targetPtY-ellipseY;
                var lengthSquared=dx*dx+dy*dy;
    
                // if new length is shortest, save this ellipse point
                if(lengthSquared<minLengthSquared){
                    minX=ellipseX;
                    minY=ellipseY;
                    minLengthSquared=lengthSquared;
                }
            }
    
            return({x:minX,y:minY});
        }
    

    Here is code and a Fiddle: http://jsfiddle.net/m1erickson/UDBkV/

    <!doctype html>
    <html>
    <head>
    <link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
    <script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
    
    <style>
        body{ background-color: ivory; padding:20px; }
        #wrapper{
            position:relative;
            width:300px;
            height:300px;
        }
        #canvas{
            position:absolute; top:0px; left:0px;
            border:1px solid green;
            width:100%;
            height:100%;
        }
        #canvas2{
            position:absolute; top:0px; left:0px;
            border:1px solid red;
            width:100%;
            height:100%;
        }
    </style>
    
    <script>
    $(function(){
    
        // get canvas references
        var canvas=document.getElementById("canvas");
        var ctx=canvas.getContext("2d");
        var canvas2=document.getElementById("canvas2");
        var ctx2=canvas2.getContext("2d");
    
        // calc canvas position on page
        var canvasOffset=$("#canvas").offset();
        var offsetX=canvasOffset.left;
        var offsetY=canvasOffset.top;
    
    
        // define the ellipse
        var cx=150;
        var cy=150;
        var radiusX=50;
        var radiusY=25;
        var halfPI=Math.PI/2;
        var steps=8; // larger == greater accuracy
    
    
        // get mouse position
        // calc a point on the ellipse that is "near-ish"
        // display a line between the mouse and that ellipse point
        function handleMouseMove(e){
          mouseX=parseInt(e.clientX-offsetX);
          mouseY=parseInt(e.clientY-offsetY);
    
          // Put your mousemove stuff here
          var pt=getEllipsePt(mouseX,mouseY);
    
          // testing: draw results
          drawResults(mouseX,mouseY,pt.x,pt.y);
        }
    
    
        // calc a point on the ellipse that is "near-ish" the target point
        // uses "brute force"
        function getEllipsePt(targetPtX,targetPtY){
    
            // calculate which ellipse quadrant the targetPt is in
            var q;
            if(targetPtX>cx){
                q=(targetPtY>cy)?0:3;
            }else{
                q=(targetPtY>cy)?1:2;
            }
    
            // calc beginning and ending radian angles to check
            var r1=q*halfPI;
            var r2=(q+1)*halfPI;
            var dr=halfPI/steps;
            var minLengthSquared=200000000;
            var minX,minY;
    
            // walk the ellipse quadrant and find a near-point
            for(var r=r1;r<r2;r+=dr){
    
                // get a point on the ellipse at radian angle == r
                var ellipseX=cx+radiusX*Math.cos(r);
                var ellipseY=cy+radiusY*Math.sin(r);
    
                // calc distance from ellipsePt to targetPt
                var dx=targetPtX-ellipseX;
                var dy=targetPtY-ellipseY;
                var lengthSquared=dx*dx+dy*dy;
    
                // if new length is shortest, save this ellipse point
                if(lengthSquared<minLengthSquared){
                    minX=ellipseX;
                    minY=ellipseY;
                    minLengthSquared=lengthSquared;
                }
            }
    
            return({x:minX,y:minY});
        }
    
        // listen for mousemoves
        $("#canvas").mousemove(function(e){handleMouseMove(e);});
    
    
    
        // testing: draw the ellipse on the background canvas
        function drawEllipse(){
            ctx2.beginPath()
            ctx2.moveTo(cx+radiusX,cy)
            for(var r=0;r<2*Math.PI;r+=2*Math.PI/60){
                var ellipseX=cx+radiusX*Math.cos(r);
                var ellipseY=cy+radiusY*Math.sin(r);
                ctx2.lineTo(ellipseX,ellipseY)
            }
            ctx2.closePath();
            ctx2.lineWidth=5;
            ctx2.stroke();
        }
    
        // testing: draw line from mouse to ellipse
        function drawResults(mouseX,mouseY,ellipseX,ellipseY){
            ctx.clearRect(0,0,canvas.width,canvas.height);
            ctx.beginPath();
            ctx.moveTo(mouseX,mouseY);
            ctx.lineTo(ellipseX,ellipseY);
            ctx.lineWidth=1;
            ctx.strokeStyle="red";
            ctx.stroke();
        }
    
    
    }); // end $(function(){});
    </script>
    
    </head>
    
    <body>
        <div id="wrapper">
            <canvas id="canvas2" width=300 height=300></canvas>
            <canvas id="canvas" width=300 height=300></canvas>
        </div>
    </body>
    </html>
    

    Original Answer

    Here's how circles and ellipses are related

    For a horizontally aligned ellipse:

    enter image description here

    (xx) / (aa) + (yy) / (bb) == 1;

    where a is the length to the horizontal vertex and where b is the length to the vertical vertex.

    How circles and ellipses relate:

    If a==b, the ellipse is a circle !

    However...!

    Calculating the minimal distance from any point to a point on an ellipse involves much more calculation than with a circle.

    Here's a link to the calculation (click on DistancePointEllipseEllipsoid.cpp):

    http://www.geometrictools.com/SampleMathematics/DistancePointEllipseEllipsoid/DistancePointEllipseEllipsoid.html