This page is best viewed on a desktop browser.
Bloom: Optimization-Driven Interactive Diagramming
We're excited to announce Bloom, an open-source JavaScript library for optimization-driven interactive diagram creation. Bloom makes it simple to describe complex, dynamic behavior using a rich vocabulary of optimization constraints and the declarative language behind Penrose. We aim to facilitate the creation of engaging, explorable explanations with a straightforward but powerful framework. Let's check out some examples!
Examples
Below, we've placed some balls in a circle. Try dragging one around to see how the others respond:
Notice how the circles push each other around? With Bloom, all you need to do is specify that the circles shouldn't overlap:
forall({ c1: Circle, c2: Circle }, ({ c1, c2 }) => {
ensure(constraints.disjoint(c1.icon, c2.icon));
});
The next diagram reflects a ray off a mirror from one point to another. To be physically realistic, the angle between the incoming ray and mirror must be the same as the angle between the outgoing ray and the mirror. Try dragging the start and endpoints around, and watch how this property is maintained:
How might you implement this? With Bloom, there's no need to calculate the exact point at which a reflected ray keeps these two angles equal. Instead, you can leave it to Bloom's optimizer:
const r1y = ray1.normVec[1];
const r2y = ray2.normVec[1];
ensure(constraints.equal(r1y, mul(-1, r2y)));
Bloom also provides an interface for your diagrams to communicate with the rest of your site. In the next diagram, you'll find:
- two vectors, and
- a point
- a point , connected to by a dotted line
Try dragging the gray handles to see how the transformation changes, along with the vectors and matrices on the right.
If you're familiar with a little linear algebra, you'll notice that the vectors and form the columns of matrix , which is applied to to from . The eigenspaces of are shown as lines and . All of this data, calculated and stored internal to the diagram, is easily synced to the LaTeX on the right using Bloom's system of shared diagram values and custom hooks.
Here's another example, integrating a button to add a square on every click:
Optimization-Driven Diagramming
We believe that the most natural way to express complex interactive behavior is through optimization. Let's take another look at our first example:
The elements of this diagram include:
- A circular enclosure
- A set of 10 draggable circles, which cannot overlap and cannot exit the enclosure
As we saw before, ensuring that the circles do not overlap is as simple as specifying a single optimization constraint for each pair of circles:
forall({ c1: Circle, c2: Circle }, ({ c1, c2 }) => {
ensure(constraints.disjoint(c1.icon, c2.icon));
});
Without optimization, implementing this behavior would be a nasty challenge. At minimum, you'd need to:
- Set up event handlers to translate circles on drag
- Detect collisions between circles
- Implement a physics engine to resolve collisions continuously
Moreover, the simplicity of the constraint-based approach allows for easy modification. Suppose we instead wanted for these balls to repel each other within a certain padding:
forall({ c1: Circle, c2: Circle }, ({ c1, c2 }) => {
ensure(constraints.disjoint(c1.icon, c2.icon, 20));
});
Or even that they should all touch the enclosure:
forall({ c: Circle }, ({ c }) => {
ensure(
constraints.equal(
ops.vnorm(c.icon.center),
sub(enclosure.icon.r, circleRad),
),
);
});
forall({ c1: Circle, c2: Circle }, ({ c1, c2 }) => {
ensure(constraints.disjoint(c1.icon, c2.icon, 20));
});
Neither change requires a substantial rewrite of the diagram's behavior; instead, we can simply modify the constraints to reflect our new requirements.
Getting Started
Bloom is still in the early stages of development, but we're excited to share it with you. If you're interested in learning more, you can take a look at our tutorial. We're excited to see what you build!