Rotating the Microsoft way

One of the main challenges in putting together the 'virtual record player' demo was how to program the rotating record so that it would look reasonably convincing. The latest flavour of CSS, version 3, will eventually cater for rotation, skewing, and other complex graphical transformations, but at the time of writing (June 2010) each of the rendering engines used by web browsers has its own set of instructions for rotation. Each of these has to be provided as CSS; each browser will apply the instructions that its rendering engine understands, and ignore the others.

Happily, for the majority of browsers, the instruction to rotate an object is straightforward. For example, to rotate 30 degrees, we need:
        -webkit-transform: rotate(30deg);
        -o-transform: rotate(30deg);
        -moz-transform: rotate(30deg);
These three instructions are effected by the Webkit (=Chrome, Safari), Opera and Mozilla (=Firefox) rendering engines respectively, and each performs the rotation about the object's geographical centre. Figure 1 shows the result of applying this instruction to a square: the original object (blue) is shown with a rotated copy (red).
Figure 1: rotation 30° about centre


To carry out the transformation in Microsoft Internet Explorer 6 to 8 is considerably more complicated. We use Microsoft's DXImageTransform filters, and to rotate an object we need to supply a matrix consisting of four numbers between -1 and 1. These numbers turn out to be the results of trigonometrical functions applied to the angle by which we want to rotate.

At this point I had to wrap a wet towel around my head and go back to some O-level trigonometry. If, as with me, it's been a week or two since you had to deal with such things, perhaps a brief explanation will be useful. Figure 2 shows a right-angled triangle, with the other two angles 30° and 60°. (In the diagram, I've made the right angle to be the, say, south-eastern corner, but the arithmetic holds no matter the orientation.) For the other two angles, the following applies.
  • The sine of an angle is the ratio between the lengths of the side of the triangle opposite the angle, and the longest side (the hypotenuse - the side opposite the right angle).
  • The cosine of an angle is the ratio between the lengths of the side next (or adjacent) to the angle, and the hypotenuse.
Figure 2: right-angled triangle


Thus, for the triangle in figure 2, the sine of 30° (written sin 30°) is 0.5, and the cosine of 30° (written cos 30°) is approximately 0.866. Conversely, the sine and cosine of 60° are 0.866 and 0.5 respectively. It can be guessed fairly readily from this - and demonstrated using Pythagoras' theorem - that, for angles up to 90°, the sine will increase with the angle, as the cosine decreases.

Getting back to IE's required matrix of four numbers, for a required rotation through angle A, these are:
        cos A        -sin A
        sin A         cos A
If A is 30°, then these four values, reading across then down, are:
        0.8660254037844387        -0.5
        0.5                        0.8660254037844387
In order to display the whole of the rotated object, and not just a clipped part the size of the original dimensions, we have to specify
	sizingMethod='auto expand'
The instruction to IE to rotate through 30° will then look like this:
	progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', 
	  "M11=0.8660254037844387, M12=0.5,
	  M21=0.5, M22=0.8660254037844387");
So to do this for any angle, we need a Javascript function that can return the appropriate trigonometrical ratios, given a desired angle. Here it is. Note that Javascript trigonometrical functions take the angle in radians: there are 2π radians in 360 degrees, and so a radian works out at about 57°. This conversion is carried out in the second line of the function.
	function ieSetRotationArray(A) {
	  var MMatrix= new Array();
	  var rad = A / (180 / Math.PI);
	  var cosA = Math.cos(rad);
	  var sinA = Math.sin(rad);
	  MMatrix[0] = cosA;  // M11
	  MMatrix[1] = -sinA; // M12
	  MMatrix[2] = sinA;  // M21
	  MMatrix[3] = cosA;  // M22
	  return MMatrix;
	}


And now we come to the tricky bit. For reasons perhaps best known to itself, IE doesn't rotate an object about its centre; in fact, rotation in IE doesn't involve a centre worthy of the name. What happens is this: as the rotational angle increases, the top left-hand corner of the object moves to the right, along the top edge of the original (unrotated) object, while the bottom left-hand corner moves up along the left-hand edge of the original. The left-hand edge thus performs a kind of inverted 'sliding ladder' act as the object rotates. A rotation of 30° yields the situation shown in figure 3.

Figure 3: rotation IE-style.


Clearly we have a bit of a problem here, in that the centre of the rotated image has moved away from where it should be. We have to move the rotated image up and to the left somewhat, in order that the two images are once again concentric. To establish the distance from the original centre to the new centre, for a square, is comparatively simple. Let's say that the square is 100 units on a side; the 'envelope' with sides parallel to the screen - shown here in yellow - that completely encloses the rotated square, after a rotation of 30°, will in this case have a side of length 136.6:

Figure 4: calculating offset for repositioning.


The rotated image is surrounded by four identical triangles, and the subtrahend required is thus, for an angle A and a side length l:
	l * (sin A + cos A - 1) / 2
As the image is a square, the same amount should be subtracted from the left and top property values. In this case, the amount is approximately 18.3.

Armed with this information, and the program code to do the arithmetic, the rotating record image can be created. Rotation in a browser - whether using CSS3, or the convoluted approach just described - is appreciably 'heavy' on processing activity, and in an effort to reduce the work that the CPU has to do, it is more efficient to create all the required rotated images prior to starting playback.

Moving pictures become appreciably jerky if the frame rate is too low. For the rotating record, 15 frames per second provides an acceptable trade-off between processor use and getting the animation to look anything like it should. Furthermore, my computer is six years old, and I figured that if the animation looked bearable on my system then it should do so on most. 15 frames a second means that a record rotating at 45rpm will need twenty images for a complete revolution, at 18° intervals. At 78rpm, the number of frames needed is eleven, and a separation of about 33°; this can unfortunately still look a little jerky because of this wider angle. When a record is dragged on to the virtual turntable, the system creates all the required rotated images in a 'stack', and then hides all but one. Every 66 milliseconds the next image in the 'stack' is shown and the old one hidden.

The other rotation needed for a record player is that of the playing arm on its pivot. We now have to deal with an image that is an oblong, rather than a simple square; things are complicated further by the fact that its axis of rotation is not in the geographical centre. Fortunately, CSS3 comes to the rescue, and for non-IE browsers we can specify the axis, in this case as a percentage offset from left and top:
	-webkit-transform-origin: 40% 33.3%; 
	-o-transform-origin: 40% 33.3%; 
	-moz-transform-origin: 40% 33.3%; 
For IE, though, the calculations become more complex. In the following example, we are attempting to rotate an oblong 50 units by 150, through 30°, around an axis situated 40% of the way across the shape and one-third of the way down. Applying the matrix derived above, the Microsoft transform yields a result like that in figure 5:

Figure 5: result of Microsoft rotation of oblong.


As before, to calculate the required offsets, we need to find out where the centre of rotation has wandered off to. On the original image, we find the geographic centre (G), and measure the angle and distance from it to the required centre of rotation (R). After applying the transformation, we measure off that distance from the new geographic centre (G') at the angle found plus the desired angle of rotation, and that should lead us to the new centre of rotation (R').

Figure 6: how the geographic and rotational centres have shifted.


Figure 6 shows a close-up of the relevant area of the diagram. The centre of rotation we want is 40% across and 33.3% down the original image, that is, at a point (20,50) relative to its top left corner. The geographic centre of the image is, trivially, halfway across and halfway down at (25,75). Javascript provides a function Math.atan2(y,x) which gives the angle between an origin at (0,0) and the point (x,y), and we can use this to work out the angle from the geographic centre G to the desired rotational centre R:
	angle = Math.atan2(Ry-Gy,Rx-Gx);
The distance is readily calculated using Pythagoras' theorem. In Javascript, this looks like:
	distance = Math.sqrt(Math.pow(Ry-Gy,2) + Math.pow(Rx-Gx,2));
If we plug our coordinates into these two formulas, we get the angle as approximately 258.7° and the distance as 25.5 units.

The geographic centre of the rotated oblong, G', is found by halving the dimensions of the new 'envelope' (shown in yellow in above figures). These dimensions are found by calculating
	width = |h sin A| + |w cos A|
        height= |w sin A| + |h cos A|
where w and h are the original width and height. From here, we find R' by simply measuring off 25.5 units on a bearing of 288.7° from G', using the following formulas:
	R'x = distance * cos (new angle) + Gx
        R'y = distance * sin (new angle) + Gy
Again, in our example this puts R' at (67.3, 53.3). Finally, the left and top subtrahends needed to shift the rotated oblong, in order to bring R' back to R, are given by the difference between those two sets of coordinates. Thus, we need to shift left by 47.3 units, and up by 3.3.

Here is a Javascript function that does all the above:
	function microsoftRotation(vObjectId,vAngle,vPivotXPct,vPivotYPct) {
	  // Create reference to original object
	  var obj=document.getElementById(vObjectId);
	  // Find out its width and height
	  var objWidth=parseInt(obj.style.width);
	  var objHeight=parseInt(obj.style.height); 
	  // Work out where the axis of rotation (pivot) is, given 
	  // the desired percentage across and down
	  var pivotX=(objWidth * vPivotXPct/100);
	  var pivotY=(objHeight * vPivotYPct/100);
	  // Calculate the DXImageTransform matrix from function (q.v.)
	  var ieRotationMatrix=ieSetRotationArray(vAngle);
	  // Calculate the angle and distance from the geographic 
	  // centre of the object to the desired axis of rotation
	  var angleGeogCentreToPivotRAD = Math.atan2(pivotY-objHeight/2,pivotX-objWidth/2);
	  var distGeogCentreToPivot = 
	    Math.sqrt(Math.pow(objHeight/2 - pivotY,2) + Math.pow(objWidth/2 - pivotX,2));
	  // Establish the dimensions of the rotated object's
	  // new 'envelope', by trigonometry
	  var newWidth=Math.abs(ieRotationMatrix[2]*objHeight) + Math.abs(ieRotationMatrix[3]*objWidth);
	  var newHeight=Math.abs(ieRotationMatrix[2]*objWidth) + Math.abs(ieRotationMatrix[3]*objHeight);
	  // Find new geographical centre of 'envelope'
	  var newGeogCentreX=newWidth / 2;
	  var newGeogCentreY=newHeight / 2;
	  // Add desired rotation angle to angle found earlier (in radians)
	  var newAngleGeogCentreToPivotRAD= vAngle * Math.PI / 180 + angleGeogCentreToPivotRAD;
	  // Find new axis of rotation by measuring out from 
	  // the new geographic centre by the distance found
	  // earlier, at the new angle
	  var newPivotX=Math.cos(newAngleGeogCentreToPivotRAD) * distGeogCentreToPivot + newGeogCentreX;
	  var newPivotY=Math.sin(newAngleGeogCentreToPivotRAD) * distGeogCentreToPivot + newGeogCentreY;
	  // Find required offsets by subtracting original from new pivot coordinates
	  var ieOffsetX=newPivotX - pivotX;
	  var ieOffsetY=newPivotY - pivotY;
	  // String together the parameters for DXImageTransform
	  var ieMFigures="M11="+ieRotationMatrix[0]+", M12="+ieRotationMatrix[1]
		+", M21="+ieRotationMatrix[2]+", M22="+ieRotationMatrix[3];
	  // Shift rotated image left and up by the offsets found
	  obj.zoom=1;
	  obj.style.left=(origLeft-ieOffsetX) + "px";
	  obj.style.top=(origTop-ieOffsetY) + "px";
	  obj.style.filter="progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', "+ieMFigures+") ";
	}
Back to previous page