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