779 lines
16 KiB
JavaScript
779 lines
16 KiB
JavaScript
|
|
|
|
if( typeof document != "undefined" ) {
|
|
|
|
document.nodes = { };
|
|
|
|
}
|
|
|
|
|
|
const colors = {
|
|
background: "#222",
|
|
gridLine: "#444",
|
|
connectionWire: "#00aaff",
|
|
nodeBackground: "#2d2d2d",
|
|
nodeBorder: "#777",
|
|
nodeNameBackground: "#2d2d2d",
|
|
nodeNameText: "#fff",
|
|
inputPortFill: "#aaa",
|
|
outputPortFill: "#00aaff",
|
|
};
|
|
|
|
export class Node {
|
|
name = "";
|
|
|
|
x = 0;
|
|
|
|
y = 0;
|
|
|
|
inputs = new Array();
|
|
|
|
outputs = new Array();
|
|
|
|
width = 0;
|
|
|
|
cornerRadius = 10;
|
|
|
|
constructor(name, x, y, inputs, outputs) {
|
|
this.name = name;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
}
|
|
|
|
getHeight() {
|
|
return 40 + Math.max(this.inputs.length, this.outputs.length) * 20;
|
|
}
|
|
|
|
updateWidth(ctx) {
|
|
|
|
const textWidth = ctx.measureText(this.name).width;
|
|
|
|
this.width = Math.max(200, textWidth + 40);
|
|
|
|
}
|
|
|
|
roundRect(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();
|
|
}
|
|
|
|
draw( ctx ) {
|
|
|
|
const nodeHeight = this.getHeight();
|
|
|
|
if( typeof document != "undefined" ) {
|
|
|
|
document.nodes[ this.name ] = { x:this.x, y: this.y };
|
|
|
|
}
|
|
|
|
this.roundRect(ctx, this.x, this.y, this.width, nodeHeight, this.cornerRadius);
|
|
ctx.fillStyle = colors.nodeBackground;
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = colors.nodeBorder;
|
|
ctx.lineWidth = 2;
|
|
this.roundRect(ctx, this.x, this.y, this.width, nodeHeight, this.cornerRadius);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = colors.nodeNameText;
|
|
ctx.fillText(this.name, this.x + 20, this.y + 20);
|
|
|
|
ctx.fillStyle = colors.inputPortFill;
|
|
for (let i = 0; i < this.inputs.length; i++) {
|
|
const pos = this.getInputPos(i);
|
|
ctx.beginPath();
|
|
ctx.arc(pos.x, pos.y, 6, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillText(this.inputs[i], pos.x + 12, pos.y);
|
|
}
|
|
|
|
ctx.fillStyle = colors.outputPortFill;
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
const pos = this.getOutputPos(i);
|
|
ctx.beginPath();
|
|
ctx.arc(pos.x, pos.y, 6, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
const label = this.outputs[i];
|
|
const labelWidth = ctx.measureText(label).width;
|
|
ctx.fillText(label, pos.x - 12 - labelWidth, pos.y);
|
|
}
|
|
}
|
|
|
|
getInputPos(index) {
|
|
return { x: this.x, y: this.y + 40 + index * 20 };
|
|
}
|
|
|
|
getOutputPos(index) {
|
|
return { x: this.x + this.width, y: this.y + 40 + index * 20 };
|
|
}
|
|
|
|
containsPoint(x, y) {
|
|
const nodeHeight = this.getHeight();
|
|
return x >= this.x && x <= this.x + this.width && y >= this.y && y <= this.y + nodeHeight;
|
|
}
|
|
}
|
|
|
|
export class Edge {
|
|
fromNode = null;
|
|
fromIndex = 0;
|
|
toNode = null;
|
|
toIndex = 0;
|
|
toPos = null;
|
|
|
|
constructor(fromNode, fromIndex, toNode = null, toIndex = null, toPos = null) {
|
|
this.fromNode = fromNode;
|
|
this.fromIndex = fromIndex;
|
|
this.toNode = toNode;
|
|
this.toIndex = toIndex;
|
|
this.toPos = toPos;
|
|
}
|
|
|
|
draw(ctx) {
|
|
const fromPos = this.fromNode.getOutputPos(this.fromIndex);
|
|
|
|
let toPos;
|
|
|
|
if (this.toNode !== null && this.toIndex !== null) {
|
|
toPos = this.toNode.getInputPos(this.toIndex);
|
|
} else if (this.toPos !== null) {
|
|
toPos = this.toPos;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
const cp1 = { x: fromPos.x + 50, y: fromPos.y };
|
|
const cp2 = { x: toPos.x - 50, y: toPos.y };
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(fromPos.x, fromPos.y);
|
|
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, toPos.x, toPos.y);
|
|
ctx.stroke();
|
|
}
|
|
|
|
|
|
distance(a, b) {
|
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
}
|
|
|
|
pointNear(pt, tolerance) {
|
|
const fromPos = this.fromNode.getOutputPos(this.fromIndex);
|
|
|
|
let toPos;
|
|
|
|
if (this.toNode !== null && this.toIndex !== null) {
|
|
toPos = this.toNode.getInputPos(this.toIndex);
|
|
} else if (this.toPos !== null) {
|
|
toPos = this.toPos;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
const cp1 = { x: fromPos.x + 50, y: fromPos.y };
|
|
const cp2 = { x: toPos.x - 50, y: toPos.y };
|
|
|
|
const samples = 20;
|
|
for (let i = 0; i <= samples; i++) {
|
|
const t = i / samples;
|
|
|
|
const x =
|
|
Math.pow(1 - t, 3) * fromPos.x +
|
|
3 * Math.pow(1 - t, 2) * t * cp1.x +
|
|
3 * (1 - t) * Math.pow(t, 2) * cp2.x +
|
|
Math.pow(t, 3) * toPos.x;
|
|
|
|
const y =
|
|
Math.pow(1 - t, 3) * fromPos.y +
|
|
3 * Math.pow(1 - t, 2) * t * cp1.y +
|
|
3 * (1 - t) * Math.pow(t, 2) * cp2.y +
|
|
Math.pow(t, 3) * toPos.y;
|
|
|
|
if (this.distance(pt, { x, y }) < tolerance) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
export class GraphExplorer {
|
|
|
|
isPanning = false;
|
|
|
|
dragNode = null;
|
|
|
|
isDrawScheduled = false;
|
|
|
|
dragOffsetX = 0;
|
|
|
|
dragOffsetY = 0;
|
|
|
|
nodes = new Array();
|
|
|
|
edges = new Array();
|
|
|
|
dragWire = null;
|
|
|
|
lastMousePos = null;
|
|
|
|
panX = 40;
|
|
|
|
panY = 20;
|
|
|
|
|
|
panStartMouse = null;
|
|
|
|
panStartOffsetX = 0;
|
|
|
|
panStartOffsetY = 0;
|
|
|
|
// Inertia variables
|
|
velocityX = 0;
|
|
|
|
velocityY = 0;
|
|
|
|
friction = 0.93;
|
|
|
|
velocityThreshold = 0.1;
|
|
|
|
|
|
zoom = 1.;
|
|
|
|
minZoom = 0.1;
|
|
|
|
maxZoom = 4;
|
|
|
|
canvas;
|
|
|
|
context;
|
|
|
|
requestDraw() {
|
|
|
|
if ( this.isDrawScheduled ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.isDrawScheduled = true;
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
this.isDrawScheduled = false;
|
|
|
|
this.draw();
|
|
|
|
});
|
|
}
|
|
|
|
draw( clear = true ) {
|
|
|
|
this.context.setTransform(1, 0, 0, 1, 0, 0);
|
|
|
|
if( clear ){
|
|
|
|
this.context.clearRect(0, 0, this.width, this.height);
|
|
|
|
}
|
|
|
|
|
|
this.context.save();
|
|
|
|
this.context.translate(this.panX, this.panY);
|
|
|
|
this.context.scale(this.zoom, this.zoom);
|
|
|
|
this.drawGrid(this.context);
|
|
|
|
for (const node of this.nodes) {
|
|
|
|
node.updateWidth(this.context);
|
|
|
|
node.draw(this.context);
|
|
|
|
}
|
|
|
|
for (const edge of this.edges) {
|
|
|
|
edge.draw(this.context);
|
|
|
|
}
|
|
|
|
this.context.restore();
|
|
}
|
|
|
|
screenToWorld(x, y) {
|
|
return {
|
|
x: (x - this.panX) / this.zoom,
|
|
y: (y - this.panY) / this.zoom,
|
|
};
|
|
}
|
|
|
|
worldToScreen(x, y) {
|
|
return {
|
|
x: x * this.zoom + this.panX,
|
|
y: y * this.zoom + this.panY,
|
|
};
|
|
}
|
|
|
|
getNodeAtPoint(x, y) {
|
|
|
|
const pt = this.screenToWorld(x, y);
|
|
|
|
for (let i = this.nodes.length - 1; i >= 0; i--) {
|
|
if ( this.nodes[i].containsPoint(pt.x, pt.y) ) {
|
|
return this.nodes[i];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
getWireAtPoint(x, y) {
|
|
|
|
const pt = this.screenToWorld(x, y);
|
|
|
|
for (let i = this.edges.length - 1; i >= 0; i--) {
|
|
if (this.edges[i].pointNear(pt, 10 / this.zoom)) {
|
|
return this.edges[i];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
drawGrid( context ) {
|
|
|
|
const gridSpacing = 50;
|
|
|
|
const panWorldX = -this.panX / this.zoom;
|
|
const panWorldY = -this.panY / this.zoom;
|
|
|
|
const widthWorld = this.context.canvas.width / this.zoom;
|
|
const heightWorld = this.context.canvas.height / this.zoom;
|
|
|
|
const startX = Math.floor(panWorldX / gridSpacing) * gridSpacing;
|
|
const endX = panWorldX + widthWorld;
|
|
|
|
const startY = Math.floor(panWorldY / gridSpacing) * gridSpacing;
|
|
const endY = panWorldY + heightWorld;
|
|
|
|
this.context.strokeStyle = "#282727";
|
|
this.context.lineWidth = 1 / this.zoom;
|
|
|
|
this.context.beginPath();
|
|
|
|
// Draw vertical lines within viewport
|
|
for (let x = startX; x <= endX; x += gridSpacing) {
|
|
this.context.moveTo(x, panWorldY);
|
|
this.context.lineTo(x, panWorldY + heightWorld);
|
|
}
|
|
|
|
// Draw horizontal lines within viewport
|
|
for (let y = startY; y <= endY; y += gridSpacing) {
|
|
this.context.moveTo(panWorldX, y);
|
|
this.context.lineTo(panWorldX + widthWorld, y);
|
|
}
|
|
|
|
this.context.stroke();
|
|
}
|
|
|
|
getMousePos( e ) {
|
|
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
|
|
return {
|
|
x: (e.clientX - rect.left),
|
|
y: (e.clientY - rect.top),
|
|
};
|
|
|
|
}
|
|
|
|
|
|
setup( canvas ) {
|
|
|
|
this.canvas = canvas;
|
|
|
|
this.context = canvas.getContext("2d");
|
|
|
|
this.canvas.addEventListener("mousedown", this.onMouseDown.bind( this ) );
|
|
|
|
this.canvas.addEventListener("mousemove", this.onMouseMove.bind( this ) );
|
|
|
|
this.canvas.addEventListener("mouseup", this.onMouseUp.bind( this ) );
|
|
|
|
this.canvas.addEventListener("contextmenu", this.onContextMenu.bind( this ) );
|
|
|
|
window.addEventListener("resize", this.onResize.bind( this ) );
|
|
|
|
document.addEventListener("click", this.onClick.bind( this ) );
|
|
|
|
document.getElementById("deleteNodeBtn").addEventListener("click", this.deleteNodeClick.bind(this) );
|
|
|
|
document.getElementById("deleteWireBtn").addEventListener("click", this.deleteWireButton.bind( this ) );
|
|
|
|
canvas.addEventListener("wheel", this.onWheel.bind( this ) );
|
|
|
|
}
|
|
async wait( milliseconds ) {
|
|
|
|
return new Promise(function( resolve ) {
|
|
|
|
setTimeout( resolve, milliseconds );
|
|
|
|
});
|
|
|
|
}
|
|
|
|
async onResize( windowWidth, windowHeight ) {
|
|
|
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
|
|
this.width = windowWidth;
|
|
this.height = windowHeight;
|
|
|
|
this.canvas.width = this.width * devicePixelRatio;
|
|
this.canvas.height = this.height * devicePixelRatio;
|
|
|
|
this.canvas.style.width = this.width + "px";
|
|
this.canvas.style.height = this.height + "px";
|
|
|
|
this.context.setTransform( 1, 0, 0, 1, 0, 0 );
|
|
this.context.scale( devicePixelRatio, devicePixelRatio );
|
|
|
|
//await this.wait(100)
|
|
|
|
this.draw( false );
|
|
|
|
}
|
|
|
|
onMouseDown( e ) {
|
|
|
|
const pos = this.getMousePos(e);
|
|
|
|
const worldPos = this.screenToWorld( pos.x, pos.y );
|
|
|
|
if (e.button === 0) {
|
|
|
|
const node = this.getNodeAtPoint( pos.x, pos.y );
|
|
|
|
if (node) {
|
|
// Check if clicked on output port to start wire drag
|
|
for (let i = 0; i < node.outputs.length; i++) {
|
|
|
|
const outputPos = node.getOutputPos(i);
|
|
|
|
const dist = Math.hypot(worldPos.x - outputPos.x, worldPos.y - outputPos.y);
|
|
|
|
if (dist < 10) { // 10 is port radius threshold
|
|
this.dragWire = new Edge(node, i, null, null, worldPos);
|
|
this.edges.push(this.dragWire);
|
|
this.requestDraw();
|
|
return; // Early return to prevent node dragging start
|
|
}
|
|
}
|
|
|
|
// Otherwise start dragging node
|
|
this.dragNode = node;
|
|
this.dragOffsetX = worldPos.x - node.x;
|
|
this.dragOffsetY = worldPos.y - node.y;
|
|
|
|
this.velocityX = 0;
|
|
this.velocityY = 0;
|
|
|
|
} else {
|
|
// Start panning canvas if clicked empty background
|
|
this.isPanning = true;
|
|
this.panStartMouse = { x: pos.x, y: pos.y };
|
|
this.panStartOffsetX = this.panX;
|
|
this.panStartOffsetY = this.panY;
|
|
|
|
this.velocityX = 0;
|
|
this.velocityY = 0;
|
|
}
|
|
}
|
|
|
|
this.lastMousePos = pos;
|
|
|
|
this.requestDraw();
|
|
}
|
|
|
|
onMouseMove( e ) {
|
|
|
|
console.log("test");
|
|
|
|
const pos = this.getMousePos(e);
|
|
const worldPos = this.screenToWorld(pos.x, pos.y);
|
|
|
|
if (this.dragWire) {
|
|
// Update wire temporary end point in world coordinates
|
|
|
|
this.dragWire.toPos = worldPos;
|
|
|
|
this.requestDraw();
|
|
|
|
} else if (this.dragNode) {
|
|
|
|
const prevX = this.dragNode.x;
|
|
const prevY = this.dragNode.y;
|
|
|
|
this.dragNode.x = worldPos.x - this.dragOffsetX;
|
|
this.dragNode.y = worldPos.y - this.dragOffsetY;
|
|
|
|
this.velocityX = (this.dragNode.x - prevX) / (e.movementX || 1);
|
|
this.velocityY = (this.dragNode.y - prevY) / (e.movementY || 1);
|
|
|
|
this.requestDraw();
|
|
|
|
} else if (this.isPanning) {
|
|
const dx = pos.x - this.panStartMouse.x;
|
|
const dy = pos.y - this.panStartMouse.y;
|
|
|
|
const prevPanX = this.panX;
|
|
const prevPanY = this.panY;
|
|
|
|
this.panX = this.panStartOffsetX + dx;
|
|
this.panY = this.panStartOffsetY + dy;
|
|
|
|
this.velocityX = this.panX - prevPanX;
|
|
this.velocityY = this.panY - prevPanY;
|
|
|
|
this.requestDraw();
|
|
}
|
|
|
|
this.lastMousePos = pos;
|
|
|
|
}
|
|
|
|
onMouseUp( e ) {
|
|
|
|
if ( e.button === 0) {
|
|
|
|
if ( this.dragWire ) {
|
|
|
|
const pos = this.getMousePos( e );
|
|
|
|
const worldPos = this.screenToWorld( pos.x, pos.y );
|
|
|
|
// Try to attach to input port of a node
|
|
let connected = false;
|
|
|
|
for (const node of this.nodes) {
|
|
|
|
for (let i = 0; i < node.inputs.length; i++) {
|
|
|
|
const inputPos = node.getInputPos( i );
|
|
|
|
const dist = Math.hypot( worldPos.x - inputPos.x, worldPos.y - inputPos.y );
|
|
|
|
if (dist < 10) {
|
|
// Attach wire end to this input port
|
|
this.dragWire.toNode = node;
|
|
this.dragWire.toIndex = i;
|
|
this.dragWire.toPos = null;
|
|
connected = true;
|
|
break;
|
|
}
|
|
}
|
|
if (connected) break;
|
|
}
|
|
|
|
if (!connected) {
|
|
// Remove the wire if not connected
|
|
const idx = this.edges.indexOf(this.dragWire);
|
|
if (idx >= 0) this.edges.splice(idx, 1);
|
|
}
|
|
|
|
this.dragWire = null;
|
|
|
|
this.requestDraw();
|
|
|
|
} else if (this.dragNode) {
|
|
this.dragNode = null;
|
|
} else if (this.isPanning) {
|
|
this.isPanning = false;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
onContextMenu(e) {
|
|
|
|
const wireMenu = document.getElementById("wireMenu");
|
|
const nodeMenu = document.getElementById("nodeMenu");
|
|
|
|
e.preventDefault();
|
|
|
|
const pos = this.getMousePos( e );
|
|
const worldPos = this.screenToWorld( pos.x, pos.y );
|
|
|
|
// Hide both menus by default
|
|
wireMenu.style.display = "none";
|
|
nodeMenu.style.display = "none";
|
|
|
|
const node = this.getNodeAtPoint( pos.x, pos.y );
|
|
|
|
if (node) {
|
|
// Show node context menu
|
|
nodeMenu.style.left = e.clientX + "px";
|
|
nodeMenu.style.top = e.clientY + "px";
|
|
nodeMenu.style.display = "block";
|
|
|
|
// Store clicked node reference for later use (e.g., deletion)
|
|
nodeMenu.currentNode = node;
|
|
|
|
} else {
|
|
const wire = this.getWireAtPoint( pos.x, pos.y );
|
|
|
|
if (wire) {
|
|
// Show wire context menu
|
|
wireMenu.style.left = e.clientX + "px";
|
|
wireMenu.style.top = e.clientY + "px";
|
|
wireMenu.style.display = "block";
|
|
|
|
// Store clicked wire reference for later use
|
|
wireMenu.currentWire = wire;
|
|
}
|
|
}
|
|
}
|
|
|
|
onClick( e ) {
|
|
|
|
const wireMenu = document.getElementById("wireMenu");
|
|
const nodeMenu = document.getElementById("nodeMenu");
|
|
|
|
// Hide context menus on any click outside them
|
|
if (e.target !== nodeMenu && !nodeMenu.contains(e.target)) {
|
|
nodeMenu.style.display = "none";
|
|
nodeMenu.currentNode = null;
|
|
}
|
|
if (e.target !== wireMenu && !wireMenu.contains(e.target)) {
|
|
wireMenu.style.display = "none";
|
|
wireMenu.currentWire = null;
|
|
}
|
|
}
|
|
|
|
deleteNodeClick() {
|
|
|
|
const wireMenu = document.getElementById("wireMenu");
|
|
const nodeMenu = document.getElementById("nodeMenu");
|
|
|
|
|
|
if (nodeMenu.currentNode) {
|
|
const index = this.nodes.indexOf(nodeMenu.currentNode);
|
|
if (index !== -1) {
|
|
// Remove edges connected to this node
|
|
for (let i = this.edges.length - 1; i >= 0; i--) {
|
|
if (this.edges[i].fromNode === nodeMenu.currentNode || this.edges[i].toNode === nodeMenu.currentNode) {
|
|
this.edges.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
this.nodes.splice(index, 1);
|
|
}
|
|
|
|
nodeMenu.style.display = "none";
|
|
|
|
nodeMenu.currentNode = null;
|
|
|
|
this.requestDraw();
|
|
}
|
|
}
|
|
|
|
deleteWireButton() {
|
|
|
|
const wireMenu = document.getElementById("wireMenu");
|
|
|
|
if (wireMenu.currentWire) {
|
|
const index = this.edges.indexOf(wireMenu.currentWire);
|
|
if (index !== -1) {
|
|
this.edges.splice(index, 1);
|
|
}
|
|
|
|
wireMenu.style.display = "none";
|
|
wireMenu.currentWire = null;
|
|
|
|
this.requestDraw();
|
|
}
|
|
|
|
}
|
|
|
|
onWheel( event ) {
|
|
|
|
event.preventDefault();
|
|
|
|
const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9;
|
|
|
|
this.zoomAtCenter( zoomFactor, this.canvas.width, this.canvas.height);
|
|
|
|
this.requestDraw();
|
|
|
|
}
|
|
|
|
zoomAtCenter( zoomDelta, canvasWidth, canvasHeight ) {
|
|
|
|
const oldZoom = this.zoom;
|
|
|
|
const centerX = canvasWidth / 2;
|
|
|
|
const centerY = canvasHeight / 2;
|
|
|
|
// Clamp new this.zoom
|
|
this.zoom = Math.min(this.maxZoom, Math.max(this.minZoom, this.zoom * zoomDelta));
|
|
|
|
// Adjust pan so this.zoom happens at canvas center (keep world pos under center fixed)
|
|
this.panX = centerX - (centerX - this.panX) * (this.zoom / oldZoom);
|
|
this.panY = centerY - (centerY - this.panY) * (this.zoom / oldZoom);
|
|
|
|
}
|
|
|
|
inertiaLoop() {
|
|
if (!this.isPanning && !this.dragNode) {
|
|
// Apply this.friction
|
|
this.velocityX *= this.friction;
|
|
this.velocityY *= this.friction;
|
|
|
|
this.panX += this.velocityX;
|
|
this.panY += this.velocityY;
|
|
|
|
if (Math.abs(this.velocityX) > this.velocityThreshold || Math.abs(this.velocityY) > this.velocityThreshold) {
|
|
this.draw();
|
|
requestAnimationFrame(this.inertiaLoop.bind(this));
|
|
}
|
|
}
|
|
else {
|
|
requestAnimationFrame(this.inertiaLoop.bind(this));
|
|
}
|
|
|
|
}
|
|
|
|
addNode( node ) {
|
|
|
|
this.nodes.push( node );
|
|
|
|
}
|
|
|
|
addEdge( edge ) {
|
|
|
|
this.edges.push( edge );
|
|
|
|
}
|
|
|
|
}
|