.container(style="margin-bottom:40px;")
  .row
    .col-sm-12
      h2 Stacked card shuffle

.card-shuffle
  .relative
    .card-shuffle__card(data-position="0")
      .container.height-inherit
        .row.height-inherit
          .col-sm-12.height-inherit
            .card-shuffle__card-inner.green.height-inherit.grab
    .card-shuffle__card(data-position="1")
      .container.height-inherit
        .row.height-inherit
          .col-sm-12.height-inherit
            .card-shuffle__card-inner.red.height-inherit.grab
    .card-shuffle__card(data-position="2")
      .container.height-inherit
        .row.height-inherit
          .col-sm-12.height-inherit
            .card-shuffle__card-inner.blue.height-inherit.grab
    .card-shuffle__card(data-position="3")
      .container.height-inherit
        .row.height-inherit
          .col-sm-12.height-inherit
            .card-shuffle__card-inner.orange.height-inherit.grab

View Compiled
body {
  background-color: #ECF0F1;
}

// Easing
$easeOutQuart: cubic-bezier(0.165, 0.840, 0.440, 1.000);
$easeOutExpo: cubic-bezier(0.190, 1.000, 0.220, 1.000);

.blue { background-color: #3498DB; }
.red { background-color: #E74C3C; }
.orange { background-color: #E67E22; }
.green { background-color: #2ECC71; }

.relative { position: relative; }
.height-inherit { height: inherit; }

.grab {
  cursor: -webkit-grab;
  cursor: -moz-grab;
  cursor: grab;
}

.grab.grabbing,
.grabbing,
.grabbing .grab,
.gxRangeHandle:active {
  cursor: -webkit-grabbing;
  cursor: -moz-grabbing;
  cursor: grabbing;
}

.container {
  max-width: 1200px;
}
@media (min-width: 768px) {
  .container {
    width: 93%;
  }
}

$stackedAdvanceDuration: .1s;
$stackedOffDeckDuration: .3s;
$stackedToBackDuration: .15s;

.card-shuffle {
  position: relative;
  padding-bottom: 20px;
  padding-top: 20px;
  height: 320px;
  width: 100%;
  overflow: hidden;
}

.card-shuffle__card {
  position: absolute;
  z-index: 0;
  top: 0;
  left: 0;
  width: 100%;
  height: 275px;
  transform: translateZ(0);
  transition: transform $stackedAdvanceDuration $easeOutQuart;
}

.card-shuffle__card-inner {
  border-radius: 2px;
  box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.2);
}

$skews: (-1.0deg, 1.5deg, -0.6deg, 0.8deg, -1.5deg);

@for $i from 1 through length($skews) {
  .card-shuffle__card[data-position="#{$i - 1}"] {
    z-index: length($skews) - $i;
  }
  .card-shuffle__card:nth-child(#{$i}) {
    transform: rotate( nth($skews, $i) );
  }
}

.card-shuffle__card[data-position="0"] {
  transition-duration: $stackedToBackDuration;
}

.card-shuffle__card.card-shuffle__card--front {
  transform: rotate(0);
}

// To bottom of the deck.
.card-shuffle__card.card-shuffle__card--to-bottom {
  z-index: 10;
  transition-duration: $stackedOffDeckDuration;
  transition-timing-function: $easeOutExpo;
}

.card-shuffle__card.card-shuffle--no-transition {
  transition-duration: 0ms;
}


@media (max-width: 568px) {
  $skews: (-2.4deg, 2.5deg, -3.0deg, 2.8deg, -2.5deg);

  @for $i from 1 through length($skews) {
    .card-shuffle__card:nth-child(#{$i}) {
      transform: rotate( nth($skews, $i) );
    }
  }

  .card-shuffle__card.card-shuffle__card--front {
    transform: rotate(0);
  }
}

@media (max-width: 767px) {

  .card-shuffle__card.card-shuffle__card--off-deck-end {
    transform: translate( calc(100% - 30px), 0 );
  }

  .card-shuffle__card.card-shuffle__card--off-deck-start {
    transform: translate( calc(-100% + 30px), 0 );
  }
}

@media (min-width: 768px) {

  .card-shuffle__card.card-shuffle__card--off-deck-end {
    transform: translate( calc(93% - 30px), 0 );
  }

  .card-shuffle__card.card-shuffle__card--off-deck-start {
    transform: translate( calc(-93% + 30px), 0 );
  }
}

@media (min-width: (1200px / 0.93)) {

  .card-shuffle__card.card-shuffle__card--off-deck-end {
    transform: translate( calc(1200px - 30px), 0 );
  }

  .card-shuffle__card.card-shuffle__card--off-deck-start {
    transform: translate( calc(-1200px + 30px), 0 );
  }
}

View Compiled

define('app/device',['require','modernizr'],function(require) {

  var Modernizr = require('modernizr');
  var Device = {};

  /**
   * Hyphenates a javascript style string to a css one. For example:
   * MozBoxSizing -> -moz-box-sizing.
   *
   * @param {string|boolean} str The string to hyphenate.
   * @return {string} The hyphenated string.
   */
  Device.hyphenate = function(str) {

    // Catch booleans.
    if (!str) {
      return '';
    }

    // Turn MozBoxSizing into -moz-box-sizing.
    return str.replace(/([A-Z])/g, function(str, m1) {
      return '-' + m1.toLowerCase();
    }).replace(/^ms-/, '-ms-');
  };


  /**
   * Object Model prefixes.
   * @type {string}
   * @private
   */
  Device.omPrefixes_ = 'Webkit Moz O ms';


  /**
   * CSS Object Model prefixes.
   * @type {Array.<string>}
   * @private
   */
  Device.cssomPrefixes_ = Device.omPrefixes_.split(' ');


  /**
   * Document Object Model prefixes.
   * @type {Array.<string>}
   * @private
   */
  Device.domPrefixes_ = Device.omPrefixes_.toLowerCase().split(' ');


  /**
   * Prefix lookup cache.
   * @type {Object}
   * @private
   */
  Device.prefixCache_ = {};

  /**
   * @param {*} obj Anything.
   * @return {boolean}
   */
  var isString = function(obj) {
    return typeof obj === 'string';
  };


  /**
   * Capitalize a string.
   * @param {string} str String to capitalize.
   * @return {string} Capitalized string.
   */
  var capitalize = function(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  };


  /**
   * Returns the prefixed style property if it exists.
   * Mimics of Modernizr.prefixed. So Device.prefixed('transform') should
   * return 'WebkitTransform' for Chrome and 'transform' for FF and IE10.
   * {@link http://perfectionkills.com/feature-testing-css-properties/}
   *
   * @param  {string} propName The property name.
   * @param  {Element=} opt_element Element to test. Defaults to html element.
   * @return {string|boolean} The style property or false.
   */
  Device.prefixed = function(propName, opt_element) {
    opt_element = opt_element || document.documentElement;
    var style = opt_element.style,
        cache = Device.prefixCache_,
        prefixes = Device.cssomPrefixes_,
        prefixed,
        uPropName;

    // check cache only when no element is given.
    if (arguments.length === 1 && isString(cache[propName])) {
      return cache[propName];
    }

    // test standard property first.
    if (isString(style[propName])) {
      return (cache[propName] = propName);
    }

    // capitalize.
    uPropName = capitalize(propName);

    // test vendor specific properties.
    for (var i = 0, l = prefixes.length; i < l; i++) {
      prefixed = prefixes[i] + uPropName;
      if (isString(style[prefixed])) {
        return (cache[propName] = prefixed);
      }
    }

    return false;
  };


  /**
   * Prefixed style properties.
   * @enum {string|boolean}
   */
  Device.Js = {
    TRANSFORM: Device.prefixed('transform'),
    TRANSITION: Device.prefixed('transition'),
    TRANSITION_PROPERTY: Device.prefixed('transitionProperty'),
    TRANSITION_DURATION: Device.prefixed('transitionDuration'),
    TRANSITION_TIMING_FUNCTION: Device.prefixed('transitionTimingFunction'),
    TRANSITION_DELAY: Device.prefixed('transitionDelay')
  };


  /**
   * Prefixed css properties.
   * @enum {string}
   */
  Device.Css = {
    TRANSFORM: Device.hyphenate(Device.Js.TRANSFORM),
    TRANSITION: Device.hyphenate(Device.Js.TRANSITION),
    TRANSITION_PROPERTY: Device.hyphenate(Device.Js.TRANSITION_PROPERTY),
    TRANSITION_DURATION: Device.hyphenate(Device.Js.TRANSITION_DURATION),
    TRANSITION_TIMING_FUNCTION: Device.hyphenate(
        Device.Js.TRANSITION_TIMING_FUNCTION),
    TRANSITION_DELAY: Device.hyphenate(Device.Js.TRANSITION_DELAY)
  };


  /**
   * Whether the browser has css transitions.
   * @type {boolean}
   */
  Device.HAS_TRANSITIONS = Modernizr.csstransitions;


  /**
   * Whether the browser has css transitions.
   * @type {boolean}
   */
  Device.HAS_TRANSFORMS = Modernizr.csstransforms;


  /**
   * The browser can use css transitions and transforms.
   * @type {boolean}
   */
  Device.CAN_TRANSITION_TRANSFORMS = Device.HAS_TRANSITIONS &&
      Device.HAS_TRANSFORMS;


  /**
   * The browser can use 3d css transforms.
   * https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/transforms3d.js
   * @type {boolean}
   */
  Device.HAS_3D_TRANSFORMS = Modernizr.csstransforms3d;


  /**
   * Whether the browser supports touch events.
   * @type {boolean}
   */
  Device.HAS_TOUCH_EVENTS = ('ontouchstart' in window) ||
      !!window.DocumentTouch && document instanceof DocumentTouch;


  /**
   * Whether the browser supports pointer events.
   * http://blogs.windows.com/windows_phone/b/wpdev/archive/2012/11/15/adapting-your-webkit-optimized-site-for-internet-explorer-10.aspx
   * @type {boolean}
   */
  Device.HAS_POINTER_EVENTS = !!navigator.pointerEnabled ||
      !!navigator.msPointerEnabled;


  return Device;
});

define('app/helpers',['require','jquery','app/device', 'modernizr', 'app/event-type'],function(require) {
  var $ = require('jquery');
  var Device = require('app/device');
  var Modernizr = require('modernizr');
  var EventType = require('app/event-type');
  var Helpers = {};

  /** @enum {string} */
  Helpers.ClassName = {
    HIDDEN: 'hidden',
    FADE: 'fade',
    IN: 'in',
    INVISIBLE: 'invisible',
    ACTIVE: 'active',
    GRAB: 'grab',
    GRABBING: 'grabbing'
  };

  // Polyfill Object.create.
  if (!Object.create) {
    Object.create = (function() {
      function F(){}
      return function(o) {
        if (arguments.length !== 1) {
          throw new Error(
              'Object.create implementation only accepts one parameter.');
        }
        F.prototype = o;
        return new F();
      };
    })();
  }

  Helpers.inherits = function(child, parent) {
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;

    /**
     * Taken from Closure :)
     *
     * Calls superclass constructor/method.
     *
     * This function is only available if you use goog.inherits to
     * express inheritance relationships between classes.
     *
     * NOTE: This is a replacement for goog.base and for superClass_
     * property defined in child.
     *
     * @param {!Object} me Should always be "this".
     * @param {string} methodName The method name to call. Calling
     *     superclass constructor can be done with the special string
     *     'constructor'.
     * @param {...*} var_args The arguments to pass to superclass
     *     method/constructor.
     * @return {*} The return value of the superclass method/constructor.
     */
    child.base = function(me, methodName, var_args) {
      var args = Array.prototype.slice.call(arguments, 2);
      return parent.prototype[methodName].apply(me, args);
    };
  };


  /**
   * Capitalize a string.
   * @param {string} str String to capitalize.
   * @return {string} Capitalized string.
   */
  Helpers.capitalize = function(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  };



  // Math utilities.


  /**
   * Clone of jQuery.isNumeric {@link https://api.jquery.com/jQuery.isNumeric}
   * Because goog.isNumber only checks if the type is a number, so NaN passes.
   * @param {*} obj Object to test.
   * @return {!boolean} Whether the given object is a number.
   */
  Helpers.isNumeric = function(obj) {
    return !isNaN(parseFloat(obj)) && isFinite(obj);
  };


  /**
   * Takes a number and clamps it to within the provided bounds.
   * @param {number} value The input number.
   * @param {number} min The minimum value to return.
   * @param {number} max The maximum value to return.
   * @return {number} The input number if it is within bounds, or the nearest
   *     number within the bounds.
   */
  Helpers.clamp = function(value, min, max) {
    return Math.min(Math.max(value, min), max);
  };


  /**
   * Class for representing a box. A box is specified as a top, right, bottom,
   * and left. A box is useful for representing margins and padding.
   *
   * @param {number} top Top.
   * @param {number} right Right.
   * @param {number} bottom Bottom.
   * @param {number} left Left.
   * @constructor
   */
  Helpers.Box = function(top, right, bottom, left) {
    /**
     * Top
     * @type {number}
     */
    this.top = top;

    /**
     * Right
     * @type {number}
     */
    this.right = right;

    /**
     * Bottom
     * @type {number}
     */
    this.bottom = bottom;

    /**
     * Left
     * @type {number}
     */
    this.left = left;
  };


  /**
   * Class for representing rectangular regions.
   * @param {number} x Left.
   * @param {number} y Top.
   * @param {number} w Width.
   * @param {number} h Height.
   * @constructor
   */
  Helpers.Rect = function(x, y, w, h) {
    /**
     * Left
     * @type {number}
     */
    this.left = x;

    /**
     * Top
     * @type {number}
     */
    this.top = y;

    /**
     * Width
     * @type {number}
     */
    this.width = w;

    /**
     * Height
     * @type {number}
     */
    this.height = h;
  };



  // Style utilities

  /**
   * Gets the height and with of an element when the display is not none.
   * @param {Element} element Element to get size of.
   * @return {!{width: number, height: number}} Object with width/height.
   */
  Helpers.getSize = function(element) {
    var offsetWidth = element.offsetWidth;
    var offsetHeight = element.offsetHeight;
    if (offsetWidth === undefined && element.getBoundingClientRect) {
      // Fall back to calling getBoundingClientRect when offsetWidth or
      // offsetHeight are not defined.
      var clientRect = element.getBoundingClientRect();
      return {
        width: clientRect.right - clientRect.left,
        height: clientRect.bottom - clientRect.top
      };
    }
    return {
      width: offsetWidth,
      height: offsetHeight
    };
  };


  Helpers._getBox = function(element, property) {
    var props = $(element).css([
      property + 'Top',
      property + 'Right',
      property + 'Left',
      property + 'Bottom'
    ]);
    return new Helpers.Box(
        Helpers.getFloat(props[property + 'Top']),
        Helpers.getFloat(props[property + 'Right']),
        Helpers.getFloat(props[property + 'Left']),
        Helpers.getFloat(props[property + 'Bottom']));
  };

  Helpers.getFloat = function(value) {
    return parseFloat(value) || 0;
  };


  Helpers.getMarginBox = function(element) {
    return Helpers._getBox(element, 'margin');
  };


  /**
   * Returns a string to be used with transforms. Uses 3d translates
   * when available.
   * @param {string=} opt_x The x position value with units. Default is zero.
   * @param {string=} opt_y The y position value with units. Default is zero.
   * @return {string} The css value for transform.
   */
  Helpers.getTranslateString = function(opt_x, opt_y) {
    var x = opt_x !== undefined ? opt_x : 0;
    var y = opt_y !== undefined ? opt_y : 0;
    var prefix = 'translate';
    var suffix = ')';

    if (Device.HAS_3D_TRANSFORMS) {
      prefix += '3d(';
      suffix = ',0' + suffix;

    } else {
      prefix += '(';
    }

    return prefix + x + ',' + y + suffix;
  };


  Helpers.onTransitionEnd = function( elem, fn, context, opt_property ) {
    // transitioned and ignore others.
    if ( elem.jquery ) {
      elem = elem[0];
    }

    var callback = $.proxy(fn, context || window);
    var fakeEvent = {
      target: elem,
      currentTarget: elem,
      fake: true
    };

    /**
     * @param {$.Event|{target: Element, currentTarget: Element}} evt Event object.
     */
    function transitionEnded(evt) {
      var source = evt.currentTarget;
      // Some other element's transition event could have bubbled up to this.
      if (!source || source !== evt.target) {
        return;
      }

      // If the browser has transitions, there will be a listener bound to the
      // `transitionend` event which needs to be removed. `listenOnce` is not used
      // because transition events can bubble up to the parent.
      if (Modernizr.csstransitions) {
        // If the optional property exists and it's not the property which was
        // transitioned, exit out of the function and continue waiting for the
        // right transition property.
        if (opt_property && !evt.fake && evt.originalEvent.propertyName !== opt_property) {
          return;
        }

        $(source).off(EventType.TRANSITIONEND, transitionEnded);
      }

      // Done!
      callback(evt);
    }


    if (Modernizr.csstransitions) {
      $(elem).on(EventType.TRANSITIONEND, transitionEnded);
      // TODO(glen): Get length of transition and set a timeout as a backup.
      // The transition will not happen if the values don't change on the element,
      // the timeout would be a failsafe for that.
    } else {

      // Push to the end of the queue with a fake event which will pass the checks
      // inside the callback function.
      setTimeout($.proxy(transitionEnded, window, fakeEvent), 0);
    }
  };


  return Helpers;
});
/**
 * @fileoverview The base abstract component providing easy-to-use things for
 * working with dom and other services.
 */

define('app/base-component',['require','jquery','modernizr','app/helpers'],function(require) {


  var $ = require('jquery');
  var Modernizr = require('modernizr');
  var Helpers = require('app/helpers');



  /**
   * The base class for modules.
   * @param {Element} element Main element of the module.
   * @param {boolean} addListener Whether to listen for the 767|768 breakpoint.
   * @constructor
   */
  function BaseComponent( element, addListener ) {
    if ( element ) {
      this.$el = $( element );
      this.element = element;
    }


    if ( addListener ) {
      /**
       * Whether the screen is smaller than 768px or not.
       * @type {boolean}
       */
      this.isSmallScreen = false;

      if ( Modernizr.mediaqueries ) {
        this._mql = window.matchMedia('(max-width: 47.9375em)');
        this._mqlListener = this.handleMediaQueryChange.bind( this );
        this.isSmallScreen = this._mql.matches;
      }
    }
  }


  BaseComponent.prototype.decorateInternal = function() {};
  BaseComponent.prototype.enterDocument = function() {};


  BaseComponent.prototype.getElement = function() {
    return this.element;
  };


  /**
   * Listen for events.
   */
  BaseComponent.prototype.listen = function() {
    // Listen for changes across 767|768.
    if ( this._mql ) {
      this._mql.addListener( this._mqlListener );
    }
  };


  /**
   * Finds an element within this class' main element.
   * @param {string} selector Selector.
   * @param {jQuery|Element} [context] Optionally provide the context (scope)
   *     for the query. Default is the main element of the class.
   * @return {jQuery} A jQuery object which may or may not contain the element
   *     which was searched for.
   */
  BaseComponent.prototype.findBySelector = function( selector, context ) {
    return $( selector, context || this.$el );
  };


  /**
   * Finds an element within this class' main element.
   * @param {string} className Class name to search for.
   * @param {jQuery|Element} [context] Optionally provide the context (scope)
   *     for the query. Default is the main element of the class.
   * @return {jQuery} A jQuery object which may or may not contain the element
   *     which was searched for.
   */
  BaseComponent.prototype.findByClass = function( className, context ) {
    return this.findBySelector( '.' + className, context );
  };


  BaseComponent.prototype.getElementByClass = function(className, context) {
    return this.findByClass(className, context).get(0) || null;
  };


  BaseComponent.prototype.getElementsByClass = function(className, context) {
    return this.findByClass(className, context).get();
  };


  /**
   * Retieves elements from the children of a parent which match the given
   * class. This function is useful when an element has the same class name
   * nested deeper within the element that is needed.
   * @param {string} className The classname to match against.
   * @param {Element} parent The element whose children will be filtered.
   * @return {!Array.<Element>} Direct descendants of the parent by class.
   */
  BaseComponent.prototype.getDirectDescendantsByClass = function(
      className, parent) {
    return $(parent).children('.' + className).get();
  };


  BaseComponent.prototype.getParentByClass = function(className, child,
      opt_context) {
    var context = opt_context || this.element;
    return $(child).closest('.' + className, context).get(0) || null;
  };


  /**
   * Cleanup DOM references and event listeners.
   */
  BaseComponent.prototype.dispose = function() {
    if ( this._mql ) {
      this._mql.removeListener( this._mqlListener );
      this._mql = null;
    }

    if ( this.$el ) {
      this.$el = null;
      this.element = null;
    }
  };


  /**
   * Triggers an event on the class instance.
   * @param {string|jQuery.Event} eventName Name of the event to trigger or
   *     an event instance.
   * @param {Array.<*>} [args] Optional arguments to send with the event.
   * @return {?boolean} If the event name is an event instance, this function
   *     returns whether or not the event was prevented using preventDefault().
   */
  BaseComponent.prototype.dispatchEvent = function(eventName, args) {
    if ($.type(eventName) === 'string') {
      $(this).trigger( eventName, args && args.length ? args : [ this ] );
      return null;
    } else {
      $(this).trigger(eventName); // undefined not a function :(
      return eventName.isDefaultPrevented();
    }
  };


  /**
   * Return the media query list.
   * @return {MediaQueryList}
   * @protected
   */
  BaseComponent.prototype.getMediaQueryList = function() {
    return this._mql;
  };


  /**
   * Media query changed. Ideally this should be on some kind of base component which
   * all modules inherit from.
   * @param {MediaQueryList} mediaQueryList The media query list object.
   * @protected
   */
  BaseComponent.prototype.handleMediaQueryChange = function( mediaQueryList ) {
    this.isSmallScreen = mediaQueryList.matches;
  };


  return BaseComponent;
});


/**
 * @fileoverview A utility class for representing two-dimensional positions.
 */

define('app/coordinate',[],function() {
  /**
   * Class for representing coordinates and positions.
   * @param {number=} opt_x Left, defaults to 0.
   * @param {number=} opt_y Top, defaults to 0.
   * @constructor
   */
  var Coordinate = function(opt_x, opt_y) {
    /**
     * X-value
     * @type {number}
     */
    this.x = opt_x !== undefined ? opt_x : 0;

    /**
     * Y-value
     * @type {number}
     */
    this.y = opt_y !== undefined ? opt_y : 0;
  };


  /**
   * Returns the distance between two coordinates.
   * @param {!Coordinate} a A Coordinate.
   * @param {!Coordinate} b A Coordinate.
   * @return {number} The distance between {@code a} and {@code b}.
   */
  Coordinate.distance = function(a, b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return Math.sqrt(dx * dx + dy * dy);
  };

  return Coordinate;
});
define('app/event-type',['app/device'], function(Device) {

  // https://github.com/Modernizr/Modernizr/blob/master/src/prefixed.js
  var transEndEventNames = {
    'WebkitTransition': 'webkitTransitionEnd', // Saf 6, Android Browser
    'MozTransition': 'transitionend', // only for FF < 15
    'transition': 'transitionend' // IE10, Opera, Chrome, FF 15+, Saf 7+
  };

  var hasUnprefixedPointerEvents = !!navigator.pointerEnabled;

  function getPointerEvent(event) {
    if (Device.HAS_POINTER_EVENTS) {
      if (hasUnprefixedPointerEvents) {
        return event.toLowerCase();
      } else {
        return 'MS' + event;
      }
    } else {
      return null;
    }
  }


  return {
    // Mouse events
    CLICK: 'click',
    MOUSEDOWN: 'mousedown',
    MOUSEUP: 'mouseup',
    MOUSEOVER: 'mouseover',
    MOUSEOUT: 'mouseout',
    MOUSEMOVE: 'mousemove',

    // WebKit touch events.
    TOUCHSTART: 'touchstart',
    TOUCHMOVE: 'touchmove',
    TOUCHEND: 'touchend',
    TOUCHCANCEL: 'touchcancel',

    POINTERCANCEL: getPointerEvent('PointerCancel'),
    POINTERDOWN: getPointerEvent('PointerDown'),
    POINTERMOVE: getPointerEvent('PointerMove'),
    POINTEROVER: getPointerEvent('PointerOver'),
    POINTEROUT: getPointerEvent('PointerOut'),
    POINTERUP: getPointerEvent('PointerUp'),

    TRANSITIONEND: transEndEventNames[Device.Js.TRANSITION]
  };
});
/**
 * @fileoverview A draggable class which uses translate when
 * available and can use a containing element instead of specific boundaries.
 * It also emits events with more useful information for swipes, like velocity,
 * distance, and direction.
 */

define('app/draggable',['require','jquery','app/helpers','app/base-component','app/coordinate','app/device','app/event-type'],function(require) {

  var $ = require('jquery');
  var Helpers = require('app/helpers');
  var BaseComponent = require('app/base-component');
  var Coordinate = require('app/coordinate');
  var Device = require('app/device');
  var EventType = require('app/event-type');

  /**
   * A class that allows mouse or touch-based dragging (moving) of an element.
   *
   * @param {Element} dragElement The element that will be dragged.
   * @param {Element} containment The element which will contain the draggable
   *     element.
   * @param {string=} opt_axis The axis to drag on. Defaults to both. Should be
   *     "x" or "y" to constrain to an axis.
   * @extends {BaseComponent}
   * @constructor
   */
  var Draggable = function(dragElement, containment, opt_axis, opt_limits) {
    Draggable.base(this, 'constructor', dragElement);

    /**
     * The draggable element.
     * @type {Element}
     * @private
     */
    this.el_ = dragElement;

    /**
     * The element which contains the target.
     * @type {Element}
     * @private
     */
    this.containerEl_ = containment;

    /**
     * Object to keep track of the current position of the handle/target.
     * @type {Coordinate}
     * @private
     */
    this.currentPosition_ = new Coordinate();

    /**
     * Object to keep track of the where the starting location for dragging was.
     * Relative to the draggable element.
     * @type {Coordinate}
     * @private
     */
    this.startPosition_ = new Coordinate();

    /**
     * Current x position of mouse or touch relative to the document.
     * @type {number}
     */
    this.pageX = 0;


    /**
     * Current y position of mouse or touch relative to the document.
     * @type {number}
     */
    this.pageY = 0;


    /**
     * The x position where the first mousedown or touchstart occurred.
     * @type {number}
     */
    this.startX = 0;


    /**
     * The y position where the first mousedown or touchstart occurred.
     * @type {number}
     */
    this.startY = 0;


    /**
     * Current x position of drag relative to target's parent.
     * @type {number}
     */
    this.deltaX = 0;


    /**
     * Current y position of drag relative to target's parent.
     * @type {number}
     */
    this.deltaY = 0;

    /**
     * Limits of how far the draggable element can be dragged.
     * @type {Helpers.Rect}
     */
    this.limits = opt_limits || new Helpers.Rect(NaN, NaN, NaN, NaN);

    /**
     * Friction to apply to dragging. A value of zero would result in no dragging,
     * 0.5 would result in the draggable element moving half as far as the user
     * dragged, and 1 is a 1:1 ratio with user movement.
     * @type {number}
     */
    this.friction_ = 1;

    /**
     * Draggable axis.
     * @type {string}
     * @private
     */
    this.axis_ = opt_axis || Draggable.Axis.BOTH;

    /**
     * Flag indicating dragging has happened. It is set on dragmove and reset
     * after the draggableend event has been dispatched.
     * @type {boolean}
     */
    this.hasDragged = false;

    /**
     * Whether the user is locked in place within the draggable element. This
     * is set to true when `preventDefault` is called on the move event.
     * @type {boolean}
     * @private
     */
    this.isLocked_ = false;

    /**
     * Whether dragging is enabled internally. If the user attempts to scroll
     * in the opposite direction of the draggable element, this is set to true
     * and no more drag move events are counted until the user releases and
     * starts dragging again.
     * @type {boolean}
     * @private
     */
    this.isDeactivated_ = false;

    /**
     * Whether dragging is currently enabled.
     * @type {boolean}
     * @private
     */
    this.enabled_ = true;

    var $el = $(this.el_);
    // Namespace
    var ns = '.draggable';

    if (Device.HAS_POINTER_EVENTS) {
      $el.on(EventType.POINTERDOWN + ns, this.handleDragStart_.bind(this));

    } else {
      $el.on(EventType.MOUSEDOWN + ns, this.handleDragStart_.bind(this));

      if (Device.HAS_TOUCH_EVENTS) {
        $el.on(EventType.TOUCHSTART + ns, this.handleDragStart_.bind(this));
      }
    }
  };

  Helpers.inherits(Draggable, BaseComponent);


  /** @enum {string} */
  Draggable.EventType = {
    START: 'draggablestart',
    MOVE: 'draggablemove',
    END: 'draggableend'
  };


  /** @enum {string} */
  Draggable.Direction = {
    RIGHT: 'right',
    LEFT: 'left',
    UP: 'up',
    DOWN: 'down',
    NONE: 'no_movement'
  };


  /** @enum {string} */
  Draggable.Axis = {
    X: 'x',
    Y: 'y',
    BOTH: 'xy'
  };


  /**
   * The scroll/drag amount (pixels) required on the draggable axis before
   * stopping further page scrolling/movement.
   * @const {number}
   */
  Draggable.LOCK_THRESHOLD = 6;


  /**
   * The scroll/drag amount (pixels) required on the opposite draggable axis
   * before dragging is deactivated for the rest of the interaction.
   * @const {number}
   */
  Draggable.DRAG_THRESHOLD = 5;


  /**
   * Calculate the velocity between two points.
   *
   * @param {number} deltaTime Change in time.
   * @param {number} deltaX Change in x.
   * @param {number} deltaY Change in y.
   * @return {Object} Velocity of the drag.
   */
  Draggable.getVelocity = function(deltaTime, deltaX, deltaY) {
    var velocityX = Math.abs(deltaX / deltaTime);
    var velocityY = Math.abs(deltaY / deltaTime);
    return {
      x: isFinite(velocityX) ? velocityX : 0,
      y: isFinite(velocityY) ? velocityY : 0
    };
  };


  /**
   * angle to direction define.
   * @param {Coordinate} coord1 The starting coordinate.
   * @param {Coordinate} coord2 The ending coordinate.
   * @return {string} Direction constant.
   */
  Draggable.getDirection = function(coord1, coord2) {
    var x = Math.abs(coord1.x - coord2.x);
    var y = Math.abs(coord1.y - coord2.y);

    if (x >= y) {
      return coord1.x - coord2.x > 0 ?
          Draggable.Direction.LEFT :
          coord1.x - coord2.x < 0 ?
            Draggable.Direction.RIGHT :
            Draggable.Direction.NONE;
    } else {
      return coord1.y - coord2.y > 0 ?
          Draggable.Direction.UP :
          coord1.y - coord2.y < 0 ?
            Draggable.Direction.DOWN :
            Draggable.Direction.NONE;
    }
  };


  /**
   * Saves the containment element's width and height and scrubber position.
   * @private
   */
  Draggable.prototype.setDimensions_ = function() {
    var containmentRect = this.containerEl_.getBoundingClientRect();
    var elRect = this.el_.getBoundingClientRect();

    var relativeElement = Device.CAN_TRANSITION_TRANSFORMS ?
        this.el_ :
        this.containerEl_;

    // getBoundingClientRect does not include margins. They must be accounted for.
    var margins = Helpers.getMarginBox(this.el_);
    var offsetCorrectionX = margins.left;
    var offsetCorrectionY = margins.top;
    offsetCorrectionX += containmentRect.left;
    offsetCorrectionY += containmentRect.top;

    this.containmentWidth_ = relativeElement.offsetWidth;
    this.containmentHeight_ = relativeElement.offsetHeight;

    if (this.containmentWidth_ === 0) {
      throw new Error('containing element\'s width is zero');
    } else if (this.containmentHeight_ === 0) {
      throw new Error('containing element\'s height is zero');
    }

    this.startPosition_.x = elRect.left - offsetCorrectionX;
    this.startPosition_.y = elRect.top - offsetCorrectionY;
  };


  /**
   * Get whether dragger is enabled
   * @return {boolean} Whether dragger is enabled.
   */
  Draggable.prototype.getEnabled = function() {
    return this.enabled_;
  };


  /**
   * Set whether dragger is enabled
   * @param {boolean} enabled Whether dragger is enabled.
   */
  Draggable.prototype.setEnabled = function(enabled) {
    this.enabled_ = enabled;
  };


  /**
   * Sets (or reset) the Drag limits after a Dragger is created.
   * @param {Helpers.Rect} limits Object containing left, top, width,
   *     height for new Dragger limits.
   */
  Draggable.prototype.setLimits = function(limits) {
    this.limits = limits || new Helpers.Rect(NaN, NaN, NaN, NaN);
  };


  /**
   * Returns the 'real' x after limits are applied (allows for some
   * limits to be undefined).
   * @param {number} x X-coordinate to limit.
   * @return {number} The 'real' X-coordinate after limits are applied.
   */
  Draggable.prototype.limitX = function(x) {
    var rect = this.limits;
    var left = isNaN(rect.left) ? null : rect.left;
    var width = isNaN(rect.width) ? 0 : rect.width;
    var maxX = left !== null ? left + width : Infinity;
    var minX = left !== null ? left : -Infinity;
    return Helpers.clamp(x, minX, maxX);
  };


  /**
   * Returns the 'real' y after limits are applied (allows for some
   * limits to be undefined).
   * @param {number} y Y-coordinate to limit.
   * @return {number} The 'real' Y-coordinate after limits are applied.
   */
  Draggable.prototype.limitY = function(y) {
    var rect = this.limits;
    var top = isNaN(rect.top) ? null : rect.top;
    var height = isNaN(rect.height) ? 0 : rect.height;
    var maxY = top !== null ? top + height : Infinity;
    var minY = top !== null ? top : -Infinity;
    return Helpers.clamp(y, minY, maxY);
  };


  /**
   * Returns the x and y positions the draggable element should be set to.
   * @param {Coordinate=} opt_position Position to set the draggable
   *     element. This will optionally override calculating the position
   *     from a drag.
   * @return {!Coordinate} The x and y coordinates.
   * @private
   */
  Draggable.prototype.getElementPosition_ = function(opt_position) {
    var outputX = 0;
    var outputY = 0;

    if (opt_position) {
      var scrubberX = (opt_position.x / 100) * this.containerEl_.offsetWidth;
      var scrubberY = (opt_position.y / 100) * this.containerEl_.offsetHeight;
      this.currentPosition_.x = this.limitX(scrubberX);
      this.currentPosition_.y = this.limitY(scrubberY);
    }

    var newX = (this.currentPosition_.x / this.containmentWidth_) * 100;
    var newY = (this.currentPosition_.y / this.containmentHeight_) * 100;

    // Drag horizontal only.
    if (this.axis_ === Draggable.Axis.X) {
      outputX = newX;

    // Drag vertical only.
    } else if (this.axis_ === Draggable.Axis.Y) {
      outputY = newY;

    // Drag both directions.
    } else {
      outputX = newX;
      outputY = newY;
    }

    return new Coordinate(outputX, outputY);
  };


  /**
   * Apply a friction value to an pixel position, reducing its value.
   * @param {number} position X or Y position.
   * @return {number} Position multiplied by friction.
   * @private
   */
  Draggable.prototype.applyFriction_ = function(position) {
    return position * this.friction_;
  };


  /**
   * Drag start handler.
   * @param  {jQuery.Event} evt The drag event object.
   * @private
   */
  Draggable.prototype.handleDragStart_ = function(evt) {
    var browserEvent = evt.originalEvent;
    var isTouchEvent = !!browserEvent.changedTouches;
    var isPointerEvent = !!browserEvent.pointerId;

    // Must be left click to drag.
    if (!this.enabled_ || !isTouchEvent && evt.which !== 1) {
      return;
    }

    // Use the first touch for the pageX and pageY.
    if (isTouchEvent) {
      this.startX = browserEvent.changedTouches[0].pageX;
      this.startY = browserEvent.changedTouches[0].pageY;

    // Pointer events have trusted pageX and pageY values, but jQuery doesn't
    // normalize it for us because it doesn't know what pointer events are yet.
    } else if (isPointerEvent) {
      this.startX = browserEvent.pageX;
      this.startY = browserEvent.pageY;
    } else {
      this.startX = evt.pageX; // Normalized by jQuery.
      this.startY = evt.pageY; // Normalized by jQuery.
    }

    this.pageX = this.startX;
    this.pageY = this.startY;
    this.deltaX = 0;
    this.deltaY = 0;

    this.timestamp = $.now();
    this.deltaTime = 0;
    this.setDimensions_();

    this.currentPosition_ = new Coordinate(
        this.startPosition_.x,
        this.startPosition_.y);

    // Give a hook to others
    var isPrevented = this.dispatchEvent(new DraggableEvent(
        Draggable.EventType.START, this, this.startPosition_,
        this.startPosition_));

    if (!isPrevented) {
      this.setupDragHandlers();
    }
  };


  /**
   * Drag move, after applyDraggableElementPosition has happened
   * @param {jQuery.Event} evt The dragger event.
   * @private
   */
  Draggable.prototype.handleDragMove_ = function(evt) {
    if (!this.enabled_ || this.isDeactivated_) {
      return;
    }

    var browserEvent = evt.originalEvent;
    var isTouchEvent = !!browserEvent.changedTouches;
    var isPointerEvent = !!browserEvent.pointerId;


    if (isTouchEvent) {
      this.pageX = browserEvent.changedTouches[0].pageX;
      this.pageY = browserEvent.changedTouches[0].pageY;
    } else if (isPointerEvent) {
      this.pageX = browserEvent.pageX;
      this.pageY = browserEvent.pageY;
    } else {
      this.pageX = evt.pageX; // Normalized by jQuery.
      this.pageY = evt.pageY; // Normalized by jQuery.
    }

    this.deltaX = this.pageX - this.startX;
    this.deltaY = this.pageY - this.startY;


    var newX = this.startPosition_.x + this.applyFriction_(this.deltaX);
    var newY = this.startPosition_.y + this.applyFriction_(this.deltaY);

    this.currentPosition_.x = this.limitX(newX);
    this.currentPosition_.y = this.limitY(newY);
    this.deltaTime = $.now() - this.timestamp;


    // Emit an event.
    var isPrevented = this.dispatchEvent(new DraggableEvent(
        Draggable.EventType.MOVE,
        this, this.startPosition_, this.currentPosition_));

    // Abort if the developer prevented default on the custom event.
    if (isPrevented) {
      return;
    }

    this.hasDragged = true;

    var absX = Math.abs(this.deltaX);
    var absY = Math.abs(this.deltaY);

    // Prevent scrolling if the user has moved past the locking threshold.
    if ((this.axis_ === Draggable.Axis.X && absX > Draggable.LOCK_THRESHOLD) ||
        (this.axis_ === Draggable.Axis.Y && absY > Draggable.LOCK_THRESHOLD)) {
      this.isLocked_ = true;
      evt.preventDefault();
    }

    // Disable dragging if the user is attempting to go the opposite direction
    // of the draggable element.
    if (!this.isLocked_ && (
        (this.axis_ === Draggable.Axis.X && absY > Draggable.DRAG_THRESHOLD) ||
        (this.axis_ === Draggable.Axis.Y && absX > Draggable.DRAG_THRESHOLD))) {
      this.isDeactivated_ = true;
    }

    if (!this.isDeactivated_) {
      this.applyDraggableElementPosition();
    }
  };


  /**
   * Dragging ended.
   * @private
   */
  Draggable.prototype.handleDragEnd_ = function() {
    this.cleanupDragHandlers();

    var start = this.startPosition_;
    var end = this.currentPosition_;

    this.deltaTime = $.now() - this.timestamp;

    // Give a hook to others
    this.dispatchEvent(new DraggableEvent(
        Draggable.EventType.END, this, start, end));

    this.hasDragged = false;
    this.isDeactivated_ = false;
    this.isLocked_ = false;
  };


  /**
   * Sets the position of thd draggable element.
   * @param {Coordinate=} opt_position Position to set the draggable
   *     element. This will optionally override calculating the position
   *     from a drag.
   * @return {Coordinate} The position the draggable element was set to.
   */
  Draggable.prototype.applyDraggableElementPosition = function(opt_position) {
    var pos = this.getElementPosition_(opt_position);

    // Add percentage unit
    var outputX = pos.x + '%';
    var outputY = pos.y + '%';

    if (Device.CAN_TRANSITION_TRANSFORMS) {
      this.el_.style[Device.Js.TRANSFORM] =
          Helpers.getTranslateString(outputX, outputY);
    } else {
      this.el_.style.left = outputX;
      this.el_.style.top = outputY;
    }

    return this.currentPosition_;
  };


  /**
   * Binds events to the document for move, end, and cancel (if cancel events
   * exist for the device).
   */
  Draggable.prototype.setupDragHandlers = function() {
    var $doc = $(document);
    var ns = '.draggable';

    if (Device.HAS_POINTER_EVENTS) {
      $doc.on(EventType.POINTERMOVE + ns, this.handleDragMove_.bind(this));
      $doc.on(EventType.POINTERUP + ns, this.handleDragEnd_.bind(this));

      // Touch and pointers have cancel events for when the user goes into
      // something like the browser chrome.
      $doc.on(EventType.POINTERCANCEL + ns, this.handleDragEnd_.bind(this));

    } else {
      $doc.on(EventType.MOUSEMOVE + ns, this.handleDragMove_.bind(this));
      $doc.on(EventType.MOUSEUP + ns, this.handleDragEnd_.bind(this));

      if (Device.HAS_TOUCH_EVENTS) {
        $doc.on(EventType.TOUCHMOVE + ns, this.handleDragMove_.bind(this));
        $doc.on(EventType.TOUCHEND + ns, this.handleDragEnd_.bind(this));

        // Touch and pointers have cancel events for when the user goes into
        // something like the browser chrome.
        $doc.on(EventType.TOUCHCANCEL + ns, this.handleDragEnd_.bind(this));
      }
    }
  };


  /**
   * Removes the events bound during drag start. The draggable namespace can be
   * used to remove all of them because the drag start event is still bound
   * to the actual element.
   */
  Draggable.prototype.cleanupDragHandlers = function() {
    var $doc = $(document);
    $doc.off('.draggable');
  };


  /**
   * Returns the current position of the draggable element.
   * @param {boolean} opt_asPercent Optionally retrieve percentage values instead
   *     of pixel values.
   * @return {Coordinate} X and Y coordinates of the draggable element.
   */
  Draggable.prototype.getPosition = function(opt_asPercent) {
    if (opt_asPercent) {
      return new Coordinate(
          (this.currentPosition_.x / this.containmentWidth_) * 100,
          (this.currentPosition_.y / this.containmentHeight_) * 100);
    } else {
      return this.currentPosition_;
    }
  };


  /**
   * Set the position of the draggable element.
   * @param {number} x X position as a percentage. Eg. 50 for "50%".
   * @param {number} y Y position as a percentage. Eg. 50 for "50%".
   * @return {Coordinate} The position the draggable element was set to.
   */
  Draggable.prototype.setPosition = function(x, y) {
    return this.applyDraggableElementPosition(new Coordinate(x, y));
  };


  /**
   * Set the friction value.
   * @param {number} friction A number between [1, 0].
   */
  Draggable.prototype.setFriction = function(friction) {
    if (friction !== this.friction_) {
      this.friction_ = friction;
    }
  };


  /**
   * Easy way to trigger setting dimensions. Useful for doing things after this
   * class has been initialized, but no dragging has occurred yet.
   * @return {Draggable} The instance.
   */
  Draggable.prototype.update = function() {
    this.setDimensions_();
    return this;
  };


  /** @override */
  Draggable.prototype.dispose = function() {
    Draggable.base(this, 'dispose');

    this.containerEl_ = null;
    this.el_ = null;

    // Remove pointer/mouse/touch events by namespace.
    $(this.el_).off('.draggable');
  };



  /**
   * Object representing a drag event.
   * @param {string} type Event type.
   * @param {Draggable} draggable The draggable instance.
   * @param {Coordinate} start The starting coordinate.
   * @param {Coordinate} end The ending coordinate.
   * @constructor
   * @extends {jQuery.Event}
   */
  var DraggableEvent = function(type, draggable, start, end) {
    $.Event.call(this, type);

    /**
     * @type {Element}
     */
    this.target = draggable.el_;

    /**
     * The change in x from drag start to end.
     * @type {number}
     */
    this.deltaX = end.x - start.x;

    /**
     * The change in y from drag start to end.
     * @type {number}
     */
    this.deltaY = end.y - start.y;

    /**
     * Time elapsed from mouse/touch down to mouse/touch up.
     * @type {number}
     */
    this.deltaTime = draggable.deltaTime;

    /**
     * Reference to the drag object for this event.
     * @type {Draggable}
     */
    this.draggable = draggable;

    /**
     * Velocity in drag.
     * @type {number}
     */
    this.velocity = Draggable.getVelocity(this.deltaTime,
        this.deltaX, this.deltaY);

    /**
     * Distance dragged.
     * @type {number}
     */
    this.distance = Coordinate.distance(start, end);

    /**
     * Direction of drag.
     * @type {string}
     */
    this.direction = Draggable.getDirection(start, end);

    var onAxis = false;

    // Is X and direction is right or left.
    if (draggable.axis_ === Draggable.Axis.X &&
      (this.direction === Draggable.Direction.LEFT ||
      this.direction === Draggable.Direction.RIGHT)) {
      onAxis = true;

    // Is Y and direction is down or up.
    } else if (draggable.axis_ === Draggable.Axis.Y &&
      (this.direction === Draggable.Direction.UP ||
      this.direction === Draggable.Direction.DOWN)) {
      onAxis = true;

    // Is both and direction is not none.
    } else if (draggable.axis_ === Draggable.Axis.BOTH &&
        this.direction !== Draggable.Direction.NONE) {
      onAxis = true;
    }

    /**
     * Whether the drag direction is on the axis of the draggable element.
     * @type {boolean}
     */
    this.isDirectionOnAxis = onAxis;

    /** @type {Coordinate} */
    this.position = draggable.currentPosition_;
  };

  Helpers.inherits(DraggableEvent, $.Event);


  return Draggable;
});


define('card-shuffle', ['require', 'jquery', 'app/device', 'app/helpers', 'app/base-component', 'app/draggable'], function(require) {
  var $ = require('jquery');
  var Device = require('app/device');
  var Helpers = require('app/helpers');
  var BaseComponent = require('app/base-component');
  var Draggable = require('app/draggable');

  var CardShuffle = function(element, isReverseStacked, isDraggable) {
    CardShuffle.base(this, 'constructor', element, true);
    this.currentIndex = 0;
    this.$cards = this.findByClass(CardShuffle.ClassName.CARD);
    this.totalCards = this.$cards.length;
    this.positions = $.map(this.$cards, function(card) {
      return parseFloat($(card).attr('data-position'));
    });
    this.isTransitioning = false;
    this.isReverseStacked = isReverseStacked;
    this.isDraggable = isDraggable;
    this.draggables = null;
    this.timerId = null;
    this.init();
  };

  Helpers.inherits(CardShuffle, BaseComponent);


  CardShuffle.ClassName = {
    CARD: 'card-shuffle__card',
    NO_TRANSITION: 'card-shuffle--no-transition',
    TO_BOTTOM: 'card-shuffle__card--to-bottom',
    FRONT: 'card-shuffle__card--front',
    START: 'card-shuffle__card--off-deck-start',
    END: 'card-shuffle__card--off-deck-end',
    INNER: 'card-shuffle__card-inner'
  };


  /**
   * When the card pops off the top of the deck, it can go in different
   * directions.
   * @type {Object}
   */
  CardShuffle.Direction = {
    START: CardShuffle.ClassName.START,
    END: CardShuffle.ClassName.END
  };


  CardShuffle.prototype.init = function() {

    var frontIndex = this.isReverseStacked ? 0 : this.totalCards - 1;
    this.$cards.filter('[data-position=' + frontIndex + ']')
      .addClass(CardShuffle.ClassName.FRONT);

    if (this.isDraggable) {
      this.draggables = new Array(this.totalCards);
      var self = this;
      this.$cards.each(function(i, card) {
        self.draggables[i] = new Draggable(card, card.parentNode, Draggable.Axis.X);
        if (i > 0) {
          self.draggables[i].setEnabled(false);
        }
      });
    }

    this.listen();
  };

  CardShuffle.prototype.listen = function() {
    CardShuffle.base(this, 'listen');

    if (this.isDraggable) {
      this.draggables.forEach(function(draggable) {
        $(draggable).on(Draggable.EventType.START, this._handleDraggableStart.bind(this));
        $(draggable).on(Draggable.EventType.END, this._handleDraggableEnd.bind(this));
      }, this);

    }
  };


  CardShuffle.prototype.dispose = function() {
    CardShuffle.base(this, 'dispose');
    if (this.draggables) {
      this.draggables.forEach(function(draggable) {
        $(draggable).off(Draggable.EventType.END);
      }, this);
    }
    this.$cards = null;
  };


  /**
   * Returns the logical index of a neighbor from a given relative number.
   * Non looped carousels at 0 and length - 1 don't have previous and next
   * neighbors, respectively, so 0 or length - 1 will be return, respectively.
   *
   * @param {number} logicalIndex The logical index which the second parameter
   *     is relative to.
   * @param {number} relativePos The relative position to the first parameter.
   * For example, -2 or 2.
   * @return {number} The logical index of the neighbor.
   * @private
   */
  CardShuffle.prototype._getRelativeIndex = function(logicalIndex, relativePos) {
    var index = logicalIndex + relativePos;

    if (index < 0) {
      return this.totalCards + index;
    } else if (index > this.totalCards - 1) {
      return index - this.totalCards;
    } else {
      return index;
    }
  };


  CardShuffle.prototype.advance = function(opt_direction) {
    var previousIndex = this.currentIndex;
    var selectedIndex = previousIndex + 1;
    var direction = opt_direction ? opt_direction : CardShuffle.Direction.END;

    if (selectedIndex >= this.totalCards) {
      selectedIndex = 0;
    }

    this.currentIndex = selectedIndex;

    this._updateCardPositions();
    this._popOffStack(previousIndex, direction);
  };


  CardShuffle.prototype._slideBack = function(draggable) {
    this.isTransitioning = true;

    this.$cards.eq(this.currentIndex).removeClass(CardShuffle.ClassName.NO_TRANSITION);

    var done = function() {
      clearTimeout(this.timerId);
      this.isTransitioning = false;
    }.bind(this);

    Helpers.onTransitionEnd(draggable.getElement(), done);
    this.timerId = setTimeout(done, 500);

    draggable.setPosition(0, 0);
  };


  CardShuffle.prototype._popOffStack = function(cardIndex, direction) {
    var $previousFront = this.$cards.eq(cardIndex);

    this.isTransitioning = true;

    // Animate card out of container
    $previousFront
      .removeClass()
      .addClass([
        CardShuffle.ClassName.CARD,
        CardShuffle.ClassName.TO_BOTTOM,
        direction
      ].join(' '));

    // When done going to the bottom, put it behind the other cards and
    // go back to the top of the container.
    Helpers.onTransitionEnd($previousFront, function($card) {
      $card.removeClass([
        CardShuffle.ClassName.TO_BOTTOM,
        CardShuffle.ClassName.START,
        CardShuffle.ClassName.END,
      ].join(' '));

      Helpers.onTransitionEnd($card, function() {
        this.isTransitioning = false;

        // Toggle dragging.
        this.draggables[cardIndex].setEnabled(false);
        this.draggables[this.currentIndex].setEnabled(true);
      }, this, Device.Css.TRANSFORM);
    }.bind(this, $previousFront));
  };


  CardShuffle.prototype._updateCardPositions = function() {
    this.$cards.each(function(i, el) {
      var currentPosition = this.positions[i];
      var nextPosition = this._getRelativeIndex(currentPosition, this.isReverseStacked ? -1 : 1);
      var $card = $(el);
      $card.attr('data-position', nextPosition);
      this.positions[i] = nextPosition;

      var isFront = false;
      if (this.isReverseStacked) {
        if (nextPosition === 0) {
          isFront = true;
        }
      } else {
        if (nextPosition === this.totalCards - 1) {
          isFront = true;
        }
      }

      $card.toggleClass(CardShuffle.ClassName.FRONT, isFront);
    }.bind(this));
  };


  CardShuffle.prototype._handleTap = function(evt) {
    var target = evt.target;
    var hasCardParent = $(target).closest('.' + CardShuffle.ClassName.INNER).length > 0;

    if (hasCardParent && !this.isTransitioning) {
      this.advance();
    }
  };


  CardShuffle.prototype._handleDraggableStart = function(evt) {
    if (this.isTransitioning) {
      evt.preventDefault();
      return;
    }

    var draggable = evt.draggable;
    var dragger = draggable.getElement();
    $(dragger).addClass([
      CardShuffle.ClassName.NO_TRANSITION,
      Helpers.ClassName.GRABBING
    ].join(' '));
  };


  CardShuffle.prototype._handleDraggableEnd = function(evt) {
    var velocityThreshold = 0.7;
    var velocity = evt.velocity;
    var direction = evt.direction;
    var draggable = evt.draggable;
    var position = draggable.getPosition(true);
    var dragger = evt.target;

    if (
        // Velocity throw
        (velocity.x > velocityThreshold && direction === Draggable.Direction.LEFT) ||
        // Dragged a quarter of the way
        (direction === Draggable.Direction.LEFT && position.x <= -25)) {
      dragger.style[Device.Js.TRANSFORM] = '';
      this.advance(CardShuffle.Direction.START);
    } else if (
        // Velocity throw
        (velocity.x > velocityThreshold && direction === Draggable.Direction.RIGHT) ||
        // Dragged a quarter of the way
        (direction === Draggable.Direction.RIGHT && position.x >= 25)) {
      dragger.style[Device.Js.TRANSFORM] = '';
      this.advance(CardShuffle.Direction.END);

    // As long as it has moved more than 1 pixel, it can be transitioned back to
    // the resting state. If it's zero, the transition end event will never
    // be triggered and the draggable will continue to be disabled.
    } else if (evt.isDirectionOnAxis) {
      this._slideBack(draggable);
    }

    $(draggable.getElement()).removeClass(Helpers.ClassName.GRABBING);
  };

  return CardShuffle;
});


requirejs.config({
  baseUrl: 'js',
  paths: {
    jquery: '//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery'
  }
});


define('modernizr', [], window.Modernizr);


require(['card-shuffle'], function(CardShuffle) {
  window.cards = new CardShuffle($('.card-shuffle--stacked')[0], true, true);
});

define("app/main", function(){});

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/require.js/2.1.11/require.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js