Epicycles
We're now going to see a different example of composition, in creating a type of curve known as an epicycle. An epicycle is produced when we trace a point on a circle rotating on another circle. We can stack circles on top of circles, and change the speed at which they rotate, to produce many different curves. Here's an example using two circles.
The parametric curve that produces this result is
val twoWheels: Angle => Point =
(angle: Angle) =>
Point(75, angle * 6) + Point(32, angle * 11).toVec
Let's walk through this code. We know that a parametric circle is
(angle: Angle) => Point(1.0, angle)
where I've chosen the radius to be 1. So
(angle: Angle) => Point(75, angle * 6)
is a parametric circle with radius 75, which is spinning six times faster. By the same reasoning
(angle: Angle) => Point(32, angle * 11)
is a parametric circle with radius 32 spinning eleven times faster.
The remaining portion of the code is a conversion from Point
to Vec
(the call to toVec
) and addition of a Point
and a Vec
.
Point(..., ...) + Point(..., ...).toVec
This moves the center of the smaller circle to the current point on the larger circle.
The conversion from Point
to Vec
is needed for technical reasons which I'll attempt to briefly explain.
A point specifies a location relative to a fixed origin.
A vector represents a displacement relative to some arbitrary starting point.
Therefore we can add a vector to a point to get a new point that is shifted by the vector.
Adding a point to a point, however, is not possible as points are both locations relative to the same origin, not displacements like a vector is.
(If this explanation doesn't work for you, there are many online resources that describe vectors.)
This is everything we need to build our own curves based on epicycles. The more circles we add, the more complex the curve we can create. The examples below are created using the following code. It uses three circles with fixed radius, but allows us to change the speed of rotation.
def epicycle(a: Int, b: Int, c: Int): Angle => Point =
(angle: Angle) =>
(Point(75, angle * a).toVec + Point(32, angle * b).toVec + Point(15, angle * c).toVec).toPoint
The examples are created by choosing the parameters a, b, c, as (1, 6, 14), (7, 13, 25), and (1, 7, -21) respectively.
Composing Epicycles
Let's return to the theme of this chapter, function composition. Using function composition we can do what we've done before, and break the epicycle curves into small reusuable parts.
The first component we need is a parametric circle that allows us to change the speed of rotation.
I called this a wheel
.
def wheel(speed: Int): Angle => Point =
???
We can use scale
, which we've already defined, to change the radius.
The other component we need is a way to position a wheel relative to another wheel.
(This is the point / vector addition part.)
I called this on
.
def on(wheel1: Angle => Point, wheel2: Angle => Point): Angle => Point =
???
Exercise: Wheels on Wheels
Implement wheel
and on
, described above.
Then use them, as well as tools we've created earlier, to draw your own epicycle curves.
wheel
is a parametric circle that rotates at the given speed.
def wheel(speed: Int): Angle => Point =
angle => Point(1.0, angle * speed)
on
implements the point and vector addition we discussed earlier.
def on(wheel1: Angle => Point, wheel2: Angle => Point): Angle => Point =
angle => wheel1(angle) + wheel2(angle).toVec
With these we can easily construct epicycle curves. The example we started this section with
val twoWheels: Angle => Point =
(angle: Angle) =>
Point(75, angle * 6) + Point(32, angle * 11).toVec
can be written as
val wheel1 = wheel(6).andThen(scale(75))
val wheel2 = wheel(11).andThen(scale(32))
val combined = on(wheel1, wheel2)