For as long as the Field project has been going we've been keeping an eye on NodeBox. Not only is it a hybrid visual programming environment for making 2d vector & bitmapped images, but it uses Python as its programming language. Field's 2d drawing tools we're started before we heard of NodeBox, but our image filtering extensions we're directly provoked and inspired by that project. But NodeBox's 2d spline code has always been well developed, well tested and well documented — so much so that we'd been thinking about ways of hacking into it to let Field users use that API as well as "ours". We like our API's but we don't think they are the last word on 2d drawing by any means.
It was going to be pretty tedious, because while Python-based, NodeBox was very much "non-pure" Python — much of it was a wrapper around Quartz (Apple's native drawing code). Thankfully, we didn't bother figuring any of this out, because the latest beta version of NodeBox has taken a very interesting and unexpected twist — it's now a Java/Jython project (just like Field) with an all new, even more hybrid (and even more interesting) interface.
Let's assume that that NodeBox's spline code remains as well developed, tested and documented as it has been even as their team goes through this complete rewrite. This can only mean one thing — it really is time to get Field's hooks into NodeBox and start tracking their beta releases.
First, let's download the beta of NodeBox from here. We're going to use the "text_fx.ndbx" example:
You have two options — you can build NodeBox from source, or you can just download one of their binaries. Building from source gets you documentation inside Field's autocomplete, but otherwise it's the same functionality.
To make the connection to Field very easy first build NodeBox with this command:
ant build dist-mac
This will give you a nodeBox.jar
file in 'dist/lib'. Using the Plugin manager in Field "add jar/directory to classpath". While you are there, why not add 'src' to Field's source code search path as well (this will give you great auto-complete and comments on NodeBox's methods):
(that's what you get when you hit command-period after typing Path().
).
Most straightforward is to just download a NodeBox.app from their site. Then, place this file inside the extensions directory of Field (that's probably /Applications/field.app/Contents/extensions). Restart Field.
In both cases you'll now be able to write and execute this code:
from nodebox.graphics import *
print Path()
without any error.
After a few minuites of poking around NodeBox's inside using autocomplete and inspection we realize that NodeBox makes heavy use of Java 2D. This means that this code works:
# this imports the nodebox libraries that we need
from nodebox.graphics import *
_self.lines.clear()
# this is pure nodebox
np = Path()
np.ellipse(10,10,10,10)
# and this takes a nodebox path and turns it into a Field FLine
line = FLine().appendShape(np.getGeneralPath())
_self.lines.add(line)
.appendShape(...)
appends a Java 2D General Path to a Field FLine
. The rest of the _self.lines
code should be familiar from the FLine drawing tutorials. And see below for more ways of moving between FLine
and Path
)
Now you can originate geometry using NodeBox routines and then integrate them with the FLine
based drawing code — pretty cool.
Well, that was easy (really, with auto-completion and 'print' it took all of 5 mins). But hacking into NodeBox at this level misses a lot of the interesting part of their libraries — in particular it misses the compositional nature of their "network". While we have mixed feelings about node-based editing interfaces we certainly like node-based abstraction.
Fortunately NodeBox's architecture and design means that we can just dig right into the good stuff. Let's "rewrite" the text_scatter.ndbx
example in Field/NodeBox, box by box.
As we can see from the first screenshot above we are looking at a composition consisting of four boxes (look at the bottom right panel for the network). One just makes an ellipse, that ought to be easy — in fact we can just look at the code in the bottom right panel.
In NodeBox we have:
from nodebox.graphics import Path
def cook(self):
p = Path()
p.ellipse(self.x, self.y, self.width, self.height)
p.fillColor = self.fill
if self.strokeWidth > 0:
p.strokeColor = self.stroke
p.strokeWidth = self.strokeWidth
else:
p.strokeColor = None
return p.asGeometry()
In Field we'll strip out the optional things and have something like:
from nodebox.graphics import *
def ellipse():
p = Path()
p.ellipse(0,0,10,10)
p.fillColor = Color(0,0.1,0.2,0.15)
p.strokeColor = Color(0,0.3,0.2,0.1)
return p.asGeometry()
For the "text path" node, NodeBox gives:
from nodebox.graphics import Path, Text
def cook(self):
t = Text(self.text, self.x, self.y, self.width, self.height)
t.fontName = self.font
t.fontSize = self.size
# valueOf requires a correct value: LEFT, CENTER, RIGHT or JUSTIFY. Anything else will
# make it crash. If users start doing crazy things and change the alignment, at least
# make sure you catch the error.
try:
t.align = Text.Align.valueOf(self.align)
except:
pass
p = t.path
p.fillColor = self.fill
if self.strokeWidth > 0:
p.strokeColor = self.stroke
p.strokeWidth = self.strokeWidth
else:
p.strokeColor = None
return p.asGeometry()
Again, stripped of some of the options we'll add:
def text():
t = Text("Field", 0, 0, 0,0)
t.fontName = "Verdana"
t.fontSize = 200
t.align = Text.Align.LEFT
return t.path.asGeometry()
Scatter is pretty interesting — it samples random points inside a shape. In NodeBox:
from nodebox.graphics import Path, Point
from random import seed, uniform
def cook(self):
seed(self.seed)
if self.shape is None: return None
bx, by, bw, bh = list(self.shape.bounds)
p = Path()
for i in xrange(self.amount):
tries = 100
while tries > 0:
pt = Point(bx + uniform(0, 1) * bw, by + uniform(0, 1) * bh)
if self.shape.contains(pt):
break
tries -= 1
if tries:
p.moveto(pt.x, pt.y)
else:
pass # add warning: no points found on the path
return p.asGeometry()
In Field:
def scatter(shape):
p = Path()
bx, by, bw, bh = list(shape.bounds)
for i in xrange(300):
tries = 100
while tries > 0:
pt = Point(bx + uniform(0, 1) * bw, by + uniform(0, 1) * bh)
if shape.contains(pt):
break
tries -= 1
if tries:
p.moveto(pt.x, pt.y)
else:
pass
return p.asGeometry()
What we're mainly doing here is converting def xxx(self): self.blah ...
— where 'self' is some magical NodeBox object/convention into a more straightforward and transparent function call def xxx(blah):
.
Finally the node that takes the ellipse and the scattered text path and puts them together. In NodeBox:
from nodebox.node import StampExpression
from nodebox.graphics import Path, Geometry, Transform
def cook(self):
if self.shape is None: return None
if self.template is None: return self.shape.clone()
if self.stamp:
stamp = StampExpression(self.node, "expr")
g = Geometry()
for i, point in enumerate(self.template.points):
if self.stamp:
context.put('CNUM', i)
stamp.evaluate(context)
self.node.stampExpressions(context)
t = Transform()
t.translate(point.x, point.y)
newShape = t.map(self.shape)
g.extend(newShape)
return g
And in Field — ignoring the Stamp expression context stuff (see below):
def stamp(shape, template):
g = Geometry()
for i, point in enumerate(template.points):
t = Transform()
t.translate(point.x, point.y)
newShape = t.map(shape)
g.extend(newShape)
return g
Now we need one more thing in Field — we actually need to call these functions to make some geometry. In the Beta 10 or later we can just do this:
_self.lines.clear()
_self.lines <<= stamp(ellipse(), scatter(text()))
And we'll get something like:
Voila! The heart of NodeBox inside Field. You can see in the screen shot that we've been unable to resist putting in a couple of embedded sliders directly into our code. Now, whether you like prefer typing stamp(ellipse(), scatter(text()))
or connecting boxes together is up to you...
One of the things that attracted us to NodeBox is that, unlike Field, it actually has a useable SVG importer. Rather than improving ours, why not borrow theirs? Sure enough:
from nodebox.graphics import *
import svg
_self.lines.clear()
f = file("/Users/marc/Downloads//iconic/vector/spin.svg", 'r')
s = f.read()
f.close()
g = Geometry()
paths = svg.parse(s, True)
for path in paths:
g.add(path)
_self.lines <<= g
... works wonderfully. Better yet, this code is, like the example above, ripped right out of the NodeBox "import" node source visible right inside the NodeBox UI itself.
Because it's short, let's look at that example line by line:
from nodebox.graphics import *
— NodeBox defines the bulk of it's Java classes in the nodebox.graphics package. We just import them wholesale into Field with this statement.import svg
— it also defines a (Python-based) svg parser in the package 'svg'. We can see this in the NodeBox environment itself. (Some version of Jython throw a spurious exception at this point, you can ignore it)._self.lines.clear()
— this line should be very familiar to you if you've done any work with Field's 2d drawing system. It clears the list of lines that this box is associated with. If you miss this line out then every time you execute this box, you'll add more and more geometry to it.f = file( ... )
— the next three lines are pure Python — they read the contents of the svg file into a string. I didn't even write these, this is the code that NodeBox uses in the 'importer' node.g = Geometry()
} — NodeBox uses Geometry
instances to hold multiple Path
instances.paths = svg.parse(s, True)
} — actually digs the paths out of the svg string._self.lines <<= g
— tells Field to add to _self.lines
the contents of the Geometry
instance g
and thus draw it.NodeBox Path
objects and Field FLine
objects are solving very similar problems, holding almost identical kinds of data (geometry and style information). Obviously, we prefer Field's but going between them is quite straightforward. But popular demand, some helper extensions to FLine
have just been added to make this pretty effortless. We can take the above example and turn this into:
from nodebox.graphics import *
import svg
f = file("/Users/marc/Downloads/iconic/vector/spin.svg", 'r')
s = f.read()
f.close()
g = Geometry()
paths = svg.parse(s, True)
_self.lines.clear()
for path in paths:
line = FLine()
line.appendShape(path.getGeneralPath())
line.filterPositions2(lambda x,y : Vector2(x,y).noise(3))
line.fillColor = Color4(0,0,0,0.2)
line.strokeColor = Color4(1,0,0,0.2)
line.filterPositions2(lambda x,y : Vector2(Math.sin(x*0.04)*200+300, Math.sin(y*0.04)*200+300))
line.derived=1
_self.lines.add(line)
That imports our svg file and noises it up, modifies the fill and stroke color and then does some strange non-linear coordinate system transformation to it before sticking it on the screen. The key new call is the FLine(path)
constructor — which makes a FLine
from a NodeBox Path
. FLine().appendShape(path.getGeneralPath())
also does exactly what you'd expect — produces a FLine
with the contents of some path
instance.
What's missing? Well, probably all kinds of things. We haven't touched their context / embedded expression framework — partly because we don't understand it, partly because we're not sure we really need it. and partly because we are a little skeptical about how it scales (you should let the first of those statements carry the most weight). We'll continue to poke around the insides of NodeBox2 as it develops.
Also, being able to directly import and instantiate NodeBox nodes in Field seems like it would be fun and easy.
In any case we're very pleased to have NodeBox join the Java/Jython party.