
[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]