A node to node visual scripting.
This library built using ScarletsFrame to maintain it's performance and simplicity. If you want to start new project with the framework, it's better to use the template because it also have the compiler.
To use it on NodeJS, Deno, or other JavaScript runtime, you can export it to JSON and use interpreter-js. But it doesn't mean exporting is just like a magic, you also need to write registerNode
and registerInterface
on the target interpreter. But if this project is still alive, Blackprint may have a package manager where developer can port their nodes to another available interpreter.
Below are the list of programming language where Blackprint that's being planned.
Current priority is JavaScript Interpreter
Some breaking changes may happen to make it suitable for every language If you use the interpreter make sure to specify the version instead of 'latest'
- JavaScript Interpreter
- PHP Interpreter
- Python Interpreter
- C# Interpreter
- Crystal Interpreter
Blackprint Interpreter that being considered:
- Java (I'm curious how much memory it would take)
- Rust (Maybe)
Planned Code Generation:
- Abstract Syntax Tree
- Dockerfile
- Nginx Config
There are styles, template, and scripts that need to be loaded.
Blackprint only giving support on modern browser, because it's designed for the future.
<!-- Must be loaded first -->
<script src="https://cdn.jsdelivr.net/npm/blackprint-interpreter@latest/dist/interpreter.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/blackprint-sketch@latest/dist/blackprint.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/scarletsframe.es6.js"></script>
<script src="https://cdn.jsdelivr.net/npm/blackprint-sketch@latest/dist/blackprint.html.js"></script>
<script src="https://cdn.jsdelivr.net/npm/blackprint-sketch@latest/dist/blackprint.min.js"></script>
Warning: This project haven't reach it stable version
If you use this on your project please put a link or information to this repository.
Maybe someone skilled like you interested to improve this open source project.
And because of their contribution, you can enjoy the improved Blackprint :)
If something isn't working feel free to open an issue, but don't create an duplicate issue.
// Create Blackprint, `sketch` in this documentation will refer to this
var sketch = new Blackprint.Sketch();
// Get the container and attach it into the DOM (If you're not using sf-views for routing)
document.body.appendChild(sketch.cloneContainer());
Default node type that was used by example above is distributed along this library. For other node type, we can create our implementation on how that node will control the HTML like this example. So we use it like below:
An interface is designed for communicate the node handler with the HTML elements. Blackprint is using ScarletsFrame to help control the element templating system.
// -> (HTML template path, options, callback)
// Path in window.templates
Blackprint.registerInterface('Blackprint/nodes/default', {
// `iface` will extend from Blackprint.Node (Optional)
extend: Blackprint.Node,
}, function(iface){
// iface = ScarletsFrame component handler (that control the HTML element)
// If the element would have value that can be exported to JSON
// It must being set inside options object
iface.options = {};
// Run after this component was initialized
iface.init = function(){
// You can use it like jQuery
iface.$el('.button').on('click', function(ev){
// We call the node handler that using this component
// `handle` from sketch.registerNode('', (handle, node)=>{})
iface.handle.onclicked(ev);
});
}
});
// Small example for using registered element above
// -> (namespace, callback)
Blackprint.registerNode('myspace/button', function(node, iface){
// Use node handler from sketch.registerInterface('Blackprint/nodes/default')
iface.interface = 'Blackprint/nodes/default';
iface.title = "My simple button";
// Called after `.button` have been clicked
node.onclicked = function(ev){
console.log("Henlo", ev);
}
});
This is where we register our logic with Blackprint.
This supposed to run on NodeJS too, so don't control any HTML element inside this.
And don't declare or use variable outside of function context/scope.
Just let the node
control your handle
.
// Register a new node
// -> (namespace, callback)
Blackprint.registerNode('math/multiply', function(node, iface){
// node = Blackprint flow handler
// iface = ScarletsFrame element handler
// Give it a title
iface.title = "Random";
// Give it a description
iface.description = "Multiply something";
// If the node would have value that can be exported to JSON
// It must being set inside options object
iface.options = {};
//> We will use `default` node type, so let this empty or undefined
// iface.interface = 'bp/default';
// ... Other information below ...
});
node
here is the Blackprint flow handler from the example above.
Property | Description |
---|---|
inputs | An array of input port registration |
outputs | An array of output port registration |
properties | An array of node property registration Still draft feature |
importing | A boolean indicating if this node is being imported/created |
Below are reserved property that filled with function/callback
Property | Arguments | Description |
---|---|---|
init | () |
Callback function to be run after current handle and all node was initialized |
request | (targetPort, sourceNode) |
Callback when other node's input port are requesting current node's output value |
update | (Cable) |
Callback when current input value are updated from the other node's output port |
imported | (options) |
This is a callback after node was created, imported options should be handled here |
For the detailed example you can see from this repository.
The port must be registered on the node
and Blackprint will create internal control with ScarletsFrame so the port can being used for sending or obtaining data from other node's port.
// ========= Port Format ==========
// ... = { PortName: DataType }
// Output port
node.outputs = {
// Declare Result as a output with Number data type
Result : Number,
// Declare Finish for trigger to other connected port's input callback
Finish : Function
// Output can have many connection into the related input data type
};
// Input port
// Let's declare as `inputs` variable too, for easy access
var inputs = node.inputs = {
// Declare A and validate this input as Number data type
A : Number, // Primitive type can only have one cable connected
// Declare B that will being called if any value
// on connected output port was updated
B : Blackprint.PortValidator(Number, function(value, Port){
// value === Port.value
}),
// Declare Start as a callback to start the multiplication
// Can have many cable connected
Start : function(/* arguments */){
// arguments are passed from the caller/output port of connected node
// Let's call declared function below
startMultiply();
},
// Accept Array of Number, can have many cable connected
C : Blackprint.PortArrayOf(Number),
};
function startMultiply(){
node.outputs.Result = inputs.A * inputs.B;
node.outputs.Finish("We can send", "arguments too");
}
Node event can be registered after the node was initialized.
To register a callback for an event you need to call node.on('event.name', function(args){})
.
Event Name | Arguments | Description |
---|---|---|
cable.connect | (Port, OtherPort, Cable) |
Trigger when a cable was connected to a port, isOwner is boolean indicating if it was created from current node |
cable.disconnect | (Port, OtherPort, Cable) |
Trigger when a cable was disconnected from a port |
cable.created | (Port, OtherPort, Cable) |
Trigger when a cable was created from a port |
port.menu | (Port, DropDowns}) |
Trigger when port menu is going to be created |
node.menu | (DropDowns) |
Trigger when node menu is going to be created |
Arguments on the table above with {...}
is a single object.
DropDowns
is an array, and you can push a callback or nested menu inside it.
iface.on('port.menu', function(data){
data.menu.push({
title:"With callback",
callback:function(){...}
});
data.menu.push({
title:"Callback with arguments",
args:[1, 2],
callback:function(one, two){...}
});
data.menu.push({
title:"Callback with context",
context:data.port,
callback:function(one, two){
// this === data.port
}
});
data.menu.push({
title:"When mouse over the dropdown item",
hover:function(){...},
unhover:function(){...},
});
data.menu.push({
title:"Deep level menu",
deep:[{
title:"Level 1",
deep:[{
title:"Level 2",
deep:[{...}]
}]
}]
});
})
If you have exported Blackprint into JSON, then you can easily import it like below:
// After imported it will automatically appended into the DOM container
var nodes = sketch.importJSON('{...}');
// nodes = [iface, iface, ...]
To create new node and put it on the DOM you can call
sketch.createNode(namespace, options)
Namespace is the registered node handler from sketch.registerNode
.
Options is a value for the registered node element from sketch.registerInterface
.
// Create the node to the view
var iface = sketch.createNode('math/multiply', {x:20, y:20});
// iface = ScarletsFrame component
// iface.node == the node handler
Blackprint does expose model and components through sketch.scope('modelName')
.
var nodeList = sketch.scope('nodes').list;
var connectionList = sketch.scope('cables').list;
Currently the available options still limited
name | value |
---|---|
visualizeFlow | true/false |
sketch.settings(name, value);
The nodes can be exported as JSON, but it's like the node namespace, position, value, and the connections.
var str = sketch.exportJSON();
// {"math/multiply":[...], ...}
Before we're getting started, let's clone this repository.
$ git clone --depth 1 https://github.com/Blackprint/Blackprint.git .
$ git clone --depth 1 https://github.com/Blackprint/blackprint.github.io.git ./example
$ git clone --depth 1 https://github.com/Blackprint/interpreter-js.git ./interpreter-js
$ git clone --depth 1 https://github.com/Blackprint/nodes.git ./nodes
# Create symbolic link to Blackprint's dist folder
# (Windows: may need administrator privileges)
$ mklink /D ".\example\dist" "..\dist"
# For linux
$ ln -s dist ./example
# If you have these package on global you can skip this step
$ npm i -g scarletsframe-compiler gulp
# It could be installed locally without `-g`
$ gulp
# The compiled interpreter-js and nodes will be placed on ./dist folder
The /example/public
folder have default index.html
for getting started, and your css
and js
should be written into /example/src
or /src
folder and your browser should being refreshed automatically.
The compiler already have versioning, so you don't need to press CTRL+F5 every time you modify your code in /src
.
But usually you want to modify some parameter on the /gulpfile.js
for customize it.
The compilation process will minify your code and also run Babel transpiler to support low end browser.
$ gulp compile
Personal story
Actually I was never use UE4 Blueprint (and I haven't ever use it yet because of my slow PC), but I have some experience with UDK Kismet. Developing a visual script by connecting nodes was my unfinished project since 2014 with ActionScript (Adobe Flash). Well, it's not professional to tell a story about my very young age with programming. But the time was passed and I have a feeling like I can continue my old project with my current skill.The main target of the project is to help developers to modify their program's logic while the program is still running. With proper custom script it can be used to manage IoT, Docker container connections, or virtual cable for electrical stuff. Well, it may feel like a dream but it can be turned into the reality.
Some of the interface design is inpired by UE4 Blueprint (I found it on Google Images), it could be modified by CSS and I will redesign it after the project reach final stage.
The other target of this project is - developers can create new custom node and design the node easily (with ScarletsFrame), so everyone can share their nodes and import it for their project. ScarletsFrame is designed to handle performance pressure while giving many simplicity to the developers (like hot reload) on the browser. You will see some feature that was exist on Blackprint, and it was being implemented with very little effort. But you may find it confusing since I haven't use ES6 modules, I will change it on the future.
MIT
This project is free to use, because I bring this for our future.
But I also need your support for the another project.
If you use this commercially, please.. or I will cry with my potato :(
Maybe I could also buy your product that built with Blackprint.
But if you use Blackprint for creating another free open source project
it may be awesome (๑˃ᴗ˂)ﻭ