new node editor (slicker controls) implemented
parent
b8742ccb66
commit
c1c7b41135
13
index.html
13
index.html
|
|
@ -23,18 +23,18 @@
|
|||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="motors-tab" data-bs-toggle="tab" data-bs-target="#motors" type="button"
|
||||
role="tab" aria-controls="motors" aria-selected="false">Motors</button>
|
||||
<button class="nav-link" id="motors-tab" data-bs-toggle="tab" data-bs-target="#motors" type="button" role="tab"
|
||||
aria-controls="motors" aria-selected="false">Motors</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="animation-tab" data-bs-toggle="tab" data-bs-target="#animation" type="button"
|
||||
role="tab" aria-controls="animation" aria-selected="true">Animation</button>
|
||||
<button class="nav-link active" id="animation-tab" data-bs-toggle="tab" data-bs-target="#animation"
|
||||
type="button" role="tab" aria-controls="animation" aria-selected="true">Animation</button>
|
||||
</li>
|
||||
<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"
|
||||
aria-controls="about" aria-selected="false">About</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- Tab content -->
|
||||
|
|
@ -208,7 +208,8 @@
|
|||
|
||||
|
||||
|
||||
<canvas id="nodeeditor"></canvas>
|
||||
<canvas id="nodeeditor" ></canvas>
|
||||
|
||||
<div id="contextMenu" style="
|
||||
position: absolute;
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -97,7 +97,8 @@ window.onload = () => {
|
|||
|
||||
const nodeCanvas = document.getElementById("nodeeditor");
|
||||
let nodeEditor = null;
|
||||
|
||||
nodeEditor = new NodeEditor(nodeCanvas);
|
||||
nodeEditor.start();
|
||||
|
||||
//nodeEditor.addServoNode(400, 150, "Servo Output", 5 );
|
||||
// nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue