Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inner div inside an outer rotated div does not follow mouse in case dragging with jQuery UI

I have an inner div inside an outer div. The inner div is draggable and outer is rotated through 40 degree. This is a test case. In an actual case it could be any angle. There is another div called point which is positioned as shown in the figure. ( I am from a flash background . In Flash if I were to drag the inner div it would follow the mouse even if its contained inside an outer rotated div.) But in HTML the inner div does not follow the mouse as it can be seen from the fiddle. I want the div 'point' to exactly follow the mouse. Is this possible. I tried to work it using trignometry but could not get it to work.

http://jsfiddle.net/bobbyfrancisjoseph/kB4ra/8/

like image 326
Bobby Francis Joseph Avatar asked May 30 '12 06:05

Bobby Francis Joseph


3 Answers

Here is my approach to this problem.

http://jsfiddle.net/2X9sT/21/

I put the point outside the rotated div. That way I'm assured that the drag event will produce a normal behavior (no jumping in weird directions). I use the draggable handler to attach the point to the mouse cursor.

In the drag event, I transform the drag offset to reflect the new values. This is done by rotating the offset around the outer div center in the opposite direction of the rotation angle.

I tested it and it seems to be working in IE9, Firefox, and Chrome.

You can try different values for angle and it should work fine.

I also modified the HTML so it is possible to apply the same logic to multiple divs in the page.

Edit:

I updated the script to account for containment behavior as well as cascading rotations as suggested in the comments.

I'm also expirementing with making the outer div draggable inside another div. Right now it is almost working. I just need to be able to update the center of the dragged div to fix the dragging behavior.

Try Dragging the red div.

http://jsfiddle.net/mohdali/kETcE/39/

like image 97
Mohamed Ali Avatar answered Oct 19 '22 20:10

Mohamed Ali


I am at work now, so I can't do the job for you, but I can explain the mathematics behind the neatest way of solving your problem (likely not the easiest solution, but unlike some of the other hacks it's a lot more flexible once you get it implemented).

First of all you must realize that the rotation plugin you are using is applying a transformation to your element (transform: rotate(30deg)), which in turn is changed into a matrix by your browser (matrix(0.8660254037844387, 0.49999999999999994, -0.49999999999999994, 0.8660254037844387, 0, 0)). Secondly it is necessary to understand that by rotating an element the axis of the child elements are rotate absolutely and entirely with it (after looking for a long time there isn't any real trick to bypass this, which makes sense), thus the only way would be to take the child out of the parent as some of the other answers suggest, but I am assuming this isn't an option in your application.

Now, what we thus need to do is cancel out the original matrix of the parent, which is a two step process. First we need to find the matrix using code along the following lines:

var styles = window.getComputedStyle(el, null);

var matrix = styles.getPropertyValue("-webkit-transform") ||
             styles.getPropertyValue("-moz-transform") ||
             styles.getPropertyValue("-ms-transform") ||
             styles.getPropertyValue("-o-transform") ||
             styles.getPropertyValue("transform");

Next the matrix will be a string as shown above which you would need to parse to an array with which you can work (there are jquery plugins to do that). Once you have done that you will need to take the inverse of the matrix (which boils down to rotate(-30deg) in your example) which can be done using for example this library (or your math book :P).

Lastly you would need to do the inverse matrix times (use the matrix library I mentioned previously) a translation matrix (use this tool to figure out how those look (translations are movements along the x and y axis, a bit like left and top on a relatively positioned element, but hardware accelerated and part of the matrix transform css property)) which will give you a new matrix which you can apply to your child element giving you the a translation on the same axis as your parent element.

Now, you could greatly simplify this by doing this with left, top and manual trigonometry1 for specifically rotations only (bypassing the entire need for inverse matrices or even matrices entirely), but this has the distinct disadvantage that it will only work for normal rotations and will need to be changed depending on each specific situation it's used in.

Oh and, if you are now thinking that flash was a lot easier, believe me, the way the axis are rotated in HTML/CSS make a lot of sense and if you want flash like behavior use this library.


1 This is what Mohamed Ali is doing in his answer for example (the transformOffset function in his jsFiddle).


Disclaimer, it has been awhile since I have been doing this stuff and my understanding of matrices has never been extremely good, so if you see any mistakes, please do point them out/fix them.

like image 3
David Mulder Avatar answered Oct 19 '22 20:10

David Mulder


For Webkit only, the webkitConvertPointFromPageToNode function handles the missing behavior:

   var point = webkitConvertPointFromPageToNode(
       document.getElementById("outer"),
       new WebKitPoint(event.pageX, event.pageY)
   );

jsFiddle: http://jsfiddle.net/kB4ra/108/

To cover other browsers as well, you can use the method described in this StackOverflow answer: https://stackoverflow.com/a/6994825/638544

function coords(event, element) {
  function a(width) {
    var l = 0, r = 200;
    while (r - l > 0.0001) {
      var mid = (r + l) / 2;
      var a = document.createElement('div');
      a.style.cssText = 'position: absolute;left:0;top:0;background: red;z-index: 1000;';
      a.style[width ? 'width' : 'height'] = mid.toFixed(3) + '%';
      a.style[width ? 'height' : 'width'] = '100%';
      element.appendChild(a);
      var x = document.elementFromPoint(event.clientX, event.clientY);
      element.removeChild(a);
      if (x === a) {
        r = mid;
      } else {
        if (r === 200) {
          return null;
        }
        l = mid;
      }
    }
    return mid;
  }
    var l = a(true),
        r = a(false);
    return (l && r) ? {
        x: l,
        y: r
    } : null;
}

This has the disadvantage of not working when the mouse is outside of the target element, but it should be possible to extend the area it covers by an arbitrary amount (though it would be rather hard to guarantee that it covers the entire window no matter how large).

jsFiddle: http://jsfiddle.net/kB4ra/122/

This can be extended to apply to #point by adding a mousemove event:

$('#outer').mousemove(function(event){
   var point = convertCoordinates(event, $("#outer"));

    $("#point").css({left: point.x+1, top: point.y+1});          
});

Note that I adjust the x and y coordinates of #point by 1px to prevent it from being directly underneath the mouse; if I didn't do that, then it would block dragging #inner. An alternative fix would be to add handlers to #point that detect mouse events and pass them on to whichever element is directly underneath #point (and stopPropagation, so that they don't run twice on larger page elements).

jsFiddle: http://jsfiddle.net/kB4ra/123/

like image 2
Brilliand Avatar answered Oct 19 '22 20:10

Brilliand