2

First of all

I searched for it for a long time, and I have already seen many questions including the two:

How to draw Arc between two points on the Canvas?

How to draw a curved line between 2 points on canvas?

Although they seem like the same question, I'm very sure they are not the same. In the first question, the center of the circle is known, and in the second, it draws a Bezier curve not an arc.

Description

Now we have two points A and B and the curve radius given, how to draw the arc as the image shows?

image

Since Path.arcTo's required arguments are RectF, startAngle and sweepAngle, there seems hardly a easy way.

My current solution

I now have my solution, I'll show that in the answer below.

Since the solution is so complex, I wonder if there is a easier way to solve it?

E_net4
  • 27,810
  • 13
  • 101
  • 139

2 Answers2

1

Probably there is no easier way. All what can do would be to refine your solution by geometrical approach. Since the center of circle is always on the perpendicular bisector of the chord, it's not required to solve so generalized equations.

By the way, it's not clear how you defined Clockwise/Counter-clockwise. Arc's winding direction should be determined independently of node-placements (=A, B's coordinates).

As is shown in the figure below, on the straight path from A to B, the center O is to be placed righthandside(CW) or lefthandside(CCW). That's all.

Path.ArcTo with radius and end-points

And, some more aspects to be altered:

  1. It's better to calculate startAngle by atan2(). Because acos() has singularity at some points.
  2. It's also possible to calculate sweepAngle with asin().

After all the code can be slightly simplified as follows.

@Throws(Exception::class)
private fun Path.arcFromTo2(
    x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
    clockwise: Boolean = true
) {

    val d = PointF((x2 - x1) * 0.5F, (y2 - y1) * 0.5F)
    val a = d.length()
    if (a > r) throw Exception()

    val side = if (clockwise) 1 else -1

    val oc = sqrt(r * r - a * a)
    val ox = (x1 + x2) * 0.5F - side * oc * d.y / a
    val oy = (y1 + y2) * 0.5F + side * oc * d.x / a

    val startAngle = atan2(y1 - oy, x1 - ox) * 180F / Math.PI.toFloat()
    val sweepAngle = side * 2.0F * asin(a / r) * 180F / Math.PI.toFloat()

    arcTo(
        ox - r, oy - r, ox + r, oy + r,
        startAngle, sweepAngle,
        false
    )
}
ardget
  • 2,561
  • 1
  • 5
  • 4
0

1. find the center of the circle

This can be solved by a binary quadratic equation, as the image shows:

center

Though there are other solutions, anyway, now the position of circle center is known.

2. calculate start angle and sweep angle

According to the circle center, RectF is easy to know. Now calculate startAngle and sweepAngle.

calculate angle

Via geometric methods, we can calculate the startAngle and sweepAngle:

    val startAngle = acos((x1 - x0) / r) / Math.PI.toFloat() * 180
    val endAngle = acos((x2 - x0) / r) / Math.PI.toFloat() * 180
    val sweepAngle = endAngle - startAngle

In this case, x1 is the x-coordinary of point A, x2 if the x-coordinary of point B, and r is the curve radius of the arc. (There are possible results, the other is [-startAngle, startAngle - endAngle]. Choose one according to actual situation.)

Thus, we get all the required arguments for Path.arcTo method and we can draw the arc now.

3. kotlin code

the entire code of the help function:

    /**
     * Append the arc which is starting at ([x1], [y1]), ending at ([x2], [y2])
     * and with the curve radius [r] to the path.
     * The Boolean value [clockwise] shows whether the process drawing the arc
     * is clockwise.
     */
    @Throws(Exception::class)
    private fun Path.arcFromTo(
        x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
        clockwise: Boolean = true
    ) {
        val c = centerPos(x1, y1, x2, y2, r, clockwise) // circle centers
        // RectF borders
        val left = c.x - r
        val top = c.y - r
        val right = c.x + r
        val bottom = c.y + r
        val startAngle = acos((x1 - c.x) / r) / Math.PI.toFloat() * 180
        val endAngle = acos((x2 - c.x) / r) / Math.PI.toFloat() * 180
        arcTo(
            left, top, right, bottom,
            if (clockwise) startAngle else -startAngle,
            if (clockwise) endAngle - startAngle else startAngle - endAngle,
            false
        )
    }

    // use similar triangles to calculate circle center
    @Throws(Exception::class)
    private fun centerPos(
        x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
        clockwise: Boolean
    ): Point {
        val ab = ((x1 - x2).p2 + (y1 - y2).p2).sqrt
        if (ab > r * 2) throw Exception("No circle fits the condition.")
        val a = ab / 2
        val oc = (r.p2 - a.p2).sqrt
        val dx = (oc * (y2 - y1) / ab).absoluteValue.toInt()
        val dy = (oc * (x2 - x1) / ab).absoluteValue.toInt()
        val cx = ((x1 + x2) / 2).toInt()
        val cy = ((y1 + y2) / 2).toInt()
        return if (x1 >= x2 && y1 >= y2 || x1 <= x2 && y1 <= y2)
            if (clockwise) Point(cx + dx, cy - dy) else Point(cx - dx, cy + dy)
        else
            if (clockwise) Point(cx - dx, cy - dy) else Point(cx + dx, cy + dy)
    }