Interacting in the canvas with lines, splines and shapes

Field seeks to be a hybrid environment — with a great text editor for small amounts of code, and a great drawing environment for all kinds of quick-and-dirty user interface. The kinds of UI that you can make in the canvas range hugely. Sometimes just having some boxes with code in it is enough to get you a kind of spatial "desktop" organization. Sometimes these boxes, together with a little custom drawing code and some time sliders, get you something that looks a lot like a score. Perhaps dragging the frames of these boxes, and editing the splines that they draw with the mouse, is interaction enough for what you want to do.

More traditional, and less "home made" options exist — you can migrate the sliders and pickers from the text editor to the canvas, or, Field being Java-based, you can embed anything from Swing into the canvas (or anything you've made that extends Swing).

This page describes two intermediate options:

  • Firstly, drawing lines and shapes that are themselves interactive. This can be the basis for level of prototyping UI design that's very bottom-up, very blank slate.
  • Secondly, constraining how lines can be edited in the spline editor. This can help extend the number of places where you can just use the default spline editor (that lets you drag spline nodes around) as the basis for your interaction.

An interactive line

Let's make a very minimalist button, from scratch.

In a fresh "Spline Drawer" (right click on the canvas to create one):

_self.lines.clear()
line = FLine()
line.rect(50,50,50,50)
line.derived=1
_self.lines.add(line)

Gets you:

Now, for this to be "button-ish" in any way, we'd like to have something happen when you click on it. The key line property involved is eventHandler which lets you associate a subclass of LineInteraction.EventHandler with a line.

Now, using a helpful class built into Field called Eventer, we add:

from field.core.plugins.drawing.opengl.LineInteraction import EventHandler
_self.lines.clear()
line = FLine()
line.rect(50,50,50,50)
line.derived=1
_self.lines.add(line)


class SomeHandler(LineInteraction.EventHandler):
    def __init__(self, line):
        self.on= line.copy()(filled=1, color=Vector4(1,0,0,0.15))

    def down(self, event):
    self.was = _self.lines.clone()
        _self.lines.add(self.on)
        return 1

    def up(self, event):
    _self.lines.clear()
    _self.lines.addAll(self.was)
        return 1

line.eventHandler = SomeHandler(line)

line.copy()

Now when we click down on this line, or anywhere in it, we get woo hoo displayed, and a red "button down" highlight:

Finally, to polish this up a little, we can add a separate mouse over highlight, to remind us that we've made this line interactive:

class SomeHandler(LineInteraction.EventHandler):
    def __init__(self, line):
        self.on= line.copy()(filled=1, color=Vector4(1,0,0,0.15))
        self.highlight= line.copy()(filled=1, color=Vector4(1,1,0,0.15))

    def enter(self, event):
        self.was2 = _self.lines.clone()      
        _self.lines.add(self.highlight)
        return 1

    def exit(self, event):
        _self.lines.clear()
        _self.lines.addAll(self.was2)
        return 1

    def down(self, event):
        self.was = _self.lines.clone()
        _self.lines.add(self.on)
        return 1

    def up(self, event):
        _self.lines.clear()
        _self.lines.addAll(self.was)
        return 1

The basic idea should be clear. Inside these callbacks you can modify the _self.lines property and cause the appearance of the element to change (as well as do any other processing you'd like to do); these callbacks are called in response to intersection testing the geometry that they are associated with. There are other callbacks as well, with drag(...) for example, its a cinch to make something you can drag around the canvas (see ForceDirectedSparql for and example). Callbacks work in 3d, although you might need a little more math into interpret the event — see WarpedRenderingTutorial.

So, now we're off to the races. Buttons don't have to be square:

And, here's a 'marking menu' implementation that I coded up no time at all:

Although Field now has it's own built in implementation.

Drawing a straight line

The spline editor allows you to edit splines created in code with the mouse. Edits can vary widely — from a tweak of a node position, to introducing arbitrary intermediate control nodes. Sometimes this flexibility is a little too flexible, and often you'd like to constrain the edits to maintain some feature or constraint of the line being edited — perhaps the line should only be horizontal (because you are using this line to mark a duration of time); perhaps the line should always be a rectangle no matter how you drag the nodes; perhaps some other line should be in the same relative position. Such constraints are really the matter of executing arbitrary code during and after the node is edited.

We'll take a rather long run up to this topic. Assuming that you've already wrapped your head around the Spline editor, you should recall that the basic idea is that by writing code of the form:

_self.lines.clear()

# A --- ... some code that makes lines and adds them to _self.lines ...

# introduce the opportunity to have these lines edited with the mouse
_self.tweaks()

# B --- ... any other code that perhaps adds a few more lines

... you can allow splines made in code to be edited with the mouse, and, even if (in some cases) that code changes, your mouse-based edits will be "reapplied".

Code that runs at time 'B' above happens after these edits are applied. If you want to do something in response to an edited line you have to dig the line back out of _self.lines.

You could do this:

_self.lines.clear()
somePoint = FLine().moveTo(50,50)
# ...
_self.lines.add(somePoint)
_self.tweaks()

# ...
somePosition = _self.lines[0].nodes[0].position2()
print "the position is this %s" % somePosition

if you wanted to use the spline editor to pick a position. That call to _self.lines[0] is ugly and brittle. Since attributes are preserved across the call to _self.tweaks(), you could even do this:

_self.lines.clear()
somePoint = FLine().moveTo(50,50)
somePoint.named = "somePoint"
# ...
_self.lines.add(somePoint)
_self.tweaks()

# ...
for n in _self.lines:
    if (n.named == "somePoint"):
        somePosition = n.nodes[0].position2()

print "the position is this %s" % somePosition

But that's even more ugly! Finally, if we try to constrain the position of this point:

_self.lines.clear()
_self.lines.add(FLine().moveTo(50,50).lineTo(100,100))
_self.tweaks()
somePosition = _self.lines[0].nodes[0].position2()
somePosition.y = 40
_self.lines[0].nodes[0].setPosition(*somePosition)

Then we'll find that the editor, while this line is being dragged around, is oblivious to this constraint. You can drag this point anywhere, only to have it snap back when you are done. Hardly polished interaction, and hardly good UI design.

Enter the addCode method on splines in Field:

def callback(line, event, context):
    where = event.position2()
    where.y = 50
    return where


_self.lines.clear()
line = FLine()
line.moveTo(50,50)
line.addCode(callback)
line.lineTo(100,100)

_self.lines.add(line)
# ...
_self.tweaks()
# ...

This registers (with this specific node) some code to be called whenever that line is edited. The callback can run arbitrary code, and should it choose to return a Vector2 the position of that line node gets modified (it could, of course, do this long-hand with event.position2(...), but this form allows the use of lambda).

This approach is really the jumping off point for the general drawing of constrained shapes, and, in turn, a lot of the 'notation' line approaches based in Field.