nodality 1.0.221 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +227 -0
- package/bin/nodality.js +64 -0
- package/dist/animator.cjs.js +1 -1
- package/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.esm.js +1 -1
- package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/bundle.umd.js.LICENSE.txt +131 -1
- package/dist/button.cjs.js +1 -1
- package/dist/button.cjs.js.LICENSE.txt +1 -1
- package/dist/button.esm.js +1 -1
- package/dist/button.esm.js.LICENSE.txt +1 -1
- package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/card-getter.esm.js.LICENSE.txt +1 -1
- package/dist/center.cjs.js +1 -1
- package/dist/center.cjs.js.LICENSE.txt +1 -1
- package/dist/center.esm.js +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js +1 -1
- package/dist/container.cjs.js.LICENSE.txt +1 -1
- package/dist/container.esm.js +1 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js +1 -1
- package/dist/data-list.cjs.js.LICENSE.txt +1 -1
- package/dist/data-list.esm.js +1 -1
- package/dist/data-list.esm.js.LICENSE.txt +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.cjs.js.LICENSE.txt +97 -1
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +97 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +17 -1
- package/dist/element-mapper.esm.js +1 -1
- package/dist/element-mapper.esm.js.LICENSE.txt +17 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/finalresult.esm.js.LICENSE.txt +158 -1
- package/dist/flex-card.cjs.js +1 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js +1 -1
- package/dist/free.cjs.js.LICENSE.txt +1 -1
- package/dist/free.esm.js +1 -1
- package/dist/free.esm.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
- package/dist/image-picker.cjs.js +1 -1
- package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js +1 -1
- package/dist/image.cjs.js.LICENSE.txt +1 -1
- package/dist/image.esm.js +1 -1
- package/dist/image.esm.js.LICENSE.txt +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.LICENSE.txt +158 -1
- package/dist/index.d.ts +962 -0
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +158 -1
- package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
- package/dist/link-getter.cjs.js +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +1 -1
- package/dist/progress.esm.js +1 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js +1 -1
- package/dist/radio.cjs.js.LICENSE.txt +1 -1
- package/dist/radio.esm.js +1 -1
- package/dist/radio.esm.js.LICENSE.txt +1 -1
- package/dist/range.cjs.js +1 -1
- package/dist/range.cjs.js.LICENSE.txt +1 -1
- package/dist/range.esm.js +1 -1
- package/dist/range.esm.js.LICENSE.txt +1 -1
- package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
- package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
- package/dist/side-bar.cjs.js +1 -1
- package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +1 -1
- package/dist/stack.esm.js +1 -1
- package/dist/stack.esm.js.LICENSE.txt +1 -1
- package/dist/stacker.cjs.js.LICENSE.txt +1 -1
- package/dist/stacker.esm.js.LICENSE.txt +1 -1
- package/dist/table.cjs.js +1 -1
- package/dist/table.cjs.js.LICENSE.txt +1 -1
- package/dist/table.esm.js +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js +1 -1
- package/dist/text.cjs.js.LICENSE.txt +1 -1
- package/dist/text.esm.js +1 -1
- package/dist/text.esm.js.LICENSE.txt +1 -1
- package/dist/theme.cjs.js.LICENSE.txt +1 -1
- package/dist/theme.esm.js.LICENSE.txt +1 -1
- package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
- package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
- package/dist/ulist.cjs.js +1 -1
- package/dist/ulist.cjs.js.LICENSE.txt +1 -1
- package/dist/ulist.esm.js +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/examples/custom-raster-op.js +174 -0
- package/layout/animator.js +30 -2
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +3 -2
- package/layout/button.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +2 -2
- package/layout/circle.js +3 -2
- package/layout/code.js +3 -2
- package/layout/container.js +3 -4
- package/layout/dropdown-2025.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +3 -2
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +2 -2
- package/layout/form-components/floating-input.js +2 -2
- package/layout/form-components/form-all.js +2 -2
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +2 -2
- package/layout/form-components/radio.js +2 -2
- package/layout/form-components/radiogroup.js +2 -2
- package/layout/form-components/range.js +2 -2
- package/layout/free.js +3 -4
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +1 -1
- package/layout/link.js +5 -5
- package/layout/list.js +3 -4
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/morph.js +1003 -0
- package/layout/multiswitcher.js +11 -3
- package/layout/nav-bar.js +2 -3
- package/layout/nav-factor/custom-div.js +2 -2
- package/layout/new-nav-bar.js +11 -4
- package/layout/polygon.js +3 -4
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/svg.js +3 -4
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +1 -1
- package/layout/text.js +5 -5
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/codegen.js +62 -0
- package/lib/data.js +37 -1
- package/lib/designer.js +18 -1
- package/lib/element-mapper.js +351 -247
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +424 -0
- package/lib/raster-inspect.js +409 -0
- package/lib/raster-ops.js +1980 -81
- package/lib/raster-presets.js +329 -0
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +28 -1
- package/lib/stacker.js +1 -1
- package/lib/suggest.js +66 -0
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/lib/transition.js +205 -0
- package/package.json +20 -7
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.1.0
|
|
3
|
+
* (c) 2026 Filip Vabrousek
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/*!
|
|
8
|
+
* raster-inspect.js — a dev overlay for running raster pipelines.
|
|
9
|
+
*
|
|
10
|
+
* Open a page that uses raster ops, call inspectRaster(), and every
|
|
11
|
+
* attached pipeline appears as a Houdini-style stack: ops in chain order,
|
|
12
|
+
* the stage each contributes to, its params as live controls, and the
|
|
13
|
+
* field wiring drawn as matching colour chips. Drag a value and the pixels
|
|
14
|
+
* change on the next frame.
|
|
15
|
+
*
|
|
16
|
+
* import { inspectRaster } from "nodality/inspect";
|
|
17
|
+
* inspectRaster();
|
|
18
|
+
*
|
|
19
|
+
* NOT a node editor. It reads the chain the page already built and lets
|
|
20
|
+
* you tune it; it does not let you add ops or rewire fields. That is
|
|
21
|
+
* roughly a fifth of an editor's work for most of its value, and it
|
|
22
|
+
* doubles as documentation — the panel IS the op reference, generated from
|
|
23
|
+
* the registry rather than written twice.
|
|
24
|
+
*
|
|
25
|
+
* Two properties it must hold, both e2e-tested:
|
|
26
|
+
*
|
|
27
|
+
* - Tuning a uniform param NEVER mutates the host subtree. The whole
|
|
28
|
+
* library claim is that these effects sit beside your DOM rather than
|
|
29
|
+
* rewriting it, and a dev tool that quietly re-rendered the page while
|
|
30
|
+
* you dragged would make that claim untestable.
|
|
31
|
+
* - Copy-as-code emits the nodes array, not a screenshot of it. Raster
|
|
32
|
+
* nodes are plain data, so what you copy is what you paste.
|
|
33
|
+
*
|
|
34
|
+
* This file styles itself inline. That is the one sanctioned exception to
|
|
35
|
+
* the no-raw-DOM-styling rule: the panel is a dev tool, not library
|
|
36
|
+
* output, and it must look the same on a page whose CSS it has never seen.
|
|
37
|
+
* Nothing here ships in a user's render tree.
|
|
38
|
+
*
|
|
39
|
+
* DOM-touching by nature, so unlike the rest of lib/ it does nothing at
|
|
40
|
+
* import time — inspectRaster() is the only entry point.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
activeRasterPipelines, isStructuralChange, REGISTRY, RASTER_PARAM_EVENT,
|
|
45
|
+
FRAMEWORK_DOC,
|
|
46
|
+
} from "./raster-ops.js";
|
|
47
|
+
|
|
48
|
+
const STAGE_OF = (op) => {
|
|
49
|
+
const s = (REGISTRY[op] || {}).stage;
|
|
50
|
+
return Array.isArray(s) ? s.join("+") : (s || "?");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Field chips: a producer and its consumers share a colour, so the wiring
|
|
54
|
+
// is readable without drawing wires.
|
|
55
|
+
const CHIP_COLORS = ["#7dd3fc", "#4ade80", "#fbbf24", "#f472b6", "#c4b5fd", "#fb923c"];
|
|
56
|
+
|
|
57
|
+
const css = (el, styles) => { for (const k in styles) el.style[k] = styles[k]; return el; };
|
|
58
|
+
const mk = (tag, styles, text) => {
|
|
59
|
+
const el = document.createElement(tag);
|
|
60
|
+
if (styles) css(el, styles);
|
|
61
|
+
if (text != null) el.textContent = text;
|
|
62
|
+
return el;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const PANEL = {
|
|
66
|
+
position: "fixed", top: "12px", right: "12px", width: "340px",
|
|
67
|
+
maxHeight: "calc(100vh - 24px)", overflowY: "auto", zIndex: "2147483600",
|
|
68
|
+
background: "rgba(11, 20, 32, 0.96)", color: "#e8eef5",
|
|
69
|
+
font: "12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
70
|
+
border: "1px solid #1e2f44", borderRadius: "12px",
|
|
71
|
+
boxShadow: "0 18px 50px rgba(0,0,0,0.5)", padding: "0 0 10px",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The params of an op, as `{ key, meta, value, defaulted }`.
|
|
76
|
+
*
|
|
77
|
+
* This used to guess, by calling `def.uniforms(node, 1)` and taking the
|
|
78
|
+
* keys. That is the UNIFORM list, which is not the param list, and the
|
|
79
|
+
* gap is not academic: `duotone` takes `colors: [dark, light]` and uploads
|
|
80
|
+
* it as two uniforms `a` and `b`, so the panel offered an "a" and a "b"
|
|
81
|
+
* box, and typing in either wrote a key duotone never reads. It also had
|
|
82
|
+
* no way to know a param the caller had not set — every such field
|
|
83
|
+
* rendered empty, and an unset boolean (`mask.invert`) came out as a text
|
|
84
|
+
* box, because the type was inferred from a value that wasn't there.
|
|
85
|
+
*
|
|
86
|
+
* With `doc.params` (phase H4) the op states its own contract: the real
|
|
87
|
+
* names, their defaults, and a unit that picks the control. Ops without a
|
|
88
|
+
* doc — anyone's op, registered at runtime — keep the old introspection,
|
|
89
|
+
* so this degrades rather than breaks.
|
|
90
|
+
*/
|
|
91
|
+
function paramsOf(node) {
|
|
92
|
+
const def = REGISTRY[node.op] || {};
|
|
93
|
+
const declared = def.doc && def.doc.params;
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
const out = [];
|
|
96
|
+
const push = (key, meta) => {
|
|
97
|
+
if (seen.has(key) || key === "op") return;
|
|
98
|
+
seen.add(key);
|
|
99
|
+
const value = node[key];
|
|
100
|
+
out.push({
|
|
101
|
+
key, meta: meta || null, value,
|
|
102
|
+
// Nothing was written for this key, so what the panel shows is
|
|
103
|
+
// the op's own fallback rather than the caller's choice.
|
|
104
|
+
defaulted: value === undefined,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (declared) {
|
|
109
|
+
for (const [k, meta] of Object.entries(declared)) push(k, meta);
|
|
110
|
+
} else {
|
|
111
|
+
try { for (const k in (def.uniforms ? def.uniforms(node, 1) : {})) push(k, null); }
|
|
112
|
+
catch (e) { /* an op whose uniforms() needs more than a bare node */ }
|
|
113
|
+
for (const k of def.structural || []) push(k, null);
|
|
114
|
+
for (const k of def.structuralOnToggle || []) push(k, null);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Shared params live on every node whatever the op, so they are
|
|
118
|
+
// defined once in raster-ops and merged in here rather than repeated
|
|
119
|
+
// in fifteen docs. Only shown when the node actually carries one —
|
|
120
|
+
// otherwise every op grows five rows nobody set.
|
|
121
|
+
for (const [k, meta] of Object.entries(FRAMEWORK_DOC)) {
|
|
122
|
+
if (node[k] !== undefined) push(k, meta);
|
|
123
|
+
}
|
|
124
|
+
// And anything else the caller set: an undocumented extension, or a
|
|
125
|
+
// typo. Showing it is how you find out which.
|
|
126
|
+
for (const k of Object.keys(node)) push(k, null);
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isNum = (v) => typeof v === "number";
|
|
131
|
+
const isBool = (v) => typeof v === "boolean";
|
|
132
|
+
const isColor = (v) => typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v);
|
|
133
|
+
|
|
134
|
+
// What control a param gets, and how it reads back. Driven by the
|
|
135
|
+
// declared unit when there is one, and by the value's own type otherwise
|
|
136
|
+
// — which is all the old code had, and why an unset boolean became a
|
|
137
|
+
// text box.
|
|
138
|
+
const KIND_BY_UNIT = {
|
|
139
|
+
bool: "bool", color: "color",
|
|
140
|
+
px: "num", ratio: "num", deg: "num", count: "num", seconds: "num",
|
|
141
|
+
name: "text", point: "text", range: "text",
|
|
142
|
+
};
|
|
143
|
+
const STEP_BY_UNIT = { ratio: "0.01", seconds: "0.01", count: "1", px: "1", deg: "1" };
|
|
144
|
+
|
|
145
|
+
function kindOf(meta, value) {
|
|
146
|
+
if (meta && meta.unit && KIND_BY_UNIT[meta.unit]) return KIND_BY_UNIT[meta.unit];
|
|
147
|
+
const v = value !== undefined ? value : (meta ? meta.default : undefined);
|
|
148
|
+
if (isBool(v)) return "bool";
|
|
149
|
+
if (isColor(v)) return "color";
|
|
150
|
+
if (isNum(v)) return "num";
|
|
151
|
+
return "text";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Open the inspector.
|
|
156
|
+
*
|
|
157
|
+
* @param {object} [opts]
|
|
158
|
+
* @param {HTMLElement} [opts.mount] where to attach the panel (default body)
|
|
159
|
+
* @returns {{ refresh(): void, close(): void }}
|
|
160
|
+
*/
|
|
161
|
+
function inspectRaster(opts = {}) {
|
|
162
|
+
if (typeof document === "undefined") {
|
|
163
|
+
throw new Error("[nodality] inspectRaster() needs a DOM");
|
|
164
|
+
}
|
|
165
|
+
const mount = opts.mount || document.body;
|
|
166
|
+
const panel = css(document.createElement("div"), PANEL);
|
|
167
|
+
panel.setAttribute("data-nodality-inspector", "");
|
|
168
|
+
|
|
169
|
+
const header = css(mk("div"), {
|
|
170
|
+
display: "flex", alignItems: "center", gap: "8px", padding: "10px 12px",
|
|
171
|
+
borderBottom: "1px solid #1e2f44", position: "sticky", top: "0",
|
|
172
|
+
background: "rgba(11, 20, 32, 0.98)",
|
|
173
|
+
});
|
|
174
|
+
const title = mk("strong", { color: "#7dd3fc", letterSpacing: "0.04em" }, "RASTER");
|
|
175
|
+
const count = mk("span", { color: "#6b7d91" }, "");
|
|
176
|
+
const closeBtn = css(mk("button", null, "✕"), {
|
|
177
|
+
marginLeft: "auto", background: "transparent", color: "#93a3b5",
|
|
178
|
+
border: "1px solid #1e2f44", borderRadius: "6px", cursor: "pointer",
|
|
179
|
+
font: "inherit", padding: "2px 7px",
|
|
180
|
+
});
|
|
181
|
+
header.append(title, count, closeBtn);
|
|
182
|
+
panel.appendChild(header);
|
|
183
|
+
|
|
184
|
+
const body = mk("div", { padding: "4px 0" });
|
|
185
|
+
panel.appendChild(body);
|
|
186
|
+
|
|
187
|
+
// Every control this render built, so a value written from ANYWHERE else
|
|
188
|
+
// — the page's own UI, the console, another panel — can pull the matching
|
|
189
|
+
// field straight. Without this the panel shows whatever was true when it
|
|
190
|
+
// opened, which is the one thing a live inspector must never do.
|
|
191
|
+
let controls = [];
|
|
192
|
+
|
|
193
|
+
const render = () => {
|
|
194
|
+
body.textContent = "";
|
|
195
|
+
controls = [];
|
|
196
|
+
const pipelines = activeRasterPipelines();
|
|
197
|
+
count.textContent = `${pipelines.length} pipeline${pipelines.length === 1 ? "" : "s"}`;
|
|
198
|
+
|
|
199
|
+
if (!pipelines.length) {
|
|
200
|
+
body.appendChild(css(mk("div", null,
|
|
201
|
+
"No raster pipelines attached. They register on attach, so open this after the page renders."),
|
|
202
|
+
{ padding: "14px 12px", color: "#6b7d91" }));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pipelines.forEach((pipe, pi) => {
|
|
207
|
+
const group = mk("div", { borderBottom: "1px solid #16243550", padding: "8px 12px" });
|
|
208
|
+
|
|
209
|
+
// The backend badge is the first thing worth knowing: half the
|
|
210
|
+
// ops behave differently, and `echo` does nothing at all, on
|
|
211
|
+
// snapshot capture.
|
|
212
|
+
const live = pipe.backend === "live";
|
|
213
|
+
const bar = mk("div", { display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" });
|
|
214
|
+
bar.append(
|
|
215
|
+
mk("span", { color: "#93a3b5" }, `#${pi}`),
|
|
216
|
+
css(mk("span", null, live ? "live" : "snapshot"), {
|
|
217
|
+
fontSize: "10px", fontWeight: "700", letterSpacing: "0.06em",
|
|
218
|
+
textTransform: "uppercase", padding: "2px 6px", borderRadius: "999px",
|
|
219
|
+
color: live ? "#4ade80" : "#93a3b5",
|
|
220
|
+
background: live ? "rgba(74,222,128,0.12)" : "rgba(147,163,181,0.1)",
|
|
221
|
+
}),
|
|
222
|
+
css(mk("span", null, `${pipe.nodes.length} op${pipe.nodes.length === 1 ? "" : "s"}`),
|
|
223
|
+
{ color: "#6b7d91", marginLeft: "auto" }),
|
|
224
|
+
);
|
|
225
|
+
group.appendChild(bar);
|
|
226
|
+
|
|
227
|
+
// Assign a colour per field name so producer and consumers match.
|
|
228
|
+
const fields = new Map();
|
|
229
|
+
const chipFor = (name) => {
|
|
230
|
+
if (!fields.has(name)) fields.set(name, CHIP_COLORS[fields.size % CHIP_COLORS.length]);
|
|
231
|
+
return fields.get(name);
|
|
232
|
+
};
|
|
233
|
+
for (const n of pipe.nodes) {
|
|
234
|
+
if (n.op === "mask" || n.op === "noise") chipFor(n.as || "mask");
|
|
235
|
+
if (n.masked) chipFor(n.masked === true ? "mask" : n.masked);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
pipe.nodes.forEach((node, ni) => {
|
|
239
|
+
const row = mk("div", {
|
|
240
|
+
borderLeft: "2px solid #1e2f44", paddingLeft: "8px", margin: "6px 0 8px",
|
|
241
|
+
});
|
|
242
|
+
const head = mk("div", { display: "flex", alignItems: "baseline", gap: "6px" });
|
|
243
|
+
const opName = mk("b", { color: "#c4b5fd" }, node.op);
|
|
244
|
+
const summary = (REGISTRY[node.op] || {}).doc?.summary;
|
|
245
|
+
if (summary) opName.title = summary;
|
|
246
|
+
head.append(
|
|
247
|
+
opName,
|
|
248
|
+
mk("span", { color: "#6b7d91", fontSize: "11px" }, STAGE_OF(node.op)),
|
|
249
|
+
);
|
|
250
|
+
// The wiring, as chips.
|
|
251
|
+
const produces = (node.op === "mask" || node.op === "noise") ? (node.as || "mask") : null;
|
|
252
|
+
const consumes = node.masked ? (node.masked === true ? "mask" : node.masked) : null;
|
|
253
|
+
for (const [label, name] of [["→", produces], ["←", consumes]]) {
|
|
254
|
+
if (!name) continue;
|
|
255
|
+
head.appendChild(css(mk("span", null, `${label} ${name}`), {
|
|
256
|
+
fontSize: "10px", padding: "1px 5px", borderRadius: "999px",
|
|
257
|
+
color: chipFor(name), background: chipFor(name) + "1f",
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
row.appendChild(head);
|
|
261
|
+
|
|
262
|
+
for (const { key, meta, value, defaulted } of paramsOf(node)) {
|
|
263
|
+
// The value the op is ACTUALLY using: what the caller set,
|
|
264
|
+
// or the documented fallback. Showing the fallback is the
|
|
265
|
+
// point — an empty box told you a param existed and
|
|
266
|
+
// nothing else.
|
|
267
|
+
const shown = defaulted && meta ? meta.default : value;
|
|
268
|
+
// Objects and arrays (merge branches, copy points, a
|
|
269
|
+
// mask's `at`) are structure, not a knob. Show the value
|
|
270
|
+
// read-only rather than faking a control OR hiding the
|
|
271
|
+
// param — the panel doubles as the op reference, and a
|
|
272
|
+
// param you cannot see is one you will never use.
|
|
273
|
+
const isStructure =
|
|
274
|
+
shown !== undefined && shown !== null && typeof shown === "object";
|
|
275
|
+
|
|
276
|
+
const kind = isStructure ? "text" : kindOf(meta, value);
|
|
277
|
+
const structural = (meta && meta.structural) ||
|
|
278
|
+
isStructuralChange(node.op, key, value, value === 0 ? 1 : 0);
|
|
279
|
+
const line = mk("label", {
|
|
280
|
+
display: "flex", alignItems: "center", gap: "6px",
|
|
281
|
+
margin: "3px 0", fontSize: "11px",
|
|
282
|
+
});
|
|
283
|
+
const label = css(mk("span", null, key), {
|
|
284
|
+
color: "#93a3b5", minWidth: "92px",
|
|
285
|
+
});
|
|
286
|
+
// The doc, where a doc is the natural place for it: the
|
|
287
|
+
// panel IS the op reference, so the summary rides along
|
|
288
|
+
// rather than living in a manual nobody has open.
|
|
289
|
+
if (meta && meta.summary) label.title = meta.summary;
|
|
290
|
+
line.appendChild(label);
|
|
291
|
+
|
|
292
|
+
let input;
|
|
293
|
+
if (kind === "bool") {
|
|
294
|
+
input = mk("input"); input.type = "checkbox"; input.checked = !!shown;
|
|
295
|
+
} else if (kind === "color") {
|
|
296
|
+
input = mk("input"); input.type = "color";
|
|
297
|
+
input.value = isColor(shown) ? shown : "#000000";
|
|
298
|
+
} else if (kind === "num") {
|
|
299
|
+
input = mk("input"); input.type = "number";
|
|
300
|
+
input.step = (meta && STEP_BY_UNIT[meta.unit]) ||
|
|
301
|
+
(Math.abs(Number(shown)) < 2 ? "0.01" : "1");
|
|
302
|
+
input.value = shown == null ? "" : String(shown);
|
|
303
|
+
} else {
|
|
304
|
+
input = mk("input"); input.type = "text";
|
|
305
|
+
input.value = isStructure ? JSON.stringify(shown)
|
|
306
|
+
: (shown == null ? "" : String(shown));
|
|
307
|
+
if (isStructure) {
|
|
308
|
+
input.readOnly = true;
|
|
309
|
+
input.title = "structure — edit this in the nodes array";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
css(input, {
|
|
313
|
+
flex: "1", minWidth: "0", background: "#0b1420", color: "#e8eef5",
|
|
314
|
+
border: "1px solid #1e2f44", borderRadius: "6px", padding: "2px 6px",
|
|
315
|
+
font: "inherit", fontSize: "11px",
|
|
316
|
+
// Dimmed while it is showing the op's fallback, so an
|
|
317
|
+
// inherited value is distinguishable from one this page
|
|
318
|
+
// chose. It stops being dim the moment you edit it.
|
|
319
|
+
opacity: defaulted ? "0.55" : "1",
|
|
320
|
+
});
|
|
321
|
+
input.oninput = () => {
|
|
322
|
+
if (isStructure) return; // read-only; nothing to write
|
|
323
|
+
const next = kind === "bool" ? input.checked
|
|
324
|
+
: kind === "num" ? Number(input.value)
|
|
325
|
+
: input.value;
|
|
326
|
+
input.style.opacity = "1";
|
|
327
|
+
const how = pipe.setParam(ni, key, next);
|
|
328
|
+
// A rebuild replaces the pipeline object, so the panel
|
|
329
|
+
// has to re-read the world rather than keep pointing at
|
|
330
|
+
// a handle that is now destroyed.
|
|
331
|
+
if (how === "rebuild") setTimeout(render, 0);
|
|
332
|
+
};
|
|
333
|
+
line.appendChild(input);
|
|
334
|
+
controls.push({ pipe, ni, key, input });
|
|
335
|
+
|
|
336
|
+
if (structural) {
|
|
337
|
+
line.appendChild(css(mk("span", null, "↻"), {
|
|
338
|
+
color: "#fbbf24", fontSize: "10px",
|
|
339
|
+
title: "changing this recompiles the shader",
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
row.appendChild(line);
|
|
343
|
+
}
|
|
344
|
+
group.appendChild(row);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Copy the chain as the array it is. No toCode() — raster nodes
|
|
348
|
+
// are data, so this round-trips by construction.
|
|
349
|
+
const copy = css(mk("button", null, "copy chain"), {
|
|
350
|
+
background: "transparent", color: "#7dd3fc", border: "1px solid #1e2f44",
|
|
351
|
+
borderRadius: "6px", cursor: "pointer", font: "inherit",
|
|
352
|
+
fontSize: "11px", padding: "3px 8px",
|
|
353
|
+
});
|
|
354
|
+
copy.onclick = async () => {
|
|
355
|
+
const src = JSON.stringify(pipe.nodes, null, 2);
|
|
356
|
+
try { await navigator.clipboard.writeText(src); copy.textContent = "copied"; }
|
|
357
|
+
catch (e) { copy.textContent = "clipboard blocked"; }
|
|
358
|
+
setTimeout(() => (copy.textContent = "copy chain"), 1200);
|
|
359
|
+
};
|
|
360
|
+
group.appendChild(copy);
|
|
361
|
+
body.appendChild(group);
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const onKey = (e) => { if (e.key === "Escape") api.close(); };
|
|
366
|
+
|
|
367
|
+
// setParam announces every write, whoever made it. Update the one field
|
|
368
|
+
// it names rather than re-rendering: a full re-render mid-drag would drop
|
|
369
|
+
// focus and the caret. The field being typed into is skipped — it already
|
|
370
|
+
// holds the value that caused this event, and writing to it would fight
|
|
371
|
+
// the user (and rewrite "0.2" to "0.2" as they reach for the 5).
|
|
372
|
+
const onParam = (e) => {
|
|
373
|
+
const { pipeline, index, key, value } = e.detail;
|
|
374
|
+
for (const f of controls) {
|
|
375
|
+
if (f.pipe !== pipeline || f.ni !== index || f.key !== key) continue;
|
|
376
|
+
if (f.input === document.activeElement) continue;
|
|
377
|
+
if (f.input.type === "checkbox") f.input.checked = !!value;
|
|
378
|
+
else f.input.value = value == null ? "" : String(value);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const api = {
|
|
383
|
+
refresh: render,
|
|
384
|
+
close() {
|
|
385
|
+
document.removeEventListener("keydown", onKey);
|
|
386
|
+
document.removeEventListener(RASTER_PARAM_EVENT, onParam);
|
|
387
|
+
panel.remove();
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
closeBtn.onclick = api.close;
|
|
391
|
+
document.addEventListener("keydown", onKey);
|
|
392
|
+
document.addEventListener(RASTER_PARAM_EVENT, onParam);
|
|
393
|
+
|
|
394
|
+
render();
|
|
395
|
+
mount.appendChild(panel);
|
|
396
|
+
return api;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Open the inspector only when the page asks for it via the URL:
|
|
401
|
+
* `?nodality-inspect=1`. Opt-in per page — nothing auto-injects a dev tool.
|
|
402
|
+
*/
|
|
403
|
+
function auto(opts) {
|
|
404
|
+
if (typeof location === "undefined") return null;
|
|
405
|
+
const on = new URLSearchParams(location.search).get("nodality-inspect");
|
|
406
|
+
return on && on !== "0" ? inspectRaster(opts) : null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export { inspectRaster, auto };
|