[This is part of the Android Diary.] For my little piece of demo-ware, I wanted to draw curved lines between the circles representing entries in a geotagged feed. Android has a function for drawing arcs, but I had to do a little trigonometry to work out the arguments. This is by way of sharing the answer with any other Androiders who want to draw curved lines, and, well, I kind of enjoyed the math and who knows, maybe someone else will too.
The Android function is like this:
public void drawArc(RectF oval, float startAngle, float sweepAngle,
boolean useCenter, Paint paint)
So it’ll draw part of the oval contained in the rectangle you give it; a circle if the rectangle is a square.
Suppose you’re starting with two points,
let’s call them e1
and e2
.
What we want to do is draw part of a circle that goes through those two
points. Here’s a plausible method declaration.
public static void connect(PointF e1, PointF e2, Paint paint, Canvas canvas) {
Here are the two points in their lonely splendor.
We’ll use one fixed parameter: how much of the circle’s arc to use. I used
10% or 36º to keep the arcs nice and smooth. Let’s call that a1
:
float a1Degrees = 36.0f;
double a1 = Math.toRadians(a1Degrees);
Given all that, we’re going to need to figure out the circle’s center and radius:
So let’s have a point c
representing the circle’s center.
We’ll draw a line between e1
and e2
, then connect
its midpoint m
, as well as e1
and e2
,
to c
. Of course, the line from the center to m
is
perpendicular to the line connecting e1
and e2
.
Now we have a couple of right triangles whose base is half the length of
the line from e1
to e2
. Let’s compute that and call
it l1
:
// l1 is half the length of the line from e1 to e2
double dx = e2.x - e1.x, dy = e2.y - e1.y;
double l = Math.sqrt((dx * dx) + (dy * dy));
double l1 = l / 2.0;
Since we know the angle at the pointy end of the right triangles, it’s easy
to compute the the circle’s radius r
and also h
, the length of the line
from m
to c
.
// h is length of the line from the middle of the connecting line to the
// center of the circle.
double h = l1 / (Math.tan(a1 / 2.0));
// r is the radius of the circle
double r = l1 / (Math.sin(a1 / 2.0));
Now let’s draw a horizontal line through e2
and a vertical one
through e1
and look at one of the angles, which
we’ll call a2
.
It’s easy to compute a2
.
// a2 is the angle at which L intersects the x axis
double a2 = Math.atan2(dy, dx);
A glance shows that a2
is part of another
right triangle too. Let’s name its other corner
a3
.
a3
is easy to compute, and is also the angle beween the x-axis
and the line from c
to m
.
// a3 is the angle at which H intersects the x axis
double a3 = (Math.PI / 2.0) - a2;
Now we’re pretty well done; we can compute cx
and
cy
, the deltas from m
to
c
.
// m is the midpoint of the line from e1 to e2
double mX = (e1.x + e2.x) / 2.0;
double mY = (e1.y + e2.y) / 2.0;
// c is the the center of the circle
double cY = mY + (h * Math.sin(a3));
double cX = mX - (h * Math.cos(a3));
All that’s left is the housekeeping to work out the sweep angle:
// rect is the square RectF that bounds the "oval"
RectF oval =
new RectF((float) (cX - r), (float) (cY - r), (float) (cX + r), (float) (cY + r));
// a4 is the starting sweep angle
double rawA4 = Math.atan2(e1.y - cY, e1.x - cX);
float a4 = (float) Math.toDegrees(rawA4);
canvas.drawArc(oval, a4, a1Degrees, false, paint);
}
Afterthoughts ·
When I was writing this up I noticed that you don’t really need to compute
the value h
, you can work it all out in terms of
r
. But then it turns out that you have to be really
clear when you’re talking about e1
and when
e2
, and it matters that on the Android, as in most GUI
libraries, the y-axis points down. Using the h
line makes those problems go away, and the computation cost seems
invisible anyhow, this redraws an immense number of times while you’re
zooming and panning without perceptible delay.
Comment feed for ongoing:
From: Gavin (Jan 02 2009, at 23:00)
Hi Tim,
Thanks for the writeup. Once I needed a curved line but didn't have the time to implement it last year when I was playing with Android.
http://www.flickr.com/photos/chihiro/2306440593/
Another need: add text labels onto those curved edges.
[link]
From: Pat Patterson (Jan 03 2009, at 00:00)
Nice! Brought memories flooding back from when I used to work on this - http://www.ces-ltd.co.uk/products/shapenest/ - heavy duty geometry there - approximating curves with straight lines to arbitrary accuracy, computing the area of profiles comprising any number of lines and arcs.
Graphics programming was SO rewarding - mostly, you could just SEE when you'd got it right :-)
[link]
From: Rui Carmo (Jan 03 2009, at 01:49)
While reading this, I wondered two things:
1. How many more people still enjoy doing this kind of math in these days of math illiteracy
2. If your app could be used to do crop circles :)
I can already envision aliens/nuts/gardeners going about their business with an Android handset...
[link]
From: Daniel (Jan 03 2009, at 05:28)
Happy New Year, Tim!
If your intention is to draw a smooth path between a sequence of points then why don't you just draw a Bezier curve? That's the popular choice since about the late 1980s.
From browsing the online docs: Building an Android.graphics.Path looks promising, then calling quadTo (or cubicTo, or even arcTo), and then passing it into android.graphics.Canvas.drawPath().
Daniel
[link]
From: John Cowan (Jan 03 2009, at 10:24)
OS/2 fixed the wrong-way-up Y axis, but that idea died with it. I suppose the inverted Y derives from the fact that we (well, most of us) read top to bottom, so it makes sense for text.
[link]
From: Christian (Jan 03 2009, at 11:39)
It looks like http://www.tbray.org/ongoing/ongoing.atom doesn't load inline pictures correctly. I get an error symbol instead of pictures in NetNewsWire.
(I actually wanted to post that some time ago)
[link]
From: Tim (Jan 03 2009, at 13:10)
Daniel: I didn't discover the Bezier-curve methods hidden down in Path until I was already working on the arc. Note that they take 1 (quad) or 2 (cubic) interpolation points, so you're going to have to do some geometry anyhow. And the canvas.drawArc curves are nice and graceful.
[link]
From: other daniel (Jan 03 2009, at 17:48)
Very cool. Lost me about half way through, but still very cool :-)
Other Daniel.
[link]
From: Luke Jones (Jan 03 2009, at 19:49)
I'm curious what is the tool you used to draw the diagrams on this posting?
[link]
From: Tim (Jan 04 2009, at 20:51)
Luke: Omnigraffle; and I don't particularly recommend it for the purpose.
[link]