Snap SVG: Dragging group doesn't update elements - javascript

JSFiddle here: JSFiddle
When dragging a group of objects, the individual objects' location attributes don't seem to be getting updated. This occurs whether I use the default drag() handler or define my own. Even the group BBox operation doesn't seem to update. Code:
var s = Snap("#svg");
var move = function (dx, dy, posx, posy) {
this.attr({
x: posx,
y: posy
});
//this.transform("t" + dx + "," + dy);
};
var block = s.rect(100, 100, 100, 100);
var circle = s.circle(100, 100, 50);
var group = s.g(block, circle);
//group.drag(move, function () {}, function () {});
group.drag();
//block.drag(move, function () {}, function () {});
//just a way to keep info coming w/o an interminable script
document.addEventListener('mousemove', function (e) {
bbox = block.getBBox();
block_x = block.attr("x");
block_y = block.attr("y");
gbbox = group.getBBox();
console.log("block is at " + block_x + "," + block_y,
" Block Bbbox is at " + bbox.x + "," + bbox.y,
" Group Bbbox is at " + gbbox.x + "," + gbbox.y);
}, false);
If I define only one object (say, a rect) and leave it out of a group, and pass my own "move" function to the call to drag, and include setting the "x" and "y" attributes explicitly, then that works. But if I include the rect in a group, then...I can't figure out how to do it, and I've tried a few ways (see the multiple commented-out lines showing things I've tried). I need to know where the rect sub-group element ends up after the drag, or at least the BBox of the whole group. Neither of these seem to be getting updated -- i.e. the console log I put in shows the same numbers forever, no matter where I move the object(s).
Can anyone help?
JSFiddle here: JSFiddle

I think this is because they are two different things, so they aren't actually interchangable.
The drag handler uses transforms. A transform doesn't affect any other attributes, its just an attribute on an element (in this case the group element).
getBBox will work in its current transform space, note this may be different to the clients (eg if the svg were zoomed in/out). So they are two slightly different methods, that do different things.
Use getBoundingClientRect if you need a bounding box relative to the client window. Use getBBox if you need a bounding box in the elements current coordinate space.

Code is using snap.svg.zpd as well, so zoom is possible. Problem is at onStopMove function. Events are fired when group is moved arround. In group is one circle(this.select('#main-inner-circle')) which does not have predefined location inside group. Im trying to get correct cx and cy of that inner circle after moving group.
self.onMove = function (dx, dy, ev, x, y) {
var clientX, clientY;
var tdx, tdy;
if ((typeof dx == 'object') && (dx.type == 'touchmove')) {
clientX = dx.changedTouches[0].clientX;
clientY = dx.changedTouches[0].clientY;
dx = clientX - this.data('ox');
dy = clientY - this.data('oy');
}
var snapInvMatrix = this.transform().diffMatrix.invert();
snapInvMatrix.e = snapInvMatrix.f = 0;
tdx = snapInvMatrix.x(dx, dy);
tdy = snapInvMatrix.y(dx, dy);
this.transform("t" + [tdx, tdy] + this.data('ot'));
}
self.onStartMove = function (x, y, ev) {
if ((typeof x == 'object') && (x.type == 'touchstart')) {
x.preventDefault();
this.data('ox', x.changedTouches[0].clientX);
this.data('oy', x.changedTouches[0].clientY);
}
this.data('ot', this.transform().local);
if (callbacks.onStartMove) {
callbacks.onStartMove();
}
}
self.onStopMove = function () {
var self = this.select('#main-inner-circle');
this.data('ot', this.transform().local);
//self.data('ot', self.transform().local);
console.log(self.getTransformedBBox());
console.log(this.getBBox());
//console.log($(self.node).offset().left - $(self.node).parent().offset().left);
var bBox = this.getBBox();
//var x = bBox.x + $(self.node).offset().left - $(self.node).parent().offset().left + self.getBBox().width / 2;
//var y = bBox.y + $(self.node).offset().top - $(self.node).parent().offset().top + self.getBBox().height / 2;
model.updateElementCoordinates(index, $(this.node).attr("rel"), { x: self.getTransformedBBox().cx, y: self.getTransformedBBox().cy });
if (callbacks.onStopMove) {
callbacks.onStopMove();
}
}

In order to post this question, I'd created the JSFiddle but left out the crucial snap.svg definitions...
<script src="http://snapsvg.io/assets/js/snap.svg-min.js"></script>
...with that, then indeed the group.getBBox() method actually works. However:
Apparently, using getBBox() is incredibly slow -- much slower than just accessing a "x" attribute of something like I was doing before grouping objects. All I know is that my code slows to a crawl if I use getBBox() (I have a lot of objects on the screen).
Further down in the same post mentioned earier ["Get coordinates of svg group on drag with snap.svg"1 recommended getBoundingClientRect(), which also works fine AND is fast enough! My new, working Fiddle showing all of these methods is here: New JSFiddle.
So, future users: use .node.getBoundingClientRect().

Related

how to make objects inside canvas clickable for popups?

i am making a webgl application i want to know is there any way of making objects(3D models made using blender) inside canvas clickable. So that when i click on them a pop up comes containing data.
I know (and have used) two major approaches.
The first one is to allocate a separate framebuffer and render interactive object to it with different colours. Then, upon a mouse event, you read a pixel corresponding to mouse position and find an object corresponding to the colour just read. For exapmle, it may look somewhat like this.
Textured and shaded scene:
Rendered for hit testing:
This approach is interesting due to it's simplicity. But it has some performance challenges and major ones among them are rendering the scene twice and reading pixel data back (its slow and synchronous). The first one was easy: just keep a dirty flag for the framebuffer and redraw it only upon a event and only if the flag is set (then of course reset it). The second one I've tackled by reading and caching from the framebuffer big chunks of pixels:
getPixel: function (x, y) {
var screenSize = this._screen.getCssSize();
x = x * HIT_TEST_BUFFER_SIZE[0] / screenSize[0] | 0;
y = y * HIT_TEST_BUFFER_SIZE[1] / screenSize[1] | 0;
var rx = x >> PIXEL_CACHE_BUCKET_IDX_SHIFT,
ry = y >> PIXEL_CACHE_BUCKET_IDX_SHIFT,
pixelCache = this._pixelCache,
bucket = pixelCache[[rx, ry]];
if (!bucket) {
this._framebuffer.bind();
bucket = pixelCache[[rx, ry]] = new Uint8Array(
4 * PIXEL_CACHE_BUCKET_SIZE[0] * PIXEL_CACHE_BUCKET_SIZE[1]
);
var gl = this._gl;
gl.readPixels(
rx << PIXEL_CACHE_BUCKET_IDX_SHIFT,
ry << PIXEL_CACHE_BUCKET_IDX_SHIFT,
PIXEL_CACHE_BUCKET_SIZE[0],
PIXEL_CACHE_BUCKET_SIZE[1],
gl.RGBA,
gl.UNSIGNED_BYTE,
bucket
);
this._framebuffer.unbind();
}
var bucketOffset = 4 * (
(y - ry * PIXEL_CACHE_BUCKET_SIZE[1]) * PIXEL_CACHE_BUCKET_SIZE[0] +
x - rx * PIXEL_CACHE_BUCKET_SIZE[0]
);
return bucket.subarray(bucketOffset, bucketOffset + 3);
}
The second major approach would be casting a ray to the scene. You take mouse position, construct a ray with it and cast it from a camera position into a scene to find which object it intersects with. That object would be the one mouse cursor pointing to. There is actually a decent implementation of that approach in Three.js, you can use it or take it as a reference to implement your own algorithm. The main challenge with that approach would be algorithmic complexity of searching an object the ray intersects with. It can be tackled with spacial indices built upon you scene.
Canvas is a simple graphics API. It draws pixels very well and nothing more. There are ways to 'fake' event handlers via mouse positions, but that takes more work. Basically you will register the location of mouse click, than match that up with the position of your 3D models to see if you have a match. You will not be able to attach event handlers directly to the 3d blender objects in canvas. In Scalable Vector Graphics (SVG) that would work fine. Just not in canvas.
function handleMouseDown(e) {
// tell the browser we'll handle this event
e.preventDefault();
e.stopPropagation();
// save the mouse position
// in case this becomes a drag operation
lastX = parseInt(e.clientX - offsetX);
lastY = parseInt(e.clientY - offsetY);
// hit all existing FrameControlPt of your blender objects
var hit = -1;
for (var i = 0; i < FrameControlPt.length; i++) {
var location = FrameControlPt[i];
var dx = lastX - location.x;
var dy = lastY - location.y;
if (dx * dx + dy * dy < stdRadius * stdRadius) {
hit = i;
}
}
// hit all existing buttons in the canvas
for (var i = 0; i < btn.length; i++) {
if ((lastX < (btn[i].x + btn[i].width)) &&
(lastX > btn[i].x) &&
(lastY < (btn[i].y + btn[i].height)) &&
(lastY > btn[i].y)) {
console.log("Button #" + (i + 1) + " has been clicked!!");
// execute button function
btn[i].action(); // execute a custom function
}
}
// if hit then set the isDown flag to start a drag
if (hit < 0) {
drawAll();
} else {
draggingCircle = FrameControlPt[hit];
isDown = true;
}
}
Obviously you'd have to handleMouseUp(event).. in this example, I was allowing the users to drag and drop elements within the canvas. You'd have to adjust your events to match your intended usage.
Code extract from this sample.

Raphael.js set movement with text "Raphael is undefined"

I have been struggling to implement drag and drop for a set in raphael.js.
I have managed to do so for sets that are made up of circles, using this code on JSFiddle.
var r = Raphael(10, 50, 600, 600);
var Intermediate = r.set();
var start = function () {
// storing original coordinates
this.ox = this.attr("cx");
this.oy = this.attr("cy");
};
move = function (dx, dy) {
// move will be called with dx and dy
Intermediate.attr({cx: this.ox + dx, cy: this.oy + dy});
};
up = function () {};
Intermediate.drag(move, start, up);
However this does not work for sets made up from rectangles and text, because they do not have cy and cx properties. Replacing cx and cy with x and y respectively does not work as two objects will have different x and y values while concentric circles will have the same cx and cy values.
I found a solution, here, also on JSFiddle, which defines a new method in Raphael.st.
Raphael.st.draggable = function() {};
It then just enables the set to be draggable:
mySet.draggable();
Hoever when I try implement this solution in visual studio I get an error "0x800a1391 - JavaScript runtime error: 'Raphael' is undefined".
I don't understand why this is, I have added the Raphael script to my HTML.

randomly mapping divs

I am creating a new "whack-a-mole" style game where the children have to hit the correct numbers in accordance to the question. So far it is going really well, I have a timer, count the right and wrong answers and when the game is started I have a number of divs called "characters" that appear in the container randomly at set times.
The problem I am having is that because it is completely random, sometimes the "characters" appear overlapped with one another. Is there a way to organize them so that they appear in set places in the container and don't overlap when they appear.
Here I have the code that maps the divs to the container..
function randomFromTo(from, to) {
return Math.floor(Math.random() * (to - from + 1) + from);
}
function scramble() {
var children = $('#container').children();
var randomId = randomFromTo(1, children.length);
moveRandom('char' + randomId);
}
function moveRandom(id) {
var cPos = $('#container').offset();
var cHeight = $('#container').height();
var cWidth = $('#container').width();
var pad = parseInt($('#container').css('padding-top').replace('px', ''));
var bHeight = $('#' + id).height();
var bWidth = $('#' + id).width();
maxY = cPos.top + cHeight - bHeight - pad;
maxX = cPos.left + cWidth - bWidth - pad;
minY = cPos.top + pad;
minX = cPos.left + pad;
newY = randomFromTo(minY, maxY);
newX = randomFromTo(minX, maxX);
$('#' + id).css({
top: newY,
left: newX
}).fadeIn(100, function () {
setTimeout(function () {
$('#' + id).fadeOut(100);
window.cont++;
}, 1000);
});
I have a fiddle if it helps.. http://jsfiddle.net/pUwKb/8/
As #aug suggests, you should know where you cannot place things at draw-time, and only place them at valid positions. The easiest way to do this is to keep currently-occupied positions handy to check them against proposed locations.
I suggest something like
// locations of current divs; elements like {x: 10, y: 40}
var boxes = [];
// p point; b box top-left corner; w and h width and height
function inside(p, w, h, b) {
return (p.x >= b.x) && (p.y >= b.y) && (p.x < b.x + w) && (p.y < b.y + h);
}
// a and b box top-left corners; w and h width and height; m is margin
function overlaps(a, b, w, h, m) {
var corners = [a, {x:a.x+w, y:a.y}, {x:a.x, y:a.y+h}, {x:a.x+w, y:a.y+h}];
var bWithMargins = {x:b.x-m, y:b.y-m};
for (var i=0; i<corners.length; i++) {
if (inside(corners[i], bWithMargins, w+2*m, h+2*m) return true;
}
return false;
}
// when placing a new piece
var box;
while (box === undefined) {
box = createRandomPosition(); // returns something like {x: 15, y: 92}
for (var i=0; i<boxes.length; i++) {
if (overlaps(box, boxes[i], boxwidth, boxheight, margin)) {
box = undefined;
break;
}
}
}
boxes.push(box);
Warning: untested code, beware the typos.
The basic idea you will have to implement is that when a random coordinate is chosen, theoretically you SHOULD know the boundaries of what is not permissible and your program should know not to choose those places (whether you find an algorithm or way of simply disregarding those ranges or your program constantly checks to make sure that the number chosen isn't within the boundary is up to you. the latter is easier to implement but is a bad way of going about it simply because you are entirely relying on chance).
Let's say for example coordinate 50, 70 is selected. If the picture is 50x50 in size, the range of what is allowed would exclude not only the dimensions of the picture, but also 50px in all directions of the picture so that no overlap may occur.
Hope this helps. If I have time, I might try to code an example but I hope this answers the conceptual aspect of the question if that is what you were having trouble with.
Oh and btw forgot to say really great job on this program. It looks awesome :)
You can approach this problem in at least two ways (these two are popped up in my head).
How about to create a 2 dimensional grid segmentation based on the number of questions, the sizes of the question panel and an array holding the position of each question coordinates and then on each time frame to position randomly these panels on one of the allowed coordinates.
Note: read this article for further information: http://eloquentjavascript.net/chapter8.html
The second approach follow the same principle, but this time to check if the panel overlap the existing panel before you place it on the canvas.
var _grids;
var GRID_SIZE = 20 //a constant holding the panel size;
function createGrids() {
_grids = new Array();
for (var i = 0; i< stage.stageWidth / GRID_SIZE; i++) {
_grids[i] = new Array();
for (var j = 0; j< stage.stageHeight / GRID_SIZE; j++) {
_grids[i][j] = new Array();
}
}
}
Then on a separate function to create the collision check. I've created a gist for collision check in Actionscript, but you can use the same principle in Javascript too. I've created this gist for inspirational purposes.
Just use a random number which is based on the width of your board and then modulo with the height...
You get a cell which is where you can put the mole.
For the positions the x and y should never change as you have 9 spots lets say where the mole could pop up.
x x x
x x x
x x x
Each cell would be sized based on % rather then pixels and would allow re sizing the screen
1%3 = 1 (x)
3%3 = 0 (y)
Then no overlap is possible.
Once the mole is positioned it can be show or hidden or moved etc based on some extended logic if required.
If want to keep things your way and you just need a quick re-position algorithm... just set the NE to the SW if the X + width >= x of the character you want to check by setting the x = y+height of the item which overlaps. You could also enforce that logic in the drawing routine by caching the last x and ensuring the random number was not < last + width of the item.
newY = randomFromTo(minY, maxY);
newX = randomFromTo(minX, maxX); if(newX > lastX + characterWidth){ /*needful*/}
There could still however be overlap...
If you wanted to totally eliminate it you would need to keep track of state such as where each x was and then iterate that list to find a new position or position them first and then all them to move about randomly without intersecting which would would be able to control with just padding from that point.
Overall I think it would be easier to just keep X starting at 0 and then and then increment until you are at a X + character width > greater then the width of the board. Then just increase Y by character height and Set X = 0 or character width or some other offset.
newX = 0; newX += characterWidth; if(newX + chracterWidth > boardWidth) newX=0; newY+= characterHeight;
That results in no overlap and having nothing to iterate or keep track of additional to what you do now, the only downside is the pattern of the displayed characters being 'checker board style' or right next to each other (with possible random spacing in between horizontal and vertical placement e.g. you could adjust the padding randomly if you wanted too)
It's the whole random thing in the first place that adds the complexity.
AND I updated your fiddle to prove I eliminated the random and stopped the overlap :)
http://jsfiddle.net/pUwKb/51/

How to remove borders and corners from Canvas object? [Fabric.js]

I'm using fabric.js in one project, where user can draw on canvas, and save to png image (using canvas.toDataURL() function).
However, we noticed if user moved an object and clicked on Save button, it saves a border and corners of previously moved object (borders are always displayed when you move or resize object).
So we need a way to remove object borders before saving, is that possible?
Yes. You probably want to deactivate all objects before saving an image:
canvas.deactivateAll().renderAll();
(renderAll updates actual visual state after deactivation of all objects)
If you like the idea of your users moving and scaling the Paths drawn on the canvas. I would go with kangax's suggestion.
Alternatively if you want to change the behaviour stlightly, you can set the hasBorders and hasControls fields on each path element to false. Then you will never see the controls/borders, and therefore they can never be printed. But it also means that you cannot rotate or scale the Paths, but you can still move them.
Or you could go a step further and make the paths unselectable (making them unmovable, which you may or may not want). So you could set selectable field on each path to false.
To make all of this easier on you you could override _finalizeDrawingPath from fabric.js, by adding the following code in your own js file after you have included fabric.js
fabric.Canvas.prototype._finalizeDrawingPath = function() {
this.contextTop.closePath();
this._isCurrentlyDrawing = false;
var minX = utilMin(this._freeDrawingXPoints),
minY = utilMin(this._freeDrawingYPoints),
maxX = utilMax(this._freeDrawingXPoints),
maxY = utilMax(this._freeDrawingYPoints),
ctx = this.contextTop,
path = [ ], xPoint, yPoint,
xPoints = this._freeDrawingXPoints,
yPoints = this._freeDrawingYPoints;
path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' ');
for (var i = 1; xPoint = xPoints[i], yPoint = yPoints[i]; i++) {
path.push('L ', xPoint - minX, ' ', yPoint - minY, ' ');
}
path = path.join('');
if (path === "M 0 0 L 0 0 ") {
return;
}
var p = new fabric.Path(path);
p.fill = null;
p.stroke = this.freeDrawingColor;
p.strokeWidth = this.freeDrawingLineWidth;
//Tyson Benson:
//Add these lines to remove borders/controls
p.hasBorders = p.hasControls = false;
//Or add this line to make the path unselectable
p.selectable = false;
this.add(p);
p.set("left", minX + (maxX - minX) / 2).set("top", minY + (maxY - minY) / 2).setCoords();
this.renderAll();
this.fire('path:created', { path: p });
};
I have removed some whitespace and comments for brevity (please refer to fabric.js for these comments).
You can set p.hasBorders = p.hasControls = false; or p.selectable = false;. You dont need both.

Drawing arrows on an HTML page to visualize semantic links between textual spans

I have an HTML page with some textual spans marked up something like this:
...
<span id="T2" class="Protein">p50</span>
...
<span id="T3" class="Protein">p65</span>
...
<span id="T34" ids="T2 T3" class="Positive_regulation">recruitment</span>
...
I.e. each span has an ID and refers to zero or more spans via their IDs.
I would like to visualize these references as arrows.
Two questions:
How can I map an ID of a span to the screen coordinates of the rendering of the span?
How do I draw arrows going from one rendering to another?
The solution should work in Firefox, working in other browsers is a plus but not really necessary. The solution could use jQuery, or some other lightweight JavaScript library.
This captured my interest for long enough to produce a little test. The code is below, and you can see it in action
It lists all the spans on the page (might want to restrict that to just those with ids starting with T if that is suitable), and uses the 'ids' attribute to build the list of links. Using a canvas element behind the spans, it draws arc arrows alternately above and below the spans for each source span.
<script type="application/x-javascript">
function generateNodeSet() {
var spans = document.getElementsByTagName("span");
var retarr = [];
for(var i=0;i<spans.length; i++) {
retarr[retarr.length] = spans[i].id;
}
return retarr;
}
function generateLinks(nodeIds) {
var retarr = [];
for(var i=0; i<nodeIds.length; i++) {
var id = nodeIds[i];
var span = document.getElementById(id);
var atts = span.attributes;
var ids_str = false;
if((atts.getNamedItem) && (atts.getNamedItem('ids'))) {
ids_str = atts.getNamedItem('ids').value;
}
if(ids_str) {
retarr[id] = ids_str.split(" ");
}
}
return retarr;
}
// degrees to radians, because most people think in degrees
function degToRad(angle_degrees) {
return angle_degrees/180*Math.PI;
}
// draw a horizontal arc
// ctx: canvas context;
// inax: first x point
// inbx: second x point
// y: y value of start and end
// alpha_degrees: (tangential) angle of start and end
// upside: true for arc above y, false for arc below y.
function drawHorizArc(ctx, inax, inbx, y, alpha_degrees, upside)
{
var alpha = degToRad(alpha_degrees);
var startangle = (upside ? ((3.0/2.0)*Math.PI + alpha) : ((1.0/2.0)*Math.PI - alpha));
var endangle = (upside ? ((3.0/2.0)*Math.PI - alpha) : ((1.0/2.0)*Math.PI + alpha));
var ax=Math.min(inax,inbx);
var bx=Math.max(inax,inbx);
// tan(alpha) = o/a = ((bx-ax)/2) / o
// o = ((bx-ax)/2/tan(alpha))
// centre of circle is (bx+ax)/2, y-o
var circleyoffset = ((bx-ax)/2)/Math.tan(alpha);
var circlex = (ax+bx)/2.0;
var circley = y + (upside ? 1 : -1) * circleyoffset;
var radius = Math.sqrt(Math.pow(circlex-ax,2) + Math.pow(circley-y,2));
ctx.beginPath();
if(upside) {
ctx.moveTo(bx,y);
ctx.arc(circlex,circley,radius,startangle,endangle,1);
} else {
ctx.moveTo(bx,y);
ctx.arc(circlex,circley,radius,startangle,endangle,0);
}
ctx.stroke();
}
// draw the head of an arrow (not the main line)
// ctx: canvas context
// x,y: coords of arrow point
// angle_from_north_clockwise: angle of the line of the arrow from horizontal
// upside: true=above the horizontal, false=below
// barb_angle: angle between barb and line of the arrow
// filled: fill the triangle? (true or false)
function drawArrowHead(ctx, x, y, angle_from_horizontal_degrees, upside, //mandatory
barb_length, barb_angle_degrees, filled) { //optional
(barb_length==undefined) && (barb_length=13);
(barb_angle_degrees==undefined) && (barb_angle_degrees = 20);
(filled==undefined) && (filled=true);
var alpha_degrees = (upside ? -1 : 1) * angle_from_horizontal_degrees;
//first point is end of one barb
var plus = degToRad(alpha_degrees - barb_angle_degrees);
a = x + (barb_length * Math.cos(plus));
b = y + (barb_length * Math.sin(plus));
//final point is end of the second barb
var minus = degToRad(alpha_degrees + barb_angle_degrees);
c = x + (barb_length * Math.cos(minus));
d = y + (barb_length * Math.sin(minus));
ctx.beginPath();
ctx.moveTo(a,b);
ctx.lineTo(x,y);
ctx.lineTo(c,d);
if(filled) {
ctx.fill();
} else {
ctx.stroke();
}
return true;
}
// draw a horizontal arcing arrow
// ctx: canvas context
// inax: start x value
// inbx: end x value
// y: y value
// alpha_degrees: angle of ends to horizontal (30=shallow, >90=silly)
function drawHorizArcArrow(ctx, inax, inbx, y, //mandatory
alpha_degrees, upside, barb_length) { //optional
(alpha_degrees==undefined) && (alpha_degrees=45);
(upside==undefined) && (upside=true);
drawHorizArc(ctx, inax, inbx, y, alpha_degrees, upside);
if(inax>inbx) {
drawArrowHead(ctx, inbx, y, alpha_degrees*0.9, upside, barb_length);
} else {
drawArrowHead(ctx, inbx, y, (180-alpha_degrees*0.9), upside, barb_length);
}
return true;
}
function drawArrow(ctx,fromelem,toelem, //mandatory
above, angle) { //optional
(above==undefined) && (above = true);
(angle==undefined) && (angle = 45); //degrees
midfrom = fromelem.offsetLeft + (fromelem.offsetWidth / 2) - left - tofromseparation/2;
midto = toelem.offsetLeft + ( toelem.offsetWidth / 2) - left + tofromseparation/2;
//var y = above ? (fromelem.offsetTop - top) : (fromelem.offsetTop + fromelem.offsetHeight - top);
var y = fromelem.offsetTop + (above ? 0 : fromelem.offsetHeight) - canvasTop;
drawHorizArcArrow(ctx, midfrom, midto, y, angle, above);
}
var canvasTop = 0;
function draw() {
var canvasdiv = document.getElementById("canvas");
var spanboxdiv = document.getElementById("spanbox");
var ctx = canvasdiv.getContext("2d");
nodeset = generateNodeSet();
linkset = generateLinks(nodeset);
tofromseparation = 20;
left = canvasdiv.offsetLeft - spanboxdiv.offsetLeft;
canvasTop = canvasdiv.offsetTop - spanboxdiv.offsetTop;
for(var key in linkset) {
for (var i=0; i<linkset[key].length; i++) {
fromid = key;
toid = linkset[key][i];
var above = (i%2==1);
drawArrow(ctx,document.getElementById(fromid),document.getElementById(toid),above);
}
}
}
</script>
And you just need a call somewhere to the draw() function:
<body onload="draw();">
Then a canvas behind the set of spans.
<canvas style='border:1px solid red' id="canvas" width="800" height="7em"></canvas><br />
<div id="spanbox" style='float:left; position:absolute; top:75px; left:50px'>
<span id="T2">p50</span>
...
<span id="T3">p65</span>
...
<span id="T34" ids="T2 T3">recruitment</span>
</div>
Future modifications, as far as I can see:
Flattening the top of longer arrows
Refactoring to be able to draw non-horizontal arrows: add a new canvas for each?
Use a better routine to get the total offsets of the canvas and span elements.
[Edit Dec 2011: Fixed, thanks #Palo]
Hope that's as useful as it was fun.
You have a couple options: svg or canvas.
From the looks of it you don't need these arrows to have any particular mathematical form, you just need them to go between elements.
Try WireIt. Have a look at this WireIt Demo (which has been deprecated). It uses a canvas tag for each individual wire between the floating dialog divs, then sizes and positions each canvas element to give the appearance of a connecting line at just the right spot. You may have to implement an additional rotating arrowhead, unless you don't mind the arrows coming in to each element at the same angle.
Edit: the demo has been deprecated.
Edit: Ignore this answer, #Phil H nailed it
A great library for arrows is JointJS that is based on Raphael as shown above. With JointJS you can easily draw arrows with curves or vertices without any complicated stuff ;-)
var j34 = s3.joint(s4, uml.arrow).setVertices(["170 130", "250 120"]);
This defines an arrow 'j34' that connects two js items s3 with s4. Everything else can be read in the documentation of JointJS.
If you don't need curved arrows, you could use absolutely positioned divs above or below the list. You could then use css to style those divs plus a couple of images that make up the arrow head. Below is an example using the icon set from the jQuery UI project (sorry about the long URL).
Here's the CSS to get things started:
<style>
.below{
border-bottom:1px solid #000;
border-left:1px solid #000;
border-right:1px solid #000;
}
.below span{
background-position:0px -16px;
top:-8px;
}
.above{
border-top:1px solid #000;
border-left:1px solid #000;
border-right:1px solid #000;
}
.above span{
background-position:-64px -16px;
bottom:-8px;
}
.arrow{
position:absolute;
display:block;
background-image:url(http://jquery-ui.googlecode.com/svn/trunk/themes/base/images/ui-icons_454545_256x240.png);
width:16px;
height:16px;
margin:0;
padding:0;
}
.left{left:-8px;}
.right{right:-9px;}
</style>
Now we can start to assemble arrow divs. For instance, to style the arrow from "requires" to "promoter" in your example above, you could do left,bottom, and right borders on the div with and upward facing arrow graphic in the top left of the div.
<div class='below' style="position:absolute;top:30px;left:30px;width:100px;height:16px">
<span class='arrow left'></span>
</div>
The inline styles would be need to be applied by script after you figured out the locations of the things you would need to connect. Let's say that your list looks like this:
<span id="promoter">Promoter</span><span>Something Else</span><span id="requires">Requires</span>
Then the following script will position your arrow:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
<script>
$(function(){
var promoterPos=$("#promoter").offset();
var requiresPos=$("#requires").offset();
$("<div class='below'><span class='arrow left'></span></div>")
.css({position:"absolute",left:promoterPos.left,right:promoterPos.top+$("#promoter").height()})
.width(requiresPos.left-promoterPos.left)
.height(16)
.appendTo("body");
});
</script>
Go ahead and paste the examples above into a blank html page. It's kind of neat.
You could try this JavaScript Vector Graphics Library - it's very clever stuff, hope it helps.
EDIT: As this link is dead, here is another link from Archive.org.
I try to go with open web technologies wherever possible but the truth is that HTML & JavaScript (or jQuery) aren't the tools for this particular job (sad but true), especially as the diagrams you're drawing increase in complexity.
On the other hand, Flash was made for this. Significantly less ActionScript 3.0 code would be required to parse that XML, layout your text (with more control over fonts & super/subscripts) and render the curves (see the flash.display.Graphics class methods like curveTo). Overall you'll be looking at less code, better maintainability, fewer hacks, wider compatibility and more stable drawing libraries.
Good luck with the project.
As others have mentioned, Javascript and html are not good tools for this sort of thing.
John Resig wrote an implementation of Processing.org in JavaScript. It uses the canvas element, so it will work in modern versions of Firefox, but it will not work in all browsers. If you only care about Firefox, this would probably be the way to go.
You might be able to use SVG, but again, this is not supported in all browsers.
I needed a similar solution, and I was looking into RaphaelJS JavaScript Library. For example you can draw a straight arrow from (x1,y1) to (x2,y2) with:
Raphael.fn.arrow = function (x1, y1, x2, y2, size) {
var angle = Math.atan2(x1-x2,y2-y1);
angle = (angle / (2 * Math.PI)) * 360;
var arrowPath = this.path(“M” + x2 + ” ” + y2 + ” L” + (x2 - size) + ” ” + (y2 - size) + ” L” + (x2 - size) + ” ” + (y2 + size) + ” L” + x2 + ” ” + y2 ).attr(“fill”,”black”).rotate((90+angle),x2,y2);
var linePath = this.path(“M” + x1 + ” ” + y1 + ” L” + x2 + ” ” + y2);
return [linePath,arrowPath];
}
I haven't figure out how to draw a curved arrow, but I'm sure it's possible.
You could get the curved arrow ends using a handful of position:absolute divs with background-image set to transparent GIFs... a set for beginning (top and bottom)... a bacground:repeat div for expandible middle, and another pair for the ends (top and bottom).
You can use this library: just annotate your SVG lines with the ids of the source & target element. It uses MutationObserver to observe changes in the connected elements.

Categories