jQuery Effects:
Beyond the Basics

Welcome!

http://pres.learningjquery.com/jqcon2010/

* Right arrow key or Return/Enter to move forward; left arrow key to move backward. Or, use slide list at bottom of page (appears on hover)

Karl Swedberg

Outline

Before We Begin

* Most of the animations in this presentation are slower than advisable on a real production site.

Gotchas

Effects Manipulate Style Properties

.animate( ) Properties

Animation Properties (gotchas)

But…

.animate( ) Property Values

Properties can be animated by:

…as long as the property allows it (e.g. opacity: only 0-1)

A Few More Gotchas

Property Values Demo

$('div.bar').animate({
: 
: 
})

Callback Gotchas

Unexpected Callback Demo

hi
hi

Table Animation Gotchas

row 1 slide row row height, cell height: 1.5em
row 2 slide cells row height: 1.5em, cell height: auto
row 3 slide cells row height: 1.5em, cell height: 4em
row 4
slide row and divs
row height, cell height: 1.5em
row 5 animate row line-height row height, cell height: 1.5em
row 6 fade row row height, cell height: 1.5em

Let's Solve Some Problems

Animated Scrolling

The Problem:

.firstScrollable( )

  $.fn.firstScrollable = function(dir) {
    var scrollable = [], scrolled = false;
    var scrollDir = /left$/i.test(dir) ? 'scrollLeft' : 'scrollTop';
    
    this.each(function() {
      if (this == document || this == window) { return; }
      var el = $(this);
      if ( el[scrollDir]() > 0 ) {
        scrollable = [this];
        return false;
      }

      el[scrollDir](1);
      scrolled  = el[scrollDir]() > 0;
      el[scrollDir](0);
      if ( scrolled ) {
        scrollable = [this];
        return false;
      }
    });
    return this.pushStack(scrollable);
  };

.firstScrollable( ) Usage

$('html, body')
  .firstScrollable()
  .animate({scrollTop: '+=200px'}, 800);

Slide in Different Directions

The Problem:

The Solution:

Slide Oppositely

The Background: What .slideUp() and .slideDown() really mean...

Let's Slide Oppositely

So, all we need is a little CSS to switch things up:

.container { position: relative; }
.slidy {
  position: absolute;
  bottom: 0; /* here we are! */
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<div class="container">
  <div class="slidy">slidy</div>
</div>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  $('div.slidy').slideToggle(1200);

Slide Oppositely Demo

slidy

.fadeToggle()

The Problem:

The Solution:

A Clue Under the Hood

All shortcut methods use .animate() internally.

jQuery.each({
  slideDown: genFx("show", 1),
  slideUp: genFx("hide", 1),
  slideToggle: genFx("toggle", 1),
  fadeIn: { opacity: "show" },
  fadeOut: { opacity: "hide" }
}, function( name, props ) {
  jQuery.fn[ name ] = function( speed, easing, callback ) {
    return this.animate( props, speed, easing, callback );
  };
});

Simple .fadeToggle() Plugin

We can do the same thing:

jQuery.fn.fadeToggle = function(speed, easing, callback) {
  return this.animate({opacity: 'toggle'}, speed, easing, callback);
};

And use it like the other shortcuts:

$('#myid').fadeToggle();
$('#myid').fadeToggle('slow');
$('#myid').fadeToggle('slow', function() {
  // do something
});

Little Less Simple Plugin

Problem: I need something more robust. I need a plugin that will…

The Solution...

Little Less Simple .toggleEffect()

.toggleEffect() Plugin

$.fn.toggleEffect = function(effect, speed, easing, callback) {
  var args = Array.prototype.slice.call(arguments);
  if (effect == undefined || typeof effect != 'string' || effect in $.fx.speeds) {
    args.unshift('opacity');
  }
  var prop = {};
  prop[ args[0] ] = 'toggle';
  speed = args[1];
  easing = args[2];
  callback = args[3];

  if ( !$.isArray(speed) ) {
    return this.animate(prop, speed, easing, callback);
  }
  return this.animate(prop, this.is(':hidden') ? speed[0] : speed[1], easing, callback);
};

.toggleEffect() Usage

Now we can do this sort of thing:

// toggle opacity w/ defaults
$('some selector').toggleEffect();

// toggle height at 200ms
$('some selector').toggleEffect('height', 200)

// fade in at 800ms; fade out at 800ms
$('some selector').toggleEffect('opacity', [800, 450])

// toggle width slowly with linear easing
// and call the doSomething function when animation is complete
$('some selector').toggleEffect('width', 'slow', 'linear', doSomething)

Chunky or Smooth?

The Problem:

The Solution:

Ease Up a Little

Easing

// 'swing' is default, so unnecessary
$('#swing').slideToggle(1200, 'swing');
// same velocity throughout. *in core*
$('#linear').slideToggle(1200, 'linear');
// all others require plugin*
$('#easeInOutBounce').slideToggle(1200, 'easeInOutBounce');

Easing Demo

linear
swing
easeInOutBounce

Limit Concurrent Animations

Avoid this sort of thing:

$('p').slice(1) // select paragraphs 2–20.
.slideDown();

Do something like this instead:

// first wrap 'em up.
var paragraphWrapper = $('p').slice(1)
.wrapAll('<div></div>')
  .parent();
// then animate the wrapper
paragraphWrapper.slideDown();

Tweak $.fx.interval

Shortcuts with the Works

The Problem: This doesn't work in jQuery 1.4.2 or below:

$('#combo').fadeOut(800, 'linear', function() {
  $(this).fadeIn();
});

The Solution: This does:

$('#combo').fadeOut({
  duration: 800,
  easing: 'linear',
  complete: function() {
    $(this).fadeIn();
  }
});

The Works Demo

// clickie on the combo. Then...
$(this).parent().prev().fadeOut({
  duration: 800,
  easing: 'linear',
  complete: function() {
    $(this).fadeIn(750);
  }
});

Shortcuts with the Works

Breaking News

(thanks, me)

// as of jQuery 1.4.3, this now works

$('#combo').fadeOut(800, 'linear', function() {
  $('#something-else').fadeIn();
});

Animation Order

The Problem:

Default Queue Behavior

The Background:

Modifying the Queue Behavior

We can change this default queue behavior by:

Using the .queue() Method

Queue Example

var $queuewrap = $('#queuewrapper'),
    $queue1 = $queuewrap.find('.demodiv').eq(0),
    $queue2 = $queuewrap.find('.demodiv').eq(1);

// .queue() callback functions
var greenify = function(next) {
  $(this).css({backgroundColor: '#090'});
  next();
};
var slideFirst = function(next) {
  $queue1.slideToggle();
  next();
};
var slideSecond = function(next) {
  $queue2.slideToggle().queue(greenify);
  next();
};

Queue Example (continued)

$queuewrap.find('button').click(function() {
  $queue1.queue(slideFirst)
         .queue(greenify)
         .queue(slideSecond);
});

Callbackitis

The Problem:

Callbackitis

The Evidence:

$('.mydiv').eq(0).fadeOut('slow', function() {
  $('.mydiv').eq(1).fadeOut('slow', function() {
    $('.mydiv').eq(2).fadeOut('slow', function() {
      $('.mydiv').eq(3).fadeOut('slow', function() {
        $('.mydiv').eq(4).fadeOut('slow', function() {
          // abandon all hope
        });
      });
    });
  });
});

Callbackitis

The Solution:

Callback Tip

var myCallback = function() {
  // I'mma gonna do something
}
function yourCallback() {
  // You do something, too!
};

$('.mydiv).slideUp(250, myCallback);
$('.yourdiv).slideUp(250, yourCallback);

Repeating Callbacks

(Thanks, Dave!)

var $sequence = $('div.sequence'),
    index = 0;

(function sequencer() {
    $sequence.eq(index++)
    .animate({opacity: 'toggle'}, 'slow', sequencer);
})();

Repeating Callbacks Demo

10
9
8
7
6
5
4
3
2
1
Blast off!

More Repeating Callbacks

var $pulse = $('div.pulse'),
    index = 0,
    total = 10;

(function pulse() {
  if (index++ < total) {
    $pulse
    .animate({opacity: 'toggle'}, pulse);
  }
})();

More Repeating Callbacks Demo

10
9
8
7
6
5
4
3
2
1

Pulsee Plugin!

$.fn.pulsee = function(options) {
  var counter = 0, self = this;
  var opts = $.extend({}, $.fn.pulsee.defaults,
    typeof options === 'number' ? {total: options} : options || {}
  );

  (function pulse() {
    if (counter++ < opts.total) {
      self.animate({opacity: 'toggle'}, opts.duration, opts.easing,
        function() {
          opts.complete.call(this, counter-1);
          pulse();
        }
      );
    }
  })();

  return self;
};
$.fn.pulsee.defaults = {total: 10, complete: $.noop};

Pulsee Plugin Demo!

var colors = ['#900', '#090', '#009', '#cc0', '#0cc', '#c0c'];
var pulseOpts = {
  total: 9,
  duration: 489,
  complete: function(i) {
    this.style.backgroundColor = colors[i % 6];
  }
};
$('#pulseer').click(function() {
  $('div.pulsee').slice(0,5).pulsee(11);
  $('div.pulsee').slice(5).pulsee(pulseOpts);
});

1
2
3
4
5
6
7
8
9
10

Horizontally Sliding Panels

A more complex, "real-world" example

Runaway Animations

The Problem:

The :animated Selector

$('#fadeinout').click(function() {
    var $div1 = $('#inoutwrap').find('div.demodiv').eq(0);
    var $div2 = $('#inoutwrap').find('div.demodiv').eq(1);

    $div1.fadeIn().fadeOut();

    if ( !$div2.is(':animated') ) {
      $div2.fadeIn().fadeOut();  
    }
});

An :animated Selector Demo

! :animated

The .stop() Method

A .stop() Example

$('#badges li').hover(function(){
  $(this).stop(true, true)
         .animate({bottom:"30px"}, 200);
}, function(){
  $(this).stop(true, false)
         .animate({bottom:"8px"}, 500);
});

See it in action

A .stop() Demo

$drop.slideDown(800);
$drop.slideUp(800);
$drop.stop(false, true).slideDown(800);
$drop.stop(true, true).slideUp(800);
$drop.stop(true, true).slideDown(800);
$drop.stop(true, false).slideUp(800);
if ( event.type === 'mouseenter' ) {  
  $drop
  .stop(true, true)
  .css('height', $drop.data('height'))
  .slideDown(800);
} else {
  $drop.stop(true, false).slideUp(800);
}

Stopping Effects Can Be Tricky

// first, store full height in element's .data()
// then...
$('#nav4 > li').bind('mouseenter mouseleave', function(event) { 
  if ( event.type === 'mouseenter' ) {  
    $drop
    // if animating, sets display: none
    .stop(true, true)
    // so setting height won't make visible
    .css('height', $drop.data('height'))
    // and now it knows what to slide down to
    .slideDown(800);
  } else {
    $drop.stop(true, false).slideUp(800);
  }
});

Catch Me If You Can

The Problem:

The Solution:

The Step Option

var $ball = $easingDiv.find('div.demodiv');
$easingDiv.find('button').click(function() {
  var $button = $(this), hit = false;
  var mystep = function() {
    if (!hit && 298 <= $ball.height() + 
      parseFloat($ball.css('top')) + 
      parseFloat($ball.css('marginTop')) 
    ) {
      $button.after('SMACK!');
      hit = true;
    }
  };
  $easingDiv.find('div.demodiv').animate(props, {
    duration: 5000, 
    easing: 'linear',
    specialEasing: myEase,
    step: mystep
  });
});

Step Option Demo

Stop the Madness

The Problem:

The Solution:

$.fx.off = !$.fx.off

Global effects are on

Default Animation Speed

The Problem:

Global Duration Change

The Clue:

// from jQuery core...
opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? 
  opt.duration :
  jQuery.fx.speeds[opt.duration] || jQuery.fx.speeds._default;

// ....
jQuery.extend(jQuery.fx, {
  speeds: {
    slow: 600,
    fast: 200,
    // Default speed
    _default: 400
  },
  // ...
});

Global Duration Change

The Solution:

$.extend($.fx.speeds, {
  _default: 300,
  crawl: 1200
});

jQuery History Lesson

/*
 * jQuery (jquery.com)
 * ...
 * 
 * $Date: 2006-05-12 12:52:18 -0400 (Fri, 12 May 2006) $
 * $Rev: 29 $
 */
// ...
var ss = {"crawl":1200,"xslow":850,"slow":600,
          "medium":400,"fast":200,"xfast":75,"normal":400};

Pause

The Problem:

A (non-)?Solution

$('#me-me').fadeIn().animate({snooze: true}, 1200).fadeOut();

delay

Another Solution

$('#delayee').fadeIn(800).delay(3000).fadeOut(600);

Delaying Animations

$('#delayez')
.fadeIn(800)
.delay(1500)
.queue(function(next) {
  $(this).css('backgroundColor', '#090');
  next();
})
.delay(1500)
.fadeOut(600);
});

setTimeout

Yet Another Solution

$('#me-too').fadeIn(backout);

function backout() {
  var self = this;
  setTimeout(function() {
    // you probably don't want to use "this" here.
    $(self).fadeOut();
  }, 2000);
}

doTimeout

$('#me-three').fadeIn(function() {
  $(this).doTimeout(1800, function() {
    this.fadeIn();
  });
});

Fun Stuff

Multi-property Easing

$('div.demodiv').animate({
  left: '300px',
  top: '245px',
  width: 0,
  height: 0,
  marginTop: '55px'
}, {
  duration: 5000, 
  easing: 'linear',
  specialEasing: {
    left: 'easeOutQuad',
    top: 'easeOutBounce'
  }
});

Multi-property Easing Demo

Custom Properties

(Thanks, John!)

$.fx.step.corner = function(fx) {
  fx.elem.style.top = fx.now + fx.unit;
  fx.elem.style.left = fx.now + fx.unit;
};

// usage
$('#cornerify').click(function() {
  $('div.cornered').animate({corner: '40px'}, 500);
});

Custom Properties Demo

hey

Custom Properties

$.fx.step.leftTop = function(fx) {
  fx.elem.style.left = fx.now + fx.unit;
  fx.elem.style.top = fx.now + fx.unit;
};
// allow start from a point other than 0 based on "left"
var _oldcur = jQuery.fx.prototype.cur;
jQuery.fx.prototype.cur = function( force ) {

if (this.prop == 'leftTop') {
  // change this.prop to 'left'
  var r = parseFloat(jQuery.css(this.elem, 'left', force));
	return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, 'left')) || 0;
}

return _oldcur.call(this, force);
};

Custom Properties Demo

$('#topleftify').click(function() {
  $('div.cornered').animate({corner: '+=40px'}, 500);
  $('div.left-topped').animate({leftTop: '+=40px'}, 500);
});

C
LT
C
LT

More Fun Stuff

Fun Stuff

Multi-property Easing

$('div.demodiv').animate({
  left: '300px',
  top: '245px',
  width: 0,
  height: 0,
  marginTop: '55px'
}, {
  duration: 5000, 
  easing: 'linear',
  specialEasing: {
    left: 'easeOutQuad',
    top: 'easeOutBounce'
  }
});

Multi-property Easing Demo

Custom Properties

(Thanks, John!)

$.fx.step.corner = function(fx) {
  fx.elem.style.top = fx.now + fx.unit;
  fx.elem.style.left = fx.now + fx.unit;
};

// usage
$('#cornerify').click(function() {
  $('div.cornered').animate({corner: '40px'}, 500);
});

Custom Properties Demo

hey

Custom Properties

$.fx.step.leftTop = function(fx) {
  fx.elem.style.left = fx.now + fx.unit;
  fx.elem.style.top = fx.now + fx.unit;
};
// allow start from a point other than 0 based on "left"
var _oldcur = $.fx.prototype.cur;
$.fx.prototype.cur = function( force ) {

if (this.prop == 'leftTop') {
  // change this.prop to 'left'
  var r = parseFloat($.css(this.elem, 'left', force));
	return r && r > -10000 ? r : parseFloat($.curCSS(this.elem, 'left')) || 0;
}

return _oldcur.call(this, force);
};

Custom Properties Demo

$('#topleftify').click(function() {
  $('div.cornered').animate({corner: '+=40px'}, 500);
  $('div.left-topped').animate({leftTop: '+=40px'}, 500);
});

C
LT
C
LT

More Fun Stuff

Thank You!

Thanks so much for listening.

Tomtar credits: