|
Crop uses a postfix syntax, like Hewlett-Packard calculators
and Adobe's PostScript language. If you're unfamiliar with
postfix (also called reverse Polish notation), you can read
about the basic technique here.
You can define variables within Crop, just as in any
other language. The collection of all the variables that exist
at any given time, along with their values, is called the
dictionary. Initially the dictionary starts off empty,
but there are a few special variables that get computed for
you on the fly when you need them, as I'll discuss below.
There are three distinct types of objects in Crop:
scalars (floating-point or integer numbers), points
(represented by pairs of numbers), and objects (circles,
ellipses, and polygons). In the following discussion, I'll
label variables starting with the letters s,
p, or o
to identify their type (I'll also use i
to refer to scalars that must be integers). I'll often include
a letter or number after the type letter to suggest its meaning
(e.g., I'll usually write sr
for a scalar that defines a radius).
Crop is written in plain text, with tokens separated
by white space. Any string of characters bounded by white
space that doesn't have a predefined meaning is simply pushed
on the stack as that string of characters. Anywhere a single
space would do, you can insert as many spaces, tabs, carriage
returns, or other white space that you want to improve legibility.
The language is case-insensitive for all commands (so, for
example, makepoint,
makePoint,
and MAKEPOINT
all do the same thing), but it's case sensitive for variable
names (so bob,
BoB, and Bob
are all distinct variables).
Let's start with the basic mathematical operations for calculating
with scalars:
s1 s2 +
Push the sum s1+s2
onto the stack.
s1 s2 -
Push the difference s1-s2
onto the stack.
s1 s2 *
Push the product s1*s2
onto the stack.
s1 s2 /
Push the ratio s1/s2
onto the stack.
Points are obviously important for describing formations.
We create a point by naming two scalars and then bundling
them together with makePoint:
s1 s2 makePoint
p
This puts the point (s1,s2)
on top of the stack.
There's also a shortcut for the special point (0,0), also
called the origin. You can use the symbol #
any time to represent the origin (this is meant to remind
us of the center of a coordinate system; I'd have used the
plus sign, but that was taken!).
We can add and subtract points, and multiply and divide them
by scalars:
p1 p2 p+
Add the components together and push the new point onto the
stack.
p1 p2 p-
Subtract the components and push the difference onto the stack.
p1 s1 p*
Scale the components and push the scaled point onto the stack.
p1 s1 p/
Divide the components and push the scaled point onto the stack.
Note that the operators here are all prefixed with the letter
p, giving us
for example p+
instead of simply +.
In computer science terms, the +
operator is not overloaded (that is, built so that it works
differently for different types of operands). If you try to
add two points with +,
rather than p+,
you'll get an error. I think that for this little language,
there's value to explicitly distinguishing these operators.
We can also find the distance between two points:
p1 p2 distance
This pushes the scalar distance |p1-p2|
onto the stack.
To name an object so we can use it later, we use the name
command. This takes whatever object is on top of the stack
(a scalar, point, or object) and assigns it to the given name.
You can redefine a name any time by just assigning a new value
to it.
pso varname name
For example, you might say 3.14
pi name to create a value for pi,
or p1 p2 + 2 / midpoint
name to set the variable midpoint
to the point between p1
and p2. This
makes it easy to refer to the same thing multiple times in
your formation without creating it anew each time. You don't
need to declare the types of your variables - the type is
inferred from the assignment. Each time you assign a new value
to a variable, the variable adopts the type of the object
it's been assigned to.
Okay, that finishes up the foundation. Let's make some geometry!
Our three stars of the geometry world are line,
ellipse, and
circle:
< p0 p1 p2
... pn > line
Draws a line from p0
to p1, then
to p2, and
so on, to pn.
Requires a minimum of two points.
p < s1 s2 ...
sn > circle
Draws concentric circles centered at p,
with radii s1,
s2, and so
on. Requires at least one radius.
pp pq ss ellipse
Draws an ellipse using the points pp
and pq as foci,
and a string of length ss.
The line and
circle commands
use lists. A list is just a sequence of values between
angle brackets. The angle brackets are necessary, even if
the list has just one or two elements. Lists cannot be empty
(or else you'd be drawing nothing).
Lines naturally create polygons. Crop supports regular polygons,
or n-gons. Here are the commands that create circles, ellipses,
and polygon objects (Each of these commands pushes the new-constructed
object onto the top of the stack.):
pc sr makeCircle
pp ps ss makeEllipse
pc in sr sa makeNgon
A circle is defined by a center pc
and a radius sr.
An ellipse is defined by two points pp
and ps and
a length ss.
A polygon is defined by a center pc,
the number of sides in,
a radius sr,
and an angle sa.The
radius is the distance from the center to the first vertex.
The angle is the number of degrees to rotate the polygon clockwise.
If the angle is zero, the first vertex lies on the +X axis.
As a convenience, the special character %
can be used for the angle rather than a number - this means
to rotate the polygon 180/in
degrees: this puts the midpoint of the last edge on the +X
axis. The percent sign (two circles and a line) is meant to
remind us of the edge between two vertices.
The next command we'll look at is trope,
which takes four arguments: two points and two scalars. The
name is a contraction of "triangle-rope," which
is a way to think of locating points in the field. Suppose
you have two points A and B, and you know you
want to find a new point that is a distance a and b
from each point, respectively. You can do this with a rope
that has those lengths marked off on it, and friends standing
at the two points. I call this loop a "triangle-rope",
so the command that emulates it is trope:
pa pb sa sb trope
trope finds
the two points that are simultaneously a distance sa
from pa, and
a distance sb
from pb. It
selects the point that's on the left side of the line from
pa to pb,
and pushes that point onto the top of the stack. The geometry
for computing this point requires finding the intersections
of two circles; the details are in my column and book.
Another way to find a point with respect to other points
is with the pwalk
command:
o1 p1 sd pwalk
This command takes an object o1,
a starting point p1,
and a distance sd
to walk clockwise around the perimeter of that object, and
pushes the newly-computed point back onto the stack. Note
that the distance is not the straight line between the starting
point and the ending point, but instead is the distance as
measured along the object itself. If the object is a circle
or ellipse, then the point is found by walking along the curve.
If the object is a polygon, then we walk along the polygon's
edges until we've covered the necessary distance.
As a convenience, before pwalk
begins it looks at the starting point, and if it's not already
on the perimeter of the object, it moves the starting point
temporarily to its nearest point on the perimeter.
Related to pwalk
is pspin:
o1 p1 sd pspin
This command takes an object o1,
a starting point p1,
and an angle sd
about which to rotate that point clockwise around the center
of the object. Like pwalk, this command will move the resulting
point to the nearest point on the perimeter of o1
if it's not already on there.
Many crop formations are built on a structure defined by
one or more regular polygons. We've seen above how to define
a polygon. Now we'll see how to use a polygon to drive the
formation of a structure. In many ways, the next command is
the heart of Crop:
[ commands ] pc
in sr sa ngonloop
Like makengon,
the values pc in sr
sa define a polygon with a center at pc,
made of in
points, with a radius sr,
rotated clockwise by an angle sa.
As before, sr
is the distance from the center to each vertex, and sa
is an angle in degrees to rotate the polygon clockwise (and
again, the special character %
used here means to rotate the polygon 180/in
degrees).
The stuff between the square brackets gets executed in
times, once for each vertex in the polygon. What makes this
loop special is that while a loop is executing, variables
beginning with the letter V
take on a special meaning: they refer to the vertices of the
polygon. The system takes the rest of the name of the variable
and interprets it as a number (if you use variables in the
loop that begin with V
but aren't immediately followed by an integer, you'll get
an error).
The variable V0
has the value of the current vertex (that is, it's a point).
The variable V1
is the next point clockwise around the polygon, V2
is the one after that, and so on. Variable V-1
is the point preceding the current point (that is, counter-clockwise
from V0), V-2
is the one counter-clockwise from that one, and so on. Note
that V-2 is
a variable name, not an arithmetic expression (both because
Crop is not an infix language, and because V-2
has no spaces).
Another special variable is LC,
which stands for LoopCount. This is an integer that tells
us how many times the loop has been completed. The first time
through the loop, LC
is zero. The next time it's one, and so on.
Before the loop begins, the system saves the current stack.
When the loop is finished, the stack is reset to its saved
condition. Each iteration of the loop begins by clearing the
stack (the dictionary is left intact). On the first iteration,
the current vertex (that is, V0)
is the vertex on the positive X axis (assuming that the angle
is zero; if it's not, we use the first vertex in its rotated
position). Then the commands are executed as usual, with the
only exception that the V
variables refer to vertices as described above. When the last
command has been executed, the stack is cleared, the current
vertex is moved to the next one in a clockwise direction,
and the commands are executed again.
You can nest loops if you want, for example by putting one
ngonloop inside
another. The variables V0,
V1, V-1,
and so on, as well as LC,
refer to the innermost loop in which they appear. If you want
to refer to the vertices of the polygon in an outer loop,
append a prime to the variable name. Thus V1'
refers to the vertex after the current one in the polygon
one loop up, and V-2''
refers to the vertex two steps before the current one in the
polygon two loops up. Similarly, LC'
refers to the loop count in the loop one up, LC''
goes two loops up, and so on. Each polygon loop can of course
have a different center, radius, and number of vertices. In
fact, a common idiom is to place the center of an inner polygon
on the vertices of an outer one.
For example, suppose we want to place a small pentagon on
each vertex of a big triangle, and draw a line from each vertex
of each pentagon to the vertex of the triangle it's centered
upon. To make things more interesting, let's rotate each pentagon
so that it's first vertex lies on the line from the center
of the triangle to the center of the pentagon. We could write:
[ [ < V0 V0' >
line ]
V0 5 1 LC 120
* ngonloop
] # 3 6 0 ngonloop
In the innermost loop, V0
refers to the current vertex of the pentagon, and V0'
refers to the current vertex of the triangle. The expression
LC 120 * rotates
the pentagon based on how many times we've gone through the
triangle loop.
That's it for the body of the language. There are a few miscellaneous
housekeeping commands that are common to most postfix languages:
pop
This command takes no arguments; it just pops the top element
off the stack and discards it.
printDictionary
This command takes no arguments; it prints the complete current
dictionary to the output.
printStack
This command takes no arguments; it prints the current stack
to the output.
// comment
Anything after a pair of double slashes is considered a comment
until the end of the line.
This wraps up the Crop language as it stands today.
Crop is a completely phenomenological language, designed
to match the formations that I've looked at and tried to replicate
compactly. I've tried hard to keep it as simple and small
as possible, while also remaining legible and easy to understand.
I encourage readers to extend the language if they think of
other commands that are as simple and useful as the ones above.
|