Animation is cool! And make BASEURL more usable in multiple configurations.
authorBenjamin Smedberg <benjamin@smedbergs.us>
Mon, 14 Apr 2008 11:19:53 -0400
changeset 15 9238af9c1accb616c0367c5227c0d529dbe98a0a
parent 14 92e5cfcae5553de1597e237e271e62e39185f380
child 16 c428ff46384ed484dea0135bb9f3aa3f1a144628
push id1
push userbsmedberg@mozilla.com
push dateMon, 14 Apr 2008 15:21:26 +0000
Animation is cool! And make BASEURL more usable in multiple configurations.
www/navigate.js
--- a/www/navigate.js
+++ b/www/navigate.js
@@ -1,19 +1,44 @@
 /* -*- Mode: Java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
 
-const BASEURL = 'http://hg.mozilla.org/';
+const BASEURL = 'http://hg.mozilla.org/%REPO%/index.cgi/';
 const SVGNS = 'http://www.w3.org/2000/svg';
 
 const REVWIDTH = 254;
 const HSPACING = 40;
 const VSPACING = 30;
 const TEXTSTYLE = '12px sans-serif';
 
+const ANIMLENGTH = 500; /* milliseconds */
+
 let gWidth, gHeight, gContext, gPendingRequest;
+let gAnimating = false;
+
+function startAnimating()
+{
+  if (!gAnimating) {
+    gAnimating = true;
+    setInterval(doAnimate, 50);
+  }
+}
+
+function doAnimate()
+{
+  redraw();
+
+  let now = new Date().getTime();
+
+  /* Clear our interval if there's nothing more to animate */
+  for each (let rev in revs) {
+    if (rev._atime != null && rev._atime < now)
+      return;
+  }
+  clearInterval(doAnimate);
+}
 
 function getCX()
 {
   let cx = $('#drawcanvas')[0].getContext('2d');
   cx.mozTextStyle = TEXTSTYLE;
   return cx;
 }
 
@@ -81,18 +106,19 @@ function Revision(node)
   this.revnodelen = measure(this.revnode);
 }
 
 Revision.prototype = {
   parents: [],
   children: [],
   _x: null,
   _y: null,
+  gc: false,
 
-  _atime: null,
+  _atime: null, /* the start time of an animation */
   /* constraint: _ax and _ay are meaningless unless _atime is set */
 
   loaded: function r_loaded()
   {
     return 'rev' in this;
   },
 
   update: function r_update(data)
@@ -132,53 +158,87 @@ Revision.prototype = {
   y: function r_y() {
     if (this._y == null)
       throw Error("Revision " + this.node + " is not positioned.");
 
     return this._y;
   },
 
   /* current animated position */
+
+  /* from 0 to 1, how far are we through the animation? */
+  easing: function r_easing()
+  {
+    let m = (new Date().getTime() - this._atime) / ANIMLENGTH;
+    if (m >= 1) {
+      this._atime = null;
+      return 1;
+    }
+    return m;
+  },
+
   ax: function r_ax() {
+    let x = this.x();
     if (this._atime == null)
-      return this.x();
+      return x;
 
-    return this._ax;
+    let e = this.easing();
+    return x + (this._ax - x) * (1 - e);
   },
 
   ay: function r_ay() {
+    let y = this.y();
     if (this._atime == null)
-      return this.y();
+      return y;
   
-    return this._ay;
+    let e = this.easing();
+    return y + (this._ay - y) * (1 - e);
   },
 
   /* height */
   height: function r_height() {
     if (this.loaded()) {
       return 12 * (3 + this.descSplit.length) + 4;
     }
     
     return 12 + 4;
   },
 
   /**
    * Move the center of the box to this point
+   * context is optional: it specifies an already positioned revision
+   * which we can animate against if we are not currently positioned.
    */
-  moveTo: function r_move(x, y) {
+  moveTo: function r_move(x, y, context) {
     if (isNaN(x))
       throw Error("x is NaN");
 
     if (isNaN(y))
       throw Error("y is NaN");
 
+    if (this._x += null) {
+      this.animStart(this._x, this._y);
+    }
+    else if (context) {
+      this.animStart(x + context.ax() - context.x(),
+                     y + context.ay() - context.y());
+    }
+
     this._x = x;
     this._y = y;
   },
 
+  animStart: function r_animStart(x, y)
+  {
+    startAnimating();
+    this._atime = new Date().getTime();
+    this._ax = x;
+    this._ay = y;
+  },
+
   shortnode: function r_shortnode()
   {
     return this.node.slice(0, 12);
   },
 
   hittest: function r_hittest(x, y)
   {
     if (!this.gc)
@@ -223,17 +283,16 @@ function doLayout()
   {
     if (rev.children.length == 0)
       return;
     
     let totalHeight = (rev.children.length - 1) * VSPACING;
     
     for each (child in rev.children) {
       totalHeight += child.height();
-      child.gc = true;
     }
 
     totalHeight -= rev.children[0].height() / 2;
     totalHeight -= rev.children[rev.children.length - 1].height() / 2;
   
     let x = rev.x();
     let y = rev.y();
     x += REVWIDTH + HSPACING;
@@ -249,40 +308,40 @@ function doLayout()
 
     if (isNaN(y)) {
       throw ("y is NaN");
     }
     
     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 (x < rightEdge) {
-        drawChildren(child, position + 1);
+      if (!child.gc) {
+        child.gc = true;
+        child.moveTo(x, y);
+        y += child.height() + VSPACING;
+        if (x < rightEdge) {
+          drawChildren(child, position + 1);
+        }
+        if (!child.loaded())
+          loadMore.push(child);
+
+        bottompositions[position] = child;
       }
-
-      if (!child.loaded())
-        loadMore.push(child);
-
-      bottompositions[position] = child;
     }
   }
   
   function drawParents(rev, position)
   {
     if (rev.parents.length == 0)
       return;
     
     let totalHeight = 0;
 
     for each (let parent in rev.parents) {
       totalHeight += parent.height();
-      parent.gc = true;
     }
     
     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;
@@ -294,26 +353,29 @@ function doLayout()
         rev.parents[0].height() / 2 + VSPACING;
       if (y < miny)
         y = miny;
     }
     
     var leftEdge = 0;
     
     for each (let parent in rev.parents) {
-      parent.moveTo(x, y);
-      y += parent.height() + VSPACING;
+      if (!parent.gc) {
+        parent.gc = true;
+        parent.moveTo(x, y);
+        y += parent.height() + VSPACING;
         
-      if (x > leftEdge) {
-        drawParents(parent, position - 1);
+        if (x > leftEdge) {
+          drawParents(parent, position - 1);
+        }
+        if (!parent.loaded())
+          loadMore.push(parent);
+
+        bottompositions[position] = parent;
       }
-      if (!parent.loaded())
-        loadMore.push(parent);
-
-      bottompositions[position] = parent;
     }
   }  
 
   let contextrev = revs[gContext];
   
   if (contextrev.loaded()) {
     document.title = $('#select-repo')[0].value + " revision " +
       contextrev.rev + ": " +
@@ -329,22 +391,28 @@ function doLayout()
     rev.gc = false;
 
   contextrev.gc = true;
   contextrev.moveTo(gWidth / 2,
                     gHeight / 2);
   
   drawChildren(contextrev, 1);
   drawParents(contextrev, -1);
+
+  for each (let rev in revs) {
+    if (!rev.gc)
+      rev._x = null;
+  }
   
   redraw();
 }
 
 function redraw()
 {
+  try {
   var cx = getCX();
 
   /**
    * Draw some text. Advance the translation down by 12px
    */
   function drawText(t, xoffset)
   {
     if (xoffset < 2)
@@ -416,17 +484,20 @@ function redraw()
       drawArrows(rev);
   }
   cx.restore();
 
   for each (let rev in revs) {
     if (rev.gc)
       drawRev(rev);
   }
-  return;
+  }
+  catch (e) {
+    alert(e + "\nstack: " + e.stack);
+  }
 }
 
 function processContextData(data)
 {
   for each (var nodeObj in data.nodes) {
       getRevision(nodeObj.node).update(nodeObj);
   }
 
@@ -447,36 +518,36 @@ function startContext(hash)
     else {
         var l = hash.split(':');
         repo = l[0];
         context = l[1];
         $('#select-repo')[0].value = repo;
         $('#node-input')[0].value = context;
     }
 
-    gPendingOptions =
-      {'url': BASEURL + repo + "/index.cgi/jsonfamily?node=" + context,
+    gPendingRequest =
+      {'url': BASEURL.replace('%REPO%', repo) + "jsonfamily?node=" + context,
        'type': 'GET',
        'dataType': 'json',
        error: function(xhr, textStatus) {
           alert("Request failed: " + textStatus);
         },
        success: processContextData,
        changeContext: true
       };
 
-    $.ajax(gPendingOptions);
+    $.ajax(gPendingRequest);
 }
 
 function navTo(node)
 {
   $('#node-input')[0].value = node;
   gContext = node;
-  if (gPendingOptions)
-    gPendingOptions.changeContext = false;
+  if (gPendingRequest)
+    gPendingRequest.changeContext = false;
 
   doLayout();
   setHash();
 }
 
 function setHash()
 {
     $.history.load($('#select-repo')[0].value + ':' + $('#node-input')[0].value)