scripter
scripter

Reputation: 115

How to plot an ellipse on canvas from 2 points on the ellipse, where slope of major axis (rx), and minor axis (ry) length are unknown

This may be more of a mathematics problem, but maybe there is a simple javascript solution that I am missing.

I want to plot an ellipse on html canvas from user input of a center point, radius of the major (longest) axis, and 2 points will fall on the ellipse.

This should potentially create 2 possible ellipse paths, both of which will center around the center point, and cross through the 2 points.

So for example, if the center = [2, 1] major axis radius a = 10, point 1 u = [4, 2] and point 2 v = [5, 6], what is the minor axis radius b and angle of rotation theta?

enter image description here

So far I have tried to implement an equation that I found from https://math.stackexchange.com/questions/3210414/find-the-angle-of-rotation-and-minor-axis-length-of-ellipse-from-major-axis-leng, but it does not return valid values. My javascript code looks like this:


function getEllipseFrom2Points(center, u, v, a) {
  function getSlope(plusOrMinus) {
      return Math.sqrt(((uy * vx - ux * vy) ** 2) / (-ux * uy * (a * (v2x + v2y) - 1) + vx * vy * (a * (u2x + u2y) - 1) - plusOrMinus * (uy * vx - ux * vy) * q) / (u2x * (1 - a * v2y) + v2x * (a * u2y - 1)));
  }
  function getMinorAxis(plusOrMinus) {
      return (u2x + u2y + v2x + v2y - a * (2 * u2x * v2x + 2 * u2y * v2y + 2 * ux * uy * vx * vy + u2y * v2x + u2x * v2y) + plusOrMinus * 2 * (ux * vx + uy * vy) * q);
  }
  var vx = v[0],
      vy = v[1],
      ux = u[0],
      uy = u[1],
      v2x = vx ** 2,
      v2y = vy ** 2,
      u2x = ux ** 2,
      u2y = uy ** 2,
      q = Math.sqrt((1 - a * (u2x + u2y)) * (1 - a * (v2x + v2y))),
      ellipse1 = { rx: a, ry: getMinorAxis(1), origin: center, rotation: getSlope(1) },
      ellipse2 = { rx: a, ry: getMinorAxis(-1), origin: center, rotation: getSlope(-1) };
}

Either the equation that I am following is wrong, or I have implemented it wrong

Upvotes: 0

Views: 386

Answers (1)

scripter
scripter

Reputation: 115

In case anyone is interested, here is my solution to the problem, which isn't really "the" solution. If anyone can solve this I would still be happy to know.

Since I can't solve for both slope of the major axis and length of the minor axis, I just take a guess at slope and then test how close it is, and then refine the result by trying in a smaller and smaller region. Since the final ellipse that gets drawn is actually an estimation constructed from bezier curves, I can get close enough in a reasonable amount of time.

function getEllipseFrom2Points (center, u, v, a) {
    function getSemiMinorAxis([x, y], a, t) {
        // equation for rotated ellipse
        // b = a(ycos(t) - xsin(t)) / sqrt(a^2 - x^2cos^2(t) - 2xysin(t)cos(t) - y^2sin^2(t)) and
        // b = a(xsin(t) - ycos(t)) / sqrt(a^2 - x^2cos^2(t) - 2xysin(t)cos(t) - y^2sin^2(t)) 
        // where a^2 !== (xcos(t) + ysin(t))^2
        // and aycos(t) !== axsin(t)

        if (a ** 2 !== (x * Math.cos(t) + y * Math.sin(t)) ** 2 &&
            a * y * Math.cos(t) !== a * x * Math.sin(t)) {
            var b = [],
                q = (Math.sqrt(a ** 2 - x ** 2 * (Math.cos(t)) ** 2 - 2 * x * y * Math.sin(t) * Math.cos(t) - y ** 2 * (Math.sin(t)) ** 2));
            b[0] = (a * (y * Math.cos(t) - x * Math.sin(t))) / q;
            b[1] = (a * (x * Math.sin(t) - y * Math.cos(t))) / q;
            return b;
        }
    }
    function getAngle_radians(point1, point2){
        return Math.atan2(point2[1] - point1[1], point2[0] - point1[0]);
    }
    function getDistance(point1, point2) {
        return Math.sqrt((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2);
    }
    function rotatePoint(point, center, radians) {
        var x = (point[0] - center[0]) * Math.cos(radians) - (point[1] - center[1]) * Math.sin(radians) + center[0];
        var y = (point[1] - center[1]) * Math.cos(radians) + (point[0] - center[0]) * Math.sin(radians) + center[1];
        return [x, y];
    }
    function measure(ellipseRotation, pointOnEllipse, minorAxisLength) {
        var d = getDistance(point, pointOnEllipse);
        if (d < bestDistanceBetweenPointAndEllipse) {
            bestDistanceBetweenPointAndEllipse = d;
            bestEstimationOfB = minorAxisLength;
            bestEstimationOfR = ellipseRotation;
        }
    }
    function getBestEstimate(min, max) {
        var testIncrement = (max - min) / 10;
        for (let r = min; r < max; r = r + testIncrement) {
            if (radPoint1 < r && radPoint2 < r || radPoint1 > r && radPoint2 > r) {//points both on same side of ellipse
                semiMinorAxis = getSemiMinorAxis(v, a, r);
                if (semiMinorAxis) {
                    for (let t = 0; t < circle; t = t + degree) {
                        ellipsePoint1 = [a * Math.cos(t), semiMinorAxis[0] * Math.sin(t)];
                        ellipsePoint2 = [a * Math.cos(t), semiMinorAxis[1] * Math.sin(t)];
                        point = rotatePoint(u, [0, 0], -r);
                        measure(r, ellipsePoint1, semiMinorAxis[0]);
                        measure(r, ellipsePoint2, semiMinorAxis[1]);
                    }
                }
            }
        }
        count++;
        if (new Date().getTime() - startTime < 200 && count < 10) //refine estimate
            getBestEstimate(bestEstimationOfR - testIncrement, bestEstimationOfR + testIncrement);
    }
    if (center instanceof Array &&
        typeof center[0] === "number" &&
        typeof center[1] === "number" &&
        u instanceof Array &&
        typeof u[0] === "number" &&
        typeof u[1] === "number" &&
        v instanceof Array &&
        typeof v[0] === "number" &&
        typeof v[1] === "number" &&
        typeof a === "number") {

        // translate points
        u = [u[0] - center[0], u[1] - center[1]];
        v = [v[0] - center[0], v[1] - center[1]];

        var bestDistanceBetweenPointAndEllipse = a,
            point,
            semiMinorAxis,
            ellipsePoint1,
            ellipsePoint2,
            bestEstimationOfB,
            bestEstimationOfR,
            radPoint1 = getAngle_radians([0, 0], v),
            radPoint2 = getAngle_radians([0, 0], u),
            circle = 2 * Math.PI,
            degree = circle / 360,
            startTime = new Date().getTime(),
            count = 0;

        getBestEstimate(0, circle);

        var ellipseModel = MakerJs.$(new MakerJs.models.Ellipse(a, bestEstimationOfB))
            .rotate(MakerJs.angle.toDegrees(bestEstimationOfR), [0, 0])
            .move(center)
            .originate([0, 0])
            .$result;

        return ellipseModel;
}

Upvotes: 0

Related Questions