Tuesday, June 19, 2012

Drawing a diagonal line in HTML/CSS/JS (with jQuery)

A simple and practical post this time. How do you draw a diagonal line in your JavaScript-powered web-application that uses HTML / CSS (and no “canvas”, since it is not trivial to keep it coordinated with regular HTML elements on the page).

This uses jQuery, but the same principles can be applied to pure JavaScript or with another library.

In my case, I have an organisation chart with nodes, which allows the user to connect the nodes visually. A way I solved it so far is have a “link” icon on each node, when the user presses it they switch to “link mode” — an on-screen line starts at the origin node and follows the mouse cursor, until the user clicks on another node (or right-clicks / presses Escape to exit the link mode).

So how do I draw that line between arbitrary points A and B on the webpage?

The answer: Just draw it and rotate it.

Warning — details below may contain traces of math, please be careful if you're allergic to it. Though its density is so low that it is unlikely to hurt anybody, honest!

Details

First of all we need to establish that HTML blocks are rectangles. So the original line will be a rectangle of desired length and width. Since it will be rotated, length is a calculated value.

Here's the initial CSS style for this link line:
#new-link-line {
    position: absolute; /* allows to position it anywhere */
    width: 3px; /* your chosen line width */
    background-color: #06a; /* line color */
    z-index: 1000; /* make sure this is above your other elements */
    -webkit-transform-origin: top left;
    -moz-transform-origin: top left;
    -o-transform-origin: top left;
    -ms-transform-origin: top left;
    transform-origin: top left;
    /* transform-origin sets rotation around top left (instead of the geometrical center) */
}

This is a 3-pixel-wide rectangle with no height set. Height will be the length of the link line, so to complete the task we need to determine its length and the rotation angle.
The battle plan

As you can see from above, we're dealing with a so called “right triangle”, and it is very easy to calculate various lengths / widths / angles for it knowing what we know — namely, the coordinates of the points A and B. Our link line is the green side.

For my particular task this code is called from the mouse move event, so I'll assign the handler:
// Assign the mouse move event
$(document).mousemove(linkMouseMoveEvent);

Length is a square root of the sum of squares of the red sides from the picture above. Each side is the absolute value of difference of coordinates: Xa - Xb and Ya - Yb, thus the following code sets the required length:
function linkMouseMoveEvent(event) {
    // Initialize values of originX and originY
    // …

    var length = Math.sqrt(
        (event.pageX - originX) 
        * (event.pageX - originX) 
        + (event.pageY - originY) 
        * (event.pageY - originY));

    $('#new-link-line').css('height', length);

I assume that top and left properties of the #new-link-line are already set to correct values — it would be inefficient to do that on every mouse move.

Next and last step is to calculate the rotation angle. It uses fairly simple trigonometry, yet I am aware that that word itself (trigonometry!) makes some people scream in fear, so if you're interested you can deduce the formula from the code below:
var angle = 180 / 3.14 * Math.acos((event.pageY - originY) / length);

// Negate the angle if mouse pointer is to the right of the origin point
if(event.pageX > originX)
    angle *= -1;

$('#new-link-line')
    .css('-webkit-transform', 'rotate(' + angle + 'deg)')
    .css('-moz-transform', 'rotate(' + angle + 'deg)')
    .css('-o-transform', 'rotate(' + angle + 'deg)')
    .css('-ms-transform', 'rotate(' + angle + 'deg)')
    .css('transform', 'rotate(' + angle + 'deg)');

This code should be inserted into the linkMouseMoveEvent function.

That's it!

If you're interested, here's how I exit the link mode by right-click or Escape button press:
// Cancel on right click
$(document).bind('mousedown.link', function(event) {
    if(event.which == 3)
        endLinkMode();
});

$(document).bind('keydown.link', function(event) {
    // ESCAPE key pressed
    if(event.keyCode == 27)
        endLinkMode();
});

function endLinkMode() {
    // Whatever needs to be done to exit link mode
    // …
}

Note that both binds are to events with “.link” suffix — this allows to easily unbind them when needed, not touching the other potential handlers of the same events.

Here's a working example (I hope this works, blogger.com doesn't exactly make it easy to include JS files on pages!), also posted on jsFiddle:
Click!

This approach allows to even set shadows and borders to the line. One downside is that it won't work below IE9 — so you may need to look for alternatives if your environment forces you to support things like IE8 (about 11% of total Internet population, so could be important in some cases).

​Quite a lot of code in this post, please let me know if I got anything wrong — or if it can be done better!

18 comments:

  1. Great work! Any ideas on how this could be extended to "targets" too? Say e.g. if you wanted to do a game for children that pairs together some animals and some fruits (by drawing lines between them).

    ReplyDelete
    Replies
    1. Hi!

      In order to handle this, I add the following event to the objects which the link will be connected to:
      $('.node').bind('click.link', linkSelectNodeEvent);

      And then in that function:
      // check for left click
      if(event.which == 1) {
      // .selected class is added on initiating linking mode
      var source = $('.node.selected');
      var target = $(this);
      // .... and continue with your logic

      I hope this helps :)

      Delete
  2. Thanks for sharing. You solved my doubt :)

    ReplyDelete
  3. How are you going to make it work in IE8?

    ReplyDelete
    Replies
    1. Yes, it is a good question, and I haven't looked for a solution yet — CSS3 transforms don't work there, yet I did see mentions of filters which could allow rotating an element.

      I will post an update if I find a working solution.

      Delete
  4. Is there any way to stop a line without removing it?? i was trying to implement the Anton Maslo example but i cannot reach the target, i would like to stop the line as soon as i click on another div so the line shd be link both without removing.
    Any help will be really appreciated

    Thx Guys.

    BTW very good example

    ReplyDelete
    Replies
    1. Hi Robert!

      Please check the source of this page, you'll see that I have removing code in the endLinkMode function, as follows:

      function endLinkMode() {
      $('#new-link-line').remove();
      $(document)
      .unbind('mousemove.link')
      .unbind('click.link')
      .unbind('keydown.link');
      }

      If I understand your question correctly, then you just need to do something instead of removing the line — perhaps create a connector line (instead of #new-link-line, in case you need to reuse it), or just leave it be.

      I hope this helps!
      Anton

      Delete
  5. This was handy. I used it in this demo I made http://jsbin.com/owakew/4/edit

    ReplyDelete
    Replies
    1. Looks great, Aaron, thanks for the feedback!

      Delete
  6. Hey this is cool. Thanks for sharing.

    I am working on similar thing. I want to generate diagonal line as I scroll my page. I dont want to use mouse event. Can you please help me with this. Thanks in advance.

    ReplyDelete
    Replies
    1. Thanks for the kinds words. I don't really understand what you try to achieve, so can't really help, sorry. There are scroll events that you can hook into, if that is what you need.

      Delete
  7. Awesome, helped me a lot. Thank you.

    ReplyDelete
  8. I believe there is a typo above. The code
    var length = Math.sqrt(
    (event.pageX - originX)
    * (event.pageX - originY)
    + (event.pageY - newLinkStartTop)
    * (event.pageY - newLinkStartTop));

    The third line should be:
    * (event.pageX - originX)

    (it's this way in the jsFiddle)

    Also, I think the variable newLinkStartTop is confusing, as it's never defined anywhere in this explanation. in the jsFiddle, it's using origin, as it should.

    Just wanted to point this out. But thank you again for this demonstration, it's proving very helpful for my current project!

    ReplyDelete
    Replies
    1. Hi Corey, thank you so much for your attentive feedback — I've corrected both issues, really appreciate you helping me improve this post!

      Delete
  9. Fixed the “click” button demo, looks like last corrections to the post re-formatted some of the included JS code.

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Hey there, thank you so much for this example! It helped me get through one part of a project I'm working on. I fiddled around a bit with it to make it do the "connected" behavior I was aiming for.

    Thanks again!

    ReplyDelete
    Replies
    1. Hi Elliot, I am really glad I could be of service, and cool to see what you did with the fiddle, good job!

      Delete