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; };