pcottle.learnGitBranching/src/tree.js
2012-09-28 14:49:35 -07:00

431 lines
11 KiB
JavaScript

var VisBranch = Backbone.Model.extend({
defaults: {
pos: null,
text: null,
rect: null,
arrow: null,
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!');
}
},
initialize: function() {
this.validateAtInit();
},
getCommitPosition: function() {
var commit = this.get('branch').get('target');
var visNode = commit.get('visNode');
return visNode.getScreenCoords();
},
getBranchStackIndex: function() {
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() {
return this.getBranchStackArray().length;
},
getBranchStackArray: function() {
return gitVisuals.branchStackMap[this.get('branch').get('target').get('id')];
},
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('offsetX'),
y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY')
};
},
getRectPosition: function() {
var pos = this.getTextPosition();
// 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 arrowTip = offset2d(this.getCommitPosition(),
this.get('arrowOffsetFromCircleX'),
0
);
var arrowEdgeUp = offset2d(arrowTip, this.get('arrowLength'), -this.get('arrowHeight'));
var arrowEdgeLow = offset2d(arrowTip, this.get('arrowLength'), this.get('arrowHeight'));
var arrowInnerUp = offset2d(arrowEdgeUp,
this.get('arrowInnerSkew'),
this.get('arrowEdgeHeight')
);
var arrowInnerLow = offset2d(arrowEdgeLow,
this.get('arrowInnerSkew'),
-this.get('arrowEdgeHeight')
);
var tailLength = 30;
var arrowStartUp = offset2d(arrowInnerUp, tailLength, 0);
var arrowStartLow = offset2d(arrowInnerLow, 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.clientWidth;
};
var textNode = this.get('text').node;
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 + hPad * 2
};
},
getName: function() {
var name = this.get('branch').get('id');
var selected = 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();
},
genGraphics: function(paper) {
var textPos = this.getTextPosition();
var name = this.getName();
var text = paper.text(textPos.x, textPos.y, String(name));
text.attr({
'font-size': 14,
'font-family': 'Monaco, Courier, font-monospace'
});
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);
rect.attr({
fill: GRAPHICS.rectFill,
stroke: GRAPHICS.rectStroke,
'stroke-width': GRAPHICS.rectStrokeWidth
});
this.set('rect', rect);
var arrowPath = this.getArrowPath();
var arrow = paper.path(arrowPath);
arrow.attr({
fill: GRAPHICS.rectFill,
stroke: GRAPHICS.rectStroke,
'stroke-width': GRAPHICS.rectStrokeWidth
});
this.set('arrow', arrow);
rect.toFront();
text.toFront();
},
updateName: function() {
this.get('text').attr({
text: this.getName()
});
},
animateUpdatedPos: function(speed, easing) {
var s = speed !== undefined ? speed : this.get('animationSpeed');
var e = easing || this.get('animationEasing');
var masterOpacity = this.getBranchStackIndex() == 0 ? 1 : 0.0;
this.updateName();
var textPos = this.getTextPosition();
this.get('text').stop().animate({
x: textPos.x,
y: textPos.y
}, s, e);
var rectPos = this.getRectPosition();
var rectSize = this.getRectSize();
this.get('rect').stop().animate({
x: rectPos.x,
y: rectPos.y,
width: rectSize.w,
height: rectSize.h,
opacity: masterOpacity,
}, s, e);
var arrowPath = this.getArrowPath();
this.get('arrow').stop().animate({
path: arrowPath,
opacity: masterOpacity
}, s, e);
}
});
var VisNode = Backbone.Model.extend({
defaults: {
depth: undefined,
id: null,
pos: null,
radius: null,
commit: null,
animationSpeed: GRAPHICS.defaultAnimationTime,
animationEasing: GRAPHICS.defaultEasing
},
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();
},
setDepthBasedOn: function(depthIncrement) {
if (this.get('depth') === undefined) {
throw new Error('no depth yet!');
}
var pos = this.get('pos');
pos.y = this.get('depth') * depthIncrement;
},
toFront: function() {
this.get('circle').toFront();
},
animateUpdatedPosition: function(speed, easing) {
var pos = this.getScreenCoords();
this.get('circle').stop().animate({
cx: pos.x,
cy: pos.y
},
speed !== undefined ? speed : this.get('animationSpeed'),
easing || this.get('animationEasing')
);
},
getScreenCoords: function() {
var pos = this.get('pos');
return gitVisuals.toScreenCoords(pos);
},
getRadius: function() {
return this.get('radius') || GRAPHICS.nodeRadius;
},
genGraphics: function(paper) {
var pos = this.getScreenCoords();
var circle = cuteSmallCircle(paper, pos.x, pos.y, {
radius: this.getRadius()
});
this.set('circle', circle);
}
});
var VisEdge = Backbone.Model.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);
},
initialize: function() {
this.validateAtInit();
},
genSmoothBezierPathString: function(tail, head) {
var tailPos = tail.getScreenCoords();
var headPos = head.getScreenCoords();
// 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, tail.getRadius());
headPos = offset(headPos, 1, 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
// TODO default sizing
var delta = GRAPHICS.arrowHeadSize || 10;
str += ' L' + coords(offset2d(headPos, -delta, delta));
str += ' L' + coords(offset2d(headPos, delta, delta));
str += ' L' + coords(headPos);
return str;
},
getBezierCurve: function() {
return this.genSmoothBezierPathString(this.get('tail'), this.get('head'));
},
genGraphics: function(paper) {
var pathString = this.getBezierCurve();
var path = cutePath(paper, pathString);
path.toBack();
this.set('path', path);
},
animateUpdatedPath: function(speed, easing) {
var newPath = this.getBezierCurve();
this.get('path').toBack();
this.get('path').stop().animate({
path: newPath
},
speed !== undefined ? speed : this.get('animationSpeed'),
easing || this.get('animationEasing')
);
},
});
var VisEdgeCollection = Backbone.Collection.extend({
model: VisEdge
});
var VisBranchCollection = Backbone.Collection.extend({
model: VisBranch
});