CSS rotation cross browser with jquery.animate()

226,473

Solution 1

CSS-Transforms are not possible to animate with jQuery, yet. You can do something like this:

function AnimateRotate(angle) {
    // caching the object for performance reasons
    var $elem = $('#MyDiv2');

    // we use a pseudo object for the animation
    // (starts from `0` to `angle`), you can name it as you want
    $({deg: 0}).animate({deg: angle}, {
        duration: 2000,
        step: function(now) {
            // in the step-callback (that is fired each step of the animation),
            // you can use the `now` paramter which contains the current
            // animation-position (`0` up to `angle`)
            $elem.css({
                transform: 'rotate(' + now + 'deg)'
            });
        }
    });
}

You can read more about the step-callback here: http://api.jquery.com/animate/#step

http://jsfiddle.net/UB2XR/23/

And, btw: you don't need to prefix css3 transforms with jQuery 1.7+

Update

You can wrap this in a jQuery-plugin to make your life a bit easier:

$.fn.animateRotate = function(angle, duration, easing, complete) {
  return this.each(function() {
    var $elem = $(this);

    $({deg: 0}).animate({deg: angle}, {
      duration: duration,
      easing: easing,
      step: function(now) {
        $elem.css({
           transform: 'rotate(' + now + 'deg)'
         });
      },
      complete: complete || $.noop
    });
  });
};

$('#MyDiv2').animateRotate(90);

http://jsbin.com/ofagog/2/edit

Update2

I optimized it a bit to make the order of easing, duration and complete insignificant.

$.fn.animateRotate = function(angle, duration, easing, complete) {
  var args = $.speed(duration, easing, complete);
  var step = args.step;
  return this.each(function(i, e) {
    args.complete = $.proxy(args.complete, e);
    args.step = function(now) {
      $.style(e, 'transform', 'rotate(' + now + 'deg)');
      if (step) return step.apply(e, arguments);
    };

    $({deg: 0}).animate({deg: angle}, args);
  });
};

Update 2.1

Thanks to matteo who noted an issue with the this-context in the complete-callback. If fixed it by binding the callback with jQuery.proxy on each node.

I've added the edition to the code before from Update 2.

Update 2.2

This is a possible modification if you want to do something like toggle the rotation back and forth. I simply added a start parameter to the function and replaced this line:

$({deg: start}).animate({deg: angle}, args);

If anyone knows how to make this more generic for all use cases, whether or not they want to set a start degree, please make the appropriate edit.


The Usage...is quite simple!

Mainly you've two ways to reach the desired result. But at the first, let's take a look on the arguments:

jQuery.fn.animateRotate(angle, duration, easing, complete)

Except of "angle" are all of them optional and fallback to the default jQuery.fn.animate-properties:

duration: 400
easing: "swing"
complete: function () {}

1st

This way is the short one, but looks a bit unclear the more arguments we pass in.

$(node).animateRotate(90);
$(node).animateRotate(90, function () {});
$(node).animateRotate(90, 1337, 'linear', function () {});

2nd

I prefer to use objects if there are more than three arguments, so this syntax is my favorit:

$(node).animateRotate(90, {
  duration: 1337,
  easing: 'linear',
  complete: function () {},
  step: function () {}
});

Solution 2

Thanks yckart! Great contribution. I fleshed out your plugin a bit more. Added startAngle for full control and cross-browser css.

$.fn.animateRotate = function(startAngle, endAngle, duration, easing, complete){
    return this.each(function(){
        var elem = $(this);

        $({deg: startAngle}).animate({deg: endAngle}, {
            duration: duration,
            easing: easing,
            step: function(now){
                elem.css({
                  '-moz-transform':'rotate('+now+'deg)',
                  '-webkit-transform':'rotate('+now+'deg)',
                  '-o-transform':'rotate('+now+'deg)',
                  '-ms-transform':'rotate('+now+'deg)',
                  'transform':'rotate('+now+'deg)'
                });
            },
            complete: complete || $.noop
        });
    });
};

Solution 3

jQuery transit will probably make your life easier if you are dealing with CSS3 animations through jQuery.

EDIT March 2014 (because my advice has constantly been up and down voted since I posted it)

Let me explain why I was initially hinting towards the plugin above:

Updating the DOM on each step (i.e. $.animate ) is not ideal in terms of performance. It works, but will most probably be slower than pure CSS3 transitions or CSS3 animations.

This is mainly because the browser gets a chance to think ahead if you indicate what the transition is going to look like from start to end.

To do so, you can for example create a CSS class for each state of the transition and only use jQuery to toggle the animation state.

This is generally quite neat as you can tweak you animations alongside the rest of your CSS instead of mixing it up with your business logic:

// initial state
.eye {
   -webkit-transform: rotate(45deg);
   -moz-transform: rotate(45deg);
   transform: rotate(45deg);
   // etc.

   // transition settings
   -webkit-transition: -webkit-transform 1s linear 0.2s;
   -moz-transition: -moz-transform 1s linear 0.2s;
   transition: transform 1s linear 0.2s;
   // etc.
}

// open state    
.eye.open {

   transform: rotate(90deg);
}

// Javascript
$('.eye').on('click', function () { $(this).addClass('open'); });

If any of the transform parameters is dynamic you can of course use the style attribute instead:

$('.eye').on('click', function () { 
    $(this).css({ 
        -webkit-transition: '-webkit-transform 1s ease-in',
        -moz-transition: '-moz-transform 1s ease-in',
        // ...

        // note that jQuery will vendor prefix the transform property automatically
        transform: 'rotate(' + (Math.random()*45+45).toFixed(3) + 'deg)'
    }); 
});

A lot more detailed information on CSS3 transitions on MDN.

HOWEVER There are a few other things to keep in mind and all this can get a bit tricky if you have complex animations, chaining etc. and jQuery Transit just does all the tricky bits under the hood:

$('.eye').transit({ rotate: '90deg'}); // easy huh ?

Solution 4

To do this cross browser including IE7+, you will need to expand the plugin with a transformation matrix. Since vendor prefix is done in jQuery from jquery-1.8+ I will leave that out for the transform property.

$.fn.animateRotate = function(endAngle, options, startAngle)
{
    return this.each(function()
    {
        var elem = $(this), rad, costheta, sintheta, matrixValues, noTransform = !('transform' in this.style || 'webkitTransform' in this.style || 'msTransform' in this.style || 'mozTransform' in this.style || 'oTransform' in this.style),
            anims = {}, animsEnd = {};
        if(typeof options !== 'object')
        {
            options = {};
        }
        else if(typeof options.extra === 'object')
        {
            anims = options.extra;
            animsEnd = options.extra;
        }
        anims.deg = startAngle;
        animsEnd.deg = endAngle;
        options.step = function(now, fx)
        {
            if(fx.prop === 'deg')
            {
                if(noTransform)
                {
                    rad = now * (Math.PI * 2 / 360);
                    costheta = Math.cos(rad);
                    sintheta = Math.sin(rad);
                    matrixValues = 'M11=' + costheta + ', M12=-'+ sintheta +', M21='+ sintheta +', M22='+ costheta;
                    $('body').append('Test ' + matrixValues + '<br />');
                    elem.css({
                        'filter': 'progid:DXImageTransform.Microsoft.Matrix(sizingMethod=\'auto expand\','+matrixValues+')',
                        '-ms-filter': 'progid:DXImageTransform.Microsoft.Matrix(sizingMethod=\'auto expand\','+matrixValues+')'
                    });
                }
                else
                {
                    elem.css({
                        //webkitTransform: 'rotate('+now+'deg)',
                        //mozTransform: 'rotate('+now+'deg)',
                        //msTransform: 'rotate('+now+'deg)',
                        //oTransform: 'rotate('+now+'deg)',
                        transform: 'rotate('+now+'deg)'
                    });
                }
            }
        };
        if(startAngle)
        {
            $(anims).animate(animsEnd, options);
        }
        else
        {
            elem.animate(animsEnd, options);
        }
    });
};

Note: The parameters options and startAngle are optional, if you only need to set startAngle use {} or null for options.

Example usage:

var obj = $(document.createElement('div'));
obj.on("click", function(){
    obj.stop().animateRotate(180, {
        duration: 250,
        complete: function()
        {
            obj.animateRotate(0, {
                duration: 250
            });
        }
    });
});
obj.text('Click me!');
obj.css({cursor: 'pointer', position: 'absolute'});
$('body').append(obj);

See also this jsfiddle for a demo.

Update: You can now also pass extra: {} in the options. This will make you able to execute other animations simultaneously. For example:

obj.animateRotate(90, {extra: {marginLeft: '100px', opacity: 0.5}});

This will rotate the element 90 degrees, and move it to the right with 100px and make it semi-transparent all at the same time during the animation.

Solution 5

this is my solution:

var matrixRegex = /(?:matrix\(|\s*,\s*)([-+]?[0-9]*\.?[0-9]+(?:[e][-+]?[0-9]+)?)/gi;

var getMatches = function(string, regex) {
    regex || (regex = matrixRegex);
    var matches = [];
    var match;
    while (match = regex.exec(string)) {
        matches.push(match[1]);
    }
    return matches;
};

$.cssHooks['rotation'] = {
    get: function(elem) {
        var $elem = $(elem);
        var matrix = getMatches($elem.css('transform'));
        if (matrix.length != 6) {
            return 0;
        }
        return Math.atan2(parseFloat(matrix[1]), parseFloat(matrix[0])) * (180/Math.PI);
    }, 
    set: function(elem, val){
        var $elem = $(elem);
        var deg = parseFloat(val);
        if (!isNaN(deg)) {
            $elem.css({ transform: 'rotate(' + deg + 'deg)' });
        }
    }
};
$.cssNumber.rotation = true;
$.fx.step.rotation = function(fx) {
    $.cssHooks.rotation.set(fx.elem, fx.now + fx.unit);
};

then you can use it in the default animate fkt:

//rotate to 90 deg cw
$('selector').animate({ rotation: 90 });

//rotate to -90 deg ccw
$('selector').animate({ rotation: -90 });

//rotate 90 deg cw from current rotation
$('selector').animate({ rotation: '+=90' });

//rotate 90 deg ccw from current rotation
$('selector').animate({ rotation: '-=90' });
Share:
226,473
frenchie
Author by

frenchie

Started programming when I was 10. Never made it my profession but always programmed, mostly with Excel VBA for finance, and for some simple front-end web design in the early 2000's (ie. no jquery back then, what a pain it was with DHTML). One day, I get the idea of outsourcing the web development of my start-up: total cacacacacacacafukkkk! So: from VBA to C# .NET and Javascript in 5,000 pages of programming books, and lots of Stackoverflow! I think asking dumb questions is a smart way to learn. And as an internet entrepreneur, I've learned that what matters most is not knowing how to program but knowing what to program: discover the goyaPhone at www.goyaphone.eu

Updated on July 08, 2022

Comments

  • frenchie
    frenchie almost 2 years

    I'm working on creating a cross-browser compatible rotation (ie9+) and I have the following code in a jsfiddle

    $(document).ready(function () { 
        DoRotate(30);
        AnimateRotate(30);
    });
    
    function DoRotate(d) {
    
        $("#MyDiv1").css({
              '-moz-transform':'rotate('+d+'deg)',
              '-webkit-transform':'rotate('+d+'deg)',
              '-o-transform':'rotate('+d+'deg)',
              '-ms-transform':'rotate('+d+'deg)',
              'transform': 'rotate('+d+'deg)'
         });  
    }
    
    function AnimateRotate(d) {
    
            $("#MyDiv2").animate({
              '-moz-transform':'rotate('+d+'deg)',
              '-webkit-transform':'rotate('+d+'deg)',
              '-o-transform':'rotate('+d+'deg)',
              '-ms-transform':'rotate('+d+'deg)',
              'transform':'rotate('+d+'deg)'
         }, 1000); 
    }
    

    The CSS and HTML are really simple and just for demo:

    .SomeDiv{
        width:50px;
        height:50px;       
        margin:50px 50px;
        background-color: red;}
    
    <div id="MyDiv1" class="SomeDiv">test</div>
    <div id="MyDiv2" class="SomeDiv">test</div>
    

    The rotation works when using .css() but not when using .animate(); why is that and is there a way to fix it?

    Thanks.

  • frenchie
    frenchie about 11 years
    Can you put this in a fiddle?
  • yckart
    yckart about 11 years
    @frenchie I'm on iPhone, no chance since jsFiddle updated their editor ;) however, I'll prepare a bin for you...
  • yckart
    yckart about 11 years
    @frenchie I forgot something! Updated my answer now... Here's an working example: jsbin.com/ofagog/2/edit
  • frenchie
    frenchie about 11 years
    Ok, very cool: that is THE plugin for cross-browser (IE9+) CSS3 rotation!! You can claim that: you built that. Nice work!
  • yckart
    yckart almost 11 years
    jQuery adds the needed vendor prefix automatically, so no need for this!
  • lsmpascal
    lsmpascal over 10 years
    +1 for the cross platform. Great. @yckart : the auto prefix doesn't work for me in this case.
  • yckart
    yckart over 10 years
    @PaxMaximinus What jQuery-version do you use? blog.jquery.com/2012/08/09/jquery-1-8-released
  • lsmpascal
    lsmpascal over 10 years
    @yckart : the 1.7.1 version.
  • yckart
    yckart over 10 years
    @PaxMaximinus As you can see in the article from jquery-blog, the auto-prefixing is just since jquery-1.8+!
  • whyAto8
    whyAto8 over 10 years
    Hey, does this solution works for IE8 as well? If not, what solution can be used for IE8, I just saw one of the options as jquery rotate plugin
  • Yeti
    Yeti over 10 years
    I made this plugin work for IE7+ using transform matrix, also I improved the parameters. You really don't want to pass just and exactly those parameters.
  • Liam
    Liam over 10 years
    Or IE9, does in Firefox, but only firefox.
  • Yeti
    Yeti about 10 years
    Okay so it works now in Chrome, Firefox and IE10. Can you test IE9, Liam? The problem was that the transform property was undefined for Chrome and IE, therefore the script thought that the transform property was unavailable. Hence, I changed the script to include all the prefixes: ms, o, webkit, moz to ensure detection correctly. The fiddle is updated as well to v12.
  • Codebeat
    Codebeat about 10 years
    Why not a css class solution?
  • matteo
    matteo almost 10 years
    Hey there's something wrong at least with the "update 2" version. When the complete function is executed, "this" inside it is supposed to be the dom element, but it is some other object. Any idea how to fix this?
  • Trevin Avery
    Trevin Avery over 9 years
    What are $.speed() and $.style()? I can't find any documentation on these two functions.
  • yckart
    yckart over 9 years
    @TrevinAvery jQuery.speed handles the arguments you pass into jQuery.fn.animate, it makes the order insignificant. (So, you can pass in duration, easing, complete or easing, duration, complete or whatever). jQuery.style is primarily the same as the already known jQuery.fn.css, however it works directly on the node instead of an jQuery-object. (jQuery.style is not an alias for jQuery.fn.css but is called in it.)
  • yckart
    yckart over 9 years
    @matteo Sorry for the late response and thanks for your test. I needed a little time fiddle the issue out, but I got it! fiddle.jshell.net/P5J4V/43 By the way, I mentioned your investigation in my anwer :)
  • Trevin Avery
    Trevin Avery over 9 years
    @matteo The reason this does not refer to a DOM object is because the context is set to the object animate() was called on, in this case {deg: 0} is set to the context. You can fix this by changing the context of each callback function with apply()/call() or $.proxy() (as @yckart has shown). Here is my solution to fix ALL callbacks and allow 3d rotation: jsfiddle.net/TrevinAvery/P5J4V/44
  • matteo
    matteo over 9 years
    Cool! Oh, I have to enter at least 15 characters. That's really cool!
  • yckart
    yckart over 9 years
    @TrevinAvery Great work, I like the way you handle the promise-callbacks. However, I recognized a logic problem. Currently the styles are applied with CSS3-properties only, they are not supported by older browsers, so nothing will move around. Making another "type of string/object"-test and applying the styles, depending on this check, should fix it.
  • Asbjørn Ulsberg
    Asbjørn Ulsberg over 9 years
    If you want to animate the same element over and over, starting on 0 degrees every time will not lead to the expected behavior, so you need to initialize with the current rotation value. How to do that is explained here: stackoverflow.com/a/11840120/61818
  • user308553
    user308553 about 8 years
    Can someone explain the $({deg: 0}).animate({deg: angle} part to me. I don't understand how it would do 0 to angle. It works even angle is negative. 1) How does it know it should do deg++ or deg-- 2) how does $({deg:0}) not return an error, I thought $() is a selector that would look for an element that fit the criteria, and the criteria has to be a certain syntax. This is like magic to me
  • raveren
    raveren about 8 years
    wow this is terrible, how about rotating back to zero? Oh, instant?
  • yckart
    yckart about 8 years
    @Raveren This is an example and nothing to copy & paste, thoughtless... Just copy and code, modify and change. If you need help at some point, create a new question.
  • Tires
    Tires about 7 years
    The binding with proxy inside of the loop is very expensive, so better use bind outside or call/apply.
  • yckart
    yckart about 7 years
    @Tires Good finding. Passing the actual iterated element as argument to the complete callback, that can not be done outside of the loop. One could easily drop it, if not required. Note, this not a full solution, use it as startingpoint for another.
  • Sean H
    Sean H almost 7 years
    For me, this method resets the start position to 0deg. So if I want to rotate an element which has a non-zero starting angle it doesn't work. Is anyone else having the same issue?
  • yckart
    yckart almost 7 years
    @Seano That's because of the deg: 0 part in those functions. You could easily adopt my code and change that 0 value to any other number.
  • Amit Kumar Gupta
    Amit Kumar Gupta over 6 years
    This solution doesn't work when you set some data to the element after the animation. So that you can continue the animation after some time. So I have took your solution and combined with @yckart solution to make it cross browser compatible.