diff --git a/build/bundle.js b/build/bundle.js index 646a2b8b..a65e2681 100644 --- a/build/bundle.js +++ b/build/bundle.js @@ -649,6 +649,7 @@ exports.AnimationFactory = AnimationFactory; require.define("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit; var Branch = require('./git').Branch; +var events = require('./main').events; var CommitCollection = Backbone.Collection.extend({ model: Commit @@ -748,7 +749,8 @@ exports.CommandBuffer = CommandBuffer; }); -require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var animationFactory = new require('./animationFactory').AnimationFactory(); +require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactoryModule = require('./animationFactory'); +var animationFactory = new AnimationFactoryModule.AnimationFactory(); // backbone or something uses _.uniqueId, so we make our own here var uniqueId = (function() { @@ -2382,6 +2384,67 @@ exports.Branch = Branch; exports.Ref = Ref; +}); + +require.define("/main.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactory = require('./animationFactory').AnimationFactory; +var CommandCollection = require('./collections').CommandCollection; +var CommandBuffer = require('./collections').CommandBuffer; +var CommandPromptView = require('./commandViews').CommandPromptView; +var CommandLineHistoryView = require('./commandViews').CommandLineHistoryView; +var Visualization = require('./visuals').Visualization; + +/** + * Globals + */ +var events = _.clone(Backbone.Events); +var ui = null; +var animationFactory = null; + +/** + * Static Classes + */ +animationFactory = new AnimationFactory(); + +/////////////////////////////////////////////////////////////////////// + +$(document).ready(function(){ + ui = new UI(); + mainVis = new Visualization({ + el: $('#canvasWrapper')[0] + }); + + if (/\?demo/.test(window.location.href)) { + setTimeout(function() { + events.trigger('submitCommandValueFromEvent', "gc; git checkout HEAD~1; git commit; git checkout -b bugFix; gc; gc; git rebase master; git checkout master; gc; gc; git merge bugFix"); + }, 500); + } +}); + +function UI() { + // static classes + this.commandCollection = new CommandCollection(); + + this.commandBuffer = new CommandBuffer({ + collection: this.commandCollection + }); + + this.commandPromptView = new CommandPromptView({ + el: $('#commandLineBar'), + collection: this.commandCollection + }); + this.commandLineHistoryView = new CommandLineHistoryView({ + el: $('#commandLineHistory'), + collection: this.commandCollection + }); + + $('#commandTextField').focus(); +} + +exports.events = events; +exports.ui = ui; +exports.animationFactory = animationFactory; + + }); require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection; @@ -2774,10 +2837,19 @@ exports.CommandLineHistoryView = CommandLineHistoryView; }); -require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var CommitCollection = require('./collections').CommitCollection; -var BranchCollection = require('./collections').BranchCollection; +require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Collections = require('./collections'); +var CommitCollection = Collections.CommitCollection; +var BranchCollection = Collections.BranchCollection; + var GitEngine = require('./git').GitEngine; +var Tree = require('./tree'); +var VisEdgeCollection = Tree.VisEdgeCollection; +var VisBranchCollection = Tree.VisBranchCollection; +var VisNode = Tree.VisNode; +var VisBranch = Tree.VisBranch; +var VisEdge = Tree.VisEdge; + var Visualization = Backbone.View.extend({ initialize: function(options) { var _this = this; @@ -3441,15 +3513,977 @@ function blendHueStrings(hueStrings) { return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')'; } -function randomHueString() { - var hue = Math.random(); - var str = 'hsb(' + String(hue) + ',0.7,1)'; - return str; -}; - exports.Visualization = Visualization; +}); + +require.define("/tree.js",function(require,module,exports,__dirname,__filename,process,global){var randomHueString = function() { + var hue = Math.random(); + var str = 'hsb(' + String(hue) + ',0.7,1)'; + return str; +}; + +var VisBase = Backbone.Model.extend({ + removeKeys: function(keys) { + _.each(keys, function(key) { + if (this.get(key)) { + this.get(key).remove(); + } + }, this); + } +}); + +var VisBranch = VisBase.extend({ + defaults: { + pos: null, + text: null, + rect: null, + arrow: null, + isHead: false, + flip: 1, + + fill: GRAPHICS.rectFill, + stroke: GRAPHICS.rectStroke, + 'stroke-width': GRAPHICS.rectStrokeWidth, + + offsetX: GRAPHICS.nodeRadius * 4.75, + offsetY: 0, + arrowHeight: 14, + arrowInnerSkew: 0, + arrowEdgeHeight: 6, + arrowLength: 14, + arrowOffsetFromCircleX: 10, + + vPad: 5, + hPad: 5, + + animationSpeed: GRAPHICS.defaultAnimationTime, + animationEasing: GRAPHICS.defaultEasing + }, + + validateAtInit: function() { + if (!this.get('branch')) { + throw new Error('need a branch!'); + } + }, + + getFill: function() { + return this.get('fill'); + }, + + getID: function() { + return this.get('branch').get('id'); + }, + + initialize: function() { + this.validateAtInit(); + + // shorthand notation for the main objects + this.gitVisuals = this.get('gitVisuals'); + this.gitEngine = this.get('gitEngine'); + if (!this.gitEngine) { + console.log('throw damnit'); + throw new Error('asd'); + } + + this.get('branch').set('visBranch', this); + var id = this.get('branch').get('id'); + + if (id == 'HEAD') { + // switch to a head ref + this.set('isHead', true); + this.set('flip', -1); + + this.set('fill', GRAPHICS.headRectFill); + } else if (id !== 'master') { + // we need to set our color to something random + this.set('fill', randomHueString()); + } + }, + + getCommitPosition: function() { + var commit = this.gitEngine.getCommitFromRef(this.get('branch')); + var visNode = commit.get('visNode'); + return visNode.getScreenCoords(); + }, + + getBranchStackIndex: function() { + if (this.get('isHead')) { + // head is never stacked with other branches + return 0; + } + + var myArray = this.getBranchStackArray(); + var index = -1; + _.each(myArray, function(branch, i) { + if (branch.obj == this.get('branch')) { + index = i; + } + }, this); + return index; + }, + + getBranchStackLength: function() { + if (this.get('isHead')) { + // head is always by itself + return 1; + } + + return this.getBranchStackArray().length; + }, + + getBranchStackArray: function() { + var arr = this.gitVisuals.branchStackMap[this.get('branch').get('target').get('id')]; + if (arr === undefined) { + // this only occurs when we are generating graphics inside of + // a new Branch instantiation, so we need to force the update + this.gitVisuals.calcBranchStacks(); + return this.getBranchStackArray(); + } + return arr; + }, + + getTextPosition: function() { + var pos = this.getCommitPosition(); + + // then order yourself accordingly. we use alphabetical sorting + // so everything is independent + var myPos = this.getBranchStackIndex(); + return { + x: pos.x + this.get('flip') * this.get('offsetX'), + y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY') + }; + }, + + getRectPosition: function() { + var pos = this.getTextPosition(); + var f = this.get('flip'); + + // first get text width and height + var textSize = this.getTextSize(); + return { + x: pos.x - 0.5 * textSize.w - this.get('hPad'), + y: pos.y - 0.5 * textSize.h - this.get('vPad') + } + }, + + getArrowPath: function() { + // should make these util functions... + var offset2d = function(pos, x, y) { + return { + x: pos.x + x, + y: pos.y + y + }; + }; + var toStringCoords = function(pos) { + return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); + }; + var f = this.get('flip'); + + var arrowTip = offset2d(this.getCommitPosition(), + f * this.get('arrowOffsetFromCircleX'), + 0 + ); + var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight')); + var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight')); + + var arrowInnerUp = offset2d(arrowEdgeUp, + f * this.get('arrowInnerSkew'), + this.get('arrowEdgeHeight') + ); + var arrowInnerLow = offset2d(arrowEdgeLow, + f * this.get('arrowInnerSkew'), + -this.get('arrowEdgeHeight') + ); + + var tailLength = 49; + var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0); + var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0); + + var pathStr = ''; + pathStr += 'M' + toStringCoords(arrowStartUp) + ' '; + var coords = [ + arrowInnerUp, + arrowEdgeUp, + arrowTip, + arrowEdgeLow, + arrowInnerLow, + arrowStartLow + ]; + _.each(coords, function(pos) { + pathStr += 'L' + toStringCoords(pos) + ' '; + }, this); + pathStr += 'z'; + return pathStr; + }, + + getTextSize: function() { + var getTextWidth = function(visBranch) { + var textNode = visBranch.get('text').node; + return (textNode === null) ? 1 : textNode.clientWidth; + }; + + var textNode = this.get('text').node; + if (this.get('isHead')) { + // HEAD is a special case + return { + w: textNode.clientWidth, + h: textNode.clientHeight + }; + } + + var maxWidth = 0; + _.each(this.getBranchStackArray(), function(branch) { + maxWidth = Math.max(maxWidth, getTextWidth( + branch.obj.get('visBranch') + )); + }); + + return { + w: maxWidth, + h: textNode.clientHeight + }; + }, + + getSingleRectSize: function() { + var textSize = this.getTextSize(); + var vPad = this.get('vPad'); + var hPad = this.get('hPad'); + return { + w: textSize.w + vPad * 2, + h: textSize.h + hPad * 2 + }; + }, + + getRectSize: function() { + var textSize = this.getTextSize(); + // enforce padding + var vPad = this.get('vPad'); + var hPad = this.get('hPad'); + + // number of other branch names we are housing + var totalNum = this.getBranchStackLength(); + return { + w: textSize.w + vPad * 2, + h: textSize.h * totalNum * 1.1 + hPad * 2 + }; + }, + + getName: function() { + var name = this.get('branch').get('id'); + var selected = this.gitEngine.HEAD.get('target').get('id'); + + var add = (selected == name) ? '*' : ''; + return name + add; + }, + + nonTextToFront: function() { + this.get('arrow').toFront(); + this.get('rect').toFront(); + }, + + textToFront: function() { + this.get('text').toFront(); + }, + + getFill: function() { + // in the easy case, just return your own fill if you are: + // - the HEAD ref + // - by yourself (length of 1) + // - part of a multi branch, but your thing is hidden + if (this.get('isHead') || + this.getBranchStackLength() == 1 || + this.getBranchStackIndex() != 0) { + return this.get('fill'); + } + + // woof. now it's hard, we need to blend hues... + return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray()); + }, + + remove: function() { + this.removeKeys(['text', 'arrow', 'rect']); + // also need to remove from this.gitVisuals + this.gitVisuals.removeVisBranch(this); + }, + + genGraphics: function(paper) { + var textPos = this.getTextPosition(); + var name = this.getName(); + var text; + + // when from a reload, we dont need to generate the text + text = paper.text(textPos.x, textPos.y, String(name)); + text.attr({ + 'font-size': 14, + 'font-family': 'Monaco, Courier, font-monospace', + opacity: this.getTextOpacity() + }); + this.set('text', text); + + var rectPos = this.getRectPosition(); + var sizeOfRect = this.getRectSize(); + var rect = paper + .rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8) + .attr(this.getAttributes().rect); + this.set('rect', rect); + + var arrowPath = this.getArrowPath(); + var arrow = paper + .path(arrowPath) + .attr(this.getAttributes().arrow); + this.set('arrow', arrow); + + rect.toFront(); + text.toFront(); + }, + + updateName: function() { + this.get('text').attr({ + text: this.getName() + }); + }, + + getNonTextOpacity: function() { + if (this.get('isHead')) { + return this.gitEngine.getDetachedHead() ? 1 : 0; + } + return this.getBranchStackIndex() == 0 ? 1 : 0.0; + }, + + getTextOpacity: function() { + if (this.get('isHead')) { + return this.gitEngine.getDetachedHead() ? 1 : 0; + } + return 1; + }, + + getAttributes: function() { + var nonTextOpacity = this.getNonTextOpacity(); + var textOpacity = this.getTextOpacity(); + this.updateName(); + + var textPos = this.getTextPosition(); + var rectPos = this.getRectPosition(); + var rectSize = this.getRectSize(); + + var arrowPath = this.getArrowPath(); + + return { + text: { + x: textPos.x, + y: textPos.y, + opacity: textOpacity + }, + rect: { + x: rectPos.x, + y: rectPos.y, + width: rectSize.w, + height: rectSize.h, + opacity: nonTextOpacity, + fill: this.getFill(), + stroke: this.get('stroke'), + 'stroke-width': this.get('stroke-width') + }, + arrow: { + path: arrowPath, + opacity: nonTextOpacity, + fill: this.getFill(), + stroke: this.get('stroke'), + 'stroke-width': this.get('stroke-width') + } + }; + }, + + animateUpdatedPos: function(speed, easing) { + var attr = this.getAttributes(); + this.animateToAttr(attr, speed, easing); + }, + + animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { + // an animation of 0 is essentially setting the attribute directly + this.animateToAttr(fromAttr, 0); + this.animateToAttr(toAttr, speed, easing); + }, + + animateToAttr: function(attr, speed, easing) { + if (speed == 0) { + this.get('text').attr(attr.text); + this.get('rect').attr(attr.rect); + this.get('arrow').attr(attr.arrow); + return; + } + + var s = speed !== undefined ? speed : this.get('animationSpeed'); + var e = easing || this.get('animationEasing'); + + this.get('text').stop().animate(attr.text, s, e); + this.get('rect').stop().animate(attr.rect, s, e); + this.get('arrow').stop().animate(attr.arrow, s, e); + } +}); + + +var VisNode = VisBase.extend({ + defaults: { + depth: undefined, + maxWidth: null, + outgoingEdges: null, + + circle: null, + text: null, + + id: null, + pos: null, + radius: null, + + commit: null, + animationSpeed: GRAPHICS.defaultAnimationTime, + animationEasing: GRAPHICS.defaultEasing, + + fill: GRAPHICS.defaultNodeFill, + 'stroke-width': GRAPHICS.defaultNodeStrokeWidth, + stroke: GRAPHICS.defaultNodeStroke + }, + + getID: function() { + return this.get('id'); + }, + + validateAtInit: function() { + if (!this.get('id')) { + throw new Error('need id for mapping'); + } + if (!this.get('commit')) { + throw new Error('need commit for linking'); + } + + if (!this.get('pos')) { + this.set('pos', { + x: Math.random(), + y: Math.random() + }); + } + }, + + initialize: function() { + this.validateAtInit(); + // shorthand for the main objects + this.gitVisuals = this.get('gitVisuals'); + this.gitEngine = this.get('gitEngine'); + + this.set('outgoingEdges', []); + }, + + setDepth: function(depth) { + // for merge commits we need to max the depths across all + this.set('depth', Math.max(this.get('depth') || 0, depth)); + }, + + setDepthBasedOn: function(depthIncrement) { + if (this.get('depth') === undefined) { + debugger + throw new Error('no depth yet!'); + } + var pos = this.get('pos'); + pos.y = this.get('depth') * depthIncrement; + }, + + getMaxWidthScaled: function() { + // returns our max width scaled based on if we are visible + // from a branch or not + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); + var map = { + branch: 1, + head: 0.3, + none: 0.1 + }; + if (map[stat] === undefined) { throw new Error('bad stat'); } + return map[stat] * this.get('maxWidth'); + }, + + toFront: function() { + this.get('circle').toFront(); + this.get('text').toFront(); + }, + + getOpacity: function() { + var map = { + 'branch': 1, + 'head': GRAPHICS.upstreamHeadOpacity, + 'none': GRAPHICS.upstreamNoneOpacity + }; + + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); + if (map[stat] === undefined) { + throw new Error('invalid status'); + } + return map[stat]; + }, + + getTextScreenCoords: function() { + return this.getScreenCoords(); + }, + + getAttributes: function() { + var pos = this.getScreenCoords(); + var textPos = this.getTextScreenCoords(); + var opacity = this.getOpacity(); + + return { + circle: { + cx: pos.x, + cy: pos.y, + opacity: opacity, + r: this.getRadius(), + fill: this.getFill(), + 'stroke-width': this.get('stroke-width'), + stroke: this.get('stroke') + }, + text: { + x: textPos.x, + y: textPos.y, + opacity: opacity + } + }; + }, + + highlightTo: function(visObj, speed, easing) { + // a small function to highlight the color of a node for demonstration purposes + var color = visObj.get('fill'); + + var attr = { + circle: { + fill: color, + stroke: color, + 'stroke-width': this.get('stroke-width') * 5 + }, + text: {} + }; + + this.animateToAttr(attr, speed, easing); + }, + + animateUpdatedPosition: function(speed, easing) { + var attr = this.getAttributes(); + this.animateToAttr(attr, speed, easing); + }, + + animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { + // an animation of 0 is essentially setting the attribute directly + this.animateToAttr(fromAttr, 0); + this.animateToAttr(toAttr, speed, easing); + }, + + animateToSnapshot: function(snapShot, speed, easing) { + if (!snapShot[this.getID()]) { + return; + } + this.animateToAttr(snapShot[this.getID()], speed, easing); + }, + + animateToAttr: function(attr, speed, easing) { + if (speed == 0) { + this.get('circle').attr(attr.circle); + this.get('text').attr(attr.text); + return; + } + + var s = speed !== undefined ? speed : this.get('animationSpeed'); + var e = easing || this.get('animationEasing'); + + this.get('circle').stop().animate(attr.circle, s, e); + this.get('text').stop().animate(attr.text, s, e); + + // animate the x attribute without bouncing so it looks like there's + // gravity in only one direction. Just a small animation polish + this.get('circle').animate(attr.circle.cx, s, 'easeInOut'); + this.get('text').animate(attr.text.x, s, 'easeInOut'); + }, + + getScreenCoords: function() { + var pos = this.get('pos'); + return this.gitVisuals.toScreenCoords(pos); + }, + + getRadius: function() { + return this.get('radius') || GRAPHICS.nodeRadius; + }, + + getParentScreenCoords: function() { + return this.get('commit').get('parents')[0].get('visNode').getScreenCoords(); + }, + + setBirthPosition: function() { + // utility method for animating it out from underneath a parent + var parentCoords = this.getParentScreenCoords(); + + this.get('circle').attr({ + cx: parentCoords.x, + cy: parentCoords.y, + opacity: 0, + r: 0, + }); + this.get('text').attr({ + x: parentCoords.x, + y: parentCoords.y, + opacity: 0, + }); + }, + + setBirthFromSnapshot: function(beforeSnapshot) { + // first get parent attribute + // woof bad data access. TODO + var parentID = this.get('commit').get('parents')[0].get('visNode').getID(); + var parentAttr = beforeSnapshot[parentID]; + + // then set myself faded on top of parent + this.get('circle').attr({ + opacity: 0, + r: 0, + cx: parentAttr.circle.cx, + cy: parentAttr.circle.cy + }); + + this.get('text').attr({ + opacity: 0, + x: parentAttr.text.x, + y: parentAttr.text.y + }); + + // then do edges + var parentCoords = { + x: parentAttr.circle.cx, + y: parentAttr.circle.cy + }; + this.setOutgoingEdgesBirthPosition(parentCoords); + }, + + setBirth: function() { + this.setBirthPosition(); + this.setOutgoingEdgesBirthPosition(this.getParentScreenCoords()); + }, + + setOutgoingEdgesOpacity: function(opacity) { + _.each(this.get('outgoingEdges'), function(edge) { + edge.setOpacity(opacity); + }); + }, + + animateOutgoingEdgesToAttr: function(snapShot, speed, easing) { + _.each(this.get('outgoingEdges'), function(edge) { + var attr = snapShot[edge.getID()]; + edge.animateToAttr(attr); + }, this); + }, + + animateOutgoingEdges: function(speed, easing) { + _.each(this.get('outgoingEdges'), function(edge) { + edge.animateUpdatedPath(speed, easing); + }, this); + }, + + animateOutgoingEdgesFromSnapshot: function(snapshot, speed, easing) { + _.each(this.get('outgoingEdges'), function(edge) { + var attr = snapshot[edge.getID()]; + edge.animateToAttr(attr, speed, easing); + }, this); + }, + + setOutgoingEdgesBirthPosition: function(parentCoords) { + + _.each(this.get('outgoingEdges'), function(edge) { + var headPos = edge.get('head').getScreenCoords(); + var path = edge.genSmoothBezierPathStringFromCoords(parentCoords, headPos); + edge.get('path').stop().attr({ + path: path, + opacity: 0 + }); + }, this); + }, + + parentInFront: function() { + // woof! talk about bad data access + this.get('commit').get('parents')[0].get('visNode').toFront(); + }, + + getFontSize: function(str) { + if (str.length < 3) { + return 12; + } else if (str.length < 5) { + return 10; + } else { + return 8; + } + }, + + getFill: function() { + // first get our status, might be easy from this + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); + if (stat == 'head') { + return GRAPHICS.headRectFill; + } else if (stat == 'none') { + return GRAPHICS.orphanNodeFill; + } + + // now we need to get branch hues + + return this.gitVisuals.getBlendedHuesForCommit(this.get('commit')); + return this.get('fill'); + }, + + attachClickHandlers: function() { + var commandStr = 'git show ' + this.get('commit').get('id'); + _.each([this.get('circle'), this.get('text')], function(rObj) { + rObj.click(function() { + events.trigger('processCommandFromEvent', commandStr); + }); + }); + }, + + setOpacity: function(opacity) { + opacity = (opacity === undefined) ? 1 : opacity; + + // set the opacity on my stuff + var keys = ['circle', 'text']; + _.each(keys, function(key) { + this.get(key).attr({ + opacity: opacity + }); + }, this); + }, + + remove: function() { + this.removeKeys(['circle'], ['text']); + // needs a manual removal of text for whatever reason + this.get('text').remove(); + + this.gitVisuals.removeVisNode(this); + }, + + removeAll: function() { + this.remove(); + _.each(this.get('outgoingEdges'), function(edge) { + edge.remove(); + }, this); + }, + + genGraphics: function() { + var paper = this.gitVisuals.paper; + + var pos = this.getScreenCoords(); + var textPos = this.getTextScreenCoords(); + + var circle = paper.circle( + pos.x, + pos.y, + this.getRadius() + ).attr(this.getAttributes().circle); + + var text = paper.text(textPos.x, textPos.y, String(this.get('id'))); + text.attr({ + 'font-size': this.getFontSize(this.get('id')), + 'font-weight': 'bold', + 'font-family': 'Monaco, Courier, font-monospace', + opacity: this.getOpacity() + }); + + this.set('circle', circle); + this.set('text', text); + + this.attachClickHandlers(); + } +}); + +var VisEdge = VisBase.extend({ + defaults: { + tail: null, + head: null, + animationSpeed: GRAPHICS.defaultAnimationTime, + animationEasing: GRAPHICS.defaultEasing + }, + + validateAtInit: function() { + required = ['tail', 'head']; + _.each(required, function(key) { + if (!this.get(key)) { + throw new Error(key + ' is required!'); + } + }, this); + }, + + getID: function() { + return this.get('tail').get('id') + '.' + this.get('head').get('id'); + }, + + initialize: function() { + this.validateAtInit(); + + // shorthand for the main objects + this.gitVisuals = this.get('gitVisuals'); + this.gitEngine = this.get('gitEngine'); + + this.get('tail').get('outgoingEdges').push(this); + }, + + remove: function() { + this.removeKeys(['path']); + this.gitVisuals.removeVisEdge(this); + }, + + genSmoothBezierPathString: function(tail, head) { + var tailPos = tail.getScreenCoords(); + var headPos = head.getScreenCoords(); + return this.genSmoothBezierPathStringFromCoords(tailPos, headPos); + }, + + genSmoothBezierPathStringFromCoords: function(tailPos, headPos) { + // we need to generate the path and control points for the bezier. format + // is M(move abs) C (curve to) (control point 1) (control point 2) (final point) + // the control points have to be __below__ to get the curve starting off straight. + + var coords = function(pos) { + return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); + }; + var offset = function(pos, dir, delta) { + delta = delta || GRAPHICS.curveControlPointOffset; + return { + x: pos.x, + y: pos.y + delta * dir + }; + }; + var offset2d = function(pos, x, y) { + return { + x: pos.x + x, + y: pos.y + y + }; + }; + + // first offset tail and head by radii + tailPos = offset(tailPos, -1, this.get('tail').getRadius()); + headPos = offset(headPos, 1, this.get('head').getRadius()); + + var str = ''; + // first move to bottom of tail + str += 'M' + coords(tailPos) + ' '; + // start bezier + str += 'C'; + // then control points above tail and below head + str += coords(offset(tailPos, -1)) + ' '; + str += coords(offset(headPos, 1)) + ' '; + // now finish + str += coords(headPos); + + // arrow head + var delta = GRAPHICS.arrowHeadSize || 10; + str += ' L' + coords(offset2d(headPos, -delta, delta)); + str += ' L' + coords(offset2d(headPos, delta, delta)); + str += ' L' + coords(headPos); + + // then go back, so we can fill correctly + str += 'C'; + str += coords(offset(headPos, 1)) + ' '; + str += coords(offset(tailPos, -1)) + ' '; + str += coords(tailPos); + + return str; + }, + + getBezierCurve: function() { + return this.genSmoothBezierPathString(this.get('tail'), this.get('head')); + }, + + getStrokeColor: function() { + return GRAPHICS.visBranchStrokeColorNone; + }, + + setOpacity: function(opacity) { + opacity = (opacity === undefined) ? 1 : opacity; + + this.get('path').attr({opacity: opacity}); + }, + + genGraphics: function(paper) { + var pathString = this.getBezierCurve(); + + var path = paper.path(pathString).attr({ + 'stroke-width': GRAPHICS.visBranchStrokeWidth, + 'stroke': this.getStrokeColor(), + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'fill': this.getStrokeColor() + }); + path.toBack(); + this.set('path', path); + }, + + getOpacity: function() { + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail')); + var map = { + 'branch': 1, + 'head': GRAPHICS.edgeUpstreamHeadOpacity, + 'none': GRAPHICS.edgeUpstreamNoneOpacity + }; + + if (map[stat] === undefined) { throw new Error('bad stat'); } + return map[stat]; + }, + + getAttributes: function() { + var newPath = this.getBezierCurve(); + var opacity = this.getOpacity(); + return { + path: { + path: newPath, + opacity: opacity + } + }; + }, + + animateUpdatedPath: function(speed, easing) { + var attr = this.getAttributes(); + this.animateToAttr(attr, speed, easing); + }, + + animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { + // an animation of 0 is essentially setting the attribute directly + this.animateToAttr(fromAttr, 0); + this.animateToAttr(toAttr, speed, easing); + }, + + animateToAttr: function(attr, speed, easing) { + if (speed == 0) { + this.get('path').attr(attr.path); + return; + } + + this.get('path').toBack(); + this.get('path').stop().animate( + attr.path, + speed !== undefined ? speed : this.get('animationSpeed'), + easing || this.get('animationEasing') + ); + }, + +}); + +var VisEdgeCollection = Backbone.Collection.extend({ + model: VisEdge +}); + +var VisBranchCollection = Backbone.Collection.extend({ + model: VisBranch +}); + +exports.VisEdgeCollection = VisEdgeCollection; +exports.VisBranchCollection = VisBranchCollection; +exports.VisNode = VisNode; +exports.VisEdge = VisEdge; +exports.VisBranch = VisBranch; + + + }); require.define("/main.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactory = require('./animationFactory').AnimationFactory; @@ -3514,6 +4548,975 @@ exports.animationFactory = animationFactory; }); require("/main.js"); +require.define("/tree.js",function(require,module,exports,__dirname,__filename,process,global){var randomHueString = function() { + var hue = Math.random(); + var str = 'hsb(' + String(hue) + ',0.7,1)'; + return str; +}; + +var VisBase = Backbone.Model.extend({ + removeKeys: function(keys) { + _.each(keys, function(key) { + if (this.get(key)) { + this.get(key).remove(); + } + }, this); + } +}); + +var VisBranch = VisBase.extend({ + defaults: { + pos: null, + text: null, + rect: null, + arrow: null, + isHead: false, + flip: 1, + + fill: GRAPHICS.rectFill, + stroke: GRAPHICS.rectStroke, + 'stroke-width': GRAPHICS.rectStrokeWidth, + + offsetX: GRAPHICS.nodeRadius * 4.75, + offsetY: 0, + arrowHeight: 14, + arrowInnerSkew: 0, + arrowEdgeHeight: 6, + arrowLength: 14, + arrowOffsetFromCircleX: 10, + + vPad: 5, + hPad: 5, + + animationSpeed: GRAPHICS.defaultAnimationTime, + animationEasing: GRAPHICS.defaultEasing + }, + + validateAtInit: function() { + if (!this.get('branch')) { + throw new Error('need a branch!'); + } + }, + + getFill: function() { + return this.get('fill'); + }, + + getID: function() { + return this.get('branch').get('id'); + }, + + initialize: function() { + this.validateAtInit(); + + // shorthand notation for the main objects + this.gitVisuals = this.get('gitVisuals'); + this.gitEngine = this.get('gitEngine'); + if (!this.gitEngine) { + console.log('throw damnit'); + throw new Error('asd'); + } + + this.get('branch').set('visBranch', this); + var id = this.get('branch').get('id'); + + if (id == 'HEAD') { + // switch to a head ref + this.set('isHead', true); + this.set('flip', -1); + + this.set('fill', GRAPHICS.headRectFill); + } else if (id !== 'master') { + // we need to set our color to something random + this.set('fill', randomHueString()); + } + }, + + getCommitPosition: function() { + var commit = this.gitEngine.getCommitFromRef(this.get('branch')); + var visNode = commit.get('visNode'); + return visNode.getScreenCoords(); + }, + + getBranchStackIndex: function() { + if (this.get('isHead')) { + // head is never stacked with other branches + return 0; + } + + var myArray = this.getBranchStackArray(); + var index = -1; + _.each(myArray, function(branch, i) { + if (branch.obj == this.get('branch')) { + index = i; + } + }, this); + return index; + }, + + getBranchStackLength: function() { + if (this.get('isHead')) { + // head is always by itself + return 1; + } + + return this.getBranchStackArray().length; + }, + + getBranchStackArray: function() { + var arr = this.gitVisuals.branchStackMap[this.get('branch').get('target').get('id')]; + if (arr === undefined) { + // this only occurs when we are generating graphics inside of + // a new Branch instantiation, so we need to force the update + this.gitVisuals.calcBranchStacks(); + return this.getBranchStackArray(); + } + return arr; + }, + + getTextPosition: function() { + var pos = this.getCommitPosition(); + + // then order yourself accordingly. we use alphabetical sorting + // so everything is independent + var myPos = this.getBranchStackIndex(); + return { + x: pos.x + this.get('flip') * this.get('offsetX'), + y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY') + }; + }, + + getRectPosition: function() { + var pos = this.getTextPosition(); + var f = this.get('flip'); + + // first get text width and height + var textSize = this.getTextSize(); + return { + x: pos.x - 0.5 * textSize.w - this.get('hPad'), + y: pos.y - 0.5 * textSize.h - this.get('vPad') + } + }, + + getArrowPath: function() { + // should make these util functions... + var offset2d = function(pos, x, y) { + return { + x: pos.x + x, + y: pos.y + y + }; + }; + var toStringCoords = function(pos) { + return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); + }; + var f = this.get('flip'); + + var arrowTip = offset2d(this.getCommitPosition(), + f * this.get('arrowOffsetFromCircleX'), + 0 + ); + var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight')); + var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight')); + + var arrowInnerUp = offset2d(arrowEdgeUp, + f * this.get('arrowInnerSkew'), + this.get('arrowEdgeHeight') + ); + var arrowInnerLow = offset2d(arrowEdgeLow, + f * this.get('arrowInnerSkew'), + -this.get('arrowEdgeHeight') + ); + + var tailLength = 49; + var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0); + var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0); + + var pathStr = ''; + pathStr += 'M' + toStringCoords(arrowStartUp) + ' '; + var coords = [ + arrowInnerUp, + arrowEdgeUp, + arrowTip, + arrowEdgeLow, + arrowInnerLow, + arrowStartLow + ]; + _.each(coords, function(pos) { + pathStr += 'L' + toStringCoords(pos) + ' '; + }, this); + pathStr += 'z'; + return pathStr; + }, + + getTextSize: function() { + var getTextWidth = function(visBranch) { + var textNode = visBranch.get('text').node; + return (textNode === null) ? 1 : textNode.clientWidth; + }; + + var textNode = this.get('text').node; + if (this.get('isHead')) { + // HEAD is a special case + return { + w: textNode.clientWidth, + h: textNode.clientHeight + }; + } + + var maxWidth = 0; + _.each(this.getBranchStackArray(), function(branch) { + maxWidth = Math.max(maxWidth, getTextWidth( + branch.obj.get('visBranch') + )); + }); + + return { + w: maxWidth, + h: textNode.clientHeight + }; + }, + + getSingleRectSize: function() { + var textSize = this.getTextSize(); + var vPad = this.get('vPad'); + var hPad = this.get('hPad'); + return { + w: textSize.w + vPad * 2, + h: textSize.h + hPad * 2 + }; + }, + + getRectSize: function() { + var textSize = this.getTextSize(); + // enforce padding + var vPad = this.get('vPad'); + var hPad = this.get('hPad'); + + // number of other branch names we are housing + var totalNum = this.getBranchStackLength(); + return { + w: textSize.w + vPad * 2, + h: textSize.h * totalNum * 1.1 + hPad * 2 + }; + }, + + getName: function() { + var name = this.get('branch').get('id'); + var selected = this.gitEngine.HEAD.get('target').get('id'); + + var add = (selected == name) ? '*' : ''; + return name + add; + }, + + nonTextToFront: function() { + this.get('arrow').toFront(); + this.get('rect').toFront(); + }, + + textToFront: function() { + this.get('text').toFront(); + }, + + getFill: function() { + // in the easy case, just return your own fill if you are: + // - the HEAD ref + // - by yourself (length of 1) + // - part of a multi branch, but your thing is hidden + if (this.get('isHead') || + this.getBranchStackLength() == 1 || + this.getBranchStackIndex() != 0) { + return this.get('fill'); + } + + // woof. now it's hard, we need to blend hues... + return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray()); + }, + + remove: function() { + this.removeKeys(['text', 'arrow', 'rect']); + // also need to remove from this.gitVisuals + this.gitVisuals.removeVisBranch(this); + }, + + genGraphics: function(paper) { + var textPos = this.getTextPosition(); + var name = this.getName(); + var text; + + // when from a reload, we dont need to generate the text + text = paper.text(textPos.x, textPos.y, String(name)); + text.attr({ + 'font-size': 14, + 'font-family': 'Monaco, Courier, font-monospace', + opacity: this.getTextOpacity() + }); + this.set('text', text); + + var rectPos = this.getRectPosition(); + var sizeOfRect = this.getRectSize(); + var rect = paper + .rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8) + .attr(this.getAttributes().rect); + this.set('rect', rect); + + var arrowPath = this.getArrowPath(); + var arrow = paper + .path(arrowPath) + .attr(this.getAttributes().arrow); + this.set('arrow', arrow); + + rect.toFront(); + text.toFront(); + }, + + updateName: function() { + this.get('text').attr({ + text: this.getName() + }); + }, + + getNonTextOpacity: function() { + if (this.get('isHead')) { + return this.gitEngine.getDetachedHead() ? 1 : 0; + } + return this.getBranchStackIndex() == 0 ? 1 : 0.0; + }, + + getTextOpacity: function() { + if (this.get('isHead')) { + return this.gitEngine.getDetachedHead() ? 1 : 0; + } + return 1; + }, + + getAttributes: function() { + var nonTextOpacity = this.getNonTextOpacity(); + var textOpacity = this.getTextOpacity(); + this.updateName(); + + var textPos = this.getTextPosition(); + var rectPos = this.getRectPosition(); + var rectSize = this.getRectSize(); + + var arrowPath = this.getArrowPath(); + + return { + text: { + x: textPos.x, + y: textPos.y, + opacity: textOpacity + }, + rect: { + x: rectPos.x, + y: rectPos.y, + width: rectSize.w, + height: rectSize.h, + opacity: nonTextOpacity, + fill: this.getFill(), + stroke: this.get('stroke'), + 'stroke-width': this.get('stroke-width') + }, + arrow: { + path: arrowPath, + opacity: nonTextOpacity, + fill: this.getFill(), + stroke: this.get('stroke'), + 'stroke-width': this.get('stroke-width') + } + }; + }, + + animateUpdatedPos: function(speed, easing) { + var attr = this.getAttributes(); + this.animateToAttr(attr, speed, easing); + }, + + animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { + // an animation of 0 is essentially setting the attribute directly + this.animateToAttr(fromAttr, 0); + this.animateToAttr(toAttr, speed, easing); + }, + + animateToAttr: function(attr, speed, easing) { + if (speed == 0) { + this.get('text').attr(attr.text); + this.get('rect').attr(attr.rect); + this.get('arrow').attr(attr.arrow); + return; + } + + var s = speed !== undefined ? speed : this.get('animationSpeed'); + var e = easing || this.get('animationEasing'); + + this.get('text').stop().animate(attr.text, s, e); + this.get('rect').stop().animate(attr.rect, s, e); + this.get('arrow').stop().animate(attr.arrow, s, e); + } +}); + + +var VisNode = VisBase.extend({ + defaults: { + depth: undefined, + maxWidth: null, + outgoingEdges: null, + + circle: null, + text: null, + + id: null, + pos: null, + radius: null, + + commit: null, + animationSpeed: GRAPHICS.defaultAnimationTime, + animationEasing: GRAPHICS.defaultEasing, + + fill: GRAPHICS.defaultNodeFill, + 'stroke-width': GRAPHICS.defaultNodeStrokeWidth, + stroke: GRAPHICS.defaultNodeStroke + }, + + getID: function() { + return this.get('id'); + }, + + validateAtInit: function() { + if (!this.get('id')) { + throw new Error('need id for mapping'); + } + if (!this.get('commit')) { + throw new Error('need commit for linking'); + } + + if (!this.get('pos')) { + this.set('pos', { + x: Math.random(), + y: Math.random() + }); + } + }, + + initialize: function() { + this.validateAtInit(); + // shorthand for the main objects + this.gitVisuals = this.get('gitVisuals'); + this.gitEngine = this.get('gitEngine'); + + this.set('outgoingEdges', []); + }, + + setDepth: function(depth) { + // for merge commits we need to max the depths across all + this.set('depth', Math.max(this.get('depth') || 0, depth)); + }, + + setDepthBasedOn: function(depthIncrement) { + if (this.get('depth') === undefined) { + debugger + throw new Error('no depth yet!'); + } + var pos = this.get('pos'); + pos.y = this.get('depth') * depthIncrement; + }, + + getMaxWidthScaled: function() { + // returns our max width scaled based on if we are visible + // from a branch or not + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); + var map = { + branch: 1, + head: 0.3, + none: 0.1 + }; + if (map[stat] === undefined) { throw new Error('bad stat'); } + return map[stat] * this.get('maxWidth'); + }, + + toFront: function() { + this.get('circle').toFront(); + this.get('text').toFront(); + }, + + getOpacity: function() { + var map = { + 'branch': 1, + 'head': GRAPHICS.upstreamHeadOpacity, + 'none': GRAPHICS.upstreamNoneOpacity + }; + + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); + if (map[stat] === undefined) { + throw new Error('invalid status'); + } + return map[stat]; + }, + + getTextScreenCoords: function() { + return this.getScreenCoords(); + }, + + getAttributes: function() { + var pos = this.getScreenCoords(); + var textPos = this.getTextScreenCoords(); + var opacity = this.getOpacity(); + + return { + circle: { + cx: pos.x, + cy: pos.y, + opacity: opacity, + r: this.getRadius(), + fill: this.getFill(), + 'stroke-width': this.get('stroke-width'), + stroke: this.get('stroke') + }, + text: { + x: textPos.x, + y: textPos.y, + opacity: opacity + } + }; + }, + + highlightTo: function(visObj, speed, easing) { + // a small function to highlight the color of a node for demonstration purposes + var color = visObj.get('fill'); + + var attr = { + circle: { + fill: color, + stroke: color, + 'stroke-width': this.get('stroke-width') * 5 + }, + text: {} + }; + + this.animateToAttr(attr, speed, easing); + }, + + animateUpdatedPosition: function(speed, easing) { + var attr = this.getAttributes(); + this.animateToAttr(attr, speed, easing); + }, + + animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { + // an animation of 0 is essentially setting the attribute directly + this.animateToAttr(fromAttr, 0); + this.animateToAttr(toAttr, speed, easing); + }, + + animateToSnapshot: function(snapShot, speed, easing) { + if (!snapShot[this.getID()]) { + return; + } + this.animateToAttr(snapShot[this.getID()], speed, easing); + }, + + animateToAttr: function(attr, speed, easing) { + if (speed == 0) { + this.get('circle').attr(attr.circle); + this.get('text').attr(attr.text); + return; + } + + var s = speed !== undefined ? speed : this.get('animationSpeed'); + var e = easing || this.get('animationEasing'); + + this.get('circle').stop().animate(attr.circle, s, e); + this.get('text').stop().animate(attr.text, s, e); + + // animate the x attribute without bouncing so it looks like there's + // gravity in only one direction. Just a small animation polish + this.get('circle').animate(attr.circle.cx, s, 'easeInOut'); + this.get('text').animate(attr.text.x, s, 'easeInOut'); + }, + + getScreenCoords: function() { + var pos = this.get('pos'); + return this.gitVisuals.toScreenCoords(pos); + }, + + getRadius: function() { + return this.get('radius') || GRAPHICS.nodeRadius; + }, + + getParentScreenCoords: function() { + return this.get('commit').get('parents')[0].get('visNode').getScreenCoords(); + }, + + setBirthPosition: function() { + // utility method for animating it out from underneath a parent + var parentCoords = this.getParentScreenCoords(); + + this.get('circle').attr({ + cx: parentCoords.x, + cy: parentCoords.y, + opacity: 0, + r: 0, + }); + this.get('text').attr({ + x: parentCoords.x, + y: parentCoords.y, + opacity: 0, + }); + }, + + setBirthFromSnapshot: function(beforeSnapshot) { + // first get parent attribute + // woof bad data access. TODO + var parentID = this.get('commit').get('parents')[0].get('visNode').getID(); + var parentAttr = beforeSnapshot[parentID]; + + // then set myself faded on top of parent + this.get('circle').attr({ + opacity: 0, + r: 0, + cx: parentAttr.circle.cx, + cy: parentAttr.circle.cy + }); + + this.get('text').attr({ + opacity: 0, + x: parentAttr.text.x, + y: parentAttr.text.y + }); + + // then do edges + var parentCoords = { + x: parentAttr.circle.cx, + y: parentAttr.circle.cy + }; + this.setOutgoingEdgesBirthPosition(parentCoords); + }, + + setBirth: function() { + this.setBirthPosition(); + this.setOutgoingEdgesBirthPosition(this.getParentScreenCoords()); + }, + + setOutgoingEdgesOpacity: function(opacity) { + _.each(this.get('outgoingEdges'), function(edge) { + edge.setOpacity(opacity); + }); + }, + + animateOutgoingEdgesToAttr: function(snapShot, speed, easing) { + _.each(this.get('outgoingEdges'), function(edge) { + var attr = snapShot[edge.getID()]; + edge.animateToAttr(attr); + }, this); + }, + + animateOutgoingEdges: function(speed, easing) { + _.each(this.get('outgoingEdges'), function(edge) { + edge.animateUpdatedPath(speed, easing); + }, this); + }, + + animateOutgoingEdgesFromSnapshot: function(snapshot, speed, easing) { + _.each(this.get('outgoingEdges'), function(edge) { + var attr = snapshot[edge.getID()]; + edge.animateToAttr(attr, speed, easing); + }, this); + }, + + setOutgoingEdgesBirthPosition: function(parentCoords) { + + _.each(this.get('outgoingEdges'), function(edge) { + var headPos = edge.get('head').getScreenCoords(); + var path = edge.genSmoothBezierPathStringFromCoords(parentCoords, headPos); + edge.get('path').stop().attr({ + path: path, + opacity: 0 + }); + }, this); + }, + + parentInFront: function() { + // woof! talk about bad data access + this.get('commit').get('parents')[0].get('visNode').toFront(); + }, + + getFontSize: function(str) { + if (str.length < 3) { + return 12; + } else if (str.length < 5) { + return 10; + } else { + return 8; + } + }, + + getFill: function() { + // first get our status, might be easy from this + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit')); + if (stat == 'head') { + return GRAPHICS.headRectFill; + } else if (stat == 'none') { + return GRAPHICS.orphanNodeFill; + } + + // now we need to get branch hues + + return this.gitVisuals.getBlendedHuesForCommit(this.get('commit')); + return this.get('fill'); + }, + + attachClickHandlers: function() { + var commandStr = 'git show ' + this.get('commit').get('id'); + _.each([this.get('circle'), this.get('text')], function(rObj) { + rObj.click(function() { + events.trigger('processCommandFromEvent', commandStr); + }); + }); + }, + + setOpacity: function(opacity) { + opacity = (opacity === undefined) ? 1 : opacity; + + // set the opacity on my stuff + var keys = ['circle', 'text']; + _.each(keys, function(key) { + this.get(key).attr({ + opacity: opacity + }); + }, this); + }, + + remove: function() { + this.removeKeys(['circle'], ['text']); + // needs a manual removal of text for whatever reason + this.get('text').remove(); + + this.gitVisuals.removeVisNode(this); + }, + + removeAll: function() { + this.remove(); + _.each(this.get('outgoingEdges'), function(edge) { + edge.remove(); + }, this); + }, + + genGraphics: function() { + var paper = this.gitVisuals.paper; + + var pos = this.getScreenCoords(); + var textPos = this.getTextScreenCoords(); + + var circle = paper.circle( + pos.x, + pos.y, + this.getRadius() + ).attr(this.getAttributes().circle); + + var text = paper.text(textPos.x, textPos.y, String(this.get('id'))); + text.attr({ + 'font-size': this.getFontSize(this.get('id')), + 'font-weight': 'bold', + 'font-family': 'Monaco, Courier, font-monospace', + opacity: this.getOpacity() + }); + + this.set('circle', circle); + this.set('text', text); + + this.attachClickHandlers(); + } +}); + +var VisEdge = VisBase.extend({ + defaults: { + tail: null, + head: null, + animationSpeed: GRAPHICS.defaultAnimationTime, + animationEasing: GRAPHICS.defaultEasing + }, + + validateAtInit: function() { + required = ['tail', 'head']; + _.each(required, function(key) { + if (!this.get(key)) { + throw new Error(key + ' is required!'); + } + }, this); + }, + + getID: function() { + return this.get('tail').get('id') + '.' + this.get('head').get('id'); + }, + + initialize: function() { + this.validateAtInit(); + + // shorthand for the main objects + this.gitVisuals = this.get('gitVisuals'); + this.gitEngine = this.get('gitEngine'); + + this.get('tail').get('outgoingEdges').push(this); + }, + + remove: function() { + this.removeKeys(['path']); + this.gitVisuals.removeVisEdge(this); + }, + + genSmoothBezierPathString: function(tail, head) { + var tailPos = tail.getScreenCoords(); + var headPos = head.getScreenCoords(); + return this.genSmoothBezierPathStringFromCoords(tailPos, headPos); + }, + + genSmoothBezierPathStringFromCoords: function(tailPos, headPos) { + // we need to generate the path and control points for the bezier. format + // is M(move abs) C (curve to) (control point 1) (control point 2) (final point) + // the control points have to be __below__ to get the curve starting off straight. + + var coords = function(pos) { + return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); + }; + var offset = function(pos, dir, delta) { + delta = delta || GRAPHICS.curveControlPointOffset; + return { + x: pos.x, + y: pos.y + delta * dir + }; + }; + var offset2d = function(pos, x, y) { + return { + x: pos.x + x, + y: pos.y + y + }; + }; + + // first offset tail and head by radii + tailPos = offset(tailPos, -1, this.get('tail').getRadius()); + headPos = offset(headPos, 1, this.get('head').getRadius()); + + var str = ''; + // first move to bottom of tail + str += 'M' + coords(tailPos) + ' '; + // start bezier + str += 'C'; + // then control points above tail and below head + str += coords(offset(tailPos, -1)) + ' '; + str += coords(offset(headPos, 1)) + ' '; + // now finish + str += coords(headPos); + + // arrow head + var delta = GRAPHICS.arrowHeadSize || 10; + str += ' L' + coords(offset2d(headPos, -delta, delta)); + str += ' L' + coords(offset2d(headPos, delta, delta)); + str += ' L' + coords(headPos); + + // then go back, so we can fill correctly + str += 'C'; + str += coords(offset(headPos, 1)) + ' '; + str += coords(offset(tailPos, -1)) + ' '; + str += coords(tailPos); + + return str; + }, + + getBezierCurve: function() { + return this.genSmoothBezierPathString(this.get('tail'), this.get('head')); + }, + + getStrokeColor: function() { + return GRAPHICS.visBranchStrokeColorNone; + }, + + setOpacity: function(opacity) { + opacity = (opacity === undefined) ? 1 : opacity; + + this.get('path').attr({opacity: opacity}); + }, + + genGraphics: function(paper) { + var pathString = this.getBezierCurve(); + + var path = paper.path(pathString).attr({ + 'stroke-width': GRAPHICS.visBranchStrokeWidth, + 'stroke': this.getStrokeColor(), + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'fill': this.getStrokeColor() + }); + path.toBack(); + this.set('path', path); + }, + + getOpacity: function() { + var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail')); + var map = { + 'branch': 1, + 'head': GRAPHICS.edgeUpstreamHeadOpacity, + 'none': GRAPHICS.edgeUpstreamNoneOpacity + }; + + if (map[stat] === undefined) { throw new Error('bad stat'); } + return map[stat]; + }, + + getAttributes: function() { + var newPath = this.getBezierCurve(); + var opacity = this.getOpacity(); + return { + path: { + path: newPath, + opacity: opacity + } + }; + }, + + animateUpdatedPath: function(speed, easing) { + var attr = this.getAttributes(); + this.animateToAttr(attr, speed, easing); + }, + + animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { + // an animation of 0 is essentially setting the attribute directly + this.animateToAttr(fromAttr, 0); + this.animateToAttr(toAttr, speed, easing); + }, + + animateToAttr: function(attr, speed, easing) { + if (speed == 0) { + this.get('path').attr(attr.path); + return; + } + + this.get('path').toBack(); + this.get('path').stop().animate( + attr.path, + speed !== undefined ? speed : this.get('animationSpeed'), + easing || this.get('animationEasing') + ); + }, + +}); + +var VisEdgeCollection = Backbone.Collection.extend({ + model: VisEdge +}); + +var VisBranchCollection = Backbone.Collection.extend({ + model: VisBranch +}); + +exports.VisEdgeCollection = VisEdgeCollection; +exports.VisBranchCollection = VisBranchCollection; +exports.VisNode = VisNode; +exports.VisEdge = VisEdge; +exports.VisBranch = VisBranch; + + + +}); +require("/tree.js"); + require.define("/animationFactory.js",function(require,module,exports,__dirname,__filename,process,global){/****************** * This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time. * The tricky thing is that when a new commit has to be "born," say in the middle of a rebase @@ -3771,7 +5774,8 @@ exports.AnimationFactory = AnimationFactory; }); require("/animationFactory.js"); -require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var animationFactory = new require('./animationFactory').AnimationFactory(); +require.define("/git.js",function(require,module,exports,__dirname,__filename,process,global){var AnimationFactoryModule = require('./animationFactory'); +var animationFactory = new AnimationFactoryModule.AnimationFactory(); // backbone or something uses _.uniqueId, so we make our own here var uniqueId = (function() { @@ -5410,6 +7414,7 @@ require("/git.js"); require.define("/collections.js",function(require,module,exports,__dirname,__filename,process,global){var Commit = require('./git').Commit; var Branch = require('./git').Branch; +var events = require('./main').events; var CommitCollection = Backbone.Collection.extend({ model: Commit @@ -5901,10 +7906,19 @@ exports.CommandLineHistoryView = CommandLineHistoryView; }); require("/commandViews.js"); -require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var CommitCollection = require('./collections').CommitCollection; -var BranchCollection = require('./collections').BranchCollection; +require.define("/visuals.js",function(require,module,exports,__dirname,__filename,process,global){var Collections = require('./collections'); +var CommitCollection = Collections.CommitCollection; +var BranchCollection = Collections.BranchCollection; + var GitEngine = require('./git').GitEngine; +var Tree = require('./tree'); +var VisEdgeCollection = Tree.VisEdgeCollection; +var VisBranchCollection = Tree.VisBranchCollection; +var VisNode = Tree.VisNode; +var VisBranch = Tree.VisBranch; +var VisEdge = Tree.VisEdge; + var Visualization = Backbone.View.extend({ initialize: function(options) { var _this = this; @@ -6568,12 +8582,6 @@ function blendHueStrings(hueStrings) { return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')'; } -function randomHueString() { - var hue = Math.random(); - var str = 'hsb(' + String(hue) + ',0.7,1)'; - return str; -}; - exports.Visualization = Visualization; diff --git a/src/collections.js b/src/collections.js index 39007a55..ce600958 100644 --- a/src/collections.js +++ b/src/collections.js @@ -1,5 +1,6 @@ var Commit = require('./git').Commit; var Branch = require('./git').Branch; +var events = require('./main').events; var CommitCollection = Backbone.Collection.extend({ model: Commit diff --git a/src/git.js b/src/git.js index 95cd4afc..01071687 100644 --- a/src/git.js +++ b/src/git.js @@ -1,5 +1,5 @@ -var animationFactory = new require('./animationFactory').AnimationFactory(); -console.log('this is what animatioinf actory is', require('./animationFactory')); +var AnimationFactoryModule = require('./animationFactory'); +var animationFactory = new AnimationFactoryModule.AnimationFactory(); // backbone or something uses _.uniqueId, so we make our own here var uniqueId = (function() { diff --git a/src/index.html b/src/index.html index 08de639d..ef673f1c 100644 --- a/src/index.html +++ b/src/index.html @@ -144,7 +144,7 @@ - + @@ -153,9 +153,6 @@ - diff --git a/src/tree.js b/src/tree.js index dbb1d39a..9f589852 100644 --- a/src/tree.js +++ b/src/tree.js @@ -1,3 +1,9 @@ +var randomHueString = function() { + var hue = Math.random(); + var str = 'hsb(' + String(hue) + ',0.7,1)'; + return str; +}; + var VisBase = Backbone.Model.extend({ removeKeys: function(keys) { _.each(keys, function(key) { @@ -949,3 +955,11 @@ var VisEdgeCollection = Backbone.Collection.extend({ var VisBranchCollection = Backbone.Collection.extend({ model: VisBranch }); + +exports.VisEdgeCollection = VisEdgeCollection; +exports.VisBranchCollection = VisBranchCollection; +exports.VisNode = VisNode; +exports.VisEdge = VisEdge; +exports.VisBranch = VisBranch; + + diff --git a/src/visuals.js b/src/visuals.js index 17dc2ddd..09225ae6 100644 --- a/src/visuals.js +++ b/src/visuals.js @@ -1,7 +1,16 @@ -var CommitCollection = require('./collections').CommitCollection; -var BranchCollection = require('./collections').BranchCollection; +var Collections = require('./collections'); +var CommitCollection = Collections.CommitCollection; +var BranchCollection = Collections.BranchCollection; + var GitEngine = require('./git').GitEngine; +var Tree = require('./tree'); +var VisEdgeCollection = Tree.VisEdgeCollection; +var VisBranchCollection = Tree.VisBranchCollection; +var VisNode = Tree.VisNode; +var VisBranch = Tree.VisBranch; +var VisEdge = Tree.VisEdge; + var Visualization = Backbone.View.extend({ initialize: function(options) { var _this = this; @@ -665,11 +674,5 @@ function blendHueStrings(hueStrings) { return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')'; } -function randomHueString() { - var hue = Math.random(); - var str = 'hsb(' + String(hue) + ',0.7,1)'; - return str; -}; - exports.Visualization = Visualization;