Logic

Reactive Formulas

Type it. It reacts. No callbacks, no update loops.

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:

ModePurposeOutput
formulaReactive property bindingPure expression returning a value
eventHandler body for click, collide, etc.Imperative block with read/write access
methodUser-defined reusable functionCallable 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:

  1. Track — formula evaluation records every signal read
  2. Detect — DFS cycle detection rejects circular dependencies before wiring
  3. Sort — Kahn’s topological sort determines evaluation order
  4. Propagate — a changed signal marks downstream formulas dirty
  5. 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.

GlobalTypeDescription
mouse.x, mouse.ynumberCursor position in canvas coordinates
mouse.downbooleanWhether any button is pressed
keys.ArrowUp, keys.a, …booleanAny key by name, true while held
timenumberSeconds since play started
mouse.buttonnumberWhich button (0 = left, 1 = middle, 2 = right)
dtnumberFrame delta in seconds
framenumberFrame counter
thisnodeThe node that owns this formula
parentnodeParent of the current node
TaglookupAccess tagged node groups
consoleobjectDebug output to DevTools
Math, JSONobjectStandard JS globals pass through
sin, cos, abs, min, max, round, floor, ceilfunctionMath functions available directly
clamp, lerpfunctionclamp(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:


Events

6 pointer events and 1 physics event. Each fires a compiled handler body with full formula context.

EventFires when
clickNode is clicked
mousedownPointer presses on node
mouseupPointer releases on node
mouseenterCursor enters node bounds
mouseleaveCursor exits node bounds
collidePhysics 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:

ShortcutAction
TabIndent selection
Shift+TabDedent selection
Ctrl+DDuplicate line
Ctrl+/Toggle comment
Ctrl+FSearch within code
Ctrl+ZUndo
Ctrl+Shift+ZRedo

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.

TaskRiveFlashFormo
Follow cursorState machine + input + listeneraddEventListener + onEnterFrameBall.x = mouse.x
Count objectsNot possibleArray + loop + text fieldTag.coin.length
Spin on hoverState + transition + blendMouse events + tweenmouseenter → this.spin()
Chain reactionsMultiple state machinesEvent dispatch chainAuto dependency graph

Start Building

Open the editor. Click a shape. Type a formula. Press play. It reacts.

Open Formo →