@field/cascade 0.8.0 → 0.11.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.
Files changed (195) hide show
  1. package/AGENTS.md +36 -3
  2. package/CHANGELOG.md +401 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -26
  5. package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
  6. package/dist/assets/CodePanel-BU-autmq.js +2 -0
  7. package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
  8. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
  9. package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
  10. package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
  11. package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
  12. package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
  13. package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
  14. package/dist/assets/definitions-GreEMXLV.js +1 -0
  15. package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
  16. package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
  17. package/dist/assets/geo-u0n_jOsD.js +1 -0
  18. package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
  19. package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
  20. package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
  21. package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
  22. package/dist/assets/index-5ZtDAuy2.js +220 -0
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-client-DqYmKstU.js +1 -0
  25. package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
  26. package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
  27. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  28. package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
  30. package/dist/assets/math-DioIQOPJ.js +1 -0
  31. package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
  32. package/dist/assets/pop-DC_TQG9t.js +1 -0
  33. package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
  34. package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
  35. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  36. package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
  37. package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
  38. package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
  39. package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
  40. package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
  41. package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
  42. package/dist/cli/chunk-2KXB47FP.js +115 -0
  43. package/dist/cli/chunk-35D343DC.js +1447 -0
  44. package/dist/cli/chunk-36P5K3SD.js +631 -0
  45. package/dist/cli/chunk-3JTJHF7V.js +267 -0
  46. package/dist/cli/chunk-4UDFYF5G.js +132 -0
  47. package/dist/cli/chunk-53NMBJAO.js +35 -0
  48. package/dist/cli/chunk-A4OAMOZH.js +310 -0
  49. package/dist/cli/chunk-BIJO5OFS.js +71 -0
  50. package/dist/cli/chunk-BP7UKF36.js +376 -0
  51. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  52. package/dist/cli/chunk-CO22XHNG.js +593 -0
  53. package/dist/cli/chunk-D6BUOHUG.js +634 -0
  54. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  55. package/dist/cli/chunk-DUIUFVU2.js +241 -0
  56. package/dist/cli/chunk-EUIAC3U3.js +3006 -0
  57. package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
  58. package/dist/cli/chunk-GFS6W3C5.js +308 -0
  59. package/dist/cli/chunk-HFHWYL55.js +139 -0
  60. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  61. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  62. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  63. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  64. package/dist/cli/chunk-MIIEFUGG.js +623 -0
  65. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  66. package/dist/cli/chunk-POMMNKZZ.js +672 -0
  67. package/dist/cli/chunk-QBBKRNGY.js +90 -0
  68. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  69. package/dist/cli/chunk-RLCAJBVR.js +224 -0
  70. package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
  71. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  72. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  73. package/dist/cli/chunk-SGJAET2L.js +197 -0
  74. package/dist/cli/chunk-VVGSLVSF.js +151 -0
  75. package/dist/cli/chunk-WD62JIP6.js +118 -0
  76. package/dist/cli/chunk-WIH4TEIS.js +104 -0
  77. package/dist/cli/chunk-WKURDVYP.js +1119 -0
  78. package/dist/cli/chunk-WPUDGEFY.js +20 -0
  79. package/dist/cli/chunk-XWDSKG7N.js +1301 -0
  80. package/dist/cli/chunk-YAT5Y25U.js +121 -0
  81. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  82. package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
  83. package/dist/cli/commands-Q23M26QM.js +1292 -0
  84. package/dist/cli/definitions-HF2FTHYU.js +32 -0
  85. package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
  86. package/dist/cli/edit-JHCPTQFT.js +28 -0
  87. package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
  88. package/dist/cli/encode-HAUZTYPT.js +17 -0
  89. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  90. package/dist/cli/geo-4RNK3QVN.js +43 -0
  91. package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
  92. package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
  93. package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
  94. package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
  95. package/dist/cli/index.js +106 -14207
  96. package/dist/cli/math-ZRMLKU2D.js +16 -0
  97. package/dist/cli/mcp-ZLEEMYGF.js +164 -0
  98. package/dist/cli/parity-KINARCP2.js +328 -0
  99. package/dist/cli/pop-USDFXB7H.js +39 -0
  100. package/dist/cli/project-K3QIPGBB.js +12 -0
  101. package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
  102. package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
  103. package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
  104. package/dist/cli/render-YURAPPUI.js +436 -0
  105. package/dist/cli/runCommand-PI764LEG.js +198 -0
  106. package/dist/cli/runner-RA6UKEPD.js +37 -0
  107. package/dist/cli/runtime/image.ts +277 -0
  108. package/dist/cli/runtime/stage.ts +74 -11
  109. package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
  110. package/dist/cli/verbs-RCE3K4VQ.js +19 -0
  111. package/dist/cli/webBuild-GT4PZT5D.js +37 -0
  112. package/dist/contracts/bundle.d.ts +75 -0
  113. package/dist/contracts/bundle.js +112 -0
  114. package/dist/contracts/definition.d.ts +67 -4
  115. package/dist/contracts/document.d.ts +29 -0
  116. package/dist/contracts/documentFormat.d.ts +18 -0
  117. package/dist/contracts/documentFormat.js +195 -0
  118. package/dist/contracts/editor.d.ts +189 -0
  119. package/dist/contracts/editor.js +2 -0
  120. package/dist/contracts/index.d.ts +3 -0
  121. package/dist/contracts/index.js +3 -0
  122. package/dist/contracts/validate.js +59 -3
  123. package/dist/contracts/values.d.ts +3 -3
  124. package/dist/contracts/values.js +1 -0
  125. package/dist/image.d.ts +141 -0
  126. package/dist/image.js +127 -0
  127. package/dist/index.html +4 -4
  128. package/dist/player/app.js +17 -5
  129. package/dist/player/editor.js +1919 -0
  130. package/dist/player/editorEngine.js +477 -0
  131. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  132. package/dist/runtime/builtins/core/bundle.js +81 -0
  133. package/dist/runtime/builtins/core/index.d.ts +3 -2
  134. package/dist/runtime/builtins/core/index.js +6 -2
  135. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  136. package/dist/runtime/builtins/core/routing.js +28 -0
  137. package/dist/runtime/builtins/headless.d.ts +19 -0
  138. package/dist/runtime/builtins/headless.js +25 -0
  139. package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
  140. package/dist/runtime/builtins/image/contactSheet.js +199 -0
  141. package/dist/runtime/builtins/image/index.d.ts +9 -0
  142. package/dist/runtime/builtins/image/index.js +9 -0
  143. package/dist/runtime/edit/document.d.ts +94 -0
  144. package/dist/runtime/edit/document.js +194 -0
  145. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  146. package/dist/runtime/edit/graphEditor.js +893 -0
  147. package/dist/runtime/edit/index.d.ts +12 -0
  148. package/dist/runtime/edit/index.js +6 -0
  149. package/dist/runtime/edit/layout.d.ts +50 -0
  150. package/dist/runtime/edit/layout.js +126 -0
  151. package/dist/runtime/edit/operations.d.ts +65 -0
  152. package/dist/runtime/edit/operations.js +57 -0
  153. package/dist/runtime/edit/parameters.d.ts +75 -0
  154. package/dist/runtime/edit/parameters.js +284 -0
  155. package/dist/runtime/edit/read.d.ts +35 -0
  156. package/dist/runtime/edit/read.js +65 -0
  157. package/dist/runtime/editor/index.d.ts +104 -0
  158. package/dist/runtime/editor/index.js +439 -0
  159. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  160. package/dist/runtime/expressions/bundlePaths.js +55 -0
  161. package/dist/runtime/expressions/index.d.ts +2 -0
  162. package/dist/runtime/expressions/index.js +1 -0
  163. package/dist/runtime/index.d.ts +2 -0
  164. package/dist/runtime/index.js +2 -0
  165. package/dist/runtime/params/animator.d.ts +5 -0
  166. package/dist/runtime/params/animator.js +16 -0
  167. package/dist/runtime/runtime.js +238 -12
  168. package/dist/runtime/seed.d.ts +29 -0
  169. package/dist/runtime/seed.js +52 -0
  170. package/dist/runtime/types.d.ts +12 -0
  171. package/dist/stage.js +43 -9
  172. package/dist/studio/panel.d.ts +2 -0
  173. package/doc/CLI_REFERENCE.md +171 -0
  174. package/doc/EDITOR_EXPORT.md +235 -0
  175. package/doc/HEADLESS_GPU.md +27 -0
  176. package/doc/MCP.md +69 -0
  177. package/doc/NODE_REFERENCE.md +208 -0
  178. package/doc/PROJECT_AUTHORING.md +258 -1
  179. package/package.json +29 -8
  180. package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
  181. package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
  182. package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
  183. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  184. package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
  185. package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
  186. package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
  187. package/dist/assets/definitions-DZpOjWKR.js +0 -1
  188. package/dist/assets/geo-D2VPjAdt.js +0 -1
  189. package/dist/assets/index-CMiBdEuT.js +0 -212
  190. package/dist/assets/index-DMIuml9a.css +0 -1
  191. package/dist/assets/index-client-BI2H82FJ.js +0 -1
  192. package/dist/assets/legacy-ZA7textG.js +0 -2
  193. package/dist/assets/math-B6hpxPJO.js +0 -1
  194. package/dist/assets/pop-CYUIjT6x.js +0 -1
  195. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -0,0 +1,3081 @@
1
+ import {
2
+ CORE_TYPES,
3
+ Node,
4
+ colorToCss,
5
+ isCoreType,
6
+ normalizeColor,
7
+ registerNodeClasses
8
+ } from "./chunk-FCDXJ6EK.js";
9
+ import "./chunk-RUU3YWXU.js";
10
+ import {
11
+ randomFromSeed,
12
+ remap
13
+ } from "./chunk-RXDIMXN5.js";
14
+ import "./chunk-HFHWYL55.js";
15
+ import "./chunk-CO22XHNG.js";
16
+ import "./chunk-BIJO5OFS.js";
17
+
18
+ // src/nodes/core/nodes/SwitchNode.ts
19
+ var SwitchNode = class extends Node {
20
+ constructor(id, graph) {
21
+ super(id, "Switch", graph);
22
+ }
23
+ /**
24
+ * Get the index of the currently active input
25
+ * Used by connection rendering to grey out inactive wires
26
+ */
27
+ get activeInputIndex() {
28
+ const inputs = this.getVariadicInputs();
29
+ return Math.min(this.props.index?.value ?? 0, inputs.length - 1);
30
+ }
31
+ /**
32
+ * Check if a specific input port is active (for connection coloring)
33
+ */
34
+ isInputActive(portId) {
35
+ const inputs = this.getVariadicInputs();
36
+ const activeIdx = this.activeInputIndex;
37
+ const portIndex = inputs.findIndex((p) => p.id === portId);
38
+ return portIndex === activeIdx;
39
+ }
40
+ setup() {
41
+ this.setVariadic();
42
+ this.output = this.out("output");
43
+ const index = this.param("index", 0, { type: "int", min: 0, max: 0, step: 1 });
44
+ index.documentField = "props";
45
+ this.watchProp("index", () => this.update());
46
+ this.onUpdate = () => this.update();
47
+ this.onReady = () => this.update();
48
+ }
49
+ onCook() {
50
+ this.update();
51
+ }
52
+ update() {
53
+ const inputs = this.getVariadicInputs();
54
+ const connectedCount = inputs.filter((p) => p.connections.length > 0).length;
55
+ this.updatePropParams("index", { max: Math.max(0, connectedCount - 1) });
56
+ const idx = Math.min(this.props.index.value, inputs.length - 1);
57
+ const selectedInput = inputs[idx];
58
+ if (selectedInput) {
59
+ this.output.setValue(selectedInput.value);
60
+ if (this.isPreviewValue(selectedInput.value)) {
61
+ this.preview = selectedInput.value;
62
+ } else {
63
+ this.preview = null;
64
+ }
65
+ } else {
66
+ this.output.setValue(null);
67
+ this.preview = null;
68
+ }
69
+ }
70
+ };
71
+
72
+ // src/nodes/core/nodes/MergeNode.ts
73
+ var MergeNode = class extends Node {
74
+ constructor(id, graph) {
75
+ super(id, "Merge", graph);
76
+ }
77
+ setup() {
78
+ this.setVariadic();
79
+ this.output = this.out("output");
80
+ this.addParm("append", {
81
+ value: false,
82
+ type: "boolean",
83
+ displayName: "Append Mode",
84
+ hidden: () => {
85
+ const inputs = this.getVariadicInputs();
86
+ return !Array.isArray(inputs[0]?.value);
87
+ }
88
+ });
89
+ this.watchProp("append", () => this.update());
90
+ this.onUpdate = () => this.update();
91
+ this.onReady = () => this.update();
92
+ }
93
+ update() {
94
+ const inputs = this.getVariadicInputs();
95
+ const values = inputs.filter((p) => p.value !== null).map((p) => p.value);
96
+ const appendMode = this.props.append.value;
97
+ if (appendMode && values.length > 0 && Array.isArray(values[0])) {
98
+ const result = [...values[0], ...values.slice(1)];
99
+ this.output.setValue(result);
100
+ } else {
101
+ this.output.setValue(values);
102
+ }
103
+ this.preview = null;
104
+ for (const val of values) {
105
+ if (val instanceof HTMLCanvasElement || val instanceof HTMLImageElement) {
106
+ this.preview = val;
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ };
112
+
113
+ // src/nodes/core/nodes/SubnetNode.ts
114
+ var SubnetNode = class extends Node {
115
+ constructor(id, graph) {
116
+ super(id, "Subnet", graph);
117
+ }
118
+ setup() {
119
+ this.onUpdate = () => this.update();
120
+ this.onReady = () => {
121
+ this.syncPorts();
122
+ this.update();
123
+ };
124
+ }
125
+ /**
126
+ * Override: Subnets are networks and can contain children
127
+ */
128
+ isNetwork() {
129
+ return true;
130
+ }
131
+ /**
132
+ * Add a child node to this subnet
133
+ */
134
+ addChild(node) {
135
+ node.parent = this;
136
+ this._children.push(node);
137
+ if (node.type === "Input" || node.type === "Output") {
138
+ this.syncPorts();
139
+ }
140
+ }
141
+ /**
142
+ * Remove a child node from this subnet
143
+ */
144
+ removeChild(node) {
145
+ const index = this._children.indexOf(node);
146
+ if (index !== -1) {
147
+ const wasInputOutput = node.type === "Input" || node.type === "Output";
148
+ this._children.splice(index, 1);
149
+ node.parent = null;
150
+ if (wasInputOutput) {
151
+ this.syncPorts();
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * Sync input/output ports based on child Input/Output nodes
157
+ */
158
+ syncPorts() {
159
+ const inputNodes = this._children.filter((n) => n.type === "Input");
160
+ const maxInputIndex = inputNodes.reduce((max, node) => {
161
+ const idx = node.inputIndex ?? 0;
162
+ return Math.max(max, idx);
163
+ }, -1);
164
+ const outputNodes = this._children.filter((n) => n.type === "Output");
165
+ const maxOutputIndex = outputNodes.reduce((max, node) => {
166
+ const idx = node.outputIndex ?? 0;
167
+ return Math.max(max, idx);
168
+ }, -1);
169
+ const requiredInputs = maxInputIndex + 1;
170
+ const currentInputs = this.inputs.length;
171
+ for (let i = currentInputs; i < requiredInputs; i++) {
172
+ const boundary = inputNodes.find((n) => n.inputIndex === i);
173
+ const port = this.in(boundary?.inputName || `input_${i}`, null, {
174
+ type: boundary?.dataType || "any"
175
+ });
176
+ port.onChange = (value) => {
177
+ this._children.filter((n) => n.type === "Input" && n.inputIndex === i).forEach((n) => n.markDirty());
178
+ };
179
+ }
180
+ for (let i = requiredInputs; i < currentInputs; i++) {
181
+ if (this.inputs[i]) {
182
+ this.inputs[i].options = { ...this.inputs[i].options, hidden: true };
183
+ }
184
+ }
185
+ for (let i = 0; i < requiredInputs; i++) {
186
+ if (this.inputs[i]) {
187
+ const boundary = inputNodes.find((n) => n.inputIndex === i);
188
+ this.inputs[i].name = boundary?.inputName || `input_${i}`;
189
+ this.inputs[i].dataType = boundary?.dataType || "any";
190
+ this.inputs[i].options = { ...this.inputs[i].options, hidden: false };
191
+ }
192
+ }
193
+ const requiredOutputs = maxOutputIndex + 1;
194
+ const currentOutputs = this.outputs.length;
195
+ for (let i = currentOutputs; i < requiredOutputs; i++) {
196
+ const boundary = outputNodes.find((n) => n.outputIndex === i);
197
+ this.out(boundary?.outputName || `output_${i}`, "param", {
198
+ type: boundary?.dataType || "any"
199
+ });
200
+ }
201
+ for (let i = requiredOutputs; i < currentOutputs; i++) {
202
+ if (this.outputs[i]) {
203
+ this.outputs[i].options = { ...this.outputs[i].options, hidden: true };
204
+ }
205
+ }
206
+ for (let i = 0; i < requiredOutputs; i++) {
207
+ if (this.outputs[i]) {
208
+ const boundary = outputNodes.find((n) => n.outputIndex === i);
209
+ this.outputs[i].name = boundary?.outputName || `output_${i}`;
210
+ this.outputs[i].dataType = boundary?.dataType || "any";
211
+ this.outputs[i].options = { ...this.outputs[i].options, hidden: false };
212
+ }
213
+ }
214
+ this.inputs = [...this.inputs];
215
+ this.outputs = [...this.outputs];
216
+ }
217
+ /**
218
+ * Get the node that defines this subnet's output
219
+ * Priority: Output node > cooking node > last node
220
+ */
221
+ outputNode() {
222
+ const outputNode = this._children.find((n) => n.type === "Output");
223
+ if (outputNode) return outputNode;
224
+ const cookingNode = this._children.find((n) => n.cook);
225
+ if (cookingNode) return cookingNode;
226
+ return this._children.length > 0 ? this._children[this._children.length - 1] : null;
227
+ }
228
+ /**
229
+ * Get the cooking (display) node in this subnet
230
+ */
231
+ displayNode() {
232
+ return this._children.find((n) => n.cook) ?? null;
233
+ }
234
+ /**
235
+ * Get all Input nodes inside this subnet
236
+ */
237
+ indirectInputs() {
238
+ return this._children.filter((n) => n.type === "Input");
239
+ }
240
+ update() {
241
+ const outputNodes = this._children.filter((n) => n.type === "Output");
242
+ const outputByIndex = /* @__PURE__ */ new Map();
243
+ for (const node of outputNodes) {
244
+ const idx = node.outputIndex ?? 0;
245
+ outputByIndex.set(idx, node);
246
+ }
247
+ for (let i = 0; i < this.outputs.length; i++) {
248
+ const outNode = outputByIndex.get(i);
249
+ if (outNode && outNode.inputs.length > 0) {
250
+ const value = outNode.inputs[0].value;
251
+ this.outputs[i].setValue(value);
252
+ if (i === 0 && (value instanceof HTMLCanvasElement || value instanceof HTMLImageElement)) {
253
+ this.preview = value;
254
+ }
255
+ } else if (i === 0) {
256
+ const cookingNode = this._children.find((n) => n.cook);
257
+ if (cookingNode && cookingNode.outputs.length > 0) {
258
+ this.outputs[i].setValue(cookingNode.outputs[0].value);
259
+ if (cookingNode.preview) {
260
+ this.preview = cookingNode.preview;
261
+ }
262
+ } else {
263
+ this.outputs[i].setValue(null);
264
+ }
265
+ } else {
266
+ this.outputs[i].setValue(null);
267
+ }
268
+ }
269
+ if (this.outputs.length === 0 || !outputByIndex.has(0)) {
270
+ const cookingNode = this._children.find((n) => n.cook);
271
+ if (!cookingNode?.preview) {
272
+ this.preview = null;
273
+ }
274
+ }
275
+ }
276
+ };
277
+
278
+ // src/nodes/core/nodes/InputNode.ts
279
+ var TYPE_CHOICES = CORE_TYPES.map((type) => ({ value: type, label: type }));
280
+ var InputNode = class extends Node {
281
+ constructor(id, graph) {
282
+ super(id, "Input", graph);
283
+ }
284
+ setup() {
285
+ this.output = this.out("output");
286
+ this.addParm("inputIndex", {
287
+ value: 0,
288
+ type: "int",
289
+ params: { min: 0, step: 1 },
290
+ displayName: "Input Index"
291
+ });
292
+ this.addParm("inputName", {
293
+ value: "",
294
+ type: "text",
295
+ displayName: "Input Name"
296
+ });
297
+ this.addParm("dataType", {
298
+ value: "any",
299
+ type: "select",
300
+ params: { options: TYPE_CHOICES },
301
+ displayName: "Data Type"
302
+ });
303
+ const syncParent = () => {
304
+ if (this.parent && this.parent.syncPorts) {
305
+ this.parent.syncPorts();
306
+ }
307
+ this.update();
308
+ };
309
+ this.watchProp("inputIndex", syncParent);
310
+ this.watchProp("inputName", syncParent);
311
+ this.watchProp("dataType", () => {
312
+ this.output.dataType = this.dataType;
313
+ syncParent();
314
+ });
315
+ this.onUpdate = () => this.update();
316
+ this.onReady = () => this.update();
317
+ }
318
+ /**
319
+ * Get the input index this node represents
320
+ */
321
+ get inputIndex() {
322
+ return this.props.inputIndex?.value ?? 0;
323
+ }
324
+ /**
325
+ * Get the input name (for documentation)
326
+ */
327
+ get inputName() {
328
+ return this.props.inputName?.value ?? "";
329
+ }
330
+ get dataType() {
331
+ const value = this.props.dataType?.value;
332
+ return typeof value === "string" && isCoreType(value) ? value : "any";
333
+ }
334
+ update() {
335
+ if (this.parent && this.parent.inputs.length > 0) {
336
+ const idx = this.inputIndex;
337
+ const parentInput = this.parent.inputs[idx];
338
+ if (parentInput) {
339
+ this.output.setValue(parentInput.value);
340
+ if (this.isPreviewValue(parentInput.value)) {
341
+ this.preview = parentInput.value;
342
+ } else {
343
+ this.preview = null;
344
+ }
345
+ return;
346
+ }
347
+ }
348
+ this.output.setValue(null);
349
+ this.preview = null;
350
+ }
351
+ };
352
+
353
+ // src/nodes/core/nodes/OutputNode.ts
354
+ var TYPE_CHOICES2 = CORE_TYPES.map((type) => ({ value: type, label: type }));
355
+ var OutputNode = class extends Node {
356
+ constructor(id, graph) {
357
+ super(id, "Output", graph);
358
+ }
359
+ setup() {
360
+ this.in("input", null);
361
+ this.addParm("outputIndex", {
362
+ value: 0,
363
+ type: "int",
364
+ params: { min: 0, step: 1 },
365
+ displayName: "Output Index"
366
+ });
367
+ this.addParm("outputName", {
368
+ value: "",
369
+ type: "text",
370
+ displayName: "Output Name"
371
+ });
372
+ this.addParm("dataType", {
373
+ value: "any",
374
+ type: "select",
375
+ params: { options: TYPE_CHOICES2 },
376
+ displayName: "Data Type"
377
+ });
378
+ const syncParent = () => {
379
+ if (this.parent && this.parent.syncPorts) {
380
+ this.parent.syncPorts();
381
+ }
382
+ this.update();
383
+ };
384
+ this.watchProp("outputIndex", syncParent);
385
+ this.watchProp("outputName", syncParent);
386
+ this.watchProp("dataType", () => {
387
+ if (this.inputs[0]) this.inputs[0].dataType = this.dataType;
388
+ syncParent();
389
+ });
390
+ this.onUpdate = () => this.update();
391
+ this.onReady = () => this.update();
392
+ }
393
+ /**
394
+ * Get the output index this node represents
395
+ */
396
+ get outputIndex() {
397
+ return this.props.outputIndex?.value ?? 0;
398
+ }
399
+ get outputName() {
400
+ return this.props.outputName?.value ?? "";
401
+ }
402
+ get dataType() {
403
+ const value = this.props.dataType?.value;
404
+ return typeof value === "string" && isCoreType(value) ? value : "any";
405
+ }
406
+ update() {
407
+ const input = this.inputs[0];
408
+ if (input) {
409
+ if (this.isPreviewValue(input.value)) {
410
+ this.preview = input.value;
411
+ } else {
412
+ this.preview = null;
413
+ }
414
+ }
415
+ }
416
+ };
417
+
418
+ // src/nodes/core/nodes/RandomNode.ts
419
+ var RandomNode = class extends Node {
420
+ constructor(id, graph) {
421
+ super(id, "Random", graph);
422
+ }
423
+ setup() {
424
+ const seed = this.in("seed", 0, { type: "int" });
425
+ const sample = this.in("sample", 0, { type: "int" });
426
+ const value = this.out("value");
427
+ const update = () => value.setValue(randomFromSeed(seed.value, sample.value));
428
+ seed.onChange = update;
429
+ sample.onChange = update;
430
+ this.onUpdate = update;
431
+ this.onReady = update;
432
+ }
433
+ };
434
+
435
+ // src/nodes/core/nodes/RemapNode.ts
436
+ var RemapNode = class extends Node {
437
+ constructor(id, graph) {
438
+ super(id, "Remap", graph);
439
+ }
440
+ setup() {
441
+ const value = this.in("value", 0, { type: "float" });
442
+ const inMin = this.in("inMin", 0, { type: "float" });
443
+ const inMax = this.in("inMax", 1, { type: "float" });
444
+ const outMin = this.in("outMin", 0, { type: "float" });
445
+ const outMax = this.in("outMax", 1, { type: "float" });
446
+ const result = this.out("result");
447
+ this.addParm("clamp", { value: false, type: "boolean", displayName: "Clamp" });
448
+ const update = () => result.setValue(remap(
449
+ value.value,
450
+ inMin.value,
451
+ inMax.value,
452
+ outMin.value,
453
+ outMax.value,
454
+ this.props.clamp.value
455
+ ));
456
+ for (const input of [value, inMin, inMax, outMin, outMax]) input.onChange = update;
457
+ this.watchProp("clamp", update);
458
+ this.onUpdate = update;
459
+ this.onReady = update;
460
+ }
461
+ };
462
+
463
+ // src/nodes/core/nodes/SelectNode.ts
464
+ var SelectNode = class extends Node {
465
+ constructor(id, graph) {
466
+ super(id, "Select", graph);
467
+ }
468
+ setup() {
469
+ this.arrayInput = this.in("array", [], { type: "array" });
470
+ this.itemOutput = this.out("item", "param");
471
+ this.countOutput = this.out("count", "param");
472
+ this.addParm("index", {
473
+ value: 0,
474
+ type: "int",
475
+ params: { min: 0, max: 0, step: 1 },
476
+ displayName: "Index"
477
+ });
478
+ this.addParm("wrap", {
479
+ value: true,
480
+ type: "boolean",
481
+ displayName: "Wrap Index"
482
+ });
483
+ this.watchProp("index", () => this.update());
484
+ this.watchProp("wrap", () => this.update());
485
+ this.onUpdate = () => this.update();
486
+ this.onReady = () => this.update();
487
+ }
488
+ update() {
489
+ const array = this.arrayInput.value;
490
+ if (!Array.isArray(array) || array.length === 0) {
491
+ this.itemOutput.setValue(null);
492
+ this.countOutput.setValue(0);
493
+ this.updatePropParams("index", { max: 0 });
494
+ return;
495
+ }
496
+ this.updatePropParams("index", { max: array.length - 1 });
497
+ let index = this.props.index?.value ?? 0;
498
+ const wrap = this.props.wrap?.value ?? true;
499
+ if (wrap && array.length > 0) {
500
+ index = (index % array.length + array.length) % array.length;
501
+ } else {
502
+ index = Math.max(0, Math.min(index, array.length - 1));
503
+ }
504
+ const item = array[index];
505
+ this.itemOutput.setValue(item);
506
+ this.countOutput.setValue(array.length);
507
+ if (item instanceof HTMLCanvasElement || item instanceof HTMLImageElement) {
508
+ this.preview = item;
509
+ } else {
510
+ this.preview = null;
511
+ }
512
+ }
513
+ };
514
+
515
+ // src/nodes/core/nodes/NullNode.ts
516
+ var NullNode = class extends Node {
517
+ constructor(id, graph) {
518
+ super(id, "Null", graph);
519
+ }
520
+ setup() {
521
+ const input = this.in("input", null, { type: "any" });
522
+ const output = this.out("output");
523
+ const update = () => output.setValue(input.value);
524
+ input.onChange = update;
525
+ this.onUpdate = update;
526
+ this.onReady = update;
527
+ }
528
+ };
529
+
530
+ // src/nodes/image/surface.ts
531
+ var globals = () => globalThis;
532
+ var MissingRenderSurfaceError = class extends Error {
533
+ constructor(kind) {
534
+ super(
535
+ `Cascade has no ${kind} in this environment. Install the optional '@napi-rs/canvas' renderer before cooking a graph that draws.`
536
+ );
537
+ this.name = "MissingRenderSurfaceError";
538
+ }
539
+ };
540
+ function provider() {
541
+ const host = globals();
542
+ if (typeof host.document?.createElement === "function") return domProvider;
543
+ if (typeof host.OffscreenCanvas === "function") return offscreenProvider;
544
+ return null;
545
+ }
546
+ function surfaceAvailable() {
547
+ return provider() !== null;
548
+ }
549
+ function createSurface(width, height) {
550
+ const current = provider();
551
+ if (!current) throw new MissingRenderSurfaceError("drawing surface");
552
+ return current.createSurface(width, height);
553
+ }
554
+ function decodeImage(source) {
555
+ const current = provider();
556
+ if (!current) throw new MissingRenderSurfaceError("image decoder");
557
+ return current.decodeImage(source);
558
+ }
559
+ function bytesOf(source) {
560
+ return source instanceof Uint8Array ? source : new Uint8Array(source);
561
+ }
562
+ async function decodeViaBitmap(source) {
563
+ const host = globals();
564
+ if (typeof host.createImageBitmap !== "function" || typeof source === "string") return null;
565
+ const input = source instanceof Uint8Array || source instanceof ArrayBuffer ? typeof host.Blob === "function" ? new host.Blob([bytesOf(source)]) : bytesOf(source) : source;
566
+ return host.createImageBitmap(input);
567
+ }
568
+ async function decodeViaImage(source) {
569
+ const host = globals();
570
+ if (typeof host.Image !== "function") throw new MissingRenderSurfaceError("image decoder");
571
+ const image = new host.Image();
572
+ if (typeof source === "string") {
573
+ if (!/^(data:|blob:)/.test(source)) image.crossOrigin = "anonymous";
574
+ image.src = source;
575
+ } else if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
576
+ image.src = bytesOf(source);
577
+ } else {
578
+ image.src = new Uint8Array(await source.arrayBuffer());
579
+ }
580
+ await image.decode();
581
+ if (!image.width || !image.height) throw new Error("cascade: could not decode image data");
582
+ return image;
583
+ }
584
+ async function decode(source) {
585
+ if (typeof source !== "string") {
586
+ const bitmap = await decodeViaBitmap(source);
587
+ if (bitmap) return bitmap;
588
+ }
589
+ return decodeViaImage(source);
590
+ }
591
+ var domProvider = {
592
+ name: "DOM canvas",
593
+ createSurface(width, height) {
594
+ const surface = globals().document.createElement("canvas");
595
+ surface.width = width;
596
+ surface.height = height;
597
+ return surface;
598
+ },
599
+ decodeImage: decode
600
+ };
601
+ var offscreenProvider = {
602
+ name: "OffscreenCanvas",
603
+ createSurface(width, height) {
604
+ const Offscreen = globals().OffscreenCanvas;
605
+ return new Offscreen(width, height);
606
+ },
607
+ decodeImage: decode
608
+ };
609
+
610
+ // src/nodes/image/ImageBuffer.ts
611
+ function isSurface(value) {
612
+ return typeof value === "object" && value !== null && typeof value.getContext === "function";
613
+ }
614
+ function hasPixels(value) {
615
+ if (typeof value !== "object" || value === null) return false;
616
+ const candidate = value;
617
+ if (typeof candidate.width !== "number" || typeof candidate.height !== "number") return false;
618
+ return typeof candidate.getContext === "function" || "src" in candidate || typeof candidate.close === "function";
619
+ }
620
+ function sourceSize(source) {
621
+ const natural = source;
622
+ return {
623
+ width: natural.naturalWidth || source.width,
624
+ height: natural.naturalHeight || source.height
625
+ };
626
+ }
627
+ function pixelCount(width, height) {
628
+ const count = width * height;
629
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1 || !Number.isSafeInteger(count)) {
630
+ throw new RangeError(`Image dimensions must be positive safe integers; received ${width}\xD7${height}`);
631
+ }
632
+ return count;
633
+ }
634
+ var ImageBuffer = class _ImageBuffer {
635
+ constructor(width, height, channels, colorSpace = "srgb") {
636
+ // Cached data for display - invalidated when data changes
637
+ this._canvas = null;
638
+ this._imageData = null;
639
+ this._dirty = true;
640
+ pixelCount(width, height);
641
+ this.width = width;
642
+ this.height = height;
643
+ this.channels = channels;
644
+ this.colorSpace = colorSpace;
645
+ this._memoryBytes = channels.reduce((sum, ch) => sum + ch.byteLength, 0);
646
+ }
647
+ /**
648
+ * Get memory usage of this buffer in bytes
649
+ */
650
+ get memoryBytes() {
651
+ return this._memoryBytes;
652
+ }
653
+ /**
654
+ * Get memory usage in human-readable format
655
+ */
656
+ get memorySizeStr() {
657
+ const bytes = this._memoryBytes;
658
+ if (bytes < 1024) return `${bytes}B`;
659
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
660
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
661
+ }
662
+ // ============ Factory Methods ============
663
+ /**
664
+ * Create a grayscale (single channel) buffer
665
+ */
666
+ static grayscale(width, height) {
667
+ const size = pixelCount(width, height);
668
+ return new _ImageBuffer(width, height, [new Float32Array(size)]);
669
+ }
670
+ /**
671
+ * Create an RGB (3 channel) buffer
672
+ */
673
+ static rgb(width, height) {
674
+ const size = pixelCount(width, height);
675
+ return new _ImageBuffer(width, height, [
676
+ new Float32Array(size),
677
+ new Float32Array(size),
678
+ new Float32Array(size)
679
+ ]);
680
+ }
681
+ /**
682
+ * Create an RGBA (4 channel) buffer
683
+ */
684
+ static rgba(width, height) {
685
+ const size = pixelCount(width, height);
686
+ return new _ImageBuffer(width, height, [
687
+ new Float32Array(size),
688
+ new Float32Array(size),
689
+ new Float32Array(size),
690
+ new Float32Array(size)
691
+ ]);
692
+ }
693
+ /** Create from a browser or headless canvas/decoded image. */
694
+ static fromCanvas(source) {
695
+ if (isSurface(source)) {
696
+ const ctx2 = source.getContext("2d");
697
+ if (!ctx2) throw new Error("cascade: canvas has no 2D context");
698
+ return _ImageBuffer.fromImageData(ctx2.getImageData(0, 0, source.width, source.height));
699
+ }
700
+ const { width, height } = sourceSize(source);
701
+ const surface = createSurface(width, height);
702
+ const ctx = surface.getContext("2d");
703
+ if (!ctx) throw new Error("cascade: canvas has no 2D context");
704
+ ctx.drawImage(source, 0, 0);
705
+ return _ImageBuffer.fromImageData(ctx.getImageData(0, 0, width, height));
706
+ }
707
+ /**
708
+ * Create from ImageData (interleaved RGBA Uint8)
709
+ */
710
+ static fromImageData(imageData) {
711
+ const { width, height, data } = imageData;
712
+ const size = width * height;
713
+ const r = new Float32Array(size);
714
+ const g = new Float32Array(size);
715
+ const b = new Float32Array(size);
716
+ const a = new Float32Array(size);
717
+ const inv255 = 1 / 255;
718
+ for (let i = 0; i < size; i++) {
719
+ const idx = i * 4;
720
+ r[i] = data[idx] * inv255;
721
+ g[i] = data[idx + 1] * inv255;
722
+ b[i] = data[idx + 2] * inv255;
723
+ a[i] = data[idx + 3] * inv255;
724
+ }
725
+ return new _ImageBuffer(width, height, [r, g, b, a]);
726
+ }
727
+ /**
728
+ * Create a filled solid color buffer
729
+ */
730
+ static solid(width, height, color) {
731
+ const buffer = _ImageBuffer.rgba(width, height);
732
+ buffer.fill(0, color.r);
733
+ buffer.fill(1, color.g);
734
+ buffer.fill(2, color.b);
735
+ buffer.fill(3, color.a ?? 1);
736
+ return buffer;
737
+ }
738
+ // ============ Properties ============
739
+ get channelCount() {
740
+ return this.channels.length;
741
+ }
742
+ get layout() {
743
+ switch (this.channels.length) {
744
+ case 1:
745
+ return "gray";
746
+ case 3:
747
+ return "rgb";
748
+ default:
749
+ return "rgba";
750
+ }
751
+ }
752
+ get pixelCount() {
753
+ return this.width * this.height;
754
+ }
755
+ // ============ Channel Access ============
756
+ /**
757
+ * Get a channel by index (0=R/Gray, 1=G, 2=B, 3=A)
758
+ */
759
+ getChannel(index) {
760
+ if (index < 0 || index >= this.channels.length) {
761
+ throw new Error(`Channel index ${index} out of bounds (0-${this.channels.length - 1})`);
762
+ }
763
+ return this.channels[index];
764
+ }
765
+ /**
766
+ * Get channel by name
767
+ */
768
+ r() {
769
+ return this.channels[0];
770
+ }
771
+ g() {
772
+ return this.channels[Math.min(1, this.channels.length - 1)];
773
+ }
774
+ b() {
775
+ return this.channels[Math.min(2, this.channels.length - 1)];
776
+ }
777
+ a() {
778
+ return this.channels.length >= 4 ? this.channels[3] : this.createOpaqueAlpha();
779
+ }
780
+ createOpaqueAlpha() {
781
+ const alpha = new Float32Array(this.pixelCount);
782
+ alpha.fill(1);
783
+ return alpha;
784
+ }
785
+ // ============ Pixel Access ============
786
+ /**
787
+ * Get pixel index from coordinates
788
+ */
789
+ index(x, y) {
790
+ return y * this.width + x;
791
+ }
792
+ /**
793
+ * Get all channel values at a pixel
794
+ */
795
+ getPixel(x, y) {
796
+ const idx = this.index(x, y);
797
+ return this.channels.map((ch) => ch[idx]);
798
+ }
799
+ /**
800
+ * Set all channel values at a pixel
801
+ */
802
+ setPixel(x, y, values) {
803
+ const idx = this.index(x, y);
804
+ for (let c = 0; c < Math.min(values.length, this.channels.length); c++) {
805
+ this.channels[c][idx] = values[c];
806
+ }
807
+ this._dirty = true;
808
+ }
809
+ /**
810
+ * Get a single channel value at a pixel
811
+ */
812
+ getValue(x, y, channel = 0) {
813
+ return this.channels[channel][this.index(x, y)];
814
+ }
815
+ /**
816
+ * Set a single channel value at a pixel
817
+ */
818
+ setValue(x, y, channel, value) {
819
+ this.channels[channel][this.index(x, y)] = value;
820
+ this._dirty = true;
821
+ }
822
+ // ============ Bulk Operations ============
823
+ /**
824
+ * Fill a channel with a constant value
825
+ */
826
+ fill(channel, value) {
827
+ this.channels[channel].fill(value);
828
+ this._dirty = true;
829
+ }
830
+ /**
831
+ * Fill all channels
832
+ */
833
+ fillAll(values) {
834
+ for (let c = 0; c < this.channels.length; c++) {
835
+ this.channels[c].fill(values[c] ?? 0);
836
+ }
837
+ this._dirty = true;
838
+ }
839
+ /**
840
+ * Create a deep copy
841
+ */
842
+ clone() {
843
+ const newChannels = this.channels.map((ch) => new Float32Array(ch));
844
+ return new _ImageBuffer(this.width, this.height, newChannels, this.colorSpace);
845
+ }
846
+ /**
847
+ * Create a copy with different dimensions (doesn't resize data)
848
+ */
849
+ emptyClone() {
850
+ return new _ImageBuffer(
851
+ this.width,
852
+ this.height,
853
+ this.channels.map(() => new Float32Array(this.pixelCount)),
854
+ this.colorSpace
855
+ );
856
+ }
857
+ /**
858
+ * Mark buffer as modified (invalidates canvas cache)
859
+ */
860
+ markDirty() {
861
+ this._dirty = true;
862
+ }
863
+ // ============ Conversion ============
864
+ /**
865
+ * Convert to grayscale (luminance)
866
+ */
867
+ toGrayscale() {
868
+ if (this.channels.length === 1) {
869
+ return this.clone();
870
+ }
871
+ const result = _ImageBuffer.grayscale(this.width, this.height);
872
+ const gray = result.channels[0];
873
+ const r = this.channels[0];
874
+ const g = this.channels[1];
875
+ const b = this.channels[2];
876
+ for (let i = 0; i < this.pixelCount; i++) {
877
+ gray[i] = 0.2126 * r[i] + 0.7152 * g[i] + 0.0722 * b[i];
878
+ }
879
+ return result;
880
+ }
881
+ /**
882
+ * Convert grayscale to RGBA
883
+ */
884
+ toRGBA() {
885
+ if (this.channels.length >= 4) {
886
+ return this.clone();
887
+ }
888
+ const result = _ImageBuffer.rgba(this.width, this.height);
889
+ if (this.channels.length === 1) {
890
+ const gray = this.channels[0];
891
+ result.channels[0].set(gray);
892
+ result.channels[1].set(gray);
893
+ result.channels[2].set(gray);
894
+ result.channels[3].fill(1);
895
+ } else if (this.channels.length === 3) {
896
+ result.channels[0].set(this.channels[0]);
897
+ result.channels[1].set(this.channels[1]);
898
+ result.channels[2].set(this.channels[2]);
899
+ result.channels[3].fill(1);
900
+ }
901
+ return result;
902
+ }
903
+ /**
904
+ * Convert to ImageData (for canvas rendering)
905
+ * Cached - only rebuilds when dirty
906
+ */
907
+ toImageData() {
908
+ if (this._imageData && !this._dirty) {
909
+ return this._imageData;
910
+ }
911
+ const imageData = new ImageData(this.width, this.height);
912
+ const data = imageData.data;
913
+ const size = this.pixelCount;
914
+ const r = this.channels[0];
915
+ const g = this.channels.length > 1 ? this.channels[1] : r;
916
+ const b = this.channels.length > 2 ? this.channels[2] : r;
917
+ const hasAlpha = this.channels.length > 3;
918
+ const a = hasAlpha ? this.channels[3] : null;
919
+ for (let i = 0; i < size; i++) {
920
+ const idx = i << 2;
921
+ let v;
922
+ v = r[i] * 255 + 0.5 | 0;
923
+ data[idx] = v < 0 ? 0 : v > 255 ? 255 : v;
924
+ v = g[i] * 255 + 0.5 | 0;
925
+ data[idx + 1] = v < 0 ? 0 : v > 255 ? 255 : v;
926
+ v = b[i] * 255 + 0.5 | 0;
927
+ data[idx + 2] = v < 0 ? 0 : v > 255 ? 255 : v;
928
+ if (hasAlpha) {
929
+ v = a[i] * 255 + 0.5 | 0;
930
+ data[idx + 3] = v < 0 ? 0 : v > 255 ? 255 : v;
931
+ } else {
932
+ data[idx + 3] = 255;
933
+ }
934
+ }
935
+ this._imageData = imageData;
936
+ return imageData;
937
+ }
938
+ /** Convert to the current host's cached drawing surface. */
939
+ toSurface() {
940
+ if (this._canvas && !this._dirty) {
941
+ return this._canvas;
942
+ }
943
+ if (!this._canvas) {
944
+ this._canvas = createSurface(this.width, this.height);
945
+ }
946
+ const ctx = this._canvas.getContext("2d");
947
+ if (!ctx) throw new Error("cascade: canvas has no 2D context");
948
+ ctx.putImageData(this.toImageData(), 0, 0);
949
+ this._dirty = false;
950
+ return this._canvas;
951
+ }
952
+ /** Browser-facing alias retained for compatibility with Studio preview code. */
953
+ toCanvas() {
954
+ return this.toSurface();
955
+ }
956
+ // ============ Utility ============
957
+ /**
958
+ * Check if coordinates are within bounds
959
+ */
960
+ inBounds(x, y) {
961
+ return x >= 0 && x < this.width && y >= 0 && y < this.height;
962
+ }
963
+ /**
964
+ * Get coordinate with edge handling
965
+ */
966
+ wrapCoord(coord, max, wrap) {
967
+ if (wrap) {
968
+ return (coord % max + max) % max;
969
+ }
970
+ return Math.max(0, Math.min(max - 1, coord));
971
+ }
972
+ /**
973
+ * Sample with bilinear interpolation
974
+ */
975
+ sample(x, y, channel = 0, wrap = false) {
976
+ const x0 = Math.floor(x);
977
+ const y0 = Math.floor(y);
978
+ const x1 = x0 + 1;
979
+ const y1 = y0 + 1;
980
+ const fx = x - x0;
981
+ const fy = y - y0;
982
+ const ch = this.channels[channel];
983
+ const w = this.width;
984
+ const h = this.height;
985
+ const px0 = this.wrapCoord(x0, w, wrap);
986
+ const px1 = this.wrapCoord(x1, w, wrap);
987
+ const py0 = this.wrapCoord(y0, h, wrap);
988
+ const py1 = this.wrapCoord(y1, h, wrap);
989
+ const v00 = ch[py0 * w + px0];
990
+ const v10 = ch[py0 * w + px1];
991
+ const v01 = ch[py1 * w + px0];
992
+ const v11 = ch[py1 * w + px1];
993
+ const v0 = v00 * (1 - fx) + v10 * fx;
994
+ const v1 = v01 * (1 - fx) + v11 * fx;
995
+ return v0 * (1 - fy) + v1 * fy;
996
+ }
997
+ /**
998
+ * Sample all channels with bilinear interpolation
999
+ */
1000
+ samplePixel(x, y, wrap = false) {
1001
+ return this.channels.map((_, c) => this.sample(x, y, c, wrap));
1002
+ }
1003
+ };
1004
+
1005
+ // src/nodes/core/nodes/FreezeNode.ts
1006
+ function previewWanted() {
1007
+ return typeof document !== "undefined";
1008
+ }
1009
+ var FreezeNode = class extends Node {
1010
+ constructor(id, graph) {
1011
+ super(id, "Freeze", graph);
1012
+ this.frozenValue = null;
1013
+ this.hasCaptured = false;
1014
+ }
1015
+ setup() {
1016
+ this.input = this.in("input", null, { type: "any" });
1017
+ this.output = this.out("output", "param");
1018
+ this.addParm("frozen", {
1019
+ value: false,
1020
+ type: "boolean",
1021
+ displayName: "Frozen"
1022
+ });
1023
+ this.addAction("capture", {
1024
+ label: "Capture",
1025
+ icon: "Camera",
1026
+ callback: () => this.capture()
1027
+ });
1028
+ this.addAction("clear", {
1029
+ label: "Clear",
1030
+ icon: "Trash2",
1031
+ callback: () => this.clear()
1032
+ });
1033
+ this.watchProp("frozen", () => this.update());
1034
+ this.onUpdate = () => this.update();
1035
+ this.onReady = () => this.update();
1036
+ }
1037
+ /**
1038
+ * Capture the current input value
1039
+ */
1040
+ capture() {
1041
+ this.frozenValue = this.deepClone(this.input.value);
1042
+ this.hasCaptured = true;
1043
+ this.setParm("frozen", true);
1044
+ this.update();
1045
+ }
1046
+ /**
1047
+ * Clear the frozen value
1048
+ */
1049
+ clear() {
1050
+ this.frozenValue = null;
1051
+ this.hasCaptured = false;
1052
+ this.setParm("frozen", false);
1053
+ this.update();
1054
+ }
1055
+ /**
1056
+ * Deep clone a value if possible
1057
+ */
1058
+ deepClone(value) {
1059
+ if (value === null || value === void 0) {
1060
+ return value;
1061
+ }
1062
+ if (hasPixels(value)) {
1063
+ const source = value;
1064
+ const width = source.naturalWidth || source.width;
1065
+ const height = source.naturalHeight || source.height;
1066
+ const canvas = createSurface(width, height);
1067
+ const ctx = canvas.getContext("2d");
1068
+ ctx?.drawImage(value, 0, 0);
1069
+ return canvas;
1070
+ }
1071
+ if (Array.isArray(value)) {
1072
+ return value.map((item) => this.deepClone(item));
1073
+ }
1074
+ if (typeof value === "object") {
1075
+ try {
1076
+ return JSON.parse(JSON.stringify(value));
1077
+ } catch {
1078
+ return value;
1079
+ }
1080
+ }
1081
+ return value;
1082
+ }
1083
+ update() {
1084
+ const isFrozen = this.props.frozen?.value ?? false;
1085
+ if (isFrozen && this.hasCaptured) {
1086
+ this.output.setValue(this.frozenValue);
1087
+ if (previewWanted() && hasPixels(this.frozenValue)) {
1088
+ this.preview = this.frozenValue;
1089
+ }
1090
+ } else {
1091
+ this.output.setValue(this.input.value);
1092
+ if (previewWanted() && hasPixels(this.input.value)) {
1093
+ this.preview = this.input.value;
1094
+ } else {
1095
+ this.preview = null;
1096
+ }
1097
+ }
1098
+ }
1099
+ /**
1100
+ * Serialize with frozen state
1101
+ */
1102
+ serialize() {
1103
+ return {
1104
+ hasCaptured: this.hasCaptured
1105
+ };
1106
+ }
1107
+ /**
1108
+ * Deserialize and restore frozen state
1109
+ */
1110
+ deserialize(data) {
1111
+ this.hasCaptured = data.hasCaptured === true;
1112
+ }
1113
+ };
1114
+
1115
+ // src/nodes/core/classes.ts
1116
+ var coreNodeClasses = {
1117
+ "Switch": SwitchNode,
1118
+ "Merge": MergeNode,
1119
+ "Subnet": SubnetNode,
1120
+ "Input": InputNode,
1121
+ "Output": OutputNode,
1122
+ "Random": RandomNode,
1123
+ "Remap": RemapNode,
1124
+ "Select": SelectNode,
1125
+ "Null": NullNode,
1126
+ "Freeze": FreezeNode
1127
+ };
1128
+
1129
+ // src/nodes/image/ImageNodeBase.ts
1130
+ function previewWanted2() {
1131
+ return typeof document !== "undefined" && surfaceAvailable();
1132
+ }
1133
+ var ImageNodeBase = class _ImageNodeBase extends Node {
1134
+ // ~30fps
1135
+ constructor(id, type, graph) {
1136
+ super(id, type, graph);
1137
+ // Preview update throttling
1138
+ this._previewPending = false;
1139
+ this._previewBuffer = null;
1140
+ this._lastPreviewTime = 0;
1141
+ }
1142
+ static {
1143
+ this.PREVIEW_INTERVAL = 33;
1144
+ }
1145
+ /**
1146
+ * Override onCook() to call the abstract render() method
1147
+ * Cook timing is handled by Node.execute()
1148
+ */
1149
+ onCook() {
1150
+ return this.render();
1151
+ }
1152
+ /**
1153
+ * Helper for onChange handlers - marks dirty for lazy evaluation
1154
+ * Execution is deferred until output is requested by a consumer (Viewer, etc.)
1155
+ */
1156
+ requestCook() {
1157
+ this.markDirty();
1158
+ }
1159
+ // ============ ImageBuffer Utilities ============
1160
+ /**
1161
+ * Convert any image input to ImageBuffer
1162
+ * Returns null if input is null/invalid
1163
+ */
1164
+ toImageBuffer(input) {
1165
+ if (!input) return null;
1166
+ if (input instanceof ImageBuffer) {
1167
+ return input;
1168
+ }
1169
+ if (hasPixels(input)) {
1170
+ return ImageBuffer.fromCanvas(input);
1171
+ }
1172
+ return null;
1173
+ }
1174
+ /**
1175
+ * Get dimensions from any image input
1176
+ */
1177
+ getImageSize(input) {
1178
+ if (!input) return { width: 0, height: 0 };
1179
+ if (input instanceof ImageBuffer) {
1180
+ return { width: input.width, height: input.height };
1181
+ }
1182
+ if (hasPixels(input)) {
1183
+ const natural = input;
1184
+ return {
1185
+ width: natural.naturalWidth || input.width,
1186
+ height: natural.naturalHeight || input.height
1187
+ };
1188
+ }
1189
+ return { width: 0, height: 0 };
1190
+ }
1191
+ // ============ Resolution Utilities ============
1192
+ /**
1193
+ * Add the standard resolution parameter used by generator nodes
1194
+ * Call this in setup() to add a resolution parameter with automatic cooking
1195
+ */
1196
+ addResolutionParm(defaultWidth = 512, defaultHeight = 512) {
1197
+ this.addParm("resolution", {
1198
+ value: [defaultWidth, defaultHeight],
1199
+ params: {
1200
+ min: [1, 1],
1201
+ max: [4096, 4096],
1202
+ integer: true
1203
+ },
1204
+ displayName: "Resolution",
1205
+ onChange: () => this.requestCook()
1206
+ });
1207
+ }
1208
+ /**
1209
+ * Get the current resolution from the resolution parameter
1210
+ * Returns [width, height] tuple
1211
+ * Requires addResolutionParm() to have been called in setup()
1212
+ */
1213
+ getResolution() {
1214
+ if (!this.props.resolution) {
1215
+ console.warn(`${this.type}: getResolution() called but no resolution parameter exists. Call addResolutionParm() in setup().`);
1216
+ return [512, 512];
1217
+ }
1218
+ return this.props.resolution.value;
1219
+ }
1220
+ /**
1221
+ * Determine output resolution based on mode and inputs
1222
+ */
1223
+ resolveOutputSize(mode, inputs, customSize) {
1224
+ const validInputs = inputs.filter((b) => b !== null);
1225
+ switch (mode) {
1226
+ case "input":
1227
+ case "input1":
1228
+ if (validInputs[0]) {
1229
+ return { width: validInputs[0].width, height: validInputs[0].height };
1230
+ }
1231
+ break;
1232
+ case "input2":
1233
+ if (validInputs[1]) {
1234
+ return { width: validInputs[1].width, height: validInputs[1].height };
1235
+ } else if (validInputs[0]) {
1236
+ return { width: validInputs[0].width, height: validInputs[0].height };
1237
+ }
1238
+ break;
1239
+ case "largest":
1240
+ if (validInputs.length > 0) {
1241
+ let maxArea = 0;
1242
+ let largest = validInputs[0];
1243
+ for (const buf of validInputs) {
1244
+ const area = buf.width * buf.height;
1245
+ if (area > maxArea) {
1246
+ maxArea = area;
1247
+ largest = buf;
1248
+ }
1249
+ }
1250
+ return { width: largest.width, height: largest.height };
1251
+ }
1252
+ break;
1253
+ case "smallest":
1254
+ if (validInputs.length > 0) {
1255
+ let minArea = Infinity;
1256
+ let smallest = validInputs[0];
1257
+ for (const buf of validInputs) {
1258
+ const area = buf.width * buf.height;
1259
+ if (area < minArea) {
1260
+ minArea = area;
1261
+ smallest = buf;
1262
+ }
1263
+ }
1264
+ return { width: smallest.width, height: smallest.height };
1265
+ }
1266
+ break;
1267
+ case "custom":
1268
+ if (customSize) {
1269
+ return { width: customSize[0], height: customSize[1] };
1270
+ }
1271
+ break;
1272
+ }
1273
+ if (validInputs[0]) {
1274
+ return { width: validInputs[0].width, height: validInputs[0].height };
1275
+ }
1276
+ return { width: 512, height: 512 };
1277
+ }
1278
+ /**
1279
+ * Resize a buffer to fit target dimensions using the specified fit mode
1280
+ *
1281
+ * Fit modes:
1282
+ * - 'fill': Scale to fill entire target, cropping if necessary (no letterboxing)
1283
+ * - 'fit': Scale to fit within target, maintaining aspect ratio (may letterbox)
1284
+ * - 'stretch': Stretch to exactly match target dimensions (may distort)
1285
+ * - 'native': Use original size, centered in target (may crop or have empty space)
1286
+ */
1287
+ resizeToFit(source, targetWidth, targetHeight, fitMode = "fill") {
1288
+ if (source.width === targetWidth && source.height === targetHeight) {
1289
+ return source;
1290
+ }
1291
+ const result = ImageBuffer.rgba(targetWidth, targetHeight);
1292
+ const srcAspect = source.width / source.height;
1293
+ const dstAspect = targetWidth / targetHeight;
1294
+ let srcX = 0, srcY = 0, srcW = source.width, srcH = source.height;
1295
+ let dstX = 0, dstY = 0, dstW = targetWidth, dstH = targetHeight;
1296
+ switch (fitMode) {
1297
+ case "fill":
1298
+ if (srcAspect > dstAspect) {
1299
+ srcW = Math.round(source.height * dstAspect);
1300
+ srcX = Math.round((source.width - srcW) / 2);
1301
+ } else {
1302
+ srcH = Math.round(source.width / dstAspect);
1303
+ srcY = Math.round((source.height - srcH) / 2);
1304
+ }
1305
+ break;
1306
+ case "fit":
1307
+ if (srcAspect > dstAspect) {
1308
+ dstH = Math.round(targetWidth / srcAspect);
1309
+ dstY = Math.round((targetHeight - dstH) / 2);
1310
+ } else {
1311
+ dstW = Math.round(targetHeight * srcAspect);
1312
+ dstX = Math.round((targetWidth - dstW) / 2);
1313
+ }
1314
+ result.fill(0, 0);
1315
+ result.fill(1, 0);
1316
+ result.fill(2, 0);
1317
+ result.fill(3, 1);
1318
+ break;
1319
+ case "stretch":
1320
+ break;
1321
+ case "native":
1322
+ dstW = Math.min(source.width, targetWidth);
1323
+ dstH = Math.min(source.height, targetHeight);
1324
+ srcW = dstW;
1325
+ srcH = dstH;
1326
+ dstX = Math.round((targetWidth - dstW) / 2);
1327
+ dstY = Math.round((targetHeight - dstH) / 2);
1328
+ srcX = Math.round((source.width - srcW) / 2);
1329
+ srcY = Math.round((source.height - srcH) / 2);
1330
+ result.fill(0, 0);
1331
+ result.fill(1, 0);
1332
+ result.fill(2, 0);
1333
+ result.fill(3, 1);
1334
+ break;
1335
+ }
1336
+ const scaleX = srcW / dstW;
1337
+ const scaleY = srcH / dstH;
1338
+ for (let c = 0; c < Math.min(source.channelCount, 4); c++) {
1339
+ const dstChannel = result.channels[c];
1340
+ for (let y = 0; y < dstH; y++) {
1341
+ for (let x = 0; x < dstW; x++) {
1342
+ const sx = srcX + x * scaleX;
1343
+ const sy = srcY + y * scaleY;
1344
+ dstChannel[(dstY + y) * targetWidth + (dstX + x)] = source.sample(sx, sy, c);
1345
+ }
1346
+ }
1347
+ }
1348
+ if (source.channelCount < 4 && fitMode !== "fit" && fitMode !== "native") {
1349
+ for (let y = 0; y < dstH; y++) {
1350
+ for (let x = 0; x < dstW; x++) {
1351
+ result.channels[3][(dstY + y) * targetWidth + (dstX + x)] = 1;
1352
+ }
1353
+ }
1354
+ }
1355
+ return result;
1356
+ }
1357
+ /**
1358
+ * Prepare multiple inputs to have matching dimensions
1359
+ * Returns buffers resized to the output resolution
1360
+ */
1361
+ prepareInputs(inputs, resolutionMode = "input", fitMode = "fill", customSize) {
1362
+ const buffers = inputs.map((input) => this.toImageBuffer(input));
1363
+ const { width, height } = this.resolveOutputSize(resolutionMode, buffers, customSize);
1364
+ const resized = buffers.map((buf) => {
1365
+ if (!buf) return null;
1366
+ if (buf.width === width && buf.height === height) return buf;
1367
+ return this.resizeToFit(buf, width, height, fitMode);
1368
+ });
1369
+ return { buffers: resized, width, height };
1370
+ }
1371
+ // ============ Output Utilities ============
1372
+ /**
1373
+ * Set output value and preview from an ImageBuffer
1374
+ * This is the standard way to output from an image node
1375
+ * Preview updates are throttled to ~30fps to avoid excessive canvas conversions
1376
+ */
1377
+ setOutput(output, buffer) {
1378
+ output.setValue(buffer);
1379
+ this._previewBuffer = buffer;
1380
+ this.updatePreviewThrottled();
1381
+ }
1382
+ /** Throttled Studio-only preview update. */
1383
+ updatePreviewThrottled() {
1384
+ if (!previewWanted2()) return;
1385
+ const now = performance.now();
1386
+ const elapsed = now - this._lastPreviewTime;
1387
+ if (elapsed >= _ImageNodeBase.PREVIEW_INTERVAL) {
1388
+ this._lastPreviewTime = now;
1389
+ if (this._previewBuffer) {
1390
+ this.preview = this._previewBuffer.toCanvas();
1391
+ }
1392
+ this._previewPending = false;
1393
+ } else if (!this._previewPending) {
1394
+ this._previewPending = true;
1395
+ const remaining = _ImageNodeBase.PREVIEW_INTERVAL - elapsed;
1396
+ setTimeout(() => {
1397
+ this._previewPending = false;
1398
+ this._lastPreviewTime = performance.now();
1399
+ if (this._previewBuffer && previewWanted2()) {
1400
+ this.preview = this._previewBuffer.toCanvas();
1401
+ }
1402
+ }, remaining);
1403
+ }
1404
+ }
1405
+ // ============ Buffer Info (for debugging/monitoring) ============
1406
+ /**
1407
+ * Get info about the current output buffer
1408
+ */
1409
+ getBufferInfo() {
1410
+ if (!this._previewBuffer) {
1411
+ return null;
1412
+ }
1413
+ const buf = this._previewBuffer;
1414
+ return {
1415
+ hasOutput: true,
1416
+ width: buf.width,
1417
+ height: buf.height,
1418
+ channels: buf.channelCount,
1419
+ layout: buf.layout,
1420
+ memoryBytes: buf.memoryBytes,
1421
+ memorySizeStr: buf.memorySizeStr
1422
+ };
1423
+ }
1424
+ };
1425
+
1426
+ // src/nodes/image/nodes/ColorNode.ts
1427
+ var ColorNode = class extends ImageNodeBase {
1428
+ constructor(id, graph) {
1429
+ super(id, "Color", graph);
1430
+ }
1431
+ setup() {
1432
+ this.addParm("color", {
1433
+ value: { r: 1, g: 1, b: 1 },
1434
+ type: "color",
1435
+ displayName: "Color"
1436
+ });
1437
+ this.addResolutionParm();
1438
+ this.output = this.out("image");
1439
+ this.watchProp("color", () => this.requestCook());
1440
+ this.onReady = () => this.requestCook();
1441
+ }
1442
+ render() {
1443
+ const [width, height] = this.getResolution();
1444
+ const color = normalizeColor(this.props.color.value);
1445
+ const buffer = ImageBuffer.solid(width, height, color);
1446
+ this.setOutput(this.output, buffer);
1447
+ }
1448
+ };
1449
+
1450
+ // src/nodes/image/nodes/ImageNode.ts
1451
+ var ImageNode = class extends ImageNodeBase {
1452
+ constructor(id, graph) {
1453
+ super(id, "File", graph);
1454
+ }
1455
+ setup() {
1456
+ this.imageInput = this.in("image", null);
1457
+ this.addParm("file", {
1458
+ value: "",
1459
+ type: "image",
1460
+ params: {
1461
+ accept: "image/*"
1462
+ },
1463
+ displayName: "File",
1464
+ onChange: () => {
1465
+ if (this.props.file.value && !this.imageInput.value) {
1466
+ this.requestCook();
1467
+ }
1468
+ }
1469
+ });
1470
+ this.addParm("resolutionMode", {
1471
+ value: "original",
1472
+ params: {
1473
+ options: [
1474
+ { value: "original", label: "Original" },
1475
+ { value: "max", label: "Max Resolution" },
1476
+ { value: "fixed", label: "Fixed Resolution" }
1477
+ ]
1478
+ },
1479
+ displayName: "Resolution Mode",
1480
+ onChange: () => {
1481
+ if (this.props.file.value || this.imageInput.value) {
1482
+ this.requestCook();
1483
+ }
1484
+ }
1485
+ });
1486
+ this.addParm("maxResolution", {
1487
+ value: [2048, 2048],
1488
+ params: {
1489
+ min: [1, 1],
1490
+ max: [4096, 4096]
1491
+ },
1492
+ displayName: "Max Resolution",
1493
+ hidden: () => this.props.resolutionMode.value !== "max",
1494
+ onChange: () => {
1495
+ if ((this.props.file.value || this.imageInput.value) && this.props.resolutionMode.value === "max") {
1496
+ this.requestCook();
1497
+ }
1498
+ }
1499
+ });
1500
+ this.addParm("fixedResolution", {
1501
+ value: [512, 512],
1502
+ params: {
1503
+ min: [1, 1],
1504
+ max: [4096, 4096]
1505
+ },
1506
+ displayName: "Fixed Resolution",
1507
+ hidden: () => this.props.resolutionMode.value !== "fixed",
1508
+ onChange: () => {
1509
+ if ((this.props.file.value || this.imageInput.value) && this.props.resolutionMode.value === "fixed") {
1510
+ this.requestCook();
1511
+ }
1512
+ }
1513
+ });
1514
+ this.output = this.out("image");
1515
+ this.imageInput.onChange = () => this.requestCook();
1516
+ this.onReady = () => {
1517
+ if (this.props.file.value || this.imageInput.value) {
1518
+ this.requestCook();
1519
+ }
1520
+ };
1521
+ }
1522
+ async render() {
1523
+ let sourceBuffer = null;
1524
+ if (this.imageInput.value) {
1525
+ if (this.imageInput.value instanceof ImageBuffer) {
1526
+ sourceBuffer = this.imageInput.value;
1527
+ } else if (typeof this.imageInput.value === "string") {
1528
+ sourceBuffer = ImageBuffer.fromCanvas(await decodeImage(this.imageInput.value));
1529
+ } else {
1530
+ sourceBuffer = ImageBuffer.fromCanvas(this.imageInput.value);
1531
+ }
1532
+ } else if (this.props.file.value) {
1533
+ sourceBuffer = ImageBuffer.fromCanvas(await decodeImage(this.props.file.value));
1534
+ }
1535
+ if (!sourceBuffer) {
1536
+ return;
1537
+ }
1538
+ try {
1539
+ const mode = this.props.resolutionMode.value;
1540
+ let outputBuffer;
1541
+ if (mode === "original") {
1542
+ outputBuffer = sourceBuffer;
1543
+ } else if (mode === "max") {
1544
+ const [maxWidth, maxHeight] = this.props.maxResolution.value;
1545
+ const aspectRatio = sourceBuffer.width / sourceBuffer.height;
1546
+ let newWidth = sourceBuffer.width;
1547
+ let newHeight = sourceBuffer.height;
1548
+ if (newWidth > maxWidth) {
1549
+ newWidth = maxWidth;
1550
+ newHeight = maxWidth / aspectRatio;
1551
+ }
1552
+ if (newHeight > maxHeight) {
1553
+ newHeight = maxHeight;
1554
+ newWidth = maxHeight * aspectRatio;
1555
+ }
1556
+ newWidth = Math.round(newWidth);
1557
+ newHeight = Math.round(newHeight);
1558
+ if (newWidth !== sourceBuffer.width || newHeight !== sourceBuffer.height) {
1559
+ outputBuffer = this.resizeBuffer(sourceBuffer, newWidth, newHeight);
1560
+ } else {
1561
+ outputBuffer = sourceBuffer;
1562
+ }
1563
+ } else {
1564
+ const [width, height] = this.props.fixedResolution.value;
1565
+ outputBuffer = this.resizeBuffer(sourceBuffer, width, height);
1566
+ }
1567
+ this.setOutput(this.output, outputBuffer);
1568
+ } catch (error) {
1569
+ this.error = error;
1570
+ console.error("Failed to process image:", error);
1571
+ }
1572
+ }
1573
+ resizeBuffer(source, newWidth, newHeight) {
1574
+ const result = ImageBuffer.rgba(newWidth, newHeight);
1575
+ const scaleX = source.width / newWidth;
1576
+ const scaleY = source.height / newHeight;
1577
+ for (let c = 0; c < Math.min(source.channelCount, 4); c++) {
1578
+ const dstChannel = result.channels[c];
1579
+ for (let y = 0; y < newHeight; y++) {
1580
+ for (let x = 0; x < newWidth; x++) {
1581
+ const srcX = x * scaleX;
1582
+ const srcY = y * scaleY;
1583
+ dstChannel[y * newWidth + x] = source.sample(srcX, srcY, c);
1584
+ }
1585
+ }
1586
+ }
1587
+ if (source.channelCount < 4) {
1588
+ result.fill(3, 1);
1589
+ }
1590
+ return result;
1591
+ }
1592
+ };
1593
+
1594
+ // src/nodes/image/nodes/CompositeNode.ts
1595
+ var CompositeNode = class extends ImageNodeBase {
1596
+ constructor(id, graph) {
1597
+ super(id, "Composite", graph);
1598
+ }
1599
+ setup() {
1600
+ this.image1 = this.in("image1", null);
1601
+ this.image2 = this.in("image2", null);
1602
+ this.addParm("blendMode", {
1603
+ value: "multiply",
1604
+ params: {
1605
+ options: [
1606
+ { value: "normal", label: "Normal" },
1607
+ { value: "multiply", label: "Multiply" },
1608
+ { value: "screen", label: "Screen" },
1609
+ { value: "overlay", label: "Overlay" },
1610
+ { value: "darken", label: "Darken" },
1611
+ { value: "lighten", label: "Lighten" },
1612
+ { value: "color-dodge", label: "Color Dodge" },
1613
+ { value: "color-burn", label: "Color Burn" },
1614
+ { value: "hard-light", label: "Hard Light" },
1615
+ { value: "soft-light", label: "Soft Light" },
1616
+ { value: "difference", label: "Difference" },
1617
+ { value: "exclusion", label: "Exclusion" },
1618
+ { value: "add", label: "Add" },
1619
+ { value: "subtract", label: "Subtract" },
1620
+ { value: "divide", label: "Divide" },
1621
+ { value: "pin-light", label: "Pin Light" },
1622
+ { value: "vivid-light", label: "Vivid Light" },
1623
+ { value: "linear-dodge", label: "Linear Dodge" },
1624
+ { value: "linear-burn", label: "Linear Burn" }
1625
+ ]
1626
+ },
1627
+ displayName: "Blend Mode",
1628
+ onChange: () => this.requestCook()
1629
+ });
1630
+ this.addParm("opacity", {
1631
+ value: 1,
1632
+ params: {
1633
+ min: 0,
1634
+ max: 1,
1635
+ step: 0.01
1636
+ },
1637
+ displayName: "Opacity",
1638
+ onChange: () => this.requestCook()
1639
+ });
1640
+ this.addParm("outputResolution", {
1641
+ value: "input1",
1642
+ params: {
1643
+ options: [
1644
+ { value: "input1", label: "Use Input 1" },
1645
+ { value: "input2", label: "Use Input 2" },
1646
+ { value: "largest", label: "Largest Input" },
1647
+ { value: "smallest", label: "Smallest Input" },
1648
+ { value: "custom", label: "Custom" }
1649
+ ]
1650
+ },
1651
+ displayName: "Output Resolution",
1652
+ onChange: () => this.requestCook()
1653
+ });
1654
+ this.addParm("customResolution", {
1655
+ value: [512, 512],
1656
+ params: {
1657
+ min: [1, 1],
1658
+ max: [4096, 4096],
1659
+ integer: true
1660
+ },
1661
+ displayName: "Resolution",
1662
+ hidden: () => this.props.outputResolution.value !== "custom",
1663
+ onChange: () => this.requestCook()
1664
+ });
1665
+ this.addParm("fitMode", {
1666
+ value: "fill",
1667
+ params: {
1668
+ options: [
1669
+ { value: "fill", label: "Fill (Crop)" },
1670
+ { value: "fit", label: "Fit (Letterbox)" },
1671
+ { value: "stretch", label: "Stretch" },
1672
+ { value: "native", label: "Native (No Scale)" }
1673
+ ]
1674
+ },
1675
+ displayName: "Fit Mode",
1676
+ onChange: () => this.requestCook()
1677
+ });
1678
+ this.output = this.out("image");
1679
+ this.image1.onChange = () => this.requestCook();
1680
+ this.image2.onChange = () => this.requestCook();
1681
+ this.onReady = () => this.requestCook();
1682
+ }
1683
+ blendBuffers(base, blend, mode, opacity) {
1684
+ const width = base.width;
1685
+ const height = base.height;
1686
+ const result = ImageBuffer.rgba(width, height);
1687
+ const baseRGBA = base.channelCount >= 4 ? base : base.toRGBA();
1688
+ const blendRGBA = blend.channelCount >= 4 ? blend : blend.toRGBA();
1689
+ const r1 = baseRGBA.channels[0];
1690
+ const g1 = baseRGBA.channels[1];
1691
+ const b1 = baseRGBA.channels[2];
1692
+ const a1 = baseRGBA.channels[3];
1693
+ const r2 = blendRGBA.channels[0];
1694
+ const g2 = blendRGBA.channels[1];
1695
+ const b2 = blendRGBA.channels[2];
1696
+ const a2 = blendRGBA.channels[3];
1697
+ const rOut = result.channels[0];
1698
+ const gOut = result.channels[1];
1699
+ const bOut = result.channels[2];
1700
+ const aOut = result.channels[3];
1701
+ for (let i = 0; i < width * height; i++) {
1702
+ const baseR = r1[i], baseG = g1[i], baseB = b1[i], baseA = a1[i];
1703
+ const blendR = r2[i], blendG = g2[i], blendB = b2[i], blendA = a2[i];
1704
+ let r, g, b;
1705
+ switch (mode) {
1706
+ case "normal":
1707
+ r = blendR;
1708
+ g = blendG;
1709
+ b = blendB;
1710
+ break;
1711
+ case "multiply":
1712
+ r = baseR * blendR;
1713
+ g = baseG * blendG;
1714
+ b = baseB * blendB;
1715
+ break;
1716
+ case "screen":
1717
+ r = 1 - (1 - baseR) * (1 - blendR);
1718
+ g = 1 - (1 - baseG) * (1 - blendG);
1719
+ b = 1 - (1 - baseB) * (1 - blendB);
1720
+ break;
1721
+ case "overlay":
1722
+ r = baseR < 0.5 ? 2 * baseR * blendR : 1 - 2 * (1 - baseR) * (1 - blendR);
1723
+ g = baseG < 0.5 ? 2 * baseG * blendG : 1 - 2 * (1 - baseG) * (1 - blendG);
1724
+ b = baseB < 0.5 ? 2 * baseB * blendB : 1 - 2 * (1 - baseB) * (1 - blendB);
1725
+ break;
1726
+ case "darken":
1727
+ r = Math.min(baseR, blendR);
1728
+ g = Math.min(baseG, blendG);
1729
+ b = Math.min(baseB, blendB);
1730
+ break;
1731
+ case "lighten":
1732
+ r = Math.max(baseR, blendR);
1733
+ g = Math.max(baseG, blendG);
1734
+ b = Math.max(baseB, blendB);
1735
+ break;
1736
+ case "color-dodge":
1737
+ r = blendR === 1 ? 1 : Math.min(1, baseR / (1 - blendR));
1738
+ g = blendG === 1 ? 1 : Math.min(1, baseG / (1 - blendG));
1739
+ b = blendB === 1 ? 1 : Math.min(1, baseB / (1 - blendB));
1740
+ break;
1741
+ case "color-burn":
1742
+ r = blendR === 0 ? 0 : Math.max(0, 1 - (1 - baseR) / blendR);
1743
+ g = blendG === 0 ? 0 : Math.max(0, 1 - (1 - baseG) / blendG);
1744
+ b = blendB === 0 ? 0 : Math.max(0, 1 - (1 - baseB) / blendB);
1745
+ break;
1746
+ case "hard-light":
1747
+ r = blendR < 0.5 ? 2 * baseR * blendR : 1 - 2 * (1 - baseR) * (1 - blendR);
1748
+ g = blendG < 0.5 ? 2 * baseG * blendG : 1 - 2 * (1 - baseG) * (1 - blendG);
1749
+ b = blendB < 0.5 ? 2 * baseB * blendB : 1 - 2 * (1 - baseB) * (1 - blendB);
1750
+ break;
1751
+ case "soft-light":
1752
+ r = blendR < 0.5 ? baseR - (1 - 2 * blendR) * baseR * (1 - baseR) : baseR + (2 * blendR - 1) * (Math.sqrt(baseR) - baseR);
1753
+ g = blendG < 0.5 ? baseG - (1 - 2 * blendG) * baseG * (1 - baseG) : baseG + (2 * blendG - 1) * (Math.sqrt(baseG) - baseG);
1754
+ b = blendB < 0.5 ? baseB - (1 - 2 * blendB) * baseB * (1 - baseB) : baseB + (2 * blendB - 1) * (Math.sqrt(baseB) - baseB);
1755
+ break;
1756
+ case "difference":
1757
+ r = Math.abs(baseR - blendR);
1758
+ g = Math.abs(baseG - blendG);
1759
+ b = Math.abs(baseB - blendB);
1760
+ break;
1761
+ case "exclusion":
1762
+ r = baseR + blendR - 2 * baseR * blendR;
1763
+ g = baseG + blendG - 2 * baseG * blendG;
1764
+ b = baseB + blendB - 2 * baseB * blendB;
1765
+ break;
1766
+ case "add":
1767
+ r = Math.min(1, baseR + blendR);
1768
+ g = Math.min(1, baseG + blendG);
1769
+ b = Math.min(1, baseB + blendB);
1770
+ break;
1771
+ case "subtract":
1772
+ r = Math.max(0, baseR - blendR);
1773
+ g = Math.max(0, baseG - blendG);
1774
+ b = Math.max(0, baseB - blendB);
1775
+ break;
1776
+ case "divide":
1777
+ r = blendR === 0 ? 1 : Math.min(1, baseR / blendR);
1778
+ g = blendG === 0 ? 1 : Math.min(1, baseG / blendG);
1779
+ b = blendB === 0 ? 1 : Math.min(1, baseB / blendB);
1780
+ break;
1781
+ case "pin-light":
1782
+ r = blendR < 0.5 ? Math.min(baseR, 2 * blendR) : Math.max(baseR, 2 * (blendR - 0.5));
1783
+ g = blendG < 0.5 ? Math.min(baseG, 2 * blendG) : Math.max(baseG, 2 * (blendG - 0.5));
1784
+ b = blendB < 0.5 ? Math.min(baseB, 2 * blendB) : Math.max(baseB, 2 * (blendB - 0.5));
1785
+ break;
1786
+ case "vivid-light":
1787
+ r = blendR < 0.5 ? blendR === 0 ? 0 : 1 - (1 - baseR) / (2 * blendR) : blendR === 1 ? 1 : baseR / (2 * (1 - blendR));
1788
+ g = blendG < 0.5 ? blendG === 0 ? 0 : 1 - (1 - baseG) / (2 * blendG) : blendG === 1 ? 1 : baseG / (2 * (1 - blendG));
1789
+ b = blendB < 0.5 ? blendB === 0 ? 0 : 1 - (1 - baseB) / (2 * blendB) : blendB === 1 ? 1 : baseB / (2 * (1 - blendB));
1790
+ break;
1791
+ case "linear-dodge":
1792
+ r = Math.min(1, baseR + blendR);
1793
+ g = Math.min(1, baseG + blendG);
1794
+ b = Math.min(1, baseB + blendB);
1795
+ break;
1796
+ case "linear-burn":
1797
+ r = Math.max(0, baseR + blendR - 1);
1798
+ g = Math.max(0, baseG + blendG - 1);
1799
+ b = Math.max(0, baseB + blendB - 1);
1800
+ break;
1801
+ default:
1802
+ r = blendR;
1803
+ g = blendG;
1804
+ b = blendB;
1805
+ }
1806
+ rOut[i] = baseR + (r - baseR) * opacity * blendA;
1807
+ gOut[i] = baseG + (g - baseG) * opacity * blendA;
1808
+ bOut[i] = baseB + (b - baseB) * opacity * blendA;
1809
+ aOut[i] = baseA + (blendA - baseA) * opacity;
1810
+ }
1811
+ return result;
1812
+ }
1813
+ render() {
1814
+ const resolutionMode = this.props.outputResolution.value;
1815
+ const fitMode = this.props.fitMode.value;
1816
+ const customSize = this.props.customResolution.value;
1817
+ const { buffers } = this.prepareInputs(
1818
+ [this.image1.value, this.image2.value],
1819
+ resolutionMode,
1820
+ fitMode,
1821
+ customSize
1822
+ );
1823
+ const [buf1, buf2] = buffers;
1824
+ if (!buf1 || !buf2) {
1825
+ return;
1826
+ }
1827
+ const blendMode = this.props.blendMode.value;
1828
+ const opacity = this.props.opacity.value;
1829
+ try {
1830
+ const result = this.blendBuffers(buf1, buf2, blendMode, opacity);
1831
+ this.setOutput(this.output, result);
1832
+ } catch (error) {
1833
+ console.error("Composite render error:", error);
1834
+ this.error = error;
1835
+ }
1836
+ }
1837
+ };
1838
+
1839
+ // src/nodes/image/nodes/CheckersNode.ts
1840
+ var CheckersNode = class extends ImageNodeBase {
1841
+ constructor(id, graph) {
1842
+ super(id, "Checkers", graph);
1843
+ }
1844
+ setup() {
1845
+ this.addParm("color1", {
1846
+ value: { r: 1, g: 1, b: 1 },
1847
+ type: "color",
1848
+ displayName: "Color 1",
1849
+ onChange: () => this.requestCook()
1850
+ });
1851
+ this.addParm("color2", {
1852
+ value: { r: 0, g: 0, b: 0 },
1853
+ type: "color",
1854
+ displayName: "Color 2",
1855
+ onChange: () => this.requestCook()
1856
+ });
1857
+ this.addParm("mode", {
1858
+ value: "size",
1859
+ params: {
1860
+ options: [
1861
+ { value: "size", label: "Size" },
1862
+ { value: "divisions", label: "Divisions" }
1863
+ ]
1864
+ },
1865
+ displayName: "Mode",
1866
+ onChange: () => this.requestCook()
1867
+ });
1868
+ this.addParm("size", {
1869
+ value: 32,
1870
+ params: {
1871
+ min: 1,
1872
+ max: 512,
1873
+ step: 0.1
1874
+ },
1875
+ displayName: "Size",
1876
+ hidden: () => this.props.mode.value !== "size",
1877
+ onChange: () => this.requestCook()
1878
+ });
1879
+ this.addParm("divisions", {
1880
+ value: 16,
1881
+ params: {
1882
+ min: 1,
1883
+ max: 512,
1884
+ step: 1,
1885
+ integer: true
1886
+ },
1887
+ displayName: "Divisions",
1888
+ hidden: () => this.props.mode.value !== "divisions",
1889
+ onChange: () => this.requestCook()
1890
+ });
1891
+ this.addParm("centered", {
1892
+ value: false,
1893
+ type: "boolean",
1894
+ displayName: "Centered",
1895
+ onChange: () => this.requestCook()
1896
+ });
1897
+ this.addResolutionParm();
1898
+ this.output = this.out("image");
1899
+ this.onReady = () => this.requestCook();
1900
+ }
1901
+ render() {
1902
+ const [width, height] = this.getResolution();
1903
+ const mode = this.props.mode.value;
1904
+ const color1 = normalizeColor(this.props.color1.value);
1905
+ const color2 = normalizeColor(this.props.color2.value);
1906
+ const centered = this.props.centered.value;
1907
+ let checkerSize;
1908
+ if (mode === "size") {
1909
+ checkerSize = this.props.size.value;
1910
+ } else {
1911
+ checkerSize = width / this.props.divisions.value;
1912
+ }
1913
+ let offsetX = 0;
1914
+ let offsetY = 0;
1915
+ if (centered) {
1916
+ offsetX = width / 2 % checkerSize - checkerSize / 2;
1917
+ offsetY = height / 2 % checkerSize - checkerSize / 2;
1918
+ }
1919
+ const buffer = ImageBuffer.rgba(width, height);
1920
+ const r = buffer.r();
1921
+ const g = buffer.g();
1922
+ const b = buffer.b();
1923
+ const a = buffer.a();
1924
+ for (let y = 0; y < height; y++) {
1925
+ for (let x = 0; x < width; x++) {
1926
+ const idx = y * width + x;
1927
+ const isEven = (Math.floor((x - offsetX) / checkerSize) + Math.floor((y - offsetY) / checkerSize)) % 2 === 0;
1928
+ const color = isEven ? color1 : color2;
1929
+ r[idx] = color.r;
1930
+ g[idx] = color.g;
1931
+ b[idx] = color.b;
1932
+ a[idx] = color.a;
1933
+ }
1934
+ }
1935
+ buffer.markDirty();
1936
+ this.setOutput(this.output, buffer);
1937
+ }
1938
+ };
1939
+
1940
+ // src/nodes/image/nodes/ResizeNode.ts
1941
+ var ResizeNode = class extends ImageNodeBase {
1942
+ constructor(id, graph) {
1943
+ super(id, "Resize", graph);
1944
+ }
1945
+ setup() {
1946
+ this.image = this.in("image", null);
1947
+ this.addParm("mode", {
1948
+ value: "scale",
1949
+ params: {
1950
+ options: [
1951
+ { value: "scale", label: "Scale" },
1952
+ { value: "fixed", label: "Fixed" }
1953
+ ]
1954
+ },
1955
+ displayName: "Mode",
1956
+ onChange: () => this.requestCook()
1957
+ });
1958
+ this.addParm("scale", {
1959
+ value: 1,
1960
+ params: {
1961
+ min: 0.1,
1962
+ max: 10,
1963
+ step: 0.1
1964
+ },
1965
+ displayName: "Scale",
1966
+ hidden: () => this.props.mode.value !== "scale",
1967
+ onChange: () => this.requestCook()
1968
+ });
1969
+ this.addParm("width", {
1970
+ value: 512,
1971
+ params: {
1972
+ min: 1,
1973
+ max: 4096,
1974
+ step: 1
1975
+ },
1976
+ displayName: "Width",
1977
+ hidden: () => this.props.mode.value !== "fixed",
1978
+ onChange: () => this.requestCook()
1979
+ });
1980
+ this.addParm("height", {
1981
+ value: 512,
1982
+ params: {
1983
+ min: 1,
1984
+ max: 4096,
1985
+ step: 1
1986
+ },
1987
+ displayName: "Height",
1988
+ hidden: () => this.props.mode.value !== "fixed",
1989
+ onChange: () => this.requestCook()
1990
+ });
1991
+ this.output = this.out("image");
1992
+ this.image.onChange = () => this.requestCook();
1993
+ this.onReady = () => this.requestCook();
1994
+ }
1995
+ resizeBuffer(source, newWidth, newHeight) {
1996
+ const result = ImageBuffer.rgba(newWidth, newHeight);
1997
+ const scaleX = source.width / newWidth;
1998
+ const scaleY = source.height / newHeight;
1999
+ for (let c = 0; c < Math.min(source.channelCount, 4); c++) {
2000
+ const dstChannel = result.channels[c];
2001
+ for (let y = 0; y < newHeight; y++) {
2002
+ for (let x = 0; x < newWidth; x++) {
2003
+ const srcX = x * scaleX;
2004
+ const srcY = y * scaleY;
2005
+ dstChannel[y * newWidth + x] = source.sample(srcX, srcY, c);
2006
+ }
2007
+ }
2008
+ }
2009
+ if (source.channelCount < 4) {
2010
+ result.fill(3, 1);
2011
+ }
2012
+ return result;
2013
+ }
2014
+ render() {
2015
+ const buffer = this.toImageBuffer(this.image.value);
2016
+ if (!buffer) {
2017
+ return;
2018
+ }
2019
+ let newWidth, newHeight;
2020
+ if (this.props.mode.value === "scale") {
2021
+ const scale = this.props.scale.value;
2022
+ newWidth = Math.round(buffer.width * scale);
2023
+ newHeight = Math.round(buffer.height * scale);
2024
+ } else {
2025
+ newWidth = this.props.width.value;
2026
+ newHeight = this.props.height.value;
2027
+ }
2028
+ const resized = this.resizeBuffer(buffer, newWidth, newHeight);
2029
+ this.setOutput(this.output, resized);
2030
+ }
2031
+ };
2032
+
2033
+ // src/nodes/image/nodes/NormalMapNode.ts
2034
+ var NormalMapNode = class extends ImageNodeBase {
2035
+ constructor(id, graph) {
2036
+ super(id, "NormalMap", graph);
2037
+ }
2038
+ setup() {
2039
+ this.image = this.in("image", null);
2040
+ this.addParm("scale", {
2041
+ value: 1,
2042
+ params: {
2043
+ min: 0,
2044
+ max: 10,
2045
+ step: 0.1
2046
+ },
2047
+ displayName: "Scale",
2048
+ onChange: () => this.requestCook()
2049
+ });
2050
+ this.addParm("flipX", {
2051
+ value: false,
2052
+ type: "boolean",
2053
+ displayName: "Flip X",
2054
+ onChange: () => this.requestCook()
2055
+ });
2056
+ this.addParm("flipY", {
2057
+ value: false,
2058
+ type: "boolean",
2059
+ displayName: "Flip Y",
2060
+ onChange: () => this.requestCook()
2061
+ });
2062
+ this.output = this.out("image");
2063
+ this.image.onChange = () => this.requestCook();
2064
+ this.onReady = () => this.requestCook();
2065
+ }
2066
+ computeNormalMap(source, scale, flipX, flipY) {
2067
+ const { width, height } = source;
2068
+ const result = ImageBuffer.rgba(width, height);
2069
+ const heightMap = source.channels[0];
2070
+ const rOut = result.channels[0];
2071
+ const gOut = result.channels[1];
2072
+ const bOut = result.channels[2];
2073
+ const aOut = result.channels[3];
2074
+ for (let y = 0; y < height; y++) {
2075
+ for (let x = 0; x < width; x++) {
2076
+ const idx = y * width + x;
2077
+ const h00 = heightMap[idx];
2078
+ const h10 = heightMap[Math.min(x + 1, width - 1) + y * width];
2079
+ const h01 = heightMap[x + Math.min(y + 1, height - 1) * width];
2080
+ const dx = (h10 - h00) * scale;
2081
+ const dy = (h01 - h00) * scale;
2082
+ const finalDx = flipX ? -dx : dx;
2083
+ const finalDy = flipY ? -dy : dy;
2084
+ const nx = -finalDx;
2085
+ const ny = -finalDy;
2086
+ const nz = 1;
2087
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
2088
+ const normalizedX = nx / length;
2089
+ const normalizedY = ny / length;
2090
+ const normalizedZ = nz / length;
2091
+ rOut[idx] = normalizedX * 0.5 + 0.5;
2092
+ gOut[idx] = normalizedY * 0.5 + 0.5;
2093
+ bOut[idx] = normalizedZ * 0.5 + 0.5;
2094
+ aOut[idx] = 1;
2095
+ }
2096
+ }
2097
+ return result;
2098
+ }
2099
+ render() {
2100
+ const buffer = this.toImageBuffer(this.image.value);
2101
+ if (!buffer) {
2102
+ return;
2103
+ }
2104
+ const scale = this.props.scale.value;
2105
+ const flipX = this.props.flipX.value;
2106
+ const flipY = this.props.flipY.value;
2107
+ const normalMap = this.computeNormalMap(buffer, scale, flipX, flipY);
2108
+ this.setOutput(this.output, normalMap);
2109
+ }
2110
+ };
2111
+
2112
+ // src/nodes/image/nodes/RampNode.ts
2113
+ var RampNode = class extends ImageNodeBase {
2114
+ constructor(id, graph) {
2115
+ super(id, "Ramp", graph);
2116
+ }
2117
+ setup() {
2118
+ this.addParm("type", {
2119
+ value: "horizontal",
2120
+ params: {
2121
+ options: [
2122
+ { value: "horizontal", label: "Horizontal" },
2123
+ { value: "vertical", label: "Vertical" },
2124
+ { value: "radial", label: "Radial" },
2125
+ { value: "concentric", label: "Concentric" }
2126
+ ]
2127
+ },
2128
+ displayName: "Type",
2129
+ onChange: () => this.requestCook()
2130
+ });
2131
+ this.addParm("points", {
2132
+ value: [
2133
+ { position: 0, color: { r: 0, g: 0, b: 0 }, interpolation: "linear" },
2134
+ { position: 1, color: { r: 1, g: 1, b: 1 }, interpolation: "linear" }
2135
+ ],
2136
+ type: "colorramp",
2137
+ displayName: "Ramp",
2138
+ onChange: () => this.requestCook()
2139
+ });
2140
+ this.addResolutionParm();
2141
+ this.output = this.out("image");
2142
+ this.onReady = () => this.requestCook();
2143
+ }
2144
+ normalizeRampColor(color) {
2145
+ if (typeof color === "object" && color !== null && "r" in color && "g" in color && "b" in color) {
2146
+ const isNormalized = color.r <= 1 && color.g <= 1 && color.b <= 1;
2147
+ return {
2148
+ r: isNormalized ? color.r : color.r / 255,
2149
+ g: isNormalized ? color.g : color.g / 255,
2150
+ b: isNormalized ? color.b : color.b / 255,
2151
+ a: color.a !== void 0 ? isNormalized ? color.a : color.a / 255 : 1
2152
+ };
2153
+ }
2154
+ if (Array.isArray(color)) {
2155
+ const isNormalized = color.every((v) => v <= 1);
2156
+ return {
2157
+ r: isNormalized ? color[0] : color[0] / 255,
2158
+ g: isNormalized ? color[1] : color[1] / 255,
2159
+ b: isNormalized ? color[2] : color[2] / 255,
2160
+ a: color[3] !== void 0 ? isNormalized ? color[3] : color[3] / 255 : 1
2161
+ };
2162
+ }
2163
+ if (typeof color === "string") {
2164
+ const rgbMatch = color.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
2165
+ if (rgbMatch) {
2166
+ return {
2167
+ r: parseInt(rgbMatch[1], 10) / 255,
2168
+ g: parseInt(rgbMatch[2], 10) / 255,
2169
+ b: parseInt(rgbMatch[3], 10) / 255,
2170
+ a: 1
2171
+ };
2172
+ }
2173
+ const hex = color.replace("#", "");
2174
+ if (hex.length === 3 || hex.length === 6) {
2175
+ const expanded = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
2176
+ const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(expanded);
2177
+ if (result) {
2178
+ return {
2179
+ r: parseInt(result[1], 16) / 255,
2180
+ g: parseInt(result[2], 16) / 255,
2181
+ b: parseInt(result[3], 16) / 255,
2182
+ a: 1
2183
+ };
2184
+ }
2185
+ }
2186
+ }
2187
+ return { r: 0, g: 0, b: 0, a: 1 };
2188
+ }
2189
+ interpolateColor(color1, color2, t, interpolation) {
2190
+ if (interpolation === "constant") {
2191
+ return t < 0.5 ? color1 : color2;
2192
+ } else if (interpolation === "smooth") {
2193
+ t = t * t * (3 - 2 * t);
2194
+ }
2195
+ return {
2196
+ r: color1.r + (color2.r - color1.r) * t,
2197
+ g: color1.g + (color2.g - color1.g) * t,
2198
+ b: color1.b + (color2.b - color1.b) * t,
2199
+ a: (color1.a ?? 1) + ((color2.a ?? 1) - (color1.a ?? 1)) * t
2200
+ };
2201
+ }
2202
+ getColorAtPosition(points, position) {
2203
+ const sortedPoints = [...points].sort((a, b) => a.position - b.position);
2204
+ position = Math.max(0, Math.min(1, position));
2205
+ let before = null;
2206
+ let after = null;
2207
+ for (let i = 0; i < sortedPoints.length; i++) {
2208
+ if (sortedPoints[i].position <= position) {
2209
+ before = sortedPoints[i];
2210
+ }
2211
+ if (sortedPoints[i].position >= position && !after) {
2212
+ after = sortedPoints[i];
2213
+ break;
2214
+ }
2215
+ }
2216
+ if (!before && after) return this.normalizeRampColor(after.color);
2217
+ if (before && !after) return this.normalizeRampColor(before.color);
2218
+ if (!before && !after) return { r: 0, g: 0, b: 0, a: 1 };
2219
+ if (before.position === position) return this.normalizeRampColor(before.color);
2220
+ if (after.position === position) return this.normalizeRampColor(after.color);
2221
+ const t = (position - before.position) / (after.position - before.position);
2222
+ const color1 = this.normalizeRampColor(before.color);
2223
+ const color2 = this.normalizeRampColor(after.color);
2224
+ const interpolation = after.interpolation || "linear";
2225
+ return this.interpolateColor(color1, color2, t, interpolation);
2226
+ }
2227
+ render() {
2228
+ const [width, height] = this.getResolution();
2229
+ const rampType = this.props.type.value;
2230
+ const points = this.props.points.value || [];
2231
+ if (points.length === 0) {
2232
+ return;
2233
+ }
2234
+ const buffer = ImageBuffer.rgba(width, height);
2235
+ const r = buffer.r();
2236
+ const g = buffer.g();
2237
+ const b = buffer.b();
2238
+ const a = buffer.a();
2239
+ const centerX = width / 2;
2240
+ const centerY = height / 2;
2241
+ const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
2242
+ for (let y = 0; y < height; y++) {
2243
+ for (let x = 0; x < width; x++) {
2244
+ let position = 0;
2245
+ if (rampType === "horizontal") {
2246
+ position = x / width;
2247
+ } else if (rampType === "vertical") {
2248
+ position = y / height;
2249
+ } else if (rampType === "radial") {
2250
+ const dx = x - centerX;
2251
+ const dy = y - centerY;
2252
+ const angle = Math.atan2(dy, dx);
2253
+ position = (angle + Math.PI) / (2 * Math.PI);
2254
+ } else if (rampType === "concentric") {
2255
+ const dx = x - centerX;
2256
+ const dy = y - centerY;
2257
+ const dist = Math.sqrt(dx * dx + dy * dy);
2258
+ position = dist / maxDist;
2259
+ }
2260
+ const color = this.getColorAtPosition(points, position);
2261
+ const idx = y * width + x;
2262
+ r[idx] = color.r;
2263
+ g[idx] = color.g;
2264
+ b[idx] = color.b;
2265
+ a[idx] = color.a ?? 1;
2266
+ }
2267
+ }
2268
+ buffer.markDirty();
2269
+ this.setOutput(this.output, buffer);
2270
+ }
2271
+ };
2272
+
2273
+ // src/nodes/image/nodes/NoiseNode.ts
2274
+ var NoiseNode = class extends ImageNodeBase {
2275
+ constructor(id, graph) {
2276
+ super(id, "Noise", graph);
2277
+ this.noise2D = null;
2278
+ this.currentSeed = null;
2279
+ }
2280
+ setup() {
2281
+ this.addParm("seed", {
2282
+ value: 0,
2283
+ params: {
2284
+ min: 0,
2285
+ max: 1e4,
2286
+ step: 1,
2287
+ integer: true
2288
+ },
2289
+ displayName: "Seed",
2290
+ onChange: () => {
2291
+ void this.initNoise().then(() => {
2292
+ if (this.noise2D) this.requestCook();
2293
+ });
2294
+ }
2295
+ });
2296
+ this.addParm("scale", {
2297
+ value: 0.01,
2298
+ params: {
2299
+ min: 1e-3,
2300
+ max: 1,
2301
+ step: 1e-3
2302
+ },
2303
+ displayName: "Scale",
2304
+ onChange: () => this.requestCook()
2305
+ });
2306
+ this.addParm("iterations", {
2307
+ value: 4,
2308
+ params: {
2309
+ min: 1,
2310
+ max: 8,
2311
+ step: 1,
2312
+ integer: true
2313
+ },
2314
+ displayName: "Iterations",
2315
+ onChange: () => this.requestCook()
2316
+ });
2317
+ this.addResolutionParm();
2318
+ this.output = this.out("image");
2319
+ this.onReady = async () => {
2320
+ await this.initNoise();
2321
+ if (this.noise2D) {
2322
+ this.requestCook();
2323
+ }
2324
+ };
2325
+ }
2326
+ async initNoise() {
2327
+ try {
2328
+ let seededRandom2 = function() {
2329
+ rng = (rng * 9301 + 49297) % 233280;
2330
+ return rng / 233280;
2331
+ };
2332
+ var seededRandom = seededRandom2;
2333
+ const simplexNoise = await this.require("simplex-noise");
2334
+ const { createNoise2D } = simplexNoise;
2335
+ const seed = this.props.seed.value;
2336
+ let rng = seed;
2337
+ this.noise2D = createNoise2D(seededRandom2);
2338
+ this.currentSeed = seed;
2339
+ } catch (error) {
2340
+ console.error("Failed to load simplex-noise:", error);
2341
+ this.error = error;
2342
+ }
2343
+ }
2344
+ render() {
2345
+ if (!this.noise2D) {
2346
+ this.initNoise().then(() => {
2347
+ if (this.noise2D) {
2348
+ this.requestCook();
2349
+ }
2350
+ });
2351
+ return;
2352
+ }
2353
+ const [width, height] = this.getResolution();
2354
+ const scale = this.props.scale.value;
2355
+ const iterations = this.props.iterations.value;
2356
+ const seed = this.props.seed.value;
2357
+ if (seed !== this.currentSeed) {
2358
+ this.initNoise().then(() => {
2359
+ if (this.noise2D) {
2360
+ this.requestCook();
2361
+ }
2362
+ });
2363
+ return;
2364
+ }
2365
+ const buffer = ImageBuffer.grayscale(width, height);
2366
+ const gray = buffer.r();
2367
+ for (let y = 0; y < height; y++) {
2368
+ for (let x = 0; x < width; x++) {
2369
+ let value = 0;
2370
+ let amplitude = 1;
2371
+ let frequency = scale;
2372
+ let maxValue = 0;
2373
+ for (let i = 0; i < iterations; i++) {
2374
+ const nx = x * frequency;
2375
+ const ny = y * frequency;
2376
+ const noiseValue = this.noise2D(nx, ny);
2377
+ value += noiseValue * amplitude;
2378
+ maxValue += amplitude;
2379
+ amplitude *= 0.5;
2380
+ frequency *= 2;
2381
+ }
2382
+ value = (value / maxValue + 1) * 0.5;
2383
+ gray[y * width + x] = value;
2384
+ }
2385
+ }
2386
+ buffer.markDirty();
2387
+ this.setOutput(this.output, buffer);
2388
+ }
2389
+ };
2390
+
2391
+ // src/nodes/image/nodes/BlurNode.ts
2392
+ var BlurNode = class extends ImageNodeBase {
2393
+ constructor(id, graph) {
2394
+ super(id, "Blur", graph);
2395
+ }
2396
+ setup() {
2397
+ this.image = this.in("image", null);
2398
+ this.addParm("method", {
2399
+ value: "box",
2400
+ params: {
2401
+ options: [
2402
+ { value: "box", label: "Box (Fast)" },
2403
+ { value: "gaussian", label: "Gaussian (Accurate)" }
2404
+ ]
2405
+ },
2406
+ displayName: "Method",
2407
+ onChange: () => this.requestCook()
2408
+ });
2409
+ this.addParm("radius", {
2410
+ value: 5,
2411
+ params: {
2412
+ min: 0,
2413
+ max: 100,
2414
+ step: 0.1
2415
+ },
2416
+ displayName: "Radius",
2417
+ onChange: () => this.requestCook()
2418
+ });
2419
+ this.addParm("wrapEdges", {
2420
+ value: false,
2421
+ type: "boolean",
2422
+ displayName: "Wrap Edges",
2423
+ onChange: () => this.requestCook()
2424
+ });
2425
+ this.output = this.out("image");
2426
+ this.image.onChange = () => this.requestCook();
2427
+ this.onReady = () => this.requestCook();
2428
+ }
2429
+ /**
2430
+ * Fast blur using stacked box blur approximation.
2431
+ * 3 passes of box blur closely approximates Gaussian and is O(1) per pixel.
2432
+ */
2433
+ boxBlur(source, radius, wrapEdges) {
2434
+ if (radius <= 0) {
2435
+ return source.clone();
2436
+ }
2437
+ const { width, height } = source;
2438
+ const sigma = radius;
2439
+ const boxes = this.boxesForGauss(sigma, 3);
2440
+ const result = source.emptyClone();
2441
+ for (let c = 0; c < source.channelCount; c++) {
2442
+ const channelData = new Float32Array(source.channels[c]);
2443
+ for (const boxSize of boxes) {
2444
+ this.boxBlurPass(channelData, width, height, boxSize, wrapEdges);
2445
+ }
2446
+ result.channels[c].set(channelData);
2447
+ }
2448
+ return result;
2449
+ }
2450
+ /**
2451
+ * Calculate box sizes for Gaussian approximation.
2452
+ * Returns array of box radii for n passes.
2453
+ */
2454
+ boxesForGauss(sigma, n) {
2455
+ const wIdeal = Math.sqrt(12 * sigma * sigma / n + 1);
2456
+ let wl = Math.floor(wIdeal);
2457
+ if (wl % 2 === 0) wl--;
2458
+ const wu = wl + 2;
2459
+ const mIdeal = (12 * sigma * sigma - n * wl * wl - 4 * n * wl - 3 * n) / (-4 * wl - 4);
2460
+ const m = Math.round(mIdeal);
2461
+ const sizes = [];
2462
+ for (let i = 0; i < n; i++) {
2463
+ sizes.push(i < m ? wl : wu);
2464
+ }
2465
+ return sizes.map((s) => (s - 1) / 2);
2466
+ }
2467
+ /**
2468
+ * Single box blur pass (horizontal then vertical) - O(1) per pixel
2469
+ */
2470
+ boxBlurPass(data, w, h, r, wrap) {
2471
+ if (r < 1) return;
2472
+ const temp = new Float32Array(w * h);
2473
+ const ri = Math.floor(r);
2474
+ const diameter = ri * 2 + 1;
2475
+ const invDiameter = 1 / diameter;
2476
+ for (let y = 0; y < h; y++) {
2477
+ const rowOffset = y * w;
2478
+ let sum = 0;
2479
+ for (let i = -ri; i <= ri; i++) {
2480
+ const px = wrap ? (i % w + w) % w : Math.max(0, Math.min(w - 1, i));
2481
+ sum += data[rowOffset + px];
2482
+ }
2483
+ temp[rowOffset] = sum * invDiameter;
2484
+ for (let x = 1; x < w; x++) {
2485
+ const removeX = wrap ? ((x - ri - 1) % w + w) % w : Math.max(0, x - ri - 1);
2486
+ const addX = wrap ? ((x + ri) % w + w) % w : Math.min(w - 1, x + ri);
2487
+ sum -= data[rowOffset + removeX];
2488
+ sum += data[rowOffset + addX];
2489
+ temp[rowOffset + x] = sum * invDiameter;
2490
+ }
2491
+ }
2492
+ for (let x = 0; x < w; x++) {
2493
+ let sum = 0;
2494
+ for (let i = -ri; i <= ri; i++) {
2495
+ const py = wrap ? (i % h + h) % h : Math.max(0, Math.min(h - 1, i));
2496
+ sum += temp[py * w + x];
2497
+ }
2498
+ data[x] = sum * invDiameter;
2499
+ for (let y = 1; y < h; y++) {
2500
+ const removeY = wrap ? ((y - ri - 1) % h + h) % h : Math.max(0, y - ri - 1);
2501
+ const addY = wrap ? ((y + ri) % h + h) % h : Math.min(h - 1, y + ri);
2502
+ sum -= temp[removeY * w + x];
2503
+ sum += temp[addY * w + x];
2504
+ data[y * w + x] = sum * invDiameter;
2505
+ }
2506
+ }
2507
+ }
2508
+ /**
2509
+ * True Gaussian blur using separable convolution.
2510
+ * More accurate but O(radius) per pixel - slower for large radii.
2511
+ */
2512
+ trueGaussianBlur(source, radius, wrapEdges) {
2513
+ if (radius <= 0) {
2514
+ return source.clone();
2515
+ }
2516
+ const { width, height } = source;
2517
+ const result = source.emptyClone();
2518
+ const kernelSize = Math.ceil(radius * 3) * 2 + 1;
2519
+ const kernel = new Float32Array(kernelSize);
2520
+ const sigma = radius / 3;
2521
+ const twoSigmaSq = 2 * sigma * sigma;
2522
+ let sum = 0;
2523
+ const halfKernel = kernelSize - 1 >> 1;
2524
+ for (let i = 0; i < kernelSize; i++) {
2525
+ const x = i - halfKernel;
2526
+ const value = Math.exp(-(x * x) / twoSigmaSq);
2527
+ kernel[i] = value;
2528
+ sum += value;
2529
+ }
2530
+ const invSum = 1 / sum;
2531
+ for (let i = 0; i < kernelSize; i++) {
2532
+ kernel[i] *= invSum;
2533
+ }
2534
+ const wrapX = wrapEdges ? (coord) => (coord % width + width) % width : (coord) => coord < 0 ? 0 : coord >= width ? width - 1 : coord;
2535
+ const wrapY = wrapEdges ? (coord) => (coord % height + height) % height : (coord) => coord < 0 ? 0 : coord >= height ? height - 1 : coord;
2536
+ for (let c = 0; c < source.channelCount; c++) {
2537
+ const srcChannel = source.channels[c];
2538
+ const dstChannel = result.channels[c];
2539
+ const temp = new Float32Array(width * height);
2540
+ for (let y = 0; y < height; y++) {
2541
+ const rowOffset = y * width;
2542
+ for (let x = 0; x < width; x++) {
2543
+ let acc = 0;
2544
+ for (let k = 0; k < kernelSize; k++) {
2545
+ const px = wrapX(x + k - halfKernel);
2546
+ acc += srcChannel[rowOffset + px] * kernel[k];
2547
+ }
2548
+ temp[rowOffset + x] = acc;
2549
+ }
2550
+ }
2551
+ for (let x = 0; x < width; x++) {
2552
+ for (let y = 0; y < height; y++) {
2553
+ let acc = 0;
2554
+ for (let k = 0; k < kernelSize; k++) {
2555
+ const py = wrapY(y + k - halfKernel);
2556
+ acc += temp[py * width + x] * kernel[k];
2557
+ }
2558
+ dstChannel[y * width + x] = acc;
2559
+ }
2560
+ }
2561
+ }
2562
+ return result;
2563
+ }
2564
+ render() {
2565
+ const buffer = this.toImageBuffer(this.image.value);
2566
+ if (!buffer) {
2567
+ return;
2568
+ }
2569
+ const method = this.props.method.value;
2570
+ const radius = this.props.radius.value;
2571
+ const wrapEdges = this.props.wrapEdges.value;
2572
+ const blurred = method === "gaussian" ? this.trueGaussianBlur(buffer, radius, wrapEdges) : this.boxBlur(buffer, radius, wrapEdges);
2573
+ this.setOutput(this.output, blurred);
2574
+ }
2575
+ };
2576
+
2577
+ // src/nodes/image/nodes/TextNode.ts
2578
+ var TextNode = class extends ImageNodeBase {
2579
+ constructor(id, graph) {
2580
+ super(id, "Text", graph);
2581
+ }
2582
+ setup() {
2583
+ this.addParm("text", {
2584
+ value: "Hello, World!",
2585
+ type: "textarea",
2586
+ displayName: "Text"
2587
+ });
2588
+ this.addParm("font", {
2589
+ value: "Inter, system-ui, sans-serif",
2590
+ type: "text",
2591
+ displayName: "Font"
2592
+ });
2593
+ this.addParm("fontSize", {
2594
+ value: 48,
2595
+ type: "slider",
2596
+ params: { min: 1, max: 500, step: 1 },
2597
+ displayName: "Font Size"
2598
+ });
2599
+ this.addParm("fontWeight", {
2600
+ value: "normal",
2601
+ type: "select",
2602
+ params: {
2603
+ options: [
2604
+ { value: "normal", label: "Normal" },
2605
+ { value: "bold", label: "Bold" },
2606
+ { value: "100", label: "Thin (100)" },
2607
+ { value: "200", label: "Extra Light (200)" },
2608
+ { value: "300", label: "Light (300)" },
2609
+ { value: "500", label: "Medium (500)" },
2610
+ { value: "600", label: "Semi Bold (600)" },
2611
+ { value: "700", label: "Bold (700)" },
2612
+ { value: "800", label: "Extra Bold (800)" },
2613
+ { value: "900", label: "Black (900)" }
2614
+ ]
2615
+ },
2616
+ displayName: "Font Weight"
2617
+ });
2618
+ this.addParm("fontStyle", {
2619
+ value: "normal",
2620
+ type: "select",
2621
+ params: {
2622
+ options: [
2623
+ { value: "normal", label: "Normal" },
2624
+ { value: "italic", label: "Italic" },
2625
+ { value: "oblique", label: "Oblique" }
2626
+ ]
2627
+ },
2628
+ displayName: "Font Style"
2629
+ });
2630
+ this.addParm("alignH", {
2631
+ value: "center",
2632
+ type: "select",
2633
+ params: {
2634
+ options: [
2635
+ { value: "left", label: "Left" },
2636
+ { value: "center", label: "Center" },
2637
+ { value: "right", label: "Right" }
2638
+ ]
2639
+ },
2640
+ displayName: "Horizontal Align"
2641
+ });
2642
+ this.addParm("alignV", {
2643
+ value: "middle",
2644
+ type: "select",
2645
+ params: {
2646
+ options: [
2647
+ { value: "top", label: "Top" },
2648
+ { value: "middle", label: "Middle" },
2649
+ { value: "bottom", label: "Bottom" }
2650
+ ]
2651
+ },
2652
+ displayName: "Vertical Align"
2653
+ });
2654
+ this.addParm("color", {
2655
+ value: { r: 1, g: 1, b: 1 },
2656
+ type: "color",
2657
+ displayName: "Color"
2658
+ });
2659
+ this.addParm("backgroundColor", {
2660
+ value: { r: 0, g: 0, b: 0, a: 0 },
2661
+ type: "color",
2662
+ displayName: "Background"
2663
+ });
2664
+ this.addResolutionParm();
2665
+ this.addParm("offset", {
2666
+ value: [0, 0],
2667
+ params: {
2668
+ min: [-2048, -2048],
2669
+ max: [2048, 2048],
2670
+ step: 1
2671
+ },
2672
+ displayName: "Offset"
2673
+ });
2674
+ this.addParm("rotation", {
2675
+ value: 0,
2676
+ type: "slider",
2677
+ params: { min: -180, max: 180, step: 0.1 },
2678
+ displayName: "Rotation"
2679
+ });
2680
+ this.addParm("letterSpacing", {
2681
+ value: 0,
2682
+ type: "slider",
2683
+ params: { min: -50, max: 100, step: 0.5 },
2684
+ displayName: "Letter Spacing"
2685
+ });
2686
+ this.addParm("lineHeight", {
2687
+ value: 1.2,
2688
+ type: "slider",
2689
+ params: { min: 0.5, max: 3, step: 0.05 },
2690
+ displayName: "Line Height"
2691
+ });
2692
+ this.addParm("opacity", {
2693
+ value: 1,
2694
+ type: "slider",
2695
+ params: { min: 0, max: 1, step: 0.01 },
2696
+ displayName: "Opacity"
2697
+ });
2698
+ this.output = this.out("image");
2699
+ const propNames = [
2700
+ "text",
2701
+ "font",
2702
+ "fontSize",
2703
+ "fontWeight",
2704
+ "fontStyle",
2705
+ "alignH",
2706
+ "alignV",
2707
+ "color",
2708
+ "backgroundColor",
2709
+ "offset",
2710
+ "rotation",
2711
+ "letterSpacing",
2712
+ "lineHeight",
2713
+ "opacity"
2714
+ ];
2715
+ propNames.forEach((prop) => this.watchProp(prop, () => this.requestCook()));
2716
+ this.onReady = () => this.requestCook();
2717
+ }
2718
+ render() {
2719
+ const [width, height] = this.getResolution();
2720
+ const text = this.props.text.value;
2721
+ const font = this.props.font.value;
2722
+ const fontSize = this.props.fontSize.value;
2723
+ const fontWeight = this.props.fontWeight.value;
2724
+ const fontStyle = this.props.fontStyle.value;
2725
+ const alignH = this.props.alignH.value;
2726
+ const alignV = this.props.alignV.value;
2727
+ const color = normalizeColor(this.props.color.value);
2728
+ const bgColor = normalizeColor(this.props.backgroundColor.value);
2729
+ const [offsetX, offsetY] = this.props.offset.value;
2730
+ const rotation = this.props.rotation.value;
2731
+ const letterSpacing = this.props.letterSpacing.value;
2732
+ const lineHeight = this.props.lineHeight.value;
2733
+ const opacity = this.props.opacity.value;
2734
+ const canvas = createSurface(width, height);
2735
+ const ctx = canvas.getContext("2d");
2736
+ if (!ctx) {
2737
+ this.setOutput(this.output, ImageBuffer.rgba(width, height));
2738
+ return;
2739
+ }
2740
+ if (bgColor.a > 0) {
2741
+ ctx.fillStyle = colorToCss(bgColor);
2742
+ ctx.fillRect(0, 0, width, height);
2743
+ } else {
2744
+ ctx.clearRect(0, 0, width, height);
2745
+ }
2746
+ ctx.save();
2747
+ ctx.globalAlpha = opacity;
2748
+ const fontString = `${fontStyle} ${fontWeight} ${fontSize}px ${font}`;
2749
+ ctx.font = fontString;
2750
+ ctx.fillStyle = colorToCss(color);
2751
+ const lines = text.split("\n");
2752
+ const lineHeightPx = fontSize * lineHeight;
2753
+ const totalTextHeight = lines.length * lineHeightPx;
2754
+ let anchorX;
2755
+ let anchorY;
2756
+ switch (alignH) {
2757
+ case "left":
2758
+ ctx.textAlign = "left";
2759
+ anchorX = 0;
2760
+ break;
2761
+ case "right":
2762
+ ctx.textAlign = "right";
2763
+ anchorX = width;
2764
+ break;
2765
+ case "center":
2766
+ default:
2767
+ ctx.textAlign = "center";
2768
+ anchorX = width / 2;
2769
+ break;
2770
+ }
2771
+ switch (alignV) {
2772
+ case "top":
2773
+ anchorY = fontSize;
2774
+ break;
2775
+ case "bottom":
2776
+ anchorY = height - totalTextHeight + fontSize;
2777
+ break;
2778
+ case "middle":
2779
+ default:
2780
+ anchorY = (height - totalTextHeight) / 2 + fontSize;
2781
+ break;
2782
+ }
2783
+ anchorX += offsetX;
2784
+ anchorY += offsetY;
2785
+ if (rotation !== 0) {
2786
+ const rotationRad = rotation * Math.PI / 180;
2787
+ ctx.translate(anchorX, anchorY - fontSize / 2 + totalTextHeight / 2);
2788
+ ctx.rotate(rotationRad);
2789
+ ctx.translate(-anchorX, -(anchorY - fontSize / 2 + totalTextHeight / 2));
2790
+ }
2791
+ if (letterSpacing !== 0) {
2792
+ ctx.textAlign = "left";
2793
+ for (let i = 0; i < lines.length; i++) {
2794
+ const line = lines[i];
2795
+ const y = anchorY + i * lineHeightPx;
2796
+ let lineWidth = 0;
2797
+ for (const char of line) {
2798
+ lineWidth += ctx.measureText(char).width + letterSpacing;
2799
+ }
2800
+ lineWidth -= letterSpacing;
2801
+ let x;
2802
+ switch (alignH) {
2803
+ case "left":
2804
+ x = anchorX;
2805
+ break;
2806
+ case "right":
2807
+ x = anchorX - lineWidth;
2808
+ break;
2809
+ case "center":
2810
+ default:
2811
+ x = anchorX - lineWidth / 2;
2812
+ break;
2813
+ }
2814
+ for (const char of line) {
2815
+ ctx.fillText(char, x, y);
2816
+ x += ctx.measureText(char).width + letterSpacing;
2817
+ }
2818
+ }
2819
+ } else {
2820
+ for (let i = 0; i < lines.length; i++) {
2821
+ const y = anchorY + i * lineHeightPx;
2822
+ ctx.fillText(lines[i], anchorX, y);
2823
+ }
2824
+ }
2825
+ ctx.restore();
2826
+ const buffer = ImageBuffer.fromCanvas(canvas);
2827
+ this.setOutput(this.output, buffer);
2828
+ }
2829
+ };
2830
+
2831
+ // src/nodes/image/nodes/TransformNode.ts
2832
+ var TransformNode = class extends ImageNodeBase {
2833
+ constructor(id, graph) {
2834
+ super(id, "Transform", graph);
2835
+ }
2836
+ setup() {
2837
+ this.imageInput = this.in("image", null);
2838
+ this.addParm("translate", {
2839
+ value: [0, 0],
2840
+ params: {
2841
+ min: [-2048, -2048],
2842
+ max: [2048, 2048],
2843
+ step: 1
2844
+ },
2845
+ displayName: "Translate",
2846
+ onChange: () => this.requestCook()
2847
+ });
2848
+ this.addParm("rotation", {
2849
+ value: 0,
2850
+ params: {
2851
+ min: -180,
2852
+ max: 180,
2853
+ step: 0.1
2854
+ },
2855
+ displayName: "Rotation",
2856
+ onChange: () => this.requestCook()
2857
+ });
2858
+ this.addParm("scale", {
2859
+ value: [1, 1],
2860
+ params: {
2861
+ min: [0.01, 0.01],
2862
+ max: [10, 10],
2863
+ step: 0.01
2864
+ },
2865
+ displayName: "Scale",
2866
+ onChange: () => this.requestCook()
2867
+ });
2868
+ this.addParm("uniformScale", {
2869
+ value: true,
2870
+ type: "boolean",
2871
+ displayName: "Uniform Scale",
2872
+ onChange: () => this.requestCook()
2873
+ });
2874
+ this.addParm("pivot", {
2875
+ value: "center",
2876
+ params: {
2877
+ options: [
2878
+ { value: "center", label: "Center" },
2879
+ { value: "top-left", label: "Top Left" },
2880
+ { value: "top-right", label: "Top Right" },
2881
+ { value: "bottom-left", label: "Bottom Left" },
2882
+ { value: "bottom-right", label: "Bottom Right" },
2883
+ { value: "custom", label: "Custom" }
2884
+ ]
2885
+ },
2886
+ displayName: "Pivot",
2887
+ onChange: () => this.requestCook()
2888
+ });
2889
+ this.addParm("customPivot", {
2890
+ value: [0.5, 0.5],
2891
+ params: {
2892
+ min: [0, 0],
2893
+ max: [1, 1],
2894
+ step: 0.01
2895
+ },
2896
+ displayName: "Custom Pivot",
2897
+ hidden: () => this.props.pivot.value !== "custom",
2898
+ onChange: () => this.requestCook()
2899
+ });
2900
+ this.addParm("filter", {
2901
+ value: "bilinear",
2902
+ params: {
2903
+ options: [
2904
+ { value: "nearest", label: "Nearest" },
2905
+ { value: "bilinear", label: "Bilinear" }
2906
+ ]
2907
+ },
2908
+ displayName: "Filter",
2909
+ onChange: () => this.requestCook()
2910
+ });
2911
+ this.addParm("wrap", {
2912
+ value: "clamp",
2913
+ params: {
2914
+ options: [
2915
+ { value: "clamp", label: "Clamp" },
2916
+ { value: "repeat", label: "Repeat" },
2917
+ { value: "mirror", label: "Mirror" },
2918
+ { value: "transparent", label: "Transparent" }
2919
+ ]
2920
+ },
2921
+ displayName: "Wrap Mode",
2922
+ onChange: () => this.requestCook()
2923
+ });
2924
+ this.output = this.out("image");
2925
+ this.imageInput.onChange = () => this.requestCook();
2926
+ this.onReady = () => this.requestCook();
2927
+ }
2928
+ getPivotPoint(width, height) {
2929
+ const pivot = this.props.pivot.value;
2930
+ switch (pivot) {
2931
+ case "center":
2932
+ return [width / 2, height / 2];
2933
+ case "top-left":
2934
+ return [0, 0];
2935
+ case "top-right":
2936
+ return [width, 0];
2937
+ case "bottom-left":
2938
+ return [0, height];
2939
+ case "bottom-right":
2940
+ return [width, height];
2941
+ case "custom":
2942
+ const [px, py] = this.props.customPivot.value;
2943
+ return [px * width, py * height];
2944
+ default:
2945
+ return [width / 2, height / 2];
2946
+ }
2947
+ }
2948
+ wrapCoord(coord, size, mode) {
2949
+ if (mode === "clamp") {
2950
+ return Math.max(0, Math.min(size - 1, coord));
2951
+ } else if (mode === "repeat") {
2952
+ coord = coord % size;
2953
+ if (coord < 0) coord += size;
2954
+ return coord;
2955
+ } else if (mode === "mirror") {
2956
+ const period = size * 2;
2957
+ coord = Math.abs(coord % period);
2958
+ if (coord >= size) coord = period - coord - 1;
2959
+ return Math.max(0, Math.min(size - 1, coord));
2960
+ }
2961
+ return coord;
2962
+ }
2963
+ render() {
2964
+ const input = this.toImageBuffer(this.imageInput.value);
2965
+ if (!input) {
2966
+ return;
2967
+ }
2968
+ const width = input.width;
2969
+ const height = input.height;
2970
+ const [tx, ty] = this.props.translate.value;
2971
+ const rotation = this.props.rotation.value * Math.PI / 180;
2972
+ let [sx, sy] = this.props.scale.value;
2973
+ const uniformScale = this.props.uniformScale.value;
2974
+ const filterMode = this.props.filter.value;
2975
+ const wrapMode = this.props.wrap.value;
2976
+ if (uniformScale) {
2977
+ sy = sx;
2978
+ }
2979
+ const [pivotX, pivotY] = this.getPivotPoint(width, height);
2980
+ const cosR = Math.cos(-rotation);
2981
+ const sinR = Math.sin(-rotation);
2982
+ const invSx = 1 / sx;
2983
+ const invSy = 1 / sy;
2984
+ const buffer = ImageBuffer.rgba(width, height);
2985
+ const channelCount = Math.min(input.channelCount, 4);
2986
+ for (let y = 0; y < height; y++) {
2987
+ for (let x = 0; x < width; x++) {
2988
+ let dx = x - pivotX - tx;
2989
+ let dy = y - pivotY - ty;
2990
+ const rx = dx * cosR - dy * sinR;
2991
+ const ry = dx * sinR + dy * cosR;
2992
+ const srcX = rx * invSx + pivotX;
2993
+ const srcY = ry * invSy + pivotY;
2994
+ if (wrapMode === "transparent") {
2995
+ if (srcX < 0 || srcX >= width || srcY < 0 || srcY >= height) {
2996
+ const idx2 = y * width + x;
2997
+ for (let c = 0; c < 4; c++) {
2998
+ buffer.channels[c][idx2] = c === 3 ? 0 : 0;
2999
+ }
3000
+ continue;
3001
+ }
3002
+ }
3003
+ const idx = y * width + x;
3004
+ if (filterMode === "nearest") {
3005
+ const sx0 = Math.round(srcX);
3006
+ const sy0 = Math.round(srcY);
3007
+ const wx = this.wrapCoord(sx0, width, wrapMode);
3008
+ const wy = this.wrapCoord(sy0, height, wrapMode);
3009
+ const srcIdx = wy * width + wx;
3010
+ for (let c = 0; c < channelCount; c++) {
3011
+ buffer.channels[c][idx] = input.channels[c][srcIdx];
3012
+ }
3013
+ if (channelCount < 4) {
3014
+ buffer.channels[3][idx] = 1;
3015
+ }
3016
+ } else {
3017
+ const x0 = Math.floor(srcX);
3018
+ const y0 = Math.floor(srcY);
3019
+ const x1 = x0 + 1;
3020
+ const y1 = y0 + 1;
3021
+ const fx = srcX - x0;
3022
+ const fy = srcY - y0;
3023
+ const wx0 = this.wrapCoord(x0, width, wrapMode);
3024
+ const wx1 = this.wrapCoord(x1, width, wrapMode);
3025
+ const wy0 = this.wrapCoord(y0, height, wrapMode);
3026
+ const wy1 = this.wrapCoord(y1, height, wrapMode);
3027
+ const idx00 = wy0 * width + wx0;
3028
+ const idx10 = wy0 * width + wx1;
3029
+ const idx01 = wy1 * width + wx0;
3030
+ const idx11 = wy1 * width + wx1;
3031
+ for (let c = 0; c < channelCount; c++) {
3032
+ const v00 = input.channels[c][idx00];
3033
+ const v10 = input.channels[c][idx10];
3034
+ const v01 = input.channels[c][idx01];
3035
+ const v11 = input.channels[c][idx11];
3036
+ const v0 = v00 + (v10 - v00) * fx;
3037
+ const v1 = v01 + (v11 - v01) * fx;
3038
+ buffer.channels[c][idx] = v0 + (v1 - v0) * fy;
3039
+ }
3040
+ if (channelCount < 4) {
3041
+ buffer.channels[3][idx] = 1;
3042
+ }
3043
+ }
3044
+ }
3045
+ }
3046
+ buffer.markDirty();
3047
+ this.setOutput(this.output, buffer);
3048
+ }
3049
+ };
3050
+
3051
+ // src/nodes/image/classes.ts
3052
+ var imageNodeClasses = {
3053
+ "Color": ColorNode,
3054
+ "Image": ImageNode,
3055
+ "Composite": CompositeNode,
3056
+ "Checkers": CheckersNode,
3057
+ "Resize": ResizeNode,
3058
+ "NormalMap": NormalMapNode,
3059
+ "Ramp": RampNode,
3060
+ "Noise": NoiseNode,
3061
+ "Blur": BlurNode,
3062
+ "Text": TextNode,
3063
+ "Transform": TransformNode
3064
+ };
3065
+
3066
+ // src/nodes/registerStandardNodes.ts
3067
+ var registered = false;
3068
+ function registerStandardNodes() {
3069
+ if (registered) return;
3070
+ registerNodeClasses("core", coreNodeClasses);
3071
+ registerNodeClasses("image", imageNodeClasses);
3072
+ registered = true;
3073
+ }
3074
+ function standardNodesRegistered() {
3075
+ return registered;
3076
+ }
3077
+ export {
3078
+ registerStandardNodes,
3079
+ standardNodesRegistered
3080
+ };
3081
+ //# sourceMappingURL=registerStandardNodes-S2VLMF6J.js.map