669 lines
17 KiB
JavaScript
669 lines
17 KiB
JavaScript
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;
|
|
}
|