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