@elicdavis/node-flow 0.1.20 → 0.2.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/dist/index.d.mts +386 -227
- package/dist/index.d.ts +386 -227
- package/dist/index.js +1866 -1059
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1849 -1058
- package/dist/index.mjs.map +1 -1
- package/index.html +1134 -329
- package/package.json +1 -1
- package/src/connection.ts +30 -5
- package/src/contextMenu.ts +66 -34
- package/src/graph.ts +12 -10
- package/src/index.ts +26 -12
- package/src/markdown/entry.ts +11 -15
- package/src/markdown/syntaxParser.ts +2 -1
- package/src/node.ts +160 -101
- package/src/nodes/subsystem.ts +113 -29
- package/src/notes/note.ts +9 -1
- package/src/organize.ts +540 -111
- package/src/popup.ts +4 -2
- package/src/port.ts +83 -6
- package/src/styles/bevel.ts +31 -0
- package/src/styles/cursor.ts +1 -0
- package/src/styles/text.ts +4 -4
- package/src/theme.ts +179 -92
- package/src/types/text.ts +5 -1
- package/src/utils/color.ts +1 -1
- package/src/widgets/button.ts +52 -107
- package/src/widgets/color.ts +91 -150
- package/src/widgets/factory.ts +89 -46
- package/src/widgets/field.ts +99 -0
- package/src/widgets/image.ts +107 -165
- package/src/widgets/inplace.ts +84 -0
- package/src/widgets/number.ts +122 -154
- package/src/widgets/row.ts +138 -0
- package/src/widgets/slider.ts +101 -216
- package/src/widgets/string.ts +39 -130
- package/src/widgets/text.ts +81 -92
- package/src/widgets/toggle.ts +62 -207
- package/src/widgets/value.ts +127 -0
- package/src/widgets/widget.ts +180 -34
- package/test/context_menu.test.ts +82 -0
- package/test/cursor.test.ts +17 -0
- package/test/hidden_port.test.ts +125 -0
- package/test/markdown.test.ts +2 -2
- package/test/markdown_code_block.test.ts +58 -0
- package/test/multi_input.test.ts +124 -0
- package/test/organize.test.ts +117 -1
- package/test/support.ts +175 -0
- package/test/theme.test.ts +86 -0
- package/test/widget_click.test.ts +111 -97
- package/test/widget_construction.test.ts +5 -4
- package/test/widgets.test.ts +333 -0
- package/test/wire_follows_camera.test.ts +57 -0
package/index.html
CHANGED
|
@@ -1,329 +1,1134 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
<link rel="
|
|
9
|
-
<link href="https://fonts.googleapis.com
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Node Flow Demo</title>
|
|
8
|
+
<link rel="shortcut icon" href="#" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
|
|
12
|
+
rel="stylesheet">
|
|
13
|
+
<style>
|
|
14
|
+
* {
|
|
15
|
+
margin: 0;
|
|
16
|
+
padding: 0;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:root {
|
|
21
|
+
--page: #07212a;
|
|
22
|
+
--font: "Source Code Pro", monospace;
|
|
23
|
+
--node-bg: #0c2b35;
|
|
24
|
+
--node-border: #1c1c1c;
|
|
25
|
+
--node-radius: 15px;
|
|
26
|
+
--node-bevel: none;
|
|
27
|
+
--title-bg: #154050;
|
|
28
|
+
--title-fg: #afb9bb;
|
|
29
|
+
--text: #afb9bb;
|
|
30
|
+
--dim: #7f9497;
|
|
31
|
+
--widget-bg: #0f313c;
|
|
32
|
+
--widget-fg: #afb9bb;
|
|
33
|
+
--widget-border: 2px solid #154050;
|
|
34
|
+
--widget-radius: 2px;
|
|
35
|
+
--widget-hover: #195366;
|
|
36
|
+
--widget-active: #1f637a;
|
|
37
|
+
--widget-bevel: none;
|
|
38
|
+
--widget-bevel-pressed: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
html,
|
|
42
|
+
body {
|
|
43
|
+
width: 100%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
background: var(--page);
|
|
47
|
+
font-family: var(--font);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#stage {
|
|
51
|
+
position: absolute;
|
|
52
|
+
inset: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
canvas {
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 100%;
|
|
58
|
+
display: block;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.node {
|
|
62
|
+
position: absolute;
|
|
63
|
+
left: 12px;
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
max-width: calc(100% - 24px);
|
|
67
|
+
background: var(--node-bg);
|
|
68
|
+
border: 1px solid var(--node-border);
|
|
69
|
+
border-radius: var(--node-radius);
|
|
70
|
+
box-shadow: var(--node-bevel);
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
color: var(--text);
|
|
73
|
+
font-size: 14px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.node-title {
|
|
77
|
+
padding: 5px 15px 8px;
|
|
78
|
+
background: var(--title-bg);
|
|
79
|
+
color: var(--title-fg);
|
|
80
|
+
text-align: center;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
font-size: 16px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
user-select: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.node-title small {
|
|
88
|
+
display: block;
|
|
89
|
+
font-size: 10px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.node-body {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 8px;
|
|
96
|
+
padding: 15px;
|
|
97
|
+
min-height: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.node.collapsed .node-body {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.widget {
|
|
105
|
+
height: 25px;
|
|
106
|
+
padding: 0 10px;
|
|
107
|
+
font: inherit;
|
|
108
|
+
color: var(--widget-fg);
|
|
109
|
+
background: var(--widget-bg);
|
|
110
|
+
border: var(--widget-border);
|
|
111
|
+
border-radius: var(--widget-radius);
|
|
112
|
+
box-shadow: var(--widget-bevel);
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
white-space: nowrap;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.widget:hover {
|
|
118
|
+
background: var(--widget-hover);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.widget:active {
|
|
122
|
+
background: var(--widget-active);
|
|
123
|
+
box-shadow: var(--widget-bevel-pressed);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#toolbar {
|
|
127
|
+
top: 12px;
|
|
128
|
+
left: auto;
|
|
129
|
+
right: 12px;
|
|
130
|
+
width: 420px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#toolbar .node-body {
|
|
134
|
+
display: grid;
|
|
135
|
+
grid-template-columns: repeat(3, 1fr);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#log {
|
|
139
|
+
bottom: 12px;
|
|
140
|
+
width: 360px;
|
|
141
|
+
max-height: 34vh;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#log-list {
|
|
145
|
+
list-style: none;
|
|
146
|
+
overflow-y: auto;
|
|
147
|
+
min-height: 0;
|
|
148
|
+
flex: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#log-list li {
|
|
152
|
+
line-height: 22px;
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
text-overflow: ellipsis;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#log-list li span {
|
|
159
|
+
color: var(--dim);
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
162
|
+
</head>
|
|
163
|
+
|
|
164
|
+
<body>
|
|
165
|
+
<div id="stage"><canvas id="canvas"></canvas></div>
|
|
166
|
+
|
|
167
|
+
<div id="toolbar" class="node">
|
|
168
|
+
<div class="node-title"><small>NodeFlowGraph</small>Controls</div>
|
|
169
|
+
<div class="node-body">
|
|
170
|
+
<button class="widget" data-act="center" title="graph.centerOnGraph()">Center</button>
|
|
171
|
+
<button class="widget" data-act="reset" title="graph.resetCamera()">Reset</button>
|
|
172
|
+
<button class="widget" data-act="minimap" title="graph.minimap().setEnabled(...)">Minimap</button>
|
|
173
|
+
<button class="widget" data-act="zoom-in" title="graph.zoom(1)">Zoom in</button>
|
|
174
|
+
<button class="widget" data-act="zoom-out" title="graph.zoom(-1)">Zoom out</button>
|
|
175
|
+
<button class="widget" data-act="organize" title="graph.organize()">Organize</button>
|
|
176
|
+
<button class="widget" data-act="select-all" title="node.select()">Select all</button>
|
|
177
|
+
<button class="widget" data-act="delete" title="graph.removeSelectedNodes()">Delete</button>
|
|
178
|
+
<button class="widget" data-act="continuous" title="graph.renderContinuously()">Animate</button>
|
|
179
|
+
<button class="widget" data-act="theme" title="setTheme(...)" style="grid-column: 1 / -1">Theme: Windows 95</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div id="log" class="node">
|
|
184
|
+
<div class="node-title"><small>Listeners</small>Events</div>
|
|
185
|
+
<div class="node-body">
|
|
186
|
+
<ol id="log-list"></ol>
|
|
187
|
+
<button class="widget" id="log-clear">Clear</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<script type="module">
|
|
192
|
+
import {
|
|
193
|
+
Theme, FlowNode, FlowNote, NodeFlowGraph, Publisher,
|
|
194
|
+
MessageType, ContextMenuItemState, DefaultConnectionRenderer,
|
|
195
|
+
WidgetBase, ValueWidget, drawFieldBox, widgetFont,
|
|
196
|
+
NumberWidget, StringWidget, ToggleWidget, ButtonWidget, TextWidget, ImageWidget, RowWidget,
|
|
197
|
+
setTheme, onThemeChange,
|
|
198
|
+
} from "./dist/web/NodeFlow.js";
|
|
199
|
+
|
|
200
|
+
const win95Font = "Tahoma, 'Microsoft Sans Serif', 'MS Sans Serif', sans-serif";
|
|
201
|
+
const win95Bevel = { Light: "#ffffff", Dark: "#808080" };
|
|
202
|
+
const themes = {
|
|
203
|
+
"Default": {
|
|
204
|
+
FontFamily: "Source Code Pro",
|
|
205
|
+
},
|
|
206
|
+
"Windows 95": {
|
|
207
|
+
FontFamily: win95Font,
|
|
208
|
+
Graph: { BackgroundColor: "#008080", DotColor: "#006a6a" },
|
|
209
|
+
Node: {
|
|
210
|
+
FontColor: "#000000",
|
|
211
|
+
BackgroundColor: "#c0c0c0",
|
|
212
|
+
Title: { Color: "#000080", FontColor: "#ffffff" },
|
|
213
|
+
BorderRadius: 0,
|
|
214
|
+
Border: { Idle: "#000000", MouseOver: "#000000", Grabbed: "#000000", Selected: "#ffffff" },
|
|
215
|
+
Bevel: win95Bevel,
|
|
216
|
+
Port: { FontColor: "#000000" },
|
|
217
|
+
Message: { InfoColor: "#ffffff", WarnColor: "#ffff00", ErrorColor: "#ff4040" },
|
|
218
|
+
},
|
|
219
|
+
BoxSelect: { Color: "#ffffff", Radius: 0 },
|
|
220
|
+
Widget: {
|
|
221
|
+
FontColor: "#000000",
|
|
222
|
+
LabelColor: "#000000",
|
|
223
|
+
FontSize: 13,
|
|
224
|
+
BackgroundColor: "#c0c0c0",
|
|
225
|
+
Border: { Color: "#000000", Size: 0, Radius: 0 },
|
|
226
|
+
Bevel: win95Bevel,
|
|
227
|
+
Hover: { BackgroundColor: "#c0c0c0" },
|
|
228
|
+
Input: { BackgroundColor: "#ffffff", HoverBackgroundColor: "#ffffff", FontColor: "#000000" },
|
|
229
|
+
Slider: { FillColor: "#a6caf0" },
|
|
230
|
+
Button: { Click: { BackgroundColor: "#c0c0c0" } },
|
|
231
|
+
},
|
|
232
|
+
ContextMenu: {
|
|
233
|
+
BackgroundColor: "#c0c0c0",
|
|
234
|
+
HighlightColor: "#000080",
|
|
235
|
+
HighlightFontColor: "#ffffff",
|
|
236
|
+
FontColor: "#000000",
|
|
237
|
+
DisabledFontColor: "#808080",
|
|
238
|
+
BorderRadius: 0,
|
|
239
|
+
},
|
|
240
|
+
Minimap: {
|
|
241
|
+
BackgroundColor: "#c0c0c0",
|
|
242
|
+
BorderColor: "#000000",
|
|
243
|
+
BorderRadius: 0,
|
|
244
|
+
NodeColor: "#000080",
|
|
245
|
+
ViewportColor: "rgba(0, 0, 0, 0.2)",
|
|
246
|
+
},
|
|
247
|
+
Popup: {
|
|
248
|
+
BackgroundColor: "#c0c0c0",
|
|
249
|
+
FontColor: "#000000",
|
|
250
|
+
FontFamily: win95Font,
|
|
251
|
+
BorderRadius: 0,
|
|
252
|
+
Backdrop: "rgba(0, 0, 0, 0.25)",
|
|
253
|
+
},
|
|
254
|
+
Note: {
|
|
255
|
+
FontColor: "#ffffff",
|
|
256
|
+
CodeBlock: { BackgroundColor: "#000000", FontColor: "#c0c0c0", BorderRadius: 0 },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
function bevelShadow(bevel, sunken) {
|
|
262
|
+
if (!bevel) {
|
|
263
|
+
return "none";
|
|
264
|
+
}
|
|
265
|
+
const [topLeft, bottomRight] = sunken ? [bevel.Dark, bevel.Light] : [bevel.Light, bevel.Dark];
|
|
266
|
+
return `inset 2px 2px 0 ${topLeft}, inset -2px -2px 0 ${bottomRight}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function syncPageToTheme() {
|
|
270
|
+
const css = document.documentElement.style;
|
|
271
|
+
const set = (name, value) => css.setProperty(name, value);
|
|
272
|
+
set("--page", Theme.Graph.BackgroundColor);
|
|
273
|
+
set("--font", Theme.FontFamily);
|
|
274
|
+
set("--node-bg", Theme.Node.BackgroundColor);
|
|
275
|
+
set("--node-border", Theme.Node.Border.Idle);
|
|
276
|
+
set("--node-radius", `${Theme.Node.BorderRadius}px`);
|
|
277
|
+
set("--node-bevel", bevelShadow(Theme.Node.Bevel, false));
|
|
278
|
+
set("--title-bg", Theme.Node.Title.Color);
|
|
279
|
+
set("--title-fg", Theme.Node.Title.FontColor);
|
|
280
|
+
set("--text", Theme.Node.Port.FontColor);
|
|
281
|
+
set("--dim", Theme.Widget.LabelColor);
|
|
282
|
+
set("--widget-bg", Theme.Widget.BackgroundColor);
|
|
283
|
+
set("--widget-fg", Theme.Widget.FontColor);
|
|
284
|
+
set("--widget-border", Theme.Widget.Border.Size > 0 ? `${Theme.Widget.Border.Size}px solid ${Theme.Widget.Border.Color}` : "0");
|
|
285
|
+
set("--widget-radius", `${Theme.Widget.Border.Radius}px`);
|
|
286
|
+
set("--widget-hover", Theme.Widget.Hover.BackgroundColor);
|
|
287
|
+
set("--widget-active", Theme.Widget.Button.Click.BackgroundColor);
|
|
288
|
+
set("--widget-bevel", bevelShadow(Theme.Widget.Bevel, false));
|
|
289
|
+
set("--widget-bevel-pressed", bevelShadow(Theme.Widget.Bevel, true));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
onThemeChange(syncPageToTheme);
|
|
293
|
+
setTheme(themes["Default"]);
|
|
294
|
+
|
|
295
|
+
const logList = document.getElementById("log-list");
|
|
296
|
+
let logging = false;
|
|
297
|
+
function log(what, detail = "") {
|
|
298
|
+
if (!logging) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const li = document.createElement("li");
|
|
302
|
+
const detailSpan = document.createElement("span");
|
|
303
|
+
detailSpan.textContent = detail;
|
|
304
|
+
li.append(what + " ", detailSpan);
|
|
305
|
+
logList.prepend(li);
|
|
306
|
+
while (logList.children.length > 80) {
|
|
307
|
+
logList.lastChild.remove();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
document.getElementById("log-clear").onclick = () => logList.replaceChildren();
|
|
311
|
+
document.querySelectorAll(".node-title").forEach((title) => {
|
|
312
|
+
title.onclick = () => title.parentElement.classList.toggle("collapsed");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const fmt = (v) => typeof v === "number" ? String(parseFloat(v.toPrecision(5))) : JSON.stringify(v);
|
|
316
|
+
|
|
317
|
+
class StarRatingWidget extends ValueWidget {
|
|
318
|
+
#mouse = undefined;
|
|
319
|
+
|
|
320
|
+
constructor(node, config) {
|
|
321
|
+
super(node, config, 0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
Size() {
|
|
325
|
+
return { x: 150, y: 25 };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
Draw(ctx, box, scale, mouse) {
|
|
329
|
+
this.#mouse = mouse;
|
|
330
|
+
super.Draw(ctx, box, scale, mouse);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#starAt(x) {
|
|
334
|
+
const box = this.box();
|
|
335
|
+
return Math.min(5, Math.max(1, Math.ceil(((x - box.Position.x) / box.Size.x) * 5)));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
render(ctx, box, scale) {
|
|
339
|
+
drawFieldBox(ctx, box, scale, { hovered: this.hovered(), value: "" });
|
|
340
|
+
const showing = this.hovered() && this.#mouse ? this.#starAt(this.#mouse.x) : this.value();
|
|
341
|
+
ctx.font = widgetFont(scale * 1.3);
|
|
342
|
+
ctx.textAlign = "center";
|
|
343
|
+
ctx.textBaseline = "middle";
|
|
344
|
+
for (let i = 0; i < 5; i++) {
|
|
345
|
+
ctx.fillStyle = i < showing ? "#ffc95c" : "#2b4a55";
|
|
346
|
+
ctx.fillText("★", box.Position.x + (box.Size.x * (i + 0.5) / 5), box.Position.y + box.Size.y / 2);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
onClick(pointer) {
|
|
351
|
+
this.commit(this.#starAt(pointer.position.x), this.value());
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
class MorePortsWidget extends WidgetBase {
|
|
356
|
+
#ports;
|
|
357
|
+
#open = false;
|
|
358
|
+
|
|
359
|
+
constructor(ports) {
|
|
360
|
+
super();
|
|
361
|
+
this.#ports = ports;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
Size() {
|
|
365
|
+
return { x: 150, y: 16 };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
render(ctx, box, scale) {
|
|
369
|
+
const waiting = this.#ports.filter((p) => p.connections().length === 0).length;
|
|
370
|
+
const label = this.#open ? "▴ less" : `▾ ${waiting} more`;
|
|
371
|
+
const y = box.Position.y + box.Size.y / 2;
|
|
372
|
+
const cx = box.Position.x + box.Size.x / 2;
|
|
373
|
+
ctx.font = widgetFont(scale * 0.8);
|
|
374
|
+
const half = ctx.measureText(label).width / 2 + 6 * scale;
|
|
375
|
+
ctx.globalAlpha = this.hovered() ? 1 : 0.55;
|
|
376
|
+
ctx.strokeStyle = ctx.fillStyle = Theme.Widget.FontColor;
|
|
377
|
+
ctx.lineWidth = Math.max(1, scale);
|
|
378
|
+
ctx.beginPath();
|
|
379
|
+
ctx.moveTo(box.Position.x, y);
|
|
380
|
+
ctx.lineTo(cx - half, y);
|
|
381
|
+
ctx.moveTo(cx + half, y);
|
|
382
|
+
ctx.lineTo(box.Position.x + box.Size.x, y);
|
|
383
|
+
ctx.stroke();
|
|
384
|
+
ctx.textAlign = "center";
|
|
385
|
+
ctx.textBaseline = "middle";
|
|
386
|
+
ctx.fillText(label, cx, y);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
onClick() {
|
|
390
|
+
this.#open = !this.#open;
|
|
391
|
+
this.#ports.forEach((p) => p.setHidden(!this.#open));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const mathPublisher = new Publisher({
|
|
396
|
+
name: "Math",
|
|
397
|
+
description: "Nodes the live example evaluates",
|
|
398
|
+
version: "v1.0.0",
|
|
399
|
+
nodes: {
|
|
400
|
+
"Number": {
|
|
401
|
+
title: "Number",
|
|
402
|
+
metadata: { op: "number" },
|
|
403
|
+
outputs: [{ name: "value", type: "float32" }],
|
|
404
|
+
widgets: [{ type: "number", config: { property: "value", value: 1, label: "Value" } }],
|
|
405
|
+
},
|
|
406
|
+
"Slider": {
|
|
407
|
+
title: "Slider",
|
|
408
|
+
metadata: { op: "number" },
|
|
409
|
+
outputs: [{ name: "value", type: "float32" }],
|
|
410
|
+
widgets: [{ type: "slider", config: { property: "value", min: 0, max: 10, value: 5, label: "0 – 10" } }],
|
|
411
|
+
},
|
|
412
|
+
"Add": {
|
|
413
|
+
title: "Add",
|
|
414
|
+
info: "Adds up everything wired into **numbers**.",
|
|
415
|
+
metadata: { op: "add" },
|
|
416
|
+
inputs: [{ name: "numbers", type: "float32", array: true, description: "Any number of values" }],
|
|
417
|
+
outputs: [{ name: "sum", type: "float32" }],
|
|
418
|
+
},
|
|
419
|
+
"Multiply": {
|
|
420
|
+
title: "Multiply",
|
|
421
|
+
metadata: { op: "multiply" },
|
|
422
|
+
inputs: [{ name: "a", type: "float32" }, { name: "b", type: "float32" }],
|
|
423
|
+
outputs: [{ name: "product", type: "float32" }],
|
|
424
|
+
},
|
|
425
|
+
"Display": {
|
|
426
|
+
title: "Display",
|
|
427
|
+
metadata: { op: "display" },
|
|
428
|
+
inputs: [{ name: "value", type: "float32" }],
|
|
429
|
+
widgets: [{ type: "text", config: { property: "text", value: "—", textBoxStyle: { size: 28, color: "#ffc95c" } } }],
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const examplePublisher = new Publisher({
|
|
435
|
+
name: "Examples",
|
|
436
|
+
description: "One node for each kind of widget",
|
|
437
|
+
version: "v1.0.0",
|
|
438
|
+
nodes: {
|
|
439
|
+
"Empty": { title: "Empty Node", canEditTitle: true, canEditPorts: true, canEditInfo: true },
|
|
440
|
+
"widgets/Number": { title: "Number", widgets: [{ type: "number", config: { label: "Value" } }] },
|
|
441
|
+
"widgets/Slider": { title: "Slider", widgets: [{ type: "slider", config: { value: 0.5 } }] },
|
|
442
|
+
"widgets/String": { title: "String", widgets: [{ type: "string", config: { label: "Text", value: "hello" } }] },
|
|
443
|
+
"widgets/Color": { title: "Color", widgets: [{ type: "color", config: { value: "#3fa7d6" } }] },
|
|
444
|
+
"widgets/Toggle": { title: "Toggle", widgets: [{ type: "toggle", config: { label: "On" } }] },
|
|
445
|
+
"widgets/Vector": {
|
|
446
|
+
title: "Vector",
|
|
447
|
+
outputs: [{ name: "out", type: "vector3" }],
|
|
448
|
+
widgets: [{
|
|
449
|
+
type: "row", config: {
|
|
450
|
+
widgets: ["X", "Y", "Z"].map((axis) => ({ type: "number", config: { property: axis.toLowerCase(), label: axis, width: 72 } })),
|
|
451
|
+
},
|
|
452
|
+
}],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const baseWire = DefaultConnectionRenderer(2, undefined, 4, undefined);
|
|
458
|
+
|
|
459
|
+
const graph = new NodeFlowGraph(document.getElementById("canvas"), {
|
|
460
|
+
minimap: { enabled: true },
|
|
461
|
+
idleConnection: {
|
|
462
|
+
renderer: (params) => {
|
|
463
|
+
const text = params.outPort?.getDataType() === "string" || params.inPort?.getDataType() === "string";
|
|
464
|
+
if (text) {
|
|
465
|
+
params.ctx.setLineDash([6 * params.graphScale, 5 * params.graphScale]);
|
|
466
|
+
}
|
|
467
|
+
baseWire(params);
|
|
468
|
+
params.ctx.setLineDash([]);
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
contextMenu: {
|
|
472
|
+
items: [
|
|
473
|
+
{ name: "Center on graph", callback: () => graph.centerOnGraph() },
|
|
474
|
+
{ name: "Organize", callback: () => graph.organize() },
|
|
475
|
+
],
|
|
476
|
+
subMenus: [{
|
|
477
|
+
name: "Demo",
|
|
478
|
+
items: [
|
|
479
|
+
{ name: "Log hello", callback: () => log("Context menu", "Demo › Log hello") },
|
|
480
|
+
{ name: "A disabled item", enabled: () => ContextMenuItemState.Disabled },
|
|
481
|
+
],
|
|
482
|
+
}],
|
|
483
|
+
},
|
|
484
|
+
nodes: {
|
|
485
|
+
onNodeCreated: (publisher, nodeType) => log("Node created", `${publisher} › ${nodeType}`),
|
|
486
|
+
},
|
|
487
|
+
board: {
|
|
488
|
+
onNoteAdded: () => log("Note added"),
|
|
489
|
+
onNoteRemoved: () => log("Note removed"),
|
|
490
|
+
onNoteDragStop: () => log("Note moved"),
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
graph.addPublisher("math", mathPublisher);
|
|
495
|
+
graph.addPublisher("examples", examplePublisher);
|
|
496
|
+
|
|
497
|
+
function evaluate(node, visiting = new Set()) {
|
|
498
|
+
const op = node.metadata()?.op;
|
|
499
|
+
if (op === undefined || visiting.has(node)) {
|
|
500
|
+
return NaN;
|
|
501
|
+
}
|
|
502
|
+
visiting.add(node);
|
|
503
|
+
const wired = (index) => node.inputPort(index).connections()
|
|
504
|
+
.map((c) => c.outNode())
|
|
505
|
+
.filter((n) => n !== null)
|
|
506
|
+
.map((n) => evaluate(n, new Set(visiting)));
|
|
507
|
+
|
|
508
|
+
switch (op) {
|
|
509
|
+
case "number": return Number(node.getProperty("value") ?? 0);
|
|
510
|
+
case "add": return wired(0).reduce((sum, v) => sum + v, 0);
|
|
511
|
+
case "multiply": return (wired(0)[0] ?? NaN) * (wired(1)[0] ?? NaN);
|
|
512
|
+
case "display": return wired(0)[0] ?? NaN;
|
|
513
|
+
}
|
|
514
|
+
return NaN;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let refreshQueued = false;
|
|
518
|
+
function refresh() {
|
|
519
|
+
if (refreshQueued) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
refreshQueued = true;
|
|
523
|
+
queueMicrotask(() => {
|
|
524
|
+
refreshQueued = false;
|
|
525
|
+
for (const node of graph.getNodes()) {
|
|
526
|
+
if (node.metadata()?.op !== "display") {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const v = evaluate(node);
|
|
530
|
+
node.setProperty("text", Number.isFinite(v) ? fmt(v) : "—");
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
graph.addOnNodeAddedListener((node) => {
|
|
536
|
+
if (node.metadata()?.op === undefined) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
node.addAnyPropertyChangeListener((name) => {
|
|
540
|
+
if (name !== "text") {
|
|
541
|
+
refresh();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
for (let i = 0; i < node.inputs(); i++) {
|
|
545
|
+
node.inputPort(i).addConnectionAddedListener(refresh);
|
|
546
|
+
node.inputPort(i).addConnectionRemovedListener(refresh);
|
|
547
|
+
}
|
|
548
|
+
refresh();
|
|
549
|
+
});
|
|
550
|
+
graph.addOnNodeRemovedListener((node) => {
|
|
551
|
+
log("Node removed", node.title());
|
|
552
|
+
refresh();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const recipes = new WeakMap();
|
|
556
|
+
graph.addOnNodeCreatedListener((publisher, nodeType, node) => recipes.set(node, { publisher, nodeType }));
|
|
557
|
+
const publishers = { math: mathPublisher, examples: examplePublisher };
|
|
558
|
+
|
|
559
|
+
let clipboard = null;
|
|
560
|
+
graph.addCopyListener((nodes, connections) => {
|
|
561
|
+
const copyable = nodes.filter((n) => recipes.has(n));
|
|
562
|
+
clipboard = {
|
|
563
|
+
nodes: copyable.map((n) => ({ ...recipes.get(n), position: { ...n.getPosition() }, value: n.getProperty("value") })),
|
|
564
|
+
connections: connections.filter((c) => recipes.has(nodes[c.fromNode]) && recipes.has(nodes[c.toNode]))
|
|
565
|
+
.map((c) => ({ ...c, fromNode: copyable.indexOf(nodes[c.fromNode]), toNode: copyable.indexOf(nodes[c.toNode]) })),
|
|
566
|
+
};
|
|
567
|
+
log("Copied", `${copyable.length} of ${nodes.length} selected (menu-made nodes copy)`);
|
|
568
|
+
});
|
|
569
|
+
graph.addPasteListener((at) => {
|
|
570
|
+
if (!clipboard || clipboard.nodes.length === 0) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const origin = clipboard.nodes[0].position;
|
|
574
|
+
graph.unselectAllNodes();
|
|
575
|
+
const made = clipboard.nodes.map((n) => {
|
|
576
|
+
const node = publishers[n.publisher].create(n.nodeType);
|
|
577
|
+
recipes.set(node, { publisher: n.publisher, nodeType: n.nodeType });
|
|
578
|
+
node.setPosition({ x: at.x + n.position.x - origin.x, y: at.y + n.position.y - origin.y });
|
|
579
|
+
if (n.value !== undefined) {
|
|
580
|
+
node.setProperty("value", n.value);
|
|
581
|
+
}
|
|
582
|
+
graph.addNode(node);
|
|
583
|
+
node.select();
|
|
584
|
+
return node;
|
|
585
|
+
});
|
|
586
|
+
for (const c of clipboard.connections) {
|
|
587
|
+
graph.connectNodes(made[c.fromNode], c.fromPort, made[c.toNode], c.toPort);
|
|
588
|
+
}
|
|
589
|
+
log("Pasted", `${made.length} nodes`);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const COL = 1100;
|
|
593
|
+
const ROW = 1250;
|
|
594
|
+
const at = (col, row, dx = 0, dy = 0) => ({ x: col * COL + dx, y: row * ROW + dy });
|
|
595
|
+
|
|
596
|
+
function note(position, text, width = 480, locked = true) {
|
|
597
|
+
graph.addNote(new FlowNote({ position, text, width, locked }));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function add(config) {
|
|
601
|
+
const node = new FlowNode(config);
|
|
602
|
+
graph.addNode(node);
|
|
603
|
+
return node;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function fromPublisher(publisher, type, position, value) {
|
|
607
|
+
const node = publishers[publisher].create(type);
|
|
608
|
+
recipes.set(node, { publisher, nodeType: type });
|
|
609
|
+
node.setPosition(position);
|
|
610
|
+
if (value !== undefined) {
|
|
611
|
+
node.setProperty("value", value);
|
|
612
|
+
}
|
|
613
|
+
graph.addNode(node);
|
|
614
|
+
return node;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function logEdits(node, ...properties) {
|
|
618
|
+
for (const p of properties) {
|
|
619
|
+
node.addPropertyEditListener(p, (v) => log(`${node.title()} edited`, `${p} = ${fmt(v)}`));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
note(at(0, 0), `
|
|
624
|
+
# Node Flow
|
|
625
|
+
|
|
626
|
+
Node Flow is a library for building node based tools, in the spirit of Unreal Blueprints and Blender's node editors. Everything on this canvas is live, so try it out.
|
|
627
|
+
|
|
628
|
+
Each area of the canvas shows one part of the library, with a note explaining it and the code to do it.
|
|
629
|
+
|
|
630
|
+
\`\`\`
|
|
631
|
+
const graph = new NodeFlowGraph(canvas);
|
|
632
|
+
graph.addNode(new FlowNode({ title: "Hello" }));
|
|
633
|
+
\`\`\`
|
|
634
|
+
`, 520);
|
|
635
|
+
|
|
636
|
+
note(at(0, 0, 0, 470), `
|
|
637
|
+
## Getting around
|
|
638
|
+
|
|
639
|
+
* **Drag** empty canvas to pan, **scroll** to zoom
|
|
640
|
+
* **Drag** a node by its body; **Ctrl+drag** on empty canvas to box select
|
|
641
|
+
* **Right-click** for menus: on the canvas, a node, or a note
|
|
642
|
+
* **Space** opens a searchable new node menu at the cursor
|
|
643
|
+
* **Drag a wire** from a port and let go on empty canvas to get a menu of nodes that fit
|
|
644
|
+
* **Delete** removes selected nodes
|
|
645
|
+
* **Ctrl+C**, **Ctrl+V**, **Ctrl+D** copy, paste and duplicate
|
|
646
|
+
* **Drop a file** on a node that accepts files
|
|
647
|
+
|
|
648
|
+
**Controls** (top right) calls the graph's methods, and **Events** (bottom left) logs what the graph reports. Click either title to fold it away.
|
|
649
|
+
`, 520);
|
|
650
|
+
|
|
651
|
+
note(at(1, 0), `
|
|
652
|
+
# Ports and types
|
|
653
|
+
|
|
654
|
+
Each port has a type, and a wire only joins ports whose types match. Hover a port to see its type and description. Try wiring **Text** into **Add**: it is refused.
|
|
655
|
+
|
|
656
|
+
A port can also accept **any** type, or a **list** of types.
|
|
657
|
+
|
|
658
|
+
\`\`\`
|
|
659
|
+
inputs: [
|
|
660
|
+
{ name: "a", type: "float32",
|
|
661
|
+
description: "First number" },
|
|
662
|
+
{ name: "value", type: "T", anyType: true },
|
|
663
|
+
{ name: "x", type: "float32",
|
|
664
|
+
acceptedTypes: ["float32", "int"] },
|
|
665
|
+
]
|
|
666
|
+
\`\`\`
|
|
667
|
+
|
|
668
|
+
Wires carrying \`string\` are dashed here by a custom connection renderer.
|
|
669
|
+
`);
|
|
670
|
+
|
|
671
|
+
const numberA = add({
|
|
672
|
+
title: "Number", position: at(1, 0, 0, 560),
|
|
673
|
+
outputs: [{ name: "value", type: "float32", description: "A number" }],
|
|
674
|
+
widgets: [{ type: "number", config: { value: 4, label: "Value" } }],
|
|
675
|
+
});
|
|
676
|
+
const text = add({
|
|
677
|
+
title: "Text", position: at(1, 0, 0, 720),
|
|
678
|
+
outputs: [{ name: "text", type: "string" }],
|
|
679
|
+
widgets: [{ type: "string", config: { value: "hello" } }],
|
|
680
|
+
});
|
|
681
|
+
const intSource = add({
|
|
682
|
+
title: "Whole number", position: at(1, 0, 0, 880),
|
|
683
|
+
outputs: [{ name: "value", type: "int" }],
|
|
684
|
+
widgets: [{ type: "number", config: { value: 3, integer: true, label: "Value" } }],
|
|
685
|
+
});
|
|
686
|
+
const addTyped = add({
|
|
687
|
+
title: "Add", subTitle: "Typed ports", position: at(1, 0, 330, 560),
|
|
688
|
+
info: "Takes two **float32** numbers.",
|
|
689
|
+
inputs: [
|
|
690
|
+
{ name: "a", type: "float32", description: "First number" },
|
|
691
|
+
{ name: "b", type: "float32", description: "Second number" },
|
|
692
|
+
],
|
|
693
|
+
outputs: [{ name: "sum", type: "float32", description: "a + b" }],
|
|
694
|
+
});
|
|
695
|
+
const anyNode = add({
|
|
696
|
+
title: "Print", subTitle: "Any type", position: at(1, 0, 330, 720),
|
|
697
|
+
inputs: [{ name: "value", type: "T", anyType: true, description: "Takes anything" }],
|
|
698
|
+
});
|
|
699
|
+
add({
|
|
700
|
+
title: "Round", subTitle: "Accepts a list of types", position: at(1, 0, 330, 860),
|
|
701
|
+
inputs: [{ name: "x", type: "float32", acceptedTypes: ["float32", "int"], description: "float32 or int" }],
|
|
702
|
+
outputs: [{ name: "rounded", type: "int" }],
|
|
703
|
+
});
|
|
704
|
+
add({
|
|
705
|
+
title: "Editable ports", position: at(1, 0, 660, 560),
|
|
706
|
+
info: "Right-click this node to add or remove ports.",
|
|
707
|
+
canEditPorts: true, canEditTitle: true, canEditInfo: true,
|
|
708
|
+
inputs: [{ name: "in", type: "float32" }],
|
|
709
|
+
outputs: [{ name: "out", type: "float32" }],
|
|
710
|
+
});
|
|
711
|
+
graph.connectNodes(numberA, 0, addTyped, 0);
|
|
712
|
+
graph.connectNodes(text, 0, anyNode, 0);
|
|
713
|
+
|
|
714
|
+
note(at(2, 0), `
|
|
715
|
+
# Inputs that take many wires
|
|
716
|
+
|
|
717
|
+
An **array** input takes any number of wires. It grows a slot per wire, in order.
|
|
718
|
+
|
|
719
|
+
* Press a **slot** to pick up just that wire
|
|
720
|
+
* Drop a wire **between slots** to put it there; a white bar marks the spot
|
|
721
|
+
* Pick up a wire and drop it in another gap to **reorder**
|
|
722
|
+
|
|
723
|
+
\`\`\`
|
|
724
|
+
inputs: [
|
|
725
|
+
{ name: "numbers", type: "float32",
|
|
726
|
+
array: true },
|
|
727
|
+
]
|
|
728
|
+
\`\`\`
|
|
729
|
+
`);
|
|
730
|
+
const one = add({ title: "One", position: at(2, 0, 0, 470), outputs: [{ name: "value", type: "float32" }] });
|
|
731
|
+
const two = add({ title: "Two", position: at(2, 0, 0, 590), outputs: [{ name: "value", type: "float32" }] });
|
|
732
|
+
const three = add({ title: "Three", position: at(2, 0, 0, 710), outputs: [{ name: "value", type: "float32" }] });
|
|
733
|
+
add({ title: "Four", subTitle: "Not wired yet", position: at(2, 0, 0, 830), outputs: [{ name: "value", type: "float32" }] });
|
|
734
|
+
const many = add({
|
|
735
|
+
title: "Sum", subTitle: "Array input", position: at(2, 0, 380, 560),
|
|
736
|
+
inputs: [{ name: "numbers", type: "float32", array: true, description: "Wire in as many as you like" }],
|
|
737
|
+
outputs: [{ name: "sum", type: "float32" }],
|
|
738
|
+
});
|
|
739
|
+
graph.connectNodes(one, 0, many, 0);
|
|
740
|
+
graph.connectNodes(two, 0, many, 0);
|
|
741
|
+
graph.connectNodes(three, 0, many, 0);
|
|
742
|
+
|
|
743
|
+
note(at(3, 0), `
|
|
744
|
+
# Hidden ports
|
|
745
|
+
|
|
746
|
+
A port can start **hidden**, to keep busy nodes small. A hidden port still shows while something is wired to it, so no wire ever points at nothing.
|
|
747
|
+
|
|
748
|
+
The "more" line is a custom widget that shows and hides them.
|
|
749
|
+
|
|
750
|
+
\`\`\`
|
|
751
|
+
inputs: [
|
|
752
|
+
{ name: "Sheen", type: "sheen",
|
|
753
|
+
hidden: true },
|
|
754
|
+
]
|
|
755
|
+
port.setHidden(false);
|
|
756
|
+
\`\`\`
|
|
757
|
+
`);
|
|
758
|
+
const sheen = add({ title: "Sheen", position: at(3, 0, 0, 560), outputs: [{ name: "sheen", type: "sheen" }] });
|
|
759
|
+
const material = add({
|
|
760
|
+
title: "Material", subTitle: "Hidden ports", position: at(3, 0, 330, 520),
|
|
761
|
+
inputs: [
|
|
762
|
+
{ name: "Color", type: "color" },
|
|
763
|
+
{ name: "Roughness", type: "float32" },
|
|
764
|
+
{ name: "Metallic", type: "float32" },
|
|
765
|
+
{ name: "Alpha", type: "float32", hidden: true },
|
|
766
|
+
{ name: "Clearcoat", type: "clearcoat", hidden: true },
|
|
767
|
+
{ name: "Sheen", type: "sheen", hidden: true },
|
|
768
|
+
{ name: "Specular", type: "specular", hidden: true },
|
|
769
|
+
{ name: "Unlit", type: "bool", hidden: true },
|
|
770
|
+
],
|
|
771
|
+
outputs: [{ name: "material", type: "material" }],
|
|
772
|
+
});
|
|
773
|
+
const hiddenPorts = [];
|
|
774
|
+
for (let i = 0; i < material.inputs(); i++) {
|
|
775
|
+
if (["Alpha", "Clearcoat", "Sheen", "Specular", "Unlit"].includes(material.inputPort(i).getDisplayName())) {
|
|
776
|
+
hiddenPorts.push(material.inputPort(i));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
material.insertWidget(0, new MorePortsWidget(hiddenPorts));
|
|
780
|
+
graph.connectNodes(sheen, 0, material, 5);
|
|
781
|
+
|
|
782
|
+
note(at(0, 1), `
|
|
783
|
+
# Widgets
|
|
784
|
+
|
|
785
|
+
Widgets sit under a node's ports and fill its width.
|
|
786
|
+
|
|
787
|
+
* **Numbers**: drag sideways to change, or click to type. Enter keeps it, Escape cancels
|
|
788
|
+
* **Sliders** drag, and keep following the pointer until you let go
|
|
789
|
+
* **Text** and **colours** open in place
|
|
790
|
+
* A **row** puts widgets side by side
|
|
791
|
+
|
|
792
|
+
Each value widget can be bound to a node **property**. Setting the property from code updates the widget quietly; a person's finished edit fires **edit listeners**. Watch the event log as you edit.
|
|
793
|
+
|
|
794
|
+
\`\`\`
|
|
795
|
+
widgets: [
|
|
796
|
+
{ type: "number", config: {
|
|
797
|
+
property: "radius", label: "Radius",
|
|
798
|
+
step: 0.05, min: 0 } },
|
|
799
|
+
{ type: "row", config: { widgets: [
|
|
800
|
+
{ type: "number", config:
|
|
801
|
+
{ property: "x", label: "X", width: 72 } },
|
|
802
|
+
...
|
|
803
|
+
] } },
|
|
804
|
+
]
|
|
805
|
+
|
|
806
|
+
node.addPropertyEditListener("radius",
|
|
807
|
+
(v) => save(v));
|
|
808
|
+
\`\`\`
|
|
809
|
+
`, 560);
|
|
810
|
+
|
|
811
|
+
const numbers = add({
|
|
812
|
+
title: "Numbers", position: at(0, 1, 640, 0),
|
|
813
|
+
widgets: [
|
|
814
|
+
{ type: "number", config: { property: "radius", label: "Radius", value: 1.25, min: 0 } },
|
|
815
|
+
{ type: "number", config: { property: "count", label: "Count", value: 3, integer: true, min: 0, max: 10 } },
|
|
816
|
+
{ type: "number", config: { property: "fine", label: "Fine", value: 0.5, step: 0.001 } },
|
|
817
|
+
],
|
|
818
|
+
});
|
|
819
|
+
logEdits(numbers, "radius", "count", "fine");
|
|
820
|
+
|
|
821
|
+
const sliders = add({
|
|
822
|
+
title: "Sliders", position: at(0, 1, 640, 230),
|
|
823
|
+
widgets: [
|
|
824
|
+
{ type: "slider", config: { property: "amount", label: "Amount", value: 0.4 } },
|
|
825
|
+
{ type: "slider", config: { property: "snapped", label: "Snapped", min: 0, max: 5, value: 2.5, snapIncrement: 0.25 } },
|
|
826
|
+
],
|
|
827
|
+
});
|
|
828
|
+
logEdits(sliders, "amount", "snapped");
|
|
829
|
+
|
|
830
|
+
const vector = add({
|
|
831
|
+
title: "Vector", subTitle: "Row of labelled numbers", position: at(0, 1, 640, 420),
|
|
832
|
+
outputs: [{ name: "position", type: "vector3" }],
|
|
833
|
+
widgets: [{
|
|
834
|
+
type: "row", config: {
|
|
835
|
+
widgets: ["X", "Y", "Z"].map((axis, i) => ({
|
|
836
|
+
type: "number", config: { property: axis.toLowerCase(), label: axis, width: 72, value: [1, 2.5, -3][i] },
|
|
837
|
+
})),
|
|
838
|
+
},
|
|
839
|
+
}],
|
|
840
|
+
});
|
|
841
|
+
logEdits(vector, "x", "y", "z");
|
|
842
|
+
|
|
843
|
+
const strings = add({
|
|
844
|
+
title: "Text and colour", position: at(0, 1, 1020, 0),
|
|
845
|
+
widgets: [
|
|
846
|
+
{ type: "string", config: { property: "name", label: "Name", value: "Node Flow" } },
|
|
847
|
+
{ type: "color", config: { property: "tint", label: "Tint", value: "#e0605080" } },
|
|
848
|
+
{ type: "color", config: { property: "paper", value: "#f2e8cf" } },
|
|
849
|
+
],
|
|
850
|
+
});
|
|
851
|
+
logEdits(strings, "name", "tint", "paper");
|
|
852
|
+
|
|
853
|
+
const toggles = add({ title: "Toggle, disable and hide", position: at(0, 1, 1020, 300) });
|
|
854
|
+
const gatedNumber = new NumberWidget(toggles, { label: "Gated", value: 7 });
|
|
855
|
+
const gatedText = new StringWidget(toggles, { label: "Also gated", value: "edit me" });
|
|
856
|
+
const detailRow = new RowWidget([
|
|
857
|
+
new NumberWidget(toggles, { label: "W", width: 72, value: 2 }),
|
|
858
|
+
new NumberWidget(toggles, { label: "H", width: 72, value: 1 }),
|
|
859
|
+
]);
|
|
860
|
+
toggles.addWidget(new ToggleWidget(toggles, {
|
|
861
|
+
label: "Editable", value: true,
|
|
862
|
+
callback: (on) => {
|
|
863
|
+
gatedNumber.setDisabled(!on);
|
|
864
|
+
gatedText.setDisabled(!on);
|
|
865
|
+
log("Toggle", `Editable = ${on}`);
|
|
866
|
+
},
|
|
867
|
+
}));
|
|
868
|
+
toggles.addWidget(gatedNumber);
|
|
869
|
+
toggles.addWidget(gatedText);
|
|
870
|
+
toggles.addWidget(new ToggleWidget(toggles, {
|
|
871
|
+
label: "Show size", value: false, onColor: "#ffc95c", offColor: "#4a3a10",
|
|
872
|
+
callback: (on) => detailRow.setHidden(!on),
|
|
873
|
+
}));
|
|
874
|
+
detailRow.setHidden(true);
|
|
875
|
+
toggles.addWidget(detailRow);
|
|
876
|
+
|
|
877
|
+
const buttons = add({ title: "Button and text", position: at(0, 1, 1020, 600) });
|
|
878
|
+
let clicks = 0;
|
|
879
|
+
const counter = new ButtonWidget({
|
|
880
|
+
text: "Click me",
|
|
881
|
+
callback: () => {
|
|
882
|
+
clicks++;
|
|
883
|
+
counter.setText(`Clicked ${clicks} time${clicks === 1 ? "" : "s"}`);
|
|
884
|
+
log("Button", `clicked ${clicks}`);
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
buttons.addWidget(counter);
|
|
888
|
+
buttons.addWidget(new TextWidget(buttons, { property: "caption", value: "Drag the number:" }));
|
|
889
|
+
buttons.addWidget(new NumberWidget(buttons, {
|
|
890
|
+
label: "Live", value: 10,
|
|
891
|
+
onInput: (v) => buttons.setProperty("caption", `Dragging… ${fmt(v)}`),
|
|
892
|
+
callback: (v) => buttons.setProperty("caption", `Kept ${fmt(v)}`),
|
|
893
|
+
}));
|
|
894
|
+
|
|
895
|
+
add({
|
|
896
|
+
title: "Image", position: at(0, 1, 1400, 0),
|
|
897
|
+
widgets: [{ type: "image", config: { image: "https://i.imgur.com/qRZgWC0.jpeg", maxWidth: 220, maxHeight: 220 } }],
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
note(at(2, 1), `
|
|
901
|
+
# Custom widgets
|
|
902
|
+
|
|
903
|
+
Extend **WidgetBase** for anything interactive, or **ValueWidget** for something that holds a value. The base class tracks hover, press, disabled and hidden for you, and passes on pointer events. Once pressed, a widget keeps getting them until the pointer is let go, even off the node.
|
|
904
|
+
|
|
905
|
+
\`\`\`
|
|
906
|
+
class Stars extends ValueWidget {
|
|
907
|
+
Size() { return { x: 150, y: 25 }; }
|
|
908
|
+
render(ctx, box, scale) { ... }
|
|
909
|
+
onClick(pointer) {
|
|
910
|
+
this.commit(starAt(pointer.position.x),
|
|
911
|
+
this.value());
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
node.addWidget(new Stars(node,
|
|
915
|
+
{ property: "rating" }));
|
|
916
|
+
\`\`\`
|
|
917
|
+
|
|
918
|
+
Use \`drawField\`, \`drawFieldBox\` and \`widgetFont\` to match the built-in look.
|
|
919
|
+
`);
|
|
920
|
+
const rating = add({ title: "Review", subTitle: "Custom widget", position: at(2, 1, 0, 660) });
|
|
921
|
+
rating.addWidget(new StarRatingWidget(rating, { property: "rating", value: 3 }));
|
|
922
|
+
rating.addWidget(new TextWidget(rating, { property: "ratingText", value: "3 of 5" }));
|
|
923
|
+
rating.addPropertyChangeListener("rating", (_, v) => rating.setProperty("ratingText", `${v} of 5`));
|
|
924
|
+
logEdits(rating, "rating");
|
|
925
|
+
|
|
926
|
+
note(at(3, 1), `
|
|
927
|
+
# Node options
|
|
928
|
+
|
|
929
|
+
* **Messages** hang under a node; some only show on hover
|
|
930
|
+
* **Styles** recolour a node's title and body
|
|
931
|
+
* A **locked** node cannot be dragged
|
|
932
|
+
* **Editable** nodes let people retitle them and edit their info
|
|
933
|
+
* A node can add its own **right-click menu**, with groups, submenus, and items that are disabled or hidden
|
|
934
|
+
* **onFileDrop** receives files dropped on the node
|
|
935
|
+
* Callbacks report **select**, **grab**, **release** and **drag stop**
|
|
936
|
+
|
|
937
|
+
\`\`\`
|
|
938
|
+
new FlowNode({
|
|
939
|
+
locked: true,
|
|
940
|
+
messages: [{ message: "Hi",
|
|
941
|
+
type: MessageType.Warning,
|
|
942
|
+
alwaysShow: true }],
|
|
943
|
+
style: { title: { color: "#6b2d5c" } },
|
|
944
|
+
onFileDrop: (file) => ...,
|
|
945
|
+
});
|
|
946
|
+
\`\`\`
|
|
947
|
+
`, 500);
|
|
948
|
+
add({
|
|
949
|
+
title: "Messages", position: at(3, 1, 0, 620),
|
|
950
|
+
messages: [
|
|
951
|
+
{ message: "Always shown", alwaysShow: true },
|
|
952
|
+
{ message: "Only on hover" },
|
|
953
|
+
{ message: "A warning", type: MessageType.Warning, alwaysShow: true },
|
|
954
|
+
{ message: "An error", type: MessageType.Error, alwaysShow: true },
|
|
955
|
+
],
|
|
956
|
+
});
|
|
957
|
+
add({
|
|
958
|
+
title: "Styled", subTitle: "Custom colours", position: at(3, 1, 330, 620),
|
|
959
|
+
style: {
|
|
960
|
+
title: { color: "#6b2d5c" },
|
|
961
|
+
idle: { color: "#2a1630" },
|
|
962
|
+
mouseOver: { color: "#3a1f42" },
|
|
963
|
+
grabbed: { color: "#4a2853" },
|
|
964
|
+
selected: { color: "#4a2853" },
|
|
965
|
+
},
|
|
966
|
+
outputs: [{ name: "out", type: "float32" }],
|
|
967
|
+
});
|
|
968
|
+
add({ title: "Locked", subTitle: "Can't be dragged", position: at(3, 1, 330, 760), locked: true });
|
|
969
|
+
add({
|
|
970
|
+
title: "Rename me", position: at(3, 1, 660, 620), canEditTitle: true, canEditInfo: true,
|
|
971
|
+
info: "Right-click › **Edit** to change my title or this text.",
|
|
972
|
+
onTitleChange: (_, before, after) => log("Title changed", `${before} → ${after}`),
|
|
973
|
+
});
|
|
974
|
+
add({
|
|
975
|
+
title: "Custom menu", subTitle: "Right-click me", position: at(3, 1, 660, 760),
|
|
976
|
+
contextMenu: {
|
|
977
|
+
items: [
|
|
978
|
+
{ name: "Say hello", callback: () => log("Menu", "hello from a node") },
|
|
979
|
+
{ name: "Disabled item", enabled: () => ContextMenuItemState.Disabled },
|
|
980
|
+
{ name: "Hidden item", enabled: () => ContextMenuItemState.Hidden },
|
|
981
|
+
{ name: "Grouped A", group: "group", callback: () => log("Menu", "grouped A") },
|
|
982
|
+
{ name: "Grouped B", group: "group", callback: () => log("Menu", "grouped B") },
|
|
983
|
+
],
|
|
984
|
+
subMenus: [{ name: "More", items: [{ name: "Nested", callback: () => log("Menu", "nested item") }] }],
|
|
985
|
+
},
|
|
986
|
+
});
|
|
987
|
+
const dropTarget = add({
|
|
988
|
+
title: "Drop a file here", subTitle: "onFileDrop", position: at(3, 1, 0, 900),
|
|
989
|
+
onSelect: () => log("Selected", "Drop a file here"),
|
|
990
|
+
onUnselect: () => log("Unselected", "Drop a file here"),
|
|
991
|
+
onDragStop: () => log("Drag stopped", "Drop a file here"),
|
|
992
|
+
onFileDrop: (file) => {
|
|
993
|
+
log("File dropped", `${file.name} (${file.size} bytes)`);
|
|
994
|
+
dropTarget.clearWidgets();
|
|
995
|
+
if (file.type.startsWith("image/")) {
|
|
996
|
+
dropTarget.addWidget(new ImageWidget({ blob: file, maxWidth: 200, maxHeight: 200 }));
|
|
997
|
+
} else {
|
|
998
|
+
dropTarget.addWidget(new TextWidget(dropTarget, { value: file.name }));
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
note(at(0, 2), `
|
|
1004
|
+
# A live graph
|
|
1005
|
+
|
|
1006
|
+
Node Flow draws and edits the graph; your app decides what it means. These **Math** nodes come from a **Publisher**, and this page recalculates them whenever a value or a wire changes.
|
|
1007
|
+
|
|
1008
|
+
Drag the numbers, rewire them, or add more from **right-click › New Node › Math**.
|
|
1009
|
+
|
|
1010
|
+
\`\`\`
|
|
1011
|
+
const math = new Publisher({ name: "Math",
|
|
1012
|
+
nodes: { "Add": { title: "Add", ... } } });
|
|
1013
|
+
graph.addPublisher("math", math);
|
|
1014
|
+
|
|
1015
|
+
node.addAnyPropertyChangeListener(recalc);
|
|
1016
|
+
port.addConnectionAddedListener(recalc);
|
|
1017
|
+
\`\`\`
|
|
1018
|
+
`, 520);
|
|
1019
|
+
const n1 = fromPublisher("math", "Number", at(0, 2, 600, 0), 2);
|
|
1020
|
+
const n2 = fromPublisher("math", "Slider", at(0, 2, 600, 180), 5);
|
|
1021
|
+
const n3 = fromPublisher("math", "Number", at(0, 2, 600, 360), 3);
|
|
1022
|
+
const sum = fromPublisher("math", "Add", at(0, 2, 940, 60));
|
|
1023
|
+
const product = fromPublisher("math", "Multiply", at(0, 2, 1220, 200));
|
|
1024
|
+
const display = fromPublisher("math", "Display", at(0, 2, 1500, 200));
|
|
1025
|
+
graph.connectNodes(n1, 0, sum, 0);
|
|
1026
|
+
graph.connectNodes(n2, 0, sum, 0);
|
|
1027
|
+
graph.connectNodes(sum, 0, product, 0);
|
|
1028
|
+
graph.connectNodes(n3, 0, product, 1);
|
|
1029
|
+
graph.connectNodes(product, 0, display, 0);
|
|
1030
|
+
|
|
1031
|
+
note(at(2, 2), `
|
|
1032
|
+
# Notes
|
|
1033
|
+
|
|
1034
|
+
Notes are **markdown**, drawn on the canvas.
|
|
1035
|
+
|
|
1036
|
+
* Headers, **bold**, *italic*
|
|
1037
|
+
* Lists
|
|
1038
|
+
* Code blocks
|
|
1039
|
+
|
|
1040
|
+
\`\`\`
|
|
1041
|
+
graph.addNote(new FlowNote({
|
|
1042
|
+
text: "# Hello", position, width: 400,
|
|
1043
|
+
}));
|
|
1044
|
+
\`\`\`
|
|
1045
|
+
|
|
1046
|
+
Every note on this page is **locked**. The one beside this is not: drag it, pull its edges to resize it, or right-click it to edit.
|
|
1047
|
+
`);
|
|
1048
|
+
graph.addNote(new FlowNote({
|
|
1049
|
+
position: at(2, 2, 540, 0),
|
|
1050
|
+
width: 360,
|
|
1051
|
+
locked: false,
|
|
1052
|
+
text: `
|
|
1053
|
+
## Edit me
|
|
1054
|
+
|
|
1055
|
+
Right-click › **Edit Note**, then click away to save.
|
|
1056
|
+
|
|
1057
|
+
Drag the left or right edge to change the width.
|
|
1058
|
+
`,
|
|
1059
|
+
onContentChange: (_, text) => log("Note edited", text.trim().split("\n")[0]),
|
|
1060
|
+
onWidthChange: (_, w) => log("Note resized", `${Math.round(w)} wide`),
|
|
1061
|
+
}));
|
|
1062
|
+
|
|
1063
|
+
note(at(3, 2), `
|
|
1064
|
+
# Graph options
|
|
1065
|
+
|
|
1066
|
+
\`\`\`
|
|
1067
|
+
new NodeFlowGraph(canvas, {
|
|
1068
|
+
backgroundColor: "#07212a",
|
|
1069
|
+
minimap: { enabled: true },
|
|
1070
|
+
idleConnection: { renderer: (p) => ... },
|
|
1071
|
+
contextMenu: { items: [...] },
|
|
1072
|
+
nodes: {
|
|
1073
|
+
publishers: { ... },
|
|
1074
|
+
onNodeCreated: (pub, type, node) => ...,
|
|
1075
|
+
},
|
|
1076
|
+
board: { notes: [...], onNoteAdded },
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
setTheme({ FontFamily: "Tahoma",
|
|
1080
|
+
Node: { BackgroundColor: "#c0c0c0" } });
|
|
1081
|
+
\`\`\`
|
|
1082
|
+
|
|
1083
|
+
**setTheme** lays your values over the default theme, and everything already on screen redraws in it. Try **Theme: Windows 95** in Controls.
|
|
1084
|
+
|
|
1085
|
+
Right-click empty canvas to see this page's own menu items next to the built-in ones. The toolbar calls \`centerOnGraph\`, \`resetCamera\`, \`zoom\`, \`organize\`, \`minimap().setEnabled\` and \`renderContinuously\`.
|
|
1086
|
+
|
|
1087
|
+
\`organize\` lays every node out again, so it will rearrange this tour.
|
|
1088
|
+
`, 520);
|
|
1089
|
+
|
|
1090
|
+
let stopContinuous = null;
|
|
1091
|
+
let themeName = "Default";
|
|
1092
|
+
const actions = {
|
|
1093
|
+
"theme": (button) => {
|
|
1094
|
+
const next = themeName === "Default" ? "Windows 95" : "Default";
|
|
1095
|
+
button.textContent = `Theme: ${themeName}`;
|
|
1096
|
+
themeName = next;
|
|
1097
|
+
setTheme(themes[next]);
|
|
1098
|
+
log("Theme", next);
|
|
1099
|
+
},
|
|
1100
|
+
"center": () => graph.centerOnGraph(),
|
|
1101
|
+
"reset": () => graph.resetCamera(),
|
|
1102
|
+
"zoom-in": () => graph.zoom(1),
|
|
1103
|
+
"zoom-out": () => graph.zoom(-1),
|
|
1104
|
+
"minimap": () => graph.minimap().setEnabled(!graph.minimap().enabled()),
|
|
1105
|
+
"organize": () => graph.organize(),
|
|
1106
|
+
"select-all": () => graph.getNodes().forEach((n) => n.select()),
|
|
1107
|
+
"delete": () => log("Deleted", `${graph.removeSelectedNodes().length} nodes`),
|
|
1108
|
+
"continuous": (button) => {
|
|
1109
|
+
if (stopContinuous) {
|
|
1110
|
+
stopContinuous();
|
|
1111
|
+
stopContinuous = null;
|
|
1112
|
+
button.textContent = "Animate";
|
|
1113
|
+
} else {
|
|
1114
|
+
stopContinuous = graph.renderContinuously();
|
|
1115
|
+
button.textContent = "Stop";
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
};
|
|
1119
|
+
document.getElementById("toolbar").addEventListener("click", (e) => {
|
|
1120
|
+
const button = e.target.closest("button");
|
|
1121
|
+
if (button) {
|
|
1122
|
+
actions[button.dataset.act]?.(button);
|
|
1123
|
+
graph.requestRender();
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
graph.setCamera({ position: { x: 40, y: 90 }, zoom: 0.55 });
|
|
1128
|
+
window.graph = graph;
|
|
1129
|
+
logging = true;
|
|
1130
|
+
log("Ready", "try dragging a number; `graph` is on window");
|
|
1131
|
+
</script>
|
|
1132
|
+
</body>
|
|
1133
|
+
|
|
1134
|
+
</html>
|