Skip to content

Correcting Transform Origin and Translate in IE

heygrady edited this page Sep 14, 2010 · 15 revisions

The matrix filter in IE has some severe limitations, not the least of which is that matrices are not something most people are used to calculating. While calculating a matrix can be easily done in JavaScript using the Sylvester library, IE also lacks support for transform-origin and translate(). This means it’s not an easy task to make a transformation look the same in IE as it does in Mozilla or WebKit.

The jQuery Transform plug-in handles these calculations automatically. There are a few articles that got me going on this:

Correcting Transform Origin

IE kind of locks the transform origin at 0, 0. However, IE also glues the top-most and left-most pixels of the transformed object to the top and left of the original object. This means that the top-left corner of the transformation is rarely where you’d expect it to be; you can’t simply set the transform-origin to 0, 0 in the other browsers. Correcting for this is not an easy task but the core of the solution relies on relative positioning; calculating the top and left takes some math.

transform-origin: left top; in other browsers default in IE

This is what a top-left origin looks like, but this is not what it looks like in IE by default.

Making IE play nice

  1. Remember the height and width! (IE forgets)
  2. Calculate a matrix (we’ll use a matrix for rotate(45)) and apply it to the element
  3. Ensure the element is positioned
  4. Calculate the offset between a 0, 0 origin and our desired origin (50%, 50% is the default in non-IE browsers)
  5. Calculate the new top and left of the transformed element
  6. Set top to offset.y + ty + newTop
  7. Set left to offset.x + tx + newLeft

Remember the height and width

After a matrix is applied to an element IE reports the height and width of the new element accurately which is actually wrong. In all other browsers, a 100px by 100px <div> continues to think it is that size no matter how it is transformed. IE on the other hand returns the resulting dimensions. So if a 100px by 100px <div> is rotated 45deg, IE correctly reports that the <div> is actually 141px by 141px. But this is a huge hassle if we’re trying to run the original <div>’s dimensions through a matrix to figure out how to re-position it.

There are two potential solutions to the issue:

  • Remember $('.example').height(), $('.example').width(), $('.example').outerHeight() and $('.example').outerWidth() in variables. (This is how I fixed this initially.)
  • Remove the matrix temporarily, read the required dimensions, and then re-apply the matrix. (This is how I’ll fix this the next time I work on the plug-in.)

Calculate a matrix

For this example, we’re going to rotate a <div> 45 degrees. For fun, we’ll start working on a crazy-simple and not very useful plug-in for doing our rotation. Below you’ll see that our example plug-in only supports rotate and only works in IE.

//for fun, we'll write a super simple plug-in
$.fn.transform = function(funcs) {
    this.each(function() {
        var elem = this;
        $.each(funcs, function(func, val) {
            if (func == 'rotate') {
                rotate(elem, val);
            }
        });
    });
}

function rotate(elem, deg) {
    var $elem = $(elem);
    
    if (!$.browser.msie) {
        //do it the easy way
    }
    
    // use parseFloat twice to kill exponential numbers and avoid things like 0.00000000
    var rad = deg * (Math.PI/180),
        costheta = parseFloat(parseFloat(Math.cos(rad)).toFixed(8)),
        sintheta = parseFloat(parseFloat(Math.sin(rad)).toFixed(8));
    
    // collect all of the values  in our matrix
    var a = costheta,
        b = sintheta,
        c = -sintheta,
        d = costheta,
        tx = 0,
        ty = 0;
    
    // Transform the element in IE
    elem.style.filter = 'progid:DXImageTransform.Microsoft.Matrix(M11=' + a + ', M12=' + c + ', M21=' + b + ', M22=' + d + ', sizingMethod=\'auto expand\')';
}

$('.example').transform({rotate: 45}); // rotates 45 degrees

Here you can see the strange idea IE has of the default origin. This looks particularly funny if you animate it. The top-most and left-most edges of the pink box always stay glued the the top and left of the gray box.

Calculate the origin offset

We don’t want to be stuck with IE’s sort-of-top-left origin, we want to use the 50%, 50% origin every other browser uses. We need to use Sylvester to create a matrix that we can use to transform some coordinates. Below you’ll see a function that uses the matrix we calculated to approximate a transform-origin(50%, 50%) in IE. Again, we’re hard-coding as much as possible to make this code easier to understand.

function rotate(elem, val) {
    var $elem = $(elem);
    ...
    // use the Sylvester $M() function to create a matrix
    var matrix = $M([
      [a, c, tx],
      [b, d, ty],
      [0, 0, 1]
    ]);

    // force an origin of 50%, 50%
    transformOrigin($elem, matrix);
}

function transformOrigin($elem, matrix) {
    // undo the filter
    var filter = $elem[0].style.filter;
    $elem[0].style.filter = '';
    
    // measure the element
    var width = $elem.outerWidth();
    var height = $elem.outerHeight();
    
    // re-do the filter
    $elem[0].style.filter = filter;
    
    // The destination origin
    toOrigin = {
        x: width * 0.5,
        y: height * 0.5
    };
    
    // The original origin
    fromOrigin = {
        x: 0,
        y: 0
    };
    
    // Multiply our rotation matrix against an x, y coord matrix
    var toCenter = matrix.x($M([
        [toOrigin.x],
        [toOrigin.y],
        [1]
    ]));
    var fromCenter = matrix.x($M([
        [fromOrigin.x],
        [fromOrigin.y],
        [1]
    ]));
    
    // Position the element
    // The double parse float simply keeps the decimals sane
    $elem.css({
        position: 'relative',
        top: parseFloat(parseFloat((fromCenter.e(2, 1) - fromOrigin.y) - (toCenter.e(2, 1) - toOrigin.y)).toFixed(8)) + 'px',
        left: parseFloat(parseFloat((fromCenter.e(1, 1) - fromOrigin.x) - (toCenter.e(1, 1) - toOrigin.x)).toFixed(8)) + 'px'
    });
}

$('.example').transform({rotate: 45}); // rotates 45 degrees

Here you can see that we’ve correctly centered the origin however, IE has pushed it way to the right because it uses the wacky boundary origin that we saw above. We need to pull the element back to the left.

Calculate the new top and left of the transformed element

Now that we’ve repositioned the element, it’s still in the wrong place. As we noted above, IE will fix the top-most and left-most edges of the element to the original top and left coordinates. To undo this we need to calculate the new positions of all of the corners and determine the new position of the top-most and left-most edges. We do this by plugging all of the coordinates into our matrix and transforming them.

NOTE: We’re recalculating the height and width and using wrap() and unwrap() simply because we’re being lazy and not remembering stuff we’ve calculated previously. So, don’t get too distracted by that; in real code you’d save everything in variables and pass it around instead of constantly asking the DOM for it.

function rotate(elem, val) {
    var $elem = $(elem);
    ...
    // use the Sylvester $M() function to create a matrix
    var matrix = $M([
      [a, c, tx],
      [b, d, ty],
      [0, 0, 1]
    ]);

    // force an origin of 50%, 50%
    transformOrigin($elem, matrix);
    fixIeBoundaryBug($elem, matrix);
}
...
function fixIeBoundaryBug($elem, matrix) {
    // undo the filter
    var filter = $elem[0].style.filter;
    $elem[0].style.filter = '';
    
    // measure the element
    var x = $elem.outerWidth();
    var y = $elem.outerHeight();
    
    // re-do the filter
    $elem[0].style.filter = filter;
    
    // create corners for the original element
    var matrices = {
        tl: matrix.x($M([[0], [0], [1]])),
        bl: matrix.x($M([[0], [y], [1]])),
        tr: matrix.x($M([[x], [0], [1]])),
        br: matrix.x($M([[x], [y], [1]]))
    };
            
    var corners = {
        tl: {
            x: parseFloat(parseFloat(matrices.tl.e(1, 1)).toFixed(8)),
            y: parseFloat(parseFloat(matrices.tl.e(2, 1)).toFixed(8))
        },
        bl: {
            x: parseFloat(parseFloat(matrices.bl.e(1, 1)).toFixed(8)),
            y: parseFloat(parseFloat(matrices.bl.e(2, 1)).toFixed(8))
        },
        tr: {
            x: parseFloat(parseFloat(matrices.tr.e(1, 1)).toFixed(8)),
            y: parseFloat(parseFloat(matrices.tr.e(2, 1)).toFixed(8))
        },
        br: {
            x: parseFloat(parseFloat(matrices.br.e(1, 1)).toFixed(8)),
            y: parseFloat(parseFloat(matrices.br.e(2, 1)).toFixed(8))
        }
    };
    
    // Initialize the sides
    var sides = {
        top: 0,
        left: 0
    };
    
    // Find the extreme corners
    for (var pos in corners) {
        // Transform the coords
        var corner = corners[pos];
        
        if (corner.y < sides.top) {
            sides.top = corner.y;
        }
        if (corner.x < sides.left) {
            sides.left = corner.x;
        }
    }
    
    // find the top and left we set earlier (the hard way)
    $elem.wrap('<div style="position: relative" />');
    var pos = $elem.position();
    $elem.unwrap();
    
    // Position the element
    $elem.css({
        top: pos.top + sides.top,
        left: pos.left + sides.left
    });
}

$('.example').transform({rotate: 45}); // rotates 45 degrees

This is the final transformation, rotated 45 degrees and corrected for a center origin. You can accomplish this cross-browser in CSS using something like this:

.example{
    -moz-transform: rotate(45deg); /* FF3.5+ */
    -o-transform: rotate(45deg); /* Opera 10.5 */
    -webkit-transform: rotate(45deg); /* Saf3.1+, Chrome */
    filter:  progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=-0.70710678, M21=0.70710678, M22=0.70710678, sizingMethod='auto expand'); /* IE6, IE7 */
    -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=-0.70710678, M21=0.70710678, M22=0.70710678, sizingMethod='auto expand')"; /* IE8 */
    zoom: 1;
    position: relative\9; /* the \9 is intentional, @see http://paulirish.com/2009/browser-specific-css-hacks/*/
    top: -21px\9;
    left: -21px\9;
}
Clone this wiki locally