- 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-widthto4. - Change fish’s
fillto"blue". - Add hello’s
touchingproperty with this formula, which returns a list of all the objects that are touching the word “hello”:
(query/intersects me)
- Add hello’s
touching-fishiesproperty 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-fishproperty with the valuetrue. - Now the fish shows up in hello’s
touching-fishies. - When I toggle fish’s
is-fishon/off, hello’stouching-fishiesproperty 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"'scostumeproperty 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/normalizedproperty to this formula, which means "thepath/normalizedproperty of mycostume. This makes the object look like the up arrow.
(? (? me "costume") "path/normalized")
- Now change
"obj5"'scostumeto 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
touchingproperty, whose value is a list of the objects that are touching the boot:
(query/overlaps me)
- Add boot’s
kickproperty 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
infectionsproperty 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
infectedproperty 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-truewith 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.