Galería Smooth Diagonal Fade con transiciones CSS3

En este tutorial vamos a crear una galería con un suave efecto de desvanecimiento diagonal, impulsado por transiciones css3. Escaneará una carpeta con fotos en el servidor y las mostrará en una cuadrícula que abarca toda la ventana del navegador. Agregar nuevas fotos es tan fácil como copiar dos archivos (una imagen normal y una miniatura, preferiblemente de 150x150 px) a la carpeta de la galería.

Los navegadores compatibles con CSS3 mostrarán un efecto de desvanecimiento diagonal suavemente animado, mientras que los navegadores más antiguos recurrirán a una versión no animada más simple pero aún perfectamente utilizable de la galería.

El HTML

Como de costumbre, lo primero que hacemos cuando comenzamos a trabajar en un nuevo proyecto es escribir el 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>

La galería depende de la biblioteca jQuery, que he incluido antes de la etiqueta del cuerpo de cierre. También agregué un excelente complemento de caja de luz llamado Swipebox, pero puede reemplazarlo fácilmente con la caja de luz que elija. Los dos divs principales son #loading y #gallery . El primero contiene un gif de carga y el segundo las fotos de la galería. La #gallery div se establece en position:fixed por lo que ocupa todo el ancho y el alto de la página. El marcado de las fotos en sí es igual de simple:

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

Las fotos en la galería son todas de 150x150 píxeles, lo que significa que casi nunca lograremos un ajuste exacto para toda la página, a menos que las cambiemos un poco de tamaño. Esto es exactamente lo que le sucedió a la foto de arriba, por lo que tiene un valor de ancho y alto en su atributo de estilo. Verá cómo calculamos esto en la sección JS.

Escaneo de fotos con PHP

Las fotos están contenidas en dos carpetas en el servidor - assets/photos/thumbs/ para las miniaturas y assets/photos/large/ uno para los tamaños completos. Con PHP, escanearemos las carpetas y generaremos un JSON con los nombres de los archivos. Alternativamente, podría devolver las imágenes desde una base de datos, pero deberá mantener la misma estructura. Aquí está el guión:

cargar.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,
));

Agregar nuevas fotos a la galería es tan fácil como copiar la imagen y su miniatura en la carpeta correcta (¡ambos archivos deben tener el mismo nombre!). He duplicado las fotos varias veces para que tengamos un grupo más grande para mostrar en la galería, pero probablemente no querrás hacer esto con tus fotos reales.

Ahora que tenemos el JSON en su lugar, ¡vamos a escribir algo de JavaScript!

JavaScript

Esto es lo que tenemos que hacer:

  1. Primero, emitiremos una solicitud AJAX GET para obtener todas las fotos en el disco desde el script PHP.
  2. Luego calcularemos cuántas fotos mostrar en la página y sus tamaños, dependiendo de las dimensiones de la ventana, para que encajen perfectamente.
  3. Precargaremos todas las imágenes que se mostrarán en la página actual con un script de precarga que usa jQuery deferred. Mientras tanto, mostraremos el div #loading.
  4. Después de cargar todo, generaremos el marcado para las fotos y las agregaremos al elemento #gallery. Luego activaremos la animación de desvanecimiento diagonal e inicializaremos la galería Swipebox.
  5. Cuando el usuario haga clic en una flecha, repetiremos los pasos 3 y 4 (con una animación arriba a la izquierda o abajo a la derecha).

El código es demasiado largo para presentarlo de una sola vez, así que se lo mostraré por partes. Primero, aquí está la estructura general que seguiremos:

activos/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?

    }

});

Algunas de las definiciones de funciones se dejan en blanco, pero puede verlas más abajo en la página. El primer grupo de definiciones de variables mantendrá el estado de la galería:dimensiones, conjunto de imágenes, página actual, etc., lo que permite una separación más clara entre la lógica y los datos. Usaremos eventos personalizados para una mejor organización del código (escuchando y activando eventos con nombres arbitrarios). Puede pensar en estos detectores de eventos como los métodos de un objeto y las variables cerca del principio como sus propiedades.

Una vez que haya leído todos los comentarios en el fragmento anterior, continúe con el primer detector de eventos, que genera la porción relevante de la matriz de imágenes según la página actual:

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 + '" class="swipebox"' +
                    'style="width:' + picture_width + 'px;height:' + picture_height + 'px;background-image:url(' + this.thumb + ')">'+
                    '</a>');
    });

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

    if(is_next_page()){
        cache.push('<a class="next" 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');
    });

});

Aunque las imágenes se añaden a la #gallery div en una sola operación, se establecen en opacity:0 con css. Esto prepara el escenario para las funciones de animación. El primero de ellos muestra las fotos sin animación, y los dos últimos las animan en una ola desde la parte superior izquierda o inferior derecha. La animación está completamente basada en CSS y se activa cuando asignamos un nombre de clase a las imágenes con 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;
        }
    }
}

La siguiente es la función que escucha el evento de cambio de tamaño de la ventana. Esto puede surgir cada vez que se cambia el tamaño de la ventana del navegador o cuando se cambia la orientación del dispositivo. En esta función calcularemos cuántas fotos caben en pantalla, y cuáles deben ser sus tamaños exactos para que quepan perfectamente.

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();

La última línea hace que la función se active justo después de definirla, lo que significa que tenemos valores correctos desde el principio.

Las siguientes funciones auxiliares abstraen algunos de los cálculos más utilizados:

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;
}

Pueden tener solo un par de líneas y usarse solo una o dos veces, pero hacen mucho para que nuestro código sea más legible.

El CSS

Y finalmente, aquí está el código CSS. Las fotos tienen una opacidad cero por defecto y se les aplica una transformación de escala de 0,8. También tienen establecida la propiedad de transición, que hará que cada cambio de un atributo sea animado. El .show La clase, que se agrega mediante las funciones de animación, aumenta la opacidad y la escala del elemento, que el navegador anima automáticamente.

activos/css/estilos.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);
}

El .estático la clase está establecida por el show_photos_static() y deshabilita todas las animaciones (con la excepción de la opacidad, ya que queremos que el efecto de desplazamiento siga siendo suave) y muestra las fotos inmediatamente (de lo contrario, en cada cambio de tamaño vería que la diagonal se desvanece). Puede ver el resto de este archivo en los archivos del tutorial, que puede descargar desde el botón cerca de la parte superior de la página.

¡Hemos terminado!

Espero que te guste este pequeño experimento y encuentres muchos usos para esta galería animada sin problemas.