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.canDelete = true; this.inputs = []; 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; this.canDelete = 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; this.canDelete = false; } 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 } ); this.inputs.push(this.inputField); } 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 = 180; this.height = 180; this.inputFrequency = new CanvasTextInput(10, 45, this.width - 20, "1.0", "Frequency", { mode: "float" }); this.inputSeed = new CanvasTextInput(10, 70 + 10, this.width - 20, "0", "Seed", { mode: "int" }); this.inputs.push(this.inputFrequency); this.inputs.push(this.inputSeed); this.hasInput = false; this.hasOutput = true; } draw(ctx) { let max = 0 super.draw(ctx); for (const input of [this.inputFrequency, this.inputSeed]) { input.x = this.x + input.offsetX; input.y = this.y + input.offsetY; input.draw(ctx); } // 🔹 Noise preview const freq = Math.max(0.01, this.inputFrequency.numericValue); const seed = this.inputSeed.numericValue; const steps = 480; const previewHeight = 60; const previewWidth = this.width - 20; const startX = this.x + 10; const startY = this.y + this.height - previewHeight - 10; ctx.beginPath(); for (let i = 0; i < steps; i++) { const t = i / steps * freq * 10; // scale up to get more variation const noise = perlin1D_octave(seed, t); if (noise > max) { max = noise; //console.log(noise); } const x = startX + (i / steps) * previewWidth; const y = startY + previewHeight / 2 - noise * (previewHeight / 2); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle = "#444"; ctx.stroke(); } contains(x, y) { return super.contains(x, y) || [this.inputFrequency, this.inputSeed].some(i => i.contains(x, y)); } handleKey(e) { return ( this.inputFrequency.handleKey(e) || this.inputSeed.handleKey(e) ); } handleClick(mx, my) { return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my)); } handleMouseMove(mx, my) { return [this.inputFrequency, this.inputSeed].some(i => i.handleMouseMove(mx, my)); } update(dt) { return [this.inputFrequency, this.inputSeed].some(i => i.update(dt)); } get outputValue() { const input = this.inputNode?.outputValue ?? 0; const inFrequency = this.inputFrequency.numericValue; const inSeed = this.inputSeed.numericValue; return 0; } } 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()", "servo"], "sine" ); this.arg0Input = new CanvasTextInput(10, 75, this.width - 20, "0", "Motor ID", { mode: "int" }); this.inputs.push(this.variableDropdown); this.inputs.push(this.arg0Input); this.hasInput = false; this.hasOutput = true; } draw(ctx) { super.draw(ctx); // Dropdown label ctx.fillText("Source", this.x + 10, this.y + 42); if (this.variableDropdown.selected === "servo") { this.arg0Input.x = this.x + this.arg0Input.offsetX; this.arg0Input.y = this.y + this.arg0Input.offsetY; this.arg0Input.draw(ctx); } // 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, this.arg0Input].some(i => i.contains(x, y)); } handleKey(e) { return ( this.arg0Input.handleKey(e) ); } handleClick(mx, my) { return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my)); } handleMouseMove(mx, my) { return [this.variableDropdown, this.arg0Input].some(i => i.handleMouseMove(mx, my)); } update(dt) { return [this.variableDropdown, this.arg0Input].some(i => i.update(dt)); } 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 = 110; this.operatorDropdown = new CanvasDropdown( 10, 45, this.width - 20, ["*", "/", "+", "-"], "*" ); this.valueInput = new CanvasTextInput(10, 75, this.width - 20, "1.0", "Value", { mode: "float" }); this.inputs.push(this.operatorDropdown); this.inputs.push(this.valueInput); 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, "4095", "In Max", { mode: "float" }); this.outMinInput = new CanvasTextInput(10, 95 + 20, this.width - 20, "1024", "Out Min", { mode: "float" }); this.outMaxInput = new CanvasTextInput(10, 120 + 30, this.width - 20, "3072", "Out Max", { mode: "float" }); this.inputs.push(this.inMinInput); this.inputs.push(this.inMaxInput); this.inputs.push(this.outMinInput); this.inputs.push(this.outMaxInput); 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; } } const fade = t => t * t * t * (t * (t * 6 - 15) + 10); const lerp = (a, b, t) => a + t * (b - a); function grad(hash, x) { const h = hash & 15; let grad = 1 + (h & 7); // 1 to 8 if (h & 8) grad = -grad; return (grad * x) / 4; //return (hash & 1 ? -1 : 1) * x; } const perlinCache = new Map(); function perlin1D_octave(seed, x, octaves = 2, persistence = 0.5) { let total = 0; let amplitude = 1; let frequency = 1; let maxValue = 0; for (let i = 0; i < octaves; i++) { total += perlin1D(seed, x * frequency) * amplitude; maxValue += amplitude; amplitude *= persistence; frequency *= 2; } return total / maxValue; } function perlin1D(seed, x) { let seedCache = perlinCache.get(seed); if (!seedCache) { seedCache = new Map(); perlinCache.set(seed, seedCache); } const key = Math.round(x * 1000) / 1000; // round to reduce precision noise if (seedCache.has(key)) { return seedCache.get(key); } const perm = generatePermutation(seed); const xi = Math.floor(x) & 255; const xf = x - Math.floor(x); const u = fade(xf); const a = perm[xi]; const b = perm[xi + 1]; const result = lerp(grad(a, xf), grad(b, xf - 1), u); seedCache.set(key, result); return result; } function generatePermutation(seed) { const perm = new Array(512); const p = new Array(256); let s = seed; for (let i = 0; i < 256; i++) { s = (s * 1664525 + 1013904223) % 4294967296; p[i] = i; } for (let i = 255; i > 0; i--) { const j = s % (i + 1); [p[i], p[j]] = [p[j], p[i]]; s = (s * 1664525 + 1013904223) % 4294967296; } for (let i = 0; i < 512; i++) { perm[i] = p[i & 255]; } return perm; }