diff --git a/curveEditor.js b/curveEditor.js index 4118042..f312ff9 100644 --- a/curveEditor.js +++ b/curveEditor.js @@ -121,9 +121,10 @@ export class CurveEditor { view.setUint16(offset, seg.endHandleX, true); offset += 2; view.setInt16(offset, this.yToExportRange(seg.endHandleY), true); offset += 2; view.setInt16(offset, this.yToExportRange(seg.endPointY), true); offset += 2; + console.log(this.yToExportRange(seg.startPointY), this.yToExportRange(seg.endPointY)); }); - console.log("๐Ÿงต Curve segments packed:", curveSegments.length); - console.log(curveSegments); + //console.log("๐Ÿงต Curve segments packed:", curveSegments.length); + //console.log(curveSegments); return new Uint8Array(buffer.slice(0, offset)); diff --git a/index.html b/index.html index 442c263..4e46cf6 100644 --- a/index.html +++ b/index.html @@ -205,8 +205,18 @@ - - +
+
+ +
diff --git a/nodeeditor/NodeEditor.js b/nodeeditor/NodeEditor.js index c056c4b..8b8df9a 100644 --- a/nodeeditor/NodeEditor.js +++ b/nodeeditor/NodeEditor.js @@ -1,11 +1,5 @@ -import { CanvasDropdown, CanvasTextInput } from "./canvastools.js" -const NODE_TYPES = { - Node: 0x01, - Servo: 0x02, - Curve: 0x03, - Noise: 0x04 -}; +import { ServoNode, CurveNode, VariableNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js" export class NodeEditor { @@ -18,6 +12,11 @@ export class NodeEditor { this.draggingWire = null; this.motorIds = options.motorIds || []; // pass motor ID list here + this.panX = 0; + this.panY = 0; + this.isPanning = false; + this.lastPan = { x: 0, y: 0 }; + this.zoom = 1.0; this._bindEvents(); @@ -29,12 +28,12 @@ export class NodeEditor { console.log(curveSets, motorIDs); for (var i = 0; i < motorIDs.length; i++) { - let inputNode = this.addCurveNode(200, 20 + i * 120, "Curve", motorIDs[i]); - let outputNode = this.addServoNode(400, 20 + i * 120, "Servo Output", motorIDs[i]); + let inputNode = this.addCurveNode(200, 20 + i * 140, "Curve", motorIDs[i]); + let outputNode = this.addServoNode(500, 40 + i * 140, "Servo Output", motorIDs[i]); this.connections.push({ from: inputNode, to: outputNode }); } - + this.addVariableNode(50, 120, "Var"); } @@ -57,8 +56,8 @@ export class NodeEditor { view.setUint8(offset++, node.type); // Node type view.setUint8(offset++, node.id); // Node ID - view.setUint16(offset, node.x, true); offset+=2; - view.setUint16(offset, node.y, true); offset+=2; + view.setUint16(offset, node.x, true); offset += 2; + view.setUint16(offset, node.y, true); offset += 2; switch (node.type) { @@ -78,6 +77,28 @@ export class NodeEditor { view.setUint8(offset++, node.seed); break; + + case NODE_TYPES.Variable: + view.setUint8(offset++, node.variableDropdown.selectedIndex); + console.log(node.variableDropdown.selectedIndex); + break; + + + case NODE_TYPES.Math: + view.setUint8(offset++, node.operatorDropdown.selectedIndex); + view.setFloat32(offset, node.valueInput.numericValue, true); offset += 4; + console.log(node.operatorDropdown.selectedIndex); + console.log(node.valueInput.numericValue); + break; + + case NODE_TYPES.Map: + view.setFloat32(offset, node.inMinInput.numericValue, true); offset += 4; + view.setFloat32(offset, node.inMaxInput.numericValue, true); offset += 4; + view.setFloat32(offset, node.outMinInput.numericValue, true); offset += 4; + view.setFloat32(offset, node.outMaxInput.numericValue, true); offset += 4; + console.log(node.inMinInput.numericValue, node.inMaxInput.numericValue, node.outMinInput.numericValue, node.outMaxInput.numericValue); + break; + default: console.warn("Unknown node type:", node.type); } @@ -142,6 +163,21 @@ export class NodeEditor { return node; } + addMathNode(x, y, label = "Math") { + const node = new MathNode(x, y, label); + this.nodes.push(node); + this._redraw(); + return node; + } + + addMapNode(x, y, label = "Map") { + const node = new MapNode(x, y, label); + this.nodes.push(node); + this._redraw(); + return node; + } + + _bindEvents() { @@ -150,11 +186,31 @@ export class NodeEditor { this.canvas.addEventListener("mouseup", (e) => this._onMouseUp(e)); this.canvas.addEventListener("dblclick", (e) => this._onDoubleClick(e)); window.addEventListener("keydown", (e) => this._onKeyDown(e)); + this.canvas.addEventListener("wheel", (e) => this._onWheel(e)); + + this.canvas.addEventListener("contextmenu", (e) => { + e.preventDefault(); + const { x, y } = this._getMouse(e); // canvas-space for node placement + this._showContextMenu(e.pageX, e.pageY, x, y); // page-space for menu placement + }); + + + + } _onMouseDown(e) { const { x, y } = this._getMouse(e); + if (e.button === 1) { // middle mouse + this.isPanning = true; + console.log("pan") + this.lastPan = this._getMouse(e); + e.preventDefault(); + return; + } + + for (const node of this.nodes) { if (node.contains(x, y)) { if (node.handleClick(x, y)) { @@ -183,12 +239,25 @@ export class NodeEditor { let needsRedraw = false; + if (this.isPanning) { + const { x, y } = this._getMouse(e); + const { screenX, screenY } = this._getMouse(e); + this.panX += screenX - this.lastPan.screenX; + this.panY += screenY - this.lastPan.screenY; + this.lastPan = { screenX, screenY }; + + this._redraw(); + //return; + } + + + // Required for highlighting dropdown options onMouseOver for (const node of this.nodes) { - if (node instanceof ServoNode || node instanceof NoiseNode) { - if (node.handleMouseMove(x, y)) { - needsRedraw = true; - } + //if (node instanceof ServoNode || node instanceof VariableNode) { + if (node.handleMouseMove(x, y)) { + needsRedraw = true; } + //} } if (needsRedraw) this._redraw(); @@ -212,9 +281,22 @@ export class NodeEditor { _onMouseUp(e) { const { x, y } = this._getMouse(e); + if (this.isPanning) { + this.isPanning = false; + } + + if (this.draggingWire) { for (const node of this.nodes) { if (node.hitInput(x, y)) { + // Remove any existing connection to this input + for (let i = this.connections.length - 1; i >= 0; i--) { + if (this.connections[i].to === node) { + this.connections.splice(i, 1); + } + } + + // Add new connection this.connections.push({ from: this.draggingWire.from, to: node }); break; } @@ -223,6 +305,7 @@ export class NodeEditor { this._redraw(); } + this.draggingNode = null; } @@ -241,12 +324,21 @@ export class NodeEditor { _getMouse(e) { const rect = this.canvas.getBoundingClientRect(); + const screenX = e.clientX - rect.left; + const screenY = e.clientY - rect.top; + + const canvasX = (screenX - this.panX) / this.zoom; + const canvasY = (screenY - this.panY) / this.zoom; + return { - x: e.clientX - rect.left, - y: e.clientY - rect.top + x: canvasX, + y: canvasY, + screenX, + screenY }; } + _onKeyDown(e) { let needsRedraw = false; @@ -260,6 +352,20 @@ export class NodeEditor { } + _onWheel(e) { + const zoomFactor = 1.1; + if (e.deltaY < 0) { + this.zoom *= zoomFactor; + } else { + this.zoom /= zoomFactor; + } + this.zoom = Math.max(0.2, Math.min(3.0, this.zoom)); // clamp zoom + this._redraw(); + e.preventDefault(); + } + + + _isPointNearLine(px, py, x1, y1, x2, y2, threshold = 10) { const dx = x2 - x1; const dy = y2 - y1; @@ -273,18 +379,82 @@ export class NodeEditor { return dist < threshold; } + _showContextMenu(screenX, screenY, canvasX, canvasY) { + const menu = document.getElementById("contextMenu"); + menu.innerHTML = ""; // clear previous items + menu.style.left = `${screenX}px`; + menu.style.top = `${screenY}px`; + menu.style.display = "block"; + + const hideMenu = () => { + menu.style.display = "none"; + window.removeEventListener("click", hideMenu); + }; + window.addEventListener("click", hideMenu); + + // โœ… Only include Variable node + let item = document.createElement("div"); + item.className = "menu-item"; + item.textContent = "โž• Add Variable Node"; + item.onclick = () => { + this.addVariableNode(canvasX, canvasY, "Variable"); + hideMenu(); + }; + menu.appendChild(item); + + item = document.createElement("div"); + item.className = "menu-item"; + item.textContent = "โž• Add Math Node"; + item.onclick = () => { + this.addMathNode(canvasX, canvasY, "Math"); + hideMenu(); + }; + menu.appendChild(item); + + item = document.createElement("div"); + item.className = "menu-item"; + item.textContent = "โž• Add Map Node"; + item.onclick = () => { + this.addMapNode(canvasX, canvasY, "Map"); + hideMenu(); + }; + menu.appendChild(item); + } + + + _drawConnections() { this.ctx.strokeStyle = "#444"; this.ctx.lineWidth = 2; for (const conn of this.connections) { + const x1 = conn.from.output.x; + const y1 = conn.from.output.y; + const x2 = conn.to.input.x; + const y2 = conn.to.input.y; + + const dx = Math.abs(x2 - x1) * 0.5; + this.ctx.beginPath(); - this.ctx.moveTo(conn.from.output.x, conn.from.output.y); - this.ctx.lineTo(conn.to.input.x, conn.to.input.y); + this.ctx.moveTo(x1, y1); + this.ctx.bezierCurveTo( + x1 + dx, y1, + x2 - dx, y2, + x2, y2 + ); this.ctx.stroke(); + } } _redraw() { + this.ctx.save(); + this.ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // clear full canvas + + this.ctx.setTransform(this.zoom, 0, 0, this.zoom, this.panX, this.panY); + + + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this._drawConnections(); for (const node of this.nodes) { @@ -299,491 +469,7 @@ export class NodeEditor { this.ctx.lineTo(this.draggingWire.x2, this.draggingWire.y2); this.ctx.stroke(); } - } -} - -class Node { - constructor(x, y, label, options = {}) { - this.type = NODE_TYPES.Node; - this.x = x; - this.y = y; - this.width = 120; - this.height = 60; - this.label = label; - this.input = { x: 0, y: 0 }; - this.output = { x: 0, y: 0 }; - //console.log(options.fill); - // Customizable visual options - this.color = options.fill || "#fef6e4"; // pastel fill default - this.border = options.stroke || "#333"; // border color default - - this.updatePorts(); - } - - - - updatePorts() { - this.input.x = this.x - 5; - this.input.y = this.y + this.height / 2; - this.output.x = this.x + this.width + 5; - this.output.y = this.y + this.height / 2; - } - - - - draw(ctx) { - ctx.fillStyle = this.color; - ctx.strokeStyle = this.border; - ctx.lineWidth = 1; - - this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = "#000"; - ctx.font = "14px sans-serif"; - ctx.fillText(this.label, this.x + 10, this.y + 20); - - ctx.beginPath(); - ctx.arc(this.input.x, this.input.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - - ctx.beginPath(); - ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - } - - - - drawRoundedRect(ctx, x, y, w, h, r) { - ctx.beginPath(); - ctx.moveTo(x + r, y); - ctx.lineTo(x + w - r, y); - ctx.quadraticCurveTo(x + w, y, x + w, y + r); - ctx.lineTo(x + w, y + h - r); - ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); - ctx.lineTo(x + r, y + h); - ctx.quadraticCurveTo(x, y + h, x, y + h - r); - ctx.lineTo(x, y + r); - ctx.quadraticCurveTo(x, y, x + r, y); - ctx.closePath(); - } - - - contains(x, y) { - return x >= this.x && x <= this.x + this.width && - y >= this.y && y <= this.y + this.height; - } - - hitOutput(x, y) { - return Math.hypot(x - this.output.x, y - this.output.y) < 8; - } - - hitInput(x, y) { - return Math.hypot(x - this.input.x, y - this.input.y) < 8; - } -} - - -export class ServoNode extends Node { - constructor(x, y, label, motorId) { - super(x, y, label); - this.type = NODE_TYPES.Servo; - this.motorId = motorId; - this.width = 140; - this.height = 80; - } - - draw(ctx) { - // Node box - ctx.fillStyle = this.color || "#fff9d6"; - ctx.strokeStyle = this.border || "#333"; - ctx.lineWidth = 1; - this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); - ctx.fill(); - ctx.stroke(); - - // Label - ctx.fillStyle = "#000"; - ctx.font = "14px sans-serif"; - ctx.fillText(this.label, this.x + 10, this.y + 20); - - // Motor ID display - ctx.font = "12px sans-serif"; - ctx.fillText(`Motor ${this.motorId}`, this.x + 10, this.y + 40); - - // Ports - this.updatePorts(); - ctx.beginPath(); - ctx.arc(this.input.x, this.input.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - - ctx.beginPath(); - ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - } - - contains(x, y) { - return super.contains(x, y); - } - - handleClick(mx, my) { - - if (this.contains(mx, my)) { - window.setSelectedMotor?.(this.motorId); // or this.selectedMotorId if using dropdown - return false; - } - return false; - } - - - handleMouseMove(mx, my) { - return false; - } - - get selectedMotorId() { - return this.motorId; - } -} - -export class CurveNode extends Node { - constructor(x, y, label = "Curve", curveId = 0) { - super(x, y, label); - this.type = NODE_TYPES.Curve; - this.curveId = curveId; - this.width = 140; - this.height = 80; - } - - draw(ctx) { - // Node box - ctx.fillStyle = this.color || "#d6f0ff"; - ctx.strokeStyle = this.border || "#333"; - ctx.lineWidth = 1; - this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); - ctx.fill(); - ctx.stroke(); - - // Label - ctx.fillStyle = "#000"; - ctx.font = "14px sans-serif"; - ctx.fillText(this.label, this.x + 10, this.y + 20); - - // Curve ID display - ctx.font = "12px sans-serif"; - ctx.fillText(`Curve ${this.curveId}`, this.x + 10, this.y + 40); - - // Output port only - this.updatePorts(); - ctx.beginPath(); - ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - } - - - contains(x, y) { - return super.contains(x, y); - } - - handleClick(mx, my) { - if (this.contains(mx, my)) { - window.setSelectedMotor?.(this.curveId); - return false; - } - return false; - } - - - handleMouseMove(mx, my) { - return false; - } - - update(dt) { - // Placeholder: simulate curve output - const t = Date.now() / 1000; - this.lastValue = Math.sin(t + this.curveId); // simple sine curve - return false; - } - - get outputValue() { - return this.lastValue; - } -} - - - -export class InputNode extends Node { - constructor(x, y, label, options) { - super(x, y, label); - console.log(options); - this.inputField = new CanvasTextInput( - this.x + 10, - this.y + 35, - this.width - 20, - "1.0", - "", - { defaultValue: 1.0, numericOnly: true, min: 0, max: 10 } - ); - - } - - - - draw(ctx) { - // Node box - ctx.fillStyle = this.color || "#e0f7ff"; - ctx.strokeStyle = this.border || "#333"; - ctx.lineWidth = 1; - this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); - ctx.fill(); - ctx.stroke(); - - // Label - ctx.fillStyle = "#000"; - ctx.font = "14px sans-serif"; - ctx.fillText(this.label, this.x + 10, this.y + 20); - - // Input field - this.inputField.x = this.x + 10; - this.inputField.y = this.y + 35; - this.inputField.draw(ctx); - - // Output port - ctx.beginPath(); - ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - } - - contains(x, y) { - return super.contains(x, y) || this.inputField.contains(x, y); - } - - handleClick(mx, my) { - console.log(mx, my); - return this.inputField.handleClick(mx, my); - } - - handleKey(e) { - return this.inputField.handleKey(e); - } - - - get outputValue() { - return this.inputField.numericValue; - } -} - - -export class NoiseNode extends Node { - constructor(x, y, label = "Noise Generator") { - super(x, y, label); - this.type = NODE_TYPES.Noise; - this.width = 160; - this.height = 280; - - this.modeDropdown = new CanvasDropdown( - 10, 35, this.width - 20, - ["impulse", "pulse", "threshold", "smooth"], - "impulse" - ); - - const inputConfigs = [ - { label: "Rate", value: "1.0", min: 0, max: 100 }, - { label: "Threshold", value: "0.8", min: 0, max: 1 }, - { label: "Pulse Width", value: "0.2", min: 0, max: 10 }, - { label: "Amplitude", value: "1.0", min: 0, max: 10 }, - { label: "Seed", value: "0", min: 0, max: 99999 } - ]; - this.inputs = []; - - const inputSpacing = 42; // includes label + input height - - for (let i = 0; i < inputConfigs.length; i++) { - const cfg = inputConfigs[i]; - const inputY = 74 + i * inputSpacing; - - this.inputs.push(new CanvasTextInput( - 10, - inputY, - undefined, // uses default width - cfg.value, - cfg.label, - { - numericOnly: true, - min: cfg.min, - max: cfg.max - } - )); - } - - this.lastValue = 0; - this.timer = 0; - } - - draw(ctx) { - ctx.fillStyle = this.color || "#f0e6ff"; - ctx.strokeStyle = this.border || "#333"; - ctx.lineWidth = 1; - this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); - ctx.fill(); - ctx.stroke(); - - - // Draw Label - ctx.fillStyle = "#000"; - ctx.font = "14px sans-serif"; - ctx.fillText(this.label, this.x + 10, this.y + 20); - - // Draw inputs - for (let i = 0; i < this.inputs.length; i++) { - const input = this.inputs[i]; - input.x = this.x + input.offsetX; - input.y = this.y + input.offsetY; // ๐Ÿ‘ˆ vertical stacking - input.draw(ctx); - } - - - // Draw output port - this.updatePorts(); - ctx.beginPath(); - ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - - - // Draw dropdowns LAST - this.modeDropdown.x = this.x + this.modeDropdown.offsetX; - this.modeDropdown.y = this.y + this.modeDropdown.offsetY; - this.modeDropdown.draw(ctx); - } - - contains(x, y) { - return super.contains(x, y) || - this.modeDropdown.contains(x, y) || - this.inputs.some(input => input.contains(x, y)); - } - - handleClick(mx, my) { - return this.modeDropdown.handleClick(mx, my) || - this.inputs.some(input => input.handleClick(mx, my)); - } - - handleKey(e) { - return this.inputs.some(input => input.handleKey(e)); - } - - - handleMouseMove(mx, my) { - return this.modeDropdown.handleMouseMove(mx, my); - } - - update(dt) { - let needsRedraw = this.modeDropdown.update(dt); - for (const input of this.inputs) { - if (input.update(dt)) needsRedraw = true; - } - - // Basic noise generation logic (placeholder) - const mode = this.modeDropdown.selected; - const rate = this.inputs[0].numericValue; - const threshold = this.inputs[1].numericValue; - const pulseWidth = this.inputs[2].numericValue; - const amplitude = this.inputs[3].numericValue; - const seed = this.inputs[4].numericValue; - - // Simple impulse logic for now - if (mode === "impulse") { - this.lastValue = Math.random() < rate * (dt / 1000) ? amplitude : 0; - } - - return needsRedraw; - } - - get outputValue() { - return this.lastValue; - } -} - -export class VariableNode extends Node { - constructor(x, y, label = "Variable") { - super(x, y, label); - this.width = 160; - this.height = 100; - - this.variableDropdown = new CanvasDropdown( - 10, 45, this.width - 20, - ["faceDetectX", "faceDetectY", "sine"], - "faceDetectX" - ); - - this.lastValue = 0; - this.timer = 0; - } - - draw(ctx) { - ctx.fillStyle = this.color || "#e6ffe6"; - ctx.strokeStyle = this.border || "#333"; - ctx.lineWidth = 1; - this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); - ctx.fill(); - ctx.stroke(); - - // Label - ctx.fillStyle = "#000"; - ctx.font = "14px sans-serif"; - ctx.fillText(this.label, this.x + 10, this.y + 20); - - // Dropdown label - ctx.fillText("Source", this.x + 10, this.y + 42); - - // Dropdown - this.variableDropdown.x = this.x + this.variableDropdown.offsetX; - this.variableDropdown.y = this.y + this.variableDropdown.offsetY; - this.variableDropdown.draw(ctx); - - // Output port - ctx.beginPath(); - ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); - ctx.fillStyle = "#888"; - ctx.fill(); - } - - contains(x, y) { - return super.contains(x, y) || this.variableDropdown.contains(x, y); - } - - handleClick(mx, my) { - return this.variableDropdown.handleClick(mx, my); - } - - handleMouseMove(mx, my) { - return this.variableDropdown.handleMouseMove(mx, my); - } - - update(dt) { - const changed = this.variableDropdown.update(dt); - - // Simulated variable values (replace with real data source) - const selected = this.variableDropdown.selected; - if (selected === "faceDetectX") { - this.lastValue = Math.random() * 640; // simulate X position - } else if (selected === "faceDetectY") { - this.lastValue = Math.random() * 480; // simulate Y position - } else if (selected === "sine") { - this.timer += dt; - this.lastValue = Math.sin(this.timer / 1000); - } - - return changed; - } - - get outputValue() { - return this.lastValue; + + this.ctx.restore(); } } diff --git a/nodeeditor/canvastools.js b/nodeeditor/canvastools.js index a4bda2d..f613611 100644 --- a/nodeeditor/canvastools.js +++ b/nodeeditor/canvastools.js @@ -13,7 +13,8 @@ export class CanvasTextInput { this.cursorVisible = false; this.cursorTimer = 0; - this.numericOnly = options.numericOnly || false; + this.mode = options.mode || "float"; // "int" or "float" + this.min = options.min ?? -Infinity; this.max = options.max ?? Infinity; this.label = label || ""; @@ -63,24 +64,23 @@ export class CanvasTextInput { handleKey(e) { if (!this.focused) return false; - + console.log(e); if (e.key === "Backspace") { this.value = this.value.slice(0, -1); } else if (e.key === "Enter") { this.clampToRange(); // ๐Ÿ‘ˆ clamp on Enter this.focused = false; } else if (e.key.length === 1) { - if (this.numericOnly) { + if (this.mode === "int") { + if (/[\d\-]/.test(e.key)) { + this.value += e.key; + } + } else if (this.mode === "float") { if (/[\d.\-]/.test(e.key)) { this.value += e.key; - - this.clampToRange(); // ๐Ÿ‘ˆ clamp on Enter } - } else { - this.value += e.key; - - this.clampToRange(); // ๐Ÿ‘ˆ clamp on Enter } + } return true; @@ -96,6 +96,12 @@ export class CanvasTextInput { } } + handleMouseMove(mx, my) { + // Optional: highlight or cursor change + return this.contains(mx, my); + } + + clampToRange() { let val = parseFloat(this.value); if (isNaN(val)) val = 0; @@ -164,6 +170,7 @@ export class CanvasDropdown { if (mx >= this.x && mx <= this.x + this.width && my >= itemY && my <= itemY + this.height) { this.selectedIndex = i; + this.selected = this.items[i]; // ๐Ÿ‘ˆ This line is missing this.open = false; this.hoverIndex = -1; return true; diff --git a/nodeeditor/nodes.js b/nodeeditor/nodes.js new file mode 100644 index 0000000..be68d15 --- /dev/null +++ b/nodeeditor/nodes.js @@ -0,0 +1,616 @@ +import { CanvasDropdown, CanvasTextInput } from "./canvastools.js" + +export const NODE_TYPES = { + Node: 0x01, + Servo: 0x02, + Curve: 0x03, + Noise: 0x04, + Variable: 0x05, + Math: 0x06, + Map: 0x07 +}; + +export class Node { + constructor(x, y, label, options = {}) { + this.type = NODE_TYPES.Node; + this.x = x; + this.y = y; + this.width = 120; + this.height = 60; + this.label = label; + this.input = { x: 0, y: 0 }; + this.output = { x: 0, y: 0 }; + //console.log(options.fill); + // Customizable visual options + this.color = options.fill || "#fef6e4"; // pastel fill default + this.border = options.stroke || "#333"; // border color default + + this.hasInput = false; + this.hasOutput = false; + + this.updatePorts(); + } + + + + updatePorts() { + this.input.x = this.x - 5; + this.input.y = this.y + this.height / 2; + this.output.x = this.x + this.width + 5; + this.output.y = this.y + this.height / 2; + } + + + + draw(ctx) { + ctx.fillStyle = this.color; + ctx.strokeStyle = this.border; + ctx.lineWidth = 1; + + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + this.updatePorts(); + if (this.hasInput) { + ctx.beginPath(); + ctx.arc(this.input.x, this.input.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + + if (this.hasOutput) { + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + } + + + + drawRoundedRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + r); + ctx.lineTo(x + w, y + h - r); + ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + ctx.lineTo(x + r, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - r); + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + ctx.closePath(); + } + + + contains(x, y) { + return x >= this.x && x <= this.x + this.width && + y >= this.y && y <= this.y + this.height; + } + + hitOutput(x, y) { + if (this.hasOutput) { + return Math.hypot(x - this.output.x, y - this.output.y) < 8; + } else { + return null; + } + } + + hitInput(x, y) { + if (this.hasInput) { + return Math.hypot(x - this.input.x, y - this.input.y) < 8; + } else { + return null; + } + } +} + +export class ServoNode extends Node { + constructor(x, y, label, motorId) { + super(x, y, label); + this.type = NODE_TYPES.Servo; + this.motorId = motorId; + this.width = 140; + this.height = 80; + + this.hasInput = true; + this.hasOutput = false; + } + + draw(ctx) { + // Node box + super.draw(ctx) + + // Motor ID display + ctx.font = "12px sans-serif"; + ctx.fillText(`Motor ${this.motorId}`, this.x + 10, this.y + 40); + + } + + hitOutput(x, y) { + return null; + } + + contains(x, y) { + return super.contains(x, y); + } + + handleClick(mx, my) { + + if (this.contains(mx, my)) { + window.setSelectedMotor?.(this.motorId); // or this.selectedMotorId if using dropdown + return false; + } + return false; + } + + + handleMouseMove(mx, my) { + return false; + } + + get selectedMotorId() { + return this.motorId; + } +} + +export class CurveNode extends Node { + constructor(x, y, label = "Curve", curveId = 0) { + super(x, y, label); + this.type = NODE_TYPES.Curve; + this.curveId = curveId; + this.width = 140; + this.height = 80; + + + this.hasInput = false; + this.hasOutput = true; + } + + draw(ctx) { + super.draw(ctx); + + // Curve ID display + ctx.font = "12px sans-serif"; + ctx.fillText(`Curve ${this.curveId}`, this.x + 10, this.y + 40); + + } + + + contains(x, y) { + return super.contains(x, y); + } + + handleClick(mx, my) { + if (this.contains(mx, my)) { + window.setSelectedMotor?.(this.curveId); + return false; + } + return false; + } + + + handleMouseMove(mx, my) { + return false; + } + + update(dt) { + // Placeholder: simulate curve output + const t = Date.now() / 1000; + this.lastValue = Math.sin(t + this.curveId); // simple sine curve + return false; + } + + get outputValue() { + return this.lastValue; + } +} + +export class InputNode extends Node { + constructor(x, y, label, options) { + super(x, y, label); + console.log(options); + this.inputField = new CanvasTextInput( + this.x + 10, + this.y + 35, + this.width - 20, + "1.0", + "", + { defaultValue: 1.0, numericOnly: true, min: 0, max: 10 } + ); + + } + + + + draw(ctx) { + // Node box + ctx.fillStyle = this.color || "#e0f7ff"; + ctx.strokeStyle = this.border || "#333"; + ctx.lineWidth = 1; + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + // Label + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + // Input field + this.inputField.x = this.x + 10; + this.inputField.y = this.y + 35; + this.inputField.draw(ctx); + + // Output port + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + + contains(x, y) { + return super.contains(x, y) || this.inputField.contains(x, y); + } + + handleClick(mx, my) { + console.log(mx, my); + return this.inputField.handleClick(mx, my); + } + + handleKey(e) { + return this.inputField.handleKey(e); + } + + + get outputValue() { + return this.inputField.numericValue; + } +} + +export class NoiseNode extends Node { + constructor(x, y, label = "Noise Generator") { + super(x, y, label); + this.type = NODE_TYPES.Noise; + this.width = 160; + this.height = 280; + + this.modeDropdown = new CanvasDropdown( + 10, 35, this.width - 20, + ["impulse", "pulse", "threshold", "smooth"], + "impulse" + ); + + const inputConfigs = [ + { label: "Rate", value: "1.0", min: 0, max: 100 }, + { label: "Threshold", value: "0.8", min: 0, max: 1 }, + { label: "Pulse Width", value: "0.2", min: 0, max: 10 }, + { label: "Amplitude", value: "1.0", min: 0, max: 10 }, + { label: "Seed", value: "0", min: 0, max: 99999 } + ]; + this.inputs = []; + + const inputSpacing = 42; // includes label + input height + + for (let i = 0; i < inputConfigs.length; i++) { + const cfg = inputConfigs[i]; + const inputY = 74 + i * inputSpacing; + + this.inputs.push(new CanvasTextInput( + 10, + inputY, + undefined, // uses default width + cfg.value, + cfg.label, + { + numericOnly: true, + min: cfg.min, + max: cfg.max + } + )); + } + + this.lastValue = 0; + this.timer = 0; + } + + draw(ctx) { + ctx.fillStyle = this.color || "#f0e6ff"; + ctx.strokeStyle = this.border || "#333"; + ctx.lineWidth = 1; + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + + // Draw Label + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + // Draw inputs + for (let i = 0; i < this.inputs.length; i++) { + const input = this.inputs[i]; + input.x = this.x + input.offsetX; + input.y = this.y + input.offsetY; // ๐Ÿ‘ˆ vertical stacking + input.draw(ctx); + } + + + // Draw output port + this.updatePorts(); + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + + + // Draw dropdowns LAST + this.modeDropdown.x = this.x + this.modeDropdown.offsetX; + this.modeDropdown.y = this.y + this.modeDropdown.offsetY; + this.modeDropdown.draw(ctx); + } + + contains(x, y) { + return super.contains(x, y) || + this.modeDropdown.contains(x, y) || + this.inputs.some(input => input.contains(x, y)); + } + + handleClick(mx, my) { + return this.modeDropdown.handleClick(mx, my) || + this.inputs.some(input => input.handleClick(mx, my)); + } + + handleKey(e) { + return this.inputs.some(input => input.handleKey(e)); + } + + + handleMouseMove(mx, my) { + return this.modeDropdown.handleMouseMove(mx, my); + } + + update(dt) { + let needsRedraw = this.modeDropdown.update(dt); + for (const input of this.inputs) { + if (input.update(dt)) needsRedraw = true; + } + + // Basic noise generation logic (placeholder) + const mode = this.modeDropdown.selected; + const rate = this.inputs[0].numericValue; + const threshold = this.inputs[1].numericValue; + const pulseWidth = this.inputs[2].numericValue; + const amplitude = this.inputs[3].numericValue; + const seed = this.inputs[4].numericValue; + + // Simple impulse logic for now + if (mode === "impulse") { + this.lastValue = Math.random() < rate * (dt / 1000) ? amplitude : 0; + } + + return needsRedraw; + } + + get outputValue() { + return this.lastValue; + } +} + +export class VariableNode extends Node { + constructor(x, y, label = "Variable") { + super(x, y, label); + this.type = NODE_TYPES.Variable; + this.width = 160; + this.height = 100; + + this.variableDropdown = new CanvasDropdown( + 10, 45, this.width - 20, + ["faceDetectX", "faceDetectY", "sine", "analogRead()"], + "sine" + ); + + + this.hasInput = false; + this.hasOutput = true; + } + + draw(ctx) { + super.draw(ctx); + + + // Dropdown label + ctx.fillText("Source", this.x + 10, this.y + 42); + + // Dropdown + this.variableDropdown.x = this.x + this.variableDropdown.offsetX; + this.variableDropdown.y = this.y + this.variableDropdown.offsetY; + this.variableDropdown.draw(ctx); + + } + + contains(x, y) { + return super.contains(x, y) || this.variableDropdown.contains(x, y); + } + + handleClick(mx, my) { + return this.variableDropdown.handleClick(mx, my); + } + + handleMouseMove(mx, my) { + return this.variableDropdown.handleMouseMove(mx, my); + } + + update(dt) { + const changed = this.variableDropdown.update(dt); + + // Simulated variable values (replace with real data source) + const selected = this.variableDropdown.selected; + if (selected === "faceDetectX") { + this.lastValue = Math.random() * 640; // simulate X position + } else if (selected === "faceDetectY") { + this.lastValue = Math.random() * 480; // simulate Y position + } else if (selected === "sine") { + this.timer += dt; + this.lastValue = Math.sin(this.timer / 1000); + } + + return changed; + } + + get outputValue() { + return this.lastValue; + } +} + +export class MathNode extends Node { + constructor(x, y, label = "Math") { + super(x, y, label); + this.type = NODE_TYPES.Math; + this.width = 160; + this.height = 100; + + this.operatorDropdown = new CanvasDropdown( + 10, 45, this.width - 20, + ["*", "/", "+", "-"], + "*" + ); + this.valueInput = new CanvasTextInput(10, 75, this.width - 20, "1.0", "Value", { mode: "float" }); + + + this.hasInput = true; + this.hasOutput = true; + } + + draw(ctx) { + super.draw(ctx); + + //ctx.fillText("Value", this.x + 10, this.y + 72); + this.valueInput.x = this.x + this.valueInput.offsetX; + this.valueInput.y = this.y + this.valueInput.offsetY; + this.valueInput.draw(ctx); + + ctx.fillText("Operator", this.x + 10, this.y + 42); + this.operatorDropdown.x = this.x + this.operatorDropdown.offsetX; + this.operatorDropdown.y = this.y + this.operatorDropdown.offsetY; + this.operatorDropdown.draw(ctx); + } + + contains(x, y) { + return ( + super.contains(x, y) || + this.operatorDropdown.contains(x, y) || + this.valueInput.contains(x, y) + ); + } + + handleKey(e) { + return this.valueInput.handleKey(e); + } + + + handleClick(mx, my) { + return ( + this.operatorDropdown.handleClick(mx, my) || + this.valueInput.handleClick(mx, my) + ); + } + + handleMouseMove(mx, my) { + return ( + this.operatorDropdown.handleMouseMove(mx, my) || + this.valueInput.handleMouseMove(mx, my) + ); + } + + update(dt) { + const changedDropdown = this.operatorDropdown.update(dt); + const changedInput = this.valueInput.update(dt); + return changedDropdown || changedInput; + } + + get outputValue() { + const input = this.inputNode?.outputValue ?? 0; + const value = parseFloat(this.valueInput.text) || 0; + const op = this.operatorDropdown.selected; + + switch (op) { + case "*": return input * value; + case "/": return value !== 0 ? input / value : 0; + case "+": return input + value; + case "-": return input - value; + default: return input; + } + } +} + +export class MapNode extends Node { + constructor(x, y, label = "Map") { + super(x, y, label); + this.type = NODE_TYPES.Map; + this.width = 180; + this.height = 180; + + this.inMinInput = new CanvasTextInput(10, 45, this.width - 20, "0", "In Min", { mode: "float" }); + this.inMaxInput = new CanvasTextInput(10, 70+10, this.width - 20, "1023", "In Max", { mode: "float" }); + this.outMinInput = new CanvasTextInput(10, 95+20, this.width - 20, "0", "Out Min", { mode: "float" }); + this.outMaxInput = new CanvasTextInput(10, 120+30, this.width - 20, "255", "Out Max", { mode: "float" }); + + this.hasInput = true; + this.hasOutput = true; + } + + draw(ctx) { + super.draw(ctx); + for (const input of [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput]) { + input.x = this.x + input.offsetX; + input.y = this.y + input.offsetY; + input.draw(ctx); + } + } + + contains(x, y) { + return super.contains(x, y) || [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.contains(x, y)); + } + + handleKey(e) { + return ( + this.inMinInput.handleKey(e) || + this.inMaxInput.handleKey(e) || + this.outMinInput.handleKey(e) || + this.outMaxInput.handleKey(e) + ); +} + + + handleClick(mx, my) { + return [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.handleClick(mx, my)); + } + + handleMouseMove(mx, my) { + return [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.handleMouseMove(mx, my)); + } + + update(dt) { + return [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.update(dt)); + } + + get outputValue() { + const input = this.inputNode?.outputValue ?? 0; + const inMin = this.inMinInput.numericValue; + const inMax = this.inMaxInput.numericValue; + const outMin = this.outMinInput.numericValue; + const outMax = this.outMaxInput.numericValue; + + if (inMax === inMin) return outMin; + return ((input - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin; + } +} + + diff --git a/script.js b/script.js index 318c3ce..9ee32f2 100644 --- a/script.js +++ b/script.js @@ -581,8 +581,8 @@ window.onload = () => { const filename = sanitizeFilename(rawInput); console.log("Sanitized filename:", filename); - const frameCount = 800; // or whatever your timeline length is - const frameRate = 50; + const frameCount = 480; // or whatever your timeline length is + const frameRate = 48; const version = 1; const headerSize = 16; @@ -611,6 +611,7 @@ window.onload = () => { let curvePacket = curveEditor.encodeCurves() + let nodeGraphPacket = nodeEditor.encodeNodeGraph(); // ๐Ÿ”น Append nodeGraphPacket diff --git a/style.css b/style.css index fb546ec..5132ac0 100644 --- a/style.css +++ b/style.css @@ -93,3 +93,12 @@ canvas { margin-bottom: 5px; } +.menu-item { + padding: 4px 8px; + cursor: pointer; +} +.menu-item:hover { + background: #ddd; +} + +