이 글은 텍스트 기반 코딩이 아닌 Visual Flow Programming (비쥬얼 플로우 프로그래밍) 도구를 소개한다. 여기서는 사용하기 편리한 node.js 기반 프로그래밍 도구를 공유한다.
NoFlo
NoFlo
NoFlo는 Node.js 브라우저 모두에서 실행되는 JavaScript 용 흐름 기반 프로그래밍 구현이다. NoFlo 구성요소는 ES6를 포함하여 JavaScript로 변환되는 모든 언어로 작성할 수 있다. 이 시스템은 J. Paul Morrison의 저서 Flow-Based Programming(1994)에서 크게 영감을 받았다.
J. Paul Morrison
NoFlo는 웹 프레임 워크 나 UI 툴킷이 아니다. JavaScript 애플리케이션에서 데이터 흐름을 조정하고 재구성하는 방법이다.
NoFlo는 웹 서버 및 도구 구축에서부터 GUI 애플리케이션 내부의 이벤트 조정, 로봇 구동 또는 인터넷에 연결된 예술 설치 구축에 이르기까지 모든 작업에 사용되고 있다.
NoFlo 자체는 JavaScript에서 흐름 기반 프로그램을 구현하기 위한 라이브러리이다. 설치 방법은 다음과 같으며, 소스 코드는 github에서 다운로드가능하다(Getting start).
npm install noflo --save
ComfyUI로 유명해진 노드 그래프 프로그래밍 라이브러리이다.
다음 기능을 제공한다.
- Canvas2D에서 렌더링(확대/축소 및 패닝, 복잡한 인터페이스를 쉽게 렌더링할 수 있으며 WebGLTexture 내에서 사용할 수 있음)
- 사용하기 쉬운 편집기 (검색 상자, 키보드 단축키, 다중 선택, 상황에 맞는 메뉴 등)
- 그래프당 수백 개의 노드를 지원하도록 최적화됨(편집기뿐만 아니라 실행 시에도 가능)
- 사용자 지정 가능한 테마(색상, 모양, 배경)
- 노드의 모든 작업/그리기/이벤트를 개인화하기 위한 콜백
- 하위 그래프(그래프 자체를 포함하는 노드)
- 라이브 모드 시스템 (그래프를 숨기지만 노드를 호출하여 원하는 대로 렌더링, UI 생성에 유용)
- 그래프는 NodeJS에서 실행할 수 있음.
- 사용자 정의 가능한 노드(색상, 모양, 슬롯 수직 또는 수평, 위젯, 사용자 지정 렌더링)
- 모든 JS 응용 프로그램에 쉽게 통합 할 수 있음 (하나의 단일 파일, 종속성 없음).
- Typescript 지원
Rete는 시각적 프로그래밍을 위한 모듈식 프레임 워크이다. Rete를 사용하면 브라우저에서 직접 노드 기반 편집기를 만들 수 있다. 사용자가 한 줄의 코드없이 편집기에서 데이터 처리 방식을 정의할 수 있다.
설치는 다음과 같다. 설명 문서는 여기를 참고하라.npm install rete rete-vue-render-plugin rete-connection-plugin
이 도구는 리액트 기반 플로우 프로그래밍 지원 프레임웍이다. 사용이 쉽고, 커스터마이징 가능하다. 빠른 렌더링과 다양한 플러그인 컴포넌트를 지원한다. 아울러, 신뢰성 높은 타입스트립트를 지원한다.
- Getting start react flow (sandbox example)
- Tutorial
- Your first start codesandbox
예제 실행은 다음과 같다.
코드샌드박스를 설치한다.
sudo npm install -g codesandbox
codesandbox 에서 https://codesandbox.io/s/6qgyt 예제 download 후 폴더에 압축 해제 후 적절한 node.js 버전을 설치한다.
sudo chown -R $USER /usr/local/lib/node_modules
sudo npm install -g n
sudo n 16.13.2
해당 패키지를 설치 후 실행한다.
sudo npm install --legacy-peer-deps
sudo npm i --force
PORT=3001 npm start
Drawflow를 사용하면 데이터 흐름을 쉽고 빠르게 만들 수 있다.
설치는 다음과 같다.
npm i drawflow
Reference
- Create custom node on node-red
- Rete.js sandbox
- Ryven - flow based programming using python
- Flow based programming using open source
- How we created Blocks: an online drag-and-drop smart contract editor.
- Top flow based programming open source projects
- Top Flow Based Programming Projects
- Flow based Programming
- 38 Flow based Programming
- Node based visual programming
- nodepen.io
- Total.js Platform
- Excalidraw
- Ryven using python
- pythonflow
- react-flow, litegraph
부록: litegraph 사용
이 부록은 간단한 그래프 및 속성창 추가 후, run, stop, load 클릭 시 적절히 동작하는 예제를 만든 것이다.
<html>
<head>
<title>custom lite graph app</title>
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="stylesheet" type="text/css" href="litegraph.css">
<link rel="stylesheet" type="text/css" href="litegraph-editor.css">
<script type="text/javascript" src="litegraph.js"></script>
<script type="text/javascript" src="litegraph-editor.js"></script>
<style>
#propertyPane {
position: absolute;
top: 10px;
left: 10px;
width: 200px;
padding: 10px;
border: 1px solid #ccc;
background-color: #333; /* Dark gray background */
color: #ccc; /* Light gray text */
resize: both;
overflow: auto;
}
#propertyPane input {
background-color: #555; /* Darker gray for input background */
color: #ccc; /* Light gray text for input */
border: 1px solid #777; /* Border color for input */
}
#propertyPane label {
color: #ccc; /* Light gray text for labels */
}
</style>
</head>
<body style='width:100%; height:100%'>
<canvas id='mycanvas' width="1000" height="600"></canvas>
</br>
<button id="runButton">Run</button>
<button id="stopButton">Stop</button>
<button id="loadSoundButton">Load Sound Graph</button>
<button id="saveButton">Save Graph JSON</button>
<button id="loadGraphButton">Load Graph JSON</button>
<div id="propertyPane">
<h4>Properties</h4>
<div id="properties"></div>
</div>
<script>
class custom_multi_node {
constructor(){
this.title = "Multiplication";
this.addInput("A","number");
this.addInput("B","number");
this.addOutput("A*B","number");
this.properties = { precision: 0.1 };
this.weightHandle = this.addWidget("slider","Weight", 100, {min: 0, max: 400, step: 10, precision: 0})
this.addWidget("combo","flag", "on", {values: ["on","off"]})
this.addWidget("text", "Address", "macross flashback")
this.addWidget("button","Click me", "", function() { alert("button clicked") })
this.serialize_widgets = true
}
onExecute(){
let a = this.getInputData(0) || 0;
let b = this.getInputData(1) || 0;
let result = a * b * this.properties.precision * this.weightHandle.value;
this.setOutputData(0, result);
}
onPropertyChanged(name, value) {
console.log(`Property ${name} changed to ${value}`);
// alert(`Property ${name} changed to ${value}`);
}
}
class custom_visual_data {
count = 0;
bigdata_result = null;
constructor(){
this.title = "custom_visual_data";
this.addInput("A","number");
this.addOutput("A","number");
this.properties = { scale: 1.0 };
this.size = [250, 70];
}
onExecute(){
let a = this.getInputData(0) || 0;
a = a * this.properties.scale;
this.setOutputData(0, a);
this.count++;
this.count = this.count % 256;
this.setDirtyCanvas(true);
// this.trigger("onDrawBackground");
this.bigdata_result = null;
calculate_bigdata(this);
}
finish_bigdata(dataset) {
this.bigdata_result = dataset;
console.log("Finished async data calculation");
}
onAdded(){
}
onRemoved(){
}
onDropFile(file){
alert("File dropped: " + file.name);
}
onDrawBackground(ctx){
ctx.save(); // Save the current state
ctx.beginPath();
ctx.rect(0, 0, this.size[0], this.size[1]); // Define the clipping region
ctx.clip();
let blue = Math.max(0, 255 - this.count % 256);
let orange = Math.min(255, this.count % 256);
ctx.fillStyle = `rgb(${orange}, ${Math.floor(orange / 2)}, ${blue})`; // Smooth transition from blue to orange
ctx.strokeStyle = "#808080"; // Gray color
ctx.beginPath();
ctx.arc(50,20,10,0,Math.PI*2);
if(this.bigdata_result){
ctx.font = "12px Arial";
ctx.fillStyle = "orange";
ctx.fillText("BigData: " + this.bigdata_result, 40, 50);
}
ctx.fill();
ctx.stroke();
ctx.restore(); // Restore the previous state */
}
}
async function processLargeData(data) {
return new Promise((resolve) => {
console.log("starting async data processing");
setTimeout(() => {
const processedData = data.map((item) => item * 2); // data double scale
console.log("finished async data processing");
resolve(processedData);
}, 3000); // processing time estimation
});
}
async function calculate_bigdata(node) {
const data = [1, 2, 3, 4, 5].map(item => item + node.count); // input data for example
const processedData = await processLargeData(data); // async data processing
console.log("Processed data:", processedData);
node.finish_bigdata(processedData);
}
LiteGraph.registerNodeType("custom/multiply", custom_multi_node);
LiteGraph.registerNodeType("custom/VisualData", custom_visual_data);
var graph = new LGraph();
var canvas = new LGraphCanvas("#mycanvas", graph);
graph.allow_scripts = true;
canvas.allow_searchbox = true;
/*
update_canvas_HiPPI();
window.addEventListener("resize", function() {
canvas.resize();
update_canvas_HiPPI();
});
function update_canvas_HiPPI() {
const ratio = window.devicePixelRatio;
if(ratio == 1)
return;
const rect = editor.canvas.parentNode.getBoundingClientRect();
const { width, height } = rect;
canvas.width = width * ratio;
canvas.height = height * ratio;
canvas.style.width = width + "px";
canvas.style.height = height + "px";
canvas.getContext("2d").scale(ratio, ratio);
return canvas;
}
*/
var nodeConstA = LiteGraph.createNode("basic/const");
nodeConstA.pos = [100,200];
nodeConstA.setValue(4.5);
graph.add(nodeConstA);
var nodeConstB = LiteGraph.createNode("basic/const");
nodeConstB.pos = [100,300];
nodeConstB.setValue(1.5);
graph.add(nodeConstB);
var nodeMult = LiteGraph.createNode("custom/multiply");
nodeMult.pos = [350,200];
graph.add(nodeMult);
var nodeWatch = LiteGraph.createNode("basic/watch");
nodeWatch.pos = [600,200];
graph.add(nodeWatch);
var nodeAsync = LiteGraph.createNode("custom/VisualData");
nodeAsync.pos = [600,300];
graph.add(nodeAsync);
// All nodes must be in the graph before connections can be made.
nodeConstA.connect(0, nodeMult, 0);
nodeConstB.connect(0, nodeMult, 1);
nodeMult.connect(0, nodeWatch, 0);
nodeMult.connect(0, nodeAsync, 0);
// Add event listener to the run button
document.getElementById("runButton").addEventListener("click", function() {
graph.start();
});
// Add event listener to the stop button
document.getElementById("stopButton").addEventListener("click", function() {
graph.stop();
});
// Add event listener to show properties when a node is clicked
canvas.onNodeSelected = function(node) {
var propertiesDiv = document.getElementById("properties");
propertiesDiv.innerHTML = "";
for (var property in node.properties) {
if (node.properties.hasOwnProperty(property)) {
var input = document.createElement("input");
input.type = "text";
input.value = node.properties[property];
input.onchange = (function(property, node) {
return function(event) {
node.setProperty(property, event.target.value);
};
})(property, node);
var label = document.createElement("label");
label.innerText = property;
propertiesDiv.appendChild(label);
propertiesDiv.appendChild(input);
propertiesDiv.appendChild(document.createElement("br"));
}
}
};
document.getElementById("loadSoundButton").addEventListener("click", function() {
fetch('audio_delay.json')
.then(response => response.json())
.then(data => {
graph.configure(data);
alert('Sound graph loaded');
})
.catch(error => console.error('Error loading JSON:', error));
});
document.getElementById("loadGraphButton").addEventListener("click", function() {
fetch('saved_graph.json')
.then(response => response.json())
.then(data => {
graph.configure(data);
alert('saved_graph loaded');
})
.catch(error => {
console.error('Error loading JSON:', error);
alert('Error loading JSON: ' + error.message);
});
});
document.getElementById("saveButton").addEventListener("click", function() {
var data = JSON.stringify(graph.serialize());
var blob = new Blob([data], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'saved_graph.json';
a.click();
URL.revokeObjectURL(url);
});
</script>
</body>
</html>
전체 화면에 그래프 캔버스 크기를 맞출려면, 위 소스에서 update_canvas_HiPPI() 부분 함수 리마크 처리를 해제한다.
결과는 다음과 같다.
참고로, JSON 은 위젯 및 속성 리스트를 담고 있다.
{
"last_node_id": 7,
"last_link_id": 7,
"nodes": [
{
"id": 6,
"type": "widget/knob",
"pos": [
199,
296
],
"size": [
64,
84
],
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "",
"type": "number",
"links": [
6
]
}
],
"properties": {
"min": 0,
"max": 2,
"value": 0.8799999999999999,
"color": "#7AF",
"precision": 2,
"wcolor": "#7AF",
"size": 50
},
"boxcolor": "rgba(112,112,112,1.0)"
}
]
}
라이트그래프 레퍼런스
- mcejp/graphexec: Execute litegraph.js graphs server-side
- LiteGraph Example / j.carson | Observable
- Theming | Flume
- Learn LiteGraph with Examples - YouTube
- Building AI With Ollama and Django
- LiteGraph: drawing on canvas - YouTube
- Ollama + Fly GPUs - Show & Tell - Django Forum
- VJS documentation and LiteGraph document
- Litegraph.js Integration - Dash Python - Plotly Community Forum
대용량 데이터 비동기 처리 레퍼런스
Thank you for sharing the valuable information aboutbnode.js-based programming tool. For on demand Node JS Development Services, visit us.
답글삭제Thanks.
삭제교수님, 정보 정말 감사합니다. 최근에 nodered로 간단하게 데쉬보드 블럭을 추가하면서 이렇게 활용할 수 있는 다른 노드툴들이 뭐있나 싶었는데...정말 정리 잘해주셨네요. ^^ 정말 감사합니다.
답글삭제넵.
삭제