Monday, March 15, 2010

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

Introduction

A coworker recently wanted to paint a spiral. He found a programming book that included some sample code to generate spirals, but there were two catches:

  • For a small company like ours: use of that code required a $500 license fee.
  • Even then: the spiral he rendered was basically a polyline. (That is: it was rendered as a series of lines, and had no curvature.)
  • Both of these restrictions made that implementation a bad idea, so I decided to flesh out a better model. What I ended up with is a model that helps convert parametric equations to java.awt.Shapes, and a subclass that caters specifically to spirals.

    Parametric Equations and PathIterators

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

    At its most basic level: drawing a shape in Java requires a java.awt.geom.PathIterator.

    Preferably we'll then package this iterator in a java.awt.Shape, but with just an iterator you can use the following code to create a fillable/drawable shape:

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

    A PathIterator breaks a path up into a series of bezier segments: so our first task is to bridge the gap between a parametric expression and bezier curve.

    Mathematically this is trivial to compute, but it may require a couple of pages of scrap paper to derive/understand everything.

    Converting Parametric Equations into Bezier Curves

    The most flexible bezier segment type that Java supports is the cubic segment, so that's the segment type we want to focus on. A cubic bezier segment has 4 control points: two for each end of the segment, and two to define the tangent slopes at each endpoint. Our goal is break our parametric function into a series of cubic bezier segments. That is: we're starting out with the parametric functions x(t) and y(t), and we have to extract from that a series of segments, where each segment is defined by 4 points.

    Before we dig in to the math here, we need to make an important distinction: we're starting with a parametric equation that uses a variable t. We're going to convert that into a series of adjacent bezier curves, but a bezier curve is also a parametric equation. So I'm going to refer to those segments as using t_bezier for their t-values. So we have "t" (for the master equation) and "t_bezier" (for the individual segments used to replicate the master equation).

    If we want our bezier curve to fit a range of t-values from [ta, tb], then we know that:

    x_bezier(0) = x(ta);
    dx_bezier(0)/dt = dx(ta)/dt;
    x_bezier(1) = x(tb);
    dx_bezier(1)/dt = dx(tb)/dt;

    This only matches our bezier curve to the endpoints. The closer those endpoints are: the better a match this piecewise solution is going to give us. (If they are infinitesimally close: they will be a perfect match.) This is discussed more below. For now: assume that "ta" and "tb" are reasonably spaced.

    Since a cubic bezier curve can be expressed in terms of a cubic polynomial, we also know that:

    x_bezier(t_bezier) = ax*(t_bezier^3) + bx*(t_bezier^2) + cx*(t_bezier) + dx;
    x_bezier(0) = ax*(0^3) + bx*(0^2) + cx*(0) + dx = dx;
    dx_bezier(0) = 3*ax*(0^2) + 2*bx*(0) + cx = cx;
    x_bezier(1) = ax*(1^3) + bx*(1^2) + cx*(1) + dx = ax + bx + cx + dx;
    dx_bezier(1) = 3*ax*(1^2) + 2*bx*(1) + cx = 3*ax + 2*bx + cx;

    So it follows that:

    x(ta) = dx;
    dx(ta)/dt = cx;
    x(tb) = ax + bx + cx + dx;
    dx(tb)/dt = 3*ax + 2*bx + cx;

    So now we've mapped our source equation to the polynomial coefficients we need. But a cubic bezier curve is traditionally expressed as a series of 4 control points -- not as 4 coefficients. The article "Shapes: an Introduction" explains most of the math needed to make this conversion. Eventually if we solve for the control points in terms of the original parametric equation, then we'll end up with:

    end1 = x(ta);
    end2 = x(tb);
    ctrl1 = (dx(ta)/dt+3*x(ta))/3;
    ctrl2 = (3*x(tb)-dx(tb)/dt)/3;

    That is all the information we need to define a cubic bezier segment that replicates our original graph from [ta, tb]. (We also have to apply exactly the same logic to the y-equations/coefficients, but the end result is the same system using "x" instead of "y".)

    This logic has been built in to the abstract ParametricPathIterator, so there is a PathIterator that breaks a parametric equation up into thousands of smaller bezier segments as needed. The most crucial piece of information we're lacking now is: what are the values for "ta" and "tb"? If they are too widely spaced apart: then the piecewise segments will not match the graph well. If they are too close together: then the shape becomes unnecessarily complex. This spacing is the responsibility of the subclass, because the varying complexity of the parametric graph we're plotting will affect the interval between t values.

    Spirals

    Now that we have a model to convert parametric graphs to PathIterators: creating a Spiral2D should be easy. First we have to identify the parametric equations involved. Traditionally a spiral is expressed as:

    x(t) = centerX+coilGap*t*cos(2*pi*t)
    y(t) = centerY+coilGap*t*sin(2*pi*t)

    After working with this format, though, I quickly realized I wanted at least two other variables:

  • angularOffset: to affect the initial angle.
  • coilOffset: to affect the coil offset.
  • My revised definition of a spiral looks like this:

    x(t) = centerX+coilGap*(t + coilOffset)*cos(2*pi*t + angularOffset)
    y(t) = centerY+coilGap*(t + coilOffset)*sin(2*pi*t + angularOffset)

    We'll also need to know the derivative. (Of course we could also just numerically approximate the derivative, but let's calculate it correctly since this article is supposed to be instructive...) After refreshing my memory regarding the product rule, I ended up with these equations:

    dx/dt = coilGap*cos(2*pi*t+angularOffset)-2*pi*coilGap*(t + coilOffset)*sin(2*pi*t+angularOffset)
    dy/dt = coilGap*sin(2*pi*t+angularOffset)+2*pi*coilGap*(t + coilOffset)*cos(2*pi*t+angularOffset)

    The Spiral2DPathIterator is the finished product. It extends the ParametricPathIterator and implements the spiral equations above. Each coil (from 0 to 360 degrees) is broken up into 8 bezier segments; this is not a magic number -- it is simply what empirically looked "good". Also (in keeping with Java conventions) there is a separate Spiral2D object that implements the java.awt.Shape interface for your convenience.

    Here is an applet demonstrating the final implementation:



    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.)

    No comments:

    Post a Comment