Monday, May 24, 2010

Rotating in jquery (and IE8 problems)

Rotating an image in jquery is harder than you might think. Harder than I had imagined it would be, at least. The first problem is that it is not part of either JQuery or JQueryUI, so you are out there in the wilderness where the 3rd party plugins live. And, trust me, it can get wild out there.

I first tried jqueryrotate. It is quite heavy, at around 10KB, but the main reason I abandoned it is that it didn't work properly for me (sorry, I cannot even remember why now).

Next I tried jquery-rotate and I soon got this working in Firefox 3. My code also worked in Safari and Firefox 3.6 first time. IE6/7/8 were the problem. Under the hood all these plugins use DXImageTransform.Microsoft.Matrix for the IE browsers (which works back to IE5 apparently), and Canvas for all other browsers (which works from at least Firefox 3). They even allow rotation of any angle, though I only needed to rotate in steps of 90 degrees.

(By the way jquery-rotate seems to be unmaintained, so I'm using this unofficial patch even though my testing didn't see the problems it fixes.)

Here is my code:
  $('#img').rotate(rotation,true);

Then:
  if(rotation==90 || rotation==270){
    $('#img').width(300);
    $('#img').height(225);
    }
  else{
    $('#img').width(300);
    $('#img').height(400);
    }

(To keep that code sample clearer I've hard-coded the sizes, to assume an image that has a 3:4 aspect ratio in its original position). The point of this code is to scale the image down to fit in the layout. This was where the first cross-browser problem appears. When Firefox/Safari rotate it they are creating a new object of the new dimensions. As far as I can tell when IE rotate it they are creating an optical illusion. It appears to have been rotated but when you read its width/height you get the numbers for the original position; and the same when you try to set them. (As an aside I tried a number of ideas to get around this underlying issue, but all ended in failure, so I guess it is just the way IE works.)

Tinaysh... Tenacish... Tenaciousness is my middle name, even if it is hard to spell. This code does the job:
  if(rotation==90 || rotation==270){
    if(document.all && !window.opera){  //IE-specific
        $('#img').width(225);
        $('#img').height(300);
        }
    else{
        $('#img').width(300);
        $('#img').height(225);
        }
    }
  else{
    $('#img').width(300);
    $('#img').height(400);
    }

OK, images rotate nicely in all browsers, on to the Next Problem.

I attach a draggable and resizable div (see the demos at http://dcook.org/software/jquery/magnifier/ to get an idea) to the image. I don't want it to leave the image so I use this:
mydiv.draggable({containment:img});

When rotation is 0 or 180 everything is fine, but rotations of 90 and 270 go wrong in IE6/IE7/IE8; it seems the different coordinate system confuse it and I can move mydiv outside the image. (Incidentally jquery's resizable containment is broken, so I had to hand-code that; my resizable containment code is not affected by this problem!)

But things get worse. In IE8 *only*, the rotated image overlaps the following page items; IE6/IE7 correctly push the page down when a landscape image gets rotated to become a portrait image. I could have lived with containment not working, but this one is a showstopper.

We can put the image in a named div, then alter the width/height of that div each time we rotate. So the code ends up as this:
  if(rotation==90 || rotation==270){
    if(document.all && !window.opera){  //IE-specific
        $('#img').width(225);
        $('#img').height(300);
        $('#img_outer').width(300);
        $('#img_outer').height(225);
        }
    else{
        $('#img').width(300);
        $('#img').height(225);
        }
    }
  else{
    $('#img').width(300);
    $('#img').height(400);
    if(document.all && !window.opera){  //IE-specific
        $('#img_outer').width(300);
        $('#img_outer').height(400);
        }
    }
You can also fix the other problem by using #img_outer as the containment div, instead of #img. That fixes all problems in IE6/IE7 (though be careful with margins, padding and borders; i.e. make sure #img_outer is exactly the same size as #img).

IE8 is still less than ideal. Using #img_outer stops it overlapping with the following content, but it puts hard white space in the area where the image would be if height/width were reversed (and the draggable div can still be dragged into that area). For a landscape image that has been rotated that means whitespace to the right of the image, messing up multi-column table layouts. For a portrait image that has been rotated that means a lump of whitespace between the bottom of the image and the start of the next page content. Using overlap:hidden did not help.

This is very novel: a bug in IE8 that isn't in any other browser, not even IE6! I've not solved this, and welcome advice. Yes, okay, I could use absolute positioning to put a "Download Firefox" icon in that white area, but that isn't quite the advice I'm looking for...

6 comments:

Aaron said...

I'm just about to embark on this issue myself in the coming few weeks. Did you ever get a resolution to the IE8 problem?

Thanks for the blog post -- it'll be a helpful starting point. It's surprising how little is available on this use of image rotation w/jQuery...

darren said...

Hello Aaron,
Unfortunately, no, I've not. If you work it out (or, conversely, don't see the problem and can work out what you do differently) let me know.

Aaron said...

Will do. I'm not climbing this particular programmatic mountain until mid-October, I think, but if I manage to get it working correctly in IE8 (or perhaps even if I don't, in commiseration), I'll keep you posted.

Thanks, Darren.

Aaron said...

Darren,

I ended up not using jquery-rotate at all, but rather combining a couple of pieces I found elsewhere (essentially using jQuery's mousedown along with setting the rotation transform in CSS).

From , I got most of what I needed. Then I added in the cross-platform transform code (using DXImageTransform.Microsoft.Matrix) from and I was good to go. (Note that you'll need to manually calculate the M21/M22/etc values on the fly; the algorithm for that is at .)

The result? I've got images that are resizable, draggable, and that can be free-rotated by the user. All the way back to IE6. Seems to work OK for the moment -- we'll see how it hangs together as I add more ornamentation.

Hope it helps!

$$ said...

Aaron,
I am working on the same scenario and got into same issue.I read that you have solved by writing your own routine.If you could share that info,it wil be very greatful.

Samir Banday said...

I think Version 2 of Rotate JS is better. It atleast works for me.
https://jqueryrotate.googlecode.com/files/jQueryRotate.2.1.js