class Cable { start = ""; end = ""; strPath = ""; color = ""; strokeWidth = 10; bufferSize = 20; buffer = []; path = null; constructor(start) { this.start = start; this.randomColor(); } randomColor() { const hue = Math.floor(Math.random() * 360); const saturation = Math.floor(50 + Math.random() * (50 + 1)) + "%"; const lightness = "75%"; this.color = "hsl(" + hue + ", " + saturation + ", " + lightness + ")"; } createCable(position) { this.path = document.createElementNS("http://www.w3.org/2000/svg", "path"); this.path.setAttribute("fill", "none"); this.path.setAttribute("stroke", this.color); this.path.setAttribute("stroke-width", this.strokeWidth); this.path.setAttribute("stroke-linecap", "round"); this.buffer = []; this.appendToBuffer(position); this.strPath = "M" + position.x + " " + position.y; this.path.setAttribute("d", this.strPath); } appendToBuffer(position) { this.buffer.push(position); while (this.buffer.length > this.bufferSize) { this.buffer.shift(); } } getAveragePoint(offset) { // Calculate the average point, starting at offset in the buffer let len = this.buffer.length; if (len % 2 === 1 || len >= this.bufferSize) { let totalX = 0; let totalY = 0; let position, i; let count = 0; for (i = offset; i < len; i++) { count++; position = this.buffer[i]; totalX += position.x; totalY += position.y; } return { x: totalX / count, y: totalY / count, }; } return null; } updatePath() { let position = this.getAveragePoint(0); if (position) { // Get the smoothed part of the path that will not change this.strPath += " L" + position.x + " " + position.y; // Get the last part of the path (close to the current mouse position) // This part will change if the mouse moves again var tmpPath = ""; for (var offset = 2; offset < this.buffer.length; offset += 2) { position = this.getAveragePoint(offset); tmpPath += " L" + position.x + " " + position.y; } this.path.setAttribute("d", this.strPath + tmpPath); } } } class Panel { width = 0; height = 0; svg = null; container = null; containerBoundingClient = null; model = {}; params = []; sockets = []; cable = null; cables = null; constructor(svg, container, debug = false) { this.svg = this.htmlToElement(svg); this.container = container; this.container.style.position = "relative"; this.containerRect = container.getBoundingClientRect(); this.debug = debug; while (container.firstChild) { container.removeChild(container.lastChild); } container.appendChild(this.svg); this.setSize(); this.createParams(); this.createSockets(); this.createCables(); this.svg.addEventListener("mousedown", (e) => this.startCable(e)); this.svg.addEventListener("mousemove", (e) => this.drawCable(e)); this.svg.addEventListener("mouseup", (e) => this.endCable(e)); // this.svg.addEventListener("mouseleave", (e) => this.endCable(e)); } htmlToElement(string) { var template = document.createElement("template"); string = string.trim(); // Never return a text node of whitespace as the result template.innerHTML = string; return template.content.firstChild; } setSize() { this.width = this.svg.getAttribute("width"); this.height = this.svg.getAttribute("height"); this.container.style.width = this.width + "px"; this.container.style.height = this.height + "px"; } createParams() { this.params = this.svg.querySelectorAll("#params [fill='#FF0000']"); this.model.params = []; for (const param of this.params) { let rect = param.getBoundingClientRect(); let control = document.createElement("input"); control.setAttribute("type", "range"); control.setAttribute("data-width", rect.width); control.setAttribute("data-height", rect.height); control.setAttribute("data-fgcolor", "white"); control.classList.add("input-knob"); control.style.position = "absolute"; control.style.left = rect.left - this.containerRect.left + "px"; control.style.top = rect.top - this.containerRect.top + "px"; control.setAttribute("name", param.id); this.container.appendChild(control); this.model.params.push(param.id); if (this.debug) { let label = document.createElement("label"); label.setAttribute("for", param.id); label.innerHTML = param.id; label.style.position = "absolute"; label.style.left = rect.left - this.containerRect.left + "px"; label.style.top = rect.top + rect.height + "px"; label.style.fontSize = "1rem"; label.style.backgroundColor = "red"; label.style.color = "white"; this.container.appendChild(label); } } let group = this.svg.querySelector("#params"); if (group) group.style.display = "none"; } createSockets() { this.sockets = this.svg.querySelectorAll("#sockets [fill='#00FF00']"); this.model.sockets = []; for (const socket of this.sockets) { let rect = socket.getBoundingClientRect(); let input = document.createElement("input"); input.setAttribute("name", socket.id); input.classList.add("socket"); input.style.position = "absolute"; input.style.left = rect.left - this.containerRect.left + "px"; input.style.top = rect.top - this.containerRect.top + "px"; input.style.width = rect.width + "px"; input.style.height = rect.height + "px"; input.style.border = "none"; input.style.background = "none"; input.style.opacity = 0; if (this.debug) { input.style.border = "1px solid #00FF00"; input.style.opacity = 1; } input.style.cursor = "alias"; input.addEventListener("mouseup", (e) => this.endCable(e)); input.addEventListener("mousedown", (e) => { e.preventDefault(); this.startCable(e); return false; }); this.container.appendChild(input); this.model.sockets.push(socket.id); } let group = this.svg.querySelector("#sockets"); if (group) group.style.display = "none"; } createCables() { let cables = document.createElementNS("http://www.w3.org/2000/svg", "svg"); cables.classList.add("cables"); cables.setAttribute("width", this.width); cables.setAttribute("height", this.height); cables.setAttribute("viewBox", `0 0 ${this.width} ${this.height}`); cables.setAttribute("fill", "none"); cables.style.pointerEvents = "none"; cables.style.position = "absolute"; cables.style.left = 0; cables.style.top = 0; cables.style.width = this.width; cables.style.height = this.height; this.cables = cables; this.container.appendChild(this.cables); } startCable(event) { let position = this.getMousePosition(event); let socket = event.target; if (socket && socket.classList.contains("socket")) { this.cable = new Cable(socket.getAttribute("name")); this.cable.createCable(position); let cable = this.cable.path; cable.style.pointerEvents = "stroke"; cable.addEventListener("click", (e) => { console.log(e); cable.remove(); }); this.cables.appendChild(this.cable.path); for (const input of this.container.querySelectorAll(".input-knob")) { input.style.pointerEvents = "none"; } } } drawCable(event) { if (this.cable) { this.cable.appendToBuffer(this.getMousePosition(event)); this.cable.updatePath(); } } endCable(event) { if (this.cable) { let socket = event.target; if (socket && socket.classList.contains("socket")) { this.cable.end = socket.getAttribute("name"); // TODO: log cable } else { this.cable.path.remove(); } for (const input of this.container.querySelectorAll(".input-knob")) { input.style.pointerEvents = "all"; } this.cable = null; } } getMousePosition(e) { return { x: e.pageX - this.containerRect.left, y: e.pageY - this.containerRect.top, }; } }