diff --git a/Box.js b/Box.js index e80ea43..79abaac 100644 --- a/Box.js +++ b/Box.js @@ -8,6 +8,7 @@ class Box { y; element; + content; static instances = []; @@ -23,11 +24,22 @@ class Box { this.title = title; this.x = Camera.x + 100; this.y = Camera.y + 200; - this.buildElement(); - this.updatePosition(); + + let closestDistance; + do { + closestDistance = 999999; + this.x += 50; + this.y += 50; + for (let box of Box.instances) { + if (box == this) continue; + closestDistance = Math.min(Math.abs(box.x - this.x), Math.abs(box.y - this.y), closestDistance); + } + } while (closestDistance < 50); } buildElement() { + if (this.element) return this.element; + this.element = document.createElement("div"); this.element.classList.add("box"); @@ -35,20 +47,19 @@ class Box { topBar.classList.add("boxTopBar"); this.element.appendChild(topBar); - let deleteButton = document.createElement("span"); deleteButton.classList.add("boxDeleteButton"); deleteButton.addEventListener("click", () => { this.deleteBox() }); topBar.appendChild(deleteButton); - let content = document.createElement("div"); - content.classList.add("boxContent"); - this.element.appendChild(content); + this.content = document.createElement("div"); + this.content.classList.add("boxContent"); + this.element.appendChild(this.content); let title = document.createElement("span"); title.classList.add("boxTitle"); title.innerText = this.title; - content.appendChild(title); + this.content.appendChild(title); let connectorContainer = document.createElement("div"); connectorContainer.classList.add("boxConnectorContainer"); @@ -64,22 +75,33 @@ class Box { connectorContainer.appendChild(inputConnectorContainer); connectorContainer.appendChild(outputConnectorContainer); - content.appendChild(connectorContainer); + this.content.appendChild(connectorContainer); - for (connector of this.inputs) { - inputConnectorContainer.appendChild(connector.element); + for (let connector of this.inputs) { + inputConnectorContainer.appendChild(connector.buildElement()); } - for (connector of this.outputs) { - outputConnectorContainer.appendChild(connector.element); + for (let connector of this.outputs) { + outputConnectorContainer.appendChild(connector.buildElement()); } + + this.updatePosition(); + return this.element; } updatePosition() { + if (!this.element) return; + this.element.style.left = (this.x - Camera.x) + "px"; this.element.style.top = (this.y - Camera.y) + "px"; } deleteBox() { + for (let connector of this.inputs) { + connector.disconnectBoth(); + } + for (let connector of this.outputs) { + connector.disconnectBoth(); + } this.element.parentNode.removeChild(this.element); for (let i = 0; i < Box.instances.length; i++) { if (Box.instances[i] == this) { @@ -88,10 +110,4 @@ class Box { } } } -} - -function addElement(type) { - let box = new Box("test"); - - document.getElementById("playground").appendChild(box.element); } \ No newline at end of file diff --git a/Connector.js b/Connector.js index e5af0a9..5bc1319 100644 --- a/Connector.js +++ b/Connector.js @@ -2,41 +2,91 @@ class Connector { static INPUT = 0; static OUTPUT = 1; - type; + direction; + label; + + connection; element; + circle; - constructor(type, label = "") { - this.type = type; + listeners = []; + + constructor(direction, label = "") { + this.direction = direction; this.element = ""; this.label = label; + if (this.label == "") { - switch (this.type) { + switch (this.direction) { case Connector.INPUT: this.label = "Input"; + break; case Connector.OUTPUT: this.label = "Output"; + break; } } - - this.buildElement(); } buildElement() { + if (this.element) return this.element; + this.element = document.createElement("div"); this.element.classList.add("connector"); - if (this.type == Connector.INPUT) { + if (this.direction == Connector.INPUT) { this.element.classList.add("connectorInput"); } - if (this.type == Connector.OUTPUT) { + if (this.direction == Connector.OUTPUT) { this.element.classList.add("connectorOutput"); } - let circle = document.createElement("span"); - circle.classList.add("connectorCircle"); + this.circle = document.createElement("span"); + this.circle.classList.add("connectorCircle"); let label = document.createElement("span"); + label.innerText = this.label; label.classList.add("connectorLabel"); + + this.element.appendChild(this.circle); + this.element.appendChild(label); + + this.element.addEventListener("click", () => { + ConnectionController.clickConnector(this); + }) + + return this.element; + } + + connect(connector) { + this.connection = connector; + this.element.classList.add("connected"); + } + + disconnect() { + this.connection = null; + this.element.classList.remove("connected"); + } + + disconnectBoth() { + if (!this.connection) return; + this.connection.disconnect(); + this.disconnect(); + } + + send(data) { + if (!this.connection) return; + this.connection.receive(data); + } + + receive(data) { + for (let listener of this.listeners) { + listener(data); + } + } + + addListener(listener) { + this.listeners.push(listener); } } \ No newline at end of file diff --git a/TextBox.js b/TextBox.js new file mode 100644 index 0000000..9e0cc9d --- /dev/null +++ b/TextBox.js @@ -0,0 +1,30 @@ +class TextBox extends Box { + textbox; + + constructor() { + super("Text Box"); + + let input = new Connector(Connector.INPUT); + input.addListener((data) => { + this.outputs[0].send(data); + this.textbox.value = data; + }); + + this.inputs.push(input); + this.outputs.push(new Connector(Connector.OUTPUT)); + } + + buildElement() { + if (this.element) return this.element; + super.buildElement(); + + this.textbox = document.createElement("textarea"); + this.textbox.placeholder = "Lots of text..."; + this.textbox.addEventListener("input", () => { + this.outputs[0].send(this.textbox.value); + }) + this.content.appendChild(this.textbox); + + return this.element; + } +} \ No newline at end of file diff --git a/draw-controller.js b/draw-controller.js new file mode 100644 index 0000000..2e51545 --- /dev/null +++ b/draw-controller.js @@ -0,0 +1,66 @@ +let cv, c; + +document.addEventListener("DOMContentLoaded", () => { + cv = document.getElementById("canvas"); + c = cv.getContext("2d"); + + window.requestAnimationFrame(draw); +}); + +function draw() { + + cv.width = window.innerWidth; + cv.height = window.innerHeight; + + + // draw connections + c.strokeStyle = "white"; + c.lineWidth = 3; + + for (let box of Box.instances) { + for (let input of box.inputs) { + if (!input.connection) continue; + let output = input.connection; + + let i = input.circle.getBoundingClientRect(); + let ix = i.x + i.width / 2; + let iy = i.y + i.height / 2; + + let o = output.circle.getBoundingClientRect(); + let ox = o.x + o.width / 2; + let oy = o.y + o.height / 2; + + drawConnection(ox, oy, ix, iy); + } + } + + // draw currently connecting connection + + if (ConnectionController.currentlySelectedConnector) { + let i = ConnectionController.currentlySelectedConnector.circle.getBoundingClientRect(); + let ix = i.x + i.width / 2; + let iy = i.y + i.height / 2; + + let ox = Mouse.position.x; + let oy = Mouse.position.y; + if (ConnectionController.currentlySelectedConnector.direction == Connector.OUTPUT) { + ox = ix; + oy = iy; + ix = Mouse.position.x; + iy = Mouse.position.y; + } + drawConnection(ox, oy, ix, iy); + } + + window.requestAnimationFrame(draw); +} + +function drawConnection(x1, y1, x2, y2) { + let cpDistance = Math.min(Math.max((x1 - x2), 50), 100); + c.beginPath(); + c.moveTo(x1, y1); + c.lineTo(x1 + 10, y1); + c.bezierCurveTo(x1 + cpDistance, y1, x2 - cpDistance, y2, x2 - 10, y2); + c.lineTo(x2, y2); + c.stroke(); +} \ No newline at end of file diff --git a/element-controller.js b/element-controller.js new file mode 100644 index 0000000..12a321e --- /dev/null +++ b/element-controller.js @@ -0,0 +1,54 @@ +function addElement(type) { + if (type == "TextBox") { + document.getElementById("playground").appendChild(new TextBox().buildElement()); + } +} + +class ConnectionController { + static currentlySelectedConnector = null; + + static clickConnector(connector) { + if (this.currentlySelectedConnector) { + + let a = this.currentlySelectedConnector; + let b = connector; + + if (a == b) { + this.unselect(); + return; + } + if (a.direction == b.direction) { + this.unselect(); + this.clickConnector(b); + return; + } + + if (b.connection) { + b.disconnectBoth(); + } + + a.element.classList.add("connected"); + b.element.classList.add("connected"); + + a.connect(b); + b.connect(a); + + a.element.classList.remove("connecting"); + this.currentlySelectedConnector = null; + } else { + + if (connector.connection) { + connector.disconnectBoth(); + } + + connector.element.classList.add("connecting"); + this.currentlySelectedConnector = connector; + } + } + + static unselect() { + if (!this.currentlySelectedConnector) return; + this.currentlySelectedConnector.element.classList.remove("connecting"); + this.currentlySelectedConnector = null; + } +} \ No newline at end of file diff --git a/index.html b/index.html index dde95cd..b0ae68b 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,13 @@ DataTools + + + + diff --git a/input-controller.js b/input-controller.js index f6a9aae..c41038e 100644 --- a/input-controller.js +++ b/input-controller.js @@ -2,9 +2,22 @@ let draggingBox = null; let draggingCamera = false; let lastMousePos = {x: 0, y: 0}; +class Mouse { + static position = {x: 0, y: 0}; + static lastPosition = {x: 0, y: 0}; + + static updatePosition(x, y) { + this.lastPosition = this.position; + this.position = {x: x, y: y}; + } + + static getDelta() { + return {x : this.position.x - this.lastPosition.x, y : this.position.y - this.lastPosition.y}; + } +} + document.addEventListener("mousedown", (e) => { - let x = e.clientX; - let y = e.clientY; + Mouse.updatePosition(e.clientX, e.clientY); if (e.button == 0) { if (e.target.classList.contains("boxDeleteButton")) return; @@ -12,9 +25,10 @@ document.addEventListener("mousedown", (e) => { let topBarRect = box.element.getElementsByClassName("boxTopBar")[0].getBoundingClientRect(); box.element.classList.remove("dragging"); - if (isPointInRect(x, y, topBarRect.x, topBarRect.y, topBarRect.width, topBarRect.height)) { + if (isPointInRect(Mouse.position.x, Mouse.position.y, topBarRect.x, topBarRect.y, topBarRect.width, topBarRect.height)) { draggingBox = box; box.element.classList.add("dragging"); + e.preventDefault(); break; } } @@ -33,16 +47,18 @@ document.addEventListener("mouseup", (e) => { if (e.button == 1 || e.button == 2) { draggingCamera = false; } -}) +}); document.addEventListener("mousemove", (e) => { + Mouse.updatePosition(e.clientX, e.clientY); - let delta = {x : e.clientX - lastMousePos.x, y : e.clientY - lastMousePos.y}; + let delta = Mouse.getDelta(); if (draggingBox != null) { draggingBox.x += delta.x; draggingBox.y += delta.y; draggingBox.updatePosition(); + e.preventDefault(); } if (draggingCamera) { @@ -53,10 +69,10 @@ document.addEventListener("mousemove", (e) => { document.getElementById("canvas").style.backgroundPosition = (-Camera.x) + "px " + (-Camera.y) + "px"; - } + e.preventDefault(); - lastMousePos = {x: e.clientX, y: e.clientY}; -}) + } +}); function isPointInRect(px, py, rx, ry, rw, rh) { return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh; diff --git a/style.css b/style.css index bb4491a..d55825a 100644 --- a/style.css +++ b/style.css @@ -124,4 +124,54 @@ textarea { height: 150px; font-family: Monospace; min-width: 200px; +} + +.boxConnectorContainer { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + margin-left: -20px; + margin-right: -20px; +} + +.connector { + display: flex; + align-items: center; + gap: 10px; + --connectorCircleSize: 25px; + cursor: pointer; +} + +.connectorCircle { + display: block; + width: var(--connectorCircleSize); + height: var(--connectorCircleSize); + box-sizing: border-box; + border: 3px solid var(--bg); + border-radius: var(--connectorCircleSize); + background-color: var(--connectorCircleColor); + transition: scale 0.05s linear; +} + +.connector.connectorInput { + flex-direction: row; + --connectorCircleColor: var(--blue); +} + +.connector.connectorOutput { + flex-direction: row-reverse; + --connectorCircleColor: var(--yellow); +} + +.connector:hover .connectorCircle { + scale: 1.2; +} + +.connector.connecting { + --connectorCircleColor: white; +} + +.connector.connected .connectorCircle { + border-color: white; } \ No newline at end of file