- title
- Inkbase demos
- dated
- Fri. March 8, 2024
I prepared the following demos for a quick tour of Inkbase at our friday Show & Tell.
Hello Fishy
This one introduces ink strokes and the property editor.
Play by play:
- Write “hello”.
- Draw a fish.
- Change hello’s
line-width
to4
. - Change fish’s
fill
to"blue"
. - Add hello’s
touching
property with this formula, which returns a list of all the objects that are touching the word “hello”:
(query/intersects me)
- Add hello’s
touching-fishies
property with this formula, which evaluates to only the fishes that aretouching
“hello”:
(filter (fn (x) (? x "is-fish")) (? me "touching"))
- This doesn’t work yet because the fish doesn’t know that it’s a fish yet. We have to add fish’s
is-fish
property with the valuetrue
. - Now the fish shows up in hello’s
touching-fishies
. - When I toggle fish’s
is-fish
on/off, hello’stouching-fishies
property is re-evaluated automatically.
Costumes
This one continues to illustrate Inkbase’s reactive dataflow thing.
Play by play:
- Draw up and down arrows. They’re given object ids
"obj1"
and"obj2"
, respectively. - Duplicate the up arrow (we get a new object with id
"obj5"
), and move it to the top of the page. - Add
"obj5"
'scostume
property with a value of"obj1"
. This is how I’m saying that I want that object to look like the up arrow — but it doesn’t work yet, because that property is not hooked up to anything. - Change
"obj5"
'spath/normalized
property to this formula, which means "thepath/normalized
property of mycostume
. This makes the object look like the up arrow.
(? (? me "costume") "path/normalized")
- Now change
"obj5"
'scostume
to this formula, whose value changes dynamically as a function of its position:
(if (< 500 (? me "bbox/y")) "obj1" "obj2")
This Demo Kicks… Rocks
This one has a formula with side effects.
Play by play:
- Draw a boot and some rocks.
- Add boot’s
touching
property, whose value is a list of the objects that are touching the boot:
(query/overlaps me)
- Add boot’s
kick
property with the following formula:
(each (? me "touching")
(fn (obj)
(! obj "bbox/x"
(+ (? obj "bbox/x") 10))))
- Now you can drag the boot around, and it will kick the rocks.
More Realistic Infections
The original infection demo in this doc didn’t have transitivity, i.e., only “patient zero” could infect other objects. This version does infections with transitivity. Also, unlike the other examples, this one is done in 3rd person (as a propety of the page, or as a field if you prefer) instead of as a property of the infected objects themselves, which I could call 1st person / object-oriented.
Play by play:
- Add page’s
infections
property with the following formula:
(each (? me "members")
(fn (obj)
(if (? obj "infected")
(do
(! obj "fill" "yellow")
(each (query/intersects obj)
(fn (otherObj) (! otherObj "infected" true)))))))
- Draw a bunch of objects.
- Add an
infected
property to one of them, with the valuetrue
. - Now you can drag that guy around and infect other objects.
- You can also pick up any infected object and infect other objects.
Logic Circuits
The Inkbase essay referenced a logic ciruits kit that I made in Etoys, a programming language for kids. They implemented something similar but with a very different feel. I wanted to try this myself, this time with wires that you can draw with the pencil and whose colors change to show whether they are carrying a value of true or false.
I’m really happy with how this one came out, but there were some hairy bits that I’ll discuss below.
Play by play:
- I started by drawing a circle that was going to act as a “source of truth”. I made its
fill
"yellow"
and added a property calledis-true
with a value oftrue
. - Then I set out to add the behavior of wires. For the sake of simplicity, I chose to have any ink stroke be a wire. I did this by adding a property to the page object (the name doesn’t matter) with the following formula:
(on "draw-started" me
(fn (obj)
(! obj "line-width" 4)
(: obj "connections"
(let (visit (fn (connections x)
(if (set/has? x connections)
connections
(reduce
visit
(set/add x connections)
(query/intersects x)))))
(set/remove me (visit [] me))))
(: obj "is-on"
(reduce
(fn (x y) (or x y))
false
(map (fn (x) (? x "is-true")) (? me "connections"))))
(: obj "stroke"
(if (? me "is-on") "yellow" "black"))))
This property uses the draw-started
event, which happens every time the user starts to draw an ink stroke. You provide an event handler function whose argument is the new ink stroke object. What I’m doing here is “installing” a bunch of properties that I want every wire to have. The first one is line-width
, which I set to 4
to make it easy to see the wires.
The second property is connections
, whose value is the set of wires that are reachable from this wire. (me
is like this
in Java/C++/etc.) It’s written in a purely functional way, using a helper function called visit
.
Next is the is-on
property. It says "I’m on if one of the things that I am (transitively) connected to is true. Note that wires are never true (i.e., w
is a wire ⇒ (not (is-true w))
). It’s only the “source of truth” circle (and its clones) that have this is-true
property. Wires have is-on
, which is computed dynamically. (This distinction is necessary because in my model, wires can conduct value in any direction.)
Last but not least, the wire’s stroke
color is yellow when it’s on, and black when it’s not.
So that’s how wires work.
And here’s how the gates work. I’ll use or as an example, but the approach is exactly the same for and and for not.
The two inputs of the or gate shown above are the objects with ids "obj17"
and "obj18"
. I added an is-true
property to each of them that works just like a wire’s is-on
property.
The output of this or gate is "obj21"
. It also has an is-true
property whose value is true if either of its inputs is-true
. The fact that the output of the logic gate is called is-true
enables it to conditionally mimic a true as far as downstream wires are concerned.
There is just one last thing that was necessary to make this work: I had to create a group that included only the inputs of the logic gate. When an object is in a group, it can detect overlaps, intersections, etc. with the objects outside its group, but not the other way around. (From the outside, the queries’ result sets include the group instead of its members. This asymetry is useful here because Inkbase’s evaluator cannot handle cycles.