Formulas API

Write an expression. It becomes live code.

Try it

title: "Spinning shape follows cursor"
description: "Two formulas bind x to the mouse and rotation to time"
nodes:
  - type: STAR, name: "Star", x: 250, y: 250, w: 80, h: 80, fill: "#F39C12", points: 5
formulas:
  - node: "Star", prop: "x", value: "mouse.x"
  - node: "Star", prop: "y", value: "mouse.y"
  - node: "Star", prop: "rotation", value: "time * 120"
tools_visible: [select, play]
ui_visible: [properties, formulas]

What is a formula

A formula is a JavaScript expression bound to a node property. Type = in any number field, write an expression, press Enter. The property now evaluates that expression every frame. Change a dependency and the formula re-evaluates automatically.

Ball.x = mouse.x // follows cursor
this.rotation = time * 90 // spins 90 degrees per second
this.opacity = keys.Space ? 1 : 0.3 // dim unless Space is held

Formulas are standard JavaScript. The compiler adds automatic node resolution and dependency tracking on top. You do not import anything, register listeners, or manage state. Write the relationship and the system maintains it.

Formula syntax

Node references

Reference any node by its scene name. Access properties with dot notation.

Ball.x // read Ball's x position
Ball.width // read Ball's width
parent.rotation // read parent's rotation
this.width // read this node's opacity
Tag.brick.length // count of nodes tagged "brick"
Tag.coin[0].x // x position of the first "coin" node

ReferenceResolves to
NodeName.propAny node by its scene name
this.propThe node that owns this formula
parent.propThe parent node in the scene tree
Tag.nameArray of all nodes with that tag
Tag.name.lengthCount of tagged nodes (reactive)
Tag.name[i].propProperty of a specific tagged node

Operators

CategoryOperatorsExample
Arithmetic+ - * / %this.x + 100
Comparison== != < > <= >=Ball.x > 400
Logical&& `\\ !`keys.Space && mouse.down
Ternary? :Ball.y < 0 ? 0 : Ball.y
Assignment (events only)= += -= *= /= %=this.x += 10
Update (events only)++ --this.score++

Formulas (property bindings) are pure expressions that return a value. They cannot contain assignments. Events and methods can use assignments, compound assignments, and update operators.

Built-in globals

Every formula, event, and method has access to these without import or setup.

Runtime state

GlobalTypeDescription
mouse.xnumberCursor x in canvas coordinates
mouse.ynumberCursor y in canvas coordinates
mouse.downbooleanWhether any pointer button is pressed
mouse.buttonnumberWhich button (0 = left, 1 = middle, 2 = right)
keys.ArrowLeftbooleanTrue while ArrowLeft is held (any key by name)
keys.SpacebooleanTrue while Space is held
keys.*booleanAny key name: keys.a, keys.Shift, keys.Enter, ...
timenumberSeconds since play started
dtnumberFrame delta in seconds (~0.016 at 60fps)
framenumberFrame counter (integer, increments each frame)
TaglookupAccess tagged node groups: Tag.brick, Tag.coin

Math functions (available directly)

FunctionSignatureDescription
sin(x)number -> numberSine (radians)
cos(x)number -> numberCosine (radians)
abs(x)number -> numberAbsolute value
min(a, b, ...)...number -> numberMinimum
max(a, b, ...)...number -> numberMaximum
round(x)number -> numberRound to nearest integer
floor(x)number -> numberRound down
ceil(x)number -> numberRound up
clamp(val, lo, hi)(number, number, number) -> numberClamp to range
lerp(a, b, t)(number, number, number) -> numberLinear interpolation

Also available: PI, E, Math (the full Math object), Infinity, NaN.

JavaScript pass-through globals

These standard JavaScript globals work unchanged inside formulas:

CategoryGlobals
DataJSON, Number, String, Array, Object, Boolean, Date, RegExp, Map, Set
ParsingparseInt, parseFloat, isNaN, isFinite
TimerssetTimeout, setInterval, clearTimeout, clearInterval
Debugconsole

Compile modes

The compiler has three modes. Each handles a different context.

ModeInputOutputUsed for
formulaBall.x + 100Pure expression returning a valueProperty bindings
eventthis.x += 10; event.target.destroy()Statement block with read/write accessEvent handlers (click, collide, ...)
methodthis.x = 0; this.y = 0Callable function on a nodeUser-defined reusable methods

Formula mode parses a single expression. You cannot assign, declare variables, or use statements. The result is the property's value.

Event mode parses a statement block. You can assign properties, call methods, use if/else, loops, and local variables. Event handlers receive an event object with event-specific data.

Method mode also parses a statement block. Methods are defined on a node and called from events or formulas: this.reset() or Ball.explode().

How the compiler works

The pipeline: Source text -> Acorn parse -> AST transform -> Astring codegen -> Compiled function

The AST transform turns node references into tracked lookups:

Ball.x -> ctx.resolve('Ball', 'x')
this.width -> this.getW()
Ball.x = 200 -> ctx.set('Ball', 'x', 200)
Ball.reset() -> ctx.callMethod('Ball', 'reset')

Formula context

The compiled function receives a context object with these methods:

MethodSignatureDescription
resolve(nodeId, prop)(string, string) -> anyRead a node property (registers dependency)
set(nodeId, prop, value)(string, string, any) -> voidWrite a node property
callMethod(nodeId, methodName)(string, string) -> anyCall a user-defined method on a node
getNode(nodeId)(string) -> VNodeGet the raw node object (for builtins like .is(), .destroy())

Node builtins is(typeOrTag) and destroy() call directly on the node object, not through callMethod.

Dependency tracking

Dependencies are tracked automatically. When a formula evaluates, the system records every signal it reads. No manual subscription is needed.

  1. Compile-time extraction -- the compiler walks the AST and collects all resolve() calls as static dependencies
  2. Runtime tracking -- Signal.startTracking() records every .get() call during evaluation
  3. Dirty propagation -- when a signal changes, all dependents are marked dirty

Evaluation order

Formulas form a directed acyclic graph (DAG). The runtime sorts them with Kahn's algorithm (topological sort):

  1. Count incoming edges (dependencies) for each formula
  2. Start with formulas that have zero dependencies
  3. Evaluate, then decrement dependents' counts
  4. Repeat until all formulas are evaluated

If A depends on B, B always evaluates first. Every formula evaluates exactly once per frame.

Cycle detection

Before wiring a new formula, the graph runs DFS cycle detection. If Ball.x depends on Wall.x and Wall.x depends on Ball.x, the formula is rejected with an error. Cycles are caught at compile time, never at runtime.

Batch evaluation

Multiple signals can change in one frame. Mouse x and y move together. A physics step updates many bodies at once. The system batches all changes and evaluates the entire dirty set once per microtask.

batchDepth++
signal1.set(...) // marks dependents dirty, defers flush
signal2.set(...) // marks more dependents dirty, still deferred
batchDepth--
flush() // evaluates all dirty formulas in topo order, once

No double evaluations. A formula reading 3 dependencies that all change in the same frame evaluates exactly once.

title: "Chained dependencies"
description: "Three shapes form a reactive chain -- move the mouse, all update in one pass"
nodes:
  - type: RECT, name: "Source", x: 100, y: 200, w: 80, h: 80, fill: "#4285F4"
  - type: RECT, name: "Middle", x: 250, y: 200, w: 80, h: 80, fill: "#34A853"
  - type: RECT, name: "Output", x: 400, y: 200, w: 80, h: 80, fill: "#EA4335"
formulas:
  - node: "Source", prop: "y", value: "mouse.y"
  - node: "Middle", prop: "y", value: "Source.y + 30"
  - node: "Output", prop: "y", value: "Middle.y + 30"
tools_visible: [select, play]
ui_visible: [properties, formulas]

Syntax highlighting

The formula editor tokenizes your code in real time. 8 token types, 8 colors:

Token typeWhat it highlights
keywordif, else, return, let, const, true, false, ...
number42, 3.14, 0xFF
string"hello", 'world'
nodeRefNode names: Ball, Platform, Score
propertyProperty after dot: .x, .width, .rotation
builtinsin, cos, clamp, lerp, Math, PI
operator+, -, *, ==, &&, ?, :
comment// note, /* block */

Node references get per-ID colors from an 8-color palette. Ball is always blue, Platform is always red -- same node, same color, everywhere in your formula. The palette cycles through: blue, red, yellow, green, orange, teal, light blue, salmon.

Highlighting uses the CSS Custom Highlight API -- no DOM overlays, no performance cost.

Source maps

Compiled formulas include VLQ-encoded inline source maps. The compiler generates a Base64-encoded JSON source map appended to the compiled output:

//# sourceURL=foximation:///Ball/x
//# sourceMappingURL=data:application/json;base64,...

Open browser DevTools, navigate to the foximation:/// sources, set a breakpoint in your formula. The debugger shows your original expression, not the compiled ctx.resolve() calls.

Property locking

When a property has a formula bound to it, the _isLocked callback returns true. This prevents manual edits and gizmo drags from overwriting formula-driven values.

Exception: combined geometric transforms (scale-from-anchor, rotate-from-origin) bypass _isLocked because they must write x, y, and rotation as a single atomic operation. After bypassing, the handler calls _notify() manually so the formula system sees the change.

MathEval: inline arithmetic

Number fields accept arithmetic without the = prefix. Type 200 + 50% in a width field and it evaluates immediately:

InputCurrent valueResult
200 + 50%200300 (50% of 200, added)
100% - 20px200180 (200 minus 20)
(100 + 50) * 2any300
.5 * 400any200

Supported: +, -, *, /, parentheses, % (relative to current value), px (explicit absolute). This is not a formula -- it evaluates once and sets the field. No reactive binding.

Performance

The formula hot path is designed for zero allocation per frame:

TechniqueWhat it avoids
Preallocated scratch buffersNo new Array() or new Object() per evaluation
Prototype-inherited contextFormula context uses JS prototype chain instead of object spread -- one object creation, not one per formula
Version countingDependencies track which version they last read. If the value hasn't changed, dependents skip re-evaluation
CSS paint cachingrgbaToCSS() results cached to avoid string allocation on every frame
Dirty propagationOnly formulas whose dependencies actually changed re-evaluate
title: "Paint sub-property animation"
description: "Formula drives a color channel directly through compound property access"
nodes:
  - type: ELLIPSE, name: "Orb", x: 250, y: 250, w: 100, h: 100, fill: "#3498DB"
formulas:
  - node: "Orb", prop: "fills[0].color.h", value: "time * 60"
  - node: "Orb", prop: "width", value: "80 + sin(time * 2) * 20"
  - node: "Orb", prop: "height", value: "80 + sin(time * 2) * 20"
tools_visible: [select, play]
ui_visible: [properties, formulas]

Events API
Nodes
Properties
← НазадFormula GlobalsДальше →Events API