all repos — gemini-redirect @ b6b1d243404ad1c9f774fde8596c11a249b748cb

content/blog/graphs/graphs.js (view raw)

  1// -------------------------------------------------------------------------- //
  2// Constants
  3var INTERPOLATION = 0.6;
  4
  5var BOX_HEIGHT = 0.15; // %, from and to which drop the items
  6var BOX_HOVER = [255, 0, 0];
  7var BOX_NORMAL = [255, 200, 200];
  8
  9var NODE_NORMAL = '#000';
 10var NODE_TEXT = '#fff';
 11var NODE_FONT = 'px Georgia';
 12
 13var FRAME_DURATION = 25; // ms
 14
 15// -------------------------------------------------------------------------- //
 16// Retrieve used HTML elements
 17canvas = document.getElementById('canvas');
 18matrixTable = document.getElementById('matrixTable');
 19matrixOrder = document.getElementById('matrixOrder');
 20matrixAccum = document.getElementById('matrixAccum');
 21matrixTableWrapper = document.getElementById('matrixTableWrapper');
 22
 23// -------------------------------------------------------------------------- //
 24// Variables
 25var orderShown, accumShown;
 26c = canvas.getContext('2d');
 27
 28// Box color, from which and to which nodes should be dragged
 29var boxColor = BOX_NORMAL.slice();
 30
 31// Available nodes and their connection matrix- these should be kept in sync
 32var nodes = [];
 33var connMatrix = Array.matrix(0, 0, 0);
 34
 35// Which node was the first we right clicked to create a new connection?
 36var startConn = null;
 37
 38// Are we dragging any node yet?
 39var anyDragging = false;
 40
 41// Highlight mode [row, col, color] for the matrix representing the graph
 42var highlight = [-1, -1, '#fff'];
 43
 44function updateOrder() {
 45  orderShown = Number(matrixOrder.value);
 46  accumShown = matrixAccum.checked;
 47  calcShowConnMatrix();
 48}
 49updateOrder();
 50
 51// -------------------------------------------------------------------------- //
 52// Declare a prototype (Node) to render the selectable elements
 53function Node(x, y, r, id) {
 54  this.x = x;
 55  this.y = y;
 56  this.r = r;
 57  this.id = id;
 58  this.color = null;
 59  this.dragging = false;
 60
 61  this.draw = function() {
 62    if (this.dragging) {
 63      c.fillStyle = NODE_NORMAL;
 64      this.x = lerp(this.x, canvas.mouseX, INTERPOLATION);
 65      this.y = lerp(this.y, canvas.mouseY, INTERPOLATION);
 66    } else {
 67      c.fillStyle = this.color;
 68    }
 69    c.circle(this.x, this.y, this.r);
 70
 71    c.fillStyle = NODE_TEXT;
 72    c.font = ''+r+NODE_FONT;
 73    // Arbitrary but nice
 74    var x = this.x - r * 0.3;
 75    var y = this.y + r * 0.3;
 76
 77    c.fillText(""+this.id, x, y);
 78  }
 79
 80  this.resetColor = function() {
 81    this.color = '#777';
 82  }
 83  this.resetColor();
 84
 85  this.inBounds = function(x, y) {
 86    return Math.abs(this.x - x) < this.r &&
 87           Math.abs(this.y - y) < this.r;
 88  }
 89}
 90
 91// -------------------------------------------------------------------------- //
 92// Node handling
 93function addNode(x, y) {
 94  nodes.push(new Node(x, y, 20, nodes.length + 1));
 95  limitOrder(nodes.length);
 96
 97  var newMatrix = Array.matrix(nodes.length, nodes.length, 0);
 98  matrixCpy(connMatrix, newMatrix);
 99  setConnMatrix(newMatrix);
100}
101
102function deleteNode(at) {
103  nodes.splice(at, 1);
104  limitOrder(nodes.length);
105
106  // Re-set the nodes IDs
107  for (var i = 0; i != nodes.length; ++i) {
108    nodes[i].id = i+1;
109  }
110
111  // Pop the at'th row (connections from the node we deleted)
112  // and also the at'th column (connections to the node we deleted)
113  setConnMatrix(matrixPop(connMatrix, at, at));
114}
115
116function clearNodes() {
117  nodes.length = 0;
118  limitOrder(1);
119  resetConnections();
120}
121
122// -------------------------------------------------------------------------- //
123// Connection matrix handling
124function setConnMatrix(matrix) {
125  connMatrix = matrix;
126  makeNodesHighlightable(matrixTable, connMatrix);
127  calcShowConnMatrix();
128}
129
130function resetConnections() {
131  setConnMatrix(Array.matrix(nodes.length, nodes.length, 0));
132}
133
134// -------------------------------------------------------------------------- //
135// Matrix representation
136function calcShowConnMatrix() {
137  if (orderShown > 1) {
138    var multiplied = Array.matrix(nodes.length, nodes.length, 0);
139    matrixCpy(connMatrix, multiplied);
140
141    if (accumShown) {
142      var shown = Array.matrix(nodes.length, nodes.length, 0);
143      matrixCpy(connMatrix, shown);
144    }
145
146    for (var i = 1; i != orderShown; ++i) {
147      multiplied = matrixMul(connMatrix, multiplied);
148      if (accumShown) {
149        matrixAdd(multiplied, shown);
150      }
151    }
152    if (accumShown) {
153      matrixRepr(shown, matrixTable, highlight);
154    } else {
155      matrixRepr(multiplied, matrixTable, highlight);
156    }
157  } else {
158    matrixRepr(connMatrix, matrixTable, highlight);
159  }
160}
161
162// Makes a table "highlightable" (when the mouse hovers,
163// it will be redrawn) by using the specified matrix.
164function makeNodesHighlightable(table, matrix) {
165  table.matrix = matrix;
166  table.addEventListener('mousemove', function(e) {
167    if (!table.matrix && !table.matrix[0])
168      return;
169
170    // TODO I know, this shouldn't have to go here and I should make a
171    //      better script to show the whole path and stuff... oh well.
172    if (orderShown > 1) {
173      if (highlight[0] != -1 || highlight[1] != -1) {
174        highlight[0] = highlight[1] = -1;
175        for (var i = 0; i != nodes.length; ++i)
176          nodes[i].resetColor();
177        calcShowConnMatrix();
178      }
179      return;
180    }
181
182    var rect = table.getBoundingClientRect();
183    var x = e.clientX - rect.left;
184    var y = e.clientY - rect.top;
185
186    var colWidth = rect.width / (table.matrix[0].length+1);
187    var col = Math.floor(x / colWidth)-1;
188    
189    var rowHeight = rect.height / (table.matrix.length+1);
190    var row = Math.floor(y / rowHeight)-1;
191
192    if (row != highlight[0] || col != highlight[1]) {
193      highlight[0] = row;
194      highlight[1] = col;
195      if (row >= 0 && col >= 0) {
196        if (table.matrix[row][col]) {
197          // there is a connection so color them
198          nodes[row].color = '#77f';
199          nodes[col].color = '#7f7';
200          highlight[2] = '#afa';
201        } else {
202          // no connection so reset its colors
203          for (var i = 0; i != nodes.length; ++i)
204            nodes[i].resetColor();
205
206          highlight[2] = '#faa';
207        }
208      }
209      calcShowConnMatrix();
210    }
211
212  }, false);
213}
214
215// -------------------------------------------------------------------------- //
216// Canvas rendering
217function renderElements() {
218  c.clear();
219  if (canvas.mouseDown && canvas.mouseY < canvas.height * BOX_HEIGHT) {
220    boxColor = lerpArray(boxColor, BOX_HOVER, 0.2);
221  } else {
222    boxColor = lerpArray(boxColor, BOX_NORMAL, 0.2);
223  }
224  c.rect(0, 0, canvas.width, canvas.height * BOX_HEIGHT);
225  c.fillStyle = 'rgb(' +
226                  Math.round(boxColor[0]) + ',' +
227                  Math.round(boxColor[1]) + ',' +
228                  Math.round(boxColor[2]) + ')';
229  c.fill();
230  
231  
232  if (nodes.length == 0)
233    return;
234
235  for (var i = 0; i != nodes.length; ++i)
236    nodes[i].draw();
237
238  c.strokeStyle = '#f00';
239
240  var ii = connMatrix.length;
241  var jj = connMatrix.length;
242
243  var x0, y0, x1, y1, cos, sin;
244  var rsq = Math.pow(nodes[0].r + 40, 2); // Radius SQuared + some margin
245  for (var i = 0; i != ii; ++i) {
246    for (var j = 0; j != jj; ++j) {
247      if (connMatrix[i][j]) {
248        // Non-zero item, this implies a connection between nodes.
249        // An element on the matrix connects row (i) -> column (j)
250        x0 = nodes[i].x;
251        y0 = nodes[i].y;
252        x1 = nodes[j].x;
253        y1 = nodes[j].y;
254
255        // Interpolate a bit to exit the radius unless we're too close
256        if (Math.pow(y1 - y0, 2) + Math.pow(x1 - x0, 2) > rsq) {
257          var angle = Math.atan2(y1 - y0, x1 - x0);
258          cos = Math.cos(angle) * nodes[0].r;
259          sin = Math.sin(angle) * nodes[0].r;
260          x0 += cos;
261          y0 += sin;
262          x1 -= cos;
263          y1 -= sin;
264        }
265        c.arrow(x0, y0, x1, y1);
266      }
267    }
268  }
269
270  if (startConn != null) {
271    c.arrow(nodes[startConn].x, nodes[startConn].y,
272            canvas.mouseX, canvas.mouseY);
273  }
274}
275
276function renderLoop() {
277  renderElements();
278  setTimeout(renderLoop, FRAME_DURATION);
279}
280
281// -------------------------------------------------------------------------- //
282// HTML interaction
283
284// To limit the maximum orden which can be specified on the HTML and
285// also clamp the currently shown order to the limit if it's exceeded
286function limitOrder(value) {
287  matrixOrder.max = value;
288  if (orderShown > value) {
289    orderShown = value;
290    matrixOrder.value = '' + value;
291  }
292}
293
294canvas.addEventListener('mousedown', function(e) {
295  if (e.button == 0) {
296      if (canvas.mouseY < canvas.height * BOX_HEIGHT) {
297        // Add node
298        addNode(canvas.mouseX, 0);
299        nodes[nodes.length - 1].dragging = true;
300        anyDragging = true;
301      } else {
302        // Drag nodes
303        for (var i = nodes.length; i--;) {
304          if (nodes[i].inBounds(canvas.mouseX, canvas.mouseY)) {
305            nodes[i].dragging = true;
306            anyDragging = true;
307            break;
308          }
309        }
310    }
311    canvas.style.cursor = anyDragging ? "crosshair" : "default";
312
313  } else if (e.button == 2) {
314    // Right button, select two nodes to join
315    if (startConn == null) {
316      for (var i = nodes.length; i--;) {
317        if (nodes[i].inBounds(canvas.mouseX, canvas.mouseY)) {
318          startConn = i;
319          break;
320        }
321      }
322    }
323  } else if (e.button == 1) {
324    // Middle click, clear the connections from and to a given node
325    for (var i = nodes.length; i--;) {
326      if (nodes[i].inBounds(canvas.mouseX, canvas.mouseY)) {
327        for (var j = 0; j != nodes.length; ++j)
328          connMatrix[i][j] = 0;
329        break;
330      }
331    }
332  }
333
334  canvas.mouseDown = e.button == 0 ? 'L' : 'R';
335}, false);
336
337canvas.addEventListener('mouseup', function(e) {
338  canvas.mouseDown = '';
339
340  if (anyDragging) {
341    anyDragging = false;
342    for (var i = nodes.length; i--; ) {
343      if (nodes[i].dragging) {
344        if (canvas.mouseY < canvas.height * BOX_HEIGHT) {
345          deleteNode(i);
346        } else {
347          nodes[i].dragging = false;
348        }
349      }
350    }
351  } else if (startConn != null) {
352    for (var i = nodes.length; i--;) {
353      if (nodes[i].inBounds(canvas.mouseX, canvas.mouseY)) {
354        // Node row connected to node column
355        if (i != startConn)
356          connMatrix[startConn][i] = 1;
357        break;
358      }
359    }
360    startConn = null;
361  }
362
363  calcShowConnMatrix();
364}, false);
365
366canvas.addEventListener('mousemove', function(e) {
367  var rect = canvas.getBoundingClientRect();
368  canvas.mouseX = e.clientX - rect.left;
369  canvas.mouseY = e.clientY - rect.top;
370
371}, false);
372
373// -------------------------------------------------------------------------- //
374// Represent the example we walked through
375function lilNoise() {
376  return (Math.random() - 0.5) * canvas.width * 0.05;
377}
378
379function getNoiseX(relative) {
380  return canvas.width * relative + lilNoise();
381}
382
383function getNoiseY(relative) {
384  return canvas.height * relative + lilNoise();
385}
386
387addNode(getNoiseX(0.2), getNoiseY(0.5));
388addNode(getNoiseX(0.5), getNoiseY(0.3));
389addNode(getNoiseX(0.8), getNoiseY(0.5));
390addNode(getNoiseX(0.7), getNoiseY(0.7));
391addNode(getNoiseX(0.3), getNoiseY(0.7));
392
393setConnMatrix([
394  [0, 1, 0, 0, 0],
395  [1, 0, 0, 0, 1],
396  [0, 0, 0, 1, 0],
397  [0, 1, 1, 0, 0],
398  [1, 0, 0, 1, 0]
399]);
400
401calcShowConnMatrix();
402
403
404// Let's go!
405renderLoop();