4

I am drawing an ellipse

CGRect paperRect = self.bounds;
CGRect strokeRect = CGRectInset(paperRect, 5.0, 5.0);
CGContextAddEllipseInRect(context, strokeRect);
CGContextStrokePath(context);

How can I get points on the ellipse (equidistant, or equiradial), so that I can use CGContextAddArcToPoint to connect two adjacent points at a time ( in a loop to cover all points).

Target is to get an ellipse whose border is made of smaller arcs. Looking for some help.

aVC
  • 2,254
  • 2
  • 24
  • 46

2 Answers2

3

Like a lot of trigonometry, this is easier than it looks.

Start by implementing this for a circle, which is much simpler because the distances and angles remain constant all the way around:

  1. Split the circumference of the circle into however many points as you want spikes on the balloon.

  2. Pick any one of those points to start from.

  3. Compute where it is relative to the center of the balloon, then moveto it.

  4. From each spike-point, compute the next spike-point, and then find the point right in the middle between the two.

  5. Use that midpoint as the center of an arcto. That will look something like CGContextAddArcToPoint(context, midPoint.x, midPoint.y, nextSpikePoint.x, nextSpikePoint.y, radius).

    (radius here is the depth of the valley between two spikes. You could hard-code this, make it user-editable, make it dependent on the balloon size, use the distance from the starting spike-point to the midpoint, or multiply that last one by something.)

    The arc is what puts the curve in between each pair of spikes. I believe the arc is always counter-clockwise, so you can change which way you go around the balloon—taking the points in counter-clockwise order or clockwise order—to change whether you draw a shout balloon or a thought balloon.

  6. Wash, rinse, repeat until you get back to the spike point you started from in step 2.

  7. Close the path, plot the tail as a separate subpath, stroke them both, and then fill them both. (The stroke will leave overlapping lines; the fill with cover those up.)

Step 1 is kind of a non-step: All you really do is decide how many points you want. You could hard-code a number of points to start with, and then make it variable later on, possibly depending on the balloon's circumference.

To derive it from circumference, you'd have a certain distance between spike-points in mind (especially if you want a certain radius, e.g., to use half the distance between points as the radius). You'd divide the circumference by that distance, and round one way or the other (or choose your starting point so that the remainder is covered by the balloon's tail).

Computing the circumference of a circle

This much is well-known:

circ = radius² × π

(or, as more famously and concisely stated, “πr²”.)

Distance between two points

Just ask Pythagoras!

What you do here is treat the two points as corner-points of a right triangle, and compute the hypotenuse of that triangle. I won't repeat the theorem, because there's a handy function for it in the C math library:

#include <tgmath.h>

CGSize size = {
   fabs(nextPoint.x - currentPoint.x),
   fabs(nextPoint.y - currentPoint.y)
};
distance = hypot(size.width, size.height);

(fabs is the absolute-value function. If its argument is negative, it returns the negation of it—the same number, but positive. If its argument is positive, it returns it unchanged.)

Finding the point in the middle of a distance

To find the mid-point, what you do is find both the distance from the current point to the next point (as above), then find the angle of the same.

Imagine a circle (a much smaller one) around that middle-point. Touching it on its circumference are the two spike-points on either side of it: the current point and the next point. Both of those points are at the same distance (radius) and different angles (actually, completely opposing angles, since our midpoint is on a straight line).

So, now move that imaginary circle onto the current point. Now, it's centered on the current point and the midpoint is on its circumference—at the same radius, in the opposite direction (angle).

The way to find where the midpoint is relative to the current point is the same way you find where each of the spike-points is relative to the center of the balloon.

Computing where each point is on the circle

Start from the center of the balloon.

Each of the points is at some angle and some distance from the center of the balloon. Given a circular balloon, they're all at the same distance, which is the radius of the circle.

First, the angle. The angle between each two spike-points is 2π (the number of radians in a complete circle) divided by the number of spikes. Compute that angle once, then multiply it by the index of each spike-point as you go through your loop. i from 0 to the number of points; i+1 is your next point.

Next, the radius. The way to find the radius trigonometrically is to set up the right triangle whose hypotenuse is that distance. Measure the hypotenuse and you've got the distance.

Small problem, though: Last time we computed a hypotenuse, we had a width and height to give it. This time, we don't.

That's what cosine and sine are for!

The cosine and sine functions take an angle (in radians) and give you the width and height (respectively) of that imaginary right triangle. Then you just pass those along to hypot.

One wrinkle: They don't take a radius. They return their results in terms of the unit circle (radius 1). That's easy to overcome: Just multiply the result by the desired radius.

So our point-along-the-circumference-finding code looks like this:

static CGPoint pointFromPolar(CGFloat theta, CGFloat radius) {
    return (CGPoint){
        cos(theta) * radius,
        sin(theta) * radius
    };
}

You'll use the same thing for finding the midpoint between two spike-points: Pass the angle of the imaginary line between them, and half of the distance of that line. Add the resulting numbers to the current point to get the midpoint.

Finding the midpoints: Alternate method

The other way to find the midpoint between two spike-points is to simply add half of the angle-between-points to the angle of the current point (from the balloon center) and then find where that angle at the same radius ends.

That point will actually be along the circumference of the balloon's circle (not on a straight line). It's not terribly important that it be one or the other; use whichever works and is simplest to implement.

And now, how to do all of that for an ellipse

So that's for a regular circle. The tricky part is generalizing it to any ellipse.

(As all squares are rectangles, so are all circles ellipses—and so, too, for the inverses: as not all rectangles are squares, so are not all ellipses circles.)

Now, I could do all the math and write a test app and work out how to do everything above for an ellipse, but what I recommend you do instead is let an affine transform do the work for you.

Note: Do not apply the transform in your context. Apply it to the path.

All you need to do is create an affine transform that scales non-uniformly—that is, more width than height, or vice versa. In Cocoa, you can create an NSAffineTransform and send it scaleXBy:yBy:. In Quartz, use CGAffineTransformMakeScale.

Then, apply this transform to your path. For an NSBezierPath, send it transformUsingAffineTransform: and pass the NSAffineTransform object. For a UIBezierPath, send it applyTransform: and pass the CGAffineTransform structure. For a CGPath, use CGPathCreateCopyByTransformingPath. Plotting the path directly into a context won't work here, since you can't transform the path without also distorting the stroke.

I'll leave it to you to decide whether you should add the tail before or after transforming the path.

Community
  • 1
  • 1
Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • Thanks a lot for the detail. I am going to try it out. – aVC Jun 09 '13 at 15:15
  • This is a remarkable coincidence. I was reading a question on Math SE, saw @aVC comment on the value of pi, checked his profile, noticed a fun sounding ellipse Q, arrived here. I read your awesome answer, thought your photo was familiar, then realized that my adjacent tab (using Chrome browser ;o) is THIS! https://www.quora.com/Stack-Overflow-4/What-does-it-feel-like-to-be-at-the-top-0-1-of-Stack-Overflow I saw it for the 1st time today, while reading *Forbes* online! The circle of confluence (dual foci? sorry, a little ;o) will be complete if `aVC = Fred W.`! – Ellie Kesselman Jun 12 '13 at 09:12
  • 1
    @FeralOink: Cool. I love those small-world moments. Who's Fred W.? A commenter on the Forbes site? (I haven't looked at the comments on that edition of my answer.) – Peter Hosey Jun 12 '13 at 09:59
  • Fred Wilson is a friendly mechanical engineer. He has a lot of engaging activity in the comments on his web site, avc dot com, I think. – Ellie Kesselman Jun 12 '13 at 10:12
  • Re-posts of Quora answers on Forbes rarely if ever elicit comments! Quora needs to enable interaction with commenters, either the original contributors or someone from Quora, as any comments that are made on the Forbes re-posts are auto-collapsed by default (for everyone, not just the trouble-makers, if Forbes has any... I'm sure they do). – Ellie Kesselman Jun 12 '13 at 10:17
  • 1
    @FeralOink aVC = Fred W - $$ ;). He seems to be a venture capitalist. – aVC Jun 12 '13 at 13:43
  • @FeralOink minus probably a lot more I guess now that I saw avc.com – aVC Jun 12 '13 at 13:59
1

I can probably give you some direction. I assume you have basic knowledge of geometry.
For this kind of requirement it will be a lot easier if you work in polar co-ordinates. Finding equidistant points on ellipse's circumference should be very similar to finding points on circle's circumference, just the equation is going to be different in the two cases.
Following link points to ellipse equation in polar co-ordinates. http://en.wikipedia.org/wiki/Ellipse#Polar_form_relative_to_center
And you should be able to find code for circle's case very easily. Here are some relevant links.
Calculating the position of points in a circle
How do I calculate a point on a circle’s circumference?
This is how your code will roughly look like

for (theta = 0 -> 360 degrees)
{
    r = ellipse_equation(theta);
    x = r*cos(theta) + h;//(h,k) is the center of the ellipse. You need to consider this translation if your ellipse is not centered at origin.
    y = r*sin(theta) + k;
    //Use these (x,y) values as you want
}

Keep in mind the translation required if center of ellipse is not at origin. Choose increment for for loop as fine as you need. You can have theta value in radians ranging from 0 to 2*PI also instead of degrees.

Community
  • 1
  • 1
zambrey
  • 1,452
  • 13
  • 21