Dev

Velo Slider

A slider interaction thing
enhanced by Velocity.js

A recent project I worked on at Urban Influence required a unique home page experience to highlight site content. Wanting to stay away from another tired-ass slider pattern, the goal was to create something new(ish) and (relatively) unexpected.

The animation I ultimately landed on involves a series of full viewport sections that scale out, move up (or down), and then scale back in. Navigation controls allow for click, key or (gasp) scroll jack. These sections, or yeah okay, ‘slides’, can accommodate either a background image or video. For some extra polish, a text slice effect transitions the type, staggered slightly with a touch of transition-delay.

In The Wild

The project was for Capital Pacific, a real estate company headquartered in the Pacific Northwest. The project is now live, so feel free to go check the actual interaction:

Capital Pacific

Here’s a demo of the final effect

See the Pen Velo Slider by Stephen Scaff (@StephenScaff) on CodePen.

Type Animations

The text slice effect is accomplished by hiding the overflow of a type element and using an additional span wrapper to push the element outside the visibility of its overflow. An active class is added when current slide is visible, allowing us to shift the type back into view with some transition. A custom cubic-bezier easing drops a bit natural ‘life’ into the transition.

The markup structre:


<!-- Text slice setup -->
<h1 class="oh"><span>What Up Playas?</span></h1>

And our CSS:



// Hide overflow
.oh{
  display: block;
  overflow-y: hidden;
  padding: 0.025em 0;
}
  
// Push el outside of overflow
.oh span{
  display: inline-block;
  transform:translate3d(0,140%,0);
  opacity: 0;
  transition: transform 0.4s ease, opacity 0.8s ease;
}

// Transition el via active class
.is-active .oh span{
  transform:translate3d(0,0%,0);
  opacity: 1;
  transition: transform 0.6s $ease-cb-3, opacity 0.1s ease;
}

// A touch of staggering
.is-active .oh:nth-of-type(2n) span{
  transition-delay:0.2s;
}

Simple idea that really adds that extra mic droppin hotness.

Progression Animations

While the smaller type animations are handled by CSS transitions, progression and scaling is obviously handled with JavaScript. After hitting some performance issues from attempting to scale such weighty elements, I introduced Velocity.JS to keep stuff all silky smooth like.

Velocity

Velocity is an animation engine with the same API as jQuery’s $.animate(), but unlike Jquery-based animation, it fast. Additionally, Velocity features a variety of useful utilities for color animation, transforms, loops, easings, SVG support, and scrolling.

The Run Down

In addition to Velocity, I also used the handy Velocity UI pack to register custom effects that I could then reference in my animation sequences.


/**
 * Velocity Effects
 */
var scaleDownAmnt = 0.7;
var boxShadowAmnt = '40px';

$.Velocity.RegisterEffect("scaleDown.moveUp", {
  defaultDuration: 1,
  calls: [
    [{
      translateY: '0%',
      scale: scaleDownAmnt,
 
    }, 0.20],
    [{
      translateY: '-100%'
    }, 0.60],
    [{
      translateY: '-100%',
      scale: '1',
      // boxShadowBlur: '0'
    }, 0.20]
  ]
});
$.Velocity.RegisterEffect("scaleDown.moveUp.scroll", {
  defaultDuration: 1,
  calls: [
    [{
      translateY: '-100%',
      scale: scaleDownAmnt,
 
    }, 0.60],
    [{
      translateY: '-100%',
      scale: '1',
      // boxShadowBlur: '0'
    }, 0.40]
  ]
});

// etc...

With the effects in place, I could now call them when setting up my animations.

Here the final code written in an object literal module pattern. Note how the setAnimation Method references our Velocity Effects, and how the nextSlide and prevSlide methods use that setup to trigger the animations.


/**
 * Velo Slider
 * A Custom Slider using Velocity and Velocity UI effects
 */
var VeloSlider = (function() {

  /**
   * Global Settings 
   */
  var settings = {
    veloInit: $('.velo-slides').data('velo-slider'),
    $veloSlide: $('.velo-slide'),
    veloSlideBg: '.velo-slide__bg',
    navPrev:  $('.velo-slides-nav').find('a.js-velo-slides-prev'),
    navNext:  $('.velo-slides-nav').find('a.js-velo-slides-next'),
    veloBtn:   $('.velo-slide__btn'),
    delta: 0,
    scrollThreshold: 7,
    currentSlide: 1,
    animating: false,
    animationDuration: 2000
  };

  // Flags
  var delta = 0,
      animating = false;

  return {
   
     /**
      * Init 
      */
     init: function() {
       this.bind();
     },
    
    /**
     * Bind our click, scroll, key events
     */
    bind: function(){
 
      //  Add active to first slide to set it off
      settings.$veloSlide.first().addClass('is-active');

      //  Init with a data attribute, 
      //  Binding the animation to scroll, arrows, keys
      if (settings.veloInit == 'on') {
        VeloSlider.initScrollJack();
        $(window).on('DOMMouseScroll mousewheel', VeloSlider.scrollJacking);
      }

      // Arrow / Click Nav
      settings.navPrev.on('click', VeloSlider.prevSlide);
      settings.navNext.on('click', VeloSlider.nextSlide);
    
      // Key Nav
      $(document).on('keydown', function(e) {
        var keyNext = (e.which == 39 || e.which == 40),
            keyPrev = (e.which == 37 || e.which == 38);

        if (keyNext && !settings.navNext.hasClass('inactive')) {
          e.preventDefault();
          VeloSlider.nextSlide();

        } else if (keyPrev && (!settings.navPrev.hasClass('inactive'))) {
          e.preventDefault();
          VeloSlider.prevSlide();
        }
      });
    
      //set navigation arrows visibility
      VeloSlider.checkNavigation();
    },

    /** 
     * Set Animation
     * Defines the animation sequence, calling our registered velocity effects
     * @see js/components/_velocity-effects.js
     */
    setAnimation: function(midStep, direction) {
      
      // Vars for our velocity effects
      var animationVisible = 'translateNone',
          animationTop = 'translateUp',
          animationBottom = 'translateDown',
          easing = 'ease',
          animDuration = settings.animationDuration;

      // Middle Step
      if (midStep) {
        animationVisible = 'scaleUp.moveUp.scroll';
        animationTop = 'scaleDown.moveUp.scroll';
        animationBottom = 'scaleDown.moveDown.scroll';
      
      } else {
        animationVisible = (direction == 'next') ? 'scaleUp.moveUp' : 'scaleUp.moveDown';
        animationTop = 'scaleDown.moveUp';
        animationBottom = 'scaleDown.moveDown';
      }

      return [animationVisible, animationTop, animationBottom, animDuration, easing];
    },

    /** 
     * Init Scroll Jaclk
     */
    initScrollJack: function() {

      var visibleSlide = settings.$veloSlide.filter('.is-active'),
          topSection = visibleSlide.prevAll(settings.$veloSlide),
          bottomSection = visibleSlide.nextAll(settings.$veloSlide),
          animationParams = VeloSlider.setAnimation(false),
          animationVisible = animationParams[0],
          animationTop = animationParams[1],
          animationBottom = animationParams[2];

      visibleSlide.children('div').velocity(animationVisible, 1, function() {
        visibleSlide.css('opacity', 1);
        topSection.css('opacity', 1);
        bottomSection.css('opacity', 1);
      });

      topSection.children('div').velocity(animationTop, 0);
      bottomSection.children('div').velocity(animationBottom, 0);
    },

    /**
     * Scroll Jack
     * On Mouse Scroll
     */
    scrollJacking: function(e) {
      if (e.originalEvent.detail < 0 || e.originalEvent.wheelDelta > 0) {
        delta--;
        (Math.abs(delta) >= settings.scrollThreshold) && VeloSlider.prevSlide();
      } else {
        delta++;
        (delta >= settings.scrollThreshold) && VeloSlider.nextSlide();
      }
      return false;
    },

    /**
     * Previous Slide
     */
    prevSlide: function(e) {

      typeof e !== 'undefined' && e.preventDefault();
      
      var visibleSlide = settings.$veloSlide.filter('.is-active'),
          animationParams = VeloSlider.setAnimation(midStep, 'prev'),
          midStep = false;
      
      visibleSlide = midStep ? visibleSlide.next(settings.$veloSlide) : visibleSlide;

      if (!animating && !visibleSlide.is(":first-child")) {
        animating = true;
        
        visibleSlide
          .removeClass('is-active')
          .children(settings.veloSlideBg)
          .velocity(animationParams[2], animationParams[3], animationParams[4])
          .end()
          .prev(settings.$veloSlide)
          .addClass('is-active')
          .children(settings.veloSlideBg)
          .velocity(animationParams[0], animationParams[3], animationParams[4], function() {
            animating = false;
          });
        currentSlide = settings.currentSlide - 1;
      }
      VeloSlider.resetScroll();
    },


    /** 
     * Next Slide
     */
    nextSlide: function(e) {
      
      //go to next section
      typeof e !== 'undefined' && e.preventDefault();
      
      var visibleSlide = settings.$veloSlide.filter('.is-active'),
          animationParams = VeloSlider.setAnimation(midStep, 'next'),
          midStep = false;

      if (!animating && !visibleSlide.is(":last-of-type")) {
        animating = true;

        visibleSlide.removeClass('is-active')
          .children(settings.veloSlideBg)
          .velocity(animationParams[1], animationParams[3])
          .end()
          .next(settings.$veloSlide)
          .addClass('is-active')
          .children(settings.veloSlideBg)
          .velocity(animationParams[0], animationParams[3], function() {
            animating = false;
        });
        currentSlide = settings.currentSlide + 1;
      }
      VeloSlider.resetScroll();
    },

    /**
     * Reset Scroll
     */
    resetScroll: function() {
      delta = 0;
      VeloSlider.checkNavigation();
    },

    /**
     * Check Nav
     * Adds / hides nav based on first/last slide
     * @todo - loop slides, without cloning if possible
     */
    checkNavigation: function() {
      //update navigation arrows visibility
      (settings.$veloSlide.filter('.is-active').is(':first-of-type')) ? settings.navPrev.addClass('inactive'): settings.navPrev.removeClass('inactive');
      (settings.$veloSlide.filter('.is-active').is(':last-of-type')) ? settings.navNext.addClass('inactive'): settings.navNext.removeClass('inactive');

    },
  };
})();

// INIT
VeloSlider.init();

Snag It Up

In addition to the CodePen demo, Velo Slider is on Github. So fork off.

Read Next

Flexy Layouts

Read Story