Skip to content

Instantly share code, notes, and snippets.

@eugenkiss
Created September 20, 2012 20:22
Show Gist options
  • Save eugenkiss/3758128 to your computer and use it in GitHub Desktop.
Save eugenkiss/3758128 to your computer and use it in GitHub Desktop.
Hacky, special case curly brace connector for jsPlumb
/******************* Curly Brace Connector *******************/
jsPlumb.Connectors.CurlyBrace = function(params) {
var self = this;
params = params || {};
this.majorAnchor = params.curviness || 150;
this.minorAnchor = 10;
var currentPoints = null;
this.type = "CurlyBrace";
this._findControlPoint = function(point, sourceAnchorPosition, targetAnchorPosition, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor) {
// determine if the two anchors are perpendicular to each other in their orientation. we swap the control
// points around if so (code could be tightened up)
var soo = sourceAnchor.getOrientation(sourceEndpoint),
too = targetAnchor.getOrientation(targetEndpoint),
perpendicular = soo[0] != too[0] || soo[1] == too[1],
p = [],
ma = self.majorAnchor, mi = self.minorAnchor;
if (!perpendicular) {
if (soo[0] == 0) // X
p.push(sourceAnchorPosition[0] < targetAnchorPosition[0] ? point[0] + mi : point[0] - mi);
else p.push(point[0] - (ma * soo[0]));
if (soo[1] == 0) // Y
p.push(sourceAnchorPosition[1] < targetAnchorPosition[1] ? point[1] + mi : point[1] - mi);
else p.push(point[1] + (ma * too[1]));
}
else {
if (too[0] == 0) // X
p.push(targetAnchorPosition[0] < sourceAnchorPosition[0] ? point[0] + mi : point[0] - mi);
else p.push(point[0] + (ma * too[0]));
if (too[1] == 0) // Y
p.push(targetAnchorPosition[1] < sourceAnchorPosition[1] ? point[1] + mi : point[1] - mi);
else p.push(point[1] + (ma * soo[1]));
}
return p;
};
var _CP, _CP2, _sx, _tx, _ty, _sx, _sy, _canvasX, _canvasY, _w, _h, _sStubX, _sStubY, _tStubX, _tStubY;
this.compute = function(sourcePos, targetPos, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor, lineWidth, minWidth) {
lineWidth = lineWidth || 0;
_w = Math.abs(sourcePos[0] - targetPos[0]) + lineWidth;
_h = Math.abs(sourcePos[1] - targetPos[1]) + lineWidth;
_canvasX = Math.min(sourcePos[0], targetPos[0])-(lineWidth/2);
_canvasY = Math.min(sourcePos[1], targetPos[1])-(lineWidth/2);
_sx = sourcePos[0] < targetPos[0] ? _w - (lineWidth/2): (lineWidth/2);
_sy = sourcePos[1] < targetPos[1] ? _h - (lineWidth/2) : (lineWidth/2);
_tx = sourcePos[0] < targetPos[0] ? (lineWidth/2) : _w - (lineWidth/2);
_ty = sourcePos[1] < targetPos[1] ? (lineWidth/2) : _h - (lineWidth/2);
_CP = self._findControlPoint([_sx,_sy], sourcePos, targetPos, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor);
_CP2 = self._findControlPoint([_tx,_ty], targetPos, sourcePos, targetEndpoint, sourceEndpoint, targetAnchor, sourceAnchor);
var minx1 = Math.min(_sx,_tx), minx2 = Math.min(_CP[0], _CP2[0]), minx = Math.min(minx1,minx2),
maxx1 = Math.max(_sx,_tx), maxx2 = Math.max(_CP[0], _CP2[0]), maxx = Math.max(maxx1,maxx2);
if (maxx > _w) _w = maxx;
if (minx < 0) {
_canvasX += minx; var ox = Math.abs(minx);
_w += ox; _CP[0] += ox; _sx += ox; _tx +=ox; _CP2[0] += ox;
}
var miny1 = Math.min(_sy,_ty), miny2 = Math.min(_CP[1], _CP2[1]), miny = Math.min(miny1,miny2),
maxy1 = Math.max(_sy,_ty), maxy2 = Math.max(_CP[1], _CP2[1]), maxy = Math.max(maxy1,maxy2);
if (maxy > _h) _h = maxy;
if (miny < 0) {
_canvasY += miny; var oy = Math.abs(miny);
_h += oy; _CP[1] += oy; _sy += oy; _ty +=oy; _CP2[1] += oy;
}
if (minWidth && _w < minWidth) {
var posAdjust = (minWidth - _w) / 2;
_w = minWidth;
_canvasX -= posAdjust; _sx = _sx + posAdjust ; _tx = _tx + posAdjust; _CP[0] = _CP[0] + posAdjust; _CP2[0] = _CP2[0] + posAdjust;
}
if (minWidth && _h < minWidth) {
var posAdjust = (minWidth - _h) / 2;
_h = minWidth;
_canvasY -= posAdjust; _sy = _sy + posAdjust ; _ty = _ty + posAdjust; _CP[1] = _CP[1] + posAdjust; _CP2[1] = _CP2[1] + posAdjust;
}
currentPoints = [_canvasX, _canvasY, _w, _h,
_sx, _sy, _tx, _ty,
_CP[0], _CP[1], _CP2[0], _CP2[1] ];
return currentPoints;
};
var _makeCurve = function() {
return [
{ x:_sx, y:_sy },
{ x:_CP[0], y:_CP[1] },
{ x:_CP2[0], y:_CP2[1] },
{ x:_tx, y:_ty }
];
};
var _translateLocation = function(curve, location, absolute) {
if (absolute)
location = jsBezier.locationAlongCurveFrom(curve, location > 0 ? 0 : 1, location);
return location;
};
/**
* returns the point on the connector's path that is 'location' along the length of the path, where 'location' is a decimal from
* 0 to 1 inclusive. for the straight line connector this is simple maths. for Bezier, not so much.
*/
this.pointOnPath = function(location, absolute) {
var c = _makeCurve();
location = _translateLocation(c, location, absolute);
return jsBezier.pointOnCurve(c, location);
};
/**
* returns the gradient of the connector at the given point.
*/
this.gradientAtPoint = function(location, absolute) {
var c = _makeCurve();
location = _translateLocation(c, location, absolute);
return jsBezier.gradientAtPoint(c, location);
};
/**
* for Bezier curves this method is a little tricky, cos calculating path distance algebraically is notoriously difficult.
* this method is iterative, jumping forward .05% of the path at a time and summing the distance between this point and the previous
* one, until the sum reaches 'distance'. the method may turn out to be computationally expensive; we'll see.
* another drawback of this method is that if the connector gets quite long, .05% of the length of it is not necessarily smaller
* than the desired distance, in which case the loop returns immediately and the arrow is mis-shapen. so a better strategy might be to
* calculate the step as a function of distance/distance between endpoints.
*/
this.pointAlongPathFrom = function(location, distance, absolute) {
var c = _makeCurve();
location = _translateLocation(c, location, absolute);
return jsBezier.pointAlongCurveFrom(c, location, distance);
};
};
// TODO refactor to renderer common script. put a ref to jsPlumb.sizeCanvas in there too.
var _connectionBeingDragged = null,
_hasClass = function(el, clazz) { return jsPlumb.CurrentLibrary.hasClass(_getElementObject(el), clazz); },
_getElementObject = function(el) { return jsPlumb.CurrentLibrary.getElementObject(el); },
_getOffset = function(el) { return jsPlumb.CurrentLibrary.getOffset(_getElementObject(el)); },
_pageXY = function(el) { return jsPlumb.CurrentLibrary.getPageXY(el); },
_clientXY = function(el) { return jsPlumb.CurrentLibrary.getClientXY(el); };
/*
* Class:CanvasMouseAdapter
* Provides support for mouse events on canvases.
*/
var CanvasMouseAdapter = function() {
var self = this;
self.overlayPlacements = [];
jsPlumb.jsPlumbUIComponent.apply(this, arguments);
jsPlumbUtil.EventGenerator.apply(this, arguments);
/**
* returns whether or not the given event is ojver a painted area of the canvas.
*/
this._over = function(e) {
var o = _getOffset(_getElementObject(self.canvas)),
pageXY = _pageXY(e),
x = pageXY[0] - o.left, y = pageXY[1] - o.top;
if (x > 0 && y > 0 && x < self.canvas.width && y < self.canvas.height) {
// first check overlays
for ( var i = 0; i < self.overlayPlacements.length; i++) {
var p = self.overlayPlacements[i];
if (p && (p[0] <= x && p[1] >= x && p[2] <= y && p[3] >= y))
return true;
}
// then the canvas
var d = self.canvas.getContext("2d").getImageData(parseInt(x), parseInt(y), 1, 1);
return d.data[0] != 0 || d.data[1] != 0 || d.data[2] != 0 || d.data[3] != 0;
}
return false;
};
var _mouseover = false, _mouseDown = false, _posWhenMouseDown = null, _mouseWasDown = false,
_nullSafeHasClass = function(el, clazz) {
return el != null && _hasClass(el, clazz);
};
this.mousemove = function(e) {
var pageXY = _pageXY(e), clientXY = _clientXY(e),
ee = document.elementFromPoint(clientXY[0], clientXY[1]),
eventSourceWasOverlay = _nullSafeHasClass(ee, "_jsPlumb_overlay");
var _continue = _connectionBeingDragged == null && (_nullSafeHasClass(ee, "_jsPlumb_endpoint") || _nullSafeHasClass(ee, "_jsPlumb_connector"));
if (!_mouseover && _continue && self._over(e)) {
_mouseover = true;
self.fire("mouseenter", self, e);
return true;
}
// TODO here there is a remote chance that the overlay the mouse moved onto
// is actually not an overlay for the current component. a more thorough check would
// be to ensure the overlay belonged to the current component.
else if (_mouseover && (!self._over(e) || !_continue) && !eventSourceWasOverlay) {
_mouseover = false;
self.fire("mouseexit", self, e);
}
self.fire("mousemove", self, e);
};
this.click = function(e) {
if (_mouseover && self._over(e) && !_mouseWasDown)
self.fire("click", self, e);
_mouseWasDown = false;
};
this.dblclick = function(e) {
if (_mouseover && self._over(e) && !_mouseWasDown)
self.fire("dblclick", self, e);
_mouseWasDown = false;
};
this.mousedown = function(e) {
if(self._over(e) && !_mouseDown) {
_mouseDown = true;
_posWhenMouseDown = _getOffset(_getElementObject(self.canvas));
self.fire("mousedown", self, e);
}
};
this.mouseup = function(e) {
_mouseDown = false;
self.fire("mouseup", self, e);
};
this.contextmenu = function(e) {
if (_mouseover && self._over(e) && !_mouseWasDown)
self.fire("contextmenu", self, e);
_mouseWasDown = false;
};
};
var _newCanvas = function(params) {
var canvas = document.createElement("canvas");
params["_jsPlumb"].appendElement(canvas, params.parent);
canvas.style.position = "absolute";
if (params["class"]) canvas.className = params["class"];
// set an id. if no id on the element and if uuid was supplied it
// will be used, otherwise we'll create one.
params["_jsPlumb"].getId(canvas, params.uuid);
if (params.tooltip) canvas.setAttribute("title", params.tooltip);
return canvas;
};
var CanvasComponent = function(params) {
CanvasMouseAdapter.apply(this, arguments);
var displayElements = [ ];
this.getDisplayElements = function() { return displayElements; };
this.appendDisplayElement = function(el) { displayElements.push(el); };
};
/**
* Class:CanvasConnector
* Superclass for Canvas Connector renderers.
*/
var CanvasConnector = jsPlumb.CanvasConnector = function(params) {
CanvasComponent.apply(this, arguments);
var _paintOneStyle = function(dim, aStyle) {
self.ctx.save();
jsPlumb.extend(self.ctx, aStyle);
if (aStyle.gradient) {
var g = self.createGradient(dim, self.ctx);
for ( var i = 0; i < aStyle.gradient.stops.length; i++)
g.addColorStop(aStyle.gradient.stops[i][0], aStyle.gradient.stops[i][1]);
self.ctx.strokeStyle = g;
}
self._paint(dim, aStyle);
self.ctx.restore();
};
var self = this,
clazz = self._jsPlumb.connectorClass + " " + (params.cssClass || "");
self.canvas = _newCanvas({
"class":clazz,
_jsPlumb:self._jsPlumb,
parent:params.parent,
tooltip:params.tooltip
});
self.ctx = self.canvas.getContext("2d");
self.appendDisplayElement(self.canvas);
self.paint = function(dim, style) {
if (style != null) {
jsPlumb.sizeCanvas(self.canvas, dim[0], dim[1], dim[2], dim[3]);
if (self.getZIndex())
self.canvas.style.zIndex = self.getZIndex();
if (style.outlineColor != null) {
var outlineWidth = style.outlineWidth || 1,
outlineStrokeWidth = style.lineWidth + (2 * outlineWidth),
outlineStyle = {
strokeStyle:style.outlineColor,
lineWidth:outlineStrokeWidth
};
_paintOneStyle(dim, outlineStyle);
}
_paintOneStyle(dim, style);
}
};
};
/*
* Canvas Bezier Connector. Draws a Bezier curve onto a Canvas element.
*/
jsPlumb.Connectors.canvas.CurlyBrace = function() {
var self = this;
jsPlumb.Connectors.Bezier.apply(this, arguments);
CanvasConnector.apply(this, arguments);
this._paint = function(dimensions, style) {
self.ctx.beginPath();
var height = dimensions[5] - dimensions[7];
var midpoint = dimensions[5] - height/2;
// draw from bottom to midpoint
self.ctx.moveTo(dimensions[4], dimensions[5]);
self.ctx.bezierCurveTo(dimensions[6]-7, dimensions[9]-10, dimensions[10]+11, midpoint+height/14, dimensions[8]+2, midpoint);
// draw from top to midpoint
self.ctx.moveTo(dimensions[4], dimensions[5]-height);
self.ctx.bezierCurveTo(dimensions[6]-7, dimensions[9]-5-height, dimensions[10]+11, midpoint-height/14, dimensions[8]+2, midpoint);
self.ctx.stroke();
};
// TODO i doubt this handles the case that source and target are swapped.
this.createGradient = function(dim, ctx, swap) {
return /*(swap) ? self.ctx.createLinearGradient(dim[4], dim[5], dim[6], dim[7]) : */self.ctx.createLinearGradient(dim[6], dim[7], dim[4], dim[5]);
};
};
@eugenkiss
Copy link
Author

The only function I changed is jsPlumb.Connectors.canvas.Bezier = function().... This is the original definition:

jsPlumb.Connectors.canvas.Bezier = function() {
        var self = this;
        jsPlumb.Connectors.Bezier.apply(this, arguments); 
        CanvasConnector.apply(this, arguments);
        this._paint = function(dimensions, style) {
            self.ctx.beginPath();
            self.ctx.moveTo(dimensions[4], dimensions[5]);
            self.ctx.bezierCurveTo(dimensions[8], dimensions[9], dimensions[10], dimensions[11], dimensions[6], dimensions[7]);             
            self.ctx.stroke();            
        };

        // TODO i doubt this handles the case that source and target are swapped.
        this.createGradient = function(dim, ctx, swap) {
            return /*(swap) ? self.ctx.createLinearGradient(dim[4], dim[5], dim[6], dim[7]) : */self.ctx.createLinearGradient(dim[6], dim[7], dim[4], dim[5]);
        };
    };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment