Navegador cruzado de rotación CSS con jquery.animate ()

81

Estoy trabajando en la creación de una rotación compatible con varios navegadores (ie9 +) y tengo el siguiente código en un 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); 
}

El CSS y HTML son realmente simples y solo para demostración:

.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>

La rotación funciona cuando se usa .css()pero no cuando se usa .animate(); ¿Por qué es así y hay alguna manera de solucionarlo?

Gracias.

Frenchie
fuente
jQuery no tiene idea de cómo animar la rotación. ¿Quizás usar transiciones CSS3?
John Dvorak
1
@JanDvorak - excepto que IE9 no es compatible con las transiciones CSS3.
Spudley
1
Votaré a favor de la parte de "arreglarlo" (es posible que termine implementando una stepdevolución de llamada), pero la parte de "por qué es eso" es bastante clara.
John Dvorak
@Spudley: sí, lo sé: el objetivo para el soporte de IE9 será usar setInterval y llamar a la función DoRotate varias veces.
frenchie
Por cierto, ya señalé la biblioteca de papel de lija CSS en mi respuesta a su otra pregunta, que es un relleno múltiple para transiciones CSS en IE. Es posible que desee probarlo.
Spudley

Respuestas:

222

Las transformaciones CSS no son posibles de animar con jQuery, todavía. Puedes hacer algo como esto:

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)'
            });
        }
    });
}

Puede leer más sobre la devolución de llamada por pasos aquí: http://api.jquery.com/animate/#step

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

Y, por cierto: no necesita prefijar las transformaciones css3 con jQuery 1.7+

Actualizar

Puede envolver esto en un jQuery-plugin para hacer su vida un poco más fácil:

$.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

Actualización2

Optimicé un poco para hacer que el orden de easing, durationy completeinsignificante.

$.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);
  });
};

Actualización 2.1

Gracias a matteo que notó un problema con el this-contexto en el completo- callback. Si se soluciona vinculando la devolución de llamada conjQuery.proxy en cada nodo.

He agregado la edición al código antes de la Actualización 2 .

Actualización 2.2

Esta es una posible modificación si desea hacer algo como alternar la rotación hacia adelante y hacia atrás. Simplemente agregué un parámetro de inicio a la función y reemplacé esta línea:

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

Si alguien sabe cómo hacer que esto sea más genérico para todos los casos de uso, ya sea que desee establecer un grado de inicio o no, realice la edición adecuada.


El uso ... ¡es bastante simple!

Básicamente, tiene dos formas de alcanzar el resultado deseado. Pero al principio, echemos un vistazo a los argumentos:

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

Excepto por "ángulo", todos son opcionales y recurren a las jQuery.fn.animatepropiedades predeterminadas :

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

Primero

Esta es la forma corta, pero parece un poco confusa cuantos más argumentos pasamos.

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

2do

Prefiero usar objetos si hay más de tres argumentos, por lo que esta sintaxis es mi favorita:

$(node).animateRotate(90, {
  duration: 1337,
  easing: 'linear',
  complete: function () {},
  step: function () {}
});
yckart
fuente
4
¿Puedes poner esto en un violín?
frenchie
4
Ok, muy bueno: ¡ese es el complemento para la rotación CSS3 entre navegadores (IE9 +)! Puedes afirmar eso: tú lo construiste. ¡Buen trabajo!
frenchie
1
@matteo Perdón por la respuesta tardía y gracias por tu prueba. Necesitaba un poco de tiempo para resolver el problema, ¡pero lo conseguí! fiddle.jshell.net/P5J4V/43 Por cierto, mencioné su investigación en mi respuesta :)
yckart
1
@matteo La razón por la thisque no se refiere a un objeto DOM es porque el contexto se establece en el objeto al que animate()se llamó, en este caso {deg: 0}se establece en el contexto. Puede solucionar esto cambiando el contexto de cada función de devolución de llamada con apply()/ call()o $.proxy()(como ha mostrado @yckart). Aquí está mi solución para arreglar TODAS las devoluciones de llamada y permitir la rotación 3d: jsfiddle.net/TrevinAvery/P5J4V/44
Trevin Avery
1
Si desea animar el mismo elemento una y otra vez, comenzar en 0grados cada vez no conducirá al comportamiento esperado, por lo que debe inicializar con el valor de rotación actual. Cómo hacerlo se explica aquí: stackoverflow.com/a/11840120/61818
Asbjørn Ulsberg
17

¡Gracias yckart! Gran aporte. Desarrollé un poco más tu complemento. Se agregó startAngle para un control total y CSS entre navegadores.

$.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
        });
    });
};
monótono
fuente
5
jQuery agrega automáticamente el prefijo de proveedor necesario, ¡así que no es necesario!
yckart
+1 para la plataforma cruzada. Excelente. @yckart: el prefijo automático no me funciona en este caso.
lsmpascal
@PaxMaximinus ¿Qué versión de jQuery usas? blog.jquery.com/2012/08/09/jquery-1-8-released
yckart
@yckart: la versión 1.7.1.
lsmpascal
1
@PaxMaximinus Como puede ver en el artículo de jquery-blog, ¡el prefijo automático es solo desde entonces jquery-1.8+!
yckart
10

El tránsito de jQuery probablemente hará su vida más fácil si está tratando con animaciones CSS3 a través de jQuery.

EDITAR Marzo de 2014 (porque mi consejo ha sido votado constantemente desde que lo publiqué)

Permítanme explicar por qué inicialmente estaba insinuando el complemento anterior:

Actualizar DOMen cada paso (es decir $.animate) no es ideal en términos de rendimiento. Funciona, pero probablemente sea más lento que las transiciones CSS3 puras o las animaciones CSS3 .

Esto se debe principalmente a que el navegador tiene la oportunidad de pensar en el futuro si usted indica cómo se verá la transición de principio a fin.

Para hacerlo, puede, por ejemplo, crear una clase CSS para cada estado de la transición y solo usar jQuery para alternar el estado de la animación.

Esto generalmente es bastante bueno, ya que puede modificar sus animaciones junto con el resto de su CSS en lugar de mezclarlo con su lógica comercial:

// 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'); });

Si alguno de los parámetros de transformación es dinámico, por supuesto, puede usar el atributo de estilo en su lugar:

$('.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)'
    }); 
});

Mucha más información detallada sobre las transiciones CSS3 en MDN .

SIN EMBARGO, hay algunas otras cosas a tener en cuenta y todo esto puede volverse un poco complicado si tiene animaciones complejas, encadenamiento, etc. y jQuery Transit solo hace todas las partes complicadas debajo del capó:

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

Para hacer este navegador cruzado, incluido IE7 +, deberá expandir el complemento con una matriz de transformación. Dado que el prefijo del proveedor se realiza en jQuery desde jquery-1.8 +, dejaré eso para la transformpropiedad.

$.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);
        }
    });
};

Nota: Los parámetros optionsy startAngleson opcionales, si solo necesita configurar startAngleuse {}o nullfor options.

Uso de ejemplo:

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);

Vea también este jsfiddle para una demostración.

Actualización : ahora también puede pasar extra: {}las opciones. Esto le permitirá ejecutar otras animaciones simultáneamente. Por ejemplo:

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

Esto rotará el elemento 90 grados y lo moverá hacia la derecha con 100px y lo hará semitransparente todo al mismo tiempo durante la animación.

Yeti
fuente
O IE9, lo hace en Firefox, pero solo en Firefox.
Liam
Bien, ahora funciona en Chrome, Firefox e IE10. ¿Puedes probar IE9, Liam? El problema era que la propiedad transform no estaba definida para Chrome e IE, por lo que el script pensó que la propiedad transform no estaba disponible. Por lo tanto, he cambiado el guión para incluir a todos los prefijos: ms, o, webkit, mozpara garantizar una detección correcta. El violín también se actualiza a v12.
Yeti
2

esta es mi solución:

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);
};

entonces puedes usarlo en el fkt animado predeterminado:

//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' });
AntiCampeR
fuente
1

Otra respuesta, porque jQuery.transit no es compatible con jQuery.easing. Esta solución viene como una extensión de jQuery. Es más genérico, la rotación es un caso específico:

$.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)");
    }
});

El uso es tan simple como:

$(element).animateStep({from: 0, to: 90, step: $.fn.rotate});
Llantas
fuente
0

Sin plugin de navegador cruzado con 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
fuente