feat: add render graph driven renderer architecture

Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-27 02:53:44 +00:00
co-authored by heaust
parent 8a8369706b
commit d4e8634f67
290 changed files with 48804 additions and 1995 deletions
+66
View File
@@ -0,0 +1,66 @@
import { defineConfig } from "vitepress";
import generated from "../reference/generated/typedoc-sidebar.json" with { type: "json" };
const learn = [
{ text: "Learn", link: "/learn/" },
{
text: "Tutorials",
collapsed: false,
items: [
{ text: "Overview", link: "/learn/tutorials/" },
{ text: "Your first node", link: "/learn/tutorials/first-node" },
{ text: "Color Balance", link: "/learn/tutorials/color-balance" },
{ text: "Live composition", link: "/learn/tutorials/live-composition" },
],
},
{
text: "Concepts",
items: [
{ text: "Overview", link: "/learn/concepts/" },
{ text: "Worker authority", link: "/learn/concepts/worker-authority" },
{ text: "Composition", link: "/learn/concepts/composition" },
{ text: "Graph state and events", link: "/learn/concepts/graph-state-and-events" },
{ text: "State and persistence", link: "/learn/concepts/state-and-persistence" },
],
},
{
text: "Guides",
items: [
{ text: "Overview", link: "/learn/guides/" },
{ text: "Browser host", link: "/learn/guides/browser-host" },
{ text: "Interactions", link: "/learn/guides/interactions" },
{ text: "Rendering and lifecycle", link: "/learn/guides/rendering-and-lifecycle" },
{ text: "Browser support", link: "/learn/guides/browser-support" },
{ text: "CSP", link: "/learn/guides/csp" },
{ text: "Accessibility", link: "/learn/guides/accessibility" },
],
},
{ text: "Examples", link: "/learn/examples/" },
];
export default defineConfig({
title: "fxnode",
description: "A typed, worker-owned node editor",
lastUpdated: true,
srcExclude: ["research/**", "decisions/**"],
ignoreDeadLinks: false,
transformPageData(pageData) {
if (pageData.relativePath.startsWith("reference/generated/"))
pageData.frontmatter = { ...pageData.frontmatter, editLink: false, lastUpdated: false };
},
themeConfig: {
nav: [
{ text: "Learn", link: "/learn/" },
{ text: "API Reference", link: "/reference/" },
],
sidebar: { "/learn/": learn, "/reference/": [{ text: "API Reference", link: "/reference/" }, ...generated] },
search: { provider: "local" },
outline: { level: [2, 3] },
editLink: {
pattern: "https://github.com/Heaust-ops/fxnode/edit/main/docs/:path",
text: "Edit this page on GitHub",
},
socialLinks: [{ icon: "github", link: "https://github.com/Heaust-ops/fxnode" }],
footer: { message: "Released under the MIT License.", copyright: "Copyright © fxnode contributors" },
},
});
@@ -0,0 +1,55 @@
# Phase 3 worker transport Protobuf benchmark
**Status:** NO-GO (measured 2026-07-22). This decision does not authorize a production transport change.
## Question and method
The experiment compared the current browser structured-clone shape with a generated Protobuf-ES schema sent as a transferable `ArrayBuffer`. It ran in headless, real Chromium with a dedicated module worker. The timed Protobuf path included the main-thread object-to-schema adapter, encode, transferable `postMessage`, worker decode and schema-to-object adapter. The structured path included `postMessage` and structured clone. Both paths computed the same FNV-1a checksum over a recursively key-sorted serialization of the reconstructed synthetic payload in the worker. Every Protobuf send asserted immediate transfer detachment.
Fixtures are deterministic multiplications of nodes, links, sockets, parameters, composition definitions and commands. Graph state, snapshot, node, socket, link and envelope fields are generated typed messages (not JSON bytes in Protobuf). Recursive `JsonValue` represents open values and composition/command/save variants. JSON byte counts below are UTF-8 diagnostics, not a measured transport. Pointer SAB, ImageBitmap frames and resource byte payloads are explicitly excluded.
The orb-constrained run used 10 warmups and 50 samples per payload/path (rather than an exhaustive 3×120). Samples were sequential in one browser, structured clone always ran first, and the harness did not retain raw samples for paired analysis. It therefore cannot establish cross-machine confidence or formal results for every row. The correctly directed `state.set` failures are far outside timer resolution and independently make the wholesale migration fail its acceptance gates.
The harness always encoded on the page and decoded in the worker. Consequently, response, snapshot, save-data, mutation, and host-projection rows were synthetic reverse-direction estimates rather than production-faithful worker-to-host measurements. Some of those envelopes also approximated, rather than passed, the current protocol validators. They corroborate the result but are not used as decisive evidence. The graph-heavy `state.set` requests did run in the production direction and used typed graph messages; those are the basis of the decision.
## Environment
- Orb: Linux 6.1.158+, x86_64, 2 reported logical CPUs
- Chromium: HeadlessChrome 149.0.7827.55, Linux; `crossOriginIsolated=false`
- Node 20.9.0, npm 10.9.8, Vite 6.1.0, Playwright 1.61.1
- Exact tools: `@bufbuild/buf@1.72.0`, `@bufbuild/protobuf@2.13.0`, `@bufbuild/protoc-gen-es@2.13.0`
- The one-off benchmark harness and generated output were removed after review; this record preserves its result and limitations.
## Results
Times are milliseconds. Delta is `(structured p95 - protobuf p95) / structured p95`; positive favors Protobuf.
| Payload | JSON bytes | clone p50 / p95 | protobuf p50 / p95 | main codec p95 | p95 delta |
| ---------------------------- | ---------: | --------------: | -----------------: | -------------: | --------: |
| tiny command | 143 | 0.00 / 0.20 | 0.30 / 0.70 | 0.20 | -250.0% |
| state.set medium | 129,422 | 3.70 / 5.40 | 28.00 / 43.30 | 31.30 | -701.9% |
| state.set large | 654,168 | 16.80 / 19.50 | 152.40 / 176.70 | 119.20 | -806.2% |
| snapshot medium | 129,413 | 3.40 / 6.00 | 26.60 / 45.20 | 27.80 | -653.3% |
| snapshot large | 654,159 | 17.40 / 20.40 | 149.70 / 183.70 | 113.70 | -800.5% |
| document.replaced large | 654,175 | 18.10 / 25.50 | 149.80 / 175.50 | 106.70 | -588.2% |
| save-data medium | 143,961 | 3.90 / 6.90 | 70.10 / 94.40 | 66.70 | -1,268.1% |
| save-data large | 624,879 | 17.30 / 21.50 | 310.50 / 383.00 | 243.60 | -1,681.4% |
| initial composition + layout | 559,668 | 14.40 / 17.70 | 279.80 / 322.00 | 204.80 | -1,719.2% |
| receipt | 152 | 0.10 / 0.20 | 0.20 / 0.30 | 0.20 | -50.0% |
| error | 139 | 0.10 / 0.20 | 0.20 / 0.30 | 0.10 | -50.0% |
| input | 179 | 0.10 / 0.20 | 0.20 / 0.30 | 0.20 | -50.0% |
| host projection | 10,084 | 0.30 / 0.30 | 3.50 / 9.00 | 3.10 | -2,900.0% |
The harness build produced 23,989 gzip bytes (417 bytes for its worker entry and 23,572 bytes for its combined codec chunk). This was not a production baseline/delta comparison and did not account for host/worker runtime duplication, so it is only a non-decisive harness-size estimate.
## Thresholds and decision
The planned gates were: graph-heavy p95 improvement ≥20% in at least 3/4 of medium/large state and snapshot cases; no >5% regression on any other large payload; a small-payload deadband (do not decide on sub-millisecond noise); main-thread codec p95 ≤4 ms and no codec task >16.7 ms; worker gzip delta ≤30 KiB and total ≤45 KiB. A noisy threshold crossing would have been **INCONCLUSIVE**. Because only request-direction cases were production-faithful and the bundle comparison was approximate, not every planned gate was formally established.
The result is still **NO-GO**. Both correctly directed medium and large `state.set` requests regressed by multiples, making the planned 3-of-4 graph gate mathematically impossible to pass. Their page-side adapter/encode latency also exceeded both codec limits by wide margins. Tiny-message differences and reverse-direction estimates are not used to strengthen the decision. The generic `JsonValue` portions do not prove every conceivable fully typed schema would be slow, but the typed graph result is sufficient to reject a wholesale migration now.
Production retains structured clone for object-rich commands, composition, state, and events; the existing `SharedArrayBuffer` pointer lane and naturally binary transferables remain the appropriate narrow optimizations. A future packed typed-array format should be considered only for a measured numeric hot path, not as a generic graph protocol.
## Repository outcome
The reviewed harness was intentionally not retained: it approximated several current envelopes, tested only host-to-worker direction, and would have permanently added a generator/runtime dependency tree for a rejected design. This ADR retains the environment, measured table, decisive evidence, and limitations without turning the one-off experiment into a misleading supported benchmark.
+31
View File
@@ -0,0 +1,31 @@
---
layout: home
hero:
name: fxnode
text: A worker-owned node editor
tagline: Compose a typed graph language, present it on Canvas, and persist it safely.
actions:
- theme: brand
text: Start learning
link: /learn/
- theme: alt
text: API Reference
link: /reference/
features:
- title: Worker authority
details: Commands, validation, hit testing, layout, history, and rendering live behind one explicit boundary.
- title: Zero or many views
details: Run headlessly or attach independent canvases with view-local cameras, selections, input, and rendering to one shared graph.
- title: Application composition
details: Your application supplies node definitions, socket compatibility, theme, and resource policies.
- title: Durable documents
details: Canonical saves, bounded decoding, opaque unknown nodes, and declarative migrations protect user data.
---
::: warning Prerelease
fxnode is an internal `0.x` prerelease. APIs and persistence contracts can still change.
:::
fxnode **presents and persists** node graphs. It does not evaluate or execute them. It is not Blender-compatible and makes no Blender feature or visual parity claim.
+11
View File
@@ -0,0 +1,11 @@
# Composition
Composition is the application-defined graph language, distinct from graph state. It owns themes, header styles, directional socket compatibility, resources, node definitions, UI rows, defaults, bypasses, and migrations. Graph state is a document written in that language. Compatibility is checked from the destination socket's accepted source types; changing it can invalidate existing links.
There is no built-in registry. Browser applications install definitions through `setTheme`, `setHeaderStyles`, `setCompatibility`, `composeSocket`, and `composeNode`, or atomically with `loadComposition`. Dependencies come first; `setState` comes last. Plain structured-clone-safe data crosses the worker boundary—no callbacks or classes.
Updates compile, validate, rebind, and publish atomically and return a receipt (`status`, composition `revision`, graph version, and whether rebinding changed the graph). A rejected candidate changes nothing. Distinct definition IDs converge regardless of concurrent installation order once dependencies exist; updates to the same ID are ordered, and references still require their dependency to be installed first.
Every committed change to a node definition resets definition-bound undo/redo history, even if no current instance uses that definition. Removing a node definition preserves its instances as opaque, read-only nodes. Removing a socket type is rejected while compatibility rules, another socket type, or a node definition references it; update or remove those dependents first. A valid composition rebind may remove graph links that have become incompatible. Reintroducing compatible definitions can promote opaque instances. A migration `rename-socket` rewrites both the node's socket data and every link endpoint that refers to it in the same transaction—there is no observable half-renamed graph. A semantic no-op emits nothing and advances neither revision nor graph version.
Static/headless authoring can use `compileFxNodeComposition` and immutable helpers to retain literal ID types. Browser handles intentionally accept string IDs because their composition authority can change live.
@@ -0,0 +1,15 @@
# Graph state and events
Runtime graph state contains `graphId`, `catalogVersion`, nodes, links, and metadata. It is not the persistence envelope. The worker commits commands atomically and checks optional optimistic `expectedVersion` values.
Committed graph changes emit mutations before snapshots, in version order. Subscribers are isolated and return an unsubscribe function. Composition changes use a separate revision domain and emit `onCompositionChanges`; when rebinding changes a graph, the composition event precedes the matching mutation and snapshot.
Command and composition calls resolve with receipts only after authoritative validation and publication. Use their returned versions/revisions for the next compare-and-swap rather than inferring them from event timing. Structured validation/protocol failures reject without partial mutation; a `noop` receipt means no graph publication.
| Domain | Meaning | Advances on |
| ---------------------- | -------------------------------------- | ---------------------------------- |
| Graph `version` | runtime document concurrency | graph-changing command/load/rebind |
| Composition `revision` | live authority concurrency | committed composition update |
| `catalogVersion` | bound composition version in documents | normalization/binding; persisted |
Do not compare or substitute these values. Gesture previews remain worker-local until one commit.
+10
View File
@@ -0,0 +1,10 @@
# Concepts
For integrators deciding where application responsibilities end and fxnode authority begins. Read in this order:
1. [Worker authority](./worker-authority): locate truth, work, and the asynchronous host boundary.
2. [Composition](./composition): model the application's graph language and live updates.
3. [Graph state and events](./graph-state-and-events): reason about documents, receipts, versions, and observation.
4. [State and persistence](./state-and-persistence): choose runtime replacement, canonical export, or replayable persistence.
Afterward you should be able to choose the correct API and concurrency domain, predict publication order, and design durable loading without treating fxnode as a graph evaluator. fxnode is an editor and presenter, not an evaluator.
@@ -0,0 +1,11 @@
# State and persistence
`getState()` and `setState()` exchange exact, process-local state for the currently installed composition. `setState()` is useful for bootstrap and controlled replacement, not historical imports.
`save()` returns the canonical current `GraphLayoutV2`—a compact graph export, not history. For replayable durable storage use `getSaveData()`: its envelope records the canonical baseline, the applied command journal since that baseline, and the effective save-time composition used to establish compatibility. The baseline and journal are composed at save time to verify that they reproduce the exported current graph.
`load()` accepts historical `GraphLayoutV1`, canonical `GraphLayoutV2`, or the save-data envelope. It stages decode, compatibility checks, declarative migrations, and replay before one atomic publication; structured issues identify paths/codes on failure, and rejected input leaves graph, history, and observable state unchanged. A successful graph change publishes the load mutation/snapshot as one commit. Loading an envelope installs its migrated baseline and command journal (including checkpoint placement); if the resulting graph equals current state, the load is a no-op but the validated journal/baseline is still installed for subsequent undo/redo and saves.
Durable `GraphLayoutV2` uses `schemaVersion: 2`; its historical `catalogVersion` field stores composition version. Unknown types and future node versions round-trip as opaque read-only records. Declarative migration edges must form a complete valid route; failures preserve the original opaque payload. Canonical ordering and bounded admission make saves deterministic and hostile inputs reject safely.
In short: **set/get state** for exact current runtime state; **save** for canonical `GraphLayoutV2`; **save data/load** for compatible persistence and replay. Selection, camera, hover, composition revision, and undo/redo internals are not durable graph fields.
+13
View File
@@ -0,0 +1,13 @@
# Worker authority
The worker is authoritative for graph state, composition, validation, command history, hit testing, layout, gestures, and rendering. One root owns one worker and one shared graph, whether it has zero, one, or many attached views. The browser client keeps only bounded host projections. It does not keep a graph shadow.
Graph state, composition, persistence, events, and history belong to the root. Canvas, viewport, camera, selection, gestures, rendering, host requests, and resource authorization belong to a view. A mutation from any view changes the shared graph and schedules every attached view, while cameras and selections remain independent.
The worker serializes view painting and cropping through one atlas canvas and one 2D context. Each attached HTML canvas has its own presentation context; `maxViews` remains a resource bound rather than a rendering-context count.
The application owns the DOM: canvas sizing, listeners, focus policy, menus, dialogs, measurement, and teardown. It turns DOM events into `feedInput()` DTOs. fxnode never registers document/window/canvas listeners or creates controls.
Host requests cross an asynchronous trust boundary. For `resource-open`, the worker emits an immutable descriptor and one-use authorization. The application chooses UI and later calls `provideResource(authorization, data)`. The token is consumed only by a valid accepted submission: failed data validation does **not** consume it, so the application may correct the data and retry. Do not depend on the original pointer's browser activation; ask for a fresh user action when required. Authorizations become stale after relevant graph/composition changes, and transferred `ArrayBuffer`s detach.
This boundary makes worker ordering definitive: await composition dependencies and treat terminal startup/protocol failures as terminal.
+33
View File
@@ -0,0 +1,33 @@
# Examples
The repository has five current experiences. Images below use the examples' existing captured assets—there are no documentation copies.
## Minimal
![A single Number Value node on the fxnode canvas](../../../examples/assets/minimal.png)
_A minimal composition and one node. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/main.ts)._
## Color Balance
![A Color Balance node with lift, gamma, and gain grading wheels](../../../examples/assets/color-balance.png)
_A focused custom-widget composition. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/color-balance/main.ts)._
## Live composition
![A live composition example with a parameter node and upgrade control](../../../examples/assets/live-composition.png)
_Replacing a node definition and migrating its instance. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/live-composition/main.ts)._
## Multi-view
![One shared graph rendered in two independent canvas views](../../../examples/assets/multi-view.png)
_One worker and graph with independent cameras and selections. The application-owned toolbar targets the active view, and the canvases forward pointer events only. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/multi-view/main.ts)._
## Blender-shaped gallery
The [larger gallery source](https://github.com/Heaust-ops/fxnode/blob/main/examples/blender/main.ts) exercises many node and interaction shapes; it is repository application code, not package authority.
These examples present and persist editable graphs; they do **not evaluate** them. Blender-shaped fixtures and visual regression images do not establish Blender compatibility, behavioral parity, or pixel parity.
+5
View File
@@ -0,0 +1,5 @@
# Accessibility
The editor is bitmap Canvas. Its nodes, sockets, labels, controls, and relationships are not semantic accessibility-tree objects. fxnode makes **no WCAG conformance claim** and must not be the only interface when assistive access is required.
Applications should provide an equivalent semantic DOM workflow, status announcements, instructions, and controls. Keyboard support alone is not accessibility. An application-owned DOM add-node dialog can implement combobox/listbox semantics and focus restoration, but that does not make the Canvas editor accessible.
+20
View File
@@ -0,0 +1,20 @@
# Browser host
`createFxNode` needs application identity/version, resource policies, and optionally a worker URL/history limit. It creates the shared root and worker without a canvas. `root.attachView()` needs a canvas, logical CSS-pixel viewport plus DPR, and optionally an initial camera. The **application owns the DOM and canvas dimensions**. Before attachment, measure the initial layout and set the backing dimensions. For a runtime resize, await `view.setViewport(next)` before updating the backing dimensions. Attach and remove your own listeners.
fxnode creates one module worker per root and never creates a resize observer, menu, modal, or file picker. A root may have no views or multiple views. Convert pointer/keyboard/wheel events to each view's `feedInput()` values. The worker performs authoritative hit testing and may issue view-scoped `add-node-menu` or `resource-open` host requests; your DOM decides presentation and ordering.
Use root methods for composition, state, persistence, subscriptions, and context-free commands. Use view methods for input, viewport changes, selection actions, resource responses, and render checkpoints. A canvas can have only one live view. On teardown, remove application listeners and observers first, detach each view, then destroy the root.
The repository example host defaults to `lifecycle: "explicit"`, so `host.destroy()` removes only host-owned policy
and never detaches its view. Opt in with `lifecycle: "detach-on-disconnect"` for component-style examples. That mode
requires an initially connected canvas and `MutationObserver`; hosts share one observer per document. A removal is
confirmed in a microtask (so a same-task remove/reinsert or reparent survives), then host resources are synchronously
removed before `view.detach()` is requested. Moving the canvas to another document counts as disconnection; hiding it
or giving it zero layout size does not.
Resize observations are coalesced while a viewport request is in flight. The host updates canvas backing dimensions
only after `setViewport()` acknowledges that request. A rejection preserves the prior backing store, reports through
`onError`, and a later observation can retry.
Install composition before initial state. Imported/historical data belongs in `load()`, not `setState()`. See [state and persistence](../concepts/state-and-persistence).
+7
View File
@@ -0,0 +1,7 @@
# Browser support
The certified functional matrix is Chromium and Firefox from Playwright 1.61.1 on desktop Linux. Chromium image goldens are regression tests, not cross-engine or Blender parity tests. WebKit, Safari-branded builds, and mobile are not certified.
The main thread requires module `Worker`, `crypto.randomUUID`, and Canvas 2D. The worker requires `OffscreenCanvas`, a 2D context, cropped `createImageBitmap`, and `ImageBitmap.close`; there is no fallback. Named capability errors identify missing features.
Cross-origin isolation enables an optional `SharedArrayBuffer` pointer lane per view. Without it, normal `postMessage` transport remains functional. Limits include 16 attached views, DPR 4, 8192 logical pixels per dimension, 16,777,216 device pixels per view, 67,108,864 device pixels across all views, and history 1,000.
+5
View File
@@ -0,0 +1,5 @@
# Content Security Policy
fxnode starts a same-origin ES module worker with no blob, classic-worker, or main-thread fallback. Permit it with an appropriate `worker-src 'self'` and `script-src`, and serve JavaScript with the correct MIME type. Pass `workerUrl` if assets move independently.
For optional shared-memory input use `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`; embedded cross-origin resources must satisfy CORS or CORP. Otherwise fxnode automatically uses messages. Worker construction blocked synchronously reports `worker.construct`; a worker script/network/module load failure reports `worker.load`; failure to complete startup in time reports `worker.timeout`. None silently downgrade to main-thread execution.
+5
View File
@@ -0,0 +1,5 @@
# Integration guides
For browser/platform engineers turning an editor bootstrap into a production integration. Start with the [browser host](./browser-host), then wire [interactions](./interactions) and [rendering and lifecycle](./rendering-and-lifecycle). These establish canvas ownership, input forwarding, render checkpoints, and teardown.
Before release, review [browser support](./browser-support), [CSP](./csp), and [accessibility](./accessibility), in that order. The outcome is a host with explicit capability fallbacks, deployable worker policy, accessible DOM-owned controls, and no leaked listeners or workers.
+7
View File
@@ -0,0 +1,7 @@
# Interactions
The host translates DOM input; the worker owns gesture state and commits. Supported editor gestures include movement, box selection, resize, link creation/replacement, Ctrl-right-click cutting, Ctrl-Alt-right-click muting, `M` node mute, `H` collapse, `G` modal move, and undo/redo.
Plain right-click on eligible empty canvas may request an add-node menu. The host owns its HTML, search, grouping, focus, dismissal, and calls `addNode`. Controls follow composition `ui` order. Scrubbing modifiers, text commit/cancel, and reset all become atomic commands.
Do not derive behavior from projected pixels or retain a parallel graph. Subscribe to committed events when application UI needs updates.
@@ -0,0 +1,7 @@
# Rendering and lifecycle
Each view's `whenRendered()` synchronizes a frame for that view. Attach another view when a second canvas needs an independent camera or selection over the same graph; graph mutations schedule all attached views. Set the initial backing dimensions before attachment. At runtime, await that view's `setViewport(next)` first, then update the host canvas backing dimensions.
Keep every listener, observer, menu, and focus behavior in an application-owned cleanup object. On unmount/page teardown, remove those resources, await `view.detach()` for each view, then call `root.destroy()`. The example browser host keeps this explicit by default; its opt-in disconnect policy can perform host cleanup and request detachment when a connected canvas is removed. Destroying that host alone never detaches. View detachment is idempotent and rejects subsequent view work with `FxNodeViewDetachedError`. Root destruction is idempotent, detaches all remaining views, and makes pending and future work reject with `FxNodeDestroyedError`. Fatal startup/protocol failures also release resources and make future calls reject the stored terminal error.
Do not use rendering completion as graph execution completion: fxnode never executes graphs.
+23
View File
@@ -0,0 +1,23 @@
# Learn fxnode
Treat this as a trail map rather than a giant manual.
## Start
Build [your first node](/learn/tutorials/first-node), then tour [all examples](/learn/examples/).
## Build
Use the [browser-host guide](/learn/guides/browser-host) to connect your DOM and the [interaction guide](/learn/guides/interactions) to translate input.
## Understand
Read [worker authority](/learn/concepts/worker-authority), [composition](/learn/concepts/composition), and [state and persistence](/learn/concepts/state-and-persistence).
## Integrate
Check [browser support](/learn/guides/browser-support), [CSP](/learn/guides/csp), lifecycle, and [accessibility](/learn/guides/accessibility) before shipping.
## Reference
When you know the concept and need an exact signature, use the [API Reference](/reference/).
+57
View File
@@ -0,0 +1,57 @@
# Build a Color Balance editor
## What you will build
A focused editor with float/color socket types and the repository's grading-wheel Color Balance definition.
## Prerequisites and checkpoint
Complete [your first node](./first-node). Confirm the empty editor renders before adding the two socket definitions.
## 1. Install dependencies
After `createFxNode`, install theme and styles, then compose `float`, compose `color`, and compose the node. Make `setState` the final bootstrap state call; attach a view, add the node through that view, attach the host, and await the view's `whenRendered()`.
```ts
import { createFxNode } from "fxnode";
const root = await createFxNode({
applicationId: "color.balance",
applicationVersion: 1,
resources: {},
});
await root.setTheme(theme);
await root.setHeaderStyles(styles);
await root.composeSocket(...floatSocket);
await root.composeSocket(...colorSocket);
await root.composeNode(...colorBalanceNode);
await root.setState({ graphId: "color-balance", catalogVersion: 1, nodes: [], links: [], metadata: {} });
const view = await root.attachView({ canvas, viewport });
host.attach(root, view);
await view.addNode({
nodeId: "color-balance",
typeId: colorBalanceNode[0],
viewPosition: { x: 300, y: 40 },
});
await view.whenRendered();
```
This is an **excerpt**: `canvas`, `host`, `viewport`, theme, styles, sockets, and node definition are application-owned setup shown in the working source.
**Checkpoint:** the Color Balance node and its three grading wheels are visible and interactive.
### Why?
Definitions refer to styles and sockets, so dependencies must exist first. The widget edits graph data; fxnode does not perform color correction or execute the graph.
## 2. Attach, verify, and clean up
Attachment starts DOM input forwarding; the view's `whenRendered()` establishes a visible-frame checkpoint. On teardown remove listeners, run `host.destroy()`, await `view.detach()`, and call `root.destroy()` (including startup failure and startup/teardown races).
## Complete example
See [`examples/color-balance/main.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/color-balance/main.ts) and the shared [node definition](https://github.com/Heaust-ops/fxnode/blob/main/examples/shared/nodes/color-balance.ts).
## Related concepts / relevant API / next
Read [composition](../concepts/composition), then inspect [`FxNode.composeNode`](/reference/generated/fxnode/interfaces/FxNode#composenode) and continue to [live composition](./live-composition).
+72
View File
@@ -0,0 +1,72 @@
# Your first node
## What you will build
A Canvas editor containing one numeric value node, matching the repository's executable minimal example.
## Prerequisites
Install `fxnode`. Give the canvas non-zero CSS dimensions (the attributes also provide a useful fallback), then prepare a browser host that measures it and forwards input:
```html
<canvas id="graph" width="1000" height="560" style="width: 100%; height: 560px"></canvas>
```
The repository's [small host implementation](https://github.com/Heaust-ops/fxnode/blob/main/examples/shared/browser-host.ts) contains viewport, resize, and input wiring; see [browser host](../guides/browser-host) for its contract.
## Checkpoint
Your canvas has non-zero CSS dimensions and your host has produced `initialViewport`.
## 1. Prepare application-owned definitions
`theme`, `minimalStyles`, `numberSocket`, and `valueNode` below are **application-owned definitions**, not fxnode globals. The socket and node are exported as `[id, definition]` tuples so they can be passed directly to the composition methods. Define or import them before bootstrap; the executable [definition file](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/definition.ts) is the compact reference.
## 2. Bootstrap in dependency order
Create the shared root first, then install composition dependencies in order: theme, header styles, sockets, nodes, and finally graph state. Attach the canvas view after that bootstrap.
```ts
import { createFxNode } from "fxnode";
const root = await createFxNode({
applicationId: "my.first.editor",
applicationVersion: 1,
resources: {},
});
await root.setTheme(theme);
await root.setHeaderStyles(minimalStyles);
await root.composeSocket(...numberSocket);
await root.composeNode(...valueNode);
await root.setState({ graphId: "first", catalogVersion: 1, nodes: [], links: [], metadata: {} });
const view = await root.attachView({ canvas, viewport: host.initialViewport });
host.attach(root, view);
await view.addNode({ nodeId: "value", typeId: valueNode[0], viewPosition: { x: 360, y: 190 } });
await view.whenRendered();
```
**Checkpoint:** a “Number Value” node is visible. The host is attached only after setup, and the view's `whenRendered()` confirms the committed node reached a frame.
### Why this order?
The worker validates every definition against current authority. `setState` is last so known nodes bind against the complete composition. Host attachment follows bootstrap so input cannot race setup.
## 3. Clean up
Remove application listeners, call `host.destroy()`, await `view.detach()`, then call `root.destroy()` on unmount or `pagehide`. Also destroy a late-created root if teardown wins a startup race. The complete source demonstrates that guard.
## Complete example
The complete executable source is [`examples/minimal/main.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/main.ts), with its [`definition.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/definition.ts).
## Related concepts
[Composition](../concepts/composition) and [worker authority](../concepts/worker-authority).
## Relevant API
[`createFxNode`](/reference/generated/fxnode/functions/createFxNode), [`FxNode`](/reference/generated/fxnode/interfaces/FxNode), and [`FxNodeView`](/reference/generated/fxnode/interfaces/FxNodeView).
## Next
Build a richer [Color Balance node](./color-balance).
+9
View File
@@ -0,0 +1,9 @@
# Tutorials
For application developers integrating fxnode for the first time. Follow these in order: each tutorial builds on the previous one's host and composition vocabulary. You will finish able to bootstrap a visible editor, install a custom widget, and safely replace a live definition with optimistic concurrency.
1. [Your first node](./first-node) — size and host a canvas, install definitions, render, and tear down.
2. [Color Balance](./color-balance) — add dependency-ordered socket types and a custom widget.
3. [Live composition](./live-composition) — migrate a visible instance using composition receipts.
Each page marks excerpts, establishes ordered checkpoints, explains why each concern exists, and links to a complete executable source.
+47
View File
@@ -0,0 +1,47 @@
# Live composition
## What you will build
An editor that replaces a version-1 node definition with version 2 and migrates its graph instance atomically.
## Prerequisites and checkpoint
Understand [composition](../concepts/composition). Start with the v1 node visible and retain the receipt's `revision`.
## 1. Acquire the v1 revision
```ts
import type { FxNode, FxNodeView } from "fxnode";
const v1Receipt = await api.composeNode("example.live.parameter", liveNodeV1);
let revision = v1Receipt.revision;
```
This is an **excerpt**: await socket dependencies first, compose v1, call `setState`, attach a view, add its instance through that view, attach the host, and render. **Checkpoint:** v1 is visible and `revision` came from its receipt—not a guessed constant.
## 2. Replace it using the v2 receipt
```ts
async function upgrade(root: FxNode, view: FxNodeView) {
const v2Receipt = await root.composeNode("example.live.parameter", liveNodeV2, {
expectedRevision: revision,
});
revision = v2Receipt.revision;
await view.whenRendered();
return v2Receipt;
}
```
Invoke this on an explicit host action. **Checkpoint:** inspect `v2Receipt.status`, `graphChanged`, `graphVersion`, and updated `revision`; the migrated v2 node is visible. Clean up the button/page listeners, host, and API on teardown.
### Why?
Composition revision and graph version are separate concurrency domains. A committed rebind can advance both; a no-op advances neither. Compare-and-swap prevents two writers from assuming the same authority.
## Complete example
See the working [`examples/live-composition/main.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/live-composition/main.ts) and its [definitions](https://github.com/Heaust-ops/fxnode/blob/main/examples/live-composition/definitions.ts).
## Related concepts / relevant API / next
Read [graph state and events](../concepts/graph-state-and-events) and [`CompositionReceipt`](/reference/generated/fxnode/type-aliases/CompositionReceipt), then plan [lifecycle cleanup](../guides/rendering-and-lifecycle).
+9
View File
@@ -0,0 +1,9 @@
# API Reference
fxnode exposes exactly three package entrypoints:
1. [`fxnode`](/reference/generated/fxnode/) — browser client, shared graph types, and composition authoring.
2. [`fxnode/headless`](/reference/generated/fxnode/headless/) — composition-bound decoding and command execution without browser resources.
3. [`fxnode/widgets/color-ramp`](/reference/generated/fxnode/widgets/color-ramp/) — immutable color-ramp model and operations.
The generated pages describe exact signatures. For intent, ordering, and ownership, return to [Learn](/learn/) or start with [your first node](/learn/tutorials/first-node). Integration guidance lives in [browser host](/learn/guides/browser-host), while [composition](/learn/concepts/composition) and [state and persistence](/learn/concepts/state-and-persistence) explain the two main data boundaries.
+13
View File
@@ -0,0 +1,13 @@
# Capturing Blender references
Use the official Linux Blender 4.5.0 binary whose archive SHA-256 is recorded in the manifest. A graphical X11 session (a disposable Xvfb session is suitable) and a whole-window capture utility are required.
For each of the eight IDs:
1. Start Blender at a deterministic 1440×900 window size with factory settings.
2. Run `blender --python tools/blender/create-reference-fixtures.py -- --fixture <id>` (add `--save /tmp/<id>.blend` if desired).
3. Wait for redraw, keep the entire Blender window—including chrome—visible, and capture it to `docs/research/blender-references/4.5.0/<id>.png`. For the hover fixture, move the pointer over the active node title before capture; this interaction cannot honestly be synthesized by Blender's data API.
4. Record UTC capture time, pixel dimensions, and `sha256sum` in `src/research/reference-manifest.ts`, change status to `captured`, and set capture method to `self-captured-blender-window`.
5. Run `npm run check:references:strict`.
The script validates Blender 4.5.x, rebuilds the current file, creates material or geometry node trees, lays nodes out deterministically, configures editor zoom, and saves only when asked. Generated `.blend` files are ignored and are not reference artifacts.
@@ -0,0 +1,3 @@
# Blender 4.5.0 self-captures
This directory intentionally contains no PNGs yet. Eight expected paths and their honest pending reasons are recorded in `src/research/reference-manifest.ts`. Do not substitute downloaded images or screenshots from the Blender Manual.
+20
View File
@@ -0,0 +1,20 @@
# Blender 4.5 research freeze
## Reproducible baseline
- Blender version: **4.5.0**
- Source commit: [`8cb6b388974a817afedf1317ce26f0c75aa5f181`](https://projects.blender.org/blender/blender/src/commit/8cb6b388974a817afedf1317ce26f0c75aa5f181)
- Official binary SHA-256: `1188b95cc12321c770b631939f7c25a096910b6f884a990bf9c0f62d52b38aec`
- Manual snapshot: [`f72fe39427bf150242dd6cfdd94d902e535d2286`](https://projects.blender.org/blender/blender-manual/src/commit/f72fe39427bf150242dd6cfdd94d902e535d2286)
Source citations are commit-pinned so later UI changes cannot silently alter the baseline. Relevant implementation entry points used only for behavioral study are [node drawing](https://projects.blender.org/blender/blender/src/commit/8cb6b388974a817afedf1317ce26f0c75aa5f181/source/blender/editors/space_node/node_draw.cc) and [node editor space](https://projects.blender.org/blender/blender/src/commit/8cb6b388974a817afedf1317ce26f0c75aa5f181/source/blender/editors/space_node/space_node.cc).
## Clean-room observations
The node editor presents a zoomable canvas. Nodes use title bars, body panels, labeled input/output sockets, links, selection/active emphasis, collapse controls, and inline controls where a socket is not linked. Frames group nodes visually; reroutes reshape link paths. Shader and geometry trees share interaction conventions while exposing domain-specific socket types and node content.
fxnode derives only functional observations and independently measured self-captures. No Blender source code, assets, icons, fonts, manual prose, or manual screenshots are copied into this repository. Names required for compatibility and factual citations are retained. See `NOTICE.md` and the typed reference manifest.
## Reference state
The eight requested captures are represented in `src/research/reference-manifest.ts`. They remain truthfully `pending` because this orb had neither Blender nor Xvfb. The normal Phase 1 check validates this state; the strict check fails until genuine captures and metadata are committed.
+60
View File
@@ -0,0 +1,60 @@
<!-- Repository research; excluded from the documentation site. -->
# Blender parity survey
## Current parity matrix
| Area | Status | Evidence and limits |
| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Uniform ordinary controls | Implemented | Number, integer, enum, boolean, string, and vector fields share composition-ordered widget-unit rows. Colors use compact swatches backed by a worker-rendered Oklch/RGBA/HSV/hex picker. Compound widgets intentionally span multiple rows. |
| Field reset | Implemented | Backspace resets the focused or hovered editable field; linked/read-only fields are inert; reset is one-step undoable. |
| Color Ramp | Implemented structurally and behaviorally | Browser-tested stop selection/drag, add/remove, modes, interpolation, hue mode, popup color edits, flip, distribute, reset, cancellation, and undo. Eyedropper integration remains unavailable. |
| Noise Texture | Implemented structurally | All Blender 4.5 dimensions/type visibility combinations are exhaustively tested. fxnode does not evaluate noise. |
| Shader Image Texture | Implemented as editor intent | Local image selection, worker decoding/thumbnail rendering, projection/interpolation/extension, and Box-only Blend are browser-tested. Texture evaluation remains outside scope. |
| Compositor Image | Partial by design | Image-user fields are represented and browser-tested. Dynamic multilayer/view/pass sockets require host resource metadata. |
| Color Balance | Implemented structurally | Lift/Gamma/Gain, Offset/Power/Slope, and White Point layouts are browser-tested. “Master Color Grading” is an example label, not a Blender type. |
| Knife and link mute | Implemented | Ctrl-RMB and Ctrl-Alt-RMB freehand gestures are browser-tested as one atomic version/history entry. |
| Node mute | Implemented | `M` grays every ordinary known node; declared compatible pairs additionally receive derived red bypass curves. There is no graph evaluation. |
| Pixel-exact Blender appearance | Not established | Blender captures remain 0/8 pending. Playwright images are fxnode regression and structural-parity evidence only. |
## Image Texture and Compositor Image
References: Blender 4.5's [Image Texture manual](https://docs.blender.org/manual/en/4.5/render/shader_nodes/textures/image.html), [Compositor Image manual](https://docs.blender.org/manual/en/4.5/compositing/types/input/image.html), [`ShaderNodeTexImage` RNA](https://docs.blender.org/api/4.5/bpy.types.ShaderNodeTexImage.html), and [`CompositorNodeImage` RNA](https://docs.blender.org/api/4.5/bpy.types.CompositorNodeImage.html).
These are intentionally distinct example composition definitions. Image Texture persists an image reference plus interpolation, projection (including Box-only Blend), extension, editor color-space intent, alpha mode, Vector input, and Color/Alpha outputs. Compositor Image persists its data-block reference and source; Movie/Sequence expose frame count, start, offset, cyclic, and auto-refresh. It has only static Image/Alpha/Z outputs. Resource controls synchronously open a host file chooser, transfer bytes into the worker, decode there, and render a bounded-cache thumbnail. The graph stores only a serializable local reference—not image bytes—so loading that graph in a fresh editor shows the filename/unavailable state until the user reopens the file. Neither node evaluates image pixels. Dynamic multilayer/render passes require a future host metadata provider and are not fabricated.
## Color Balance / “Master Color Grading” example
References: Blender 4.5's [Color Balance manual](https://docs.blender.org/manual/en/4.5/compositing/types/color/adjust/color_balance.html), [`CompositorNodeColorBalance` RNA](https://docs.blender.org/api/4.5/bpy.types.CompositorNodeColorBalance.html), and [commit-pinned Blender compositor node source](https://projects.blender.org/blender/blender/src/commit/8cb6b388974a817afedf1317ce26f0c75aa5f181/source/blender/nodes/composite/nodes/node_composite_colorbalance.cc).
The example definition remains **Color Balance**. “Master Color Grading” is only the parity fixture's custom instance label; Blender has no core Master node. Lift/Gamma/Gain and Offset/Power/Slope use paired scalar/color rows. White Point exposes Input and Output temperature, tint, and color sections. Eyedroppers are visibly disabled placeholders pending a host bridge. This is presentation and persistence only; fxnode performs no compositing evaluation.
## Color Ramp
References: [Blender 4.5 Color Ramp manual](https://docs.blender.org/manual/en/4.5/modeling/geometry_nodes/utilities/color_ramp.html), [Blender 4.5 `ColorRamp` RNA](https://docs.blender.org/api/4.5/bpy.types.ColorRamp.html), and [commit-pinned Blender source](https://projects.blender.org/blender/blender/src/commit/8cb6b388974a817afedf1317ce26f0c75aa5f181/source/blender/editors/interface/templates/interface_template_color_ramp.cc).
The V2 persisted value records RGB/HSV/HSL mode, all five interpolation modes, hue interpolation, and two to 32 sorted, identified RGBA stops. Legacy arrays receive stable index-derived IDs. Worker-owned interactions cover overlapping selection, sampled insertion, Blender-style midpoint insertion, removal, clamped/reordering movement, RGBA updates, flip, even distribution, descriptor reset, cancellation, and one-step undo. The compound row owns toolbar/menu, checker-gradient, handle, selector, position, and color bounds; stops are not sockets. Active-stop selection remains transient and is not serialized.
Deferred Blender details: eyedropper, precise Blender cardinal/B-spline kernels and HSL conversion, context popup styling, and keyboard navigation within popup menus.
## Noise Texture (Blender 4.5)
References: [Blender 4.5 Noise Texture manual](https://docs.blender.org/manual/en/4.5/render/shader_nodes/textures/noise.html) and [Blender 4.5 ShaderNodeTexNoise RNA](https://docs.blender.org/api/4.5/bpy.types.ShaderNodeTexNoise.html).
Dimensions and fractal Type drive V2 `in`/`equals` visibility expressions. Vector is shown for 2D/3D/4D, W for 1D/4D, Normalize only for fBM, Offset for Hybrid/Ridged/Hetero, and Gain for Hybrid/Ridged. Links and defaults remain in the document while their rows are hidden.
## Link knife and mute
`Ctrl`+RMB draws a captured freehand knife and atomically removes every crossed visible effective link. `Ctrl`+`Alt`+RMB uses the same gesture to toggle authored link mute instead; muted and reroute-propagated links are red and do not suppress input defaults. Escape, pointer cancellation, and blur cancel without history. Each completed gesture is one version/event/history entry.
`M` toggles mute for selected ordinary known nodes, including generators. Every muted node receives a clipped neutral overlay. Math, Vector Math, Mix, Set Position, and Transform Geometry additionally show explicitly declared, type-compatible red bypass curves; generator and type-incompatible nodes intentionally have no fabricated bypass. Bypasses are layout-only; fxnode still does not evaluate graphs. Collapse state is immediate and undoable while the header chevron rotates between expanded/down and collapsed/right with a short worker-owned animation.
### Shortcut table
| Shortcut | Behavior |
| --------------------- | ------------------------------------- |
| RMB on empty canvas | Open searchable add-node dialog |
| `Ctrl`+RMB drag | Knife/remove crossed effective links |
| `Ctrl`+`Alt`+RMB drag | Toggle mute on crossed authored links |
| `M` | Toggle selected node mute |
| `Escape` | Cancel active gesture silently |
+24
View File
@@ -0,0 +1,24 @@
<!-- Repository research; excluded from the documentation site. -->
# Phase 7 spatial performance
The deterministic stress workload contains exactly **5,000 node rectangles** and
**10,000 link rectangles** (seed 7). Nodes are arranged on a sparse 100 × 50
grid and links connect nearby nodes. The query viewport is 1200 × 800 logical
pixels at DPR 2. CI runs `npm run check:performance`; it checks exact totals and
requires at least 90% culling at candidate p95. Timing is reported, never gated.
Measured in the Amp orb on 2026-07-20: Node 20.9.0, Playwright/Chromium 1.49.1,
generic orb CPU (Intel Xeon 2.60 GHz). Across 100 deterministic viewport queries:
| metric | result |
| -------------- | ----------: |
| index build | 32.604 ms |
| candidates p50 | 72 / 15,000 |
| candidates p95 | 75 / 15,000 |
| query p50 | 0.295 ms |
| query p95 | 0.431 ms |
| p95 culling | 99.50% |
These values are one-orb observations, not performance guarantees. Candidate
counts and the 90% ratio are deterministic; elapsed times vary by host load.