pcottle.learnGitBranching/src/visuals.js
2012-10-13 00:22:57 -07:00

540 lines
14 KiB
JavaScript

function GitVisuals(options) {
this.commitCollection = options.commitCollection;
this.branchCollection = options.branchCollection;
this.visNodeMap = {};
this.edgeCollection = new VisEdgeCollection();
this.visBranchCollection = new VisBranchCollection();
this.commitMap = {};
this.rootCommit = null;
this.branchStackMap = null;
this.upstreamBranchSet = null;
this.upstreamHeadSet = null;
this.paperReady = false;
this.paperWidth = null;
this.paperHeight = null;
this.commitCollection.on('change', this.collectionChanged, this);
this.branchCollection.on('add', this.addBranch, this);
this.branchCollection.on('remove', this.removeBranch, this);
events.on('canvasResize', _.bind(
this.canvasResize, this
));
events.on('raphaelReady', _.bind(
this.drawTreeFirstTime, this
));
events.on('refreshTree', _.bind(
this.refreshTree, this
));
events.on('gitEngineReady', this.whenGitEngineReady, this);
}
GitVisuals.prototype.whenGitEngineReady = function(gitEngine) {
// seed this with the HEAD pseudo-branch
this.visBranchCollection.add(new VisBranch({
branch: gitEngine.HEAD
}));
};
GitVisuals.prototype.getScreenBounds = function() {
// for now we return the node radius subtracted from the walls
return {
widthPadding: GRAPHICS.nodeRadius * 1.5,
heightPadding: GRAPHICS.nodeRadius * 1.5
};
};
GitVisuals.prototype.toScreenCoords = function(pos) {
if (!this.paperWidth) {
throw new Error('being called too early for screen coords');
}
var bounds = this.getScreenBounds();
var shrink = function(frac, total, padding) {
return padding + frac * (total - padding * 2);
};
return {
x: shrink(pos.x, this.paperWidth, bounds.widthPadding),
y: shrink(pos.y, this.paperHeight, bounds.heightPadding)
};
};
GitVisuals.prototype.animateAllFromAttrToAttr = function(fromSnapshot, toSnapshot) {
var animate = function(obj) {
var id = obj.getID();
if (!fromSnapshot[id] || !toSnapshot[id]) {
console.warn('this obj doesnt exist yet', id);
return;
}
obj.animateFromAttrToAttr(fromSnapshot[id], toSnapshot[id]);
};
this.visBranchCollection.each(function(visBranch) {
animate(visBranch);
});
this.edgeCollection.each(function(visEdge) {
animate(visEdge);
});
_.each(this.visNodeMap, function(visNode) {
animate(visNode);
});
};
/***************************************
== BEGIN Tree Calculation Parts ==
_ __ __ _
\\/ / \ \//_
\ \ / __| __
\ \___/ /_____/ /
| _______ \
\ ( ) / \_\
\ /
| |
| |
____+-_=+-^ ^+-=_=__________
^^ I drew that :D
**************************************/
GitVisuals.prototype.genSnapshot = function() {
this.fullCalc();
var snapshot = {};
_.each(this.visNodeMap, function(visNode) {
snapshot[visNode.get('id')] = visNode.getAttributes();
}, this);
this.visBranchCollection.each(function(visBranch) {
snapshot[visBranch.getID()] = visBranch.getAttributes();
}, this);
this.edgeCollection.each(function(visEdge) {
snapshot[visEdge.getID()] = visEdge.getAttributes();
}, this);
return snapshot;
};
GitVisuals.prototype.refreshTree = function(speed) {
if (!this.paperReady) {
return;
}
// this method can only be called after graphics are rendered
this.fullCalc();
this.animateAll(speed);
};
GitVisuals.prototype.refreshTreeHarsh = function() {
this.fullCalc();
this.animateAll(0);
};
GitVisuals.prototype.animateAll = function(speed) {
this.zIndexReflow();
this.animateEdges(speed);
this.animateNodePositions(speed);
this.animateRefs(speed);
};
GitVisuals.prototype.fullCalc = function() {
this.calcTreeCoords();
this.calcGraphicsCoords();
};
GitVisuals.prototype.calcTreeCoords = function() {
// this method can only contain things that dont rely on graphics
if (!this.rootCommit) {
throw new Error('grr, no root commit!');
}
this.calcUpstreamSets();
this.calcBranchStacks();
this.calcDepth();
this.calcWidth();
};
GitVisuals.prototype.calcGraphicsCoords = function() {
this.visBranchCollection.each(function(visBranch) {
visBranch.updateName();
});
};
GitVisuals.prototype.calcUpstreamSets = function() {
this.upstreamBranchSet = gitEngine.getUpstreamBranchSet();
this.upstreamHeadSet = gitEngine.getUpstreamHeadSet();
};
GitVisuals.prototype.getCommitUpstreamBranches = function(commit) {
return this.branchStackMap[commit.get('id')];
};
GitVisuals.prototype.getBlendedHuesForCommit = function(commit) {
var branches = this.upstreamBranchSet[commit.get('id')];
if (!branches) {
throw new Error('that commit doesnt have upstream branches!');
}
return this.blendHuesFromBranchStack(branches);
};
GitVisuals.prototype.blendHuesFromBranchStack = function(branchStackArray) {
var hueStrings = [];
_.each(branchStackArray, function(branchWrapper) {
var fill = branchWrapper.obj.get('visBranch').get('fill');
if (fill.slice(0,3) !== 'hsb') {
// crap! convert
var color = Raphael.color(fill);
fill = 'hsb(' + String(color.h) + ',' + String(color.l);
fill = fill + ',' + String(color.s) + ')';
}
hueStrings.push(fill);
});
return blendHueStrings(hueStrings);
};
GitVisuals.prototype.getCommitUpstreamStatus = function(commit) {
if (!this.upstreamBranchSet) {
throw new Error("Can't calculate this yet!");
}
var id = commit.get('id');
var branch = this.upstreamBranchSet;
var head = this.upstreamHeadSet;
if (branch[id]) {
return 'branch';
} else if (head[id]) {
return 'head';
} else {
return 'none';
}
};
GitVisuals.prototype.calcBranchStacks = function() {
var branches = gitEngine.getBranches();
var map = {};
_.each(branches, function(branch) {
var thisId = branch.target.get('id');
map[thisId] = map[thisId] || [];
map[thisId].push(branch);
map[thisId].sort(function(a, b) {
var aId = a.obj.get('id');
var bId = b.obj.get('id');
if (aId == 'master' || bId == 'master') {
return aId == 'master' ? -1 : 1;
}
return aId.localeCompare(bId);
});
});
this.branchStackMap = map;
};
GitVisuals.prototype.calcWidth = function() {
this.maxWidthRecursive(this.rootCommit);
this.assignBoundsRecursive(this.rootCommit, 0, 1);
};
GitVisuals.prototype.maxWidthRecursive = function(commit) {
var childrenTotalWidth = 0;
_.each(commit.get('children'), function(child) {
var childWidth = this.maxWidthRecursive(child);
childrenTotalWidth += childWidth;
}, this);
var maxWidth = Math.max(1, childrenTotalWidth);
commit.get('visNode').set('maxWidth', maxWidth);
return maxWidth;
};
GitVisuals.prototype.assignBoundsRecursive = function(commit, min, max) {
// I always center myself within my bounds
var myWidthPos = (min + max) / 2.0;
commit.get('visNode').get('pos').x = myWidthPos;
if (commit.get('children').length == 0) {
return;
}
// i have a certain length to divide up
var myLength = max - min;
// I will divide up that length based on my children's max width in a
// basic box-flex model
var totalFlex = 0;
var children = commit.get('children');
_.each(children, function(child) {
totalFlex += child.get('visNode').getMaxWidthScaled();
}, this);
var prevBound = min;
// now go through and do everything
// TODO: order so the max width children are in the middle!!
_.each(children, function(child) {
var flex = child.get('visNode').getMaxWidthScaled();
var portion = (flex / totalFlex) * myLength;
var childMin = prevBound;
var childMax = childMin + portion;
this.assignBoundsRecursive(child, childMin, childMax);
prevBound = childMax;
}, this);
};
GitVisuals.prototype.calcDepth = function() {
var maxDepth = this.calcDepthRecursive(this.rootCommit, 0);
if (maxDepth > 15) {
// issue warning
events.trigger('issueWarning',
'Max Depth Exceeded! Visuals may degrade here. ' +
'Please start fresh'
);
}
var depthIncrement = this.getDepthIncrement(maxDepth);
_.each(this.visNodeMap, function(visNode) {
visNode.setDepthBasedOn(depthIncrement);
}, this);
};
/***************************************
== END Tree Calculation ==
_ __ __ _
\\/ / \ \//_
\ \ / __| __
\ \___/ /_____/ /
| _______ \
\ ( ) / \_\
\ /
| |
| |
____+-_=+-^ ^+-=_=__________
^^ I drew that :D
**************************************/
GitVisuals.prototype.animateNodePositions = function(speed) {
_.each(this.visNodeMap, function(visNode) {
visNode.animateUpdatedPosition(speed);
}, this);
};
GitVisuals.prototype.addBranch = function(branch) {
var visBranch = new VisBranch({
branch: branch
});
this.visBranchCollection.add(visBranch);
if (this.paperReady) {
visBranch.genGraphics(paper);
}
};
GitVisuals.prototype.removeVisBranch = function(visBranch) {
this.visBranchCollection.remove(visBranch);
};
GitVisuals.prototype.animateRefs = function(speed) {
this.visBranchCollection.each(function(visBranch) {
visBranch.animateUpdatedPos(speed);
}, this);
};
GitVisuals.prototype.animateEdges = function(speed) {
this.edgeCollection.each(function(edge) {
edge.animateUpdatedPath(speed);
}, this);
};
GitVisuals.prototype.getDepthIncrement = function(maxDepth) {
// assume there are at least 7 layers until later
maxDepth = Math.max(maxDepth, 7);
var increment = 1.0 / maxDepth;
return increment;
};
GitVisuals.prototype.calcDepthRecursive = function(commit, depth) {
commit.get('visNode').set('depth', depth);
var children = commit.get('children');
var maxDepth = depth;
for (var i = 0; i < children.length; i++) {
var d = this.calcDepthRecursive(children[i], depth + 1);
maxDepth = Math.max(d, maxDepth);
}
return maxDepth;
// TODO for merge commits, a specific fancy schamncy "main" commit line
};
GitVisuals.prototype.canvasResize = function(width, height) {
this.paperWidth = width;
this.paperHeight = height;
// refresh when we are ready
this.refreshTree();
// TODO when animation is happening, do this: events.trigger('processCommandFromEvent', 'refresh');
};
GitVisuals.prototype.addNode = function(id, commit) {
this.commitMap[id] = commit;
if (commit.get('rootCommit')) {
this.rootCommit = commit;
}
var visNode = new VisNode({
id: id,
commit: commit
});
this.visNodeMap[id] = visNode;
if (this.paperReady) {
visNode.genGraphics(paper);
}
return visNode;
};
GitVisuals.prototype.addEdge = function(idTail, idHead) {
var visNodeTail = this.visNodeMap[idTail];
var visNodeHead = this.visNodeMap[idHead];
if (!visNodeTail || !visNodeHead) {
throw new Error('one of the ids in (' + idTail +
', ' + idHead + ') does not exist');
}
var edge = new VisEdge({
tail: visNodeTail,
head: visNodeHead
});
this.edgeCollection.add(edge);
if (this.paperReady) {
edge.genGraphics(paper);
}
};
GitVisuals.prototype.collectionChanged = function() {
console.log('git visuals... collection was changed');
// redo stuff
};
GitVisuals.prototype.zIndexReflow = function() {
this.visNodesFront();
this.visBranchesFront();
};
GitVisuals.prototype.visNodesFront = function() {
_.each(this.visNodeMap, function(visNode) {
visNode.toFront();
});
};
GitVisuals.prototype.visBranchesFront = function() {
this.visBranchCollection.each(function(vBranch) {
vBranch.nonTextToFront();
});
this.visBranchCollection.each(function(vBranch) {
vBranch.textToFront();
});
};
GitVisuals.prototype.drawTreeFirstTime = function() {
this.paperReady = true;
this.calcTreeCoords();
_.each(this.visNodeMap, function(visNode) {
visNode.genGraphics(paper);
}, this);
this.edgeCollection.each(function(edge) {
edge.genGraphics(paper);
}, this);
this.visBranchCollection.each(function(visBranch) {
visBranch.genGraphics(paper);
}, this);
this.zIndexReflow();
};
/************************
* Random util functions, some from liquidGraph
***********************/
function blendHueStrings(hueStrings) {
// assumes a sat of 0.7 and brightness of 1
var x = 0;
var y = 0;
var totalSat = 0;
var totalBright = 0;
var length = hueStrings.length;
_.each(hueStrings, function(hueString) {
var exploded = hueString.split('(')[1];
exploded = exploded.split(')')[0];
exploded = exploded.split(',');
totalSat += parseFloat(exploded[1]);
totalBright += parseFloat(exploded[2]);
var hue = parseFloat(exploded[0]);
var angle = hue * Math.PI * 2;
x += Math.cos(angle);
y += Math.sin(angle);
});
x = x / length;
y = y / length;
totalSat = totalSat / length;
totalBright = totalBright / length;
var hue = Math.atan2(y, x) / (Math.PI * 2); // could fail on 0's
if (hue < 0) {
hue = hue + 1;
}
return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')';
}
function randomHueString() {
var hue = Math.random();
var str = 'hsb(' + String(hue) + ',0.7,1)';
return str;
};
function cuteSmallCircle(paper, x, y, options) {
var options = options || {};
var wantsSameColor = options.sameColor;
var radius = options.radius || 4;
var c = paper.circle(x, y, radius, radius);
if (!wantsSameColor) {
c.attr("fill","hsba(0.5,0.8,0.7,1)");
} else {
c.attr("fill","hsba(" + String(Math.random()) + ",0.8,0.7,1)");
}
c.attr("stroke","#FFF");
c.attr("stroke-width",2);
return c;
};