line
function to make simple line segments and our own
version of the polygon function,path
function to create arbitrary forms,path
function as a
useful design pattern, andAlong the way, you will also learn a little more about the programming features in Little.
One kind of basic SVG shape that we have not mentioned is a finite line segment.
Just like the other basic shapes, there is a built-in function that allows the
creation of the appropriate SVG node, naturally called line
:
(line fill w x1 y1 x2 y2)
This creates a line from the point (x1,y1)
to (x2,y2)
with the color fill
and width w
. This function works very well for making very simple line
segments between shapes.
We can also draw polygons by linking line segments together. In fact, let's do
just that. Our goal is to write a function ourPolygon
that, given a list of
points [[x1 y1] [x2 y2] ... ]
, creates the outline of a polygon by drawing
line segments between all the adjacent points. To do this, we will need to
introduce a couple more programming features in Little.
case
Expressions and Recursive FunctionsWe want our ourPolygon
function to work no matter how many points are provided
in the input list. To achieve this, we will write a recursive function that
walks through each point in the list and "does its thing," where "its thing"
will fall into different cases depending on what the list of points happens
to look like. Let's start with the latter.
The built-in case
function takes an expressionToExamine
and a
sequence of branches to evaluate depending on what pattern
expressionToExamine
matches:
(case expressionToExamine
(pattern1 returnThisIfExpressionMatchesPattern1)
(pattern2 returnThisIfExpressionMatchesPattern2)
...
(_ returnThisIfExpressionMatchesNoPatterns))
The extra newlines and spaces between the cases are optional but often make these expressions easier to read. As you've probably come to expect by now, all of the parentheses — around the entire expression and around each of the branches — are required.
The idea behind this expression is that if expressionToExamine
matches one of
the patterns, then the expression that is associated with that pattern is
returned. The patterns are checked from the top down, and an underscore _
matches anything. If you don't have too much experience with functional
languages, you can think of case
statements as somewhat analogous to
if-expressions found in most programming languages. However, case
is
generalized to work on more than just Boolean values and with more than just
two different branches.
Pattern matching is most often useful for dealing with lists, for which there
are two kinds of patterns. First, a list pattern of the form [x1 ... xn]
matches lists with exactly n
elements. Second, a list pattern of the form `[x1
... xn | xrest]` matches lists with at least n
elements, where all extra
elements (if any) get bound to the variable xrest
. You can think of this
second kind of pattern as matching the first n
elements from the "head" of the
list and then giving the name xrest
to the "tail" of the list.
Thought Exercise: Can you guess what the Little expressions `[e1 ...
en]` and [e1 ... en | erest]
mean?
Here is a sample case
function and some example inputs, where the comments
below describe which patterns get matched:
(case inputList
([] ...) ; pattern 1
([x] ...) ; pattern 2
([x y] ...) ; pattern 3
([x] ...) ; pattern 4
([ [a b] | rest ] ...) ; pattern 5
(_ ...) ; pattern 6
; sampleInput the patterns that sampleInput matches
; ------------------------ ----------------------------------------
; [] matches pattern 1
; [ 2 ] matches pattern 2, and *not* pattern 4
; [ 1 2 3 ] matches pattern 6
; [ [ 2 4 ] ] matches pattern 5
; [ [ 2 3 4 ] ] matches pattern 6
; 'oh my, i am not a list' matches pattern 6
; 3.14 matches pattern 6
Since Little does not enforce any type constraints, the catch-all pattern '_
'
can match any kind of input — even ones that you may sometimes not
want to operate on at all, so be careful when using it!
A very common pattern that you will encounter is to use case
in recursive
functions that operate on lists by defining a base case for the empty list and a
case for non-empty lists. Remember the built-in concat
function that takes a
list of lists and concatenates them into a single list? Here's how we can define
it ourselves:
(defrec ourConcat (\listOfLists
(case listOfLists
([] [])
([[] | rest] (ourConcat rest))
([[ x | xs ] | rest] [x | (ourConcat [ xs | rest ])]))))
Notice that we say defrec
, rather than def
, because the definition of
ourConcat
recursively calls itself. This function takes a listOfLists
and
returns something based on which of the following three patterns it matches:
listOfLists
is empty...listOfLists
is empty...rest
listOfLists
x
and tail xs
...x
plus everything in xs
and rest
.If ourConcat
is called with anything besides a list, then this function will
crash at run-time (by design). If you have functional programming experience,
this will likely look like a standard recursive definition — albeit with a
different syntax than you're used to.
With the knowledge of how to write functions that operate on lists, we can now
return to the task of creating a polygon out of a list of points. Our approach
is to draw a line
between each pair of points, as well as the first and last
ones:
(def [c w] ['black' 10]) ; color and width
(def connect (\([x1 y1] [x2 y2]) (line c w x1 y1 x2 y2)))
(defrec ourPolygon_ (\(first prev points)
(case points
([] [ (connect prev first) ])
([next | rest] [ (connect prev next) | (ourPolygon_ first next rest) ]))))
(def ourPolygon (\points
(case points
([] [])
([lonelyPoint] [])
([first | rest] (ourPolygon_ first first rest)))))
Awesome! Our function ourPolygon
handles preparing the arguments for the
helper function, ourPolygon_
which then operates recursively on the elements
of points list. As it operates on each point, it connects it with a black line
to the previous point.
Practice 4.1: Try out this program with a list of points in Sketch-n-Sketch to draw some polygons.
Exercise 4.1: Here's an excuse to get more practice with this syntax. As it
is, calling ourPolygon
with the list of points [[10 10] [50 50]]
draws two
nearly-identical line segments. Add an additional case in ourPolygon_
that
results in only one line segment is drawn.
Exercise 4.2 (Optional): If you know what "fold" or "reduce" means and if you're
feeling ambitious, redefine ourPolygon
to use the built-in foldl
or foldr
functions instead. This exercise is completely optional for the purposes of this
tutorial.
Implementing ourPolygon
was a good excuse to learn more about Little. But
because polygons are so common, it should be no surprise that they are primitive
in SVG. Therefore, Little also provides a polygon
function to match; check
out Prelude if you'd like to see how to use it.
Although drawing polygons with straight edges is useful, one often wants curved
edges as well. For this, SVG offers a more general path
primitive that
subsumes line
and polygon
because it can also draw curved lines. The goal
of this next section is to get you familiar enough with SVG paths so that
you will be comfortable exploring all of the functionality the
specification has to offer.
The general structure of an SVG path is a list of draw commands, each of which
has associated "control points" that are interpreted, or evaluated, in order to
draw the shape. This list of commands is put into a path
node in SVG with some
additional attributes, and then the browser handles the rendering of the overall
path. Sketch-n-Sketch supports manipulation of the control points of a path for
all of the most commonly used commands, which allows for intuitive direct
manipulation of SVG paths.1 To create an SVG path node in
Little, the path
function can be invoked with arguments of the following form:
(path fillColor lineColor lineThickness pathCommands)
The arguments can be interpreted as follows:
fillColor
represents the color of the interior of the path ('none'
is a
valid entry to specify no fill),lineColor
represents the color of the path itself (again, 'none'
is a
valid entry for no border),lineThickness
represents the thickness of the border that lineColor
colors, andpathCommands
is a list of commands with their control points that define the
SVG path.In general, the list of path commands for any given path begins with a "moveto" command, which sets the "cursor" for the path to that location without drawing anything. Then, there is a sequence of draw commands which may optinally be followed by a "closepath" command which connects the end of the path back to the beginning. There are fancier things you can do with SVG paths, but even these commands are enough to make some complicated shapes and forms.
Here is an example path which draws a square with a light blue fill and a gray outline:
(path 'lightblue' 'gray' 5
[ 'M' 100 100
'L' 200 100
'L' 200 200
'L' 100 200
'Z' ])
We begin the path command list with a "moveto", denoted by a captial
'M'
.2 Then, we have three successive "lineto" commands, denoted by a
capital 'L'
, which draw three edges starting from the initial (100,100)
coordinate going clockwise around a square with edge length 100
. Then, we end
the command list with a "closepath" command, indicated with a capital 'Z'
.
This draws an edge back to the start at (100,100)
. This syntax mirrors the
syntax defined in the SVG path specification linked above, so you can use it as a
reference for how to specify all the control points for each command.
Notice that the list of path commands above contains different "types" of values, namely, strings and numbers. This is no problem in Little, because there are no types. As a result, we have chosen this representation of path commands because we can, and because it makes it easier to copy-and-paste sample paths from any "raw" SVG path examples to Little.
This is definitely nicer than using a whole bunch of line segments like we did
before! What's especialy nice is that SVG paths also have support for specifying
a variety of curves. Here we will demonstrate only the quadratic Bézier curve,
which is the simplest type of curve to define and is specified with a 'Q'
.
Below is an example that draws an unfilled, sinusoidal-looking curve.
(def curve
(path 'none' 'black' 5
[ 'M' 300 300
'Q' 350 250
400 300
'Q' 450 350
500 300 ]))
(svg [curve])
Practice 4.2: Go ahead and enter this into a document in Sketch-n-Sketch, and turn on the basic zones. See the points that are off of the line? Try manipulating them. These are the control points for the first and second quadratic curves, respectively.
Exercise 4.3: Now that you've seen how basic paths work, it's your turn. Try
experimenting with mixing 'L'
and 'Q'
commands. For example, you may try
drawing your favorite letter of the alphabet — just make sure it has at least
one curve in it!
After that, look at some of the built-in examples that use paths in this way, such as the Chicago Botanic Garden Logo or the Eye Icon. In these designs, you will notice that there are many variables defined as top-level definitions that are directly or near-directly dropped in to the coordinate positions inside of the command list. This results in a lot of constants and variable names in play, which can get unwieldy. However, sometimes there is no way to avoid having a lot of parameters at once, as a complicated design (such as the Eye Icon) inherently requires them.
So far we have seen how Sketch-n-Sketch works with basic paths, but the tool also supports and displays control for the other sorts of curves in a similar fashion.3 Because SVG paths can become quite complex, we have found that working with them by mixing programmatic and direct manipulation is one area where Sketch-n-Sketch excels. Our built-in examples include many involved designs that rely heavily on paths. Next, we identify two design patterns that we have found to be useful.
Often times the specification for a path can be improved upon by introducing a parameter that is shared by some or all the points. For example, if we knew that we wanted our curve to have both the up and down portions to have the same amplitude and that we would like the curves to be evenly spaced between each other, we could redefine it in the following way:
(def [amplitude spacing x0 y0] [50 100 300 300])
(def curve
(path 'none' 'black' 5
[ 'M' x0 y0
'Q' (+ x0 (/ spacing 2!)) (- y0 amplitude)
(+ x0 spacing) y0
'Q' (+ x0 (* 1.5! spacing)) (+ y0 amplitude)
(+ x0 (* 2! spacing)) y0 ]))
(svg [curve])
Practice 4.3: Enter the above into your document and manipulate the control points as before. You should now see that your changes affect the other parts of the path that depend upon the same parameters. Depending on your design, this can be a great improvement over defining each portion individually both in terms of number of parameters that are in play as well as adjusting attributes shared by many portions of your path.
Quite a few of the built-in examples that utilize paths fall into this pattern
of design, including Active Trans, the Haskell.org logo, POP-PL logo, and the
Wikimedia logo. The Active Trans logo is a particularly good example of this.
Switch to it and look at the definition for grayPts
. Notice how all of the
points of the skyline are defined as offsets from a shared height parameter. Try
manipulating the control points of the skyline and observe how they all move up
and down together. Since the skyline profile of the design is meant to be fixed,
this allows for easy direct manipulation of a design characteristic that would
otherwise by very annoying to change manually. While you're at at, see how that
example uses a "button" (which is just a slider that controls a Boolean value)
to allow two different versions of the logo to be manipulated in sync.
Exercise 4.4 (Optional): Modify the Active Trans logo so that each of the buildings rise and fall independently rather than as one unit.
Exercise 4.5: Go ahead and define your own path in terms of a parameter that is shared between all or some of the points. Try to use at least one curve and one straight line in your design, but you can keep it fairly simple for now. If you'd like to continue with the alphabetic theme, you might select a letter that has some symmetry to it (such as S or B).
While the former design pattern is suitable for a lot of the graphics that you will work with, there are certain types of designs that lend themselves to a program structure that generates a path instead of being specified by hand. This could be for a few different reasons, including that the path is such that the control points are tedious to write by hand, the path has such a large number of control points as to be unwieldy to edit, or that the user would like to experiment with different numbers of control points, which cannot be accomplished without changing the number of path commands, which can only be done either by hand or if the list of commands is being generated by a function.
A good example of the parameters being tedious to specify by hand is in the Pie
Chart example. Notice that each wedge is a path with a specific fill
and edge style, but that they all share exactly the same structure in terms of
how the "pie slice" is defined (two straight edges with a circular curve). So,
instead of defining all of the control points by for the number of pie slices
that happen to be in the design at the moment, which would be both time
consuming, repetitive, and difficult to add or remove a pie slice, the wedge
function takes care of all of that. That function is then
mapped over the unique information for each wedge, saving the user the trouble
of writing it all themselves and making the program drastically more extensible.
Furthermore, this allows for a much more dynamic design than is otherwise possible.
In the case of Pie Chart, each path is completely specified by one function,
meaning that the initial "moveto" and "closepath" statement is encapsulated in
the function definition. However, in the Sailboat example, the situation is
different. As the waves are intended to be all one path, the commands to create
each wave must be appended to a growing command list, depending on the
particular value of the parameters that currently exist. Namely, the overall
number of times that the wave should be dependent is determined both by the
width of the wave and the overall width of the graphic. So, the number of
control points and path commands that are involved need to be able to change. As
such, the function mkwaves
is defined such that it builds up the path one wave
at a time.
When generating your own paths, make sure that the final command list begins with a "moveto" command and that the last point of each section is interacting with the first point of the next section exactly how you would like it to be. Now it's time for you to set sail.
Exercise 4.6: Your mission, should you choose to accept it, is the following:We encourage you to be amibitious with this exercise! Paths open up a world of possiblities for expressing different graphics and is one of the places where being able to directly manipulate designs along with programmatic specification really shines. Don't forget to use the SVG path specification as a reference for the path commands and to use the practices or built-in examples we discussed above if you get stuck on syntax.
The interpretation of supported commands can be found in the Sketch-n-Sketch source here.↩
If you are already familiar with SVG paths, note that Sketch-n-Sketch currently supports path commands with absolute coordinates (commands with uppercase letters) but not relative coordinates (commands with lowercase letters).↩
For an in depth look at the various sorts of paths and how to use their specific commands, we recommend that you check out the Mozilla Developer Network tutorial on the subject.↩