Do you want to give something back to this project? I'm interested in redesigning my java.net page. If you're any good at web design, please get in touch and help me give it a facelift.

Shapes: Parametric Equations (and Spirals!)

This article is split up in 2 parts:
  1. Converting a parametric graph into a PathIterator
  2. An applet demoing a Spiral2D class

Parametric Equations and PathIterators


Suppose you have a parametric graph expressed as x(t) and y(t): how do you draw that in Java?

We need a java.awt.geom.PathIterator. With a PathIterator, we can call:

GeneralPath path = new GeneralPath();
path.append(myPathIterator, false);

(A separate blog article will cover actually making java.awt.Shape... this article already has a lot of ground to cover, so that comes later.)

The graph needs to be broken up into segments for the PathIterator. Specifically we should use cubic path segments. A cubic segment lets you supply 4 pieces of information. In this case those four pieces of information should be:

f(0) = v0
f(1) = v1
df/dt(0) = dv0
df/dt(1) = dv1

Having these 4 pieces lets us define the end points and the angles/tangents at each end point. Those angles are crucial to achieve a continuous-looking curve between segments.

Of course this is a parametric graph, so we have an x-equation and a y-equation -- and they can be treated in isolation.

The article "Shapes: an Introduction" explains how to convert bezier control points into an algebraic expression:

double ax = -lastX+3*x1-3*x2+x3;
double bx = 3*lastX-6*x1+3*x2;
double cx = -3*lastX+3*x1;
double dx = lastX;
//and x(t) = ax*(t^3)+bx*(t^2)+cx*t+dx
//and dx/dt(t) = 3*ax*(t^2)+2*bx*t+cx

So this is how each cubic segment is structured. Our task here is to calculate lastX, x1, x2 and x3. Our givens are:
x(0) = dx
x(1) = ax+bx+cx+dx
dx/dt(0) = cx
dx/dt(1) = 3*ax+2*bx+cx

Substitute our coefficients and reduce those expressions:
x(0) = lastX
x(1) = x3
dx/dt(0) = -3*lastX+3*x1
dx/dt(1) = -3*x2+3*x3

Now in this case we'll know our parametric functions; what we need to find are the points to define the bezier curve. So to isolate the x-variables:
lastX = x(0)
x3 = x(1)
x1 = (dx/dt(0)+3*x(0))/3
x2 = (3*x(1)-dx/dt(1))/3

I wrapped this up in a nice abstract class: the ParametricPathIterator. The core methods you need to implement are:
protected abstract double getX(double t);
protected abstract double getY(double t);
protected abstract double getDX(double t);
protected abstract double getDY(double t);

Also you're responsible for the methods getMaxT() and getNextT(double prevT).

Spirals


All this originally came up when a coworker recently asked for a Spiral2D class. He found one from a programming book that provided an example, but:
  1. It was line-based, so it looked bad.
  2. As per all examples in this book, the license to use this would be $500 for our company. (OK, really this should be problem #1.)

So having fleshed out the ParametricPathIterator, this is a really easy undertaking. What is a spiral?
x(t) = centerX+coilGap*t*cos(2*pi*t)
y(t) = centerY+coilGap*t*sin(2*pi*t)

The derivative is a little trickier. I had to refresh my memory on the product rule to really get this right:
dx/dt = coilGap*cos(2*pi*t)-2*pi*coilGap*t*sin(2*pi*t)
dy/dt = coilGap*sin(2*pi*t)-2*pi*coilGap*t*cos(2*pi*t)

Now in this case -- and in more complicated cases -- you don't necessarily need to calculate the derivative. If you wanted to be lazy you could just write this method:
protected double getDX(double t) {
double incr = .0000001;
return (getX(v+incr/2)-getX(v-incr/2))/incr;
}

This is easier to program, but it will introduce some rounding errors as values get really small. Maybe there's not that much of a difference in practice? That's up to you to decide.

So that's the math. Codewise, these are the controls I wanted:

  1. center (Point2D): the center of this spiral.
  2. coilGap (double): the space between two coils.
  3. coils (double): the number of coil revolutions.
  4. clockwise (boolean): if this is false, we multiply the t expression in sine/cosine by negative one.
  5. offset (double): this is an angular offset in the sine/cosine.
  6. outward (boolean): this is a really subtle control. If this is true then the spiral will start at the center and spiral outward; if it is false it starts outside and spirals inward. This will not make a visual difference if you just want to "draw a spiral".

What's left to do? Not much. I need constructors, and a lot of getters and setters. The only thing that I haven't mentioned are the abstract methods getNextT() and getMaxT(). In this case these are really simple:
protected double getMaxT() {
return coils;
}

protected double getNextT(double t) {
//8 partitions in a coil
return t + 1.0/8.0;
}

The number of partitions was arbitrary, but I found that if used 4 partitions in coil it didn't look right. A little more detail made a big visual difference.

Here is an applet that shows off this project:




This applet (source included) is available here.

p.s. Try clicking in the applet to define the endpoint for the spiral. (Also try using the shift key.)

0 comments:

Post a Comment