405 lines
11 KiB
JavaScript
405 lines
11 KiB
JavaScript
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;
|
|
}
|
|
|
|
|