Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw line from html element id to another html element with jquery and canvas

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!

like image 251
user1478200 Avatar asked Feb 15 '23 07:02

user1478200


1 Answers

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/

example output

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.

like image 155
Phil H Avatar answered Feb 17 '23 02:02

Phil H