Search code examples
mathlanguage-agnostictrigonometry

Distance to circumference of circle from any point and angle within the circle


Technically it's a math problem, but I don't understand how to turn it into code.

Illustration

I have the center C of a circle, its radius r, and points A and B within the circle. I need to find the distance AD, so B is only used for the angle. So this means solving the triangle ACD with angles and sides calculated from 3 points. In other words, I want to find the distance to the circumference of the circle from any point within the circle, with an angle.

Pseudo-code of what I tried:

float dis(float a_x, float a_y, float b_x, float b_y, float c_x, float c_y, float r) {
  float ab_x = a_x - b_x;
  float ab_y = a_y - b_y;

  float ac_x = a_x - c_x;
  float ac_y = a_y - c_y;

  float ab_a = atan(ab_y, ab_x);
  float ac_a = atan(ac_y, ac_x);
  float dc_a = pi - ac_a - ab_a;

  return (sin(dc_a) * r) / sin(ab_a);
}

This produced wrong results probably because of more than one mistake. How does one do it correctly?

I read about it in math exchange, eventually learned that it's a simple triangle, but with programming context I couldn't find questions that answered mine.


Solution

  • You could approach this as follows:

    First, we can translate ("move") the given points such that 𝐶 has coordinates (0, 0). So we subtract 𝐶 from both 𝐴 and 𝐵. This does not influence the solution. So now we only have 𝐴 and 𝐵 as inputs.

    Secondly we can scale the problem such that the radius of the circle is 1. This means we divide both 𝐴 and 𝐵 by the current radius (by |𝐴|).

    The line through 𝐴 and 𝐵 can be written as 𝐴 + 𝑛(𝐵−𝐴) where 𝑛 is variable. Let's call 𝑊=𝐵−𝐴, and let's normalise 𝑊 so it has size 1 (Here we must assume that 𝐴 and 𝐵 are different points). Then we have the following equations that must be satisfied to determine the value of 𝑚:

          𝐷 = 𝐴 + 𝑚𝑊 (𝐷 lies on the line through 𝐴 and 𝐵), and
          |𝐷| = 1 (𝐷 lies on the unit circle)

    To ease calculations we can replace the second equation with:

          |𝐷|² = 1

    Substituting 𝐷, we get:

          |𝐴 + 𝑚𝑊|² = 1
          (𝐴𝑥 + 𝑚𝑊𝑥)² + (𝐴𝑦 + 𝑚𝑊𝑦)² = 1
          𝐴𝑥² + 2𝑚𝐴𝑥𝑊𝑥 + 𝑚²𝑊𝑥² + 𝐴𝑦² + 2𝑚𝐴𝑦𝑊𝑦 + 𝑚²𝑊𝑦² = 1
          𝐴𝑥² + 𝐴𝑦² + 2𝑚(𝐴𝑥𝑊𝑥 + 𝐴𝑦𝑊𝑦) + 𝑚²(𝑊𝑥² + 𝑊𝑦²) = 1
          (𝑊𝑥² + 𝑊𝑦²)𝑚² + 2(𝐴𝑥𝑊𝑥 + 𝐴𝑦𝑊𝑦)𝑚 + 𝐴𝑥² + 𝐴𝑦² − 1 = 0

    Now 𝐴𝑥² + 𝐴𝑦² is |𝐴|², which we already determined to be 1 when we scaled the problem (If we choose 𝑚=0, then 𝐷 = 𝐴 + 𝑚𝑊 becomes 𝐷 = 𝐴, and |𝐷|² = |𝐴|² = 1). So the above simplifies to:

          (𝑊𝑥² + 𝑊𝑦²)𝑚² + 2(𝐴𝑥𝑊𝑥 + 𝐴𝑦𝑊𝑦)𝑚 = 0

    The line 𝐴𝐵 crosses the circle twice (in general), and when 𝑚 is zero we have 𝐴. We want the non-zero 𝑚 (if there is a second crossing), and so we divide the equation by 𝑚:

          (𝑊𝑥² + 𝑊𝑦²)𝑚 + 2(𝐴𝑥𝑊𝑥 + 𝐴𝑦𝑊𝑦) = 0
          𝑚 = −2(𝐴𝑥𝑊𝑥 + 𝐴𝑦𝑊𝑦)/(𝑊𝑥² + 𝑊𝑦²)

    Finally, because we scaled the problem to a unit circle, the found value for 𝑚 needs to be scaled back to the original problem.

    Here is an implementation in JavaScript:

    // Utility functions to work with 2D vectors:
    function sub(u, v) {
        return [u[0] - v[0], u[1] - v[1]];
    }
    
    function mul(u, m) { // Scalar multiplication
        return [m * u[0], m * u[1]];
    }
    
    function size(u) {
        return Math.sqrt(u[0] * u[0] + u[1] * u[1]);
    }
    
    function norm(u) { // Resize vector to size 1
        return mul(u, 1 / size(u));
    }
    
    function solve(a, b, c) {
        // Translate problem to C at (0, 0): this does not influence the result
        a = sub(a, c);
        b = sub(b, c);
        // Scale problem to Unit circle
        const r = size(a);
        a = mul(a, 1/r);
        b = mul(b, 1/r);
        // Define W as the direction vector of the line A-B, with size 1.
        const w = norm(sub(b, a));
        // Solve equation to find m, such that D = A+mW and |D|² is 1, i.e.
        // |A+mW|² = 1, or 
        // (Ax+mWx)²+(Ay+mWy)² = 1, or 
        // (Ax)² + 2mAxWx + m²(Wx)² + (Ay)² + 2mAyWy + m²(Wy)² = 1, or
        // (Ax)²+(Ay)² - 1 + 2m(AxWx+AyWy) + m²((Wx)²+(Wy)²) = 0
        // As (Ax)²+(Ay)² = 1, we can eliminate (Ax)²+(Ay)² - 1.
        // m=0 is a solution. But we want to get the other root. 
        // So divide the equation by m, and solve by m:
        const m = -2 * (a[0] * w[0] + a[1] * w[1]) / (w[0] * w[0] + w[1] * w[1]);
        // Scale the solution back to the original size
        return m * r; // This is a signed number. For distance, take absolute value
    }
    
    // Demo 
    const dist = solve([3, 5], [6, 6], [5, 10]);
    console.log(dist);

    Here is an interactive version where points 𝐴 and 𝐶 are fixed, but 𝐵 corresponds to where the mouse pointer is. You can then see the distance as you move the mouse pointer, and the corresponding line segement.

    // Utility functions to work with 2D vectors:
    function sub(a, b) {
        return [a[0] - b[0], a[1] - b[1]];
    }
    
    function add(a, b) {
        return [a[0] + b[0], a[1] + b[1]];
    }
    
    function mul(a, m) { // Scalar multiplication
        return [m * a[0], m * a[1]];
    }
    
    function size(a) {
        return Math.sqrt(a[0] * a[0] + a[1] * a[1]);
    }
    
    function norm(a) { // Resize vector to size 1
        return mul(a, 1 / size(a));
    }
    
    function solve(a, b, c) {
        // Translate problem to C at (0, 0): this does not influence the result
        a = sub(a, c);
        b = sub(b, c);
        // Scale problem to Unit circle
        const r = size(a);
        a = mul(a, 1/r);
        b = mul(b, 1/r);
        // Define G as the direction vector of the line A-B, with size 1.
        const w = norm(sub(b, a));
        // Solve equation to find m, such that D = A+mW and |D|² is 1, i.e.
        // |A+mW|² = 1, or 
        // (Ax+mWx)²+(Ay+mWy)² = 1, or 
        // (Ax)² + 2mAxWx + m²(Wx)² + (Ay)² + 2mAyWy + m²(Wy)² = 1, or
        // (Ax)²+(Ay)² - 1 + 2m(AxWx+AyWy) + m²((Wx)²+(Wy)²) = 0
        // As (Ax)²+(Ay)² = 1, we can eliminate (Ax)²+(Ay)² - 1.
        // m=0 is a solution. But we want to get the other root. 
        // So divide the equation by m, and solve by m:
        const m = -2 * (a[0] * w[0] + a[1] * w[1]) / (w[0] * w[0] + w[1] * w[1]);
        // Scale the solution back to the original size
        return m * r; // This is a signed number. For distance, take absolute value
    }
    
    // I/O handling and actual call of the solve function. 
    
    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    const output = document.querySelector("span");
    
    function drawCircle(c, a) {
        ctx.beginPath();
        ctx.arc(...c, size(sub(c, a)), 0, 2 * Math.PI);
        ctx.stroke();
    }
    
    function drawLine(a, d) {
        ctx.beginPath();
        ctx.moveTo(...a);
        ctx.lineTo(...d);
        ctx.stroke();
    }
    
    function clear() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }
    
    document.addEventListener("mousemove", refresh);
    
    function refresh(e) {
        // For demo we fix points a and c. b is determined by pointer position
        const c = [200,  90];
        const a = [150,  30];
        clear();
        drawCircle(c, a);
        if (!e) return; // No coordinates for b provided.
        // Convert mouse pointer to coordinates for b (relative to the canvas)
        const b = [e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop];
        const dist = solve(a, b, c); // <<<< EXECUTE THE ALGORITHM >>>>
        output.textContent = Math.abs(dist); // Output the result
        // For drawing the line segment, calculate d using a, b and dist
        const d = add(a, mul(norm(sub(b, a)), dist));
        drawLine(a, d);
    }
    refresh();
    <canvas height="170" width="500"></canvas>
    <div>Size of line segment: <span></span></div>