88

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
  • 51,731
  • 109
  • 304
  • 510
  • jQuery has no idea how to animate the rotation. Perhaps use CSS3 transitions? – John Dvorak Mar 03 '13 at 21:27
  • 1
    @JanDvorak - except that IE9 doesn't support CSS3 Transitions. – Spudley Mar 03 '13 at 21:27
  • 1
    I'll upvote for the "fix it" part (you might end up implementing a `step` callback), but the "why is that" part is pretty much clear. – John Dvorak Mar 03 '13 at 21:28
  • @Spudley: yes, I know: the goal for IE9 support will be to using setInterval and call the DoRotate function several times. – frenchie Mar 03 '13 at 21:28
  • BTW - I already pointed out the CSS Sandpaper library in my answer on your other question, which is a polyfill for CSS Transitions in IE. You might want to try it. – Spudley Mar 03 '13 at 21:30
  • @Spudley: I looked at Sandpaper but I'd rather avoid a dependency, which is why I'm looking to get this code to work. – frenchie Mar 03 '13 at 21:30
  • Fair enough. The reason it's worth it is because it would only be a dependency for IE9, and other browsers would be able to use real CSS transforms (with all the rendering benefits that go with that). If you write a manual jQuery animation, you'll be punishing the other browsers for IE's lack of support. And you'd have a dependency anyway (on your own jQuery code). If you're sure you'd rather not use it, that's fair enough, but it is a good tool. – Spudley Mar 03 '13 at 21:35

7 Answers7

239

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 () {}
});
Ethan Brouwer
  • 975
  • 9
  • 32
yckart
  • 32,460
  • 9
  • 122
  • 129
  • 4
    Can you put this in a fiddle? – frenchie Mar 03 '13 at 21:32
  • @frenchie I'm on iPhone, no chance since jsFiddle updated their editor ;) however, I'll prepare a bin for you... – yckart Mar 03 '13 at 21:33
  • @frenchie I forgot something! Updated my answer now... Here's an working example: http://jsbin.com/ofagog/2/edit – yckart Mar 03 '13 at 21:39
  • 4
    Ok, very cool: that is THE plugin for cross-browser (IE9+) CSS3 rotation!! You can claim that: you built that. Nice work! – frenchie Mar 04 '13 at 11:54
  • 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. – Yeti Oct 17 '13 at 11:30
  • Why not a css class solution? – Codebeat Feb 24 '14 at 06:56
  • 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? – matteo Jun 29 '14 at 20:16
  • What are `$.speed()` and `$.style()`? I can't find any documentation on these two functions. – Trevin Avery Oct 30 '14 at 15:08
  • @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 Oct 30 '14 at 17:04
  • 1
    @matteo Sorry for the late response and thanks for your test. I needed a little time fiddle the issue out, but I got it! http://fiddle.jshell.net/P5J4V/43/ By the way, I mentioned your investigation in my anwer :) – yckart Oct 30 '14 at 18:15
  • 1
    @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: http://jsfiddle.net/TrevinAvery/P5J4V/44/ – Trevin Avery Oct 30 '14 at 20:02
  • Cool! Oh, I have to enter at least 15 characters. That's really cool! – matteo Oct 30 '14 at 22:34
  • @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. – yckart Dec 15 '14 at 01:09
  • 1
    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: http://stackoverflow.com/a/11840120/61818 – Asbjørn Ulsberg Jan 21 '15 at 13:17
  • 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 – user308553 Feb 08 '16 at 06:34
  • wow this is terrible, how about rotating back to zero? Oh, instant? – raveren Mar 29 '16 at 18:32
  • @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. – yckart Mar 29 '16 at 19:31
  • The binding with proxy inside of the loop is very expensive, so better use bind outside or call/apply. – Tires Mar 22 '17 at 19:01
  • 1
    @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. – yckart Mar 23 '17 at 06:02
  • 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? – Sean H Jun 15 '17 at 10:14
  • 1
    @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. – yckart Jun 15 '17 at 10:27
20

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
        });
    });
};
drabname
  • 201
  • 2
  • 2
  • 5
    jQuery adds the needed vendor prefix automatically, so no need for this! – yckart Jul 23 '13 at 13:43
  • +1 for the cross platform. Great. @yckart : the auto prefix doesn't work for me in this case. – lsmpascal Aug 27 '13 at 09:54
  • @PaxMaximinus What jQuery-version do you use? http://blog.jquery.com/2012/08/09/jquery-1-8-released/ – yckart Aug 27 '13 at 10:25
  • @yckart : the 1.7.1 version. – lsmpascal Aug 28 '13 at 08:33
  • 1
    @PaxMaximinus As you can see in the article from jquery-blog, the auto-prefixing is just since `jquery-1.8+`! – yckart Aug 28 '13 at 09:01
  • 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 – whyAto8 Sep 11 '13 at 06:57
  • 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. – Amit Kumar Gupta Jan 28 '18 at 03:57
11

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 ?
Theo.T
  • 8,905
  • 3
  • 24
  • 38
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.

Yeti
  • 2,647
  • 2
  • 33
  • 37
  • Or IE9, does in Firefox, but only firefox. – Liam Oct 18 '13 at 10:17
  • 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. – Yeti Feb 10 '14 at 11:25
2

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' });
Kirk Beard
  • 9,569
  • 12
  • 43
  • 47
AntiCampeR
  • 21
  • 1
1

Another answer, because jQuery.transit is not compatible with jQuery.easing. This solution comes as an jQuery extension. Is more generic, rotation is a specific case:

$.fn.extend({
    animateStep: function(options) {
        return this.each(function() {
            var elementOptions = $.extend({}, options, {step: options.step.bind($(this))});
            $({x: options.from}).animate({x: options.to}, elementOptions);
        });
    },
    rotate: function(value) {
        return this.css("transform", "rotate(" + value + "deg)");
    }
});

The usage is as simple as:

$(element).animateStep({from: 0, to: 90, step: $.fn.rotate});
Tires
  • 1,529
  • 16
  • 27
0

Without plugin cross browser with setInterval:

                        function rotatePic() {
                            jQuery({deg: 0}).animate(
                               {deg: 360},  
                               {duration: 3000, easing : 'linear', 
                                 step: function(now, fx){
                                   jQuery("#id").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)'
                                  });
                              }
                            });
                        }

                        var sec = 3;
                        rotatePic();
                        var timerInterval = setInterval(function() {
                            rotatePic();
                            sec+=3;
                            if (sec > 30) {
                                clearInterval(timerInterval);
                            }
                        }, 3000);
Alexey Alexeenka
  • 891
  • 12
  • 15