@field/cascade 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -4
- package/ARCHITECTURE.md +5 -4
- package/CHANGELOG.md +195 -0
- package/README.md +1 -1
- package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
- package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
- package/dist/assets/{CodeEditor-4O7FQ__R.js → CodeEditor-C-xYG-Wn.js} +120 -120
- package/dist/assets/CodePanel-CtXd50m6.js +2 -0
- package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
- package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
- package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
- package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
- package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
- package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
- package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
- package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
- package/dist/assets/{SettingsDialog-Bz8JxISH.js → SettingsDialog-CBX3Wa87.js} +1 -1
- package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
- package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
- package/dist/assets/{VersionHistoryDialog-D5Z0nJ5-.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
- package/dist/assets/assert-BO2_erPD.js +1 -0
- package/dist/assets/assert-hvykGCnP.js +1 -0
- package/dist/assets/builder-BmBKcDix.js +1 -0
- package/dist/assets/camera-DFF7YA3F.js +1 -0
- package/dist/assets/checks-DAvyO9Gf.js +1 -0
- package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
- package/dist/assets/{core-Yqi0Ni61.js → core-DB-MkGjj.js} +3 -4
- package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
- package/dist/assets/{cssMode-CxUzc4vp.js → cssMode-DPVckSOc.js} +1 -1
- package/dist/assets/definitions-B227e7f_.js +1 -0
- package/dist/assets/definitions-D0WOUJd7.js +1 -0
- package/dist/assets/{editor.api-BNFb25CX.js → editor.api-Dkutg_Z1.js} +1 -1
- package/dist/assets/expressions-X-Z-GrgW.js +5 -0
- package/dist/assets/file-DAL6IfjV.js +1 -0
- package/dist/assets/filters-D3k1LRMa.js +1 -0
- package/dist/assets/{freemarker2-0ZDnWxeM.js → freemarker2-CLiLKk3B.js} +1 -1
- package/dist/assets/generators-BhDxRAFC.js +1 -0
- package/dist/assets/geo-BsMImJIJ.js +1 -0
- package/dist/assets/geo-C4YngDob.js +2 -0
- package/dist/assets/{handlebars-Cy18MoaD.js → handlebars-CcD3aMeL.js} +1 -1
- package/dist/assets/{html-B-2Dx7nq.js → html-DvZWGfqb.js} +1 -1
- package/dist/assets/{htmlMode-DUAc9B_d.js → htmlMode-DUD3VaoN.js} +1 -1
- package/dist/assets/image-0SfH-snh.js +1 -0
- package/dist/assets/image-Ck-RdGU4.js +2 -0
- package/dist/assets/index-BWnrmIB9.js +137 -0
- package/dist/assets/index-DIaxEONI.css +1 -0
- package/dist/assets/io-BGmt2g0D.js +1 -0
- package/dist/assets/{javascript-DeQsDqP5.js → javascript-Cap2ahgh.js} +1 -1
- package/dist/assets/{jsonMode-BiFwJVSk.js → jsonMode-B0uLjp9C.js} +1 -1
- package/dist/assets/{liquid-BLoHH68a.js → liquid-CnxwhfLn.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-CNFgm1Kv.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
- package/dist/assets/math-GeULS-sk.js +1 -0
- package/dist/assets/{mdx-CFXQVNfs.js → mdx-BV3jbrVa.js} +1 -1
- package/dist/assets/pop-KMF_qloi.js +1 -0
- package/dist/assets/preload-helper-Czpn1I53.js +1 -0
- package/dist/assets/{python-Cf7S7p2E.js → python-CGyFk4XK.js} +1 -1
- package/dist/assets/{razor-CMHBcG-g.js → razor-CQfzX3-N.js} +1 -1
- package/dist/assets/remap-5SjXQU-q.js +1 -0
- package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
- package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
- package/dist/assets/text-ZpX7qNkx.js +2 -0
- package/dist/assets/time-P_OiBhcf.js +1 -0
- package/dist/assets/{tsMode-BEih0GDj.js → tsMode-BOeXzO9H.js} +1 -1
- package/dist/assets/{typescript-COEKYD_a.js → typescript-J1SnE_Tu.js} +1 -1
- package/dist/assets/{workers-BNhAhMNn.js → workers-CYwwTiVF.js} +1 -1
- package/dist/assets/{xml-CPiMpnPl.js → xml-DnPRNzK9.js} +1 -1
- package/dist/assets/{yaml-Bgp1ITLR.js → yaml-BmmYBNa9.js} +1 -1
- package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
- package/dist/cli/{Graph-ZJJTRQTM.js → Graph-OB6EWSGL.js} +12 -12
- package/dist/cli/assert-GROGQNCQ.js +25 -0
- package/dist/cli/checks-3WB44XBL.js +189 -0
- package/dist/cli/{chunk-RQZ4SX5R.js → chunk-2JEU3MGB.js} +2 -2
- package/dist/cli/{chunk-WKURDVYP.js → chunk-3RXL5GHB.js} +28 -331
- package/dist/cli/chunk-53LGFPWX.js +138 -0
- package/dist/cli/chunk-5ORUQ6ES.js +267 -0
- package/dist/cli/chunk-73S4JZXK.js +336 -0
- package/dist/cli/{chunk-YAT5Y25U.js → chunk-7QE2ZNSN.js} +3 -3
- package/dist/cli/{chunk-SGJAET2L.js → chunk-BPODRH52.js} +8 -4
- package/dist/cli/{chunk-XWDSKG7N.js → chunk-BZ63XQ3I.js} +2 -2
- package/dist/cli/{chunk-FCDXJ6EK.js → chunk-CTPLRSWF.js} +2 -13
- package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
- package/dist/cli/{chunk-WIH4TEIS.js → chunk-EZRWJ4G2.js} +1 -1
- package/dist/cli/{chunk-BIJO5OFS.js → chunk-FWMSGKEH.js} +1 -1
- package/dist/cli/{chunk-POMMNKZZ.js → chunk-G23A2BZZ.js} +38 -5
- package/dist/cli/{chunk-35D343DC.js → chunk-GRIN2DDP.js} +19 -1
- package/dist/cli/chunk-JVOCK5W4.js +83 -0
- package/dist/cli/chunk-KBGUO27Q.js +15 -0
- package/dist/cli/{chunk-GFS6W3C5.js → chunk-L6B6FUQP.js} +54 -32
- package/dist/cli/chunk-LTXWBXOM.js +438 -0
- package/dist/cli/chunk-MI5JLQYA.js +220 -0
- package/dist/cli/{chunk-EUIAC3U3.js → chunk-MP653A5O.js} +884 -124
- package/dist/cli/{chunk-A4OAMOZH.js → chunk-NJFMDXHR.js} +13 -3
- package/dist/cli/{chunk-3JTJHF7V.js → chunk-OM6BEME6.js} +4 -4
- package/dist/cli/{chunk-DUIUFVU2.js → chunk-ST6LROUG.js} +49 -23
- package/dist/cli/{chunk-RLCAJBVR.js → chunk-TJW67QSI.js} +8 -7
- package/dist/cli/chunk-WGKPJZDY.js +364 -0
- package/dist/cli/{chunk-D6BUOHUG.js → chunk-X4WKAOQP.js} +4 -4
- package/dist/cli/{chunk-VVGSLVSF.js → chunk-X566FWBV.js} +6 -1
- package/dist/cli/chunk-YH637PDL.js +317 -0
- package/dist/cli/chunk-ZSCUZE6I.js +112 -0
- package/dist/cli/{cliCommands-UQSFGEHF.js → cliCommands-JJN5Z3ZC.js} +23 -17
- package/dist/cli/{commands-Q23M26QM.js → commands-PHYFYA3F.js} +129 -47
- package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
- package/dist/cli/{definitions-HF2FTHYU.js → definitions-BPOPW52L.js} +7 -7
- package/dist/cli/{doctorCommand-B7BPQMN6.js → doctorCommand-ORWQRNYP.js} +36 -6
- package/dist/cli/{edit-JHCPTQFT.js → edit-S5KEQ5RA.js} +3 -3
- package/dist/cli/editorCommands-NFR32JOY.js +41 -0
- package/dist/cli/{encode-HAUZTYPT.js → encode-ICPEPMH5.js} +2 -2
- package/dist/cli/file-3HTQHPYQ.js +41 -0
- package/dist/cli/filters-6ODFWYK4.js +379 -0
- package/dist/cli/generators-YU7GB4MD.js +210 -0
- package/dist/cli/{geo-4RNK3QVN.js → geo-6ZPGKZRU.js} +7 -7
- package/dist/cli/{graphCommands-6BMAKGQE.js → graphCommands-EOUOBOIJ.js} +4 -4
- package/dist/cli/{graphFile-VPSQ6OF7.js → graphFile-YTGJB5F5.js} +25 -19
- package/dist/cli/{headlessCanvas-5XLDZT7M.js → headlessCanvas-ENRHC7GS.js} +3 -2
- package/dist/cli/{helpCommand-3VRE3KVO.js → helpCommand-PULH6ZYB.js} +9 -7
- package/dist/cli/image-TXELZZK5.js +55 -0
- package/dist/cli/index.js +14 -14
- package/dist/cli/{math-ZRMLKU2D.js → math-RSYRQ3C6.js} +5 -5
- package/dist/cli/{mcp-ZLEEMYGF.js → mcp-JVZFQOEX.js} +4 -4
- package/dist/cli/{parity-KINARCP2.js → parity-X6DEH2DV.js} +42 -36
- package/dist/cli/{pop-USDFXB7H.js → pop-B7SIIYKN.js} +5 -5
- package/dist/cli/project-VYGIA7JX.js +12 -0
- package/dist/cli/{projectDefinition-DMTZN2NE.js → projectDefinition-F3D7UIHJ.js} +8 -8
- package/dist/cli/{projectRuntime-Z5PJOH47.js → projectRuntime-2Q4JKEH3.js} +19 -12
- package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
- package/dist/cli/{render-YURAPPUI.js → render-PT4MWI6U.js} +24 -22
- package/dist/cli/{runCommand-PI764LEG.js → runCommand-HNPQVZMS.js} +3 -3
- package/dist/cli/runner-5XPXVW7J.js +43 -0
- package/dist/cli/runtime/image.ts +27 -2
- package/dist/cli/runtime/io.ts +78 -0
- package/dist/cli/share-MHF6TNRK.js +166 -0
- package/dist/cli/stageCache-TY2BFZJR.js +15 -0
- package/dist/cli/{stageRunner-KUNIMQKJ.js → stageRunner-KY2EJP7X.js} +2 -2
- package/dist/cli/text-MB3EKR4H.js +126 -0
- package/dist/cli/{verbs-RCE3K4VQ.js → verbs-PI2JH5FI.js} +2 -2
- package/dist/cli/webBuild-WPPMGGW7.js +44 -0
- package/dist/contracts/capabilities.d.ts +18 -1
- package/dist/contracts/color.d.ts +8 -0
- package/dist/contracts/color.js +23 -0
- package/dist/contracts/definition.d.ts +10 -0
- package/dist/contracts/schema.d.ts +1 -1
- package/dist/contracts/schema.js +1 -0
- package/dist/contracts/validate.js +11 -1
- package/dist/contracts/values.d.ts +24 -0
- package/dist/image.d.ts +15 -0
- package/dist/image.js +36 -2
- package/dist/index.html +8 -2
- package/dist/io.d.ts +29 -0
- package/dist/io.js +40 -0
- package/dist/player/app.js +3 -2
- package/dist/player/editor.js +3 -2
- package/dist/player/editorEngine.js +3 -2
- package/dist/runtime/builtins/assert/checks.d.ts +18 -0
- package/dist/runtime/builtins/assert/checks.js +207 -0
- package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
- package/dist/runtime/builtins/assert/definitions.js +54 -0
- package/dist/runtime/builtins/assert/index.d.ts +152 -0
- package/dist/runtime/builtins/assert/index.js +36 -0
- package/dist/runtime/builtins/image/contactSheet.d.ts +3 -165
- package/dist/runtime/builtins/image/contactSheet.js +0 -49
- package/dist/runtime/builtins/image/definitions.d.ts +950 -0
- package/dist/runtime/builtins/image/definitions.js +343 -0
- package/dist/runtime/builtins/image/file.d.ts +8 -0
- package/dist/runtime/builtins/image/file.js +37 -0
- package/dist/runtime/builtins/image/filters.d.ts +8 -0
- package/dist/runtime/builtins/image/filters.js +394 -0
- package/dist/runtime/builtins/image/generators.d.ts +7 -0
- package/dist/runtime/builtins/image/generators.js +144 -0
- package/dist/runtime/builtins/image/index.d.ts +947 -4
- package/dist/runtime/builtins/image/index.js +41 -6
- package/dist/runtime/builtins/image/io.d.ts +100 -0
- package/dist/runtime/builtins/image/io.js +142 -0
- package/dist/runtime/builtins/image/planar.d.ts +55 -0
- package/dist/runtime/builtins/image/planar.js +274 -0
- package/dist/runtime/builtins/image/simplex.d.ts +22 -0
- package/dist/runtime/builtins/image/simplex.js +92 -0
- package/dist/runtime/builtins/image/text.d.ts +4 -0
- package/dist/runtime/builtins/image/text.js +141 -0
- package/dist/runtime/builtins/index.d.ts +3 -0
- package/dist/runtime/builtins/index.js +7 -0
- package/dist/runtime/gpu.d.ts +25 -0
- package/dist/runtime/gpu.js +40 -0
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.js +268 -44
- package/dist/runtime/types.d.ts +30 -0
- package/doc/CLI_REFERENCE.md +21 -3
- package/doc/EDITOR_EXPORT.md +26 -0
- package/doc/MCP.md +2 -1
- package/doc/NODE_REFERENCE.md +286 -14
- package/doc/PROJECT_AUTHORING.md +98 -3
- package/package.json +4 -1
- package/dist/assets/CodePanel-BU-autmq.js +0 -2
- package/dist/assets/CustomNodeDialog-BRpDfOV5.js +0 -1
- package/dist/assets/OpenGraphDialog-BwbRlC0X.js +0 -1
- package/dist/assets/ProjectSettings-DLOXLxcA.js +0 -2
- package/dist/assets/definitions-GreEMXLV.js +0 -1
- package/dist/assets/geo-u0n_jOsD.js +0 -1
- package/dist/assets/image-cAa-Nu9O.js +0 -2062
- package/dist/assets/index-5ZtDAuy2.js +0 -220
- package/dist/assets/index-7vae6YFC.css +0 -1
- package/dist/assets/math-DioIQOPJ.js +0 -1
- package/dist/assets/pop-DC_TQG9t.js +0 -1
- package/dist/cli/chunk-36P5K3SD.js +0 -631
- package/dist/cli/chunk-4UDFYF5G.js +0 -132
- package/dist/cli/chunk-53NMBJAO.js +0 -35
- package/dist/cli/chunk-MIIEFUGG.js +0 -623
- package/dist/cli/chunk-QBBKRNGY.js +0 -90
- package/dist/cli/chunk-WPUDGEFY.js +0 -20
- package/dist/cli/editorCommands-UIF5YLUZ.js +0 -33
- package/dist/cli/project-K3QIPGBB.js +0 -12
- package/dist/cli/registerStandardNodes-S2VLMF6J.js +0 -3081
- package/dist/cli/runner-RA6UKEPD.js +0 -37
- package/dist/cli/webBuild-GT4PZT5D.js +0 -37
- package/dist/runtime/builtins/headless.d.ts +0 -19
- package/dist/runtime/builtins/headless.js +0 -25
|
@@ -1,3081 +0,0 @@
|
|
|
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
|