Write an expression — it becomes live. Ball.x = mouse.x and the ball follows your cursor. No event listeners. No update loops. No state management. One line, and the relationship holds forever.
Ball.x = mouse.x // follows cursor
Score.text = Tag.coin.length // counts remaining coins
this.rotation = time * 90 // spins 90°/sec
this.y = sin(time) * 100 // oscillates vertically
Every formula is a reactive binding. Change a dependency — every downstream formula re-evaluates automatically, in the correct order, in a single batch.
Ball follows cursor
One formula makes a shape track the mouse in real time
Acorn-Based Compiler
The formula compiler parses your expression with acorn, walks the AST with estree-walker, and transforms node references into reactive lookups. Star.width becomes a tracked signal read. Star.width = 200 becomes a tracked write. Node names auto-resolve — rename the node, the formula updates.
3 compile modes handle every context:
| Mode | Purpose | Output |
|---|---|---|
| formula | Reactive property binding | Pure expression returning a value |
| event | Handler body for click, collide, etc. | Imperative block with read/write access |
| method | User-defined reusable function | Callable from formulas and events |
Source maps attach to every compiled function. Open DevTools, set a breakpoint, step through your formula.
Signal Graph
Formulas form a dependency graph. The runtime auto-tracks which signals each formula reads, builds a topological order using Kahn’s algorithm, and evaluates dirty nodes in a single batch.
The pipeline:
- Track — formula evaluation records every signal read
- Detect — DFS cycle detection rejects circular dependencies before wiring
- Sort — Kahn’s topological sort determines evaluation order
- Propagate — a changed signal marks downstream formulas dirty
- Batch — all dirty formulas evaluate once per microtask, in sorted order
No double evaluations. No glitches. A formula reading 3 dependencies that all change in the same frame still evaluates exactly once.
Chained dependencies
Three formulas form a reactive chain — change one, all downstream update
14 Built-In Globals
Every formula has access to runtime state without imports or setup.
| Global | Type | Description |
|---|---|---|
mouse.x, mouse.y | number | Cursor position in canvas coordinates |
mouse.down | boolean | Whether any button is pressed |
keys.ArrowUp, keys.a, … | boolean | Any key by name, true while held |
time | number | Seconds since play started |
mouse.button | number | Which button (0 = left, 1 = middle, 2 = right) |
dt | number | Frame delta in seconds |
frame | number | Frame counter |
this | node | The node that owns this formula |
parent | node | Parent of the current node |
Tag | lookup | Access tagged node groups |
console | object | Debug output to DevTools |
Math, JSON | object | Standard JS globals pass through |
sin, cos, abs, min, max, round, floor, ceil | function | Math functions available directly |
clamp, lerp | function | clamp(val, lo, hi) and lerp(a, b, t) |
Syntax Highlighting
The editor tokenizes your formula in real time and highlights 8 token types: keywords, numbers, strings, node references, properties, builtins, operators, and comments.
Node references get per-ID colors from an 8-color palette. Ball is blue. Platform is red. Score is yellow. Same node, same color, everywhere in the formula. Brace matching highlights the paired bracket as you type.
The highlight system uses CSS Custom Highlight API — no DOM manipulation, no overlays, no performance cost.
Context-Aware Autocomplete
Type a node name — completions appear. Type a dot — the node’s properties fill the dropdown. The autocomplete system knows the context:
- After dot on a node: all properties for that node type (x, y, w, h, rotation, opacity, fill, …)
- After
mouse.: x, y, down, button - After
keys.: any key name - After
Tag.: all tag names in the document - After
event.: properties for the current event type (target, normal, point for collide) - Bare identifier: node names, builtins (sin, cos, clamp, lerp), specials (this, parent, mouse, time, dt, frame, keys, Tag)
- Physics proxy: velocity, angularVelocity, applyForce(), applyImpulse() after physics-enabled nodes
Events
6 pointer events and 1 physics event. Each fires a compiled handler body with full formula context.
| Event | Fires when |
|---|---|
click | Node is clicked |
mousedown | Pointer presses on node |
mouseup | Pointer releases on node |
mouseenter | Cursor enters node bounds |
mouseleave | Cursor exits node bounds |
collide | Physics body contacts another body |
Collision events provide event.target (the other node), event.normal, event.point, and collider names. Write game logic directly:
if (event.target.is("brick")) {
event.target.destroy()
}
Every event handler re-evaluates the formula graph after executing — side effects propagate immediately.
Methods
Define named functions on any node. Call them from events, formulas, or other methods.
// Method "reset" on Player:
this.x = 200
this.y = 100
this.velocity.x = 0
this.velocity.y = 0
// Event "click" on ResetButton:
Player.reset()
Methods compile in method mode — same compiler, same node reference resolution, same source maps. Reuse logic without duplication.
Tag Groups
Tag nodes with names. Access groups reactively in formulas.
Tag.brick // → all nodes tagged "brick"
Tag.brick.length // → count, updates when bricks are destroyed
Tag.coin.length // → remaining coins
Tags are reactive. Destroy a tagged node — Tag.brick.length decrements. Add a new one — it increments. Build counters, win conditions, and group behaviors with one expression.
Score tracks remaining targets
Tag.target.length counts tagged nodes — destroy one, the score updates
Inline Math
Number fields accept arithmetic expressions. Type 200 + 50% in a width field — it evaluates to 300 (50% of the current 200, added to 200). Supports +, -, *, /, parentheses, % (relative to current value), and px units. No formula setup needed — just type and press Enter.
Code Editor
Events and methods open in a multiline code editor with line numbers, a breakpoint gutter, and search.
Keyboard shortcuts:
| Shortcut | Action |
|---|---|
| Tab | Indent selection |
| Shift+Tab | Dedent selection |
| Ctrl+D | Duplicate line |
| Ctrl+/ | Toggle comment |
| Ctrl+F | Search within code |
| Ctrl+Z | Undo |
| Ctrl+Shift+Z | Redo |
The code editor wraps the same formula editor — same syntax highlighting, same autocomplete, same per-node-ID colors. Draggable dialog, resizable, docked to the canvas.
Standalone Export
The formula emitter generates standalone JavaScript from your formula graph. Evaluation functions, topological ordering, $on() subscription wiring, batch updates, and teardown — all in one output file. No editor dependency. No runtime library. Ship reactive vector content anywhere.
Not State Machines
Rive uses visual state machines. A ball following the cursor requires a state, a transition, an input binding, a listener, and a blend tree. In Formo: Ball.x = mouse.x. One line.
Flash used ActionScript — a full programming language with classes, imports, and event dispatchers. Too much machinery for simple interactivity.
Formo formulas sit in the middle: more powerful than visual wiring, simpler than a programming language. Write the relationship. The system handles the rest.
| Task | Rive | Flash | Formo |
|---|---|---|---|
| Follow cursor | State machine + input + listener | addEventListener + onEnterFrame | Ball.x = mouse.x |
| Count objects | Not possible | Array + loop + text field | Tag.coin.length |
| Spin on hover | State + transition + blend | Mouse events + tween | mouseenter → this.spin() |
| Chain reactions | Multiple state machines | Event dispatch chain | Auto dependency graph |
Start Building
Open the editor. Click a shape. Type a formula. Press play. It reacts.