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:
+122
@@ -0,0 +1,122 @@
|
||||
# fxnode orb workflow
|
||||
|
||||
This repository is a TypeScript node-editor library with a browser client and worker-owned authority. This guide is
|
||||
the operating checklist for Amp orbs and future implementation threads.
|
||||
|
||||
## Environment and services
|
||||
|
||||
- No `.env` file or secrets are required for development or tests.
|
||||
- `.agents/setup` installs the exact `package-lock.json` dependency tree, Chromium, Firefox, and their Linux runtime
|
||||
dependencies. It then typechecks the repository and starts declared services.
|
||||
- `.agents/resume` only reconciles declared services. Keep it fast and idempotent; do not install dependencies there.
|
||||
- `.amp/services.yaml` declares the examples gallery. Use `amp orb services ensure` to start it and obtain its portal.
|
||||
Inspect it with `amp orb service status examples` and `amp orb service logs examples`.
|
||||
- Run `npm run examples` outside an orb when a local Vite server is sufficient. Vite uses `examples/` as its root.
|
||||
- Generated `.amp/portals/` manifests and resume logs are local runtime state and must not be committed.
|
||||
|
||||
## Architectural invariants
|
||||
|
||||
Preserve these unless the user explicitly requests an architectural change:
|
||||
|
||||
1. The main-thread root is a message-passing facade. Graph state, composition, history, commands, selection behavior,
|
||||
and rendering authority live in the worker.
|
||||
2. A root can be headless or own multiple attached views. Views share graph/history while camera, selection, pointer
|
||||
lane, host requests, and render barriers remain view-local.
|
||||
3. The worker lazily owns one `OffscreenCanvas` atlas and one retained worker 2D context. Views occupy atlas slots;
|
||||
paint and cropped bitmap production are serialized. The final detach releases the atlas.
|
||||
4. Main-thread HTML canvases necessarily have one presentation context each. Do not describe the design as one
|
||||
context across the entire browser application.
|
||||
5. RAF polling remains continuous for responsive shared-pointer-lane input, but layout, atlas paint, bitmap creation,
|
||||
worker frame messages, and presentation remain dirty/on-demand.
|
||||
6. `FxNodeView.setViewport()` is acknowledged and transactional. Runtime hosts await it before changing canvas backing
|
||||
dimensions. Surface generations and dimensions reject stale or incoherent frames.
|
||||
7. The core library does not register DOM listeners, observers, menus, modals, or file pickers. Those are application
|
||||
policy. The example browser host demonstrates explicit cleanup and opt-in detach-on-disconnect behavior.
|
||||
8. Composition definitions are collections installed through `setTheme`, `setHeaderStyles`, `composeSocket`, and
|
||||
`composeNode`; initial graph layout is applied afterward with `setState`.
|
||||
9. Durable save/load uses command journals plus save-time composition. `setState`/`getState` are portable layout/state
|
||||
APIs, not the preferred persistence mechanism.
|
||||
10. Commands distinguish durable graph mutations from transient UI previews and provide undo/redo semantics.
|
||||
|
||||
The implementation history and Oracle-reviewed atlas/multi-view decisions are in the originating
|
||||
[Amp thread](https://ampcode.com/threads/T-019f806f-11fe-74ef-a1f1-90933c1dc543). Treat the current code, tests, README,
|
||||
and authored docs as authoritative when they differ from historical discussion.
|
||||
|
||||
## Design and implementation process
|
||||
|
||||
1. Read the nearest code, applicable `AGENTS.md`, public types, protocol validators, and focused tests before editing.
|
||||
2. Prefer the smallest change at the existing ownership boundary. Remove obsolete paths rather than layering adapters.
|
||||
3. For cross-layer architectural work, ask Oracle for a phased plan. Before each phase ask for concrete implementation
|
||||
details; after each phase ask for a high-confidence blocker review. Address blockers before moving on.
|
||||
4. Keep protocol boundaries exact and hostile-safe. A public type change normally requires protocol validation, client,
|
||||
worker, declaration/type tests, docs, and real-worker browser coverage.
|
||||
5. Keep DOM policy outside `src/browser/client.ts`. Extend application hosts or examples instead of secretly attaching
|
||||
listeners in `attachView()`.
|
||||
6. For rendering changes, reason explicitly about device/logical coordinates, DPR, atlas slot clipping, transforms,
|
||||
`putImageData` ignoring clip/CTM, bitmap ownership/closing, frame ACKs, and context-loss generations.
|
||||
7. Do not revert unrelated worktree changes. Do not commit generated `.amp` runtime files. Commit/push only when asked.
|
||||
|
||||
## Verification ladder
|
||||
|
||||
Choose the narrowest useful checks while iterating, then broaden according to blast radius.
|
||||
|
||||
### Fast and focused
|
||||
|
||||
```sh
|
||||
npx prettier --write <touched-files>
|
||||
npm run typecheck -- --pretty false
|
||||
node --import tsx --test test/<focused>.test.ts
|
||||
npx playwright test test/browser/<focused>.spec.ts --config playwright.config.ts --project chromium
|
||||
```
|
||||
|
||||
For multi-view or worker lifecycle changes, run focused tests in both supported engines:
|
||||
|
||||
```sh
|
||||
npx playwright test \
|
||||
test/browser/client-multiview.spec.ts \
|
||||
test/browser/worker-multiview.spec.ts \
|
||||
--config playwright.config.ts
|
||||
```
|
||||
|
||||
### Test groups
|
||||
|
||||
```sh
|
||||
npm test # Node unit, protocol, layout, persistence, atlas, and scheduler tests
|
||||
npm run test:browser # Real browser behavior in Chromium and Firefox
|
||||
npm run test:visual # Core Chromium screenshot baselines
|
||||
npm run test:examples:visual # Documentation/example screenshots
|
||||
npm run check:performance # Large-layout performance budgets
|
||||
npm run check:docs # TypeDoc generation plus VitePress build
|
||||
npm run check:readme # README structure and link checks
|
||||
npm run check:package # Packed-package and worker-asset smoke test
|
||||
npm run build # Vite library bundle and declaration build
|
||||
```
|
||||
|
||||
### Full release gate
|
||||
|
||||
Run this after shared contracts, architecture, rendering, examples, persistence, or release-facing docs change:
|
||||
|
||||
```sh
|
||||
npm run release:check
|
||||
git diff --check
|
||||
npm run format:check
|
||||
```
|
||||
|
||||
`release:check` includes typecheck, all unit/browser/visual/example tests, fixture and reference checks, performance,
|
||||
build, docs, README, composition, and package smoke verification.
|
||||
|
||||
## Visual review policy
|
||||
|
||||
- Never update screenshots merely to make a failure green.
|
||||
- Inspect expected, actual, and diff images under `test-results/`. Confirm whether changes are intended and localized.
|
||||
- Use `npm run test:visual:update` only for reviewed core baseline changes.
|
||||
- Use `npm run test:examples:visual:update` only for reviewed example/documentation image changes.
|
||||
- Re-run the corresponding non-update command after refreshing a baseline.
|
||||
- Save one-off user-review screenshots under `.amp/in/artifacts/`; keep transient inspection images in `test-results/`.
|
||||
|
||||
## Documentation expectations
|
||||
|
||||
- README and VitePress learning docs explain user workflows and ownership boundaries.
|
||||
- TypeDoc is the API contract; regenerate it with `npm run docs:api` after public type changes.
|
||||
- Examples import library source through `@lib/`, never relative `../src` paths.
|
||||
- Keep the examples gallery linked to every standalone application and relevant focused scene.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p .amp
|
||||
echo "[fxnode resume] ensuring declared orb services"
|
||||
amp orb services ensure >.amp/resume-services.log 2>&1
|
||||
echo "[fxnode resume] ready"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "[fxnode setup] installing locked npm dependencies"
|
||||
npm ci
|
||||
|
||||
echo "[fxnode setup] installing Playwright browsers and Linux dependencies"
|
||||
npx playwright install --with-deps chromium firefox
|
||||
|
||||
echo "[fxnode setup] checking the TypeScript environment"
|
||||
npm run typecheck -- --pretty false
|
||||
|
||||
echo "[fxnode setup] ensuring declared orb services"
|
||||
amp orb services ensure
|
||||
|
||||
echo "[fxnode setup] complete; see .agents/ORB_WORKFLOW.md for development and verification guidance"
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
examples:
|
||||
command: npm run examples -- --port "$PORT"
|
||||
portal:
|
||||
title: fxnode examples
|
||||
description: Browse the interactive examples gallery and focused Blender-inspired test scenes.
|
||||
@@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.blend binary
|
||||
@@ -0,0 +1,16 @@
|
||||
node_modules/
|
||||
dist/
|
||||
docs/reference/generated/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
*.blend
|
||||
*.blend1
|
||||
.DS_Store
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.amp/portals/
|
||||
.amp/resume-services.log
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
node_modules/
|
||||
dist/
|
||||
docs/reference/generated/
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
.amp/
|
||||
|
||||
# Canonical generated data is formatted by its generator and byte-checked.
|
||||
examples/blender/initialLayout.json
|
||||
examples/blender/**/initialLayout.json
|
||||
|
||||
# Binary and captured reference assets.
|
||||
**/*.png
|
||||
**/*.blend
|
||||
**/*.blend1
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
# fxnode agent guidance
|
||||
|
||||
Read [`.agents/ORB_WORKFLOW.md`](.agents/ORB_WORKFLOW.md) before changing architecture, browser/worker boundaries,
|
||||
examples, tests, or documentation. It records the repository's design invariants, phased review process, test ladder,
|
||||
visual-baseline policy, and orb service workflow.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 fxnode contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
# Reference notice
|
||||
|
||||
Blender is free software from the Blender Foundation and contributors, licensed under GNU GPL. “Blender” is used here only to identify the researched application. This project is not affiliated with or endorsed by the Blender Foundation.
|
||||
|
||||
`tools/blender/create-reference-fixtures.py` is project-authored fixture automation and is licensed under **GPL-3.0-or-later** because it executes through Blender's Python API. All other project-authored content is under the package license unless noted.
|
||||
|
||||
No Blender source, bundled assets, icons, fonts, or Blender Manual screenshots are distributed here. Future files in `docs/research/blender-references/4.5.0/` must be whole-window screenshots captured by this project from the verified baseline binary; their hashes and capture metadata must be recorded in the manifest.
|
||||
Vendored
+326
@@ -0,0 +1,326 @@
|
||||
# fxnode
|
||||
|
||||
> **Internal private 0.x prerelease.** This package has `private: true`; its API and data formats may change.
|
||||
|
||||

|
||||
|
||||
_The Color Balance example rendered by fxnode. It demonstrates editor presentation only; fxnode does not process pixels._
|
||||
|
||||
fxnode is a typed, worker-owned node editor for application-defined nodes, sockets, styles, resources, and migrations.
|
||||
One root can run headlessly or attach multiple independent canvas views to the same graph and worker. The application
|
||||
explicitly owns its DOM integration: canvas sizing, input forwarding, menus, file pickers, accessibility, lifecycle,
|
||||
and cleanup. The worker owns graph state, validation, history, hit testing, layout, and frame generation; the browser
|
||||
client presents transferred frames without keeping a shadow graph.
|
||||
|
||||
fxnode edits and presents graphs. It does **not** execute or evaluate graphs and contains no image-processing engine.
|
||||
It does not read or write Blender files, and makes no Blender compatibility, affiliation, endorsement, or parity claim.
|
||||
|
||||
## Install and platform requirements
|
||||
|
||||
```sh
|
||||
npm install fxnode
|
||||
```
|
||||
|
||||
That command is for a future registry release; this repository is currently private and not publishable.
|
||||
Consumers need a modern browser with module workers, Canvas 2D, and worker-side `OffscreenCanvas`/`ImageBitmap` support.
|
||||
See the committed [browser support matrix](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/guides/browser-support.md) and
|
||||
[Content Security Policy guide](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/guides/csp.md) before integrating.
|
||||
|
||||
## The simplest node
|
||||
|
||||
Definitions are serializable, consumer-facing tuples. This complete definition matches the executable
|
||||
[minimal definition](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/definition.ts):
|
||||
|
||||
```ts
|
||||
import type { FxNodeDefinition, FxNodeSocketTypeDefinition, FxNodeStyleDefinition } from "fxnode";
|
||||
|
||||
export const numberSocket = [
|
||||
"number",
|
||||
{ title: "Number", color: "#a8a8a8", acceptsFrom: ["number"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
|
||||
export const minimalStyles = {
|
||||
value: { header: "#4c6ef5" },
|
||||
} as const satisfies Readonly<Record<string, FxNodeStyleDefinition>>;
|
||||
|
||||
export const valueNode = [
|
||||
"example.minimal.value",
|
||||
{
|
||||
version: 1,
|
||||
title: "Number Value",
|
||||
behavior: "standard",
|
||||
style: "value",
|
||||
parameters: {
|
||||
value: { type: "number", default: { kind: "number", value: 42 }, step: 1 },
|
||||
},
|
||||
sockets: {
|
||||
value: {
|
||||
title: "Value",
|
||||
direction: "output",
|
||||
type: "number",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{ kind: "parameter", parameter: "value" },
|
||||
{ kind: "socket", socket: "value" },
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
```
|
||||
|
||||
Bootstrap the editor after creating an application-local host and theme:
|
||||
|
||||
```ts
|
||||
import { createFxNode } from "fxnode";
|
||||
import { prepareFxNodeBrowserHost } from "./browser-host.js";
|
||||
import { exampleTheme } from "./theme.js";
|
||||
import { minimalStyles, numberSocket, valueNode } from "./definition.js";
|
||||
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#graph")!;
|
||||
const host = prepareFxNodeBrowserHost({ canvas });
|
||||
let cleaned = false;
|
||||
let api: Awaited<ReturnType<typeof createFxNode>> | null = null;
|
||||
let view: Awaited<ReturnType<Awaited<ReturnType<typeof createFxNode>>["attachView"]>> | null = null;
|
||||
function cleanup() {
|
||||
window.removeEventListener("pagehide", cleanup);
|
||||
cleaned = true;
|
||||
const root = api;
|
||||
api = null;
|
||||
host.destroy();
|
||||
const destroyRoot = () => root?.destroy();
|
||||
if (view) void view.detach().then(destroyRoot, destroyRoot);
|
||||
else destroyRoot();
|
||||
view = null;
|
||||
}
|
||||
window.addEventListener("pagehide", cleanup);
|
||||
try {
|
||||
const created = await createFxNode({
|
||||
applicationId: "fxnode.example.minimal",
|
||||
applicationVersion: 1,
|
||||
resources: {},
|
||||
});
|
||||
if (cleaned) created.destroy();
|
||||
else {
|
||||
api = created;
|
||||
await api.setTheme(exampleTheme);
|
||||
await api.setHeaderStyles(minimalStyles);
|
||||
await api.composeSocket(...numberSocket);
|
||||
await api.composeNode(...valueNode);
|
||||
await api.setState({ graphId: "minimal", catalogVersion: 1, nodes: [], links: [], metadata: {} });
|
||||
view = await api.attachView({ canvas, viewport: host.initialViewport });
|
||||
host.attach(api, view);
|
||||
await view.addNode({ nodeId: "value", typeId: valueNode[0], viewPosition: { x: 360, y: 190 } });
|
||||
await view.whenRendered();
|
||||
}
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
`exampleTheme` and `prepareFxNodeBrowserHost` are application-local examples, not fxnode package exports.
|
||||

|
||||
|
||||
Complete sources: [definition](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/definition.ts), [bootstrap](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/main.ts), and
|
||||
[first-node tutorial](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/tutorials/first-node.md).
|
||||
|
||||
## One graph, zero or many views
|
||||
|
||||
`createFxNode()` creates the shared graph authority and starts one worker, but creates no canvas. This is valid for
|
||||
headless browser workflows. Call `attachView()` whenever the application needs a presentation. Each view has its own
|
||||
canvas, viewport, camera, selection, input stream, host requests, render barriers, and lifecycle; graph state,
|
||||
composition, versions, events, persistence, and undo/redo remain shared on the root.
|
||||
|
||||
All views render through one worker-owned atlas canvas and one 2D rendering context. Each HTML canvas keeps its own
|
||||
presentation context. `FXNODE_VIEW_LIMITS.maxViews` is a resource bound, not a count of worker rendering contexts.
|
||||
|
||||
```ts
|
||||
const left = await api.attachView({
|
||||
canvas: leftCanvas,
|
||||
viewport: leftViewport,
|
||||
initialCamera: { center: { x: 480, y: -550 }, zoom: 0.5 },
|
||||
});
|
||||
const right = await api.attachView({
|
||||
canvas: rightCanvas,
|
||||
viewport: rightViewport,
|
||||
initialCamera: { center: { x: 2080, y: -400 }, zoom: 0.45 },
|
||||
});
|
||||
|
||||
left.feedInput(pointerInputFromLeftCanvas);
|
||||
await left.addNode({ typeId: "fxnode.shader.noise-texture", viewPosition: { x: 400, y: 260 } });
|
||||
await Promise.all([left.whenRendered(), right.whenRendered()]);
|
||||
|
||||
await Promise.all([left.detach(), right.detach()]);
|
||||
api.destroy();
|
||||
```
|
||||
|
||||
View-local `addNode`, `removeSelected`, and `setSelectedMuted` use that view's camera and selection. Root-level
|
||||
`dispatch`, `undo`, and `redo` are useful when no view context is required. A canvas can belong to only one live view.
|
||||
The application must detach its view before reusing that canvas.
|
||||
|
||||

|
||||
|
||||
The [multi-view example](https://github.com/Heaust-ops/fxnode/tree/main/examples/multi-view) uses a single DOM toolbar
|
||||
to target whichever canvas was most recently activated. Its canvases forward pointer events only; menus, controls,
|
||||
resize observation, and teardown remain application code.
|
||||
|
||||
## Focused example: Color Balance
|
||||
|
||||
The Color Balance example keeps strict local socket tuples and styles, then composes its larger node definition:
|
||||
|
||||
```ts
|
||||
import type { FxNode, FxNodeSocketTypeDefinition, FxNodeStyleDefinition } from "fxnode";
|
||||
import { colorBalanceNode } from "./color-balance.js";
|
||||
import { exampleTheme } from "./theme.js";
|
||||
|
||||
const floatSocket = [
|
||||
"float",
|
||||
{ title: "Float", color: "#a8a8a8", acceptsFrom: ["float"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
const colorSocket = [
|
||||
"color",
|
||||
{ title: "Color", color: "#d7ca63", acceptsFrom: ["color"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
const styles = {
|
||||
compositorColor: { header: "#8c5cc4" },
|
||||
} as const satisfies Readonly<Record<string, FxNodeStyleDefinition>>;
|
||||
|
||||
async function installColorBalance(api: FxNode) {
|
||||
await api.setTheme(exampleTheme);
|
||||
await api.setHeaderStyles(styles);
|
||||
await api.composeSocket(...floatSocket);
|
||||
await api.composeSocket(...colorSocket);
|
||||
await api.composeNode(...colorBalanceNode);
|
||||
}
|
||||
```
|
||||
|
||||
The node's UI schema includes a representative grading-wheels row:
|
||||
|
||||
```ts
|
||||
import type { FxNodeDefinition } from "fxnode";
|
||||
|
||||
const gradingWheelsRow = {
|
||||
kind: "widget",
|
||||
widget: "grading-wheels",
|
||||
bindings: [
|
||||
{ title: "Lift", scalar: "lift", color: "liftColor" },
|
||||
{ title: "Gamma", scalar: "gamma", color: "gammaColor" },
|
||||
{ title: "Gain", scalar: "gain", color: "gainColor" },
|
||||
],
|
||||
visibleWhen: { parameter: "mode", equals: "Lift/Gamma/Gain" },
|
||||
} satisfies FxNodeDefinition["ui"][number];
|
||||
```
|
||||
|
||||
See the complete [bootstrap](https://github.com/Heaust-ops/fxnode/blob/main/examples/color-balance/main.ts),
|
||||
[definition](https://github.com/Heaust-ops/fxnode/blob/main/examples/shared/nodes/color-balance.ts), and
|
||||
[tutorial](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/tutorials/color-balance.md). This schema renders controls and connections; it does not evaluate
|
||||
the graph or process pixels.
|
||||
|
||||
## Application-owned browser host
|
||||
|
||||
fxnode deliberately does not install global listeners or create application UI. A compact host should:
|
||||
|
||||
- measure and DPR-size the canvas, observe resize, and call `setViewport`;
|
||||
- translate pointer, wheel, keyboard, focus, and outside-pointer events into `feedInput`;
|
||||
- manage pointer capture, focus, context menus, add-node UI, and authorized resource pickers;
|
||||
- subscribe to bounded host projections/requests and provide an accessible DOM workflow; and
|
||||
- remove listeners, subscriptions, observers, temporary DOM, and captures during teardown.
|
||||
|
||||
The example host defaults to explicit lifecycle ownership. Its opt-in `lifecycle: "detach-on-disconnect"` policy
|
||||
uses a document-level observer to tear down host policy and detach the view after a canvas remains disconnected for a
|
||||
microtask; same-task moves are preserved. `host.destroy()` itself never detaches the view.
|
||||
|
||||
The host receives both the shared `FxNode` root and one `FxNodeView`. Route graph subscriptions and composition calls
|
||||
to the root; route canvas input, viewport updates, selection actions, host requests, resources, and rendering to the
|
||||
view. For multiple canvases, create one host (or equivalent listener owner) per view.
|
||||
|
||||
Use the [browser-host guide](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/guides/browser-host.md),
|
||||
[interaction guide](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/guides/interactions.md), and the repository's
|
||||
[compact host implementation](https://github.com/Heaust-ops/fxnode/blob/main/examples/shared/browser-host.ts).
|
||||
|
||||
## State, persistence, and events
|
||||
|
||||
The worker is authoritative. All calls are asynchronous unless their signature says otherwise.
|
||||
|
||||
| API | Meaning |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `getState()` | Detached, readonly current graph snapshot with graph version; does not mutate runtime state. |
|
||||
| `setState(value)` | Validates and atomically replaces graph state; supports optimistic `expectedVersion`. |
|
||||
| `save()` | Canonical current `GraphLayoutV2`; a compact graph export, not command history. |
|
||||
| `getSaveData()` | Durable envelope: canonical baseline, applied journal, and effective save-time composition. |
|
||||
| `load(value)` | Atomically validates/loads durable data; failure preserves current state. |
|
||||
|
||||
Committed graph changes publish mutations **before** snapshots in version order. `onMutations` and `onSnapshots`
|
||||
do not mutate state; each returns an unsubscribe function, and subscriber failures are isolated. Composition has a
|
||||
separate revision domain and `onCompositionChanges`; a rebind that changes the graph publishes its composition event
|
||||
before the corresponding mutation and snapshot. Read [graph state and events](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/concepts/graph-state-and-events.md)
|
||||
and [state and persistence](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/concepts/state-and-persistence.md).
|
||||
|
||||
## Headless use
|
||||
|
||||
`fxnode/headless` exposes the same composition-bound document and command authority without browser or worker
|
||||
resources. With the minimal tuples above:
|
||||
|
||||
```ts
|
||||
import { createFxNodeHeadless } from "fxnode/headless";
|
||||
import { exampleTheme } from "./theme.js";
|
||||
|
||||
const runtime = createFxNodeHeadless({
|
||||
schemaVersion: 2,
|
||||
id: "fxnode.example.minimal",
|
||||
version: 1,
|
||||
compatibility: { wildcardInputTypes: [] },
|
||||
theme: exampleTheme,
|
||||
socketTypes: { [numberSocket[0]]: numberSocket[1] },
|
||||
nodeStyles: minimalStyles,
|
||||
resources: {},
|
||||
nodes: { [valueNode[0]]: valueNode[1] },
|
||||
} as const);
|
||||
|
||||
const empty = runtime.emptyDocument("minimal");
|
||||
const document = {
|
||||
...empty,
|
||||
nodes: { value: runtime.materializeNode("value", valueNode[0], { x: 360, y: 190 }) },
|
||||
};
|
||||
const issues = runtime.validateDocument(document);
|
||||
if (issues.length) throw new Error(issues.map((issue) => issue.message).join("; "));
|
||||
const layout = runtime.save(document);
|
||||
```
|
||||
|
||||
Headless operations are explicit and immutable; they still edit graph documents and never evaluate them.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Learn](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/index.md): tutorials, concepts, integration guides, and examples
|
||||
- [Concepts](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/concepts/index.md): worker authority, composition, state, and persistence
|
||||
- [Guides](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/guides/index.md): browser hosting, lifecycle, accessibility, CSP, and support
|
||||
- [Tutorials](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/tutorials/index.md): minimal, Color Balance, and live composition
|
||||
- [API reference landing page](https://github.com/Heaust-ops/fxnode/blob/main/docs/reference/index.md)
|
||||
- [Research notes](https://github.com/Heaust-ops/fxnode/blob/main/docs/research/blender.md) and [architecture decision](https://github.com/Heaust-ops/fxnode/blob/main/docs/decisions/worker-transport-protobuf-benchmark.md)
|
||||
|
||||
## Development
|
||||
|
||||
Requires Node.js 20 or newer.
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run typecheck # TypeScript checks
|
||||
npm test # Node test suite
|
||||
npm run build # Vite library build + declarations
|
||||
npm run examples # Example gallery development server
|
||||
npm run test:examples:visual # Example screenshot checks
|
||||
npm run docs:dev # Generated API + VitePress development server
|
||||
npm run docs:build # Build generated API and documentation
|
||||
npm run format:check # Prettier verification
|
||||
npm run release:check # Full release gate (maintainers)
|
||||
```
|
||||
|
||||
For contribution context, start with the executable [examples](https://github.com/Heaust-ops/fxnode/tree/main/examples) and committed
|
||||
[Learn landing page](https://github.com/Heaust-ops/fxnode/blob/main/docs/learn/index.md). The MIT license is in [LICENSE](LICENSE); notices are in
|
||||
[NOTICE.md](NOTICE.md).
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
# fxnode upstream provenance
|
||||
|
||||
- Source: https://github.com/Heaust-ops/fxnode
|
||||
- Commit: `3f8745717bf4574577be72e9769373475cc300c9`
|
||||
- Tree: `fb8bf407854b68dad8c0249c9ba63c7fd0bd9332`
|
||||
- Imported: 2026-07-26
|
||||
- License: MIT (see `LICENSE` and `NOTICE.md`)
|
||||
- Local patches: none
|
||||
|
||||
This directory is a committed source snapshot. Application adaptations belong outside
|
||||
`vendor/fxnode`; do not modify vendored source for integration convenience.
|
||||
|
||||
## Reimport
|
||||
|
||||
```sh
|
||||
git clone --filter=blob:none https://github.com/Heaust-ops/fxnode /tmp/fxnode
|
||||
git -C /tmp/fxnode fetch --depth=1 origin 3f8745717bf4574577be72e9769373475cc300c9
|
||||
git -C /tmp/fxnode checkout --detach 3f8745717bf4574577be72e9769373475cc300c9
|
||||
rm -rf vendor/fxnode
|
||||
mkdir -p vendor/fxnode
|
||||
git -C /tmp/fxnode archive HEAD | tar -x -C vendor/fxnode
|
||||
```
|
||||
|
||||
After importing, restore this file with the new commit/tree hashes and document any
|
||||
unavoidable local patches explicitly.
|
||||
+66
@@ -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.
|
||||
Vendored
+31
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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 minimal composition and one node. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/main.ts)._
|
||||
|
||||
## Color Balance
|
||||
|
||||

|
||||
|
||||
_A focused custom-widget composition. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/color-balance/main.ts)._
|
||||
|
||||
## Live composition
|
||||
|
||||

|
||||
|
||||
_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 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.
|
||||
@@ -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
@@ -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).
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
Vendored
+23
@@ -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/).
|
||||
@@ -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).
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 |
|
||||
@@ -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.
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>fxnode all supported</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="graph"></canvas>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
import { createApplicationFxNode } from "../application-browser.js";
|
||||
import { prepareFxNodeBrowserHost } from "../../shared/browser-host.js";
|
||||
import initialLayout from "./initialLayout.json" with { type: "json" };
|
||||
const canvas = document.querySelector("canvas")!,
|
||||
host = prepareFxNodeBrowserHost({ canvas });
|
||||
let root: Awaited<ReturnType<typeof createApplicationFxNode>> | undefined,
|
||||
view: Awaited<ReturnType<NonNullable<typeof root>["attachView"]>> | undefined;
|
||||
try {
|
||||
root = await createApplicationFxNode();
|
||||
await root.setState(initialLayout);
|
||||
view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
host.attach(root, view);
|
||||
(window as unknown as { fxnodeExample: unknown }).fxnodeExample = { root, view };
|
||||
await view.whenRendered();
|
||||
} catch (error) {
|
||||
host.destroy();
|
||||
await view?.detach();
|
||||
root?.destroy();
|
||||
throw error;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #202124;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { createFxNode, type CreateFxNodeOptions, type FxNode } from "@lib/index.js";
|
||||
import {
|
||||
APPLICATION_HEADER_STYLES,
|
||||
APPLICATION_ID,
|
||||
APPLICATION_RESOURCES,
|
||||
APPLICATION_VERSION,
|
||||
applicationCompatibility,
|
||||
} from "./nodes/application.js";
|
||||
import { exampleTheme } from "../shared/theme.js";
|
||||
import { frameNode } from "./nodes/common/frame.js";
|
||||
import { rerouteNode } from "./nodes/common/reroute.js";
|
||||
import {
|
||||
anySocket,
|
||||
floatSocket,
|
||||
vectorSocket,
|
||||
colorSocket,
|
||||
shaderSocket,
|
||||
geometrySocket,
|
||||
} from "./nodes/socket-types.js";
|
||||
import { groupInputNode } from "./nodes/common/group-input.js";
|
||||
import { groupOutputNode } from "./nodes/common/group-output.js";
|
||||
import { valueNode } from "./nodes/shader/value.js";
|
||||
import { colorNode } from "./nodes/shader/color.js";
|
||||
import { mathNode } from "./nodes/shader/math.js";
|
||||
import { vectorMathNode } from "./nodes/shader/vector-math.js";
|
||||
import { mixNode } from "./nodes/shader/mix.js";
|
||||
import { colorRampNode } from "./nodes/shader/color-ramp.js";
|
||||
import { textureCoordinateNode } from "./nodes/shader/texture-coordinate.js";
|
||||
import { noiseTextureNode } from "./nodes/shader/noise-texture.js";
|
||||
import { imageTextureNode } from "./nodes/shader/image-texture.js";
|
||||
import { principledBsdfNode } from "./nodes/shader/principled-bsdf.js";
|
||||
import { materialOutputNode } from "./nodes/shader/material-output.js";
|
||||
import { positionNode } from "./nodes/geometry/position.js";
|
||||
import { meshCubeNode } from "./nodes/geometry/mesh-cube.js";
|
||||
import { setPositionNode } from "./nodes/geometry/set-position.js";
|
||||
import { transformGeometryNode } from "./nodes/geometry/transform-geometry.js";
|
||||
import { joinGeometryNode } from "./nodes/geometry/join-geometry.js";
|
||||
import { imageNode } from "./nodes/compositor/image.js";
|
||||
import { colorBalanceNode } from "../shared/nodes/color-balance.js";
|
||||
export type ApplicationFxNode = FxNode;
|
||||
type ApplicationBrowserOptions = Omit<CreateFxNodeOptions, "applicationId" | "applicationVersion" | "resources">;
|
||||
export async function createApplicationFxNode(options: ApplicationBrowserOptions = {}): Promise<ApplicationFxNode> {
|
||||
const api = await createFxNode({
|
||||
...options,
|
||||
applicationId: APPLICATION_ID,
|
||||
applicationVersion: APPLICATION_VERSION,
|
||||
resources: APPLICATION_RESOURCES,
|
||||
});
|
||||
try {
|
||||
await api.setTheme(exampleTheme);
|
||||
await api.setHeaderStyles(APPLICATION_HEADER_STYLES);
|
||||
await api.composeSocket(...anySocket);
|
||||
await api.composeSocket(...floatSocket);
|
||||
await api.composeSocket(...vectorSocket);
|
||||
await api.composeSocket(...colorSocket);
|
||||
await api.composeSocket(...shaderSocket);
|
||||
await api.composeSocket(...geometrySocket);
|
||||
await api.setCompatibility(applicationCompatibility);
|
||||
await api.composeNode(...frameNode);
|
||||
await api.composeNode(...rerouteNode);
|
||||
await api.composeNode(...groupInputNode);
|
||||
await api.composeNode(...groupOutputNode);
|
||||
await api.composeNode(...valueNode);
|
||||
await api.composeNode(...colorNode);
|
||||
await api.composeNode(...mathNode);
|
||||
await api.composeNode(...vectorMathNode);
|
||||
await api.composeNode(...mixNode);
|
||||
await api.composeNode(...colorRampNode);
|
||||
await api.composeNode(...textureCoordinateNode);
|
||||
await api.composeNode(...noiseTextureNode);
|
||||
await api.composeNode(...imageTextureNode);
|
||||
await api.composeNode(...principledBsdfNode);
|
||||
await api.composeNode(...materialOutputNode);
|
||||
await api.composeNode(...positionNode);
|
||||
await api.composeNode(...meshCubeNode);
|
||||
await api.composeNode(...setPositionNode);
|
||||
await api.composeNode(...transformGeometryNode);
|
||||
await api.composeNode(...joinGeometryNode);
|
||||
await api.composeNode(...imageNode);
|
||||
await api.composeNode(...colorBalanceNode);
|
||||
return api;
|
||||
} catch (error) {
|
||||
api.destroy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>FxNode controls</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 1200px;
|
||||
height: 640px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="controls"></canvas>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"graphId": "control-test",
|
||||
"catalogVersion": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "color",
|
||||
"typeId": "fxnode.shader.color",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 300,
|
||||
"y": 200
|
||||
},
|
||||
"size": {
|
||||
"x": 151,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Color",
|
||||
"parameters": {
|
||||
"color": {
|
||||
"kind": "color",
|
||||
"value": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "color:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "group",
|
||||
"typeId": "fxnode.common.group-input",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -100,
|
||||
"y": -100
|
||||
},
|
||||
"size": {
|
||||
"x": 257,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Group Input",
|
||||
"parameters": {
|
||||
"interfaceName": {
|
||||
"kind": "string",
|
||||
"value": "Socket"
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "group:output",
|
||||
"key": "output",
|
||||
"label": "Interface",
|
||||
"direction": "output",
|
||||
"dataType": "any",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "math",
|
||||
"typeId": "fxnode.shader.math",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -250,
|
||||
"y": 200
|
||||
},
|
||||
"size": {
|
||||
"x": 192,
|
||||
"y": 148
|
||||
},
|
||||
"label": "Math",
|
||||
"parameters": {
|
||||
"operation": {
|
||||
"kind": "string",
|
||||
"value": "add"
|
||||
},
|
||||
"clamp": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "math:a",
|
||||
"key": "a",
|
||||
"label": "A",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math:b",
|
||||
"key": "b",
|
||||
"label": "B",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "value",
|
||||
"typeId": "fxnode.shader.value",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -500,
|
||||
"y": 200
|
||||
},
|
||||
"size": {
|
||||
"x": 151,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Value",
|
||||
"parameters": {
|
||||
"value": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "value:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "vector",
|
||||
"typeId": "fxnode.shader.vector-math",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 20,
|
||||
"y": 200
|
||||
},
|
||||
"size": {
|
||||
"x": 340,
|
||||
"y": 148
|
||||
},
|
||||
"label": "Vector Math",
|
||||
"parameters": {
|
||||
"operation": {
|
||||
"kind": "string",
|
||||
"value": "add"
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "vector:a",
|
||||
"key": "a",
|
||||
"label": "A",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vector:b",
|
||||
"key": "b",
|
||||
"label": "B",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vector:vector",
|
||||
"key": "vector",
|
||||
"label": "Vector",
|
||||
"direction": "output",
|
||||
"dataType": "vector",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "vector:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "value-math",
|
||||
"fromNodeId": "value",
|
||||
"fromSocketId": "value:value",
|
||||
"toNodeId": "math",
|
||||
"toSocketId": "math:a",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
}
|
||||
],
|
||||
"metadata": {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createApplicationFxNode, type ApplicationFxNode } from "../application-browser.js";
|
||||
import type { FxNodeView } from "@lib/index.js";
|
||||
import { prepareFxNodeBrowserHost } from "../../shared/browser-host.js";
|
||||
import initialLayout from "./initialLayout.json" with { type: "json" };
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#controls");
|
||||
if (!canvas) throw new Error("Control test canvas missing");
|
||||
const host = prepareFxNodeBrowserHost({ canvas });
|
||||
const handle: { root: ApplicationFxNode | null; view: FxNodeView | null; ready: Promise<void> } = {
|
||||
root: null,
|
||||
view: null,
|
||||
ready: Promise.resolve(),
|
||||
};
|
||||
window.controlTest = handle;
|
||||
handle.ready = (async () => {
|
||||
let root: ApplicationFxNode | undefined, view: FxNodeView | undefined;
|
||||
try {
|
||||
root = await createApplicationFxNode();
|
||||
await root.setState(initialLayout);
|
||||
view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
handle.root = root;
|
||||
handle.view = view;
|
||||
host.attach(root, view);
|
||||
await view.whenRendered();
|
||||
} catch (error) {
|
||||
host.destroy();
|
||||
await view?.detach();
|
||||
root?.destroy();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import type { FxNode, FxNodeView } from "@lib/index.js";
|
||||
import type { PreparedFxNodeBrowserHost } from "../shared/browser-host.js";
|
||||
|
||||
declare global {
|
||||
interface FxNodeExampleHandle {
|
||||
root: FxNode | null;
|
||||
view: FxNodeView | null;
|
||||
host: PreparedFxNodeBrowserHost;
|
||||
ready: Promise<void>;
|
||||
readonly rendered: Promise<void>;
|
||||
}
|
||||
interface Window {
|
||||
fxnodeExample: FxNodeExampleHandle;
|
||||
linkToolsTest: { root: FxNode | null; view: FxNodeView | null; ready: Promise<void> };
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>fxnode deterministic browser example</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>fxnode browser baseline</h1>
|
||||
<p>Deterministic fxnode regression composition; genuine Blender parity references remain pending.</p>
|
||||
<canvas id="graph" width="1200" height="640" aria-label="Material node graph"></canvas>
|
||||
</main>
|
||||
<template id="add-node-menu-template">
|
||||
<div data-fxnode-add-menu style="position: fixed; z-index: 2147483647; visibility: hidden">
|
||||
<style>
|
||||
[data-fxnode-add-menu] * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.fxnode-add-dialog {
|
||||
width: 286px;
|
||||
max-height: min(520px, calc(100vh - 16px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
background: #25282d;
|
||||
color: #e5e5e5;
|
||||
border: 1px solid #111216;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 12px 32px #000a;
|
||||
font:
|
||||
12px/1.3 system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.fxnode-add-title {
|
||||
font-weight: 650;
|
||||
margin: 1px 2px 7px;
|
||||
}
|
||||
.fxnode-add-search {
|
||||
width: 100%;
|
||||
border: 1px solid #111216;
|
||||
border-radius: 4px;
|
||||
background: #181a1e;
|
||||
color: #fff;
|
||||
padding: 7px 8px;
|
||||
outline: none;
|
||||
}
|
||||
.fxnode-add-search:focus {
|
||||
border-color: #ed5700;
|
||||
}
|
||||
.fxnode-add-results {
|
||||
overflow: auto;
|
||||
margin-top: 7px;
|
||||
min-height: 40px;
|
||||
}
|
||||
.fxnode-add-group {
|
||||
margin: 4px 0 7px;
|
||||
}
|
||||
.fxnode-add-heading {
|
||||
padding: 3px 7px;
|
||||
color: #a5a8ad;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
}
|
||||
.fxnode-add-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #e5e5e5;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: default;
|
||||
}
|
||||
.fxnode-add-option:hover,
|
||||
.fxnode-add-option.active {
|
||||
background: #ed5700;
|
||||
color: #fff;
|
||||
}
|
||||
.fxnode-add-empty {
|
||||
padding: 14px 8px;
|
||||
color: #a5a8ad;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<section class="fxnode-add-dialog" role="dialog" aria-label="Add node">
|
||||
<div class="fxnode-add-title">Add Node</div>
|
||||
<input
|
||||
data-fxnode-menu-search
|
||||
class="fxnode-add-search"
|
||||
type="search"
|
||||
role="combobox"
|
||||
aria-label="Search nodes"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="fxnode-node-results"
|
||||
autocomplete="off"
|
||||
placeholder="Search…"
|
||||
/>
|
||||
<div data-fxnode-menu-results class="fxnode-add-results" id="fxnode-node-results" role="listbox">
|
||||
<section data-fxnode-menu-group class="fxnode-add-group" role="group" aria-label="Common">
|
||||
<h2 data-fxnode-menu-heading class="fxnode-add-heading">Common</h2>
|
||||
<button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.common.frame"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Frame</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.common.reroute"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Reroute</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.common.group-input"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Group Input</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.common.group-output"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Group Output
|
||||
</button>
|
||||
</section>
|
||||
<section data-fxnode-menu-group class="fxnode-add-group" role="group" aria-label="Shader">
|
||||
<h2 data-fxnode-menu-heading class="fxnode-add-heading">Shader</h2>
|
||||
<button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.value"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Value</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.color"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Color</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.math"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Math</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.vector-math"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Vector Math</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.mix"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Mix</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.color-ramp"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Color Ramp</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.texture-coordinate"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Texture Coordinate</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.noise-texture"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Noise Texture</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.image-texture"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Image Texture</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.principled-bsdf"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Principled BSDF</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.shader.material-output"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Material Output
|
||||
</button>
|
||||
</section>
|
||||
<section data-fxnode-menu-group class="fxnode-add-group" role="group" aria-label="Geometry">
|
||||
<h2 data-fxnode-menu-heading class="fxnode-add-heading">Geometry</h2>
|
||||
<button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.geometry.position"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Position</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.geometry.mesh-cube"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Mesh Cube</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.geometry.set-position"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Set Position</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.geometry.transform-geometry"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Transform Geometry</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.geometry.join-geometry"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Join Geometry
|
||||
</button>
|
||||
</section>
|
||||
<section data-fxnode-menu-group class="fxnode-add-group" role="group" aria-label="Compositor">
|
||||
<h2 data-fxnode-menu-heading class="fxnode-add-heading">Compositor</h2>
|
||||
<button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.compositor.image"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Image</button
|
||||
><button
|
||||
data-fxnode-menu-option
|
||||
data-type-id="fxnode.compositor.color-balance"
|
||||
class="fxnode-add-option"
|
||||
type="button"
|
||||
role="option"
|
||||
>
|
||||
Color Balance
|
||||
</button>
|
||||
</section>
|
||||
<div data-fxnode-menu-empty class="fxnode-add-empty" role="status" hidden>No nodes found</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+697
@@ -0,0 +1,697 @@
|
||||
{
|
||||
"graphId": "phase-4-example",
|
||||
"catalogVersion": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "frame",
|
||||
"typeId": "fxnode.common.frame",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": -550,
|
||||
"y": 250
|
||||
},
|
||||
"size": {
|
||||
"x": 190,
|
||||
"y": 270
|
||||
},
|
||||
"label": "Surface Controls",
|
||||
"parameters": {},
|
||||
"sockets": [],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "math",
|
||||
"typeId": "fxnode.shader.math",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": -300,
|
||||
"y": 170
|
||||
},
|
||||
"size": {
|
||||
"x": 160,
|
||||
"y": 110
|
||||
},
|
||||
"label": "Math",
|
||||
"parameters": {
|
||||
"operation": {
|
||||
"kind": "string",
|
||||
"value": "add"
|
||||
},
|
||||
"clamp": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "math:a",
|
||||
"key": "a",
|
||||
"label": "A",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math:b",
|
||||
"key": "b",
|
||||
"label": "B",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "noise",
|
||||
"typeId": "fxnode.shader.noise-texture",
|
||||
"typeVersion": 2,
|
||||
"position": {
|
||||
"x": -100,
|
||||
"y": 190
|
||||
},
|
||||
"size": {
|
||||
"x": 165,
|
||||
"y": 130
|
||||
},
|
||||
"label": "Noise Texture",
|
||||
"parameters": {
|
||||
"dimensions": {
|
||||
"kind": "string",
|
||||
"value": "3d"
|
||||
},
|
||||
"noiseType": {
|
||||
"kind": "string",
|
||||
"value": "fbm"
|
||||
},
|
||||
"normalize": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "noise:vector",
|
||||
"key": "vector",
|
||||
"label": "Vector",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:w",
|
||||
"key": "w",
|
||||
"label": "W",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:scale",
|
||||
"key": "scale",
|
||||
"label": "Scale",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:detail",
|
||||
"key": "detail",
|
||||
"label": "Detail",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:roughness",
|
||||
"key": "roughness",
|
||||
"label": "Roughness",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:lacunarity",
|
||||
"key": "lacunarity",
|
||||
"label": "Lacunarity",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:offset",
|
||||
"key": "offset",
|
||||
"label": "Offset",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:gain",
|
||||
"key": "gain",
|
||||
"label": "Gain",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:distortion",
|
||||
"key": "distortion",
|
||||
"label": "Distortion",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:factor",
|
||||
"key": "factor",
|
||||
"label": "Factor",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "noise:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "output",
|
||||
"typeId": "fxnode.shader.material-output",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": 405,
|
||||
"y": 115
|
||||
},
|
||||
"size": {
|
||||
"x": 155,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Material Output",
|
||||
"parameters": {},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "output:surface",
|
||||
"key": "surface",
|
||||
"label": "Surface",
|
||||
"direction": "input",
|
||||
"dataType": "shader",
|
||||
"accepts": [
|
||||
"shader",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "output:volume",
|
||||
"key": "volume",
|
||||
"label": "Volume",
|
||||
"direction": "input",
|
||||
"dataType": "shader",
|
||||
"accepts": [
|
||||
"shader",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "output:displacement",
|
||||
"key": "displacement",
|
||||
"label": "Displacement",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "principled",
|
||||
"typeId": "fxnode.shader.principled-bsdf",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": 165,
|
||||
"y": 175
|
||||
},
|
||||
"size": {
|
||||
"x": 185,
|
||||
"y": 125
|
||||
},
|
||||
"label": "Principled BSDF",
|
||||
"parameters": {},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "principled:base-color",
|
||||
"key": "base-color",
|
||||
"label": "Base Color",
|
||||
"direction": "input",
|
||||
"dataType": "color",
|
||||
"accepts": [
|
||||
"color",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "color",
|
||||
"value": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "principled:metallic",
|
||||
"key": "metallic",
|
||||
"label": "Metallic",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "principled:roughness",
|
||||
"key": "roughness",
|
||||
"label": "Roughness",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "principled:ior",
|
||||
"key": "ior",
|
||||
"label": "IOR",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 1.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "principled:alpha",
|
||||
"key": "alpha",
|
||||
"label": "Alpha",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "principled:normal",
|
||||
"key": "normal",
|
||||
"label": "Normal",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "principled:bsdf",
|
||||
"key": "bsdf",
|
||||
"label": "BSDF",
|
||||
"direction": "output",
|
||||
"dataType": "shader",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "reroute",
|
||||
"typeId": "fxnode.common.reroute",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": 105,
|
||||
"y": 55
|
||||
},
|
||||
"size": {
|
||||
"x": 140,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Reroute",
|
||||
"parameters": {},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "reroute:input",
|
||||
"key": "input",
|
||||
"label": "Input",
|
||||
"direction": "input",
|
||||
"dataType": "any",
|
||||
"accepts": [
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "reroute:output",
|
||||
"key": "output",
|
||||
"label": "Output",
|
||||
"direction": "output",
|
||||
"dataType": "any",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "value-collapsed",
|
||||
"typeId": "fxnode.shader.value",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": 30,
|
||||
"y": -175
|
||||
},
|
||||
"size": {
|
||||
"x": 140,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Value",
|
||||
"parameters": {
|
||||
"value": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "value-collapsed:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": true,
|
||||
"extensions": {},
|
||||
"parentId": "frame",
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "value-expanded",
|
||||
"typeId": "fxnode.shader.value",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": 30,
|
||||
"y": -55
|
||||
},
|
||||
"size": {
|
||||
"x": 140,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Value",
|
||||
"parameters": {
|
||||
"value": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "value-expanded:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"parentId": "frame",
|
||||
"known": true
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "link-1",
|
||||
"fromNodeId": "value-expanded",
|
||||
"fromSocketId": "value-expanded:value",
|
||||
"toNodeId": "math",
|
||||
"toSocketId": "math:a",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "link-2",
|
||||
"fromNodeId": "value-collapsed",
|
||||
"fromSocketId": "value-collapsed:value",
|
||||
"toNodeId": "math",
|
||||
"toSocketId": "math:b",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "link-3",
|
||||
"fromNodeId": "math",
|
||||
"fromSocketId": "math:value",
|
||||
"toNodeId": "noise",
|
||||
"toSocketId": "noise:scale",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "link-4",
|
||||
"fromNodeId": "noise",
|
||||
"fromSocketId": "noise:factor",
|
||||
"toNodeId": "reroute",
|
||||
"toSocketId": "reroute:input",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "link-5",
|
||||
"fromNodeId": "reroute",
|
||||
"fromSocketId": "reroute:output",
|
||||
"toNodeId": "principled",
|
||||
"toSocketId": "principled:roughness",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "link-6",
|
||||
"fromNodeId": "principled",
|
||||
"fromSocketId": "principled:bsdf",
|
||||
"toNodeId": "output",
|
||||
"toSocketId": "output:surface",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"description": "Deterministic fxnode browser baseline"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>FxNode link tools test</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 1200px;
|
||||
height: 640px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="link-tools"></canvas>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,855 @@
|
||||
{
|
||||
"graphId": "link-tools-test",
|
||||
"catalogVersion": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "chain-math",
|
||||
"typeId": "fxnode.shader.math",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 260,
|
||||
"y": -100
|
||||
},
|
||||
"size": {
|
||||
"x": 192,
|
||||
"y": 148
|
||||
},
|
||||
"label": "Math",
|
||||
"parameters": {
|
||||
"operation": {
|
||||
"kind": "string",
|
||||
"value": "add"
|
||||
},
|
||||
"clamp": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "chain-math:a",
|
||||
"key": "a",
|
||||
"label": "A",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "chain-math:b",
|
||||
"key": "b",
|
||||
"label": "B",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "chain-math:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "chain-source",
|
||||
"typeId": "fxnode.shader.value",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -560,
|
||||
"y": -100
|
||||
},
|
||||
"size": {
|
||||
"x": 151,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Value",
|
||||
"parameters": {
|
||||
"value": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "chain-source:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "math-a",
|
||||
"typeId": "fxnode.shader.math",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 240
|
||||
},
|
||||
"size": {
|
||||
"x": 192,
|
||||
"y": 148
|
||||
},
|
||||
"label": "Math",
|
||||
"parameters": {
|
||||
"operation": {
|
||||
"kind": "string",
|
||||
"value": "add"
|
||||
},
|
||||
"clamp": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "math-a:a",
|
||||
"key": "a",
|
||||
"label": "A",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math-a:b",
|
||||
"key": "b",
|
||||
"label": "B",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math-a:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "math-b",
|
||||
"typeId": "fxnode.shader.math",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 120
|
||||
},
|
||||
"size": {
|
||||
"x": 192,
|
||||
"y": 148
|
||||
},
|
||||
"label": "Math",
|
||||
"parameters": {
|
||||
"operation": {
|
||||
"kind": "string",
|
||||
"value": "add"
|
||||
},
|
||||
"clamp": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "math-b:a",
|
||||
"key": "a",
|
||||
"label": "A",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math-b:b",
|
||||
"key": "b",
|
||||
"label": "B",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math-b:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "math-c",
|
||||
"typeId": "fxnode.shader.math",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"x": 192,
|
||||
"y": 148
|
||||
},
|
||||
"label": "Math",
|
||||
"parameters": {
|
||||
"operation": {
|
||||
"kind": "string",
|
||||
"value": "add"
|
||||
},
|
||||
"clamp": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "math-c:a",
|
||||
"key": "a",
|
||||
"label": "A",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math-c:b",
|
||||
"key": "b",
|
||||
"label": "B",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "math-c:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "noise",
|
||||
"typeId": "fxnode.shader.noise-texture",
|
||||
"typeVersion": 2,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": -230
|
||||
},
|
||||
"size": {
|
||||
"x": 286,
|
||||
"y": 292
|
||||
},
|
||||
"label": "Noise Texture",
|
||||
"parameters": {
|
||||
"dimensions": {
|
||||
"kind": "string",
|
||||
"value": "3d"
|
||||
},
|
||||
"noiseType": {
|
||||
"kind": "string",
|
||||
"value": "fbm"
|
||||
},
|
||||
"normalize": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "noise:vector",
|
||||
"key": "vector",
|
||||
"label": "Vector",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:w",
|
||||
"key": "w",
|
||||
"label": "W",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:scale",
|
||||
"key": "scale",
|
||||
"label": "Scale",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:detail",
|
||||
"key": "detail",
|
||||
"label": "Detail",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:roughness",
|
||||
"key": "roughness",
|
||||
"label": "Roughness",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:lacunarity",
|
||||
"key": "lacunarity",
|
||||
"label": "Lacunarity",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:offset",
|
||||
"key": "offset",
|
||||
"label": "Offset",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:gain",
|
||||
"key": "gain",
|
||||
"label": "Gain",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:distortion",
|
||||
"key": "distortion",
|
||||
"label": "Distortion",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise:factor",
|
||||
"key": "factor",
|
||||
"label": "Factor",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "noise:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "reroute",
|
||||
"typeId": "fxnode.common.reroute",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": -100
|
||||
},
|
||||
"size": {
|
||||
"x": 10,
|
||||
"y": 10
|
||||
},
|
||||
"label": "Reroute",
|
||||
"parameters": {},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "reroute:input",
|
||||
"key": "input",
|
||||
"label": "Input",
|
||||
"direction": "input",
|
||||
"dataType": "any",
|
||||
"accepts": [
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "reroute:output",
|
||||
"key": "output",
|
||||
"label": "Output",
|
||||
"direction": "output",
|
||||
"dataType": "any",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "source-a",
|
||||
"typeId": "fxnode.shader.value",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -560,
|
||||
"y": 240
|
||||
},
|
||||
"size": {
|
||||
"x": 151,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Value",
|
||||
"parameters": {
|
||||
"value": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "source-a:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "source-b",
|
||||
"typeId": "fxnode.shader.value",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -560,
|
||||
"y": 120
|
||||
},
|
||||
"size": {
|
||||
"x": 151,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Value",
|
||||
"parameters": {
|
||||
"value": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "source-b:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "source-c",
|
||||
"typeId": "fxnode.shader.value",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -560,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"x": 151,
|
||||
"y": 100
|
||||
},
|
||||
"label": "Value",
|
||||
"parameters": {
|
||||
"value": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "source-c:value",
|
||||
"key": "value",
|
||||
"label": "Value",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "transform",
|
||||
"typeId": "fxnode.geometry.transform-geometry",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -300,
|
||||
"y": -230
|
||||
},
|
||||
"size": {
|
||||
"x": 340,
|
||||
"y": 148
|
||||
},
|
||||
"label": "Transform Geometry",
|
||||
"parameters": {},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "transform:geometry",
|
||||
"key": "geometry",
|
||||
"label": "Geometry",
|
||||
"direction": "input",
|
||||
"dataType": "geometry",
|
||||
"accepts": [
|
||||
"geometry",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "transform:translation",
|
||||
"key": "translation",
|
||||
"label": "Translation",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "transform:rotation",
|
||||
"key": "rotation",
|
||||
"label": "Rotation",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "transform:scale",
|
||||
"key": "scale",
|
||||
"label": "Scale",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": [
|
||||
"vector",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "transform:result",
|
||||
"key": "result",
|
||||
"label": "Geometry",
|
||||
"direction": "output",
|
||||
"dataType": "geometry",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "chain-in",
|
||||
"fromNodeId": "chain-source",
|
||||
"fromSocketId": "chain-source:value",
|
||||
"toNodeId": "reroute",
|
||||
"toSocketId": "reroute:input",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "chain-out",
|
||||
"fromNodeId": "reroute",
|
||||
"fromSocketId": "reroute:output",
|
||||
"toNodeId": "chain-math",
|
||||
"toSocketId": "chain-math:a",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "parallel-a",
|
||||
"fromNodeId": "source-a",
|
||||
"fromSocketId": "source-a:value",
|
||||
"toNodeId": "math-a",
|
||||
"toSocketId": "math-a:a",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "parallel-b",
|
||||
"fromNodeId": "source-b",
|
||||
"fromSocketId": "source-b:value",
|
||||
"toNodeId": "math-b",
|
||||
"toSocketId": "math-b:a",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "parallel-c",
|
||||
"fromNodeId": "source-c",
|
||||
"fromSocketId": "source-c:value",
|
||||
"toNodeId": "math-c",
|
||||
"toSocketId": "math-c:a",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
}
|
||||
],
|
||||
"metadata": {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createApplicationFxNode, type ApplicationFxNode } from "../application-browser.js";
|
||||
import type { FxNodeView } from "@lib/index.js";
|
||||
import { prepareFxNodeBrowserHost } from "../../shared/browser-host.js";
|
||||
import initialLayout from "./initialLayout.json" with { type: "json" };
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#link-tools");
|
||||
if (!canvas) throw new Error("Link tools test canvas missing");
|
||||
const host = prepareFxNodeBrowserHost({ canvas });
|
||||
const handle: { root: ApplicationFxNode | null; view: FxNodeView | null; ready: Promise<void> } = {
|
||||
root: null,
|
||||
view: null,
|
||||
ready: Promise.resolve(),
|
||||
};
|
||||
window.linkToolsTest = handle;
|
||||
handle.ready = (async () => {
|
||||
let root: ApplicationFxNode | undefined, view: FxNodeView | undefined;
|
||||
try {
|
||||
root = await createApplicationFxNode();
|
||||
await root.setState(initialLayout);
|
||||
view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
handle.root = root;
|
||||
handle.view = view;
|
||||
host.attach(root, view);
|
||||
await view.whenRendered();
|
||||
} catch (error) {
|
||||
host.destroy();
|
||||
await view?.detach();
|
||||
root?.destroy();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { createApplicationFxNode } from "./application-browser.js";
|
||||
import { prepareFxNodeBrowserHost } from "../shared/browser-host.js";
|
||||
import initialLayout from "./initialLayout.json" with { type: "json" };
|
||||
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#graph");
|
||||
const addNodeMenuTemplate = document.querySelector<HTMLTemplateElement>("#add-node-menu-template");
|
||||
if (!canvas || !addNodeMenuTemplate) throw new Error("Example canvas or add-node menu template is missing");
|
||||
const host = prepareFxNodeBrowserHost({ canvas, addNodeMenuTemplate });
|
||||
|
||||
let resolveRendered!: () => void;
|
||||
const rendered = new Promise<void>((resolve) => {
|
||||
resolveRendered = resolve;
|
||||
});
|
||||
const handle: FxNodeExampleHandle = { root: null, view: null, host, ready: Promise.resolve(), rendered };
|
||||
window.fxnodeExample = handle;
|
||||
|
||||
handle.ready = (async () => {
|
||||
let root: Awaited<ReturnType<typeof createApplicationFxNode>> | undefined,
|
||||
view: Awaited<ReturnType<NonNullable<typeof root>["attachView"]>> | undefined;
|
||||
try {
|
||||
root = await createApplicationFxNode();
|
||||
await root.setState(initialLayout);
|
||||
view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
handle.root = root;
|
||||
handle.view = view;
|
||||
host.attach(root, view);
|
||||
await view.whenRendered();
|
||||
resolveRendered();
|
||||
} catch (error) {
|
||||
host.destroy();
|
||||
await view?.detach();
|
||||
root?.destroy();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,49 @@
|
||||
/** Shared constants for the example application composition. */
|
||||
import type { FxNodeCompositionData } from "@lib/composition/index.js";
|
||||
export const applicationCompatibility = {
|
||||
wildcardInputTypes: ["any"],
|
||||
} as const satisfies FxNodeCompositionData["compatibility"];
|
||||
export const APPLICATION_ID = "fxnode.example.application";
|
||||
export const APPLICATION_VERSION = 4;
|
||||
export const APPLICATION_HEADER_STYLES = {
|
||||
input: {
|
||||
header: "#8b3f72",
|
||||
},
|
||||
converter: {
|
||||
header: "#4f5964",
|
||||
},
|
||||
texture: {
|
||||
header: "#a36b34",
|
||||
},
|
||||
shader: {
|
||||
header: "#3b7551",
|
||||
},
|
||||
output: {
|
||||
header: "#963d3d",
|
||||
},
|
||||
geometry: {
|
||||
header: "#2c7a75",
|
||||
},
|
||||
common: {
|
||||
header: "#555b64",
|
||||
},
|
||||
compositorInput: {
|
||||
header: "#8b3f72",
|
||||
},
|
||||
compositorColor: {
|
||||
header: "#4f5964",
|
||||
},
|
||||
} as const satisfies FxNodeCompositionData["nodeStyles"];
|
||||
export const APPLICATION_RESOURCES = {
|
||||
legacyImage: {
|
||||
kind: "image",
|
||||
title: "Image",
|
||||
openTitle: "Open",
|
||||
accept: ["image/*"],
|
||||
referencePrefix: "fxnode-local-image:",
|
||||
maxBytes: 33554432,
|
||||
maxWidth: 8192,
|
||||
maxHeight: 8192,
|
||||
maxPixels: 16777216,
|
||||
},
|
||||
} as const satisfies FxNodeCompositionData["resources"];
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const frameNode = [
|
||||
"fxnode.common.frame",
|
||||
{
|
||||
version: 1,
|
||||
title: "Frame",
|
||||
behavior: "frame",
|
||||
style: "common",
|
||||
parameters: {},
|
||||
sockets: {},
|
||||
ui: [],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const groupInputNode = [
|
||||
"fxnode.common.group-input",
|
||||
{
|
||||
version: 1,
|
||||
title: "Group Input",
|
||||
behavior: "standard",
|
||||
style: "input",
|
||||
parameters: {
|
||||
interfaceName: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "Socket",
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
output: {
|
||||
title: "Interface",
|
||||
direction: "output",
|
||||
type: "any",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "interfaceName",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "output",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const groupOutputNode = [
|
||||
"fxnode.common.group-output",
|
||||
{
|
||||
version: 1,
|
||||
title: "Group Output",
|
||||
behavior: "standard",
|
||||
style: "output",
|
||||
parameters: {
|
||||
interfaceName: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "Socket",
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
input: {
|
||||
title: "Interface",
|
||||
direction: "input",
|
||||
type: "any",
|
||||
maxIncomingLinks: 9007199254740991,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "interfaceName",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "input",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const rerouteNode = [
|
||||
"fxnode.common.reroute",
|
||||
{
|
||||
version: 1,
|
||||
title: "Reroute",
|
||||
behavior: "reroute",
|
||||
style: "common",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
input: {
|
||||
title: "Input",
|
||||
direction: "input",
|
||||
type: "any",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
output: {
|
||||
title: "Output",
|
||||
direction: "output",
|
||||
type: "any",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "input",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "output",
|
||||
},
|
||||
],
|
||||
muteBypass: [["input", "output"]],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const imageNode = [
|
||||
"fxnode.compositor.image",
|
||||
{
|
||||
version: 1,
|
||||
title: "Image",
|
||||
behavior: "standard",
|
||||
style: "compositorInput",
|
||||
parameters: {
|
||||
image: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
source: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "File",
|
||||
},
|
||||
enum: ["Generated", "File", "Movie", "Sequence", "Multilayer"],
|
||||
},
|
||||
frames: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
minimum: 1,
|
||||
maximum: 1048574,
|
||||
integer: true,
|
||||
},
|
||||
startFrame: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
minimum: -1048574,
|
||||
maximum: 1048574,
|
||||
integer: true,
|
||||
},
|
||||
offset: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
minimum: -1048574,
|
||||
maximum: 1048574,
|
||||
integer: true,
|
||||
},
|
||||
cyclic: {
|
||||
type: "boolean",
|
||||
default: {
|
||||
kind: "boolean",
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
autoRefresh: {
|
||||
type: "boolean",
|
||||
default: {
|
||||
kind: "boolean",
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
image: {
|
||||
title: "Image",
|
||||
direction: "output",
|
||||
type: "color",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
alpha: {
|
||||
title: "Alpha",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
z: {
|
||||
title: "Z",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "resource",
|
||||
resource: "legacyImage",
|
||||
parameter: "image",
|
||||
title: "Image",
|
||||
openTitle: "Open",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "source",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "frames",
|
||||
title: "Frames",
|
||||
visibleWhen: {
|
||||
parameter: "source",
|
||||
in: ["Movie", "Sequence"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "startFrame",
|
||||
title: "Start Frame",
|
||||
visibleWhen: {
|
||||
parameter: "source",
|
||||
in: ["Movie", "Sequence"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "offset",
|
||||
visibleWhen: {
|
||||
parameter: "source",
|
||||
in: ["Movie", "Sequence"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "cyclic",
|
||||
visibleWhen: {
|
||||
parameter: "source",
|
||||
in: ["Movie", "Sequence"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "autoRefresh",
|
||||
title: "Auto Refresh",
|
||||
visibleWhen: {
|
||||
parameter: "source",
|
||||
in: ["Movie", "Sequence"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "image",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "alpha",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "z",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const joinGeometryNode = [
|
||||
"fxnode.geometry.join-geometry",
|
||||
{
|
||||
version: 1,
|
||||
title: "Join Geometry",
|
||||
behavior: "standard",
|
||||
style: "geometry",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
geometry: {
|
||||
title: "Geometry",
|
||||
direction: "input",
|
||||
type: "geometry",
|
||||
maxIncomingLinks: 9007199254740991,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
result: {
|
||||
title: "Geometry",
|
||||
direction: "output",
|
||||
type: "geometry",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "geometry",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "result",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const meshCubeNode = [
|
||||
"fxnode.geometry.mesh-cube",
|
||||
{
|
||||
version: 1,
|
||||
title: "Mesh Cube",
|
||||
behavior: "standard",
|
||||
style: "geometry",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
size: {
|
||||
title: "Size",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [1, 1, 1],
|
||||
},
|
||||
minimum: 0,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
"vertices-x": {
|
||||
title: "Vertices X",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 2,
|
||||
},
|
||||
minimum: 2,
|
||||
maximum: 1000,
|
||||
integer: true,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
"vertices-y": {
|
||||
title: "Vertices Y",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 2,
|
||||
},
|
||||
minimum: 2,
|
||||
maximum: 1000,
|
||||
integer: true,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
"vertices-z": {
|
||||
title: "Vertices Z",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 2,
|
||||
},
|
||||
minimum: 2,
|
||||
maximum: 1000,
|
||||
integer: true,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
mesh: {
|
||||
title: "Mesh",
|
||||
direction: "output",
|
||||
type: "geometry",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "size",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "vertices-x",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "vertices-y",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "vertices-z",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "mesh",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const positionNode = [
|
||||
"fxnode.geometry.position",
|
||||
{
|
||||
version: 1,
|
||||
title: "Position",
|
||||
behavior: "standard",
|
||||
style: "geometry",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
position: {
|
||||
title: "Position",
|
||||
direction: "output",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "position",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const setPositionNode = [
|
||||
"fxnode.geometry.set-position",
|
||||
{
|
||||
version: 1,
|
||||
title: "Set Position",
|
||||
behavior: "standard",
|
||||
style: "geometry",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
geometry: {
|
||||
title: "Geometry",
|
||||
direction: "input",
|
||||
type: "geometry",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
position: {
|
||||
title: "Position",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
offset: {
|
||||
title: "Offset",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
result: {
|
||||
title: "Geometry",
|
||||
direction: "output",
|
||||
type: "geometry",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "geometry",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "position",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "offset",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "result",
|
||||
},
|
||||
],
|
||||
muteBypass: [["geometry", "result"]],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const transformGeometryNode = [
|
||||
"fxnode.geometry.transform-geometry",
|
||||
{
|
||||
version: 1,
|
||||
title: "Transform Geometry",
|
||||
behavior: "standard",
|
||||
style: "geometry",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
geometry: {
|
||||
title: "Geometry",
|
||||
direction: "input",
|
||||
type: "geometry",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
translation: {
|
||||
title: "Translation",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
rotation: {
|
||||
title: "Rotation",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
scale: {
|
||||
title: "Scale",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [1, 1, 1],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
result: {
|
||||
title: "Geometry",
|
||||
direction: "output",
|
||||
type: "geometry",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "geometry",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "translation",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "rotation",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "scale",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "result",
|
||||
},
|
||||
],
|
||||
muteBypass: [["geometry", "result"]],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const colorRampNode = [
|
||||
"fxnode.shader.color-ramp",
|
||||
{
|
||||
version: 2,
|
||||
title: "Color Ramp",
|
||||
behavior: "standard",
|
||||
style: "shader",
|
||||
parameters: {
|
||||
ramp: {
|
||||
type: "json",
|
||||
codec: "color-ramp/v1",
|
||||
default: {
|
||||
kind: "json",
|
||||
value: {
|
||||
colorMode: "rgb",
|
||||
interpolation: "linear",
|
||||
hueInterpolation: "near",
|
||||
stops: [
|
||||
{
|
||||
id: "stop-0",
|
||||
position: 0,
|
||||
color: [0, 0, 0, 1],
|
||||
},
|
||||
{
|
||||
id: "stop-1",
|
||||
position: 1,
|
||||
color: [1, 1, 1, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
factor: {
|
||||
title: "Factor",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0.5,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
color: {
|
||||
title: "Color",
|
||||
direction: "output",
|
||||
type: "color",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
alpha: {
|
||||
title: "Alpha",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "color",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "alpha",
|
||||
},
|
||||
{
|
||||
kind: "widget",
|
||||
widget: "color-ramp",
|
||||
parameter: "ramp",
|
||||
title: "",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "factor",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [
|
||||
{
|
||||
fromVersion: 1,
|
||||
toVersion: 2,
|
||||
steps: [
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "parameter",
|
||||
key: "ramp",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "factor",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "color",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "alpha",
|
||||
},
|
||||
{
|
||||
kind: "migrate-parameter",
|
||||
parameter: "ramp",
|
||||
codec: "color-ramp/legacy-stops",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const colorNode = [
|
||||
"fxnode.shader.color",
|
||||
{
|
||||
version: 1,
|
||||
title: "Color",
|
||||
behavior: "standard",
|
||||
style: "shader",
|
||||
parameters: {
|
||||
color: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [0.8, 0.8, 0.8, 1],
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
color: {
|
||||
title: "Color",
|
||||
direction: "output",
|
||||
type: "color",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "color",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "color",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const imageTextureNode = [
|
||||
"fxnode.shader.image-texture",
|
||||
{
|
||||
version: 1,
|
||||
title: "Image Texture",
|
||||
behavior: "standard",
|
||||
style: "input",
|
||||
parameters: {
|
||||
image: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
interpolation: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "Linear",
|
||||
},
|
||||
enum: ["Linear", "Closest", "Cubic", "Smart"],
|
||||
},
|
||||
projection: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "Flat",
|
||||
},
|
||||
enum: ["Flat", "Box", "Sphere", "Tube"],
|
||||
},
|
||||
blend: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
extension: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "Repeat",
|
||||
},
|
||||
enum: ["Repeat", "Extend", "Clip", "Mirror"],
|
||||
},
|
||||
colorSpace: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "sRGB",
|
||||
},
|
||||
enum: ["sRGB", "Linear", "Non-Color"],
|
||||
},
|
||||
alphaMode: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "Straight",
|
||||
},
|
||||
enum: ["Straight", "Premultiplied", "Channel Packed", "None"],
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
vector: {
|
||||
title: "Vector",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: false,
|
||||
},
|
||||
color: {
|
||||
title: "Color",
|
||||
direction: "output",
|
||||
type: "color",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
alpha: {
|
||||
title: "Alpha",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "resource",
|
||||
resource: "legacyImage",
|
||||
parameter: "image",
|
||||
title: "Image",
|
||||
openTitle: "Open",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "interpolation",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "projection",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "blend",
|
||||
title: "Blend",
|
||||
visibleWhen: {
|
||||
parameter: "projection",
|
||||
equals: "Box",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "extension",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "colorSpace",
|
||||
title: "Color Space",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "alphaMode",
|
||||
title: "Alpha Mode",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "vector",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "color",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "alpha",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const materialOutputNode = [
|
||||
"fxnode.shader.material-output",
|
||||
{
|
||||
version: 1,
|
||||
title: "Material Output",
|
||||
behavior: "standard",
|
||||
style: "output",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
surface: {
|
||||
title: "Surface",
|
||||
direction: "input",
|
||||
type: "shader",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
volume: {
|
||||
title: "Volume",
|
||||
direction: "input",
|
||||
type: "shader",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
displacement: {
|
||||
title: "Displacement",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "surface",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "volume",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "displacement",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const mathNode = [
|
||||
"fxnode.shader.math",
|
||||
{
|
||||
version: 1,
|
||||
title: "Math",
|
||||
behavior: "standard",
|
||||
style: "converter",
|
||||
parameters: {
|
||||
operation: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "add",
|
||||
},
|
||||
enum: ["add", "subtract", "multiply", "divide", "power", "minimum", "maximum"],
|
||||
},
|
||||
clamp: {
|
||||
type: "boolean",
|
||||
default: {
|
||||
kind: "boolean",
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
a: {
|
||||
title: "A",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
b: {
|
||||
title: "B",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
value: {
|
||||
title: "Value",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "operation",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "clamp",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "a",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "b",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "value",
|
||||
},
|
||||
],
|
||||
muteBypass: [["a", "value"]],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const mixNode = [
|
||||
"fxnode.shader.mix",
|
||||
{
|
||||
version: 1,
|
||||
title: "Mix",
|
||||
behavior: "standard",
|
||||
style: "shader",
|
||||
parameters: {
|
||||
blend: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "mix",
|
||||
},
|
||||
enum: ["mix", "add", "multiply", "screen", "overlay"],
|
||||
},
|
||||
clamp: {
|
||||
type: "boolean",
|
||||
default: {
|
||||
kind: "boolean",
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
factor: {
|
||||
title: "Factor",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0.5,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
a: {
|
||||
title: "A",
|
||||
direction: "input",
|
||||
type: "color",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [0.8, 0.8, 0.8, 1],
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
b: {
|
||||
title: "B",
|
||||
direction: "input",
|
||||
type: "color",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [0.8, 0.8, 0.8, 1],
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
result: {
|
||||
title: "Result",
|
||||
direction: "output",
|
||||
type: "color",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "blend",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "clamp",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "factor",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "a",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "b",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "result",
|
||||
},
|
||||
],
|
||||
muteBypass: [["a", "result"]],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,364 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const noiseTextureNode = [
|
||||
"fxnode.shader.noise-texture",
|
||||
{
|
||||
version: 2,
|
||||
title: "Noise Texture",
|
||||
behavior: "standard",
|
||||
style: "texture",
|
||||
parameters: {
|
||||
dimensions: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "3d",
|
||||
},
|
||||
enum: ["1d", "2d", "3d", "4d"],
|
||||
},
|
||||
noiseType: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "fbm",
|
||||
},
|
||||
enum: ["fbm", "multifractal", "hybrid-multifractal", "ridged-multifractal", "hetero-terrain"],
|
||||
},
|
||||
normalize: {
|
||||
type: "boolean",
|
||||
default: {
|
||||
kind: "boolean",
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
vector: {
|
||||
title: "Vector",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: false,
|
||||
},
|
||||
w: {
|
||||
title: "W",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
scale: {
|
||||
title: "Scale",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 5,
|
||||
},
|
||||
minimum: -1000,
|
||||
maximum: 1000,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
detail: {
|
||||
title: "Detail",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 2,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 15,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
roughness: {
|
||||
title: "Roughness",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0.5,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
lacunarity: {
|
||||
title: "Lacunarity",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 2,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1000,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
offset: {
|
||||
title: "Offset",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
minimum: -1000,
|
||||
maximum: 1000,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
gain: {
|
||||
title: "Gain",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1000,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
distortion: {
|
||||
title: "Distortion",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
minimum: -1000,
|
||||
maximum: 1000,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
factor: {
|
||||
title: "Factor",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
color: {
|
||||
title: "Color",
|
||||
direction: "output",
|
||||
type: "color",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "dimensions",
|
||||
title: "Dimensions",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "noiseType",
|
||||
title: "Type",
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "normalize",
|
||||
title: "Normalize",
|
||||
visibleWhen: {
|
||||
parameter: "noiseType",
|
||||
equals: "fbm",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "vector",
|
||||
visibleWhen: {
|
||||
parameter: "dimensions",
|
||||
in: ["2d", "3d", "4d"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "w",
|
||||
visibleWhen: {
|
||||
parameter: "dimensions",
|
||||
in: ["1d", "4d"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "scale",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "detail",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "roughness",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "lacunarity",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "offset",
|
||||
visibleWhen: {
|
||||
parameter: "noiseType",
|
||||
in: ["hybrid-multifractal", "ridged-multifractal", "hetero-terrain"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "gain",
|
||||
visibleWhen: {
|
||||
parameter: "noiseType",
|
||||
in: ["hybrid-multifractal", "ridged-multifractal"],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "distortion",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "factor",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "color",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [
|
||||
{
|
||||
fromVersion: 1,
|
||||
toVersion: 2,
|
||||
steps: [
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "parameter",
|
||||
key: "dimensions",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "parameter",
|
||||
key: "noiseType",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "parameter",
|
||||
key: "normalize",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "vector",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "w",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "scale",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "detail",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "roughness",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "lacunarity",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "offset",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "gain",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "distortion",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "factor",
|
||||
},
|
||||
{
|
||||
kind: "materialize-missing",
|
||||
target: "socket",
|
||||
key: "color",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const principledBsdfNode = [
|
||||
"fxnode.shader.principled-bsdf",
|
||||
{
|
||||
version: 1,
|
||||
title: "Principled BSDF",
|
||||
behavior: "standard",
|
||||
style: "shader",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
"base-color": {
|
||||
title: "Base Color",
|
||||
direction: "input",
|
||||
type: "color",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [0.8, 0.8, 0.8, 1],
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
metallic: {
|
||||
title: "Metallic",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
roughness: {
|
||||
title: "Roughness",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0.5,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
ior: {
|
||||
title: "IOR",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1.5,
|
||||
},
|
||||
minimum: 1,
|
||||
maximum: 1000,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
alpha: {
|
||||
title: "Alpha",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
normal: {
|
||||
title: "Normal",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: false,
|
||||
},
|
||||
bsdf: {
|
||||
title: "BSDF",
|
||||
direction: "output",
|
||||
type: "shader",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "base-color",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "metallic",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "roughness",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "ior",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "alpha",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "normal",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "bsdf",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const textureCoordinateNode = [
|
||||
"fxnode.shader.texture-coordinate",
|
||||
{
|
||||
version: 1,
|
||||
title: "Texture Coordinate",
|
||||
behavior: "standard",
|
||||
style: "shader",
|
||||
parameters: {},
|
||||
sockets: {
|
||||
generated: {
|
||||
title: "Generated",
|
||||
direction: "output",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
normal: {
|
||||
title: "Normal",
|
||||
direction: "output",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
uv: {
|
||||
title: "UV",
|
||||
direction: "output",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
object: {
|
||||
title: "Object",
|
||||
direction: "output",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "generated",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "normal",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "uv",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "object",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const valueNode = [
|
||||
"fxnode.shader.value",
|
||||
{
|
||||
version: 1,
|
||||
title: "Value",
|
||||
behavior: "standard",
|
||||
style: "shader",
|
||||
parameters: {
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
step: 1,
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
value: {
|
||||
title: "Value",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "value",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "value",
|
||||
},
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { FxNodeDefinition } from "@lib/composition/index.js";
|
||||
export const vectorMathNode = [
|
||||
"fxnode.shader.vector-math",
|
||||
{
|
||||
version: 1,
|
||||
title: "Vector Math",
|
||||
behavior: "standard",
|
||||
style: "converter",
|
||||
parameters: {
|
||||
operation: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "add",
|
||||
},
|
||||
enum: ["add", "subtract", "multiply", "dot-product", "length", "normalize"],
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
a: {
|
||||
title: "A",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
b: {
|
||||
title: "B",
|
||||
direction: "input",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "vector",
|
||||
default: {
|
||||
kind: "vector",
|
||||
value: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
vector: {
|
||||
title: "Vector",
|
||||
direction: "output",
|
||||
type: "vector",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
value: {
|
||||
title: "Value",
|
||||
direction: "output",
|
||||
type: "float",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "operation",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "a",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "b",
|
||||
visibleWhen: {
|
||||
parameter: "operation",
|
||||
equals: "add",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "vector",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "value",
|
||||
},
|
||||
],
|
||||
muteBypass: [["a", "vector"]],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { FxNodeSocketTypeDefinition } from "@lib/composition/index.js";
|
||||
export const anySocket = ["any", { title: "any", color: "#999999", acceptsFrom: ["any"] }] as const satisfies readonly [
|
||||
string,
|
||||
FxNodeSocketTypeDefinition,
|
||||
];
|
||||
export const floatSocket = [
|
||||
"float",
|
||||
{ title: "float", color: "#a8a8a8", acceptsFrom: ["float", "any"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
export const vectorSocket = [
|
||||
"vector",
|
||||
{ title: "vector", color: "#6476dc", acceptsFrom: ["vector", "any"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
export const colorSocket = [
|
||||
"color",
|
||||
{ title: "color", color: "#d7ca63", acceptsFrom: ["color", "any"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
export const shaderSocket = [
|
||||
"shader",
|
||||
{ title: "shader", color: "#62b34f", acceptsFrom: ["shader", "any"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
export const geometrySocket = [
|
||||
"geometry",
|
||||
{ title: "geometry", color: "#00bfa5", acceptsFrom: ["geometry", "any"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>fxnode image and grading parity</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="graph"></canvas>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,754 @@
|
||||
{
|
||||
"graphId": "parity",
|
||||
"catalogVersion": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "color-ramp",
|
||||
"typeId": "fxnode.shader.color-ramp",
|
||||
"typeVersion": 2,
|
||||
"position": {
|
||||
"x": 350,
|
||||
"y": 180
|
||||
},
|
||||
"size": {
|
||||
"x": 320,
|
||||
"y": 292
|
||||
},
|
||||
"label": "Color Ramp",
|
||||
"parameters": {
|
||||
"ramp": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"colorMode": "rgb",
|
||||
"interpolation": "linear",
|
||||
"hueInterpolation": "near",
|
||||
"stops": [
|
||||
{
|
||||
"id": "stop-0",
|
||||
"position": 0,
|
||||
"color": [0, 0, 0, 1]
|
||||
},
|
||||
{
|
||||
"id": "stop-1",
|
||||
"position": 1,
|
||||
"color": [1, 1, 1, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "color-ramp:factor",
|
||||
"key": "factor",
|
||||
"label": "Factor",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "color-ramp:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "color-ramp:alpha",
|
||||
"key": "alpha",
|
||||
"label": "Alpha",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "compositor-image",
|
||||
"typeId": "fxnode.compositor.image",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": -340,
|
||||
"y": -260
|
||||
},
|
||||
"size": {
|
||||
"x": 176,
|
||||
"y": 220
|
||||
},
|
||||
"label": "Image",
|
||||
"parameters": {
|
||||
"image": {
|
||||
"kind": "string",
|
||||
"value": ""
|
||||
},
|
||||
"source": {
|
||||
"kind": "string",
|
||||
"value": "File"
|
||||
},
|
||||
"frames": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
},
|
||||
"startFrame": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
},
|
||||
"offset": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
},
|
||||
"cyclic": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
},
|
||||
"autoRefresh": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "compositor-image:image",
|
||||
"key": "image",
|
||||
"label": "Image",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "compositor-image:alpha",
|
||||
"key": "alpha",
|
||||
"label": "Alpha",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "compositor-image:z",
|
||||
"key": "z",
|
||||
"label": "Z",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "image-texture",
|
||||
"typeId": "fxnode.shader.image-texture",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": -520,
|
||||
"y": 180
|
||||
},
|
||||
"size": {
|
||||
"x": 257,
|
||||
"y": 316
|
||||
},
|
||||
"label": "Image Texture",
|
||||
"parameters": {
|
||||
"image": {
|
||||
"kind": "string",
|
||||
"value": ""
|
||||
},
|
||||
"interpolation": {
|
||||
"kind": "string",
|
||||
"value": "Linear"
|
||||
},
|
||||
"projection": {
|
||||
"kind": "string",
|
||||
"value": "Flat"
|
||||
},
|
||||
"blend": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
},
|
||||
"extension": {
|
||||
"kind": "string",
|
||||
"value": "Repeat"
|
||||
},
|
||||
"colorSpace": {
|
||||
"kind": "string",
|
||||
"value": "sRGB"
|
||||
},
|
||||
"alphaMode": {
|
||||
"kind": "string",
|
||||
"value": "Straight"
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "image-texture:vector",
|
||||
"key": "vector",
|
||||
"label": "Vector",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": ["vector", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [0, 0, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "image-texture:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "image-texture:alpha",
|
||||
"key": "alpha",
|
||||
"label": "Alpha",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "master",
|
||||
"typeId": "fxnode.compositor.color-balance",
|
||||
"typeVersion": 1,
|
||||
"position": {
|
||||
"x": 20,
|
||||
"y": -260
|
||||
},
|
||||
"size": {
|
||||
"x": 400,
|
||||
"y": 292
|
||||
},
|
||||
"label": "Master Color Grading",
|
||||
"parameters": {
|
||||
"mode": {
|
||||
"kind": "string",
|
||||
"value": "Lift/Gamma/Gain"
|
||||
},
|
||||
"lift": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
},
|
||||
"liftColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
},
|
||||
"gamma": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
},
|
||||
"gammaColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
},
|
||||
"gain": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
},
|
||||
"gainColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
},
|
||||
"offset": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
},
|
||||
"offsetColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
},
|
||||
"power": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
},
|
||||
"powerColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
},
|
||||
"slope": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
},
|
||||
"slopeColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
},
|
||||
"inputTemperature": {
|
||||
"kind": "number",
|
||||
"value": 6500
|
||||
},
|
||||
"inputTint": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
},
|
||||
"inputColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
},
|
||||
"outputTemperature": {
|
||||
"kind": "number",
|
||||
"value": 6500
|
||||
},
|
||||
"outputTint": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
},
|
||||
"outputColor": {
|
||||
"kind": "color",
|
||||
"value": [1, 1, 1, 1]
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "master:image",
|
||||
"key": "image",
|
||||
"label": "Image",
|
||||
"direction": "input",
|
||||
"dataType": "color",
|
||||
"accepts": ["color", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "color",
|
||||
"value": [0.8, 0.8, 0.8, 1]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "master:factor",
|
||||
"key": "factor",
|
||||
"label": "Factor",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "master:result",
|
||||
"key": "result",
|
||||
"label": "Image",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "noise-3d",
|
||||
"typeId": "fxnode.shader.noise-texture",
|
||||
"typeVersion": 2,
|
||||
"position": {
|
||||
"x": -220,
|
||||
"y": 180
|
||||
},
|
||||
"size": {
|
||||
"x": 286,
|
||||
"y": 292
|
||||
},
|
||||
"label": "Noise Texture",
|
||||
"parameters": {
|
||||
"dimensions": {
|
||||
"kind": "string",
|
||||
"value": "3d"
|
||||
},
|
||||
"noiseType": {
|
||||
"kind": "string",
|
||||
"value": "fbm"
|
||||
},
|
||||
"normalize": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "noise-3d:vector",
|
||||
"key": "vector",
|
||||
"label": "Vector",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": ["vector", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [0, 0, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:w",
|
||||
"key": "w",
|
||||
"label": "W",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:scale",
|
||||
"key": "scale",
|
||||
"label": "Scale",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:detail",
|
||||
"key": "detail",
|
||||
"label": "Detail",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:roughness",
|
||||
"key": "roughness",
|
||||
"label": "Roughness",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:lacunarity",
|
||||
"key": "lacunarity",
|
||||
"label": "Lacunarity",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:offset",
|
||||
"key": "offset",
|
||||
"label": "Offset",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:gain",
|
||||
"key": "gain",
|
||||
"label": "Gain",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:distortion",
|
||||
"key": "distortion",
|
||||
"label": "Distortion",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:factor",
|
||||
"key": "factor",
|
||||
"label": "Factor",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "noise-3d:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
},
|
||||
{
|
||||
"id": "noise-4d",
|
||||
"typeId": "fxnode.shader.noise-texture",
|
||||
"typeVersion": 2,
|
||||
"position": {
|
||||
"x": 60,
|
||||
"y": 180
|
||||
},
|
||||
"size": {
|
||||
"x": 286,
|
||||
"y": 292
|
||||
},
|
||||
"label": "Noise Texture",
|
||||
"parameters": {
|
||||
"dimensions": {
|
||||
"kind": "string",
|
||||
"value": "4d"
|
||||
},
|
||||
"noiseType": {
|
||||
"kind": "string",
|
||||
"value": "fbm"
|
||||
},
|
||||
"normalize": {
|
||||
"kind": "boolean",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "noise-4d:vector",
|
||||
"key": "vector",
|
||||
"label": "Vector",
|
||||
"direction": "input",
|
||||
"dataType": "vector",
|
||||
"accepts": ["vector", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "vector",
|
||||
"value": [0, 0, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:w",
|
||||
"key": "w",
|
||||
"label": "W",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:scale",
|
||||
"key": "scale",
|
||||
"label": "Scale",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:detail",
|
||||
"key": "detail",
|
||||
"label": "Detail",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:roughness",
|
||||
"key": "roughness",
|
||||
"label": "Roughness",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:lacunarity",
|
||||
"key": "lacunarity",
|
||||
"label": "Lacunarity",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:offset",
|
||||
"key": "offset",
|
||||
"label": "Offset",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:gain",
|
||||
"key": "gain",
|
||||
"label": "Gain",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:distortion",
|
||||
"key": "distortion",
|
||||
"label": "Distortion",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": ["float", "any"],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:factor",
|
||||
"key": "factor",
|
||||
"label": "Factor",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "noise-4d:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"known": true
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "compositor-grade",
|
||||
"fromNodeId": "compositor-image",
|
||||
"fromSocketId": "compositor-image:image",
|
||||
"toNodeId": "master",
|
||||
"toSocketId": "master:image",
|
||||
"muted": false,
|
||||
"extensions": {}
|
||||
}
|
||||
],
|
||||
"metadata": {}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { createApplicationFxNode } from "../application-browser.js";
|
||||
import { prepareFxNodeBrowserHost } from "../../shared/browser-host.js";
|
||||
import initialLayout from "./initialLayout.json" with { type: "json" };
|
||||
const canvas = document.querySelector("canvas")!;
|
||||
const host = prepareFxNodeBrowserHost({ canvas });
|
||||
let root: Awaited<ReturnType<typeof createApplicationFxNode>> | undefined,
|
||||
view: Awaited<ReturnType<NonNullable<typeof root>["attachView"]>> | undefined;
|
||||
try {
|
||||
root = await createApplicationFxNode();
|
||||
await root.setState(initialLayout);
|
||||
view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
host.attach(root, view);
|
||||
await view.whenRendered();
|
||||
window.parityExample = { root, view };
|
||||
} catch (error) {
|
||||
host.destroy();
|
||||
await view?.detach();
|
||||
root?.destroy();
|
||||
throw error;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #202124;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>FxNode ramp test</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 1200px;
|
||||
height: 640px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="ramp"></canvas>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"graphId": "ramp-test",
|
||||
"catalogVersion": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "frame",
|
||||
"typeId": "fxnode.common.frame",
|
||||
"typeVersion": 1,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": -300,
|
||||
"y": 250
|
||||
},
|
||||
"size": {
|
||||
"x": 380,
|
||||
"y": 310
|
||||
},
|
||||
"label": "Frame",
|
||||
"parameters": {},
|
||||
"sockets": [],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {}
|
||||
},
|
||||
{
|
||||
"id": "ramp",
|
||||
"typeId": "fxnode.shader.color-ramp",
|
||||
"typeVersion": 2,
|
||||
"known": true,
|
||||
"position": {
|
||||
"x": 40,
|
||||
"y": -40
|
||||
},
|
||||
"size": {
|
||||
"x": 320,
|
||||
"y": 292
|
||||
},
|
||||
"label": "Color Ramp",
|
||||
"parameters": {
|
||||
"ramp": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"colorMode": "rgb",
|
||||
"interpolation": "linear",
|
||||
"hueInterpolation": "near",
|
||||
"stops": [
|
||||
{
|
||||
"id": "red",
|
||||
"position": 0.1,
|
||||
"color": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "green",
|
||||
"position": 0.35,
|
||||
"color": [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0.8
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blue",
|
||||
"position": 0.9,
|
||||
"color": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.6
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"sockets": [
|
||||
{
|
||||
"id": "ramp:factor",
|
||||
"key": "factor",
|
||||
"label": "Factor",
|
||||
"direction": "input",
|
||||
"dataType": "float",
|
||||
"accepts": [
|
||||
"float",
|
||||
"any"
|
||||
],
|
||||
"maxIncomingLinks": 1,
|
||||
"visible": true,
|
||||
"defaultValue": {
|
||||
"kind": "number",
|
||||
"value": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ramp:color",
|
||||
"key": "color",
|
||||
"label": "Color",
|
||||
"direction": "output",
|
||||
"dataType": "color",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": "ramp:alpha",
|
||||
"key": "alpha",
|
||||
"label": "Alpha",
|
||||
"direction": "output",
|
||||
"dataType": "float",
|
||||
"accepts": [],
|
||||
"maxIncomingLinks": 0,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"collapsed": false,
|
||||
"extensions": {},
|
||||
"parentId": "frame"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"metadata": {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createApplicationFxNode, type ApplicationFxNode } from "../application-browser.js";
|
||||
import type { FxNodeView } from "@lib/index.js";
|
||||
import { prepareFxNodeBrowserHost } from "../../shared/browser-host.js";
|
||||
import initialLayout from "./initialLayout.json" with { type: "json" };
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#ramp");
|
||||
if (!canvas) throw new Error("Ramp test canvas missing");
|
||||
const host = prepareFxNodeBrowserHost({ canvas });
|
||||
const handle: { root: ApplicationFxNode | null; view: FxNodeView | null; ready: Promise<void> } = {
|
||||
root: null,
|
||||
view: null,
|
||||
ready: Promise.resolve(),
|
||||
};
|
||||
(window as typeof window & { rampTest: typeof handle }).rampTest = handle;
|
||||
handle.ready = (async () => {
|
||||
let root: ApplicationFxNode | undefined, view: FxNodeView | undefined;
|
||||
try {
|
||||
root = await createApplicationFxNode();
|
||||
await root.setState(initialLayout);
|
||||
view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
handle.root = root;
|
||||
handle.view = view;
|
||||
host.attach(root, view);
|
||||
await view.whenRendered();
|
||||
} catch (error) {
|
||||
host.destroy();
|
||||
await view?.detach();
|
||||
root?.destroy();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #111318;
|
||||
color: #e5e5e5;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 22px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 12px;
|
||||
color: #aeb2ba;
|
||||
font-size: 14px;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 1200px;
|
||||
height: 640px;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>fxnode Color Balance</title>
|
||||
<link rel="stylesheet" href="../shared/example.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Color Balance schema</h1>
|
||||
<p>
|
||||
Demonstrates schema, controls, persistence, and presentation. It does not process or composite pixels and does
|
||||
not execute or evaluate the graph.
|
||||
</p>
|
||||
<canvas id="graph" width="1000" height="560" aria-label="Color Balance node graph"></canvas>
|
||||
</main>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { createFxNode, type FxNodeSocketTypeDefinition, type FxNodeStyleDefinition } from "@lib/index.js";
|
||||
import { prepareFxNodeBrowserHost } from "../shared/browser-host.js";
|
||||
import { colorBalanceNode } from "../shared/nodes/color-balance.js";
|
||||
import { exampleTheme } from "../shared/theme.js";
|
||||
const floatSocket = [
|
||||
"float",
|
||||
{ title: "Float", color: "#a8a8a8", acceptsFrom: ["float"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
const colorSocket = [
|
||||
"color",
|
||||
{ title: "Color", color: "#d7ca63", acceptsFrom: ["color"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
const styles = { compositorColor: { header: "#8c5cc4" } } as const satisfies Readonly<
|
||||
Record<string, FxNodeStyleDefinition>
|
||||
>;
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#graph")!,
|
||||
host = prepareFxNodeBrowserHost({ canvas });
|
||||
let cleanedUp = false;
|
||||
function cleanup() {
|
||||
window.removeEventListener("pagehide", cleanup);
|
||||
cleanedUp = true;
|
||||
const root = handle.root,
|
||||
view = handle.view;
|
||||
handle.root = null;
|
||||
handle.view = null;
|
||||
host.destroy();
|
||||
const destroyRoot = () => root?.destroy();
|
||||
if (view) void view.detach().then(destroyRoot, destroyRoot);
|
||||
else destroyRoot();
|
||||
}
|
||||
const handle: StandaloneExampleHandle = {
|
||||
root: null,
|
||||
view: null,
|
||||
host,
|
||||
ready: Promise.resolve(),
|
||||
cleanup,
|
||||
};
|
||||
window.fxnodeStandalone = handle;
|
||||
window.addEventListener("pagehide", cleanup);
|
||||
handle.ready = (async () => {
|
||||
try {
|
||||
const root = await createFxNode({
|
||||
applicationId: "fxnode.example.color-balance",
|
||||
applicationVersion: 1,
|
||||
resources: {},
|
||||
});
|
||||
if (cleanedUp) {
|
||||
root.destroy();
|
||||
return;
|
||||
}
|
||||
handle.root = root;
|
||||
await root.setTheme(exampleTheme);
|
||||
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.initialViewport });
|
||||
handle.view = view;
|
||||
host.attach(root, view);
|
||||
await view.addNode({ nodeId: "color-balance", typeId: colorBalanceNode[0], viewPosition: { x: 300, y: 40 } });
|
||||
await view.whenRendered();
|
||||
} catch (error) {
|
||||
handle.cleanup();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
Vendored
+255
@@ -0,0 +1,255 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
background: #0b0d11;
|
||||
color: #f5f5f4;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 64px 28px 40px;
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, #ed570018, transparent 26rem),
|
||||
radial-gradient(circle at 95% 8%, #6286ff12, transparent 24rem);
|
||||
}
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(270px, 0.7fr);
|
||||
align-items: end;
|
||||
gap: 48px;
|
||||
padding: 52px 0 68px;
|
||||
border-bottom: 1px solid #282b31;
|
||||
}
|
||||
.eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 14px;
|
||||
color: #f07834;
|
||||
font-size: 0.73rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
h1 {
|
||||
max-width: 780px;
|
||||
margin-bottom: 24px;
|
||||
font-size: clamp(3rem, 7vw, 6.4rem);
|
||||
font-weight: 670;
|
||||
letter-spacing: -0.065em;
|
||||
line-height: 0.92;
|
||||
}
|
||||
h1 em {
|
||||
color: #f07834;
|
||||
font-style: normal;
|
||||
}
|
||||
.hero p {
|
||||
max-width: 680px;
|
||||
margin-bottom: 0;
|
||||
color: #aaaeb7;
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.architecture {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
color: #c7cad0;
|
||||
background: #14171cbb;
|
||||
border: 1px solid #30343c;
|
||||
border-radius: 12px;
|
||||
font:
|
||||
650 0.8rem/1.2 ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
monospace;
|
||||
}
|
||||
.architecture span {
|
||||
padding: 11px 13px;
|
||||
background: #1d2026;
|
||||
border: 1px solid #343840;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.architecture i {
|
||||
padding-left: 14px;
|
||||
color: #f07834;
|
||||
font-style: normal;
|
||||
}
|
||||
main {
|
||||
padding: 68px 0;
|
||||
}
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
font-size: clamp(1.8rem, 3vw, 2.55rem);
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
.section-heading > p {
|
||||
margin-bottom: 4px;
|
||||
color: #858a94;
|
||||
}
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
.gallery a {
|
||||
display: block;
|
||||
padding: 18px;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background: #15181d;
|
||||
border: 1px solid #2b2f36;
|
||||
border-radius: 12px;
|
||||
transition: 160ms ease;
|
||||
}
|
||||
.gallery a:hover {
|
||||
border-color: #f07834;
|
||||
box-shadow: 0 18px 48px #0007;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.gallery img {
|
||||
display: block;
|
||||
width: calc(100% + 36px);
|
||||
aspect-ratio: 16 / 9;
|
||||
margin: -18px -18px 18px;
|
||||
object-fit: cover;
|
||||
border-bottom: 1px solid #30343b;
|
||||
}
|
||||
.gallery strong {
|
||||
display: block;
|
||||
margin: 6px 0 7px;
|
||||
font-size: 1.28rem;
|
||||
}
|
||||
.gallery p {
|
||||
margin-bottom: 0;
|
||||
color: #9297a1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tag {
|
||||
color: #f07834;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.gallery .featured {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 210px;
|
||||
padding: 34px;
|
||||
background:
|
||||
linear-gradient(100deg, #181b21 0%, #181b21e8 54%, #211913c9),
|
||||
radial-gradient(circle at 90% 50%, #ed570055, transparent 30%);
|
||||
}
|
||||
.featured strong {
|
||||
max-width: 600px;
|
||||
margin-top: 12px;
|
||||
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.featured p {
|
||||
max-width: 600px;
|
||||
}
|
||||
.open {
|
||||
display: block;
|
||||
margin-top: 28px;
|
||||
color: #f5f5f4;
|
||||
font-weight: 700;
|
||||
}
|
||||
.open b {
|
||||
margin-left: 6px;
|
||||
color: #f07834;
|
||||
}
|
||||
.labs {
|
||||
margin-top: 72px;
|
||||
padding-top: 54px;
|
||||
border-top: 1px solid #282b31;
|
||||
}
|
||||
.labs nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.labs a {
|
||||
min-height: 108px;
|
||||
padding: 16px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background: #13161a;
|
||||
border: 1px solid #292d33;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.labs a:hover {
|
||||
background: #1a1d22;
|
||||
border-color: #f07834;
|
||||
}
|
||||
.labs strong,
|
||||
.labs span {
|
||||
display: block;
|
||||
}
|
||||
.labs strong {
|
||||
margin-bottom: 9px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.labs span {
|
||||
color: #858a94;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
footer {
|
||||
padding: 24px 0 4px;
|
||||
color: #6f747e;
|
||||
border-top: 1px solid #282b31;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
@media (max-width: 820px) {
|
||||
body {
|
||||
padding: 28px 18px;
|
||||
}
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 34px 0 48px;
|
||||
}
|
||||
.architecture {
|
||||
display: none;
|
||||
}
|
||||
.section-heading {
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.gallery {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.gallery .featured {
|
||||
grid-column: auto;
|
||||
}
|
||||
.labs nav {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 460px) {
|
||||
.labs nav {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>fxnode examples</title>
|
||||
<link rel="stylesheet" href="./gallery.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="hero">
|
||||
<div>
|
||||
<span class="eyebrow">Interactive TypeScript examples</span>
|
||||
<h1>Build node editors with <em>fxnode</em></h1>
|
||||
<p>
|
||||
Explore composable nodes, live worker-owned state, independent views, and a Blender-inspired editor. Every
|
||||
example runs locally in your browser.
|
||||
</p>
|
||||
</div>
|
||||
<div class="architecture" aria-label="Architecture summary">
|
||||
<span>Main thread host</span><i>→</i><span>Worker authority</span><i>→</i><span>Shared atlas</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Start here</span>
|
||||
<h2>Standalone applications</h2>
|
||||
</div>
|
||||
<p>Open any card to run it.</p>
|
||||
</div>
|
||||
<nav class="gallery" aria-label="Featured examples">
|
||||
<a href="./minimal/">
|
||||
<img src="./assets/minimal.png" alt="Minimal node example" />
|
||||
<span class="tag">Fundamentals</span><strong>Minimal</strong>
|
||||
<p>Compose a socket and value node from scratch.</p>
|
||||
</a>
|
||||
<a href="./color-balance/">
|
||||
<img src="./assets/color-balance.png" alt="Color Balance example" />
|
||||
<span class="tag">Custom controls</span><strong>Color Balance</strong>
|
||||
<p>Oklab grading wheels, controls, and persistence.</p>
|
||||
</a>
|
||||
<a href="./live-composition/">
|
||||
<img src="./assets/live-composition.png" alt="Live composition example" />
|
||||
<span class="tag">Runtime API</span><strong>Live composition</strong>
|
||||
<p>Version and migrate definitions while the editor is running.</p>
|
||||
</a>
|
||||
<a href="./multi-view/">
|
||||
<img src="./assets/multi-view.png" alt="Multi-view example" />
|
||||
<span class="tag">Shared graph</span><strong>Multi-view</strong>
|
||||
<p>Two cameras and selections backed by one worker and graph.</p>
|
||||
</a>
|
||||
<a class="featured" href="./blender/">
|
||||
<span class="tag">Full application</span><strong>Blender-inspired catalog</strong>
|
||||
<p>Explore the complete interactive node editor and its host-owned add menu.</p>
|
||||
<span class="open">Launch editor <b>→</b></span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<section class="labs">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Component lab</span>
|
||||
<h2>Focused test scenes</h2>
|
||||
</div>
|
||||
<p>Useful for inspecting specific interactions and controls.</p>
|
||||
</div>
|
||||
<nav aria-label="Focused examples">
|
||||
<a href="./blender/all-supported/"><strong>All supported nodes</strong><span>Complete catalog</span></a>
|
||||
<a href="./blender/parity/"><strong>Parity scene</strong><span>Image and grading nodes</span></a>
|
||||
<a href="./blender/control-test/"><strong>Control test</strong><span>Input editing</span></a>
|
||||
<a href="./blender/ramp-test/"><strong>Color Ramp test</strong><span>Stops and interpolation</span></a>
|
||||
<a href="./blender/link-tools-test/"><strong>Link tools</strong><span>Knife, mute, and reroute</span></a>
|
||||
</nav>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>fxnode keeps graph authority in a worker. Application code owns DOM presentation and lifecycle.</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { FxNodeDefinition, FxNodeSocketTypeDefinition, FxNodeStyleDefinition } from "@lib/index.js";
|
||||
export const liveSocket = [
|
||||
"number",
|
||||
{ title: "Number", color: "#74c0fc", acceptsFrom: ["number"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
export const liveStyles = { live: { header: "#0b7285" } } as const satisfies Readonly<
|
||||
Record<string, FxNodeStyleDefinition>
|
||||
>;
|
||||
const base = {
|
||||
title: "Live Parameter",
|
||||
behavior: "standard",
|
||||
style: "live",
|
||||
sockets: {
|
||||
result: {
|
||||
title: "Result",
|
||||
direction: "output",
|
||||
type: "number",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
muteBypass: [],
|
||||
} as const;
|
||||
export const liveNodeV1 = [
|
||||
"example.live.parameter",
|
||||
{
|
||||
...base,
|
||||
version: 1,
|
||||
parameters: { value: { type: "number", default: { kind: "number", value: 1 } } },
|
||||
ui: [
|
||||
{ kind: "parameter", parameter: "value" },
|
||||
{ kind: "socket", socket: "result" },
|
||||
],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
export const liveNodeV2 = [
|
||||
"example.live.parameter",
|
||||
{
|
||||
...base,
|
||||
version: 2,
|
||||
parameters: {
|
||||
value: { type: "number", default: { kind: "number", value: 1 } },
|
||||
detail: { type: "number", default: { kind: "number", value: 0.5 }, minimum: 0, maximum: 1, step: 0.1 },
|
||||
},
|
||||
ui: [
|
||||
{ kind: "parameter", parameter: "value" },
|
||||
{ kind: "parameter", parameter: "detail" },
|
||||
{ kind: "socket", socket: "result" },
|
||||
],
|
||||
migrations: [
|
||||
{ fromVersion: 1, toVersion: 2, steps: [{ kind: "materialize-missing", target: "parameter", key: "detail" }] },
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>fxnode live composition</title>
|
||||
<link rel="stylesheet" href="../shared/example.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Live composition migration</h1>
|
||||
<p>
|
||||
Updates a version 1 schema to version 2 and materializes a missing detail parameter. This demonstrates
|
||||
composition, not graph execution or evaluation.
|
||||
</p>
|
||||
<button id="compose" type="button">Compose version 2</button>
|
||||
<span id="status" role="status">Version 1 ready</span
|
||||
><canvas id="graph" width="1000" height="560" aria-label="Live composition node graph"></canvas>
|
||||
</main>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { createFxNode } from "@lib/index.js";
|
||||
import { prepareFxNodeBrowserHost } from "../shared/browser-host.js";
|
||||
import { exampleTheme } from "../shared/theme.js";
|
||||
import { liveNodeV1, liveNodeV2, liveSocket, liveStyles } from "./definitions.js";
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#graph")!,
|
||||
button = document.querySelector<HTMLButtonElement>("#compose")!,
|
||||
status = document.querySelector<HTMLElement>("#status")!,
|
||||
host = prepareFxNodeBrowserHost({ canvas });
|
||||
let cleanedUp = false;
|
||||
function cleanup() {
|
||||
window.removeEventListener("pagehide", cleanup);
|
||||
cleanedUp = true;
|
||||
button.removeEventListener("click", compose);
|
||||
const root = handle.root,
|
||||
view = handle.view;
|
||||
handle.root = null;
|
||||
handle.view = null;
|
||||
host.destroy();
|
||||
const destroyRoot = () => root?.destroy();
|
||||
if (view) void view.detach().then(destroyRoot, destroyRoot);
|
||||
else destroyRoot();
|
||||
}
|
||||
const handle: StandaloneExampleHandle = {
|
||||
root: null,
|
||||
view: null,
|
||||
host,
|
||||
ready: Promise.resolve(),
|
||||
cleanup,
|
||||
};
|
||||
window.fxnodeStandalone = handle;
|
||||
window.addEventListener("pagehide", cleanup);
|
||||
let revision = 0;
|
||||
async function compose() {
|
||||
if (!handle.root || !handle.view) return;
|
||||
button.disabled = true;
|
||||
status.textContent = "Composing version 2…";
|
||||
try {
|
||||
const receipt = await handle.root.composeNode(...liveNodeV2, { expectedRevision: revision });
|
||||
revision = receipt.revision;
|
||||
handle.lastCompositionReceipt = receipt;
|
||||
status.textContent = `Version 2 ${receipt.status}; revision ${receipt.revision}; graph ${receipt.graphChanged ? "migrated" : "unchanged"}`;
|
||||
await handle.view.whenRendered();
|
||||
} catch (error) {
|
||||
status.textContent = error instanceof Error ? error.message : String(error);
|
||||
button.disabled = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
button.addEventListener("click", compose);
|
||||
handle.ready = (async () => {
|
||||
try {
|
||||
const root = await createFxNode({
|
||||
applicationId: "fxnode.example.live-composition",
|
||||
applicationVersion: 1,
|
||||
resources: {},
|
||||
});
|
||||
if (cleanedUp) {
|
||||
root.destroy();
|
||||
return;
|
||||
}
|
||||
handle.root = root;
|
||||
await root.setTheme(exampleTheme);
|
||||
await root.setHeaderStyles(liveStyles);
|
||||
await root.composeSocket(...liveSocket);
|
||||
const receipt = await root.composeNode(...liveNodeV1);
|
||||
revision = receipt.revision;
|
||||
await root.setState({ graphId: "live", catalogVersion: 1, nodes: [], links: [], metadata: {} });
|
||||
const view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
handle.view = view;
|
||||
host.attach(root, view);
|
||||
const added = await view.addNode({
|
||||
nodeId: "live-node",
|
||||
typeId: liveNodeV1[0],
|
||||
viewPosition: { x: 340, y: 160 },
|
||||
});
|
||||
handle.graphVersion = added.version;
|
||||
await view.whenRendered();
|
||||
} catch (error) {
|
||||
handle.cleanup();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import type { FxNodeDefinition, FxNodeSocketTypeDefinition, FxNodeStyleDefinition } from "@lib/index.js";
|
||||
export const numberSocket = [
|
||||
"number",
|
||||
{ title: "Number", color: "#a8a8a8", acceptsFrom: ["number"] },
|
||||
] as const satisfies readonly [string, FxNodeSocketTypeDefinition];
|
||||
export const minimalStyles = { value: { header: "#4c6ef5" } } as const satisfies Readonly<
|
||||
Record<string, FxNodeStyleDefinition>
|
||||
>;
|
||||
export const valueNode = [
|
||||
"example.minimal.value",
|
||||
{
|
||||
version: 1,
|
||||
title: "Number Value",
|
||||
behavior: "standard",
|
||||
style: "value",
|
||||
parameters: { value: { type: "number", default: { kind: "number", value: 42 }, step: 1 } },
|
||||
sockets: {
|
||||
value: {
|
||||
title: "Value",
|
||||
direction: "output",
|
||||
type: "number",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{ kind: "parameter", parameter: "value" },
|
||||
{ kind: "socket", socket: "value" },
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>fxnode minimal example</title>
|
||||
<link rel="stylesheet" href="../shared/example.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Minimal custom node</h1>
|
||||
<p>The smallest useful custom number/value node.</p>
|
||||
<canvas id="graph" width="1000" height="560" aria-label="Minimal node graph"></canvas>
|
||||
</main>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { createFxNode } from "@lib/index.js";
|
||||
import { prepareFxNodeBrowserHost } from "../shared/browser-host.js";
|
||||
import { exampleTheme } from "../shared/theme.js";
|
||||
import { minimalStyles, numberSocket, valueNode } from "./definition.js";
|
||||
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#graph")!;
|
||||
const host = prepareFxNodeBrowserHost({ canvas });
|
||||
let cleanedUp = false;
|
||||
function cleanup() {
|
||||
window.removeEventListener("pagehide", cleanup);
|
||||
cleanedUp = true;
|
||||
const root = handle.root,
|
||||
view = handle.view;
|
||||
handle.root = null;
|
||||
handle.view = null;
|
||||
host.destroy();
|
||||
const destroyRoot = () => root?.destroy();
|
||||
if (view) void view.detach().then(destroyRoot, destroyRoot);
|
||||
else destroyRoot();
|
||||
}
|
||||
const handle: StandaloneExampleHandle = {
|
||||
root: null,
|
||||
view: null,
|
||||
host,
|
||||
ready: Promise.resolve(),
|
||||
cleanup,
|
||||
};
|
||||
window.fxnodeStandalone = handle;
|
||||
window.addEventListener("pagehide", cleanup);
|
||||
handle.ready = (async () => {
|
||||
try {
|
||||
const root = await createFxNode({
|
||||
applicationId: "fxnode.example.minimal",
|
||||
applicationVersion: 1,
|
||||
resources: {},
|
||||
});
|
||||
if (cleanedUp) {
|
||||
root.destroy();
|
||||
return;
|
||||
}
|
||||
handle.root = root;
|
||||
await root.setTheme(exampleTheme);
|
||||
await root.setHeaderStyles(minimalStyles);
|
||||
await root.composeSocket(...numberSocket);
|
||||
await root.composeNode(...valueNode);
|
||||
await root.setState({ graphId: "minimal", catalogVersion: 1, nodes: [], links: [], metadata: {} });
|
||||
const view = await root.attachView({ canvas, viewport: host.initialViewport });
|
||||
handle.view = view;
|
||||
host.attach(root, view);
|
||||
await view.addNode({ nodeId: "value", typeId: valueNode[0], viewPosition: { x: 360, y: 190 } });
|
||||
await view.whenRendered();
|
||||
} catch (error) {
|
||||
handle.cleanup();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user