Friday, September 21, 2012

Fullscreen slit slider with JQuery and CSS3

Fullscreen slit slider

A tutorial on how to create a fullscreen slideshow with a twist: the idea is to slice open the current slide when navigating to the next or previous one. Using jQuery and CSS animations we can create unique slide transitions.

In this tutorial we'll create a fullscreen slideshow with a twist: we'll cut the current slide open in order to reveal the next or previous slide. Using different data-attributes, we'll define the type, rotation angle and scale of a slide's parts, giving us the possibility to create unique effects of each slide transition.

We'll be using the following jQuery plugins:

  • jQuery cond by Ben Alman, for chainable "if-then-else" statements
  • jQuery Transit by Rico Sta. Cruz, for easy and smooth CSS3 transformations and transitions

The animal icon font that we'll be using is by Alan Carr and you can find it here.

The MarkUp

Our initial markup will consist of a main container with the class and id sl-slider which will hold all the slides, each one having the class sl-slide. Additional classes for the slide's color will also be added:

<section id="sl-slider" class="sl-slider">
             
    <div class="sl-slide">
        <div class="sl-deco" data-icon="6"></div>
        <h2>A bene placito</h2>
        <blockquote>
            <p>You have just dined, and however scrupulously 
            the slaughterhouse is concealed in the graceful 
            distance of miles, there is complicity.
            </p>
            <cite>Ralph Waldo Emerson</cite>
        </blockquote>
    </div>
     
    <div class="sl-slide sl-slide-dark">
        <!-- ... -->
    </div>
     
    <!-- ... -->
     
</section>

Every slide will also have some data-attributes that we will use in order to control the effect for each slide. The data attributes that we want are the following:

data-orientation
data-cut1-rotation
data-cut2-rotation
data-cut1-scale 
data-cut2-scale

The first one, data-orientation should be either vertical or horizontal. This we need in order to know where to "cut" the slide. It will be either cut horizontally or vertically. The data-cut1-rotation and data-cut2-rotation value will be the rotation degree for each one of the cuts and the data-cut1-scale and data-cut2-scale value will be the scale value.

So, our first slide will have the following values:

<div class="sl-slide" data-orientation="horizontal" data-cut1-rotation="-25" data-cut2-rotation="-25" data-cut1-scale="2" data-cut2-scale="2">

Our structure is a "base structure". We will build upon that structure using JavaScript in order to be able to create the effects. So, we will want to transform it into this (each slide will also have the data attributes):

<section id="sl-slider" class="sl-slider">
             
    <div class="sl-slides-wrapper">
     
        <div class="sl-slide sl-slide-horizontal">
            <div class="sl-content-wrapper">
                <div class="sl-content">
                    <!-- the content -->
                </div>
            </div>
        </div>
         
        <!-- ... -->
         
    </div>
         
    <nav>
        <span class="sl-prev">Previous</span>
        <span class="sl-next">Next</span>
    </nav>
         
</section>

We will add a navigation and and some wrappers for the content.

In the moment that we navigate to the next or previous slide we will take the current slide and duplicate its content wrapper, creating the "cuts":

<div class="sl-slide sl-slide-horizontal" >

    <div class="sl-content-cut">
        <div class="sl-content-wrapper">
            <div class="sl-content">
                <!-- ... -->
            </div>
        </div>
    </div>
     
    <div class="sl-content-cut">
        <div class="sl-content-wrapper">
            <div class="sl-content">
                <!-- ... -->
            </div>
        </div>
    </div>
     
</div>

The CSS

Note, that we will omit any vendor prefixes here.

We want the whole thing to be fullscreen, so let's make the slider's position absolute:

.sl-slider {
    position: absolute;
    top: 0;
    left: 0;
    font-family: 'Montserrat', Arial, sans-serif;
}

The width and height will be set dynamically in the Javascript.
For the navigation arrows we will use this image-less technique. We’ll simply have a little box and rotate it 45 degrees. Then we’ll add some dashed border to the sides and voilĂ , we made ourselves some neat arrows:

.sl-slider nav span {
    position: fixed;
    z-index: 2000;
    top: 50%;
    width: 80px;
    height: 80px;
    border: 2px dashed #ddd;
    border: 2px dashed rgba(150,150,150,0.4);
    text-indent: -90000px;
    margin-top: -40px;
    cursor: pointer;
    transform: rotate(45deg);
    transition: all 0.3s ease-in-out;
}
.sl-slider nav span.sl-prev {
    left: 60px;
    border-right: none;
    border-top: none;
}

.sl-slider nav span.sl-next {
    right: 60px;
    border-left: none;
    border-bottom: none;
}

We've also added a transition in order to change the opacity value of the rgba smoothly on hover:

.sl-slider nav span:hover { border-color: rgba(150,150,150,0.9); }

The slides and the new wrapper will be of absolute position and occupy 100% width and height:

.sl-slide, .sl-slides-wrapper {
    position: absolute;
    width: 100%; height: 100%;
    top: 0; left: 0;
    overflow: hidden;
}

Each slide should have a z-index of 1; we’ll control the appearance and the stacking of the slides with JavaScript:

.sl-slide { z-index: 1; }

The content "cuts" will be positioned absolutely and their common style is the following:

/* The duplicate parts/cuts */

.sl-content-cut {
    overflow: hidden;
    position: absolute;
    box-sizing: content-box;
    background: #fff;
}

We use box-sizing: content-box here because by default (in our normlize.css) we use border-box.

The content "cuts" will be horizontal or vertical, meaning that either the height or the width will be half of the screensize. In order to avoid seeing the edges of a cut when we rotate it, we'll add some padding.

/* Horizontal cut */

.sl-slide-horizontal .sl-content-cut {
    width: 100%; height: 50%;
    left: -200px;
}

.sl-slide-horizontal .sl-content-cut:first-child {
    top: -200px;
    padding: 200px 200px 0px 200px;
}

.sl-slide-horizontal .sl-content-cut:nth-child(2) {
    top: 50%;
    padding: 0px 200px 200px 200px;
}

/* Vertical cut */

.sl-slide-vertical .sl-content-cut {
    width: 50%; height: 100%;
    top: -200px;
}

.sl-slide-vertical .sl-content-cut:first-child {
    left: -200px;
    padding: 200px 0px 200px 200px;
}

.sl-slide-vertical .sl-content-cut:nth-child(2) {
    left: 50%;
    padding: 200px 200px 200px 0px;
}

We use negative position values in order to "pull" the divisions into place.
Let’s style the content wrapper and the content division:

/* Content wrapper */
/* Width and height is set dynamically */
.sl-content-wrapper { position: absolute; }

.sl-content {
    width: 100%; height: 100%;
    background: #fff;
}

The division with the class sl-content-wrapper will get a height and width dynamically. If, for example, the slide is horizontal, the wrapper will have a width of 100% of the screen width and 50% of the screen height. The wrapper of the second cut will also have a negative top (horizontal) or left (vertical) margin in order to "pull" the duplicated content up or to the left.

The elements that we'll use in the content will be a decorative element (the animal with the circles), a headline and a blockquote. We'll use a font to give us some cute animal "icons" that we'll place as a pseudo element of the decorative div.

The division with the class sl-deco, just like all the other content elements, will have an absolute position. We’ll center it horizontally and give it a bottom value of 50%:

/* Content elements */

.sl-deco{
    width: 260px; height: 260px;
    border: 2px dashed rgba(150,150,150,0.4);
    border-radius: 50%;
    position: absolute;
    bottom: 50%; left: 50%;
    margin-left: -130px;
}

We use a data attribute data-icon in the decorative element and we’ll style the pseudo-element :after to contain the letter from the animal icon font as its content:

[data-icon]:after {
    content: attr(data-icon);
    font-family: 'AnimalsNormal';
    font-size: 100px;
    color: #000;
    text-shadow: 0 0 1px #000; text-align: center;
    position: absolute;
    width: 220px; height: 220px;
    line-height: 220px;
    top: 50%; left: 50%;
    margin: -110px 0 0 -110px;
    box-shadow: inset 0 0 0 10px #f7f7f7;
    border-radius: 50%;
}

The box shadow will create a "fake" inset border.

The headline will also be positioned absolutely and we'll give it the same bottom value like we gave to the decorative element, which is 50%. We then add a negative bottom margin in order to place it under the other element. Like that we can use the decorative element as a reference point and position the other elements relatively to it using a negative bottom margin:

.sl-slide h2 {
    color: #000;
    text-shadow: 0 0 1px #000; text-align: center;
    position: absolute;
    font-size: 34px; font-weight: 300;
    letter-spacing: 13px;
    text-transform: uppercase;
    width: 80%;
    line-height: 50px;
    bottom: 50%; left: 10%;
    margin: 0 0 -120px 0; padding: 20px;
}

The blockquote will have a width of 30% and since we want to center it, we’ll give it a left value of 35% (because we have 70% left and we need to take half of that) and the respective text-align value:

.sl-slide blockquote {
    position: absolute;
    width: 30%; height: 70px;
    text-align: center;
    left: 35%; bottom: 50%;
    font-size: 13px;
    line-height: 20px;
    color: #8b8b8b;
    z-index: 2;
    margin: 0 0 -200px 0; padding: 0;
}

Let's add a quotation mark to the blockquote. Using the pseudo-class :before, we'll add a over-sized quotation mark behind the blockquote:

.sl-slide blockquote:before {
    color: rgba(244,244,244,0.65);
    font-family: "Bookman Old Style", Bookman, Garamond, serif;
    font-size: 200px;
    position: absolute;
    line-height: 60px;
    width: 75px; height: 75px;
    z-index: -1;
    left: -15px; top: 35px;
    content: '\201C';
}

And the cite will have a different look:

.sl-slide blockquote cite {
    font-size: 10px;font-style: normal;
    text-transform: uppercase;
    letter-spacing: 4px;
}

Next, we'll define some classes for controling the colors of the slides. When we give this color class to the slide, we want the background color and the color of the elements to be different. By default, our slides are white and the content elements are black and grey.
The dark or black slides will have an inverted color scheme:

/* Dark slides */
.sl-slide-dark .sl-content-cut, .sl-slide-dark .sl-content { background: #000; }
.sl-slide-dark [data-icon]:after,.sl-slide-dark.sl-slide h2 { color: #fff; }
.sl-slide-dark.sl-slide blockquote:before { color: #222; }

The other colors are simply fitting values for the color schemes:

/* Color 1 slides */
.sl-slide-color-1 .sl-content-cut, .sl-slide-color-1 .sl-content { background: #8d0f39; }

.sl-slide-color-1 [data-icon]:after {
    color: #e6a6bb;
    text-shadow: 0 0 1px #e6a6bb;
    box-shadow: inset 0 0 0 10px #e6a6bb;
}

.sl-slide-color-1.sl-slide h2, .sl-slide-color-1.sl-slide blockquote{ color: #fff; }
.sl-slide-color-1.sl-slide blockquote:before { color: #7b0c31; }

/* Color 2 slides */
.sl-slide-color-2 .sl-content-cut, .sl-slide-color-2 .sl-content { background: #ade1f4; }

.sl-slide-color-2 [data-icon]:after {
    text-shadow: 0 0 1px #8bc7dd;
    color: #8bc7dd;
}

.sl-slide-color-2.sl-slide h2, .sl-slide-color-2.sl-slide blockquote{
    color: #fff;
    text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
}

.sl-slide-color-2.sl-slide blockquote:before { color: #8bc7dd; }

/* Color 3 slides */
.sl-slide-color-3 .sl-content-cut, .sl-slide-color-3 .sl-content { background: #ffeb41; }

.sl-slide-color-3.sl-slide h2, .sl-slide-color-3.sl-slide blockquote{
    color: #000;
    text-shadow: 1px 1px 1px rgba(0,0,0,0.1);
}

.sl-slide-color-3.sl-slide blockquote:before { color: #ecd82c; }

And now, let’s add some motion to the content elements! When we navigate the slides, we want the content elements to do something fun, so we will add a class to the next slide whenever we navigate to the "right" (or "in" since this almost looks like as if we are moving further). That class will "trigger" an animation for each one of the content elements:

/* Animations for elements */

.sl-trans-elems .sl-deco{ animation: roll 1s ease-out both; }
.sl-trans-elems h2{ animation: moveUp 1s ease-in-out both; }
.sl-trans-elems blockquote{ animation: fadeIn 0.5s linear 0.5s both; }

@keyframes roll{
    0% {transform: translateX(500px) rotate(360deg); opacity: 0;}
    100% {transform: translateX(0px) rotate(0deg); opacity: 1;}
}

@keyframes moveUp{
    0% {transform: translateY(40px);}
    100% {transform: translateY(0px);}
}

@keyframes fadeIn{
    0% {opacity: 0;}
    100% {opacity: 1;}
}

The decorative element will "roll in" from the right side, the heading will move up and the blockquote will simply fade in.
Now, when we navigate back (or "out"), we want to see the reverse happening:

.sl-trans-back-elems .sl-deco{ animation: scaleDown 1s ease-in-out both; }
.sl-trans-back-elems h2{ animation: fadeOut 1s ease-in-out both; }
.sl-trans-back-elems blockquote{ animation: fadeOut 1s linear both; }
@keyframes scaleDown{
    0% {transform: scale(1);}
    100% {transform: scale(0.5);}
}
@keyframes fadeOut{
    0% {opacity: 1;}
    100% {opacity: 0;}
}

Here we will scale down the decorative element and simply fade out the rest.
And that’s all the style! Let’s take a look at the JavaScript.

The Javascript

Let's first take a look at our plugin options:

$.Slitslider.defaults   = {
    speed       : 1000,
    autoplay    : false,
    interval    : 4000,
    optOpacity  : false,
    translateF  : 160,
    maxAngle    : 25,
    maxScale    : 2
};

We can set the speed of the transitions, set the slideshow to play automatically with a specific interval and also make the slide's cuts opacity change during the transition.

The translateF option is the amount in percentage for translating both cuts. You can adjust this value as necessary as you change the slide’s scale and angle data attributes and the maxAngle and maxScale values.

We will start by executing the _init function.

_init               : function( options ) {
             
    // the options
    this.options    = $.extend( true, {}, $.Slitslider.defaults, options );
    // the slider
    this.$slides    = this.$slider.children( '.sl-slide' ).hide();
    // total number of slides
    this.slidesCount= this.$slides.length;
    // the current slide
    this.current    = 0;
    // currently animating?
    this.isAnimating= false;
    // get the window size
    this._getWinSize();
    // build the layout
    this._layout();
    // load some events
    this._loadEvents();
    // start the slideshow
    if( this.options.autoplay ) {
     
        this._startSlideshow();
     
    }
     
}

Let's take a look at the _layout function:

_layout             : function() {
             
    this.$slideWrapper = $( '<div class="sl-slides-wrapper" />' );
    // wrap the slides into "sl-slides-wrapper"
    this.$slides.wrapAll( this.$slideWrapper ).each( function( i ) {
         
        var $slide          = $( this ),
            // vertical || horizontal
            orientation     = $slide.data( 'orientation' );
             
        $slide.addClass( 'sl-slide-' + orientation )
              .children()
              .wrapAll( '<div class="sl-content-wrapper" />' )
              .wrapAll( '<div class="sl-content" />' );
     
    } );
     
    // set the right size of the slider and slides according to the current window size
    this._setSize();
    // show first slide
    this.$slides.eq( this.current ).show();
    // add navigation
    if( this.slidesCount > 1 ) {
         
        this.$slider.append(
            '<nav><span class="sl-prev">Previous</span><span class="sl-next">Next</span></nav>'
        );
         
    }
     
}

We are wrapping the slides into a division with the class sl-slides-wrapper. As we’ve mentioned before, each slide’s content will also be wrapped by two divisions, one with the class sl-content and one with the class sl-content-wrapper.

We also add the respective orientation class to the slide (sl-slide-vertical or sl-slide-horizontal).

The slider and its sl-content-wrapper division need to have the window’s width and height. That’s what we do in the _setSize function.

Finally, we'll show the current/first slide and add the navigation buttons.
In the _loadEvents function we will bind the click events for both the navigation buttons and the resize (smartresize) event to the window:

_loadEvents         : function() {
             
    var _self = this;
     
    if( this.slidesCount > 1 ) {
         
        // navigate "in" or "out"
        this.$slider.find( 'nav > span.sl-prev' ).on( 'click.slitslider', function( event ) {
             
            if( _self.options.autoplay ) {
             
                clearTimeout( _self.slideshow );
                _self.options.autoplay  = false;
             
            }
            _self._navigate( 'out' );
         
        } ).end().find( 'nav > span.sl-next' ).on( 'click.slitslider', function( event ) {
             
            if( _self.options.autoplay ) {
             
                clearTimeout( _self.slideshow );
                _self.options.autoplay  = false;
             
            }
            _self._navigate( 'in' );
         
        } );
     
    }
     
    $( window ).on( 'smartresize.slitslider', function( event ) {
         
        // update window size
        _self._getWinSize();
        _self._setSize();
         
    } );

}

Let's see how we "slice" the slides and move to the next one:

_navigate           : function( dir ) {
             
    // return if currently navigating / animating
    if( this.isAnimating ) {
     
        return false;
     
    }
     
    var _self = this;
     
    // while isAnimating is true we cant navigate..
    this.isAnimating = true;
     
    // the current slide
    var $currentSlide   = this.$slides.eq( this.current ), css;
     
    // set new current
    ( dir === 'in' ) ?
            ( ( this.current < this.slidesCount - 1 ) ? ++this.current : this.current = 0 ) :
            ( ( this.current > 0 ) ? --this.current : this.current = this.slidesCount - 1 )
         
     
        // next slide to be shown
    var $nextSlide      = this.$slides.eq( this.current ).show(),
        // the slide we want to cut and animate
        $movingSlide    = ( dir === 'in' ) ? $currentSlide : $nextSlide,
        // the following are the data attrs set for each slide
        orientation     = $movingSlide.data( 'orientation' ) || 'horizontal',
        cut1angle       = $movingSlide.data( 'cut1Rotation' ) || 0,
        cut1scale       = $movingSlide.data( 'cut1Scale' ) || 1,
        cut2angle       = $movingSlide.data( 'cut2Rotation' ) || 0,
        cut2scale       = $movingSlide.data( 'cut2Scale' ) || 1;
     
    this._validateValues( cut1angle, cut2angle, cut1scale, cut2scale, orientation );
     
    if( orientation === 'vertical' ) {
     
        css = { marginLeft : -this.windowProp.width / 2 };
     
    }
    else if( orientation === 'horizontal' ) {
     
        css = { marginTop : -this.windowProp.height / 2 };
     
    }
    // default slides cuts style
    var resetStyle  = ( orientation === 'horizontal' ) ? { x : '0%', y : '0%', rotate : 0, scale : 1, opacity : 1 } : { x : '0%', y : '0%', rotate : 0, scale : 1, opacity : 1 },
         
        // cut1 style
        cut1Style   = ( orientation === 'horizontal' ) ? { y : '-' + this.options.translateF + '%', rotate : cut1angle, scale : cut1scale } : { x : '-' + this.options.translateF + '%', rotate : cut1angle, scale : cut1scale },
         
        // cut2 style
        cut2Style   = ( orientation === 'horizontal' ) ? { y : this.options.translateF + '%', rotate : cut2angle, scale : cut2scale } : { x : this.options.translateF + '%', rotate : cut2angle, scale : cut2scale };
     
    if( this.options.optOpacity ) {
     
        cut1Style.opacity = 0;
        cut2Style.opacity = 0;
     
    }
     
    // we are adding the classes sl-trans-elems and sl-trans-back-elems 
    // to the slide that is either coming "in"
    // or going "out" according to the direction
     
    // the idea is to make it more interesting by 
    // giving some animations to the respective slides elements
     
    ( dir === 'in' ) ? $nextSlide.addClass( 'sl-trans-elems' ) : $currentSlide.addClass( 'sl-trans-back-elems' );
     
    $currentSlide.removeClass( 'sl-trans-elems' );
     
    // add the 2 cuts and animate them 
    // (we are using the jquery.transit plugin: 
    // http://ricostacruz.com/jquery.transit/ to 
    // add transitions to the elements)
     
    $movingSlide.css( 'z-index', this.slidesCount )
                .find( 'div.sl-content-wrapper' )
                .wrap( '<div class="sl-content-cut" />' )
                .parent()
                .cond(
                    dir === 'out',
                    function() {
                     
                        this.css( cut1Style )
                            .transition( resetStyle, _self.options.speed, dir );
                                  
                    }, 
                    function() {
                         
                        this.transition( cut1Style, _self.options.speed, dir )
                 
                    }
                )
                .clone()
                .appendTo( $movingSlide )
                .cond(
                    dir === 'out', 
                    function() {
                         
                        var cut = this;
                        cut.css( cut2Style )
                            .transition( resetStyle, _self.options.speed, dir , function() {
          
                                _self._onEndNavigate( cut, $currentSlide, dir );
                          
                            } )
                 
                    },
                    function() {
                         
                        var cut = this;
                        cut.transition( cut2Style, _self.options.speed, dir, function() {
                             
                            _self._onEndNavigate( cut, $currentSlide, dir );
                          
                        } )
                         
                    }
                )
                .find( 'div.sl-content-wrapper' )
                .css( css );
     
}

So, the trick is to duplicate the slide’s content into the divisions with the class sl-content-cut and to set the second one’s margin-left or margin-top to half the window’s width or height. That will make everything look "normal" and we won’t see any separation.

Then, according to the values defined in the element’s data attributes, we’ll animate the slide’s cuts using the jQuery Transit plugin.

According to the direction, we will either slice the current slide and show the next one, or we will slice the previous one (not shown), and put together its slices on top of the current one.

We are adding the classes sl-trans-elems and sl-trans-back-elems to the slide that is next (when we navigate "in") or the current one (when we navigate "out"). Like we have seen before in the CSS part, adding those classes will make the content elements of the respective slide animate in a specific way.

Once the transition ends, we call the _onEndNavigate function where we will unwrap the content of the current slide, thus removing the two sl-content-cut divisions:

_onEndNavigate      : function( $slice, $oldSlide, dir ) {
             
    // reset previous slide's style after next slide is shown
    var $slide          = $slice.parent(),
        removeClasses   = 'sl-trans-elems sl-trans-back-elems';
     
    // remove second slide's cut
    $slice.remove();
    // unwrap..
    $slide.css( 'z-index', 1 )
          .find( 'div.sl-content-wrapper' )
          .unwrap();
     
    // hide previous current slide
    $oldSlide.hide().removeClass( removeClasses );
    $slide.removeClass( removeClasses );
    // now we can navigate again..
    this.isAnimating = false;
     
}

View Demo


via Codrops - by