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;
+}
+
+