import { SliderControl, TextInputControl, DropdownControl } from './Control.js'; export class Node { constructor(x, y) { this.x = x; this.y = y; this.width = 180; this.headerHeight = 6; this.padding = 12; this.isHovered = false; this.isSelected = false; this.isDragging = false; this.offsetX = 0; this.offsetY = 0; this.hasInput = true; this.hasOutput = true; this.input = { x: 0, y: 0 }; this.output = { x: 0, y: 0 }; this.hoveredPort = null; // "input", "output", or null this.canDelete = true; this.controls = [] this.addControl(new SliderControl(this.padding, 0, this.width - this.padding * 2, 38, "Volume", 0, 4095, 1)); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "text", "Text", "")); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "float", "Float", "")); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "int", "int", "")); this.addControl(new DropdownControl(this.padding, 0, this.width - this.padding * 2, 38, "Mode", ["Auto", "Manual", "Disabled"], "Auto")); } get height() { if (this.controls.length === 0) { return this.headerHeight + this.padding * 2; } const bottom = Math.max( ...this.controls .filter(c => !c.hidden) .map(c => c.offsetY + c.height) ); return bottom + this.padding; } 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) { const radius = 8; ctx.fillStyle = "#444"; ctx.strokeStyle = "#aaa"; ctx.lineWidth = 1; const inset = ctx.lineWidth / 2; if (this.isHovered) ctx.fillStyle = "#555"; if (this.isSelected) ctx.strokeStyle = "#0af"; if (this.isDragging) ctx.fillStyle = "#666"; ctx.beginPath(); ctx.roundRect(this.x + inset, this.y + inset, this.width - ctx.lineWidth, this.height - ctx.lineWidth, radius); ctx.fill(); ctx.stroke(); // Draw header label //this.drawLabel(ctx, "Node"); // Draw controls for (const control of this.controls) { if (!control.hidden) { control.draw(ctx, this.x, this.y); } } for (const control of this.controls) { if (!control.hidden) { control.drawLate(ctx, this.x, this.y); } } this.updatePorts(); if (this.hasInput) { ctx.beginPath(); const r = this.hoveredPort === "input" ? 8 : 6; ctx.arc(this.input.x, this.input.y, r, 0, Math.PI * 2); ctx.fillStyle = "#888"; ctx.fill(); } if (this.hasOutput) { ctx.beginPath(); const r = this.hoveredPort === "output" ? 8 : 6; ctx.arc(this.output.x, this.output.y, r, 0, Math.PI * 2); ctx.fillStyle = "#888"; ctx.fill(); } } drawLabel(ctx, label) { ctx.fillStyle = "#fff"; ctx.font = "13px sans-serif"; ctx.textBaseline = "middle"; ctx.fillText(label, this.x + this.padding, this.y + 16); } contains(mx, my) { return mx > this.x && mx < this.x + this.width && my > this.y && my < this.y + this.height; } onHover(state) { this.isHovered = state; } onSelect(state) { this.isSelected = state; } onDrag(state) { this.isDragging = state; } onMouseMove(mx, my) { // Check if mouse is near input or output this.hoveredPort = null; if (this.hasInput) { const dx = mx - this.input.x; const dy = my - this.input.y; if (dx * dx + dy * dy < 64) { this.hoveredPort = "input"; return; } } if (this.hasOutput) { const dx = mx - this.output.x; const dy = my - this.output.y; if (dx * dx + dy * dy < 64) { this.hoveredPort = "output"; } } } addControl(control) { const lastBottom = this.controls.length > 0 ? Math.max(...this.controls.filter(c => !c.hidden).map(c => c.offsetY + c.height)) : this.headerHeight + this.padding; control.offsetY = lastBottom + this.padding; this.controls.push(control); } } export class ServoNode extends Node { constructor(x, y, motorID) { super(x, y); this.hasInput = true; this.hasOutput = false; this.canDelete = false; this.controls = []; this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "int", "ID", motorID, true)); } draw(ctx) { super.draw(ctx); super.drawLabel(ctx, "Servo Output"); } } export class CurveNode extends Node { constructor(x, y, motorID) { super(x, y); this.hasInput = false; this.hasOutput = true; this.canDelete = false; this.controls = []; this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "int", "ID", motorID, true)); } draw(ctx) { super.draw(ctx); super.drawLabel(ctx, "Animation Curve"); } } export class VariableNode extends Node { constructor(x, y) { super(x, y); this.hasInput = false; this.hasOutput = true; this.controls = []; this.addControl(new DropdownControl(this.padding, 0, this.width - this.padding * 2, 38, "Variable", ["faceDetectX", "faceDetectY", "sine", "analogRead()", "servo"], "faceDetectX")); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "int", "arg", 0, false)); } draw(ctx) { super.draw(ctx); super.drawLabel(ctx, "Variable Source"); if (this.controls[0].getValue() == 3 || this.controls[0].getValue() == 4) { this.controls[1].hidden = false; } else { this.controls[1].hidden = true; } } } export class MathNode extends Node { constructor(x, y) { super(x, y); this.hasInput = true; this.hasOutput = true; this.controls = []; this.addControl(new DropdownControl(this.padding, 0, this.width - this.padding * 2, 38, "Operator", ["*", "/", "+", "-"], "*")); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "float", "arg", 0, false)); } draw(ctx) { super.draw(ctx); super.drawLabel(ctx, "Math Operator"); } } export class MapNode extends Node { constructor(x, y) { super(x, y); this.hasInput = true; this.hasOutput = true; this.controls = []; this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "float", "Input Min", 0, false)); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "float", "Input Max", 0, false)); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "float", "Output Min", 0, false)); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "float", "Output Max", 0, false)); } draw(ctx) { super.draw(ctx); super.drawLabel(ctx, "Map Value"); } } export class NoiseNode extends Node { constructor(x, y) { super(x, y); this.hasInput = false; this.hasOutput = true; this.controls = []; this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "float", "Frequency", 0.5, false)); this.addControl(new TextInputControl(this.padding, 0, this.width - this.padding * 2, 38, "int", "Seed", 0, false)); } get height() { const baseHeight = super.height; const previewHeight = 60; return baseHeight + previewHeight + this.padding * 2; } draw(ctx) { super.draw(ctx); super.drawLabel(ctx, "Noise"); const freq = Math.max(0.01, parseFloat(this.controls[0].getValue()) || 0); const seed = parseFloat(this.controls[1].getValue()) || 0; const steps = 256; const previewHeight = 60; const previewWidth = this.width - this.padding * 2; const startX = this.x + this.padding; const startY = this.y + this.height - previewHeight - this.padding; ctx.fillStyle = "#f0f0f0"; // light gray background ctx.strokeStyle = "#ccc"; // optional border ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect( startX - this.padding / 2, startY - this.padding / 2, previewWidth + this.padding, previewHeight + this.padding, 6 ); ctx.fill(); ctx.stroke(); ctx.beginPath(); for (let i = 0; i < steps; i++) { const t = i / steps * freq * 10; const noise = perlin1D_octave(seed, t); const x = startX + (i / steps) * previewWidth; const y = startY + previewHeight / 2 - noise * (previewHeight / 2); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); } ctx.strokeStyle = "#444"; ctx.stroke(); } } 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; }