Parametric Curves

Right now we only know how to create basic shapes like circles and rectangles. We'll need more control to create the flower shapes that are our goal. We're going to use a tool from mathematics known as a parametric equation or parametric curve to do so.

A parametric equation is a function from some input (the parameter in "parametric") to a point, a location in space. The input tells us how far along the curve we are. For example, a parametric equation for a circle might have as its input an angle and it would give us the point on the circle at that angle. In Scala we could write

def parametricCircle(angle: Angle): Point =
  ???

If we choose lots of different values for the input, and then draw a shape at each point we get back from the parametric equation, we can suggest the shape of the curve.

In Figure hof:parametric-circles we give an example of drawing small circles at the points generated by the parametric equation for a circle. Going from left to right we draw points every 90, 45, and 22.5 degrees. You can see how the outline of the shape, the large circle, becomes clearer as we draw more points.

Parametric circle with points drawn, from left to right, every 90, 45, and 22.5 degrees.

To create parametric curves we need to learn 1) how to represent points in Doodle, 2) how to position an image at a particular point in space, and 3) revise a bit of geometry you might not have touched since high school. Let's look at each item in turn.

Points

In Doodle we have a Point type to represent a position in two dimensions. We have two equivalent representations in terms of:

This difference is shown in Figure hof:representation.

A point represented in cartesian (x and y) coordinates and polar (radius and angle) coordinates

We can create points in the cartesian representation using Point(Double, Double) where the two parameters are the x and y coordinates, and in the polar representation using Point(Double, Angle) where we specify the radius and the angle. The table below shows the main methods on Point.

+----------------------------+---------+----------------------------+---------------------------+ | Constructor |Type | Description | Example | +============================+=========+============================+===========================+ |Point(Double, Double) | Point | Constructs a Point using | Point(1.0, 1.0) | | | | the cartesian | | | | | representation. | | +----------------------------+---------+----------------------------+---------------------------+ |Point(Double, Angle) | Point | Constructs a Point using | Point(1.0, 90.degrees) | | | | the polar representation. | | +----------------------------+---------+----------------------------+---------------------------+ |Point.zero | Point | Constructs a Point at the| Point.zero | | | | origin (x and y are zero) | | +----------------------------+---------+----------------------------+---------------------------+ |Point.x | Double| Gets the x coordinate of | Point.zero.x | | | | the Point. | | +----------------------------+---------+----------------------------+---------------------------+ |Point.y | Double| Gets the y coordinate of | Point.zero.y | | | | the Point. | | +----------------------------+---------+----------------------------+---------------------------+ |Point.r | Double| Gets the radius of the | Point.zero.r | | | | Point. | | +----------------------------+---------+----------------------------+---------------------------+ |Point.angle | Angle | Gets the angle of the | Point.zero.angle | | | | Point. | | +----------------------------+---------+----------------------------+---------------------------+

Flexible Layout

Can we position an Image at a point? So far we only know how to layout images with on, beside, and above. We need an additional tool, the at method, to achieve more flexible layout. Here's an example using at that draws a dot at the corners of a square.

val dot = Image.circle(5).strokeWidth(3).strokeColor(Color.crimson)
val squareDots =
  dot.at(0, 0)
    .on(dot.at(0, 100))
    .on(dot.at(100, 100))
    .on(dot.at(100, 0))

This produces the image shown in Figure hof:square-dots.

Using `at` layout to position four dots at the corners of a square.

To understand how at layout works, and why we have to place the dots on each other, we need to know a bit more about how Doodle does layout.

Every Image in Doodle has a point called its origin, and a bounding box which determines the limits of the image. By convention the origin is in the center of the bounding box but this is not required. We can see the origin and bounding box of an Image by calling the debug method. In Figure hof:debug we show the output of the code

val c = Image.circle(40)
val c1 = c.beside(c.at(10, 10)).beside(c.at(10, -10)).debug
val c2 = c.debug.beside(c.at(10, 10).debug).beside(c.at(10, -10).debug)
val c3 = c.debug.beside(c.debug.at(10, 10)).beside(c.debug.at(10, -10))
c1.above(c2).above(c3)

This shows how the origin and bounding box change as we combines Images.

Using the `debug` method to inspect the origin and bounding box of an `Image`

When we layout Images using above, beside, or on it is the bounding boxes and origins that determine how the individual components are positioned relative to one another. For on the rule is that the origins are placed on top of one another. For beside the rule is that origins are horizontally aligned and placed so that the bounding boxes just touch. The origin of the compound image is placed equidistant from the left and right edges of the compound bounding box on the horizontal line that connects the origins of the component images. The rule for above is the same as beside, but we use vertical alignment instead of horizontal alignment.

Using at we can move an Image relative to its origin. In the examples we're using here we want all the elements to share the same origin, so we use on to combine Images that we have moved using at.

There are four ways we can call at:

We can convert a Point to a Vec using the toVec method.

Point.cartesian(1.0, 1.0).toVec
// res1: Vec = Vec(x = 1.0, y = 1.0)

Geometry

The final building block is the geometry to position points. If a point is positioned at a distance r from the origin at an angle a, the x- and y-coordinates are (a.cos) * r and (a.sin) * r respectively. Alternatively we can just use polar form! For example, here's how we would position a point at a distance of 1 and an angle of 45 degrees.

val polar = Point(1.0, 45.degrees)
// polar: Point = Polar(r = 1.0, angle = Angle(0.7853981633974483))
val cartesian = Point((45.degrees.cos) * 1.0, (45.degrees.sin) * 1.0)
// cartesian: Point = Cartesian(x = 0.7071067811865476, y = 0.7071067811865475)

// They are the same
polar.toCartesian == cartesian
// res2: Boolean = true
cartesian.toPolar == polar
// res3: Boolean = true

Putting It All Together

We can put this all together to create a parametric circle. In cartesian coordinates the code for a parametric circle with radius 200 is

def parametricCircle(angle: Angle): Point =
  Point.cartesian(angle.cos * 200, angle.sin * 200)

In polar form it is simply

def parametricCircle(angle: Angle): Point =
  Point.polar(200, angle)

Now we could sample a number of points evenly spaced around the circle. To create an image we can draw something at each point (say, a triangle).

def sample(samples: Int): Image = {
  // Angle.one is one complete turn. I.e. 360 degrees
  val step = Angle.one / samples
  val dot = Image
              .triangle(10, 10)
              .fillColor(Color.limeGreen)
              .strokeColor(Color.lawngreen)
  def loop(count: Int): Image = {
    val angle = step * count
    count match {
      case 0 => Image.empty
      case n =>
        dot.at(parametricCircle(angle)).on(loop(n - 1))
    }
  }
  
  loop(samples)
}

This is a structural recursion, which is hopefully a familiar pattern by now.

If we draw this we'll see the outline of a circle suggested by the triangles. See Figure hof:triangle-circle, which shows the result of sample(72).

Triangles arranged in a circle, using the code from `sample` above.

Parametric Curves as First-class Functions

So far we haven't seen anything that requires we use our parametric curves as functions instead of methods (and, indeed, we have defined them as method though we know we can easily convert methods to functions.) It's time we got something useful from functions. Remember that functions are first-class values, which means: we can pass them to a method, we can return them from a method, and we can give them a name using val. We're going to see an example where the first property---the ability to pass them as parameters---is useful.

We've just defined a method called sample that samples from our parametric curve. Right now it is restricted to sampling from the method parametricCircle. It would make a lot of sense to reuse this method with different parametric curves, which means we need to be able to pass a parametric curve to sample from to the sample method. We can do this with a function parameter. Here is what the code might look like.

def sample(samples: Int, dot: Image, curve: Angle => Point): Image = {
  val step = Angle.one / samples
  def loop(count: Int): Image = {
    val angle = step * count
    count match {
      case 0 => Image.empty
      case n =>
        dot.at(curve(angle)).on(loop(n - 1))
    }
  }
  
  loop(samples)
}

In this implementation of sample I have added two new parameters, the parametric curve to sample from and the Image to use to draw the samples. This gives us more flexibility in the output. Now we just need to define some more parametric curves, which is what the next exercise involves.

Exercises {-}

We have some new tools in our toolbox. It's time to have some fun exploring what we can do with them.

Spirals

To create a circle we keep the radius constant as the angle increases. If, instead, the radius increases as the angle increases we'll get a spiral. (How quickly should the radius increase? It's up to you! Different choices will give you different spirals.)

Implement a function or method parametricSpiral that creates a spiral.

<div class="solution"> Here's a type of spiral, known as a logarithmic spiral, that has a particularly pleasing shape. sample it and see for yourself!

def parametricSpiral(angle: Angle): Point =
  Point((Math.exp(angle.toTurns) - 1) * 200, angle)

</div>

Samples

Use the parametric curves we have defined so far to create something interesting. There is an example in Figure hof:psychedelic-spirals

A picture created using the parametric curves we have seen so far.

Conclusions→