Field let's you do all kinds of 2D drawing on its canvas using visual elements that contain drawing commands. On this page, we'll put the "reference" documentation for the central class that makes all of this possible —
FLine — but for a more general overview see BasicDrawing.
The FLine drawing system is covered in this downloadable tutorial.
Reference material for creating geometry in here on this page; for changing the appearance of geometry see DrawingFLinesProperties and for editing and querying existing lines see DrawingFLinesEditing.
To run any of the examples in this page you'll need a visual element that's been configured to draw splines. Either make a new one (and then type in the name) using the menu, or pressing 'p':
or you take any old visual element and execute:
in it. This will "upgrade" the element to include the spline drawing helper properties.
What are the spline drawing helper properties? There's really only one:
_self.lines is a list of things to draw: a list, that is, of
FLine instances that contain the geometry that you'd like to put on the screen. There are some other helpful things that happen for boxes that draw splines, but
_self.lines is by far the most important.
_self.lines.clear() myLine = FLine() myLine.moveTo(75,75) myLine.lineTo(100,100) _self.lines.add(myLine)
This code does five things:
_self.lines.clear()— gets rid of any previous geometry associated with this box
myLine = FLine()— makes a new, empty,
moveTo(75,75)— 'moves' this Pline's cursor to position 75,75
lineTo(100,100)— makes a line from where the cursor is to 100,100
_self.lines.add(myLine)— adds our line to the list of lines that need drawing
The coordinate system is such that
0,0 the origin is in the top left hand corner of the "page" with positive
x going to the right and positive
y going downwards. This is upside down from the typical cartesian presentation of coordinates but pretty typical in computer graphics (and is, in fact, the same order as this page is read in).
And thus, our visual element looks like:
The grey box is just the visual indication of the frame of this "box" (you can make that disappear/reappear completely by pressing f2 if you find it distracting). You get the box for free with the spline drawing functionality. The box moves around: it always tries to be near the lines that you put in it and it provides a stable place that you can always click on to select it (so that you can edit the code inside it). But, if you want to, you can also click on any spline that's part of this box's material.
Users coming over from NodeBox or Processing need to notice one additional thing about this first example.
FLine is a Python class, so that
myLine = FLine() makes a new instance of a
FLine and assigns it to a variable called "myLine". All drawing gets done on and through these instances. While you are calling
.lineTo and anything else you are not drawing on the screen, but rather you are simply adding instructions to your
FLine object. At some point, you'll add this object to the list of things to be drawn, hence
_self.lines.add(myLine) — if you don't, then you'll never see this line.
Both NodeBox and Processing often deliberately remove this apparent indirection for the sake of simplicity. For example, this code in NodeBox:
def curves(n=40): autoclosepath(False) beginpath(random(WIDTH), random(HEIGHT)) for i in range(n): h1x = random(1000) h1y = random(1000) h2x = random(1000) h2y = random(1000) x = random(0, WIDTH) y = random(0, HEIGHT) curveto(h1x, h1y, h2x, h2y, x, y) return endpath(draw=False)
In this environment there's one line that you are drawing, and there's always one line. Once you are done with it you have to tell NodeBox not to put it on the screen. In Field you have to tell it to actually put the line on the screen. The equivalent code in Field:
#NodeBox has these set to be the dimensions of it's window. Field's canvas is infinite. WIDTH=1000 HEIGHT=1000 def curves(n=40): path = FLine() for i in range(n): h1x = Math.random()*1000 h1y = Math.random()*1000 h2x = Math.random()*1000 h2y = Math.random()*1000 x = Math.random()*WIDTH y = Math.random()*HEIGHT path.cubicTo(h1x, h1y, h2x, h2y, x, y) return path
In NodeBox, you could then
drawPath(curves()), in Field you would
What's the difference in the end? Well, two main things:
Field lets you work more naturally on a number of different lines simultaneously, while letting you pass these lines to (your own and anybody else's) functions even as they are still being drawn. This determines just how complex your drawing can get. For example if you want to temporarily hide a line in Field, just comment out the spot where it's added to
_self.lines. If you want to hide a line in NodeBox, you might end up having to track down where it gets
endpath'd.If you are building your own libraries of drawing functions, who knows where that might be?
Field encapsulates all of the things you can call (
cubicTo, moveTo and so on) into one spot — the
FLine class. This gives you two benefits. Firstly, you can have other things called
moveTo without clashing with the line drawing system; secondly it's easy to provide contextual help for the drawing functions: you just look at all the things you can do to and with a
FLine. This is the basis for Field's in-place documentation system.
We think that these two benefits outweigh the extra typing and the cognitive overhead. We note that, should you really want to, you can always build a NodeBox/Processing style "state machine" style system out of the primitives given in Field. But it's exceedingly difficult to do in the opposite direction. And, finally, we get the impression (by squinting at the sources) that Processing itself is thinking about heading in this direction.
In the examples that follow, things like
_self.lines.add(someLine) have been left off. So you'll have to pay a little more attention.
It's worth noting that
FLine methods that don't have anything better to return, return the
FLine itself. This means that we could write our original example in a single line:
A line is a sequence of such segments, and the "cursor" moves to where this line now is. This means that we can write:
myLine.moveTo(75,75) myLine.lineTo(100,100) myLine.lineTo(150, 100) myLine.moveTo(90,85) myLine.lineTo(105, 85)
Which gets us this:
Note the connected line segments (two
.lineTo in a row) and the skip (the second
Essentially what happens is this: the pen goes down, draws a line segment, draws another line segment, then picks up the pen, moves it, and draws another line.
Finally we note that typing out
lineTo can get very tedious fast. Field allows you to compress them to
l, giving you things like
myLine.m(75,75).l(100,100). (And you can get the abbreviations possible at any time with autocomplete.) We'll keep the long form in example code here for readability.
So much for straight lines. Field handles curved segments as well:
myLine.moveTo(75,75) myLine.cubicTo(100, 75, 75, 100, 100, 100)
cubicTo(cx1, cy1, cx2, cy2, x2, y2) produces a curved segment (a "cubic spline") that goes from wherever the FLine cursor is to
x2, y2 using
cx1, cy1 and
cx2, cy2 as intermediate control points.
By selecting the points in Field we can cause it to draw these control points:
You ought to be able to convince yourself that these hidden intermediate points are in fact at
100, 75 and
75, 100 respectively. The line doesn't go through these points, it just inflects towards them along the tangents described by these control points.
.lineTo have compressed synonyms
.cubicTo can be written as
.cubicTo should be pretty obvious, but it can be inconvenient. Sometimes you want to draw a line that is straight-ish but bends a little in some direction or the other. Using
.cubicTo you have to remember where the FLine's cursor is and do some math to make the correct control points.
.relCubicTo does this for you:
myLine.moveTo(75,75) myLine.relCubicTo(20, 0, -20, 0, 100, 100)
relCubicTo(dx1, dy1, dx2, dy2, x2, y2) makes the control points by displacing points that trisect the straight line from the cursor to
In the picture above, the part of the line close to the start has been nudged right by 20 units (hence
20,0) and the part near the end has been nudged left by 20 units (hence
.polarCubicTo is similar and helpful in similar situations:
myLine.moveTo(75,75) myLine.polarCubicTo(-1, 1, -1, 1, 100, 100)
polarCubicTo(angle1, length1, angle2, length2, x2,y2) again displaces the midpoints of the line between the cursor and
x2,y2. In this case, it rotates the mid-point around the start (angle1) or the end (angle2) of the line and then scales it by length1 or length2 respectively.
A quiz to check your understanding: you should be able to work out why both
polarCubicTo(0,1,0,1, 100, 100) and
polarCubicTo(1,0,1,0, 100, 100) give you a straight line.
(Answer: in the first case, the midpoints aren't rotated at all and are scaled by '1'— i.e. just left the same; in the second case the they are rotated, but then they are scaled by '0' — i.e. reduced to being just the start and end points themselves).
Finally, one more way of making curves. Rather than specifying a destination and some control points that 'inflect' the curve towards them but don't actually pass through them
.circleTo(x1, y1, x2, y2) uses part of the circle that is uniquely defined by the cursor and the next two points
circleLine = FLine().moveTo(60,60).circleTo(56, 95, 80, 100).circleTo(70,70, 60,60)
(Note that circular arcs are decomposed into a small number of cubic splines, so if you are looking at Field's guide 'dots' that it draws for selected visual elements you might see a few more then you expect).
The methods above help you avoid a little math in the most common cases. But sometimes you need to know the position of the cursor:
.position returns the current cursor position of the line:
somePlace = myLine.position
It returns a
Vector2 object which (as you can tell from autocompletion) has all kinds of useful things for doing 2D geometry. There's much more information on the useful
Vector2 class (and its higher dimensional friends,
Everything so far has been assembling lines with the very simplest atoms: straight lines and simple curves. How about something a little more convenient?
.rect(r) — appends a free standing
Rect to the
Rect is Field's data-structure for holding a Rectangle. Therefore,
FLine().rect(Rect(50,50,20,30)) is a line with a rectangle in it, the top-left corner is at
50,50 and it's
20 wide and
.circle(radius, x, y) — appends a circle centered on
.ellipse(radius_x, radius_y, x, y appends an ellipse.
.line(x1, y1, x2, y2 — is simply a shorthand for
The code above concerns making geometry, often line-segment by line-segment. Another way of making geometry is by filtering, transforming, or editing some other piece of geometry.
Let's build up a line with a few curved segments to show some of Field's operations that work on lines as a whole. For the purposes of this example, any old curve will do:
myLine = FLine() myLine.moveTo(75,75) for n in range(0, 4): myLine.polarCubicTo(1,-1, 1, Math.random(), 50+Math.random()*150,50+Math.random()*150)
Which gives, a random-ish set of curves:
First of all, we can copy this line, to get a completely independent line that contains all the same drawing instructions:
anotherLine = myLine.copy()
These two lines
anotherLine are now independent. You can add things to each one of them and the other will not be altered in any way.
Given a line, we can translate it:
anotherLine += Vector2(30,0)
In this case 30 units to the right:
Or we can rotate it:
anotherLine += rotation(Math.PI/2)
In this case 90 degrees (a.k.a. Pi/2) clockwise:
Rotation happens around some point. In the above example it's happening around the center of bounding rectangle that surrounds the FLine(). To rotate around some other point:
anotherLine += rotation(Math.PI/2, around=Vector2(0,0))
That's a rotation around the point
0,0 — the very top left of the canvas (unless, of course, you pan around the canvas):
Sometimes it's useful to pass in a function that returns the point you want to rotate around:
def aroundSomething(aLine): return aLine.bounds().topRight() anotherLine += rotation(Math.PI/2, around=aroundSomething)
this can, of course, be more succinctly written:
anotherLine += rotation(Math.PI/2, around=lambda x : x.bounds().topRight())
Both these examples use the
.bounds() method on
FLine which returns a
Rect that is the smallest rectangle that encloses the lines. Again, in this case, autocomplete / inspection on this
Rect is the best way to go about seeing what methods you can call on it.
scale() is just like
rotation(), although here we use the
= operator (scaling things is more like multiplication, translation is more like adding):
anotherLine *= Vector2(1, 0.5) # a non-uniform squish in the y-direction anotherLine *= scale(0.5) # a uniform scale by 1/2 around the midpoint anotherLine *# scale(0.5, aroundVector2(0,0)) # a uniform scale around the origin anotherLine *# scale(0.5, aroundlambda x : x.bounds().bottomLeft()) # around the bottom left of the bounding box
It should be clear what these will do given the
rotation examples above.
Sometimes you'd like to transform the content of the line in some other way. One way of doing this is
visitPositions, which allows you to transform every point on the line with any given function. Thus:
def myFunction(x,y): return Vector2(x,y).noise(40) for n in range(0, 10): anotherLine = myLine.copy() anotherLine.visitPositions(myFunction) anotherLine.color = Color4(0,0,0,0.2) _self.lines.add(anotherLine)
Or equivalently (with the use of Python's '''lambda'''):
for n in range(0, 10): anotherLine = myLine.copy() anotherLine.visitPositions(lambda x,y : Vector2(x,y).noise(40) ) anotherLine.color = Color4(0,0,0,0.2) _self.lines.add(anotherLine)
visitPositions( lambda x,y : [...] ) applies the
unction(x,y) that you pass in to every point (and every control point, if any) along the line and replaces its position with the
Vector2 that this function returns.
The above code gets you something like:
visitPositions is very simple way of transforming a
FLine, but it treats all data-points on the line the same — be they actual points on the line, or control points; be they straight lines or curved. And the result of filtering the
Line is always a straight line if the original
Line was straight and curved if it was curved.
visitNodes gives us a little more control. Let's start with a line that has obvious sharp corners at its nodes:
myLine = FLine() myLine.moveTo(80, 80) for n in range(2, 6): myLine.polarCubicTo(1, -1.5, -0.5, 4.3, 40+n*40, 40+n*40)
There's no simple function that you can pass into
visitPositions that will smooth those kinks out, because to smooth this line you need to be able to shape the in and out cubic curve ''tangents'' at those points while keeping the actual positions along the line constant. There's no single transformation of all the points that does this.
visitNodes comes in. It takes a function that will be applied to each node in the FLine, and this function gets not just the position of the node, but the "before" and "after" points or control points (if they exist). It can modify any and all of these things by rewriting these variables. For example:
def nodeFunc(before, now, after, incomingCurve, outgoingCurve): tangent = ( (after or now)-(before or now) )*0.5 if (before): before[:] = now-tangent if (after): after[:] = now+tangent myLine.visitNodes(nodeFunc)
This sets the
after tangents to be their average.
Note that the
(after or now) construction ensures that this code works if
None which will be the case at the end of the line.
outgoingCurve are set to
after is coming from a control point or not.
visitNodes is handy for modifying lines in cartesian space, sometimes you'd like to filter the ''direction'' that lines travel in. Since this can be a tedious piece of high-school trigonometry to craft by yourself, we have:
Let's start with a circle:
myLine = FLine() myLine.circle(40, 150, 150)
visitPolar takes a function to apply to each node in the line, just like
visitNodes does. But its information is presented in a ''local polar coordinate'' system, specifically as an instance of a [source:/development/java/field/core/plugins/drawing/opengl/Polar.java PolarMove].
We can begin to unroll our circle a little:
def mover(move): move.alpha /= 1.3 myLine.visitPolar(mover)
move.alpha is the angle turned by successive segments. Set it to
0 and you'll end up with a straight line (one that wiggles because the tangents are not straight, but progresses in a straight line nonetheless):
def mover(move): move.beta1 = 0 move.beta2 = 0 myLine.visitPolar(mover)
beta2 control the angle that the incoming and outgoing tangents make with the angle that the line is heading in. Set to `` we end up with something a lot more square:
r2 control the ''length'' of the new segment and its incoming and outgoing tangents. If you scale each of these things by two, in this case you end up with a circle that's twice as big. But do something a little different:
def mover(move): move.rMain *= 2 move.r1 *= 2 move.r2 *= -4 myLine.visitPolar(mover)
and you get something like this: