Reputation: 87
Is it possible to draw a line with html and jquery just by refering to the element id? I have an important word in a text and want to draw a line between this word and an image that describes it. I have seen that that it is possible to draw between elements with canvas but they have style position set to absolute. Since my element is a word in a text I can't set it to absolute. Example
<p>This is my text with this very <span id="important_word">important</span> word</p>
...
<img src="important.jpg" id="important_img"/>
Now I want a line drawn between the span and the img. Is it possible?
Thanks in advance!
Upvotes: 4
Views: 9148
Reputation: 20151
Since this comes up now and again, I've put a bit of effort in. It's not jquery, so you can probably simplify to some degree. FYI this answer is also posted in an answer to this other question, but the request is the same. Using the html and CSS of that question, there is a jsbin demo here http://jsbin.com/guken/3/
The method is to create a floating canvas element (shaded pink), and lay that underneath the rest of the DOM (using z-index). I then calculate the points on the borders of the two boxes that correspond with a line between the box centres. The red and blue squares are actually divs that move with the line ends, which could be used for annotation like source, target etc.
In that jsbin, you can click on one element, and then get a line ready to click on the next. It detects hover for the chosen elements and snaps to a target if you hover over one.
I won't paste all the code here, but the bit where we draw a line from one x,y position to another in client DOM coordinates is this:
var lineElem;
function drawLineXY(fromXY, toXY) {
if(!lineElem) {
lineElem = document.createElement('canvas');
lineElem.style.position = "absolute";
lineElem.style.zIndex = -100;
document.body.appendChild(lineElem);
}
var leftpoint, rightpoint;
if(fromXY.x < toXY.x) {
leftpoint = fromXY;
rightpoint = toXY;
} else {
leftpoint = toXY;
rightpoint = fromXY;
}
var lineWidthPix = 4;
var gutterPix = 10;
var origin = {x:leftpoint.x-gutterPix,
y:Math.min(fromXY.y, toXY.y)-gutterPix};
lineElem.width = Math.max(rightpoint.x - leftpoint.x, lineWidthPix) +
2.0*gutterPix;
lineElem.height = Math.abs(fromXY.y - toXY.y) + 2.0*gutterPix;
lineElem.style.left = origin.x;
lineElem.style.top = origin.y;
var ctx = lineElem.getContext('2d');
// Use the identity matrix while clearing the canvas
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, lineElem.width, lineElem.height);
ctx.restore();
ctx.lineWidth = 4;
ctx.strokeStyle = '#09f';
ctx.beginPath();
ctx.moveTo(fromXY.x - origin.x, fromXY.y - origin.y);
ctx.lineTo(toXY.x - origin.x, toXY.y - origin.y);
ctx.stroke();
}
As the example is just one line, and we can always store lines that have been 'finished' ready to create more, it uses a global variable lineElem
. On the first attempt to draw a line, it creates a canvas element, inserts it into the DOM and assigns it to lineElem. After this construction, it subsequently reuses the canvas element, changing the size and redrawing for new coordinate pairs.
To prevent the line being cut off by the edge of the canvas, there is a gutter setting which pads the canvas width and height. The rest is just getting the coordinate translation right between client DOM coordinates and the coordinates for drawing on the canvas itself.
The only other unstraightforward bit is calculating the coordinates of a point on the border of a box along a line. It's not perfect, but it's a reasonable start. The crux is to calculate the angle of the target (to
) point from the perspective of the source (from
) point, and see how that compares to the known angles of the box corners:
function getNearestPointOutside(from, to, boxSize) {
// which side does it hit?
// get the angle of to from from.
var theta = Math.atan2(boxSize.y, boxSize.x);
var phi = Math.atan2(to.y - from.y, to.x - from.x);
var nearestPoint = {};
if(Math.abs(phi) < theta) { // crosses +x
nearestPoint.x = from.x + boxSize.x/2.0;
nearestPoint.y = from.y + ((to.x === from.x) ? from.y :
((to.y - from.y)/(to.x - from.x) * boxSize.x/2.0));
} else if(Math.PI-Math.abs(phi) < theta) { // crosses -x
nearestPoint.x = from.x - boxSize.x/2.0;
nearestPoint.y = from.y + ((to.x === from.x) ? from.y :
(-(to.y - from.y)/(to.x - from.x) * boxSize.x/2.0));
} else if(to.y > from.y) { // crosses +y
nearestPoint.y = from.y + boxSize.y/2.0;
nearestPoint.x = from.x + ((to.y === from.y) ? 0 :
((to.x - from.x)/(to.y - from.y) * boxSize.y/2.0));
} else { // crosses -y
nearestPoint.y = from.y - boxSize.y/2.0;
nearestPoint.x = from.x - ((to.y === from.y) ? 0 :
((to.x - from.x)/(to.y - from.y) * boxSize.y/2.0));
}
return nearestPoint;
}
Theta is the angle to the first box corner, and phi is the actual line angle.
To get the positions of the boxes in client coordinates, you need to use elem.getBoundingClientRect()
, which yields left, top, width, height among other things, and I use to find the centre of a box:
function getCentreOfElement(el) {
var bounds = el.getBoundingClientRect();
return {x:bounds.left + bounds.width/2.0,
y:bounds.top + bounds.height/2.0};
}
Putting all that together, you can draw a line from one element to another.
Upvotes: 5
Reputation: 30135
I made a very crude example of how i'd go about it. It should set you on the right track though:
I used class line for hoverable elements and a data-id attribute for the corresponding element.
HTML:
<p>This is my <span class="line" data-id="important_img">text</span> with this very <span class="line" data-id="important_img2">important</span> word</p>...
<img src="important.jpg" id="important_img" />
<br>
<br>o asf isj biojso jo f ad f
<img src="important.jpg" id="important_img2" />
jQuery:
$('.line').hover(function () {
var $t = $(this);
var $i = $('#' + $t.data('id'));
// find offset positions for the word (t = this) and image (i)
var ot = {
x: $t.offset().left + $t.width() / 2,
y: $t.offset().top + $t.height() / 2
};
var oi = {
x: $i.offset().left + $i.width() / 2,
y: $i.offset().top + $i.height() / 2
};
// x,y = top left corner
// x1,y1 = bottom right corner
var p = {
x: ot.x < oi.x ? ot.x : oi.x,
x1: ot.x > oi.x ? ot.x : oi.x,
y: ot.y < oi.y ? ot.y : oi.y,
y1: ot.y > oi.y ? ot.y : oi.y
};
// create canvas between those points
var c = $('<canvas/>').attr({
'width': p.x1 - p.x,
'height': p.y1 - p.y
}).css({
'position': 'absolute',
'left': p.x,
'top': p.y,
'z-index': 1
}).appendTo($('body'))[0].getContext('2d');
// draw line
c.strokeStyle = '#f00';
c.lineWidth = 2;
c.beginPath();
c.moveTo(ot.x - p.x, ot.y - p.y);
c.lineTo(oi.x - p.x, oi.y - p.y);
c.stroke();
}, function () {
$('canvas').remove();
});
Demo:
Upvotes: 3