Canvas has some advantages and disadvantages... but right now, not having to keep track of DOM mappings is a huge advantage!
authorBenjamin Smedberg <benjamin@smedbergs.us>
Mon, 14 Apr 2008 10:16:22 -0400
changeset 12 de26e8c9b7350e2c50f7002203245a8958c99d3b
parent 11 d0a8c7314fff51cc69e69f61cbd26c33754a4b7c
child 13 a74f146eb2de46aede0e6d387b197e5eefd9cca7
push id1
push userbsmedberg@mozilla.com
push dateMon, 14 Apr 2008 15:21:26 +0000
Canvas has some advantages and disadvantages... but right now, not having to keep track of DOM mappings is a huge advantage!
www/index.xhtml
www/navigate.js
--- a/www/index.xhtml
+++ b/www/index.xhtml
@@ -7,92 +7,44 @@
     body {
       margin: 0px;
       padding: 0px;
       font-family: sans-serif;
       font-size: 10px;
       overflow: hidden;
     }
     #topnav {
-      position: absolute;
-      width: 100%;
       text-align: right;
       background-color: black;
       color: white;
-      padding: 3px;
+    }
+    #topnav-inside {
+      padding: 2px;
     }
     #select-repo {
       float: left;
     }
     input, select {
       border: 1px solid #888;
     }
-    #arrows, #inside-scrolling {
-      position: absolute;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-    }
-    .revision {
-      position: absolute;
-      border: 1px solid black;
-      padding: 1px;
-      width: 250px;
-      text-align: center;
-      max-height: 250px;
-      visibility: hidden;
-      cursor: pointer;
-    }
-    .arrow {
-      stroke: black;
-      stroke-width: 1.5px;
-      marker-end: url(#arrowhead);
-    }
-    .arrow.ambiguous {
-      stroke-dasharray: 4,4;
+    #drawcanvas {
+      overflow: hidden;
     }
   </style>
-  <script type="text/javascript" src="jquery-1.2.3.js" />
+  <script type="text/javascript;" src="jquery-1.2.3.js" />
   <script type="text/javascript" src="jquery.history.js" />
-  <script type="text/javascript" src="navigate.js" />
+  <script type="application/javascript;version=1.7" src="navigate.js" />
 </head>
 <body onload="init()">
-  <svg id="arrows" xmlns="http://www.w3.org/2000/svg">
-    <defs>
-      <marker id="arrowhead"
-	      viewBox="0 0 10 10" refX="10" refY="5"
-	      markerUnits="strokeWidth"
-	      markerWidth="6"
-	      markerHeight="4"
-	      orient="auto">
-	<path d="M 0 0 L 10 5 L 0 10 z" />
-      </marker>
-      <line id="arrow-template" class="arrow"
-	    x1="500" y1="350" x2="200" y2="300"
-            visibility="hidden" />
-    </defs>
-    <g id="scroller" transform="translate(0,0)">
-    </g>
-  </svg>
-  <div id="inside-scrolling" />
-
-  <div id="revision-template"
-       class="revision">
-    <div class="node">{Node}</div>
-    <div class="user">{User}</div>
-    <div class="date">{Date}</div>
-    <div class="desc">{Description}</div>
-  </div>
-
-  <div id="topnav">
+  <div id="topnav"><div id="topnav-inside">
     <select id="select-repo">
       <option value="mozilla-central">mozilla-central</option>
-      <option value="mercurial-crew">mercurial-crew</option>
+      <option value="actionmonkey">actionmonkey</option>
     </select>
 
     <input
       type="text" id="node-input" value="tip"
       title="Enter a revision, node ID or tag to navigate to that changeset." />
     <input type="button" value="Go" id="node-choose" />
-  </div>
+  </div></div>
+  <canvas id="drawcanvas"></canvas>
 </body>
 </html>
--- a/www/navigate.js
+++ b/www/navigate.js
@@ -1,368 +1,424 @@
-const BASEURL = '/hgwebdir.cgi/';
+/* -*- Mode: Java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
+
+const BASEURL = 'http://hg.mozilla.org/';
 const SVGNS = 'http://www.w3.org/2000/svg';
 
 const REVWIDTH = 254;
 const HSPACING = 40;
 const VSPACING = 30;
+const TEXTSTYLE = '12px sans-serif';
+
+let gWidth, gHeight, gContext, gPendingRequest;
+
+function getCX()
+{
+  let cx = $('#drawcanvas')[0].getContext('2d');
+  cx.mozTextStyle = TEXTSTYLE;
+  return cx;
+}
+
+function measure(t, cx)
+{
+  if (!cx)
+    cx = getCX();
+
+  return cx.mozMeasureText(t);
+}
+
+/**
+ * Take a string of text. Split it into up to as many as three lines. Yield
+ * each line.
+ */
+function splitText(t, cx)
+{
+  if (!cx)
+    cx = getCX();
+
+  var syllables = t.split(' ');
+  var unplaced = 0;
+
+  for (var i = 0; i < 3 && unplaced < syllables.length; ++i) {
+    for (var placed = unplaced + 2; placed < syllables.length; ++placed) {
+      if (measure(syllables.slice(unplaced, placed).join(' '), cx) >
+          REVWIDTH) {
+        --placed;
+        break;
+      }
+    }
+    var str = syllables.slice(unplaced, placed).join(" ");
+    if (i == 2 && placed < syllables.length)
+      str += "…";
+
+    yield str;
+    unplaced = placed;
+  }
+}
 
 /**
  * map from long node strings to Revision objects
  * The following mappings are added to the JSON:
  *   .element from the node to the element
  *   .parentArrows is a map of arrows pointing to this element, keyed on
  *                 parent node
  *   .childArrows  is a map of arrows pointing from this element, keyed on
  *                 child node
  */
 
 var revs = {};
 
-function Revision(data)
+function getRevision(node)
 {
-    var i, d, a;
-
-    this.node = data.node;
-    this.rev = data.rev;
-    this.user = data.user;
-    this.date = data.date
-    this.description = data.description;
-    this.children = data.children;
-    this.parents = data.parents;
-
-    d = $('#revision-template').clone();
-    d.attr('id', 'rev' + shortrev(this.node));
-    $('.node', d).text(this.rev + ": " + shortrev(this.node));
-    $('.user', d).text(this.user);
-    $('.date', d).text(this.date);
-    $('.desc', d).text(this.description);
-
-    this.element = d[0];
-
-    d.appendTo('#inside-scrolling');
-    d.click(navTo);
-    this.height = measure($(d), 'height');
-
-    this.childArrows = {};
-    this.parentArrows = {};
+  if (!(node in revs)) {
+    revs[node] = new Revision(node);
+  }
+  return revs[node];
+}
 
-    for (i in this.children) {
-        if (revs[this.children[i]]) {
-            a = revs[this.children[i]].parentArrows[this.node];
-            this.childArrows[this.children[i]] = a;
-        }
-        else {
-            // We haven't met this child yet... make an arrow for it
-            a = $('#arrow-template').clone();
-            a.attr('id', 'ar' + shortrev(this.node) + ':' + shortrev(this.children[i]));
-            a.appendTo('#scroller');
-            this.childArrows[this.children[i]] = a[0];
-        }
-    }
-
-    for (i in this.parents) {
-        if (revs[this.parents[i]]) {
-            var a = revs[this.parents[i]].childArrows[this.node];
-            this.parentArrows[this.parents[i]] = a;
-        }
-        else {
-            // We haven't met this parent yet... make an arrow for it
-            a = $('#arrow-template').clone();
-            a.attr('id', 'ar' + shortrev(this.parents[i] + ':' + shortrev(this.node)));
-            a.appendTo('#scroller');
-            this.parentArrows[this.parents[i]] = a[0];
-        }
-    }
-
-    revs[this.node] = this;
+function Revision(node)
+{
+  this.node = node;
+  this.revnode = "??: " + this.shortnode();
+  this.revnodelen = measure(this.revnode);
 }
 
 Revision.prototype = {
-  visible: function r_visible() {
-    return $(this.element).css('visibility') != 'hidden';
+  parents: [],
+  children: [],
+  _position: null,
+
+  loaded: function r_loaded()
+  {
+    return 'rev' in this;
   },
 
+  update: function r_update(data)
+  {
+    if (data.node != this.node)
+      throw Error("node doesn't match in Revision.update\nthis.node: " +
+                  this.node + "\ndata.node: " + data.node);
+
+    let cx = getCX();
+
+    this.rev = data.rev;
+
+    this.revnode = this.rev + ": " + this.shortnode();
+    this.revnodelen = measure(this.revnode, cx);
+
+    this.userlen = measure(data.user, cx);
+    this.user = data.user;
+
+    this.date = data.date;
+    this.datelen = measure(data.date, cx);
+
+    this.description = data.description;
+    this.descSplit = [line for (line in splitText(data.description))];
+
+    this.children = [getRevision(node) for each (node in data.children)];
+    this.parents = [getRevision(node) for each (node in data.parents)];
+  },
+
+  /* x and y are the center of the revision */
   x: function r_x() {
-    if (!this.visible())
-      throw Error("Revision " + this.node + " is not visible.");
+    if (!this._position)
+      throw Error("Revision " + this.node + " is not positioned.");
 
-    return measure($(this.element), 'left');
+    return this._position.x;
   },
 
   y: function r_y() {
-    if (!this.visible())
-      throw Error("Revision " + this.node + " is not visible.");
-
-    return measure($(this.element), 'top');
-  },
+    if (!this._position)
+      throw Error("Revision " + this.node + " is not positioned.");
 
-  center: function r_center() {
-    if (!this.visible())
-      throw Error("Revision " + this.node + " is not visible.");
-    
-    return {x: this.x() + REVWIDTH / 2,
-            y: this.y() + this.height / 2};
+    return this._position.y;
   },
 
-  parentPoint: function r_parentPoint() {
-    if (!this.visible())
-      throw Error("Revision " + this.node + " is not visible.");
-
-    return {x: this.x(),
-            y: this.y() + this.height / 2 };
+  /* height */
+  height: function r_height() {
+    if (this.loaded()) {
+      return 12 * (3 + this.descSplit.length) + 4;
+    }
+    
+    return 12 + 4;
   },
-  
-  childPoint: function r_childPoint() {
-    if (!this.visible())
-      throw Error("Revision " + this.node + " is not visible.");
 
-    var e = $(this.element);
-    return {x: this.x() + REVWIDTH,
-            y: this.y() + this.height / 2 };
-  },
-  
   /**
    * Move the center of the box to this point
    */
-  moveTo: function r_move(point) {
-    var e, child, parent, p, a;
-    
-    e = $(this.element);
-    e.css('visibility', 'visible');
-    e.css('left', point.x - REVWIDTH / 2);
-    e.css('top', point.y - this.height / 2);
-    
-    p = this.childPoint();
-    for each (child in this.children) {
-      a = $(this.childArrows[child]);
-      a.attr('x1', p.x);
-      a.attr('y1', p.y);
-      if (!(child in revs)) {
-        a.attr('x2', p.x + 25);
-        a.attr('y2', p.y);
-      }
-      a.css('visibility', 'visible');
-    }
-    
-    p = this.parentPoint();
-    for each (parent in this.parents) {
-      a = $(this.parentArrows[parent]);
-      a.attr('x2', p.x);
-      a.attr('y2', p.y);
-      if (!(parent in revs)) {
-        a.attr('x1', p.x - 25);
-        a.attr('y1', p.y);
-      }
-      a.css('visibility', 'visible');
-    }
+  moveTo: function r_move(x, y) {
+    if (isNaN(x))
+      throw Error("x is NaN");
+
+    if (isNaN(y))
+      throw Error("y is NaN");
+
+    this._position = {'x': x, 'y': y};
   },
-  
-  /**
-   * Hide gc'ed revisions, set arrow visibility, and move the other end of unbounded arrows.
-   * Each node is responsible for all its parent arrows, as well as child arrows pointing to unknown revisions.
-   */
-  cleanLayout: function r_moveArrows()
+
+  shortnode: function r_shortnode()
   {
-    var child, parent, p, a, i;
+    return this.node.slice(0, 12);
+  },
+
+  hittest: function r_hittest(x, y)
+  {
+    if (!this.gc)
+      return false;
 
-    if (!this.gc) {
-      $(this.element).css('visibility', 'hidden');
+    let tx = this.x();
+    let ty = this.y();
+    let th = this.height();
+
+    if (tx - REVWIDTH / 2 <= x &&
+        tx + REVWIDTH / 2 >= x &&
+        ty - th / 2 <= y &&
+        ty + th / 2 >= y)
+      return true;
 
-      for each (child in this.children) {
-        if (!(child in revs)) {
-          $(this.childArrows[child]).css('visibility', 'hidden');
-        }
-      }
-      
-      for each (parent in this.parents) {
-        if (!(parent in revs) || !(revs[parent].gc)) {
-          $(this.parentArrows[parent]).css('visibility', 'hidden');
-        }
-      }
-    }
-    else {
-      // We've already been positioned and are visible; all we need is to position the "other" end
-      // of arrows that point to offscreen revisions
-      p = this.childPoint();
-      for (i in this.children) {
-        child = this.children[i];
-        
-        if (!(child in revs) || !revs[child].gc) {
-          this.childArrows[child].setAttribute('class', 'arrow ambiguous');
-          a = $(this.childArrows[child]);
-          a.attr('x2', p.x + 25);
-          a.attr('y2', this.y() + (Number(i) + 0.5) * (this.height / this.children.length));
-        }
-      }
+    return false;
+  },
 
-      p = this.parentPoint();
-      for (i in this.parents) {
-        parent = this.parents[i];
-        
-        if (!(parent in revs) || !revs[parent].gc) {
-          this.parentArrows[parent].setAttribute('class', 'arrow ambiguous');
-          a = $(this.parentArrows[parent]);
-          a.attr('x1', p.x - 25);
-          a.attr('y1', this.y() + (Number(i) + 0.5) * (this.height / this.parents.length));
-        }
-        else {
-          this.parentArrows[parent].setAttribute('class', 'arrow');
-        }
-      }
-    }
+  toString: function r_toString()
+  {
+    return "Revision:" +
+    "\nnode: " + this.node +
+    "\nposition: " + uneval(this._position) +
+    "\ndescSplit: " + uneval(this.descSplit);
   }
 };
 
-function shortrev(node)
-{
-    return node.slice(0, 12);
-}
-
-/**
- * Limit a string to len characters... if it is too long, add an ellipsis.
- */
 function limit(str, len)
 {
-    if (str.length < len) {
-        return str;
-    }
+  if (str.length < len)
+    return str;
 
-    return str.slice(0, len) + "…";
+  return str.slice(0, len) + "…";
 }
 
-function measure(r, prop)
-{
-    return Number(r.css(prop).replace('px', ''));
-}
-
-function doLayout(node)
+function doLayout()
 {
-  var contextrev, rev, i, loadMore;
-  
-  loadMore = [];
+  let loadMore = [];
+  let bottompositions = [];
   
-  function drawChildren(rev)
+  function drawChildren(rev, position)
   {
-    var p, child, totalHeight, avgHeight, c, childrev;
-  
     if (rev.children.length == 0)
       return;
     
-    totalHeight = 0;
-    c = 0;
+    let totalHeight = (rev.children.length - 1) * VSPACING;
     
     for each (child in rev.children) {
-      if (child in revs) {
-        totalHeight += revs[child].height;
-        revs[child].gc = true;
-        ++c;
-      }
+      totalHeight += child.height();
+      child.gc = true;
     }
+
+    totalHeight -= rev.children[0].height() / 2;
+    totalHeight -= rev.children[rev.children.length - 1].height() / 2;
   
-    avgHeight = totalHeight / c;
-    
-    p = new Object(rev.center());
-    p.x += REVWIDTH + HSPACING;
-    p.y -= (totalHeight - avgHeight) / 2;
+    let x = rev.x();
+    let y = rev.y();
+    x += REVWIDTH + HSPACING;
+    y -= totalHeight / 2;
+
+    if (bottompositions[position]) {
+      let p = bottompositions[position];
+      let miny = p.y() + p.height() / 2 +
+        rev.children[0].height() / 2 + VSPACING;
+      if (y < miny)
+        y = miny;
+    }
+
+    if (isNaN(y)) {
+      throw ("y is NaN");
+    }
     
-    var rightEdge = measure($('#inside-scrolling'), 'left') + measure($('#inside-scrolling'), 'width');
-    
-    for each (child in rev.children) {
-      if (child in revs) {
-        childrev = revs[child];
-        childrev.moveTo(p);
-        p.y += childrev.height + VSPACING;
+    let rightEdge = gWidth; // XXX won't be true if we introduce scaling
+
+    for each (let child in rev.children) {
+      child.moveTo(x, y);
+      y += child.height() + VSPACING;
   
-        if (p.x < rightEdge) {
-          drawChildren(childrev);
-        }
+      if (x < rightEdge) {
+        drawChildren(child, position + 1);
       }
-      else {
+
+      if (!child.loaded())
         loadMore.push(child);
-      }
+
+      bottompositions[position] = child;
     }
   }
   
-  function drawParents(rev)
+  function drawParents(rev, position)
   {
-    var p, parent, totalHeight, avgHeight, c, parentrev;
-    
     if (rev.parents.length == 0)
       return;
     
-    totalHeight = 0;
-    c = 0;
+    let totalHeight = 0;
+
+    for each (let parent in rev.parents) {
+      totalHeight += parent.height();
+      parent.gc = true;
+    }
     
-    for each (parent in rev.parents) {
-      if (parent in revs) {
-        totalHeight += revs[parent].height;
-        revs[parent].gc = true;
-        ++c;
-      }
+    totalHeight -= rev.parents[0].height() / 2;
+    totalHeight -= rev.parents[rev.parents.length - 1].height() / 2;
+  
+    let x = rev.x();
+    let y = rev.y();
+    x -= REVWIDTH + HSPACING;
+    y -= totalHeight / 2;
+
+    if (bottompositions[position]) {
+      let p = bottompositions[position];
+      let miny = p.y() + p.height() / 2 +
+        rev.parents[0].height() / 2 + VSPACING;
+      if (y < miny)
+        y = miny;
     }
     
-    avgHeight = totalHeight / c;
-    
-    p = new Object(rev.center());
-    p.x -= REVWIDTH + HSPACING;
-    p.y -= (totalHeight - avgHeight) / 2;
-    
-    var leftEdge = measure($('#inside-scrolling'), 'left');
+    var leftEdge = 0;
     
-    for each (parent in rev.parents) {
-      if (parent in revs) {
-        parentrev = revs[parent];
-        parentrev.moveTo(p);
-        p.y += parentrev.height + VSPACING;
+    for each (let parent in rev.parents) {
+      parent.moveTo(x, y);
+      y += parent.height() + VSPACING;
         
-        if (p.x > leftEdge) {
-          drawParents(parentrev);
-        }
+      if (x > leftEdge) {
+        drawParents(parent, position - 1);
       }
-      else {
+      if (!parent.loaded())
         loadMore.push(parent);
-      }
+
+      bottompositions[position] = parent;
     }
   }  
+
+  let contextrev = revs[gContext];
   
-  contextrev = revs[node];
-    
-  document.title = $('#select-repo')[0].value + " revision " +
-    contextrev.rev + ": " +
-    limit(contextrev.description, 60);
+  if (contextrev.loaded()) {
+    document.title = $('#select-repo')[0].value + " revision " +
+      contextrev.rev + ": " +
+      limit(contextrev.description, 60);
+  }
+  else {
+    document.title = $('#select-repo')[0].value + " node " + contextrev.node;
+  }
 
-  // All the nodes which have .gc = false at the end will be hidden
-  for each (rev in revs)
+  // All the nodes which have .gc = false at the end are offscreen and can
+  // be ignored
+  for each (let rev in revs)
     rev.gc = false;
 
   contextrev.gc = true;
-  i = $('#inside-scrolling');
-  contextrev.moveTo({x: measure(i, 'width') / 2,
-                     y: measure(i, 'height') / 2});
+  contextrev.moveTo(gWidth / 2,
+                    gHeight / 2);
   
-  drawChildren(contextrev);
-  drawParents(contextrev);
+  drawChildren(contextrev, 1);
+  drawParents(contextrev, -1);
   
-  for each (rev in revs)
-    rev.cleanLayout();
+  redraw();
 }
 
+function redraw()
+{
+  var cx = getCX();
 
+  /**
+   * Draw some text. Advance the translation down by 12px
+   */
+  function drawText(t, xoffset)
+  {
+    if (xoffset < 2)
+      xoffset = 2;
+
+    cx.translate(0, 12);
+    cx.save();
+    cx.translate(xoffset, 0);
+    cx.mozDrawText(t);
+    cx.restore();
+  }
+
+  function drawRev(r)
+  {
+    let h = r.height();
+    let left = r.x() - REVWIDTH / 2;
+    let top = r.y() - h / 2;
+
+    cx.save();
+    // clip the text to the box
+    cx.beginPath();
+    cx.rect(left, top, REVWIDTH, h);
+    cx.stroke();
+    cx.clip();
+
+    cx.save();
+    cx.fillStyle = "white";
+    cx.fill();
+    cx.restore();
+
+    if (!r.loaded())
+      cx.fillStyle = "#900";
+
+    cx.translate(left, top);
+    drawText(r.revnode, (REVWIDTH - r.revnodelen) / 2);
+    if (r.loaded()) {
+      drawText(r.user, (REVWIDTH - r.userlen) / 2);
+      drawText(r.date, (REVWIDTH - r.datelen) / 2);
+      for each (let line in r.descSplit) {
+        drawText(line, 2);
+      }
+    }
+    cx.restore();
+  }
+
+  function drawArrows(r)
+  {
+    /* draw all arrows from this rev */
+    for each (let child in r.children) {
+      let childx = r.x() + 100;
+      let childy = r.y();
+      if (child.gc) {
+        childx = child.x() - REVWIDTH / 2;
+        childy = child.y();
+      }
+      cx.beginPath();
+      cx.moveTo(r.x(), r.y());
+      cx.lineTo(childx, childy);
+      cx.stroke();
+    }
+  }
+
+  cx.clearRect(0, 0, gWidth, gHeight);
+
+  cx.save();
+  cx.lineWidth = 2;
+  for each (let rev in revs) {
+    if (rev.gc)
+      drawArrows(rev);
+  }
+  cx.restore();
+
+  for each (let rev in revs) {
+    if (rev.gc)
+      drawRev(rev);
+  }
+  return;
+}
 
 function processContextData(data)
 {
-  for each (var node in data.nodes) {
-      if (node.node in revs)
-          continue;
-
-      new Revision(node);
+  for each (var nodeObj in data.nodes) {
+      getRevision(nodeObj.node).update(nodeObj);
   }
 
-  doLayout(data.context);
+  if (this.changeContext)
+    gContext = data.context;
+
+  doLayout();
 }
 
 function startContext(hash)
 {
     var repo, context;
 
     if (hash == '') {
         repo = $('#select-repo')[0].value;
@@ -371,35 +427,77 @@ function startContext(hash)
     else {
         var l = hash.split(':');
         repo = l[0];
         context = l[1];
         $('#select-repo')[0].value = repo;
         $('#node-input')[0].value = context;
     }
 
-    $.ajax({'url': BASEURL + repo + "/jsonfamily?node=" + context,
-            'type': 'GET',
-            'dataType': 'json',
-            error: function(xhr, textStatus) {
-                alert("Request failed: " + textStatus);
-            },
-            success: processContextData
-           });
+    gPendingOptions =
+      {'url': BASEURL + repo + "/index.cgi/jsonfamily?node=" + context,
+       'type': 'GET',
+       'dataType': 'json',
+       error: function(xhr, textStatus) {
+          alert("Request failed: " + textStatus);
+        },
+       success: processContextData,
+       changeContext: true
+      };
+
+    $.ajax(gPendingOptions);
 }
 
-function navTo()
+function navTo(node)
 {
-    $('#node-input')[0].value = this.id.replace('rev', '');
-    setHash();
+  $('#node-input')[0].value = node;
+  gContext = node;
+  if (gPendingOptions)
+    gPendingOptions.changeContext = false;
+
+  doLayout();
+  setHash();
 }
 
 function setHash()
 {
     $.history.load($('#select-repo')[0].value + ':' + $('#node-input')[0].value)
 }
 
+function doResize()
+{
+  var w = $(window);
+  var c = $('#drawcanvas');
+
+  gWidth = w.width();
+  gHeight = w.height() - $('#topnav').height();
+
+  c.attr('width', gWidth);
+  c.attr('height', gHeight);
+
+  if (gContext)
+    doLayout();
+}
+
+function clickCanvas(e)
+{
+  let o = $(this).offset();
+  let canvasX = e.clientX - o.left;
+  let canvasY = e.clientY - o.top;
+
+  for each (let rev in revs) {
+    if (rev.hittest(canvasX, canvasY)) {
+      navTo(rev.node);
+      break;
+    }
+  }
+}
+
 function init()
 {
-    $('#select-repo').change(setHash);
-    $('#node-choose').click(setHash);
-    $.history.init(startContext);
+  $('#drawcanvas').click(clickCanvas);
+  $('#select-repo').change(setHash);
+  $('#node-choose').click(setHash);
+  $.history.init(startContext);
+
+  $(window).resize(doResize);
+  doResize();
 }