Some Guy Named Dylan  

Fun With Transform Origins and Matrices

Forcing IE to support a transform origin, transform origin IE. Plus a transform animation class for YUI 2.7.

Contents...

Implementing Transform Origin In IE

The key to this is IE solution is that as the matrix filter is applied to the element, its offsetWidth and offsetHeight are the dimensions of the transformed element's bounding box if and only if the sizing method parameter is set to 'auto expand'.

.matrix-transform{
filter: progid:DXImageTransform.Microsoft.Matrix(M11='0.7071',M12='-0.2479',M21='0.7071',M22='1.1663', sizingMethod='auto expand');

}

If 'auto expand' is not used, then you will have to write code that finds the dimensions of the bounding box of the transformed element and manually set the width and height yourself. If you use 'auto expand' then IE will resize the element for you free of charge.

Another issue is that in IE the element needs to be positioned absolutely so the element will be removed from the document flow. As a workaround I wrap the element in a relatively positioned element sized to the same dimensions.

So with this in mind to support a transform origin in internet explorer the following steps must be taken: Note: for this example we'll be trying to achieve the following styling on an element:

	div.example {
		transform: rotate(45deg) skewX(33deg);
		transform-origin(150% 33%);
		width:100px;
		height: 100px;
	}

The element's bounding box will be a transparent blue and has the dimensions of width:97px, height:187px

The transformed element will have a containing bounding box's left and top properties set in order to illustrate each step.

A rotation of 45 degrees, and skewing about the x axis 33 degrees will result in the following matrix (I've truncated to 4 decimal places):

0.7071 -0.2479
0.7071 1.1663

Let's proceed with the implementation steps.

  1. Always have the original dimensions of the element handy.

    		width:100px;
    		height: 100px;
    
  2. Take the original center of the element and translate it by the negated transform origin.

    	center = (50, 50)
    	origin = (100 * 150%, 100 * 33%) = (150, 33)
    	translated center = (50 - 150, 50 - 33) = (-100, 17)
    
  3. Apply the matrix transform to the result.

    	[[0.7071, -0.2479],
    	 [0.7071, 1.1663]]
    	 * [-100, 17]
    	 = [0.7071 * -100 + -0.2479 * 17, 0.7071 * -100 + 1.1663 * 17]
    	 = [-74.9242, -50.8834]
    
  4. Translate the result by the transform origin.

    	[-74.9242 + 150, -50.8834 + 33]
    	= [75.0758, -17.8834]
    
  5. Subtract from the x value of the result half the width of the bounding box, and from the y value of the result half the height of the bounding box.

    	[75.0758 - 97/2, -17.8834 - 187/2] ~= [26.5, -112]
    
  6. This is where the top left corner of the bounding box should be

    Actual Code:

    	#true-transform {
    	-moz-transform: rotate(45deg) skewX(33deg);
    	-moz-transform-origin: 150% 33%;
    	-webkit-transform: rotate(45deg) skewX(33deg);
    	-webkit-transform-origin: 150% 33%;
    

    Mathematically, this is nothing new, it's really just the algorithm for transforming a point or set of points (think corners of a polygon) about an origin which is:

    1. Translate by the negated value of the origin.
    2. Apply the transform to the points, i.e the matrix.
    3. Translate by the origin.

The tough part is the origin dimensions, I don't have a viewport change listener or anything going on, so if the screen is resized in anyway then the effects won't be consistent in ie since the code has sized the element but all in all I think it's working pretty well.

Examples

Here are some rudimentary examples:

  • Origin Manipulation through Drag 'n Drop
  • Tabview POC using matrix animation
  • Download the Code

    Just Updated 1/07/2011. Could not transform properly in IE because my code was shortsighted and couldn't parse floating point numbers. Never fear, this POC code works now :)

    Just updated 03/10/2010. Could not rotate by increments of 90 degrees due to css style object's choking on really small numbers in exponential format, i.e 6.2345197345e-17.
    Thanks to Denny Wong for finding the issue.

Coding

To apply transform styles element by element I created a utility object named ElementTransformer that adds in some nice fluentness for readability. For convenience I found typing new ElementTransformer to be a pain so using a function caled elTransform makes things a bit more wrist friendly.

This will enable you to write code like:

	elTransform('id-of-element').
	origin('left top').
	matrix({rotate:60/*degrees*/});

All methods that require an angle are normalized to degrees. You're welcome to modify the code as you see fit, personally I find working with bulky floating point numbers due to radians a pain in the wrist.

Another thing is all code (minus the MatrixAnim class) is library agnostic. You're free to drop it in and muddle with it as you see fit. Using a solid library to handle some of the plumbing will probably stablize the code even more.

Animation Class

I also wrote an animation class utilizing YUI 2.7.0.

The code is along the same lines as the coding conventions used there like:

	 var anim = new MatrixAnim('some-element', {
                rotate: {
                    by: 360
                },
                scale :{by:0.25},
                skewY : {from:10, to:45}
            }, 3, YAHOO.util.Easing.bounceOut);
	
	//kick off the animation
	anim.animate ();

Limitations with IE

  • The element must be rendered in order for the transform origin to work properly. Note: It does not need to be visible though.
  • Only works with block level elements like div, p, table, h1-6.

    Trying to transform a table row or table cell element or a span nested within an anchor tag will result in failure.

    Why?

    Because IE requires some work arounds. My solution entails wrapping the transformed element with a relatively positioned div sized to the same dimensions. This wrapper element allows:

    • Layout preservation.
    • transform origin support because the absolutely positioned element will now be positioned relative to its parent.
  • In ie 8, absolutely positioned elements inside of the transformed element are not transformed. They are in version ie 6 and 7 though.

Other Findings

I found an issue with Mozilla's documentation on the matrix method in css.

From the documentation at https://developer.mozilla.org/en/CSS/-moz-transform#matrix on 7/22/2009

-moz-transform: matrix(a, b, c, d, tx, ty)

/* Where a, b, c, d build the transformation matrix

a b
c d

and tx, ty are the translate values.*/

The problem is evident in that the resultant matrix of a 45 degree rotation rounded to 4 decimal places is:

0.7071 -0.7071
0.7071 0.7071

If we had an element with the dimensions of 100x100 pixels it would have the following coordinates:

[[0,0], [100,0], [100,100], [0,100]]

Applying a 45 degree matrix to each point (transform origin at 0,0) yields:

[[0,0], [70.71, 70.71], [0, 141.2], [-70.71, 70.71]]

Applying this matrix in firefox 3.5 or webkit browsers it will rotate the element counter clockwise.

-moz-transform: rotate(45deg); 
-webkit-transform: rotate(45deg);
I am an example
45 degree rotation

-(moz|webkit)-transform: matrix(0.7071, -0.7071, 0.7071, 0.7071, 0, 0);

I am an example
With an equivalent 45 degree matrix, But I'm going the wrong way.
This is an easy fix

Given a matrix

a b
c d

You merely apply it by columns, not rows and this will give the correct result.

-(moz|webkit)-transform: matrix (a, c, b, d, dx, dy)

-moz-transform: rotate(45deg); 
-webkit-transform: rotate(45deg);
I am an example
Been rotated 45 degrees

-(moz|webkit)-transform: matrix(0.7071, 0.7071, -0.7071, 0.7071, 0, 0);

Now I'm rotated the right way.

Some other nuances between firefox and safari. If you apply the transform to an element and query the element's position, for instance this element above Firefox will consistenly return 101 x 101.

Webkit's results are very inconsistent, I wasn't able to track down a pattern or any rhyme or reason to it. In my origin manipulation example I had to get the region of the animated element outside of my drag drop end handler. I found if I got the region of the element in the handler while the element was being animated the element would shoot all over the place.

Last Modified 9/18/2009 4:33 PM PST


References, Inspiration

I was really inspired to do this after seeing these uses of IE's CSS filter property to support matrix transformations.

 
© Dylan Oudyk 2007-2012