Skip to content

Instantly share code, notes, and snippets.

@ragingbal
Created May 21, 2022 08:25
Show Gist options
  • Save ragingbal/450872f1fcda8bb7ec196460b59b108b to your computer and use it in GitHub Desktop.
Save ragingbal/450872f1fcda8bb7ec196460b59b108b to your computer and use it in GitHub Desktop.
Node Editor UI
<svg id="svg">
</svg>

Node Editor UI

A node-based editor of some sort using CSS, SVG and jQuery. Inputs can be attached to node outputs via mouse interaction.

A Pen by Laerin on CodePen.

License.

// SVG SETUP
// ===========
var svg = document.getElementById('svg');
svg.ns = svg.namespaceURI;
// MOUSE SETUP
// =============
var mouse = {
currentInput: null,
createPath: function(a, b){
var diff = {
x: b.x - a.x,
y: b.y - a.y
};
var pathStr = 'M' + a.x + ',' + a.y + ' ';
pathStr += 'C';
pathStr += a.x + diff.x / 3 * 2 + ',' + a.y + ' ';
pathStr += a.x + diff.x / 3 + ',' + b.y + ' ';
pathStr += b.x + ',' + b.y;
return pathStr;
}
};
window.onmousemove = function(e){
if(mouse.currentInput){
var p = mouse.currentInput.path;
var iP = mouse.currentInput.getAttachPoint();
var oP = {x: e.pageX, y: e.pageY};
var s = mouse.createPath(iP, oP);
p.setAttributeNS(null, 'd', s);
}
};
window.onclick = function(e){
if(mouse.currentInput){
mouse.currentInput.path.removeAttribute('d');
if(mouse.currentInput.node){
mouse.currentInput.node.detachInput(mouse.currentInput);
}
mouse.currentInput = null;
}
};
// CLEAN UP AND ACTUAL CODE [WIP]
// ================================
function GetFullOffset(element){
var offset = {
top: element.offsetTop,
left: element.offsetLeft,
};
if(element.offsetParent){
var po = GetFullOffset(element.offsetParent);
offset.top += po.top;
offset.left += po.left;
return offset;
}
else
return offset;
}
function Node(name){
// DOM Element creation
this.domElement = document.createElement('div');
this.domElement.classList.add('node');
this.domElement.setAttribute('title', name);
// Create output visual
var outDom = document.createElement('span');
outDom.classList.add('output');
outDom.innerHTML = '&nbsp;';
this.domElement.appendChild(outDom);
// Output Click handler
var that = this;
outDom.onclick = function(e){
if(mouse.currentInput &&
!that.ownsInput(mouse.currentInput)){
var tmp = mouse.currentInput;
mouse.currentInput = null;
that.connectTo(tmp);
}
e.stopPropagation();
};
// Node Stuffs
this.value = '';
this.inputs = [];
this.connected = false;
// SVG Connectors
this.attachedPaths = [];
}
function NodeInput(name){
this.name = name;
this.node = null;
// The dom element, here is where we could add
// different input types
this.domElement = document.createElement('div');
this.domElement.innerHTML = name;
this.domElement.classList.add('connection');
this.domElement.classList.add('empty');
// SVG Connector
this.path = document.createElementNS(svg.ns, 'path');
this.path.setAttributeNS(null, 'stroke', '#8e8e8e');
this.path.setAttributeNS(null, 'stroke-width', '2');
this.path.setAttributeNS(null, 'fill', 'none');
svg.appendChild(this.path);
// DOM Event handlers
var that = this;
this.domElement.onclick = function(e){
if(mouse.currentInput){
if(mouse.currentInput.path.hasAttribute('d'))
mouse.currentInput.path.removeAttribute('d');
if(mouse.currentInput.node){
mouse.currentInput.node.detachInput(mouse.currentInput);
mouse.currentInput.node = null;
}
}
mouse.currentInput = that;
if(that.node){
that.node.detachInput(that);
that.domElement.classList.remove('filled');
that.domElement.classList.add('empty');
}
e.stopPropagation();
};
}
NodeInput.prototype.getAttachPoint = function(){
var offset = GetFullOffset(this.domElement);
return {
x: offset.left + this.domElement.offsetWidth - 2,
y: offset.top + this.domElement.offsetHeight / 2
};
};
Node.prototype.getOutputPoint = function(){
var tmp = this.domElement.firstElementChild;
var offset = GetFullOffset(tmp);
return {
x: offset.left + tmp.offsetWidth / 2,
y: offset.top + tmp.offsetHeight / 2
};
};
Node.prototype.addInput = function(name){
var input = new NodeInput(name);
this.inputs.push(input);
this.domElement.appendChild(input.domElement);
return input;
};
Node.prototype.detachInput = function(input){
var index = -1;
for(var i = 0; i < this.attachedPaths.length; i++){
if(this.attachedPaths[i].input == input)
index = i;
};
if(index >= 0){
this.attachedPaths[index].path.removeAttribute('d');
this.attachedPaths[index].input.node = null;
this.attachedPaths.splice(index, 1);
}
if(this.attachedPaths.length <= 0){
this.domElement.classList.remove('connected');
}
};
Node.prototype.ownsInput = function(input){
for(var i = 0; i < this.inputs.length; i++){
if(this.inputs[i] == input)
return true;
}
return false;
};
Node.prototype.updatePosition = function(){
var outPoint = this.getOutputPoint();
var aPaths = this.attachedPaths;
for(var i = 0; i < aPaths.length; i++){
var iPoint = aPaths[i].input.getAttachPoint();
var pathStr = this.createPath(iPoint, outPoint);
aPaths[i].path.setAttributeNS(null, 'd', pathStr);
}
for(var j = 0; j < this.inputs.length; j++){
if(this.inputs[j].node != null){
var iP = this.inputs[j].getAttachPoint();
var oP = this.inputs[j].node.getOutputPoint();
var pStr = this.createPath(iP, oP);
this.inputs[j].path.setAttributeNS(null, 'd', pStr);
}
}
};
Node.prototype.createPath = function(a, b){
var diff = {
x: b.x - a.x,
y: b.y - a.y
};
var pathStr = 'M' + a.x + ',' + a.y + ' ';
pathStr += 'C';
pathStr += a.x + diff.x / 3 * 2 + ',' + a.y + ' ';
pathStr += a.x + diff.x / 3 + ',' + b.y + ' ';
pathStr += b.x + ',' + b.y;
return pathStr;
};
Node.prototype.connectTo = function(input){
input.node = this;
this.connected = true;
this.domElement.classList.add('connected');
input.domElement.classList.remove('empty');
input.domElement.classList.add('filled');
this.attachedPaths.push({
input: input,
path: input.path
});
var iPoint = input.getAttachPoint();
var oPoint = this.getOutputPoint();
var pathStr = this.createPath(iPoint, oPoint);
input.path.setAttributeNS(null, 'd',pathStr);
};
Node.prototype.moveTo = function(point){
this.domElement.style.top = point.y + 'px';
this.domElement.style.left = point.x + 'px';
this.updatePosition();
};
Node.prototype.initUI = function(){
var that = this;
// Make draggable
$(this.domElement).draggable({
containment: 'window',
cancel: '.connection,.output',
drag: function(event, ui){
that.updatePosition();
}
});
// Fix positioning
this.domElement.style.position = 'absolute';
document.body.appendChild(this.domElement);
// Update Visual
this.updatePosition();
};
// DEMO
// ========
// Node 1
var node = new Node('Another One');
node.addInput('Value1');
node.addInput('Value2');
node.addInput('Value3');
// Node 2
var node2 = new Node('Node 2');
node2.addInput('Text In');
node2.addInput('Value 5');
// Node 3
var node3 = new Node('Something Else');
node3.addInput('Color4');
node3.addInput('Position');
node3.addInput('Noise Octaves');
// Move to initial positions
node.moveTo({x: 150, y: 20});
node2.moveTo({x: 20, y: 70});
node3.moveTo({x:300, y:150});
// Connect Nodes
node.connectTo(node2.inputs[0]);
node3.connectTo(node2.inputs[1]);
node3.connectTo(node.inputs[0]);
// Add to DOM
node.initUI();
node2.initUI();
node3.initUI();
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js"></script>
body{
background-color: #2e2e2e;
color: #d4d4d4;
font-family: sans-serif;
}
.node:before{
content:attr(title) " ";
display: block;
border-top-left-radius:.75em;
border-top-right-radius:.75em;
background-color:#6e6e6e;
padding:0.1em .3em 0em;
margin:-.1em -.3em 0.2em;
}
.node{
background-color: #4e4e4e;
border-radius: .75em;
display: inline-block;
padding:0.1em .3em .25em;
position:absolute;
}
.output,
.connection:after{
position:absolute;
border:solid 1px #dedede;
background-color:#2e2e2e;
width:0.5em;
height:0.5em;
border-radius:0.5em;
}
.node.connected > .output,
.connection.filled:after{
border:solid 1px transparent;
background-color:#aeaeae;
}
.node > .output:hover,
.connection:hover:after{
border-color:red;
}
.output{
left: -.5em;
top:1em;
cursor: pointer;
}
.connection{
width:100%;
position:relative;
padding-right:0.5em;
cursor:pointer;
}
.connection:after{
content:"";
right:0em;
top:0.25em;
}
svg{
position:absolute;
top:0px;
left:0px;
z-index:-100;
width:100%;
height:100%;
}
<link href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/themes/smoothness/jquery-ui.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment