Reactive Signals
How formulas know when to update
When you write Ball.x = mouse.x, the formula system creates a dependency: Ball.x depends on mouse.x. When the mouse moves, Ball.x re-evaluates. This page explains the signal graph that makes it work.
The signal primitive
Every reactive value is a signal. Signals have:
- A current value
- A version number (incremented on change)
- A list of dependents (signals that read this signal)
When you read a signal during formula evaluation, the system records the dependency automatically. No manual subscription needed.
title: "Chain reaction"
description: "Three shapes linked by formulas — move one, all update"
nodes:
- type: ELLIPSE, name: "Leader", x: 200, y: 200, w: 60, h: 60, fill: "#E74C3C"
- type: ELLIPSE, name: "Follower1", w: 50, h: 50, fill: "#3498DB"
- type: ELLIPSE, name: "Follower2", w: 40, h: 40, fill: "#2ECC71"
formulas:
- node: "Follower1", prop: "x", value: "Leader.x + 80"
- node: "Follower1", prop: "y", value: "Leader.y"
- node: "Follower2", prop: "x", value: "Follower1.x + 70"
- node: "Follower2", prop: "y", value: "Follower1.y"
tools_visible: [select, play]
ui_visible: [properties, formulas]Evaluation order
Formulas form a directed acyclic graph (DAG). The system uses Kahn's algorithm (topological sort) to determine evaluation order. If A depends on B, B evaluates first.
mouse.x → Ball.x → Shadow.x → Score.text ↘ Ball.y → Shadow.y
Every formula evaluates exactly once per frame, in the correct order.
Cycle detection
If Ball.x depends on Wall.x and Wall.x depends on Ball.x, that's a cycle. The system uses depth-first search to detect cycles and reports them before evaluation. Circular dependencies are caught at compile time, not at runtime.
Dirty propagation
When a signal changes, its dependents are marked dirty. Only dirty signals re-evaluate. If mouse.x changes but mouse.y doesn't, formulas depending only on mouse.y stay clean.
Batch evaluation
Multiple signals can change in one frame (mouse.x and mouse.y move together). The system batches all changes and evaluates the entire dirty set once — no redundant intermediate states.
Version counting
Each signal has a version number. Dependencies track which version they last read. If the version hasn't changed, the dependent skips re-evaluation. This prevents unnecessary work when a formula evaluates to the same result.
Performance
The signal system is designed for the hot path:
- Zero allocation — preallocated scratch buffers, no GC pressure
- Prototype-inherited context — formula context uses JS prototype chain instead of object spread
- Scalar accessors — VN data accessed via typed array indices, not object properties
- CSS paint caching —
rgbaToCSSresults cached to avoid string allocation