Fraxel is built around three interlocking ideas: a node tree that maps directly to canvas draw calls, a signal-based reactivity model that updates only what changed, and a custom JSX runtime that lets you describe both using familiar component syntax. Understanding how these three pieces interact will make every other part of the engine click into place. The mental model is straightforward: JSX describes a tree of engine nodes, signals hold mutable state, and any JSX prop that references a signal automatically re-evaluates when that signal changes — without diffing, without re-rendering components, and without touching the DOM.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/sanchedev/fraxel/llms.txt
Use this file to discover all available pages before exploring further.
The Node Tree
Every JSX element in Fraxel maps directly to an engine class that knows how to update and draw itself on the HTML5 Canvas. There is no intermediate virtual representation — when you write<sprite textureId={TEX} />, you are constructing a Sprite node.
The full list of built-in node types is defined in the PrimaryNode enum:
Hierarchy and Rendering Order
The tree structure determines both rendering order and spatial hierarchy. Child nodes are drawn on top of their parent, and their positions are relative to their parent’s position. The abstractNode2D base class translates the canvas context by position before drawing each child, so the coordinate system is automatically local:
zIndex. A higher zIndex draws on top:
The node tree has no connection to the browser DOM. There are no HTML elements created, no CSS, and no layout engine involved. The canvas element itself is the only DOM node Fraxel creates.
The Node Lifecycle
Every node follows the same lifecycle, driven by three methods called by the game loop:| Method | When it fires |
|---|---|
start() | Once, when the node is first added to an active scene |
update(delta) | Every frame, before drawing. delta is elapsed seconds since last frame. |
draw(delta) | Every frame, after update. Applies translation and renders to canvas. |
destroy() | When the node is explicitly removed from the tree |
started, updated, drawed, destroyed) that hooks subscribe to. You never call these methods directly — the engine calls them and hooks let you react to them.
Node IDs and Child Lookup
Nodes can be assigned string IDs and located later by path. IDs must match the pattern[a-zA-Z][a-zA-Z0-9-_]*:
Signals & Reactivity
Fraxel’s reactivity model is fine-grained: state is held in signals, and only the canvas properties that depend on a changed signal are updated. There is no Virtual DOM, no component re-render, and no diffing.useSignal
useSignal creates a reactive signal and returns a [getter, setter] tuple:
health) is a plain TypeScript function. Calling health() returns the current value. Inside a JSX prop, an auto-computed effect, or a useEffect callback, calling health() also registers the signal as a dependency of that context.
Auto-Computed Props
Any JSX prop that receives a function (rather than a plain value) is treated as a computed reactive expression. Fraxel evaluates the function immediately to get the initial value, tracks which signals were read, and re-evaluates the function whenever any of those signals change:setHealth(0) is called, only the grayscale and brightness canvas properties are updated. The position is not touched. This is the key difference from a Virtual DOM approach where the entire component output would be re-evaluated and compared.
useComputed
useComputed creates a derived signal — a read-only getter whose value is automatically recomputed whenever its dependencies change:
useEffect
useEffect runs a side effect whenever the signals it reads change. Re-executions are batched — if multiple signals change synchronously in the same tick, the effect runs only once before the next frame:
useEffect batches re-runs using queueMicrotask. This means all synchronous signal updates in a single frame settle before any effect fires, preventing redundant work.The JSX Runtime
Fraxel ships its own JSX transform under thefraxel/jsx-runtime path. When you set jsxImportSource: "fraxel" in your tsconfig.json, TypeScript’s automatic JSX transform imports from fraxel/jsx-runtime instead of react/jsx-runtime.
This means every .tsx file in your project uses the Fraxel engine as its JSX factory. There is no React import needed, and there is no React anywhere in the bundle.
How JSX Becomes Nodes
JSX elements fall into two categories in Fraxel:- Lowercase elements (
<transform>,<sprite>,<collider>) — map to built-in engine node constructors. These are resolved at startup whencreateGameprocesses the JSX tree. - Capitalized components (
<Player>,<Enemy>) — are plain TypeScript functions that return more JSX. They are called immediately during tree construction, not re-called on updates.
createGame and Scene Loading
createGame is the entry point that bridges the JSX declaration and the running engine:
createGame:
- Calls
Game.setup()with the canvas dimensions and mounts the canvas inside the root element - Registers each
<Scene>with theSceneManager - Calls
SceneManager.setScene(defaultScene)to queue the initial scene - Starts the
requestAnimationFrameloop viaGame.play()
<Game> and <Scene> components themselves return null — they exist only to carry configuration as JSX props, which createGame reads before discarding the JSX tree.
Hooks
Hooks are the primary way to attach behavior to nodes in Fraxel. They must be called inside a component function (not inside event handlers or async callbacks).| Hook | Purpose |
|---|---|
useSignal(initial) | Creates a reactive [getter, setter] pair |
useComputed(fn) | Derives a read-only signal from other signals |
useEffect(fn) | Runs a side effect when dependencies change |
useMount(fn) | Runs once on node start; returns optional cleanup |
useNode(type) | Creates a typed node reference for use with ref prop |
useEvent(ref, event, fn) | Subscribes to a node lifecycle event |
useSpawn(ref) | Returns a function that dynamically adds child nodes |
useGame() | Gets the GameControls object for scene switching and pause |
useChild(path, type) | Retrieves a descendant node by path after mount |
Lifecycle Hooks
useMount is the simplest lifecycle hook. It runs once when the node starts, and optionally returns a cleanup function that runs on destroy:
useEvent subscribes to any node event by name. This is the idiomatic way to hook into the per-frame updated cycle for movement and game logic:
Scenes
Scenes are the top-level containers for game content. Each scene has a name and a component function that returns a node tree. TheSceneManager handles loading, unloading, and switching between scenes.
Defining Scenes
Scenes are registered through the<Scene> JSX element inside <Game>:
SceneManager will only load the scene’s module when it is first activated.
Switching Scenes
UseuseGame() inside any component to get the GameControls object, then call changeScene:
changeScene returns a Promise<void> that resolves once the new scene is fully loaded and active. For seamless transitions, use preloadScene to load the next scene in the background while the current one is still running:
Scene Controls Reference
Scripts
FraxelScript is an optional class-based system for separating game logic from the JSX component tree. Attach a script to any node via the script prop:
script prop:
connect(eventName, callback) provides the same type-safe event subscription as useEvent, but scoped to the node the script is attached to via this.me. The script’s lifecycle matches the node it is attached to — when the node is destroyed, the script’s event listeners are cleaned up automatically.