6 * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
9 * This file contains the HTML5 canvas renderers. Support for canvas was dropped in 1.4.2.
10 * This is being kept around because canvas might make a comeback as a single-page solution
11 * that also supports node rendering.
13 * Copyright (c) 2010 - 2013 Simon Porritt (http://jsplumb.org)
16 * http://github.com/sporritt/jsplumb
17 * http://code.google.com/p/jsplumb
19 * Dual licensed under the MIT and GPL2 licenses.
25 // ********************************* CANVAS RENDERERS FOR CONNECTORS AND ENDPOINTS *******************************************************************
27 // TODO refactor to renderer common script. put a ref to jsPlumb.sizeCanvas in there too.
28 var _connectionBeingDragged = null,
29 _hasClass = function(el, clazz) { return jsPlumb.CurrentLibrary.hasClass(_getElementObject(el), clazz); },
30 _getElementObject = function(el) { return jsPlumb.CurrentLibrary.getElementObject(el); },
31 _getOffset = function(el) { return jsPlumb.CurrentLibrary.getOffset(_getElementObject(el)); },
32 _pageXY = function(el) { return jsPlumb.CurrentLibrary.getPageXY(el); },
33 _clientXY = function(el) { return jsPlumb.CurrentLibrary.getClientXY(el); };
36 * Class:CanvasMouseAdapter
37 * Provides support for mouse events on canvases.
39 var CanvasMouseAdapter = window.CanvasMouseAdapter = function() {
41 this.overlayPlacements = [];
42 jsPlumb.jsPlumbUIComponent.apply(this, arguments);
43 jsPlumbUtil.EventGenerator.apply(this, arguments);
45 * returns whether or not the given event is ojver a painted area of the canvas.
47 this._over = function(e) {
48 var o = _getOffset(_getElementObject(self.canvas)),
50 x = pageXY[0] - o.left, y = pageXY[1] - o.top;
51 if (x > 0 && y > 0 && x < self.canvas.width && y < self.canvas.height) {
52 // first check overlays
53 for ( var i = 0; i < self.overlayPlacements.length; i++) {
54 var p = self.overlayPlacements[i];
55 if (p && (p[0] <= x && p[1] >= x && p[2] <= y && p[3] >= y))
59 var d = self.canvas.getContext("2d").getImageData(parseInt(x, 10), parseInt(y, 10), 1, 1);
60 return d.data[0] !== 0 || d.data[1] !== 0 || d.data[2] !== 0 || d.data[3] !== 0;
65 var _mouseover = false, _mouseDown = false, _posWhenMouseDown = null, _mouseWasDown = false,
66 _nullSafeHasClass = function(el, clazz) {
67 return el !== null && _hasClass(el, clazz);
69 this.mousemove = function(e) {
70 var pageXY = _pageXY(e), clientXY = _clientXY(e),
71 ee = document.elementFromPoint(clientXY[0], clientXY[1]),
72 eventSourceWasOverlay = _nullSafeHasClass(ee, "_jsPlumb_overlay");
73 var _continue = _connectionBeingDragged === null && (_nullSafeHasClass(ee, "_jsPlumb_endpoint") || _nullSafeHasClass(ee, "_jsPlumb_connector"));
74 if (!_mouseover && _continue && self._over(e)) {
76 self.fire("mouseenter", self, e);
79 // TODO here there is a remote chance that the overlay the mouse moved onto
80 // is actually not an overlay for the current component. a more thorough check would
81 // be to ensure the overlay belonged to the current component.
82 else if (_mouseover && (!self._over(e) || !_continue) && !eventSourceWasOverlay) {
84 self.fire("mouseexit", self, e);
86 self.fire("mousemove", self, e);
89 this.click = function(e) {
90 if (_mouseover && self._over(e) && !_mouseWasDown)
91 self.fire("click", self, e);
92 _mouseWasDown = false;
95 this.dblclick = function(e) {
96 if (_mouseover && self._over(e) && !_mouseWasDown)
97 self.fire("dblclick", self, e);
98 _mouseWasDown = false;
101 this.mousedown = function(e) {
102 if(self._over(e) && !_mouseDown) {
104 _posWhenMouseDown = _getOffset(_getElementObject(self.canvas));
105 self.fire("mousedown", self, e);
109 this.mouseup = function(e) {
111 self.fire("mouseup", self, e);
114 this.contextmenu = function(e) {
115 if (_mouseover && self._over(e) && !_mouseWasDown)
116 self.fire("contextmenu", self, e);
117 _mouseWasDown = false;
120 jsPlumbUtil.extend(CanvasMouseAdapter, [ jsPlumb.jsPlumbUIComponent, jsPlumbUtil.EventGenerator ]);
122 var _newCanvas = function(params) {
123 var canvas = document.createElement("canvas");
124 params._jsPlumb.instance.appendElement(canvas, params.parent);
125 canvas.style.position = "absolute";
126 if (params["class"]) canvas.className = params["class"];
127 // set an id. if no id on the element and if uuid was supplied it
128 // will be used, otherwise we'll create one.
129 params._jsPlumb.instance.getId(canvas, params.uuid);
130 if (params.tooltip) canvas.setAttribute("title", params.tooltip);
135 var CanvasComponent = window.CanvasComponent = function(params) {
136 CanvasMouseAdapter.apply(this, arguments);
138 var displayElements = [ ];
139 this.getDisplayElements = function() { return displayElements; };
140 this.appendDisplayElement = function(el) { displayElements.push(el); };
142 jsPlumbUtil.extend(CanvasComponent, CanvasMouseAdapter, {
143 setVisible:function(state) {
144 this.canvas.style.display = state ? "block" : "none";
148 var segmentMultipliers = [null, [1, -1], [1, 1], [-1, 1], [-1, -1] ];
149 var maybeMakeGradient = function(ctx, style, gradientFunction) {
150 if (style.gradient) {
151 var g = gradientFunction();
152 for ( var i = 0; i < style.gradient.stops.length; i++)
153 g.addColorStop(style.gradient.stops[i][0], style.gradient.stops[i][1]);
157 var segmentRenderer = function(segment, ctx, style, dx, dy) {
159 "Straight":function(segment, ctx, style, dx, dy) {
160 var d = segment.params;
162 maybeMakeGradient(ctx, style, function() { return ctx.createLinearGradient(d.x1, d.y1, d.x2, d.y2); });
164 ctx.translate(dx, dy);
165 if (style.dashstyle && style.dashstyle.split(" ").length === 2) {
166 // only a very simple dashed style is supported - having two values, which define the stroke length
167 // (as a multiple of the stroke width) and then the space length (also as a multiple of stroke width).
168 var ds = style.dashstyle.split(" ");
169 if (ds.length !== 2) ds = [2, 2];
170 var dss = [ ds[0] * style.lineWidth, ds[1] * style.lineWidth ],
171 m = (d.x2- d.x1) / (d.y2 - d.y1),
172 s = jsPlumbUtil.segment([d.x1, d.y1], [ d.x2, d.y2 ]),
173 sm = segmentMultipliers[s],
174 theta = Math.atan(m),
175 l = Math.sqrt(Math.pow(d.x2 - d.x1, 2) + Math.pow(d.y2 - d.y1, 2)),
176 repeats = Math.floor(l / (dss[0] + dss[1])),
177 curPos = [d.x1, d.y1];
180 // TODO: the question here is why could we not support this in all connector types? it's really
181 // just a case of going along and asking jsPlumb for the next point on the path a few times, until it
182 // reaches the end. every type of connector supports that method, after all. but right now its only the
183 // bezier connector that gives you back the new location on the path along with the x,y coordinates, which
184 // we would need. we'd start out at loc=0 and ask for the point along the path that is dss[0] pixels away.
185 // we then ask for the point that is (dss[0] + dss[1]) pixels away; and from that one we need not just the
186 // x,y but the location, cos we're gonna plug that location back in in order to find where that dash ends.
188 // it also strikes me that it should be trivial to support arbitrary dash styles (having more or less than two
189 // entries). you'd just iterate that array using a step size of 2, and generify the (rss[0] + rss[1])
190 // computation to be sum(rss[0]..rss[n]).
192 for (var i = 0; i < repeats; i++) {
193 ctx.moveTo(curPos[0], curPos[1]);
195 var nextEndX = curPos[0] + (Math.abs(Math.sin(theta) * dss[0]) * sm[0]),
196 nextEndY = curPos[1] + (Math.abs(Math.cos(theta) * dss[0]) * sm[1]),
197 nextStartX = curPos[0] + (Math.abs(Math.sin(theta) * (dss[0] + dss[1])) * sm[0]),
198 nextStartY = curPos[1] + (Math.abs(Math.cos(theta) * (dss[0] + dss[1])) * sm[1]);
200 ctx.lineTo(nextEndX, nextEndY);
201 curPos = [nextStartX, nextStartY];
204 // now draw the last bit
205 ctx.moveTo(curPos[0], curPos[1]);
206 ctx.lineTo(d.x2, d.y2);
210 ctx.moveTo(d.x1, d.y1);
211 ctx.lineTo(d.x2, d.y2);
218 "Bezier":function(segment, ctx, style, dx, dy) {
219 var d = segment.params;
221 maybeMakeGradient(ctx, style, function() { return ctx.createLinearGradient(d.x2 + dx, d.y2 + dy, d.x1 + dx, d.y1 + dy); });
223 ctx.translate(dx, dy);
224 ctx.moveTo(d.x1, d.y1);
225 ctx.bezierCurveTo(d.cp1x, d.cp1y, d.cp2x, d.cp2y, d.x2, d.y2);
229 "Arc":function(segment, ctx, style, dx, dy) {
230 var d = segment.params;
233 ctx.translate(dx, dy);
234 ctx.arc(d.cx, d.cy, d.r, segment.startAngle, segment.endAngle, d.ac);
238 })[segment.type](segment, ctx, style, dx, dy);
242 * Class:CanvasConnector
243 * Superclass for Canvas Connector renderers.
245 var CanvasConnector = jsPlumb.ConnectorRenderers.canvas = function(params) {
246 CanvasComponent.apply(this, arguments);
248 var _paintOneStyle = function(aStyle, dx, dy) {
250 jsPlumb.extend(this.ctx, aStyle);
252 var segments = this.getSegments();
253 for (var i = 0; i < segments.length; i++) {
254 segmentRenderer(segments[i], this.ctx, aStyle, dx, dy);
259 var clazz = this._jsPlumb.instance.connectorClass + " " + (params.cssClass || "");
260 this.canvas = _newCanvas({
262 _jsPlumb:this._jsPlumb,
265 this.ctx = this.canvas.getContext("2d");
267 this.appendDisplayElement(this.canvas);
269 this.paint = function(style, anchor, extents) {
272 var xy = [ this.x, this.y ], wh = [ this.w, this.h ], p,
275 if (extents != null) {
276 if (extents.xmin < 0) {
277 xy[0] += extents.xmin;
280 if (extents.ymin < 0) {
281 xy[1] += extents.ymin;
284 wh[0] = extents.xmax + ((extents.xmin < 0) ? -extents.xmin : 0);
285 wh[1] = extents.ymax + ((extents.ymin < 0) ? -extents.ymin : 0);
288 this.translateX = dx;
289 this.translateY = dy;
291 jsPlumbUtil.sizeElement(this.canvas, xy[0], xy[1], wh[0], wh[1]);
293 if (style.outlineColor != null) {
294 var outlineWidth = style.outlineWidth || 1,
295 outlineStrokeWidth = style.lineWidth + (2 * outlineWidth),
297 strokeStyle:style.outlineColor,
298 lineWidth:outlineStrokeWidth
300 _paintOneStyle(outlineStyle, dx, dy);
302 _paintOneStyle(style, dx, dy);
306 jsPlumbUtil.extend(CanvasConnector, CanvasComponent);
310 * Class:CanvasEndpoint
311 * Superclass for Canvas Endpoint renderers.
313 var CanvasEndpoint = function(params) {
314 CanvasComponent.apply(this, arguments);
315 var clazz = this._jsPlumb.instance.endpointClass + " " + (params.cssClass || ""),
318 _jsPlumb:this._jsPlumb,
319 parent:params.parent,
322 this.canvas = _newCanvas(canvasParams);
323 this.ctx = this.canvas.getContext("2d");
325 this.appendDisplayElement(this.canvas);
327 this.paint = function(style, anchor, extents) {
328 jsPlumbUtil.sizeElement(this.canvas, this.x, this.y, this.w, this.h);
329 if (style.outlineColor != null) {
330 var outlineWidth = style.outlineWidth || 1,
331 outlineStrokeWidth = style.lineWidth + (2 * outlineWidth);
333 strokeStyle:style.outlineColor,
334 lineWidth:outlineStrokeWidth
338 this._paint.apply(this, arguments);
341 jsPlumbUtil.extend(CanvasEndpoint, CanvasComponent);
343 jsPlumb.Endpoints.canvas.Dot = function(params) {
344 jsPlumb.Endpoints.Dot.apply(this, arguments);
345 CanvasEndpoint.apply(this, arguments);
347 parseValue = function(value) {
348 try { return parseInt(value, 10); }
350 if (value.substring(value.length - 1) == '%')
351 return parseInt(value.substring(0, value - 1), 10);
354 calculateAdjustments = function(gradient) {
355 var offsetAdjustment = self.defaultOffset, innerRadius = self.defaultInnerRadius;
356 if (gradient.offset) offsetAdjustment = parseValue(gradient.offset);
357 if (gradient.innerRadius) innerRadius = parseValue(gradient.innerRadius);
358 return [offsetAdjustment, innerRadius];
360 this._paint = function(style) {
362 var ctx = self.canvas.getContext('2d'),
363 orientation = params.endpoint.anchor.getOrientation(params.endpoint);
365 jsPlumb.extend(ctx, style);
366 if (style.gradient) {
367 var adjustments = calculateAdjustments(style.gradient),
368 yAdjust = orientation[1] == 1 ? adjustments[0] * -1 : adjustments[0],
369 xAdjust = orientation[0] == 1 ? adjustments[0] * -1: adjustments[0],
370 g = ctx.createRadialGradient(self.radius, self.radius, self.radius, self.radius + xAdjust, self.radius + yAdjust, adjustments[1]);
371 for (var i = 0; i < style.gradient.stops.length; i++)
372 g.addColorStop(style.gradient.stops[i][0], style.gradient.stops[i][1]);
376 //ctx.translate(dx, dy);
377 ctx.arc(self.radius, self.radius, self.radius, 0, Math.PI*2, true);
379 if (style.fillStyle || style.gradient) ctx.fill();
380 if (style.strokeStyle) ctx.stroke();
384 jsPlumbUtil.extend(jsPlumb.Endpoints.canvas.Dot, [ jsPlumb.Endpoints.Dot, CanvasEndpoint ]);
386 jsPlumb.Endpoints.canvas.Rectangle = function(params) {
389 jsPlumb.Endpoints.Rectangle.apply(this, arguments);
390 CanvasEndpoint.apply(this, arguments);
392 this._paint = function(style) {
394 var ctx = self.canvas.getContext("2d"),
395 orientation = params.endpoint.anchor.getOrientation(params.endpoint);
397 jsPlumb.extend(ctx, style);
399 /* canvas gradient */
400 if (style.gradient) {
401 // first figure out which direction to run the gradient in (it depends on the orientation of the anchors)
402 var y1 = orientation[1] == 1 ? self.h : orientation[1] === 0 ? self.h / 2 : 0;
403 var y2 = orientation[1] == -1 ? self.h : orientation[1] === 0 ? self.h / 2 : 0;
404 var x1 = orientation[0] == 1 ? self.w : orientation[0] === 0 ? self.w / 2 : 0;
405 var x2 = orientation[0] == -1 ? self.w : orientation[0] === 0 ? self.w / 2 : 0;
406 var g = ctx.createLinearGradient(x1,y1,x2,y2);
407 for (var i = 0; i < style.gradient.stops.length; i++)
408 g.addColorStop(style.gradient.stops[i][0], style.gradient.stops[i][1]);
413 ctx.rect(0, 0, self.w, self.h);
415 if (style.fillStyle || style.gradient) ctx.fill();
416 if (style.strokeStyle) ctx.stroke();
419 jsPlumbUtil.extend(jsPlumb.Endpoints.canvas.Rectangle, [ jsPlumb.Endpoints.Rectangle, CanvasEndpoint ]);
421 jsPlumb.Endpoints.canvas.Triangle = function(params) {
424 jsPlumb.Endpoints.Triangle.apply(this, arguments);
425 CanvasEndpoint.apply(this, arguments);
427 this._paint = function(style) {
428 var ctx = self.canvas.getContext('2d'),
429 offsetX = 0, offsetY = 0, angle = 0,
430 orientation = params.endpoint.anchor.getOrientation(params.endpoint);
432 if( orientation[0] == 1 ) {
433 offsetX = self.width;
434 offsetY = self.height;
437 if( orientation[1] == -1 ) {
438 offsetX = self.width;
441 if( orientation[1] == 1 ) {
442 offsetY = self.height;
446 ctx.fillStyle = style.fillStyle;
448 ctx.translate(offsetX, offsetY);
449 ctx.rotate(angle * Math.PI/180);
453 ctx.lineTo(self.width/2, self.height/2);
454 ctx.lineTo(0, self.height);
456 if (style.fillStyle || style.gradient) ctx.fill();
457 if (style.strokeStyle) ctx.stroke();
460 jsPlumbUtil.extend(jsPlumb.Endpoints.canvas.Triangle, [ jsPlumb.Endpoints.Triangle, CanvasEndpoint ]);
463 * Canvas Image Endpoint: uses the default version, which creates an <img> tag.
465 jsPlumb.Endpoints.canvas.Image = jsPlumb.Endpoints.Image;
468 * Blank endpoint in all renderers is just the default Blank endpoint.
470 jsPlumb.Endpoints.canvas.Blank = jsPlumb.Endpoints.Blank;
472 // ********************************* END OF CANVAS RENDERERS *******************************************************************
474 jsPlumb.Overlays.canvas.Label = jsPlumb.Overlays.Label;
475 jsPlumb.Overlays.canvas.Custom = jsPlumb.Overlays.Custom;
478 * a placeholder right now, really just exists to mirror the fact that there are SVG and VML versions of this.
480 var CanvasOverlay = function() {
481 jsPlumb.jsPlumbUIComponent.apply(this, arguments);
483 jsPlumbUtil.extend(CanvasOverlay, jsPlumb.jsPlumbUIComponent, {
484 setVisible : function(state) {
485 this.visible = state;
486 this.component.repaint();
490 var AbstractCanvasArrowOverlay = function(superclass, originalArgs) {
491 superclass.apply(this, originalArgs);
492 CanvasOverlay.apply(this, originalArgs);
493 this.paint = function(params, containerExtents) {
494 var ctx = params.component.ctx, d = params.d;
498 ctx.lineWidth = params.lineWidth;
500 ctx.translate(params.component.translateX, params.component.translateY);
501 ctx.moveTo(d.hxy.x, d.hxy.y);
502 ctx.lineTo(d.tail[0].x, d.tail[0].y);
503 ctx.lineTo(d.cxy.x, d.cxy.y);
504 ctx.lineTo(d.tail[1].x, d.tail[1].y);
505 ctx.lineTo(d.hxy.x, d.hxy.y);
508 if (params.strokeStyle) {
509 ctx.strokeStyle = params.strokeStyle;
512 if (params.fillStyle) {
513 ctx.fillStyle = params.fillStyle;
521 jsPlumb.Overlays.canvas.Arrow = function() {
522 AbstractCanvasArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]);
524 jsPlumbUtil.extend(jsPlumb.Overlays.canvas.Arrow, [ jsPlumb.Overlays.Arrow, CanvasOverlay ] );
526 jsPlumb.Overlays.canvas.PlainArrow = function() {
527 AbstractCanvasArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]);
529 jsPlumbUtil.extend(jsPlumb.Overlays.canvas.PlainArrow, [ jsPlumb.Overlays.PlainArrow, CanvasOverlay ] );
531 jsPlumb.Overlays.canvas.Diamond = function() {
532 AbstractCanvasArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]);
534 jsPlumbUtil.extend(jsPlumb.Overlays.canvas.Diamond, [ jsPlumb.Overlays.Diamond, CanvasOverlay ] );