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:
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;
|
||||
}
|
||||
})();
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>fxnode multi-view example</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<div>
|
||||
<h1>One graph, two views</h1>
|
||||
<p>Selection and camera are local to each canvas; graph edits render in both.</p>
|
||||
</div>
|
||||
<div id="toolbar" role="toolbar" aria-label="Active view tools">
|
||||
<label
|
||||
>Node
|
||||
<select id="node-type">
|
||||
<option value="fxnode.shader.value">Value</option>
|
||||
<option value="fxnode.shader.color">Color</option>
|
||||
<option value="fxnode.shader.math">Math</option>
|
||||
<option value="fxnode.shader.noise-texture">Noise Texture</option>
|
||||
</select></label
|
||||
>
|
||||
<button data-action="add">Add</button>
|
||||
<button data-action="delete" disabled>Delete</button>
|
||||
<button data-action="mute" aria-pressed="false" disabled>Mute</button>
|
||||
<output id="toolbar-status" aria-live="polite"></output>
|
||||
</div>
|
||||
</header>
|
||||
<section class="views">
|
||||
<article class="active">
|
||||
<h2>Material detail</h2>
|
||||
<canvas id="view-a"></canvas>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Output overview</h2>
|
||||
<canvas id="view-b"></canvas>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import type { FxNodeView } from "@lib/index.js";
|
||||
import { createApplicationFxNode } from "../blender/application-browser.js";
|
||||
import initialLayout from "../blender/all-supported/initialLayout.json" with { type: "json" };
|
||||
import { prepareFxNodeBrowserHost } from "../shared/browser-host.js";
|
||||
|
||||
const canvases = [
|
||||
document.querySelector<HTMLCanvasElement>("#view-a")!,
|
||||
document.querySelector<HTMLCanvasElement>("#view-b")!,
|
||||
];
|
||||
const toolbar = document.querySelector<HTMLElement>("#toolbar")!;
|
||||
const nodeType = document.querySelector<HTMLSelectElement>("#node-type")!;
|
||||
const addButton = toolbar.querySelector<HTMLButtonElement>("[data-action=add]")!;
|
||||
const deleteButton = toolbar.querySelector<HTMLButtonElement>("[data-action=delete]")!;
|
||||
const muteButton = toolbar.querySelector<HTMLButtonElement>("[data-action=mute]")!;
|
||||
const status = toolbar.querySelector<HTMLOutputElement>("#toolbar-status")!;
|
||||
let activeIndex = 0,
|
||||
toolbarPending = false,
|
||||
cleaned = false,
|
||||
root: Awaited<ReturnType<typeof createApplicationFxNode>> | null = null;
|
||||
const views: FxNodeView[] = [],
|
||||
unsubscribers: (() => void)[] = [];
|
||||
const hosts = canvases.map((canvas) => prepareFxNodeBrowserHost({ canvas, lifecycle: "detach-on-disconnect" }));
|
||||
const renderCounts = [0, 0];
|
||||
function updateToolbar() {
|
||||
const snapshot = views[activeIndex]?.getHostSnapshot();
|
||||
const muted = snapshot?.selection.mute.enabled === true && snapshot.selection.mute.state === "all-muted";
|
||||
addButton.disabled = toolbarPending || !views[activeIndex];
|
||||
deleteButton.disabled = toolbarPending || !snapshot?.selection.canRemove;
|
||||
muteButton.disabled = toolbarPending || snapshot?.selection.mute.enabled !== true;
|
||||
muteButton.setAttribute("aria-pressed", String(muted));
|
||||
muteButton.textContent = muted ? "Unmute" : "Mute";
|
||||
}
|
||||
function activate(index: number) {
|
||||
activeIndex = index;
|
||||
canvases.forEach((canvas, candidate) => canvas.parentElement?.classList.toggle("active", candidate === index));
|
||||
updateToolbar();
|
||||
}
|
||||
const pointerListeners = canvases
|
||||
.map((canvas, index) => {
|
||||
const listener = () => activate(index);
|
||||
canvas.addEventListener("pointerdown", listener);
|
||||
return [{ canvas, listener }];
|
||||
})
|
||||
.flat();
|
||||
const toolbarListener = async (event: Event) => {
|
||||
const action = (event.target as HTMLElement).closest<HTMLButtonElement>("button")?.dataset.action,
|
||||
view = views[activeIndex];
|
||||
if (!action || !view || toolbarPending) return;
|
||||
toolbarPending = true;
|
||||
status.textContent = "";
|
||||
updateToolbar();
|
||||
try {
|
||||
const viewport = hosts[activeIndex]!.currentViewport;
|
||||
await (action === "add"
|
||||
? view.addNode({
|
||||
typeId: nodeType.value,
|
||||
viewPosition: { x: viewport.width / 2, y: viewport.height / 2 },
|
||||
})
|
||||
: action === "delete"
|
||||
? view.removeSelected()
|
||||
: view.setSelectedMuted(muteButton.getAttribute("aria-pressed") !== "true"));
|
||||
} catch (error) {
|
||||
status.textContent = error instanceof Error ? error.message : "Action failed";
|
||||
} finally {
|
||||
toolbarPending = false;
|
||||
if (!cleaned) updateToolbar();
|
||||
}
|
||||
};
|
||||
toolbar.addEventListener("click", toolbarListener);
|
||||
|
||||
async function cleanup() {
|
||||
if (cleaned) return;
|
||||
cleaned = true;
|
||||
window.removeEventListener("pagehide", cleanup);
|
||||
toolbar.removeEventListener("click", toolbarListener);
|
||||
for (const { canvas, listener } of pointerListeners) canvas.removeEventListener("pointerdown", listener);
|
||||
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
||||
hosts.forEach((host) => host.destroy());
|
||||
await Promise.allSettled(views.map((view) => view.detach()));
|
||||
root?.destroy();
|
||||
root = null;
|
||||
handle.root = null;
|
||||
handle.views = [];
|
||||
}
|
||||
const handle: MultiViewExampleHandle = { root: null, views, ready: Promise.resolve(), cleanup, renderCounts };
|
||||
window.fxnodeMultiView = handle;
|
||||
window.addEventListener("pagehide", cleanup);
|
||||
handle.ready = (async () => {
|
||||
try {
|
||||
const created = await createApplicationFxNode();
|
||||
if (cleaned) {
|
||||
created.destroy();
|
||||
return;
|
||||
}
|
||||
root = created;
|
||||
handle.root = created;
|
||||
await root.setState(initialLayout);
|
||||
for (const [index, canvas] of canvases.entries()) {
|
||||
const host = hosts[index]!,
|
||||
viewport = host.initialViewport;
|
||||
const context = canvas.getContext("2d")!,
|
||||
drawImage = context.drawImage.bind(context);
|
||||
context.drawImage = ((...args: Parameters<CanvasRenderingContext2D["drawImage"]>) => {
|
||||
renderCounts[index]!++;
|
||||
Reflect.apply(drawImage, context, args);
|
||||
}) as typeof context.drawImage;
|
||||
const view = await root.attachView({
|
||||
canvas,
|
||||
viewport,
|
||||
initialCamera: index
|
||||
? { center: { x: 2080, y: -400 }, zoom: 0.45 }
|
||||
: { center: { x: 480, y: -550 }, zoom: 0.5 },
|
||||
});
|
||||
views.push(view);
|
||||
host.attach(root, view);
|
||||
unsubscribers.push(view.subscribeHost(updateToolbar));
|
||||
}
|
||||
await Promise.all(views.map((view) => view.whenRendered()));
|
||||
updateToolbar();
|
||||
} catch (error) {
|
||||
await cleanup();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
background: #101216;
|
||||
color: #f1f3f5;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
width: 1160px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 5px;
|
||||
font-size: 25px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #adb5bd;
|
||||
}
|
||||
#toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px;
|
||||
background: #1d1f23;
|
||||
border: 1px solid #343a40;
|
||||
border-radius: 7px;
|
||||
}
|
||||
select,
|
||||
button {
|
||||
padding: 7px 10px;
|
||||
color: inherit;
|
||||
background: #292c31;
|
||||
border: 1px solid #495057;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #748ffc;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
#toolbar-status {
|
||||
max-width: 180px;
|
||||
color: #ff8787;
|
||||
font-size: 12px;
|
||||
}
|
||||
.views {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
article {
|
||||
padding: 8px;
|
||||
background: #181a1f;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
article.active {
|
||||
border-color: #5c7cfa;
|
||||
}
|
||||
h2 {
|
||||
margin: 2px 3px 9px;
|
||||
font-size: 14px;
|
||||
color: #ced4da;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 554px;
|
||||
height: 520px;
|
||||
background: #1d1f23;
|
||||
touch-action: none;
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import type { FxNodeView } from "@lib/index.js";
|
||||
|
||||
/** Application-owned add-node menu used by the standalone examples. */
|
||||
|
||||
interface Point {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
}
|
||||
|
||||
export interface AddNodeMenu {
|
||||
open(viewPosition: Point): void;
|
||||
close(restoreCanvasFocus?: boolean): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
function cloneElement<T extends Element>(template: HTMLTemplateElement, selector: string): T {
|
||||
const element = template.content.firstElementChild?.cloneNode(true);
|
||||
if (!(element instanceof Element)) throw new Error(`Add-node menu template ${selector} is empty`);
|
||||
return element as T;
|
||||
}
|
||||
|
||||
export function createAddNodeMenu(
|
||||
template: HTMLTemplateElement,
|
||||
canvas: HTMLCanvasElement,
|
||||
api: FxNodeView,
|
||||
onError: (error: unknown) => void,
|
||||
): AddNodeMenu {
|
||||
const entries = [...template.content.querySelectorAll<HTMLButtonElement>("button[data-fxnode-menu-option]")].map(
|
||||
(option) => {
|
||||
const typeId = option.dataset.typeId;
|
||||
const title = option.textContent?.trim();
|
||||
const group = option.closest<HTMLElement>("[data-fxnode-menu-group]");
|
||||
const groupTitle = group?.querySelector<HTMLElement>("[data-fxnode-menu-heading]")?.textContent?.trim();
|
||||
if (!typeId || !title || !group || !groupTitle) throw new Error("Add-node menu option is missing HTML context");
|
||||
return {
|
||||
typeId,
|
||||
searchText: `${title} ${groupTitle} ${typeId} ${option.dataset.keywords ?? ""}`.toLowerCase(),
|
||||
option,
|
||||
};
|
||||
},
|
||||
);
|
||||
if (!entries.length) throw new Error("Add-node menu HTML template has no node options");
|
||||
let host: HTMLDivElement | undefined;
|
||||
let removeListeners = () => {};
|
||||
|
||||
const close = (restore = false) => {
|
||||
if (!host) return;
|
||||
removeListeners();
|
||||
host.remove();
|
||||
host = undefined;
|
||||
if (restore) canvas.focus();
|
||||
};
|
||||
|
||||
const open = (viewPosition: Point) => {
|
||||
close();
|
||||
host = cloneElement<HTMLDivElement>(template, "root");
|
||||
const input = host.querySelector<HTMLInputElement>("[data-fxnode-menu-search]");
|
||||
const results = host.querySelector<HTMLDivElement>("[data-fxnode-menu-results]");
|
||||
const empty = host.querySelector<HTMLElement>("[data-fxnode-menu-empty]");
|
||||
const options = [...host.querySelectorAll<HTMLButtonElement>("button[data-fxnode-menu-option]")];
|
||||
if (!input || !results || !empty || options.length !== entries.length) {
|
||||
host.remove();
|
||||
host = undefined;
|
||||
throw new Error("Add-node menu HTML template is incomplete");
|
||||
}
|
||||
|
||||
let visible = entries.slice();
|
||||
let active = 0;
|
||||
const choose = (index: number) => {
|
||||
const item = visible[index];
|
||||
if (!item) return;
|
||||
close(true);
|
||||
void api.addNode({ typeId: item.typeId, viewPosition }).catch(onError);
|
||||
};
|
||||
const render = () => {
|
||||
entries.forEach((item, sourceIndex) => {
|
||||
const button = options[sourceIndex]!;
|
||||
const index = visible.indexOf(item);
|
||||
button.hidden = index < 0;
|
||||
if (index >= 0) {
|
||||
button.classList.toggle("active", index === active);
|
||||
button.id = `fxnode-node-option-${index}`;
|
||||
button.setAttribute("aria-selected", String(index === active));
|
||||
button.onpointerenter = () => {
|
||||
if (active !== index) {
|
||||
active = index;
|
||||
render();
|
||||
}
|
||||
};
|
||||
button.onclick = () => choose(index);
|
||||
}
|
||||
});
|
||||
for (const group of results.querySelectorAll<HTMLElement>("[data-fxnode-menu-group]"))
|
||||
group.hidden = !group.querySelector("button[data-fxnode-menu-option]:not([hidden])");
|
||||
empty.hidden = visible.length > 0;
|
||||
input.setAttribute("aria-expanded", "true");
|
||||
input.setAttribute("aria-activedescendant", visible.length ? `fxnode-node-option-${active}` : "");
|
||||
};
|
||||
input.oninput = () => {
|
||||
const query = input.value.trim().toLowerCase();
|
||||
visible = entries.filter((item) => !query || item.searchText.includes(query));
|
||||
active = 0;
|
||||
render();
|
||||
};
|
||||
input.onkeydown = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
close(true);
|
||||
return;
|
||||
}
|
||||
if (!visible.length) return;
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") {
|
||||
event.preventDefault();
|
||||
active =
|
||||
event.key === "Home"
|
||||
? 0
|
||||
: event.key === "End"
|
||||
? visible.length - 1
|
||||
: (active + (event.key === "ArrowDown" ? 1 : -1) + visible.length) % visible.length;
|
||||
render();
|
||||
host?.querySelector(`#fxnode-node-option-${active}`)?.scrollIntoView({ block: "nearest" });
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
choose(active);
|
||||
}
|
||||
};
|
||||
|
||||
render();
|
||||
document.body.append(host);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const menu = host.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
host.style.left = `${Math.max(margin, Math.min(rect.left + viewPosition.x, window.innerWidth - menu.width - margin))}px`;
|
||||
host.style.top = `${Math.max(margin, Math.min(rect.top + viewPosition.y, window.innerHeight - menu.height - margin))}px`;
|
||||
host.style.visibility = "visible";
|
||||
input.focus();
|
||||
const outside = (event: PointerEvent) => {
|
||||
if (host && !event.composedPath().includes(host)) close(false);
|
||||
};
|
||||
const blur = () => close(false);
|
||||
const scroll = () => close(false);
|
||||
document.addEventListener("pointerdown", outside, true);
|
||||
window.addEventListener("blur", blur);
|
||||
window.addEventListener("scroll", scroll, true);
|
||||
removeListeners = () => {
|
||||
document.removeEventListener("pointerdown", outside, true);
|
||||
window.removeEventListener("blur", blur);
|
||||
window.removeEventListener("scroll", scroll, true);
|
||||
removeListeners = () => {};
|
||||
};
|
||||
};
|
||||
|
||||
return { open, close, destroy: () => close(false) };
|
||||
}
|
||||
+439
@@ -0,0 +1,439 @@
|
||||
import type { FxNode, FxNodeModifiers, FxNodeResourceOpenRequest, FxNodeView, FxNodeViewport } from "@lib/index.js";
|
||||
import { createAddNodeMenu, type AddNodeMenu } from "./add-node-menu.js";
|
||||
|
||||
/** Explicit DOM host adapter; callers own attachment and cleanup. */
|
||||
|
||||
export interface FxNodeBrowserHostOptions {
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
readonly lifecycle?: "explicit" | "detach-on-disconnect";
|
||||
readonly addNodeMenuTemplate?: HTMLTemplateElement;
|
||||
readonly activateResourcePicker?: (request: FxNodeResourceOpenRequest) => void | Promise<void>;
|
||||
readonly onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export interface PreparedFxNodeBrowserHost {
|
||||
readonly initialViewport: FxNodeViewport;
|
||||
readonly currentViewport: FxNodeViewport;
|
||||
attach(root: FxNode, view: FxNodeView): void;
|
||||
syncViewport(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
const INPUT_EVENTS = [
|
||||
"pointerdown",
|
||||
"pointermove",
|
||||
"pointerup",
|
||||
"pointercancel",
|
||||
"mousedown",
|
||||
"wheel",
|
||||
"keydown",
|
||||
"keyup",
|
||||
"focus",
|
||||
"blur",
|
||||
] as const;
|
||||
const activeHosts = new WeakMap<HTMLCanvasElement, PreparedFxNodeBrowserHost>();
|
||||
type DisconnectRegistration = { canvas: HTMLCanvasElement; disconnected(): void };
|
||||
const disconnectRegistries = new WeakMap<
|
||||
Document,
|
||||
{ observer: MutationObserver; registrations: Set<DisconnectRegistration> }
|
||||
>();
|
||||
|
||||
function watchDisconnect(documentValue: Document, registration: DisconnectRegistration): () => void {
|
||||
let registry = disconnectRegistries.get(documentValue);
|
||||
if (!registry) {
|
||||
const registrations = new Set<DisconnectRegistration>();
|
||||
const Observer = documentValue.defaultView?.MutationObserver;
|
||||
if (!Observer) throw new Error("detach-on-disconnect requires a connected canvas and MutationObserver");
|
||||
const observer = new Observer(() => {
|
||||
for (const candidate of registrations) {
|
||||
if (candidate.canvas.ownerDocument === documentValue && candidate.canvas.isConnected) continue;
|
||||
queueMicrotask(() => {
|
||||
if (
|
||||
registrations.has(candidate) &&
|
||||
(candidate.canvas.ownerDocument !== documentValue || !candidate.canvas.isConnected)
|
||||
)
|
||||
candidate.disconnected();
|
||||
});
|
||||
}
|
||||
});
|
||||
observer.observe(documentValue, { childList: true, subtree: true });
|
||||
registry = { observer, registrations };
|
||||
disconnectRegistries.set(documentValue, registry);
|
||||
}
|
||||
registry.registrations.add(registration);
|
||||
return () => {
|
||||
registry!.registrations.delete(registration);
|
||||
if (registry!.registrations.size === 0) {
|
||||
registry!.observer.disconnect();
|
||||
disconnectRegistries.delete(documentValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function measureViewport(canvas: HTMLCanvasElement, devicePixelRatio: number): FxNodeViewport {
|
||||
const dpr = Math.min(4, Math.max(1, devicePixelRatio || 1)),
|
||||
maxLogicalDimension = Math.floor(8192 / dpr),
|
||||
maxLogicalPixels = Math.floor(16_777_216 / (dpr * dpr)),
|
||||
width = Math.min(maxLogicalDimension, Math.max(1, canvas.clientWidth)),
|
||||
height = Math.min(maxLogicalDimension, Math.floor(maxLogicalPixels / width), Math.max(1, canvas.clientHeight));
|
||||
return { width, height, dpr };
|
||||
}
|
||||
|
||||
function sameViewport(left: FxNodeViewport, right: FxNodeViewport): boolean {
|
||||
return left.width === right.width && left.height === right.height && left.dpr === right.dpr;
|
||||
}
|
||||
function sizeCanvas(canvas: HTMLCanvasElement, viewport: FxNodeViewport): void {
|
||||
const width = Math.max(1, Math.round(viewport.width * viewport.dpr)),
|
||||
height = Math.max(1, Math.round(viewport.height * viewport.dpr));
|
||||
if (canvas.width !== width) canvas.width = width;
|
||||
if (canvas.height !== height) canvas.height = height;
|
||||
}
|
||||
function modifiers(event: MouseEvent | KeyboardEvent): FxNodeModifiers {
|
||||
return { alt: event.altKey, control: event.ctrlKey, meta: event.metaKey, shift: event.shiftKey };
|
||||
}
|
||||
export function prepareFxNodeBrowserHost({
|
||||
canvas,
|
||||
lifecycle = "explicit",
|
||||
addNodeMenuTemplate,
|
||||
activateResourcePicker,
|
||||
onError = console.error,
|
||||
}: FxNodeBrowserHostOptions): PreparedFxNodeBrowserHost {
|
||||
if (activeHosts.has(canvas)) throw new Error("Canvas already has an active FxNode browser host");
|
||||
if (
|
||||
lifecycle === "detach-on-disconnect" &&
|
||||
(!canvas.isConnected || !canvas.ownerDocument.defaultView?.MutationObserver)
|
||||
)
|
||||
throw new Error("detach-on-disconnect requires a connected canvas and MutationObserver");
|
||||
const ownerDocument = canvas.ownerDocument,
|
||||
ownerWindow = ownerDocument.defaultView ?? window,
|
||||
report = (error: unknown) => {
|
||||
try {
|
||||
onError(error);
|
||||
} catch (reportError) {
|
||||
console.error("FxNode browser host error callback failed", reportError);
|
||||
}
|
||||
};
|
||||
const originalTabIndex = canvas.getAttribute("tabindex"),
|
||||
originalTouchAction = canvas.style.touchAction;
|
||||
let viewport = measureViewport(canvas, ownerWindow.devicePixelRatio);
|
||||
sizeCanvas(canvas, viewport);
|
||||
let view: FxNodeView | undefined,
|
||||
active = true,
|
||||
attached = false,
|
||||
lifecycleGeneration = 0,
|
||||
authorization: { request: FxNodeResourceOpenRequest; generation: number } | undefined,
|
||||
observer: ResizeObserver | undefined;
|
||||
let unwatchDisconnect: (() => void) | undefined,
|
||||
pendingViewport: FxNodeViewport | undefined,
|
||||
resizeInFlight = false;
|
||||
let changedTabIndex = false,
|
||||
appliedTabIndex: string | null = null,
|
||||
changedTouchAction = false,
|
||||
pickerGeneration = 0,
|
||||
menuPending = false;
|
||||
const capturedPointers = new Set<number>(),
|
||||
subscriptions = new Set<() => void>();
|
||||
let menu: AddNodeMenu | undefined;
|
||||
let resourceFile: HTMLInputElement | undefined;
|
||||
|
||||
const defaultPicker = (request: FxNodeResourceOpenRequest) => {
|
||||
if (!resourceFile) {
|
||||
resourceFile = ownerDocument.createElement("input");
|
||||
resourceFile.type = "file";
|
||||
resourceFile.hidden = true;
|
||||
resourceFile.dataset.fxnodeResourceFile = "";
|
||||
ownerDocument.body.append(resourceFile);
|
||||
resourceFile.addEventListener("change", resourceChanged);
|
||||
}
|
||||
const generation = ++pickerGeneration;
|
||||
authorization = { request, generation };
|
||||
resourceFile.accept = request.resource.accept.join(",");
|
||||
resourceFile.value = "";
|
||||
resourceFile.click();
|
||||
};
|
||||
const resourceChanged = () => {
|
||||
const pending = authorization,
|
||||
file = resourceFile?.files?.[0];
|
||||
authorization = undefined;
|
||||
if (!pending || !file || !view) return;
|
||||
void file
|
||||
.arrayBuffer()
|
||||
.then((bytes) => {
|
||||
if (active && view && pending.generation === pickerGeneration)
|
||||
return view.provideResource(pending.request.authorization, { name: file.name, mime: file.type, bytes });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (active && pending.generation === pickerGeneration) report(error);
|
||||
});
|
||||
};
|
||||
const activate = activateResourcePicker ?? defaultPicker;
|
||||
const position = (event: MouseEvent) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
||||
};
|
||||
|
||||
const input = (event: Event) => {
|
||||
if (!active || !view) return;
|
||||
if (event instanceof PointerEvent) {
|
||||
const phase =
|
||||
event.type === "pointerdown"
|
||||
? "down"
|
||||
: event.type === "pointermove"
|
||||
? "move"
|
||||
: event.type === "pointerup"
|
||||
? "up"
|
||||
: "cancel";
|
||||
const point = position(event);
|
||||
if (phase === "down") {
|
||||
menu?.close(false);
|
||||
menuPending = event.button === 2 && !event.ctrlKey && (event.buttons & 1) === 0;
|
||||
canvas.focus();
|
||||
try {
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
capturedPointers.add(event.pointerId);
|
||||
} catch {
|
||||
/* unsupported or detached */
|
||||
}
|
||||
}
|
||||
if ((phase === "up" || phase === "cancel") && capturedPointers.delete(event.pointerId))
|
||||
try {
|
||||
if (canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
|
||||
} catch {
|
||||
/* detached */
|
||||
}
|
||||
view.feedInput({
|
||||
kind: "pointer",
|
||||
phase,
|
||||
pointerId: event.pointerId,
|
||||
pointerType: event.pointerType,
|
||||
position: point,
|
||||
button: event.button,
|
||||
buttons: event.buttons,
|
||||
modifiers: modifiers(event),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event instanceof WheelEvent) {
|
||||
event.preventDefault();
|
||||
menu?.close(false);
|
||||
menuPending = false;
|
||||
const rect = canvas.getBoundingClientRect(),
|
||||
scale =
|
||||
event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
||||
? 16
|
||||
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
||||
? Math.max(1, rect.height)
|
||||
: 1;
|
||||
view.feedInput({
|
||||
kind: "wheel",
|
||||
position: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
||||
delta: { x: event.deltaX * scale, y: event.deltaY * scale },
|
||||
modifiers: modifiers(event),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event instanceof MouseEvent) {
|
||||
if (event.button !== 2 || (event.buttons & 1) === 0) return;
|
||||
menuPending = false;
|
||||
view.feedInput({
|
||||
kind: "pointer",
|
||||
phase: "down",
|
||||
pointerId: 1,
|
||||
pointerType: "mouse",
|
||||
position: position(event),
|
||||
button: event.button,
|
||||
buttons: event.buttons,
|
||||
modifiers: modifiers(event),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event instanceof KeyboardEvent) {
|
||||
menu?.close(false);
|
||||
menuPending = false;
|
||||
view.feedInput({
|
||||
kind: "key",
|
||||
phase: event.type === "keydown" ? "down" : "up",
|
||||
key: event.key,
|
||||
code: event.code,
|
||||
repeat: event.repeat,
|
||||
modifiers: modifiers(event),
|
||||
});
|
||||
return;
|
||||
}
|
||||
view.feedInput({ kind: "focus", phase: event.type === "focus" ? "focus" : "blur" });
|
||||
};
|
||||
const contextMenu = (event: Event) => event.preventDefault();
|
||||
const lostCapture = (event: PointerEvent) => capturedPointers.delete(event.pointerId);
|
||||
const outsidePointer = (event: PointerEvent) => {
|
||||
if (
|
||||
active &&
|
||||
view &&
|
||||
event.button === 0 &&
|
||||
view.getHostSnapshot().colorPickerOpen &&
|
||||
event.target !== canvas &&
|
||||
!canvas.contains(event.target as Node)
|
||||
) {
|
||||
menuPending = false;
|
||||
view.feedInput({ kind: "outside-pointer", button: 0 });
|
||||
}
|
||||
};
|
||||
const pumpViewport = () => {
|
||||
if (!active || !view || resizeInFlight || !pendingViewport) return;
|
||||
const next = pendingViewport,
|
||||
generation = lifecycleGeneration;
|
||||
pendingViewport = undefined;
|
||||
if (sameViewport(viewport, next)) {
|
||||
pumpViewport();
|
||||
return;
|
||||
}
|
||||
resizeInFlight = true;
|
||||
menu?.close(false);
|
||||
menuPending = false;
|
||||
void Promise.resolve(view.setViewport(next))
|
||||
.then(() => {
|
||||
if (!active || generation !== lifecycleGeneration) return;
|
||||
viewport = next;
|
||||
sizeCanvas(canvas, next);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (active && generation === lifecycleGeneration) report(error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!active || generation !== lifecycleGeneration) return;
|
||||
resizeInFlight = false;
|
||||
pumpViewport();
|
||||
});
|
||||
};
|
||||
const syncViewport = () => {
|
||||
if (!active) return;
|
||||
const next = measureViewport(canvas, ownerWindow.devicePixelRatio);
|
||||
if (view) {
|
||||
pendingViewport = next;
|
||||
pumpViewport();
|
||||
} else {
|
||||
viewport = next;
|
||||
sizeCanvas(canvas, next);
|
||||
}
|
||||
};
|
||||
const resize = () => syncViewport();
|
||||
const host: PreparedFxNodeBrowserHost = {
|
||||
initialViewport: viewport,
|
||||
get currentViewport() {
|
||||
return viewport;
|
||||
},
|
||||
attach(rootValue, viewValue) {
|
||||
if (!active) throw new Error("FxNode browser host has been destroyed");
|
||||
if (attached) throw new Error("FxNode browser host is already attached");
|
||||
if (lifecycle === "detach-on-disconnect" && (!canvas.isConnected || canvas.ownerDocument !== ownerDocument)) {
|
||||
host.destroy();
|
||||
throw new Error("detach-on-disconnect requires the canvas to remain connected to its original document");
|
||||
}
|
||||
attached = true;
|
||||
view = viewValue;
|
||||
try {
|
||||
if (addNodeMenuTemplate) menu = createAddNodeMenu(addNodeMenuTemplate, canvas, viewValue, report);
|
||||
if (canvas.tabIndex < 0) {
|
||||
canvas.tabIndex = 0;
|
||||
changedTabIndex = true;
|
||||
appliedTabIndex = canvas.getAttribute("tabindex");
|
||||
}
|
||||
if (canvas.style.touchAction !== "none") {
|
||||
canvas.style.touchAction = "none";
|
||||
changedTouchAction = true;
|
||||
}
|
||||
observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(resize);
|
||||
for (const name of INPUT_EVENTS) canvas.addEventListener(name, input, { passive: name !== "wheel" });
|
||||
canvas.addEventListener("contextmenu", contextMenu);
|
||||
canvas.addEventListener("lostpointercapture", lostCapture);
|
||||
ownerDocument.addEventListener("pointerdown", outsidePointer, true);
|
||||
ownerWindow.addEventListener("resize", resize);
|
||||
observer?.observe(canvas);
|
||||
if (lifecycle === "detach-on-disconnect")
|
||||
unwatchDisconnect = watchDisconnect(ownerDocument, {
|
||||
canvas,
|
||||
disconnected() {
|
||||
const detachedView = view;
|
||||
host.destroy();
|
||||
void detachedView?.detach().catch(report);
|
||||
},
|
||||
});
|
||||
subscriptions.add(
|
||||
viewValue.onHostRequests((request) => {
|
||||
if (request.kind === "add-node-menu") {
|
||||
if (!menuPending) return;
|
||||
menuPending = false;
|
||||
menu?.open(request.viewPosition);
|
||||
return;
|
||||
}
|
||||
menuPending = false;
|
||||
menu?.close(false);
|
||||
try {
|
||||
void Promise.resolve(activate(request)).catch((error) => {
|
||||
if (active) report(error);
|
||||
});
|
||||
} catch (error) {
|
||||
if (active) report(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
const closeMenu = () => {
|
||||
menuPending = false;
|
||||
menu?.close(false);
|
||||
};
|
||||
const invalidatePicker = () => {
|
||||
pickerGeneration++;
|
||||
authorization = undefined;
|
||||
closeMenu();
|
||||
};
|
||||
subscriptions.add(rootValue.onCompositionChanges(invalidatePicker));
|
||||
subscriptions.add(rootValue.onMutations(invalidatePicker));
|
||||
syncViewport();
|
||||
} catch (error) {
|
||||
host.destroy();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
syncViewport,
|
||||
destroy() {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
lifecycleGeneration++;
|
||||
pendingViewport = undefined;
|
||||
unwatchDisconnect?.();
|
||||
unwatchDisconnect = undefined;
|
||||
for (const unsubscribe of subscriptions) unsubscribe();
|
||||
subscriptions.clear();
|
||||
pickerGeneration++;
|
||||
menuPending = false;
|
||||
observer?.disconnect();
|
||||
ownerWindow.removeEventListener("resize", resize);
|
||||
ownerDocument.removeEventListener("pointerdown", outsidePointer, true);
|
||||
for (const name of INPUT_EVENTS) canvas.removeEventListener(name, input);
|
||||
canvas.removeEventListener("contextmenu", contextMenu);
|
||||
canvas.removeEventListener("lostpointercapture", lostCapture);
|
||||
for (const pointerId of capturedPointers)
|
||||
try {
|
||||
if (canvas.hasPointerCapture(pointerId)) canvas.releasePointerCapture(pointerId);
|
||||
} catch {
|
||||
/* detached */
|
||||
}
|
||||
capturedPointers.clear();
|
||||
authorization = undefined;
|
||||
menu?.destroy();
|
||||
if (resourceFile) {
|
||||
resourceFile.removeEventListener("change", resourceChanged);
|
||||
resourceFile.remove();
|
||||
resourceFile = undefined;
|
||||
}
|
||||
if (changedTouchAction && canvas.style.touchAction === "none") canvas.style.touchAction = originalTouchAction;
|
||||
if (changedTabIndex && canvas.getAttribute("tabindex") === appliedTabIndex) {
|
||||
if (originalTabIndex === null) canvas.removeAttribute("tabindex");
|
||||
else canvas.setAttribute("tabindex", originalTabIndex);
|
||||
}
|
||||
if (activeHosts.get(canvas) === host) activeHosts.delete(canvas);
|
||||
view = undefined;
|
||||
},
|
||||
};
|
||||
activeHosts.set(canvas, host);
|
||||
return host;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
background: #101216;
|
||||
color: #f1f3f5;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
width: 1000px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 24px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 14px;
|
||||
color: #adb5bd;
|
||||
line-height: 1.45;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 1000px;
|
||||
height: 560px;
|
||||
background: #1d1f23;
|
||||
}
|
||||
button {
|
||||
margin: 0 0 14px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
#status {
|
||||
color: #ffd8a8;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import type { CompositionReceipt, FxNode, FxNodeView } from "@lib/index.js";
|
||||
import type { PreparedFxNodeBrowserHost } from "./browser-host.js";
|
||||
|
||||
declare global {
|
||||
interface StandaloneExampleHandle {
|
||||
root: FxNode | null;
|
||||
view: FxNodeView | null;
|
||||
host: PreparedFxNodeBrowserHost;
|
||||
ready: Promise<void>;
|
||||
graphVersion?: number;
|
||||
lastCompositionReceipt?: CompositionReceipt;
|
||||
cleanup(): void;
|
||||
}
|
||||
interface Window {
|
||||
fxnodeStandalone: StandaloneExampleHandle;
|
||||
fxnodeMultiView: MultiViewExampleHandle;
|
||||
}
|
||||
interface MultiViewExampleHandle {
|
||||
root: FxNode | null;
|
||||
views: FxNodeView[];
|
||||
ready: Promise<void>;
|
||||
cleanup(): Promise<void>;
|
||||
renderCounts: number[];
|
||||
}
|
||||
}
|
||||
export {};
|
||||
@@ -0,0 +1,352 @@
|
||||
import type { FxNodeDefinition } from "@lib/index.js";
|
||||
/** Shared canonical presentation/schema example; it does not evaluate pixels. */
|
||||
export const colorBalanceNode = [
|
||||
"fxnode.compositor.color-balance",
|
||||
{
|
||||
version: 1,
|
||||
title: "Color Balance",
|
||||
behavior: "standard",
|
||||
style: "compositorColor",
|
||||
parameters: {
|
||||
mode: {
|
||||
type: "string",
|
||||
default: {
|
||||
kind: "string",
|
||||
value: "Lift/Gamma/Gain",
|
||||
},
|
||||
enum: ["Lift/Gamma/Gain", "Offset/Power/Slope", "White Point"],
|
||||
},
|
||||
lift: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
liftColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
gamma: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
gammaColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
gain: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
gainColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
offset: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
offsetColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
power: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
powerColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
slope: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
slopeColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
inputTemperature: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 6500,
|
||||
},
|
||||
},
|
||||
inputTint: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
inputColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
outputTemperature: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 6500,
|
||||
},
|
||||
},
|
||||
outputTint: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
outputColor: {
|
||||
type: "color",
|
||||
default: {
|
||||
kind: "color",
|
||||
value: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
},
|
||||
sockets: {
|
||||
image: {
|
||||
title: "Image",
|
||||
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,
|
||||
},
|
||||
factor: {
|
||||
title: "Factor",
|
||||
direction: "input",
|
||||
type: "float",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: {
|
||||
type: "number",
|
||||
default: {
|
||||
kind: "number",
|
||||
value: 1,
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
showValue: true,
|
||||
},
|
||||
result: {
|
||||
title: "Image",
|
||||
direction: "output",
|
||||
type: "color",
|
||||
maxIncomingLinks: 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
},
|
||||
},
|
||||
ui: [
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "mode",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "widget",
|
||||
widget: "grading-wheels",
|
||||
bindings: [
|
||||
{
|
||||
title: "Offset",
|
||||
scalar: "offset",
|
||||
color: "offsetColor",
|
||||
},
|
||||
{
|
||||
title: "Power",
|
||||
scalar: "power",
|
||||
color: "powerColor",
|
||||
},
|
||||
{
|
||||
title: "Slope",
|
||||
scalar: "slope",
|
||||
color: "slopeColor",
|
||||
},
|
||||
],
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "Offset/Power/Slope",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
variant: "section",
|
||||
title: "Input",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "inputTemperature",
|
||||
title: "Temperature",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "inputTint",
|
||||
title: "Tint",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "inputColor",
|
||||
title: "Color",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
variant: "placeholder",
|
||||
title: "Eyedropper (host bridge)",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
variant: "section",
|
||||
title: "Output",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "outputTemperature",
|
||||
title: "Temperature",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "outputTint",
|
||||
title: "Tint",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "parameter",
|
||||
parameter: "outputColor",
|
||||
title: "Color",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "text",
|
||||
variant: "placeholder",
|
||||
title: "Eyedropper (host bridge)",
|
||||
visibleWhen: {
|
||||
parameter: "mode",
|
||||
equals: "White Point",
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "image",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "factor",
|
||||
},
|
||||
{
|
||||
kind: "socket",
|
||||
socket: "result",
|
||||
},
|
||||
],
|
||||
muteBypass: [["image", "result"]],
|
||||
migrations: [],
|
||||
},
|
||||
] as const satisfies readonly [string, FxNodeDefinition];
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import type { FxNodeTheme } from "@lib/index.js";
|
||||
export const exampleTheme = {
|
||||
background: "#1d1f23",
|
||||
grid: "#33363c",
|
||||
frame: "#30343a80",
|
||||
frameHeader: "#59616c",
|
||||
body: "#35383e",
|
||||
control: "#24272b",
|
||||
controlFill: "#4775b8",
|
||||
controlEditing: "#181a1d",
|
||||
textSelection: "#4775b8",
|
||||
outline: "#111216",
|
||||
text: "#e5e5e5",
|
||||
muted: "#a5a8ad",
|
||||
shadow: "#00000088",
|
||||
nodeSelected: "#ed5700",
|
||||
nodeActive: "#ffffff",
|
||||
unknownHeader: "#555b64",
|
||||
unknownSocket: "#999999",
|
||||
linkMuted: "#d94b4b",
|
||||
knifeMuted: "#e85b5b",
|
||||
emphasis: "#ffffff",
|
||||
focus: "#f5a623",
|
||||
editOutline: "#666a70",
|
||||
resize: "#8b8e95",
|
||||
muteOverlay: "#14141459",
|
||||
boxSelectionFill: "#f5a6231f",
|
||||
checkerLight: "#aaaaaa",
|
||||
checkerDark: "#777777",
|
||||
widgetBorder: "#111216",
|
||||
rampBorder: "#111111",
|
||||
resourceBackground: "#202228",
|
||||
} as const satisfies FxNodeTheme;
|
||||
Reference in New Issue
Block a user