-
Notifications
You must be signed in to change notification settings - Fork 87
Correcting Transform Origin and Translate in IE
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.
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.
- Remember the height and width! (IE forgets)
- Calculate a matrix (we’ll use a matrix for
rotate(45)
) and apply it to the element - Ensure the element is positioned
- Calculate the offset between a 0, 0 origin and our desired origin (50%, 50% is the default in non-IE browsers)
- Calculate the new top and left of the transformed element
- Set
top
to offset.y + ty + newTop - Set
left
to offset.x + tx + newLeft
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.)
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. It also creates a matrix using Sylvester for no good reason.
//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, val) {
var $elem = $(elem);
if (!$.browser.msie) {
//do it the easy way
}
// use parseFloat twice to kill exponential number 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\')';
// use the Sylvester $M() function to create a matrix
var matrix = $M([
[a, c, tx],
[b, d, ty],
[0, 0, 1]
]);
}
$('.example').transform({rotate: 45}); // rotates 45 degrees
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. 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
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;
}
}
// The the top and left we set earlier
$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