The system diagrams uses for constructing paths can seem overly complex at first—there are many different types involved, and a plethora of ways to convert between them. However, it is the result of deep thought and careful design—that's not to say it is perfect, but for the most part, things are the way they are for a reason. The concept of a "path" has quite a few inherent subtleties!
The goal of this tutorial, therefore, is to give you proficiency in working with trails and paths via practice on numerous exercises. This tutorial is not intended to be a comprehensive reference; for that, consult the user manual. As a prerequisite, you should already be familiar with the material in the tutorial on vectors and points.
The basic building block of diagrams paths is the segment. For the purposes of this tutorial, however, it is enough to know that a segment can be either a straight line segment or a (cubic Bézier) curve, and that the lines, loops, trails, and paths we will meet in the following sections are all built out of segments. Typically, in creating diagrams, one does not need to work directly with segments. Consult the user manual if you wish to know more about them.
The first foundational concept to understand is that of a line, which is a translation-invariant path through space, consisting of a sequence of segments end-to-end. Note that a line can be arbitrarily kinked and curved. Think not of a "straight line", but rather of a "train line" or a "subway line".
A line is a kind of trail (we will meet the other kind in the
following section), and has type Trail' Line v
for some vector type
v
(typically V2 Double
).
Lines are an instance of the TrailLike
class, so to construct a
line, you can use any function with a return type like TrailLike t =>
... -> t
. Examples of such functions include fromOffsets
,
fromVertices
, fromSegments
, (~~)
, circle
, arc
, triangle
,
square
, pentagon
, rect
, roundedRect
, polygon
, arc
,
cubicSpline
, and bspline
, among many others (click a function name
to see its type, its documentation, and other nearby functions).
A line can be turned into a diagram with strokeLine
. Since lines are
translation-invariant, they have no definite location in space, and
strokeLine
must arbitrarily pick a location; by default, the origin
is chosen as the starting location of the line. (Of course, in many
situations this does not matter.)
Lines are never filled, so setting a fill color on a line has no effect. (For things that do get filled, see the next section on Loops.)
Below are a series of diagrams that you should attempt to reproduce. Each exercise lists functions that you may find useful in completing it. If you are really stuck or want to check your answers, you can find the source code for this tutorial on github.
fromOffsets
lineOffset
, direction
fromVertices
pentagon
pentagon
, onLineSegments
A very important feature of lines is that they are an instance of
Monoid
, with the empty line (containing no segments) as mempty
,
and concatenation of lines as mappend
(aka <>
).
mappend
iterateN
, rotateBy
, mconcat
reverseLine
A loop is another kind of trail, with type Trail' Loop v n
. Loops
are like lines, except for the fact that they are "closed": they end
in the same place where they start, and have an "inside" and an
"outside".
Loops are also an instance of TrailLike
, so many of the same
functions mentioned in the previous section for constructing lines can
also be used to construct loops.
Loops can be turned into diagrams with strokeLoop
.
strokeLoop
Change strokeLoop
to strokeLine
in your solution to the
previous exercise. Explain the difference in the output.
There are two functions which allow converting a line into a loop.
The first is glueLine
. It simply assumes that the line ends in the
same place that it starts, and "glues" the line closed. (If the line
does not end in the same place that it starts, the final segment will
be altered so that it does.)
Note that unlike lines, loops do not have a Monoid
instance. One
common pattern for constructing complicated loops is to concatenate
some lines and then call glueLine
on the result. You try:
glueLine
arc
You may also find this function useful:
> andThen t1 t2 = t1 <> t2 # rotate (d1 ^-^ d2)
> where
> d1 = direction (tangentAtEnd t1)
> d2 = direction (tangentAtStart t2)
Alternatively, you can use Diagrams.TwoD.Path.Follow
from
the diagrams-contrib
package.
The second function for converting from lines to loops, closeLine
,
adds an extra (linear) segment from the end of the line to the
beginning.
closeLine
Finally, to convert from a loop to a line, use cutLoop
, which "cuts"
a loop at its shared start/end point, resulting in a line which "just
happens" to end where it starts. It is harder to come up with
exercises requiring the use of cutLoop
; in most cases where you
might think of using it, you could simply construct a line in the
first place. For example,
> (square 1 :: Trail' Loop V2 Double) # cutLoop :: Trail' Line V2 Double
is exactly the same as square 1 :: Trail' Line V2 Double
. So there
are no exercises here; it's simply useful to be aware that in any
situation where something that is naturally a loop is interpreted as a
line (for example, square 1 :: Trail' Line V2 Double
), cutLoop
is being
used under the hood.
We have now seen both types of trails. The Trail
type is simply a
wrapper around both lines and loops. That is, something of type
Trail v n
is either a line or a loop, wrapped up so the type does not
tell you which it is (though it is possible to recover the information
dynamically, using functions like withTrail
). To make a line or
loop into a Trail
, use wrapLine
or wrapLoop
, respectively. Many
of the functions we have seen on lines and loops have corresponding
versions that operate on Trail
s, such as strokeTrail
, glueTrail
,
closeTrail
, reverseTrail
, and cutTrail
.
The Located
wrapper associates a point location with an object,
turning translation-invariant things into located things.
To give a location to something, use at :: a -> Point (V a) (N a) ->
Located a
. Located lines, loops, and trails can be turned into
diagrams with strokeLocLine
, strokeLocLoop
, and strokeLocTrail
respectively.
One reason you may sometimes want to work with Located
trails is
when using explodeTrail
to turn a trail into a collection of
Located
trails, one for each individual segment. Using Located
in
this way remembers the locations of the segments relative to one
another.
explodeTrail
, mapLoc
A path is simply a collection of located trails.
star
, pathTrails
atPoints
, fillRule