new node editor (slicker controls) implemented

master
Jake 2025-11-03 10:17:07 +08:00
parent b8742ccb66
commit c1c7b41135
8 changed files with 1942 additions and 1478 deletions

View File

@ -22,19 +22,19 @@
<!-- Nav tabs --> <!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="motors-tab" data-bs-toggle="tab" data-bs-target="#motors" type="button" <button class="nav-link" id="motors-tab" data-bs-toggle="tab" data-bs-target="#motors" type="button" role="tab"
role="tab" aria-controls="motors" aria-selected="false">Motors</button> aria-controls="motors" aria-selected="false">Motors</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="animation-tab" data-bs-toggle="tab" data-bs-target="#animation" type="button" <button class="nav-link active" id="animation-tab" data-bs-toggle="tab" data-bs-target="#animation"
role="tab" aria-controls="animation" aria-selected="true">Animation</button> type="button" role="tab" aria-controls="animation" aria-selected="true">Animation</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="about-tab" data-bs-toggle="tab" data-bs-target="#about" type="button" role="tab" <button class="nav-link" id="about-tab" data-bs-toggle="tab" data-bs-target="#about" type="button" role="tab"
aria-controls="about" aria-selected="false">About</button> aria-controls="about" aria-selected="false">About</button>
</li> </li>
</ul> </ul>
<!-- Tab content --> <!-- Tab content -->
@ -208,7 +208,8 @@
<canvas id="nodeeditor"></canvas> <canvas id="nodeeditor" ></canvas>
<div id="contextMenu" style=" <div id="contextMenu" style="
position: absolute; position: absolute;
display: none; display: none;

672
nodeeditor/Control.js Normal file
View File

@ -0,0 +1,672 @@
export class Control {
constructor(offsetX, offsetY, width, height, label = "", readOnly = false, hidden = false) {
this.offsetX = offsetX;
this.offsetY = offsetY;
this.width = width;
this.height = height;
this.label = label;
this.isHovered = false;
this.isFocused = false;
this.value = null;
this.onChange = null;
this.readOnly = readOnly;
this.hidden = hidden;
this.dragStartX = 0;
this.dragAccum = 0;
this.dragging = false;
}
getValue() {
return this.value;
}
draw(ctx, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
ctx.save();
ctx.fillStyle = this.isHovered ? "#eee" : "#f8f8f8";
ctx.strokeStyle = this.isFocused ? "#0af" : "#999";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, y, this.width, this.height, 6);
ctx.fill();
ctx.stroke();
this.drawLabel(ctx, x, y);
ctx.restore();
}
drawLate(ctx, parentX, parentY) {
}
drawLabel(ctx, x, y) {
ctx.fillStyle = "#333";
ctx.font = "12px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText(this.label, x + 6, y + 6);
}
contains(mx, my, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
return mx > x && mx < x + this.width &&
my > y && my < y + this.height;
}
onEvent(type, data) {
if (this.readOnly) {
if (type === "mouseMove") {
return this.onMouseMove?.(data.mx, data.my, data.parentX, data.parentY, data.modifiers);
}
return;
}
switch (type) {
case "mouseDown": return this.onMouseDown?.(data.mx, data.my, data.parentX, data.parentY);
case "mouseUp": return this.onMouseUp?.(data.mx, data.my, data.parentX, data.parentY);
case "mouseMove": return this.onMouseMove?.(data.mx, data.my, data.parentX, data.parentY, data.modifiers);
case "keyDown": return this.onKeyDown?.(data.key);
case "scroll": return this.onScroll?.(data.delta);
case "clipboard": return this.onClipboard?.(data.event);
default:
console.warn(`Unhandled event type: ${type}`, data);
}
}
onMouseDown(mx, my, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
if (mx > x && mx < x + this.width &&
my > y && my < y + this.height) {
this.onClick(); // delegate to subclass
return true;
}
return false;
}
onMouseUp(mx, my, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
const inside = mx > x && mx < x + this.width &&
my > y && my < y + this.height;
this.dragging = false;
if (!inside) {
this.onBlur();
}
}
onMouseMove(mx, my, parentX, parentY, modifiers = {}) {
if (!this.dragging) return;
const deltaX = mx - this.dragStartX;
this.dragStartX = mx;
this.dragAccum += deltaX;
this.onDragDelta?.(deltaX, modifiers);
}
onScroll(delta) {
const step = 1;
this.value = Math.max(this.min, Math.min(this.max, this.value + delta * step));
if (this.onChange) this.onChange(this.value);
}
onHover(state) {
this.isHovered = state;
}
onFocus(state) {
this.isFocused = state;
}
onClick() {
this.onFocus(true);
}
onBlur() {
this.onFocus(false);
}
onClipboard(e) {
// Default: do nothing
}
}
export class SliderControl extends Control {
constructor(x, y, width, height, label = "", min = 0, max = 100, initial = 50, readOnly = false) {
super(x, y, width, height, label, readOnly);
this.min = min;
this.max = max;
this.value = initial;
}
getValue() {
return this.value;
}
draw(ctx, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
// Draw background and border
ctx.save();
ctx.fillStyle = this.isHovered ? "#eee" : "#f8f8f8";
ctx.strokeStyle = this.isFocused ? "#0af" : "#999";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, y, this.width, this.height, 6);
ctx.fill();
ctx.stroke();
this.drawLabel(ctx, x, y);
// Draw value (top-right)
ctx.textAlign = "right";
ctx.fillText(this.value.toFixed(0), x + this.width - 6, y + 6);
// Draw slider track
const padding = 10;
const trackY = y + this.height - 14; // lower in control
const trackStart = x + padding;
const trackEnd = x + this.width - padding;
const trackWidth = trackEnd - trackStart;
const percent = (this.value - this.min) / (this.max - this.min);
const knobX = trackStart + percent * trackWidth;
ctx.strokeStyle = "#aaa";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(trackStart, trackY);
ctx.lineTo(trackEnd, trackY);
ctx.stroke();
// Draw knob
ctx.fillStyle = this.isFocused ? "#0af" : "#666";
ctx.beginPath();
ctx.arc(knobX, trackY, 6, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
onMouseDown(mx, my, parentX, parentY) {
const handled = super.onMouseDown(mx, my, parentX, parentY);
if (handled) {
this.dragging = true;
this.dragStartX = mx;
this.dragAccum = 0;
return true;
}
return false;
}
onMouseUp(mx, my, parentX, parentY) {
this.dragging = false;
this.onBlur();
}
onScroll(delta) {
super.onScroll?.(delta); // optional chaining in case base is extended later
}
onDragDelta(accumulatedDelta, modifiers = {}) {
const trackWidth = this.width - 20;
const valuePerPixel = (this.max - this.min) / trackWidth;
const screenDelta = accumulatedDelta / (1 / modifiers.zoom || 1);
const sensitivity = modifiers.ctrl ? 0.015 : 1.0;
const deltaValue = screenDelta * valuePerPixel * sensitivity;
this.dragAccum += deltaValue;
if (Math.abs(this.dragAccum) >= 1) {
const change = Math.round(this.dragAccum);
this.value = Math.max(this.min, Math.min(this.max, this.value + change));
this.dragAccum -= change;
if (this.onChange) this.onChange(this.value);
}
}
}
export class TextInputControl extends Control {
constructor(x, y, width, height, inputType = "text", label = "", initial = "", readOnly = false) {
super(x, y, width, height, label, readOnly);
this.text = String(initial);
this.cursorVisible = false;
this.cursorTimer = 0;
this.selecting = false;
this.selectionStart = 0;
this.selectionEnd = 0;
this.cursorIndex = this.text.length;
this.inputType = inputType;//"float"; // "text", "int", "float", "password"
this.allowNegative = true;
this.maxLength = 20;
this.regex = /^[a-zA-Z0-9]+$/
}
getValue() {
return this.text;
}
draw(ctx, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
ctx.save();
ctx.fillStyle = this.isHovered ? "#eee" : "#fff";
ctx.strokeStyle = this.isFocused ? "#0af" : "#999";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, y, this.width, this.height, 6);
ctx.fill();
ctx.stroke();
this.drawLabel(ctx, x, y);
const textBefore = this.text.slice(0, this.selectionStart);
const selectedText = this.text.slice(this.selectionStart, this.selectionEnd);
const textWidthBefore = ctx.measureText(textBefore).width;
const selectedWidth = ctx.measureText(selectedText).width;
if (this.selectionStart !== this.selectionEnd) {
ctx.fillStyle = "#cce5ff";
ctx.fillRect(x + 6 + textWidthBefore, textY - 10, selectedWidth, 20);
}
// Text field
ctx.font = "13px monospace";
ctx.textBaseline = "middle";
ctx.fillStyle = "#000";
ctx.textAlign = "left";
const textY = y + this.height / 2 + 4;
const displayText = this.inputType === "password"
? "*".repeat(this.text.length)
: this.text;
ctx.fillText(displayText, x + 6, textY);
// Blinking cursor
if (this.isFocused && this.cursorVisible) {
const textWidth = ctx.measureText(this.text).width;
const cursorX = x + 6 + ctx.measureText(this.text.slice(0, this.cursorIndex)).width;
ctx.beginPath();
ctx.moveTo(cursorX, textY - 8);
ctx.lineTo(cursorX, textY + 8);
ctx.strokeStyle = "#0af";
ctx.stroke();
}
ctx.restore();
}
onClick() {
super.onClick();
this.cursorVisible = true;
this.cursorTimer = 0;
}
onScroll(delta) {
const current = parseFloat(this.text) || 0;
const step = this.inputType === "int" ? 1 : 0.1;
const next = current + delta * step;
// Clamp if min/max are defined
const clamped = Math.max(this.min ?? -Infinity, Math.min(this.max ?? Infinity, next));
// Round to avoid floating point artifacts
const precision = step < 1 ? 2 : 0;
const factor = Math.pow(10, precision);
const rounded = Math.round(clamped * factor) / factor;
this.text = String(rounded);
this.cursorIndex = this.text.length;
this.selectionStart = this.selectionEnd = this.cursorIndex;
if (this.onChange) this.onChange(this.getValue());
}
onKeyDown(key) {
if (!this.isFocused) return;
const hasShift = window.event?.shiftKey;
if (key === "ArrowLeft") {
if (hasShift) {
this.cursorIndex = Math.max(0, this.cursorIndex - 1);
this.selectionEnd = this.cursorIndex;
} else {
this.cursorIndex = Math.max(0, this.cursorIndex - 1);
this.selectionStart = this.selectionEnd = this.cursorIndex;
}
} else if (key === "ArrowRight") {
if (hasShift) {
this.cursorIndex = Math.min(this.text.length, this.cursorIndex + 1);
this.selectionEnd = this.cursorIndex;
} else {
this.cursorIndex = Math.min(this.text.length, this.cursorIndex + 1);
this.selectionStart = this.selectionEnd = this.cursorIndex;
}
} else if (key === "Backspace") {
if (this.selectionStart !== this.selectionEnd) {
this.text = this.text.slice(0, this.selectionStart) + this.text.slice(this.selectionEnd);
this.cursorIndex = this.selectionStart;
} else if (this.cursorIndex > 0) {
this.text = this.text.slice(0, this.cursorIndex - 1) + this.text.slice(this.cursorIndex);
this.cursorIndex--;
}
this.selectionStart = this.selectionEnd = this.cursorIndex;
} else if (key === "Delete") {
if (this.selectionStart !== this.selectionEnd) {
this.text = this.text.slice(0, this.selectionStart) + this.text.slice(this.selectionEnd);
this.cursorIndex = this.selectionStart;
} else if (this.cursorIndex < this.text.length) {
this.text = this.text.slice(0, this.cursorIndex) + this.text.slice(this.cursorIndex + 1);
}
this.selectionStart = this.selectionEnd = this.cursorIndex;
} else if (key.length === 1) {
const preview = this.text.slice(0, this.selectionStart) + key + this.text.slice(this.selectionEnd);
if (!this.isValidInput(preview)) return;
if (this.selectionStart !== this.selectionEnd) {
this.text = this.text.slice(0, this.selectionStart) + key + this.text.slice(this.selectionEnd);
this.cursorIndex = this.selectionStart + 1;
} else {
this.text = this.text.slice(0, this.cursorIndex) + key + this.text.slice(this.cursorIndex);
this.cursorIndex++;
}
this.selectionStart = this.selectionEnd = this.cursorIndex;
}
if (this.onChange) this.onChange(this.text);
}
onClipboard(e) {
if (e.ctrlKey && e.key === "c") {
const selected = this.text.slice(this.selectionStart, this.selectionEnd);
navigator.clipboard.writeText(selected);
e.preventDefault();
}
if (e.ctrlKey && e.key === "v") {
navigator.clipboard.readText().then((clip) => {
let filtered = clip;
const preview = this.text.slice(0, this.selectionStart) + clip + this.text.slice(this.selectionEnd);
if (!this.isValidInput(preview)) return;
const before = this.text.slice(0, this.selectionStart);
const after = this.text.slice(this.selectionEnd);
this.text = before + filtered + after;
this.cursorIndex = before.length + filtered.length;
this.selectionStart = this.selectionEnd = this.cursorIndex;
if (this.onChange) this.onChange(this.text);
});
e.preventDefault();
}
}
isValidInput(input, strict = false) {
// Allow empty input
if (input === "") return true;
if (this.inputType === "int") {
if (!strict) {
// Allow "-" while typing
if (input === "-" && this.allowNegative) return true;
}
const intPattern = this.allowNegative ? /^-?\d+$/ : /^\d+$/;
return intPattern.test(input);
}
if (this.inputType === "float") {
if (!strict) {
// Relaxed pattern for typing
const relaxedPattern = this.allowNegative
? /^-?(\d+)?(\.)?(\d*)?$/
: /^(\d+)?(\.)?(\d*)?$/;
return relaxedPattern.test(input);
}
// Auto-correct ".123" → "0.123", "-.123" → "-0.123"
if (/^\.\d+$/.test(input)) {
this.text = "0" + input;
} else if (/^-\.\d+$/.test(input)) {
input = "-0" + input.slice(1);
console.log(">");
}
// Strict pattern for finalized float
const strictPattern = this.allowNegative
? /^-?\d+(\.\d+)?$/
: /^\d+(\.\d+)?$/;
return strictPattern.test(input);
}
// Regex check (applies to all types)
if (this.regex && !this.regex.test(input)) return false;
// Max length
const currentLength = this.text.length - (this.selectionEnd - this.selectionStart);
if (input.length + currentLength > this.maxLength) return false;
return true;
}
onBlur() {
super.onBlur();
if (!this.isValidInput(this.text, true)) {
// Optionally reset or trim invalid input
// this.text = "";
// this.cursorIndex = 0;
// this.selectionStart = this.selectionEnd = 0;
if (this.onChange) this.onChange(this.text);
}
}
update(deltaTime) {
if (this.isFocused) {
this.cursorTimer += deltaTime;
if (this.cursorTimer > 500) {
this.cursorVisible = !this.cursorVisible;
this.cursorTimer = 0;
}
}
}
}
export class DropdownControl extends Control {
constructor(x, y, width, height, label = "", options = [], initial = "", readOnly = false) {
super(x, y, width, height, label, readOnly);
this.options = options;
if (typeof initial === "number" && initial >= 0 && initial < options.length) {
this.value = options[initial];
} else if (typeof initial === "string" && options.includes(initial)) {
this.value = initial;
} else {
this.value = options.length > 0 ? options[0] : "";
}
this.isOpen = false;
this.hoveredIndex = -1;
}
getValue() {
return this.options.indexOf(this.value);
//return this.value; // returns string vlue
}
draw(ctx, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
ctx.save();
ctx.fillStyle = this.isHovered ? "#eee" : "#f8f8f8";
ctx.strokeStyle = this.isFocused ? "#0af" : "#999";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, y, this.width, this.height, 6);
ctx.fill();
ctx.stroke();
this.drawLabel(ctx, x, y);
// Draw selected value
ctx.fillStyle = "#000";
ctx.font = "13px sans-serif";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(this.value, x + this.width - 20, y + this.height / 2);
// Draw dropdown arrow
ctx.beginPath();
ctx.moveTo(x + this.width - 16, y + this.height / 2 - 4);
ctx.lineTo(x + this.width - 10, y + this.height / 2 - 4);
ctx.lineTo(x + this.width - 13, y + this.height / 2 + 2);
ctx.closePath();
ctx.fillStyle = "#666";
ctx.fill();
ctx.restore();
}
drawLate(ctx, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
// Draw options if open
if (this.isOpen) {
for (let i = 0; i < this.options.length; i++) {
const optY = y + this.height + i * this.height;
const isHovered = i === this.hoveredIndex;
ctx.fillStyle = isHovered ? "#cce5ff" : (this.options[i] === this.value ? "#ddd" : "#fff");
ctx.strokeStyle = "#999";
ctx.beginPath();
ctx.rect(x, optY, this.width, this.height);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#000";
ctx.textAlign = "left";
ctx.fillText(this.options[i], x + 6, optY + this.height / 2);
}
}
}
onClick() {
super.onClick();
this.isOpen = !this.isOpen;
}
onMouseDown(mx, my, parentX, parentY) {
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
if (this.isOpen) {
for (let i = 0; i < this.options.length; i++) {
const optY = y + this.height + i * this.height;
if (mx > x && mx < x + this.width && my > optY && my < optY + this.height) {
this.value = this.options[i];
this.isOpen = false;
if (this.onChange) this.onChange(this.value);
return true;
}
}
}
return super.onMouseDown(mx, my, parentX, parentY);
}
onMouseMove(mx, my, parentX, parentY) {
if (!this.isOpen) {
this.hoveredIndex = -1;
return;
}
const x = parentX + this.offsetX;
const y = parentY + this.offsetY;
this.hoveredIndex = -1;
for (let i = 0; i < this.options.length; i++) {
const optY = y + this.height + i * this.height;
if (mx > x && mx < x + this.width && my > optY && my < optY + this.height) {
this.hoveredIndex = i;
break;
}
}
}
onScroll(delta) {
delta = -delta;
const currentIndex = this.options.indexOf(this.value);
if (currentIndex === -1 || this.options.length === 0) return;
const nextIndex = Math.max(0, Math.min(this.options.length - 1, currentIndex + delta));
this.value = this.options[nextIndex];
if (this.onChange) this.onChange(this.getValue());
}
onBlur() {
super.onBlur();
this.isOpen = false;
}
}

404
nodeeditor/Node.js Normal file
View File

@ -0,0 +1,404 @@
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;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,224 @@
import { Node, ServoNode, VariableNode, CurveNode, MathNode, MapNode, NoiseNode } from './Node.js';
export const NODE_TYPES = {
Node: 0x01,
Servo: 0x02,
Curve: 0x03,
Noise: 0x04,
Variable: 0x05,
Math: 0x06,
Map: 0x07
};
function GetNodeType(node) {
console.log(node.constructor.name);
switch (node.constructor.name) {
case "ServoNode":
return NODE_TYPES.Servo;
case "CurveNode":
return NODE_TYPES.Curve;
case "NoiseNode":
return NODE_TYPES.Noise;
case "VariableNode":
return NODE_TYPES.Variable;
case "MathNode":
return NODE_TYPES.Math;
case "MapNode":
return NODE_TYPES.Map;
default:
return NODE_TYPES.Node;
}
}
export function encodeNodeGraph(nodes, connections) {
const bufferSize = 1024; // adjust based on expected graph size
const buffer = new ArrayBuffer(bufferSize);
const view = new DataView(buffer);
let offset = 0;
// Node count (1 byte)
view.setUint8(offset++, nodes.length);
//console.log(ndoes);
// Encode nodes
nodes.forEach((node, index) => {
node.id = index;
node.type = GetNodeType(node);
view.setUint8(offset++, node.type); // Node type
view.setUint8(offset++, node.id); // Node ID
view.setUint16(offset, node.x, true); offset += 2;
view.setUint16(offset, node.y, true); offset += 2;
switch (node.type) {
case NODE_TYPES.Servo:
view.setUint8(offset++, node.controls[0].getValue());
break;
case NODE_TYPES.Curve:
view.setUint8(offset++, node.controls[0].getValue());
break;
case NODE_TYPES.Noise:
view.setFloat32(offset, node.controls[0].getValue(), true); offset += 4;
view.setUint16(offset, node.controls[1].getValue()); offset += 2;
break;
case NODE_TYPES.Variable:
view.setUint8(offset++, node.controls[0].getValue());
view.setUint8(offset++, node.controls[1].getValue());
break;
case NODE_TYPES.Math:
view.setUint8(offset++, node.controls[0].getValue());
view.setFloat32(offset, node.controls[1].getValue(), true); offset += 4;
break;
case NODE_TYPES.Map:
view.setFloat32(offset, node.controls[0].getValue(), true); offset += 4;
view.setFloat32(offset, node.controls[1].getValue(), true); offset += 4;
view.setFloat32(offset, node.controls[2].getValue(), true); offset += 4;
view.setFloat32(offset, node.controls[3].getValue(), true); offset += 4;
//console.log(node.inMinInput.numericValue, node.inMaxInput.numericValue, node.outMinInput.numericValue, node.outMaxInput.numericValue);
break;
default:
console.warn("Unknown node type:", node);
}
});
// Connection count (1 byte)
view.setUint8(offset++, connections.length);
// Encode connections
connections.forEach(conn => {
view.setUint8(offset++, conn.from.id);
view.setUint8(offset++, conn.to.id);
});
// Slice the buffer to actual used size
return new Uint8Array(buffer.slice(0, offset));
}
export function loadFromBinary(editor, data) {
let nodes = []
let connections = []
const view = new DataView(data.buffer);
let offset = 0;
const nodeCount = view.getUint8(offset++);
const idMap = {}; // Map node IDs to actual node instances
for (let i = 0; i < nodeCount; i++) {
const type = view.getUint8(offset++);
const id = view.getUint8(offset++);
const x = view.getUint16(offset, true); offset += 2;
const y = view.getUint16(offset, true); offset += 2;
let node = null;
switch (type) {
case NODE_TYPES.Servo: {
const motorID = view.getUint8(offset++);
const newNode = new ServoNode(x, y, motorID);
editor.nodes.push(newNode);
node = newNode;
break;
}
case NODE_TYPES.Curve: {
const curveID = view.getUint8(offset++);
const newNode = new CurveNode(x, y, curveID);
editor.nodes.push(newNode);
node = newNode;
break;
}
case NODE_TYPES.Noise: {
const frequency = view.getFloat32(offset, true); offset += 4;
const seed = view.getUint16(offset, true); offset += 2;
const newNode = new NoiseNode(x, y);
editor.nodes.push(newNode);
node = newNode;
node.controls[0].text = String(frequency);
node.controls[1].text = String(seed);
break;
}
case NODE_TYPES.Variable: {
const source = view.getUint8(offset++);
const arg0 = view.getUint8(offset++);
const newNode = new VariableNode(x, y);
editor.nodes.push(newNode);
node = newNode;
node.controls[0].selectedIndex = source;
node.controls[1].value = arg0;
break;
}
case NODE_TYPES.Math: {
const op = view.getUint8(offset++);
const value = view.getFloat32(offset, true); offset += 4;
const newNode = new MathNode(x, y);
editor.nodes.push(newNode);
node = newNode;
node.controls[0].selectedIndex = op;
node.controls[1].text = String(value);
break;
}
case NODE_TYPES.Map: {
const inMin = view.getFloat32(offset, true); offset += 4;
const inMax = view.getFloat32(offset, true); offset += 4;
const outMin = view.getFloat32(offset, true); offset += 4;
const outMax = view.getFloat32(offset, true); offset += 4;
const newNode = new MapNode(x, y);
editor.nodes.push(newNode);
node = newNode;
node.controls[0].text = String(inMin);
node.controls[1].text = String(inMax);
node.controls[2].text = String(outMin);
node.controls[3].text = String(outMax);
break;
}
default: {
console.error("UNKNOWN NODE");
break;
}
}
if (node) {
node.id = id;
idMap[id] = node;
}
//editor.nodes.push(node);
}
//console.log(this.getNodeByID(0));
// 🔗 Load connections
const connectionCount = view.getUint8(offset++);
for (let i = 0; i < connectionCount; i++) {
const fromID = view.getUint8(offset++);
const toID = view.getUint8(offset++);
const fromNode = idMap[fromID];
const toNode = idMap[toID];
if (fromNode && toNode) {
editor.connections.push({ from: fromNode, to: toNode });
}
}
// Reencode all positions to SIGNED ints
editor.nodes.forEach(node => {
node.x = (node.x << 16) >> 16;
node.y = (node.y << 16) >> 16;
});
//editor.nodes = nodes;
//editor.connections = connections;
console.log(editor.nodes);
console.log(editor.connections);
editor.draw();
//this._redraw();
}

View File

@ -1,217 +0,0 @@
export class CanvasTextInput {
constructor(offsetX, offsetY, width = 120, value = "", label = "", options = {}) {
this.x = 0;
this.y = 0;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.width = width;
this.height = 24;
this.labelOffset = 16; // space above input for label
this.value = String(value);
//this.placeholder = placeholder;
this.focused = false;
this.cursorVisible = false;
this.cursorTimer = 0;
this.mode = options.mode || "float"; // "int" or "float"
this.min = options.min ?? -Infinity;
this.max = options.max ?? Infinity;
this.label = label || "";
}
draw(ctx) {
if (this.label) {
ctx.fillStyle = "#000";
ctx.font = "13px sans-serif";
ctx.fillText(this.label, this.x, this.y - this.labelOffset + 15);
}
ctx.fillStyle = "#fff";
ctx.strokeStyle = "#666";
ctx.fillRect(this.x, this.y, this.width, this.height);
ctx.strokeRect(this.x, this.y, this.width, this.height);
ctx.fillStyle = "#000";
ctx.font = "14px sans-serif";
const text = this.value || this.placeholder;
ctx.fillText(text, this.x + 5, this.y + 15);
if (this.focused && this.cursorVisible) {
const textWidth = ctx.measureText(this.value).width;
ctx.beginPath();
ctx.moveTo(this.x + 5 + textWidth, this.y + 4);
ctx.lineTo(this.x + 5 + textWidth, this.y + this.height - 4);
ctx.strokeStyle = "#000";
ctx.stroke();
}
}
contains(mx, my) {
return mx >= this.x && mx <= this.x + this.width &&
my >= this.y && my <= this.y + this.height;
}
handleClick(mx, my) {
console.log("HERE", mx, my);
this.focused = this.contains(mx, my);
return this.focused;
}
handleKey(e) {
if (!this.focused) return false;
console.log(e);
if (e.key === "Backspace") {
this.value = String(this.value).slice(0, -1);
} else if (e.key === "Enter") {
this.clampToRange(); // 👈 clamp on Enter
this.focused = false;
} else if (e.key.length === 1) {
if (this.mode === "int") {
if (/[\d\-]/.test(e.key)) {
this.value += e.key;
}
} else if (this.mode === "float") {
if (/[\d.\-]/.test(e.key)) {
this.value += e.key;
}
}
}
return true;
}
update(dt) {
this.cursorTimer += dt;
if (this.cursorTimer > 500) {
this.cursorVisible = !this.cursorVisible;
this.cursorTimer = 0;
}
}
handleMouseMove(mx, my) {
// Optional: highlight or cursor change
return this.contains(mx, my);
}
clampToRange() {
let val = parseFloat(this.value);
if (isNaN(val)) val = 0;
val = Math.min(this.max, Math.max(this.min, val));
this.value = String(val);
}
get numericValue() {
let val = parseFloat(this.value);
if (isNaN(val)) return 0;
return Math.min(this.max, Math.max(this.min, val));
}
}
export class CanvasDropdown {
constructor(offsetX, offsetY, width, items = [], selected = 0) {
this.x = 0;
this.y = 0
this.offsetX = offsetX;
this.offsetY = offsetY;
this.width = width;
this.height = 20;
this.items = items;
this.selectedIndex = 0;
this.open = false;
this.hoverIndex = -1;
}
draw(ctx) {
// Collapsed box
ctx.fillStyle = "#eee";
ctx.strokeStyle = "#666";
ctx.fillRect(this.x, this.y, this.width, this.height);
ctx.strokeRect(this.x, this.y, this.width, this.height);
ctx.fillStyle = "#000";
ctx.font = "14px sans-serif";
ctx.fillText(this.items[this.selectedIndex], this.x + 5, this.y + 15);
// Expanded list
if (this.open) {
for (let i = 0; i < this.items.length; i++) {
const itemY = this.y + this.height + i * this.height;
ctx.fillStyle = i === this.hoverIndex ? "#cce" : "#fff";
ctx.fillRect(this.x, itemY, this.width, this.height);
ctx.strokeRect(this.x, itemY, this.width, this.height);
ctx.fillStyle = "#000";
ctx.fillText(this.items[i], this.x + 5, itemY + 15);
}
}
}
contains(mx, my) {
const totalHeight = this.height + (this.open ? this.items.length * this.height : 0);
return mx >= this.x && mx <= this.x + this.width &&
my >= this.y && my <= this.y + totalHeight;
}
handleClick(mx, my) {
if (this.open) {
for (let i = 0; i < this.items.length; i++) {
const itemY = this.y + this.height + i * this.height;
if (mx >= this.x && mx <= this.x + this.width &&
my >= itemY && my <= itemY + this.height) {
this.selectedIndex = i;
this.selected = this.items[i]; // 👈 This line is missing
this.open = false;
this.hoverIndex = -1;
return true;
}
}
this.open = false;
this.hoverIndex = -1;
return true;
}
if (mx >= this.x && mx <= this.x + this.width &&
my >= this.y && my <= this.y + this.height) {
this.open = true;
return true;
}
return false;
}
handleMouseMove(mx, my) {
if (!this.open) return false;
let hovered = -1;
for (let i = 0; i < this.items.length; i++) {
const itemY = this.y + this.height + i * this.height;
if (mx >= this.x && mx <= this.x + this.width &&
my >= itemY && my <= itemY + this.height) {
hovered = i;
break;
}
}
if (hovered !== this.hoverIndex) {
this.hoverIndex = hovered;
return true;
}
return false;
}
get selectedValue() {
return this.items[this.selectedIndex];
}
}

View File

@ -1,668 +0,0 @@
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;
}

View File

@ -97,7 +97,8 @@ window.onload = () => {
const nodeCanvas = document.getElementById("nodeeditor"); const nodeCanvas = document.getElementById("nodeeditor");
let nodeEditor = null; let nodeEditor = null;
nodeEditor = new NodeEditor(nodeCanvas);
nodeEditor.start();
//nodeEditor.addServoNode(400, 150, "Servo Output", 5 ); //nodeEditor.addServoNode(400, 150, "Servo Output", 5 );
// nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 }); // nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 });
@ -1077,7 +1078,7 @@ window.onload = () => {
const packets = collectChangePackets(0); // or 2 const packets = collectChangePackets(0); // or 2
for (var i = 0; i < packets.length; i++) { for (var i = 0; i < packets.length; i++) {
let channel = 0 let channel = 0
let servoMotor = getServoMotorByID(channel, parseInt(packets[i].id, 10)); let servoMotor = getServoMotorByID(channel, parseInt(packets[i].id, 10));
let dataKey = packets[i].title; let dataKey = packets[i].title;
let value = packets[i].value; let value = packets[i].value;
servoMotor[dataKey] = value; servoMotor[dataKey] = value;