A node-based editor of some sort using CSS, SVG and jQuery. Inputs can be attached to node outputs via mouse interaction.
Created
May 21, 2022 08:25
-
-
Save ragingbal/450872f1fcda8bb7ec196460b59b108b to your computer and use it in GitHub Desktop.
Node Editor UI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<svg id="svg"> | |
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 = ' '; | |
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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