Галерея с плавными диагональными переходами на CSS3

В сегодняшнем руководстве мы хотим рассказать вам о процессе создания галереи с плавными диагональными переходами, основанными на CSS3. Она будет сканировать директорию с изображениями на сервере, а затем отображать их в виде сетки, которая растягивается на все окно браузера. Добавлять новые фотографии будет невероятно просто – вам нужно будет просто скопировать в папку два изображения, — оригинал изображения и миниатюру в размере 150х150 пикселей.

Браузеры, поддерживающие CSS3, будут отображать плавный эффект диагонального перехода в то время, как старые версии браузеров будут отображать более простой вариант без анимации.

HTML-код

Как обычно, первое, что нам нужно сделать, это проработать наш HTML-код для документа:

Index.html

<!DOCTYPE html>
<html>

    <head>
        <meta charset="utf-8"/>
        <title>Smooth Diagonal Fade Gallery with CSS3 Transitions</title>

        <!-- The Swipebox plugin -->
        <link href="assets/swipebox/swipebox.css" rel="stylesheet" />

        <!-- The main CSS file -->
        <link href="assets/css/style.css" rel="stylesheet" />

        <!--[if lt IE 9]>
            <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
        <![endif]-->
    </head>

    <body>

        <div id="loading"></div>

        <div id="gallery"></div>

        <!-- javascript Includes -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
        <script src="assets/swipebox/jquery.swipebox.min.js"></script>
        <script src="assets/js/jquery.loadImage.js"></script>
        <script src="assets/js/script.js"></script>

    </body>
</html>

 
Галерея работает при помощи библиотеки jQuery, которую мы включаем перед закрывающим тегом body. Мы также воспользовались великолепным небольшим плагином лайтбокса под названием Swipebox, но вы можете без труда заменить его другим плагином по вашему усмотрению. Два основных div-элемента: #loading и #gallery. Первый содержит gif-анимацию с индикатором загрузки в то время, как второй отображает фотографии из галереи. Div-элемент #gallery обозначен свойством position:fixed, и поэтому занимает все окно браузера. Верстка для самих фотографий довольно проста:

<a href="assets/photos/large/34.jpg" class="swipebox static"
    style="width:148px;height:129px;background-image:url(assets/photos/thumbs/34.jpg)">
</a>

 
Фотографии в галереи представлены в размере 150х150 пикселей, и это означает, что они никогда не будут подходить по размеру, если мы не будем масштабировать миниатюры вручную. Именно это и произошло с фотографией сверху, и поэтому в атрибуте style указываются значения высоты и ширины. В разделе с JS-кодом вы сможете увидеть, как мы будем эти значения высчитывать.

1381835035_img-02

Сканирование фотографий при помощи PHP

Фотографии содержатся в двух папках на сервере — assets/photos/thumbs/ для миниатюр, и assets/photos/large/ для оригинальных изображений. При помощи PHP мы сканируем папки и выдаем JSON с названиями файлов. С другой стороны, вы можете извлечь изображения из базы данных, но вам придется сохранять ту же структуру. Взгляните на скрипт:

Load.php

// Scan all the photos in the folder
$files = glob('assets/photos/large/*.jpg');

$data = array();
foreach($files as $f){
    $data[] = array(
        'thumb' => str_replace('large', 'thumbs', $f),
        'large' => $f
    );
}

// Duplicate the photos a few times, so that we have what to paginate in the demo.
// You most certainly wouldn't want to do this with your real photos.
// $data = array_merge($data, $data);
// $data = array_merge($data, $data);
// $data = array_merge($data, $data);

header('Content-type: application/json');

echo json_encode(array(
    'data' => $data,
));

 
Добавлять новые изображения в галерею довольно просто – вам просто нужно скопировать изображение и миниатюру (у них должны быть одинаковые названия). Мы скопировали изображения несколько раз, чтобы наша галерея немного заполнилась, но вам вероятно следует проделать эту операцию с разными изображениями.

Теперь, когда мы разобрались с JSON, давайте приступим к написанию javascript-кода!

javascript-код

Вот что нам нужно сделать:

* Сначала мы запустим GET-запрос AJAX, чтобы извлечь все фотографии на диске из PHP-скрипта.
* Затем мы подсчитаем, сколько фотографий нужно отображать на странице и их размер в зависимости от габаритов окна, чтобы они идеально выстроились в сетку.
* Мы предварительно подгрузим изображения, которые должны быть отображены на текущей странице при помощи скрипта предварительной загрузки, который использует jQuery deferreds. Тем временем будет отображаться div #loading.
* После того, как все будет загружено, мы сгенерируем верстку для фотографий и добавим их в элемент #gallery. Затем мы запустим диагональную анимацию и инициализируем галерею Swipebox.
* Когда пользователь кликает по ссылке, мы повторяем этапы 3 и 4 (с анимацией либо от верхнего левого угла, либо от правого нижнего).

Вряд ли здесь возможно представить вам весь код сразу, поэтому мы постараемся разделить его на части. Сначала предлагаем вам взглянуть на полную структуру, которой мы будем следовать:

assets/js/script.js

$(function(){

    // Global variables that hold state

    var page = 0,
        per_page = 100,
        photo_default_size = 150,
        picture_width = photo_default_size,
        picture_height = photo_default_size,
        max_w_photos, max_h_photos
        data = [];

    // Global variables that cache selectors

    var win = $(window),
        loading = $('#loading'),
        gallery = $('#gallery');

    gallery.on('data-ready window-resized page-turned', function(event, direction){

        // Here we will have the javascript that preloads the images
        // and adds them to the gallery

    });

    // Fetch all the available images with 
    // a GET AJAX request on load

    $.get('load.php', function(response){

        // response.data holds the photos

        data = response.data;

        // Trigger our custom data-ready event
        gallery.trigger('data-ready');

    });

    gallery.on('loading',function(){
        // show the preloader
        loading.show();
    });

    gallery.on('loading-finished',function(){
        // hide the preloader
        loading.hide();
    });

    gallery.on('click', '.next', function(){
        page++;
        gallery.trigger('page-turned',['br']);
    });

    gallery.on('click', '.prev', function(){
        page--;
        gallery.trigger('page-turned',['tl']);
    });

    win.on('resize', function(e){

        // Here we will monitor the resizing of the window
        // and will recalculate how many pictures we can show
        // at once and what their sizes should be so they fit perfectly

    }).resize();

    /* Animation functions */

    function show_photos_static(){

        // This function will show the images without any animations
    }

    function show_photos_with_animation_tl(){

        // This one will animate the images from the top-left

    }

    function show_photos_with_animation_br(){

        // This one will animate the images from the bottom-right

    }

    /* Helper functions */

    function get_per_page(){

        // Here we will calculate how many pictures
        // should be shown on current page

    }

    function get_page_start(p){

        // This function will tell us which is the first
        // photo that we will have to show on the given page

    }

    function is_next_page(){

        // Should we show the next arrow?

    }

    function is_prev_page(){

        // Should we show the previous arrow?

    }

});

 
Некоторые из определений функций остаются пустыми, но вы можете видеть их дальше на странице. Первая группа определенных переменных будет содержать в себе состояние галереи: размеры, массив изображений, текущую страницу и так далее, — что позволяет нам более четко разграничить логику и данные. Мы воспользуемся кастомными событиями для лучшей организации кода (за счет прослушивания и запуска случайно названных событий). Вы можете расценивать этих слушателей событий как методы объекта, а переменные в самом начале как параметры.

После того, как вы ознакомитесь со всеми комментариями в вышеприведенном фрагменте кода, продолжите работу с первым слушателем событий, который выдает необходимую порцию массива изображения в зависимости от текущей страницы:

gallery.on('data-ready window-resized page-turned', function(event, direction){

    var cache = [],
        deferreds = [];

    gallery.trigger('loading');

    // The photos that we should be showing on the new screen
    var set = data.slice(get_page_start(), get_page_start() + get_per_page());

    $.each(set, function(){

        // Create a deferred for each image, so
        // we know when they are all loaded
        deferreds.push($.loadImage(this.thumb));

        // build the cache
        cache.push('<a href="' + this.large + '"' +
                    'style="width:' + picture_width + 'px;height:' + picture_height + 'px;background-image:url(' + this.thumb + ')">'+
                    '</a>');
    });

    if(is_prev_page()){
        cache.unshift('<a style="width:' + picture_width + 'px;height:' + picture_height + 'px;"></a>');
    }

    if(is_next_page()){
        cache.push('<a style="width:' + picture_width + 'px;height:' + picture_height + 'px;"></a>');
    }

    if(!cache.length){
        // There aren't any images
        return false;
    }

    // Call the $.when() function using apply, so that 
    // the deferreds array is passed as individual arguments.
    // $.when(arg1, arg2) is the same as $.when.apply($, [arg1, arg2])

    $.when.apply($, deferreds).always(function(){

        // All images have been loaded!

        if(event.type == 'window-resized'){

            // No need to animate the photos
            // if this is a resize event

            gallery.html(cache.join(''));
            show_photos_static();

            // Re-initialize the swipebox
            $('#gallery .swipebox').swipebox();

        }
        else{

            // Create a fade out effect
            gallery.fadeOut(function(){

                // Add the photos to the gallery
                gallery.html(cache.join(''));

                if(event.type == 'page-turned' && direction == 'br'){
                    show_photos_with_animation_br();
                }
                else{
                    show_photos_with_animation_tl();
                }

                // Re-initialize the swipebox
                $('#gallery .swipebox').swipebox();

                gallery.show();

            });
        }

        gallery.trigger('loading-finished');
    });

});

 
Несмотря на то, что изображения добавляются в div #gallery в рамках одной операции, посредством CSS и выставляется свойство opacity:0. Это дает основу для функций анимации. Первая из них отображает фотографии без анимации, а последние две анимируют их, применяя эффект волны с верхнего левого или правого нижнего края. Анимация полностью основана на CSS, и запускается тогда, когда мы задаем изображению имя класса посредством jQuery.

function show_photos_static(){

    // Show the images without any animations
    gallery.find('a').addClass('static');

}

function show_photos_with_animation_tl(){

    // Animate the images from the top-left

    var photos = gallery.find('a');

    for(var i=0; i<max_w_photos + max_h_photos; i++){

        var j = i;

        // Loop through all the lines
        for(var l = 0; l < max_h_photos; l++){

            // If the photo is not of the current line, stop.
            if(j < l*max_w_photos) break;

            // Schedule a timeout. It is wrapped in an anonymous
            // function to preserve the value of the j variable

            (function(j){
                setTimeout(function(){
                    photos.eq(j).addClass('show');
                }, i*50);
            })(j);

            // Increment the counter so it points to the photo
            // to the left on the line below

            j += max_w_photos - 1;
        }
    }
}

function show_photos_with_animation_br(){

    // Animate the images from the bottom-right

    var photos = gallery.find('a');

    for(var i=0; i<max_w_photos + max_h_photos; i++){

        var j = per_page - i;

        // Loop through all the lines
        for(var l = max_h_photos-1; l >= 0; l--){

            // If the photo is not of the current line, stop.
            if(j > (l+1)*max_w_photos-1) break;

            // Schedule a timeout. It is wrapped in an anonymous
            // function to preserve the value of the j variable

            (function(j){
                setTimeout(function(){
                    photos.eq(j).addClass('show');
                }, i*50);
            })(j);

            // Decrement the counter so it points to the photo
            // to the right on the line above

            j -= max_w_photos - 1;
        }
    }
}

 
Дальше идет функция, которая прослушивает событие изменения окна в размере. Это может произойти когда окно браузера изменяют в размере, либо когда изменяется ориентация устройства. В этой функции мы высчитываем, сколько фотографий уместится на экране, и какой у них должен быть размер, чтобы они идеально выстроились в сетку.

win.on('resize', function(e){

    var width = win.width(),
        height = win.height(),
        gallery_width, gallery_height,
        difference;

    // How many photos can we fit on one line?
    max_w_photos = Math.ceil(width/photo_default_size);

    // Difference holds how much we should shrink each of the photos
    difference = (max_w_photos * photo_default_size - width) / max_w_photos;

    // Set the global width variable of the pictures.
    picture_width = Math.ceil(photo_default_size - difference);

    // Set the gallery width
    gallery_width = max_w_photos * picture_width;

    // Let's do the same with the height:

    max_h_photos = Math.ceil(height/photo_default_size);
    difference = (max_h_photos * photo_default_size - height) / max_h_photos;
    picture_height = Math.ceil(photo_default_size - difference);
    gallery_height = max_h_photos * picture_height;

    // How many photos to show per page?
    per_page = max_w_photos*max_h_photos;

    // Resize the gallery holder
    gallery.width(gallery_width).height(gallery_height);

    gallery.trigger('window-resized');

}).resize();

 
Последняя строка предназначена для того, чтобы функция запускалась сразу после определения, что означает, что мы корректируем значения с самого начала.

Следующие вспомогательные функции берут на себя большинство часто используемых вычислений:

function get_per_page(){

    // How many pictures should be shown on current page

    // The first page has only one arrow,
    // so we decrease the per_page argument with 1
    if(page == 0){
        return per_page - 1;
    }

    // Is this the last page?
    if(get_page_start() + per_page - 1 > data.length - 1){
        // It also has 1 arrow.
        return per_page - 1;
    }

    // The other pages have two arrows.
    return per_page - 2;
}

function get_page_start(p){

    // Which position holds the first photo
    // that is to be shown on the give page

    if(p === undefined){
        p = page;
    }

    if(p == 0){
        return 0;
    }

    // (per_page - 2) because the arrows take up two places for photos
    // + 1 at the end because the first page has only a next arrow.

    return (per_page - 2)*p + 1;
}

function is_next_page(){

    // Should we show the next arrow?

    return data.length > get_page_start(page + 1);
}

function is_prev_page(){

    // Should we show the previous arrow?

    return page > 0;
}

 
Здесь может быть всего несколько строк, и использоваться они могут всего лишь единожды или дважды, но они делают огромную работу для того, чтобы код был максимально читаемым.

1381835047_img-03

CSS-код

И наконец, мы добрались до CSS-кода. По умолчанию у изображений выставлено нулевое значение непрозрачности, и к ним применяется scale-трансформация до 0.8. Мы также используем параметр transition, который анимирует каждое изменение данного атрибута. Класс .show, который добавляется к функциям анимации, повышает уровень непрозрачности и scale элемента, что автоматически анимируется браузером.

assets/css/styles.css

#gallery{
    position:fixed;
    top:0;
    left:0;
    width:100%;
    height:100%;
}

#gallery a{
    opacity:0;
    float:left;
    background-size:cover;
    background-position: center center;

    -webkit-transform:scale(0.8);
    -moz-transform:scale(0.8);
    transform:scale(0.8);

    -webkit-transition:0.4s;
    -moz-transition:0.4s;
    transition:0.4s;
}

#gallery a.static:hover,
#gallery a.show:hover{
    opacity:0.9 !important;
}

#gallery a.static{
    opacity:1;

    -webkit-transform:none;
    -moz-transform:none;
    transform:none;

    -webkit-transition:opacity 0.4s;
    -moz-transition:opacity 0.4s;
    transition:opacity 0.4s;
}

#gallery a.next,
#gallery a.prev{
    background-color:#333;
    cursor:pointer;
}

#gallery a.next{
    background-image:url('../img/arrow_next.jpg');
}

#gallery a.prev{
    background-image:url('../img/arrow_prev.jpg');
}

#gallery a.show{
    opacity:1;

    -webkit-transform:scale(1);
    -moz-transform:scale(1);
    transform:scale(1);
}

 
Класс .static устанавливается посредством функции the show_photos_static(), и нужен он для отключения всех анимаций (за исключением opacity, так как нам нужно, чтобы эффект при наведении был плавным), и мгновенно отображает фотографии (в противном случае, при каждом изменении размера окна вы будете наблюдать диагональный эффект затемнения). Остальную часть этого файла вы можете скачать в файлах к руководству, которые можно скачать в самом верху страницы!

Мы закончили!

Надеемся, что вам понравится этот небольшой эксперимент, и вы сможете найти применение данного эффекта в своих будущих проектах.