Search code examples
jquerycssgoogle-chromecss-transitionsinline-styles

Chrome weird (scaleY) transition behaviour


So I've been working on a lightweight plugin built on CSS transitions. It's capable of adding inline styles (transitions) on the fly.

Some parts of the plugin code (like prefixing CSS attributes) are removed to make things clearer:

(function($, window, document) {
    'use strict';

    var plugin   = 'transition';

    // Constructors
    function Transition(element, animation, options) {
        this.element   = element;
        this.animation = animation;
        this.direction = null;
        this.settings  = $.extend({}, $.fn[plugin].defaults, options);
        this.init();
    }

    // Instance
    $.extend(Transition.prototype, {

        init: function() {
            var instance = this;
          
            instance.direction = $(instance.element).is(':visible') ? // toggle
                'outward':
                'inward' ;

            setTimeout(function() { // separate queue entry to make sure previous re-draw events are finished
                instance.settings.animations.hasOwnProperty(instance.animation) ?
                    instance.start():
                    console.error('Trying to call an undefined animation');
            }, 0);
        },

        /**
         * Start the transition.
         */
        start: function() {
            var instance  = this,
                $element  = $(instance.element);

            // Bind handlers
            $element
                .one('transitionstart', function() {
                    instance.settings.onStart.call($element);
                })
                .one('transitionend', function() {
                    instance.end();
                });

            // Add inline styles
            $element
                .css(instance.style('start'))
                .show() // ensure the element is visible
                .css(instance.style('end'));
        },


        /**
         * End the transition.
         */
        end: function() {
            var instance  = this,
                $element  = $(instance.element);

            instance.direction == 'inward' ?
                $element.show():
                $element.hide();

            instance.settings.onEnd.call($element);

            $element.css({
                opacity: '',
                transform: '',
                transformOrigin: '',
                transition: ''
            }).dequeue();
        },

        /**
         * Get the inline style for the transition.
         *
         * @param state
         */
        style: function(state) {
            var instance  = this,
                animation = instance.settings.animations[instance.animation],
                direction = instance.direction,
                css       = {};

            if (state === 'start') {
                css = (direction == 'inward') ?
                    animation.start:
                    animation.end; // reversed

                css['transition'] = 'all ' + 
                    instance.settings.duration + 'ms ' + 
                    instance.settings.curve + ' ' + 
                    instance.settings.delay + 'ms';
            } else {
                css = (direction == 'inward') ?
                    animation.end:
                    animation.start; // reversed
            }
            
            return css;
        }
    });

    // Plugin definition
    $.fn[plugin] = function(animation, options) {
        return this.each(function() {
            $(this).queue(function() {
                new Transition(this, animation, options);
            });
        });
    };

    // Default settings
    $.fn[plugin].defaults = {
        duration : 500,
        delay    : 0,
        curve    : 'ease',
        onStart  : function() {},
        onEnd    : function() {}
    };

    $.fn[plugin].defaults.animations = {
        fade: {
            start : { 'opacity': 0 },
            end   : { 'opacity': 1 }
        },
        scale: {
            start : { 'opacity': 0, 'transform': 'scale(0.8)' },
            end   : { 'opacity': 1, 'transform': 'scale(1.0)' }
        },
        slide: {
            start : { 'opacity': 0, 'transform': 'scaleY(0)', 'transform-origin': 'bottom'},
            end   : { 'opacity': 1, 'transform': 'scaleY(1)', 'transform-origin': 'bottom'}
        }
    };

})(jQuery, window, document);

$('#fading').transition('fade', {duration: 1000, delay: 1000});
$('#scaling').transition('scale', {duration: 1000, delay: 1000});
$('#sliding').transition('slide', {duration: 1000, delay: 1000});
div {
  display: inline-block;
  margin-bottom: 1em;
  padding: 3em 2em;
  background-color: #EEE;
  border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="fading">Fading block</div>
<div id="scaling">Scaling block</div>
<div id="sliding">Sliding block</div>

For some reason the sliding animation doesn't work on Chrome, the element just fades in/out. FireFox and Edge don't have this issue.

The CSS being applied for the sliding animation:

slide: {
    start : { 'opacity': 0, 'transform': 'scaleY(0)', 'transform-origin': 'bottom'},
    end   : { 'opacity': 1, 'transform': 'scaleY(1)', 'transform-origin': 'bottom'}
}

Hoping any magician could figure out what is going on.


Solution

  • It's a known bug with transitioning scale(0). Just change it to scale(0.01).

    (function($, window, document) {
      'use strict';
    
      var plugin = 'transition';
    
      // Constructors
      function Transition(element, animation, options) {
        this.element = element;
        this.animation = animation;
        this.direction = null;
        this.settings = $.extend({}, $.fn[plugin].defaults, options);
        this.init();
      }
    
      // Instance
      $.extend(Transition.prototype, {
    
        init: function() {
          var instance = this;
    
          instance.direction = $(instance.element).is(':visible') ? // toggle
            'outward' :
            'inward';
    
          setTimeout(function() { // separate queue entry to make sure previous re-draw events are finished
            instance.settings.animations.hasOwnProperty(instance.animation) ?
              instance.start() :
              console.error('Trying to call an undefined animation');
          }, 0);
        },
    
        /**
         * Start the transition.
         */
        start: function() {
          var instance = this,
            $element = $(instance.element);
    
          // Bind handlers
          $element
            .one('transitionstart', function() {
              instance.settings.onStart.call($element);
            })
            .one('transitionend', function() {
              instance.end();
            });
    
          // Add inline styles
          $element
            .css(instance.style('start'))
            .show() // ensure the element is visible
            .css(instance.style('end'));
        },
    
    
        /**
         * End the transition.
         */
        end: function() {
          var instance = this,
            $element = $(instance.element);
    
          instance.direction == 'inward' ?
            $element.show() :
            $element.hide();
    
          instance.settings.onEnd.call($element);
    
          $element.css({
            opacity: '',
            transform: '',
            transformOrigin: '',
            transition: ''
          }).dequeue();
        },
    
        /**
         * Get the inline style for the transition.
         *
         * @param state
         */
        style: function(state) {
          var instance = this,
            animation = instance.settings.animations[instance.animation],
            direction = instance.direction,
            css = {};
    
          if (state === 'start') {
            css = (direction == 'inward') ?
              animation.start :
              animation.end; // reversed
    
            css['transition'] = 'all ' +
              instance.settings.duration + 'ms ' +
              instance.settings.curve + ' ' +
              instance.settings.delay + 'ms';
          } else {
            css = (direction == 'inward') ?
              animation.end :
              animation.start; // reversed
          }
    
          return css;
        }
      });
    
      // Plugin definition
      $.fn[plugin] = function(animation, options) {
        return this.each(function() {
          $(this).queue(function() {
            new Transition(this, animation, options);
          });
        });
      };
    
      // Default settings
      $.fn[plugin].defaults = {
        duration: 500,
        delay: 0,
        curve: 'ease',
        onStart: function() {},
        onEnd: function() {}
      };
    
      $.fn[plugin].defaults.animations = {
        fade: {
          start: {
            'opacity': 0
          },
          end: {
            'opacity': 1
          }
        },
        scale: {
          start: {
            'opacity': 0,
            'transform': 'scale(0.8)'
          },
          end: {
            'opacity': 1,
            'transform': 'scale(1.0)'
          }
        },
        slide: {
          start: {
            'opacity': 0.5,
            'transform': 'scaleY(0.01)',
            'transform-origin': 'bottom'
          },
          end: {
            'opacity': 1,
            'transform': 'scaleY(1)',
            'transform-origin': 'bottom'
          }
        }
      };
    
    })(jQuery, window, document);
    
    $('#fading').transition('fade', {
      duration: 1000,
      delay: 1000
    });
    $('#scaling').transition('scale', {
      duration: 1000,
      delay: 1000
    });
    $('#sliding').transition('slide', {
      duration: 1000,
      delay: 1000
    });
    div {
      display: inline-block;
      margin-bottom: 1em;
      padding: 3em 2em;
      background-color: #EEE;
      border: 1px solid red;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <div id="fading">Fading block</div>
    <div id="scaling">Scaling block</div>
    <div id="sliding">Sliding block</div>