18

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/

Tariq M Nasim
  • 1,278
  • 11
  • 24
Bobby Francis Joseph
  • 606
  • 2
  • 14
  • 34

6 Answers6

9

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/

Mohamed Ali
  • 551
  • 5
  • 17
  • How to contain **#inner** div, either by `overflow` or `containment` or `?`, so that it remains inside **#outer**? Approach not withstanding is **+1**, simply amazing! – arttronics Jun 05 '12 at 23:29
  • 1
    Since we are simply changing the CSS top and left properties, it will be easy to apply containment by applying upper and lower bands on the values. Check this fiddle http://jsfiddle.net/4XJgP/ – Mohamed Ali Jun 06 '12 at 06:45
  • 1
    The main disadvantage - as I point out in my own answer - is that `transformOffset` is a very specific function only applicable to this very specific transformation (rotation) and will mess up if any other transformation is added to any parent element (which is quite likely if you're already doing stuff with rotations). – David Mulder Jun 11 '12 at 12:08
  • @DavidMulder True. I did some testing on the effect of nested rotations. I came up with an initial solution that still needs to be fixed. Right now I get the directions correctly but there is a drag offset because I'm not detecting the center of rotation of the second element correctly. I tried your method to get the matrix but it doesn't provide the total transformation of the inner element. Also css `top` and `left` are not accounted for in the `matrix`. Please check http://jsfiddle.net/mohdali/kETcE/5/ – Mohamed Ali Jun 11 '12 at 14:04
  • @MohamedAli: True, but 'adding' the matrices of all the parent elements to eachother is quite a lot easier than, say, add the rotations of two elements with two different centers of rotations. Or of a parent element with a rotation and a skew for example. – David Mulder Jun 11 '12 at 14:07
  • My previous comment "For IE8, the use of Chrome Frame is required because IE8 CSS3 Rotate version via -matrix is limited only to images, not div's." was removed since it was not correct. IE **can rotate div's** when set [correctly](http://www.useragentman.com/IETransformsTranslator/) (via a plugin I imagine.). – arttronics Jun 11 '12 at 23:46
  • @MohamedAli, I recommend placing your other jsFiddles in your Answer as some readers may not realize those alternate versions are powerful solutions too. – arttronics Jun 11 '12 at 23:51
  • 1
    @arttronics Thanks for the suggestion. I edited my answer to add a combined script for containment and cascaded rotations. – Mohamed Ali Jun 12 '12 at 14:22
  • I am followed your instructions I am getting correct positions after drag.But those draggable div is not dropping and if I comment helper code then it is dropping Please look at http://jsbin.com/xozase/1/edit?js,output .only one time I can able to dragging. please look at once – Sarath Babu Nuthimadugu Apr 17 '15 at 13:23
3

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.

David Mulder
  • 26,123
  • 9
  • 51
  • 114
  • I agree that this can work with any type of transform (not just rotation). However your method will return the local transformation matrix only. In order to deal with transformations on the parent elements, I think we need to travese the DOM tree and revese all the translations. – Mohamed Ali Jun 11 '12 at 13:59
  • @MohamedAli: True, but traversing the parents with a simple while loop and 'adding' the matrices should be fairly simple (right now I am not sure whether it would be the dot or cross product of the matrices... or something different (I would need to try it to know)), unlike adding various rotations or rotations with something else, but yeah, you're right that traversing the dom would be necessary, but that would still be quite generic (as you don't need to separately define the parent state(s)). – David Mulder Jun 11 '12 at 14:11
  • You could also simply traverse the parents and convert the x and y coordinates at each step, as I did in http://jsfiddle.net/kB4ra/118/ - which would be a suitable starting point for implementing this method. – Brilliand Jun 11 '12 at 20:18
  • Note that different browsers return the parts of the matrix in different orders, so you'll have to treat it differently depending on which CSS property it came from. – Brilliand Jun 11 '12 at 20:18
  • 2
    @DavidMulder I updated my answer to include traversing the DOM. It is working now with cascaded rotated divs. Can you please check it and apply your solution for the general transformation? – Mohamed Ali Jun 12 '12 at 14:49
2

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/

Community
  • 1
  • 1
Brilliand
  • 13,404
  • 6
  • 46
  • 58
  • Here's a version that comes much closer to handling Firefox correctly: http://jsfiddle.net/kB4ra/118/ (Still needs code to account for transformations, which are apparently rather hard to get information on). – Brilliand Jun 05 '12 at 10:54
  • Found a way that actually works for Firefox. I've edited it into my answer. – Brilliand Jun 05 '12 at 11:05
  • Added handling of #point to my answer. Also, I noticed the `getRotateAngle` function of the jQuery plugin being used to rotate - it would make it feasible to account for rotations in http://jsfiddle.net/kB4ra/118 (in a way that would work even with the mouse outside of #outer). – Brilliand Jun 05 '12 at 12:00
0

It seems to me that if you do not rotate the div, the div exactly follows the mouse. This might be a problem with the plugin..maybe you could simulate the draggable function corretly?

Fazi
  • 3,909
  • 4
  • 27
  • 23
  • I am from a flash background. In flash if we were to rotate the outer movieclip and ask the inner movie clip to follow the mouse it would work easily. There is a local to global / global to local conversion happening and it is handled internally by flash. But here that conversion does not seem to be happening correctly. That seems to be problem here in my limited intelligence :-) – Bobby Francis Joseph Jun 01 '12 at 05:48
0

This basically will do what you need though it is buggy. Bind the drag event handler, intercept the ui object and modify it to use the offset X and Y of the parent element. All of the X, Y, top, left etc. are in those objects. I will try to get you a better example sometime when today when I get a bit more time. Good luck!

http://jsfiddle.net/kB4ra/107/

thomallen
  • 1,926
  • 1
  • 18
  • 32
  • Thank you so much.. I had almost given up on it.. I will start a bounty.. 250 points.. cheers... – Bobby Francis Joseph Jun 05 '12 at 06:53
  • 1
    @thomallen I tried the fiddle but the small, grey rectangle is not draggable at all in FF 12.0 and it flickers a lot in webkit browsers. – toniedzwiedz Jun 05 '12 at 09:08
  • @Tom, The problem with the flickering is due to the event.offsetX & Y returning `NaN` time to time. – Starx Jun 05 '12 at 11:02
  • In Chrome, the flickering is caused by event.offsetX & Y returning 1, not NaN. This in turn is caused by the mouse moving over the #inner element, resulting in the offset coordinates being relative to it. The Firefox breakage is due to event.offsetX & Y always returning undefined. – Brilliand Jun 05 '12 at 11:08
  • @Brilliand, Well, the chrome returned `NaN` in my experiment – Starx Jun 05 '12 at 15:09
  • Bobby, I will slay this problem tonight if I get a minute. Sorry for the delay. – thomallen Jun 11 '12 at 20:24
-1

may be this is issue of your jquery library or you can check this by assigning z-order value of inner div and outer div make sure that you give higher number to inner div.