Tuesday, September 11, 2012

CSS3 Rotations: Where in the DOM is the Element?

I've been attempting to rotate DOM elements and position them relative to other DOM elements. I wanted to make sure I knew jQuery would position things based on the same point I was using and found that there are several different behaviors to take into account.

I generated several examples in my sandbox as a reference. Using one of the examples to refer to below, here's how it was generated and what each object represents. (The example pulls from the jQuery CDN so will always be the current version. As of this writing, that is 1.8.1). The examples were also tested in all the current versions of IE (9 and 10), Firefox, Chrome, Safari, and Opera and all produced the same results.


Using this image as a reference

First, create a reference axis and center the blue box at this point.

Next, use jQuery.offset() to retrieve the top/left of the blue box, rotate the red box, and then set the red box's top/left to the blue box's top/left:


pos = $bluebox.offset();

$redbox.css({
transformOrigin: 'bottom right',
transform: 'rotate(27deg)'
})
.html('rotated');

$redbox.css({
top: (pos.top) + 'px',
left: (pos.left) + 'px'
});


Now, add in some reference points we can study. The orange dot is where jQuery.offset() reports the top/left position of the red box.


pos = $r.offset();

$('<div>').appendTo($q)
.addClass('box j')
.css({
top: pos.top + 'px',
left: pos.left + 'px'
});


The green dot is positioned based on the DOM offsetTop/offsetLeft properties of the DOM node.


pos = {top: $redbox[0].offsetTop, left: $redbox[0].offsetLeft};

$('<div>').appendTo($q)
.addClass('box d')
.css({
top: pos.top + 'px',
left: pos.left + 'px'
});


And the black-border box is drawn to match the getBoundingClientRect() of the redbox.


box = $redbox[0].getBoundingClientRect();

$('<div>').appendTo($q)
.addClass('box c')
.css({
top: box.top + 'px',
left: box.left + 'px',
height: box.height + 'px',
width: box.width + 'px'
});


If you inspected the DOM and looked at the resulting HTML, it would look something like this:



<canvas height="200" width="200"></canvas>

<div style="-moz-transform-origin: right bottom; -moz-transform: rotate(27deg); top: 691px; left: 234.5px;" class="box r">rotated</div>

<div style="top: 691px; left: 234.5px;" class="box a"></div>

<div style="top: 660.133px; left: 243.217px;" class="box j"></div>

<div style="top: 691px; left: 235px;" class="box d"></div>

<div style="top: 660.133px; left: 243.217px; height: 80.8667px; width: 93.9833px;" class="box c"></div>



Notice how "box r" and "box a" have the same top/left styles but, visually, the top/left corners are not aligned.

Here are several observations:


  1. When positioning any rotated element relative to another element, the top/left stays the same as the unrotated element. If you refer to the examples, you can see that if you did not rotate the red box, its top/left corner will align with the blue box's top/left corner. The only time the top/left point of the rotated element will touch the blue box is when the transform-origin is set to "top left" (this is the second row in the examples).

  2. jQuery uses getBoundingClientRect() in the offset() function to find the top/left position of an element (the position() function uses offset() so it will result in the same behavior). This approach will result in a point not on the element nor will it be the same as what the browser will use when setting the position of the rotated element via CSS styles. Using the DOM offset* properties will, however, provide the same coordinates as the CSS styles. This means you must use care when using jQuery's offset/position functions on rotated objects. The point you get back will not be something you can use in a css() call to position the element (unless you want to position things relative to the rotated element's bounding box). You can use offset() to set the top/left corner of the element's bounding box relative to other items (see that demo here)
  3. It seems that rotating an element relative to its top/left corner will make it easier to position relative to other elements because the top/left corner will be fixed in the rotation and will visually be the same top/left on the rendered page as it is in the DOM. If you want to rotate around a different origin point, you'll have to do some math to find the visual top/left of the element so you can position it accordingly. The default origin (at least in FireFox) is the center of the element. You have to explicitly set transform-origin to "top left" to achieve that behavior.




Based on these experiments, I now know what to expect when positioning rotated elements. I don't consider any of the above behaviors wrong - I just have to be aware how everything behaves and design my solution accordingly. The approach you take is based on what you need to accomplish. There are many different positional values to manage once you start rotating things around on the page. Knowing what the browser and libraries are doing can save a lot of headaches as you design your site.