Wednesday, August 22, 2012

Analytical fix for IE rotation origin

The problem

For a recent project I needed to create a client side image editor that allows rotating HTML elements.
The editor outputs an HTML page that contains the elements with the rotation applied on them as CSS rules.

The specification were as follows:
  1. The editor should run on a modern browser (i.e. - not IE<9)
  2. The result page should run on an old browser (i.e. including IE >= 7)
  3. The result page should contain only HTML and CSS, not JS (since JS might be disabled on the viewer).
In modern browsers, CSS transforms can rotate elements and you can also specify the rotation origin. However, in IE<9, they are not available so we are left with the Microsoft specific "DXImageTransform".

The problem with  DXImageTransform, is that the transform origin is not controllable by CSS and is calculated differently from the default in other browsers (which is to rotate around the center of the element).
So for IE, we need to have specific CSS rules that will fix the position of the rotated image to match the result we would get on a modern browser.

How IE positions rotated items

disclaimer: I understood the following from reading stuff online, and playing with the browser, I am not certain this is accurate or independent of other properties.

For the following demo, I assume you are using a modern browser. Consider 2 nested div and a 100X100 size (a red div inside a blue div):


Since they are completely on top of each other, you can only see the inner red div. Now let's rotate the inner div (using our current modern browser css transform).



As you 'should' see, the inner div is rotated by 40 degrees while remaining concentric with the container div.
For that reason, it "sticks out" from all four sides of the container.
in IE, however we would have gotten something like this:



Since IE moves the element just enough so it will never "stick out" the top or left side, just the bottom and the right.

Existing solutions

There are several plugins (jQuery or not) that handle cross browser rotations. Most of them work by first doing a feature detection and then applying the css accordingly.
They handle the IE problem by measuring the element size before and after the rotation. After the rotation, the offsetWidth and offsetHeight properties of the rotated element give the size of the bounding rectangle of the element and not of the element itself. The difference between the center of the bounding rectangle and the element itself is the dislocation we need to account for - problem solved.

However... I couldn't use this solution since it requires the fix to be calculated in JS on an older browser. Since my editor runs on a modern browser it can't measure the size difference and since I am not allowed to use JS on the viewer - I can't apply the rotation programmatically there also.

My solution

For my case, I have to calculate all the fixes in advance, so instead of measuring the bounding rectangle size in the browser, I am calculating it analytically using simple trigonometry.
I will not elaborate the calculation here unless someone explicitly wants me to, but here are the results:
// These variables are all you need to calculate the fix
var h = 80; // height in pixels
var w = 20; // width in pixels
var deg = 30; // rotation degrees
////////////////////////////////////////////////////////

// Calculate the radian and wrap around PI/2
var rad = deg * Math.PI / 180;
rad %= 2 * Math.PI;
if (rad < 0) rad += 2 * Math.PI;
rad %= Math.PI;
if (rad > Math.PI / 2) rad = Math.PI - rad;

// Precalculate cos and sin
var cos = Math.cos(rad);
var sin = Math.sin(-rad);

var top_fix = (h - h * cos + w * sin)/2;
var left_fix = (w - w * cos + h * sin)/2;

Now that I have the numbers, I still need to apply them without using JS.
To do that, I add another div in between the container and the inner div with IE conditional css (for details on this great method see here).
We style the div with a relative position and the left,top fix according to the calculation.

Demo

To see this at work, checkout this fiddle, It should show a blue rotated div in a modern browser and a red rotated div in IE<9, and in the same position. Notice the calculation of the IE fix is analytical and does not use DOM method so it can run on any browser apriori with the same results.

Problem solved - let's go eat!