Portafolio de efectos de obturador con jQuery y Canvas

En el tutorial de hoy, usaremos el elemento canvas de HTML5 para crear un portafolio de fotografía simple, que muestra un conjunto de fotos destacadas con un efecto de obturador de cámara. Esta funcionalidad vendrá en forma de un complemento jQuery fácil de usar que puede incorporar fácilmente en cualquier sitio web.

La idea

El elemento de lienzo es un área especial en la que puede dibujar con JavaScript y aplicar todo tipo de manipulaciones a su imagen. Sin embargo, existen limitaciones en cuanto a lo que se puede hacer con él. Generar animaciones complejas en tiempo real es un desafío, ya que debe volver a dibujar el lienzo en cada cuadro.

Esto requiere mucha potencia de procesamiento que los navegadores web no pueden proporcionar actualmente y, como resultado, las animaciones fluidas son casi imposibles. Pero hay una forma de sortear esta limitación. Si ha jugado con la demostración, habrá notado lo suave que funciona. Esto se debe a que los marcos se generan con anticipación y cada uno se crea como un elemento de lienzo independiente.

Después de la carga inicial de la página (cuando se generan los marcos), el trabajo del complemento se convierte simplemente en recorrer los marcos.

El obturador en sí se genera dibujando la misma imagen triangular ligeramente curvada. Con cada marco, la abertura es más pequeña hasta que las piezas encajan.

HTML

Primero echemos un vistazo más de cerca al marcado HTML de la página. Como estamos usando el elemento canvas, necesitamos definir el documento como HTML5 con el tipo de documento apropiado.

index.html

<!DOCTYPE html> <!-- Defining the document as HTML5 -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>&quot;Shutter Effect&quot; with Canvas and jQuery | Tutorialzine Demo</title>

<link rel="stylesheet" type="text/css" href="assets/css/styles.css" />
<link rel="stylesheet" type="text/css" href="assets/jquery.shutter/jquery.shutter.css" />

</head>
<body>

<div id="top"></div>

<div id="page">

    <h1>Shutter Folio Photography</h1>

    <div id="container">
        <ul>
            <li><img src="assets/img/photos/1.jpg" width="640" height="400" /></li>
            <li><img src="assets/img/photos/2.jpg" width="640" height="400" /></li>
            <li><img src="assets/img/photos/3.jpg" width="640" height="400" /></li>
            <li><img src="assets/img/photos/4.jpg" width="640" height="400" /></li>
        </ul>
    </div>

</div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js"></script>
<script src="assets/jquery.shutter/jquery.shutter.js"></script>
<script src="assets/js/script.js"></script>

</body>
</html>

Las hojas de estilo para la página y el complemento se incluyen en la sección principal, y los archivos de script justo antes de la etiqueta del cuerpo de cierre. El #content div contiene una lista desordenada con cuatro fotos, que se mostrarán como una presentación de diapositivas. Si el navegador del usuario no es compatible con el elemento de lienzo, simplemente recorreremos estas imágenes sin mostrar el efecto de obturador.

Cuando se llama al complemento del obturador, genera el siguiente marcado HTML. En nuestro ejemplo, lo llamamos en el #content div, por lo que se adjunta el siguiente código.

HTML generado

<div class="shutterAnimationHolder" style="width: 640px; height: 400px;">
  <div class="film"
  style="height: 15000px; width: 1000px; margin-left: -500px; top: -300px;">

    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
    <canvas width="1000" height="1000"></canvas>
  </div>
</div>

Cada elemento del lienzo contiene un cuadro de la animación del obturador. La altura del .film div se configura para que sea lo suficientemente grande como para mostrar los elementos del lienzo uno encima del otro. Al animar la propiedad superior de la película, podemos saltar los fotogramas y crear la animación.

El .shutterAnimationHolder div está configurado para tener la misma altura que el contenedor en el que se inserta y se muestra sobre la lista desordenada con las fotos. Con desbordamiento:oculto oculta el resto de la película y solo muestra un cuadro a la vez. Puede pensar en los elementos del lienzo como PNG normales, por lo que admiten una transparencia total y muestran la foto debajo de ellos.

Volveremos a esto en el paso jQuery del tutorial.

CSS

El CSS que impulsa la demostración es bastante simple, ya que la mayor parte del trabajo se realiza generando las imágenes de los lienzos. Sin embargo, aún deben organizarse como una película y animarse correctamente para lograr una animación fluida.

jquery.obturador.css

.shutterAnimationHolder .film canvas{
    display: block;
    margin: 0 auto;
}

.shutterAnimationHolder .film{
    position:absolute;
    left:50%;
    top:0;
}

.shutterAnimationHolder{
    position:absolute;
    overflow:hidden;
    top:0;
    left:0;
    z-index:1000;
}

Estos tres conjuntos de reglas tienen el prefijo .shutterAnimationHolder class, por lo que los estilos solo afectan el marcado, generado por el complemento. Si le gusta la optimización, puede optar por copiar este código en su hoja de estilo principal para minimizar el número de solicitudes HTTP.

jQuery

Esta es la parte más interesante del tutorial. Aquí crearemos un complemento jQuery - tzShutter - que es fácil de usar y requiere modificaciones mínimas en su sitio web para poder usarlo.

Un aspecto importante del desarrollo de este complemento es proporcionar el soporte adecuado para los usuarios cuyos navegadores no entienden la etiqueta del lienzo (básicamente todas las versiones de IE excepto la 9). Esto se puede hacer fácilmente omitiendo la generación del lienzo en este caso.

También debemos proporcionar una forma para que los usuarios de tzShutter activen las animaciones de apertura y cierre. Lo lograremos vinculando dos eventos personalizados al elemento contenedor:shutterOpen y obturadorCerrar , ambos ejecutados fácilmente con el trigger() método jQuery.

Además, el complemento proporcionará a los usuarios una forma de conectar la funcionalidad personalizada mediante funciones de devolución de llamada, pasadas como parámetros. Estos se ejecutan en partes clave del proceso de animación:cuando se generan los elementos del lienzo y cuando se abre o se cierra el obturador.

Puede ver el código del complemento a continuación.

jquery.obturador.js

(function(){

    // Creating a regular jQuery plugin:

    $.fn.tzShutter = function(options){

        // Checking for canvas support. Works in all modern browsers:
        var supportsCanvas = 'getContext' in document.createElement('canvas');

        // Providing default values:

        options = $.extend({
            openCallback:function(){},
            closeCallback:function(){},
            loadCompleteCallback:function(){},
            hideWhenOpened:true,
            imgSrc: 'jquery.shutter/shutter.png'
        },options);

        var element = this;

        if(!supportsCanvas){

            // If there is no support for canvas, bind the
            // callack functions straight away and exit:

            element.bind('shutterOpen',options.openCallback)
                   .bind('shutterClose',options.closeCallback);

            options.loadCompleteCallback();

            return element;
        }

        window.setTimeout(function(){

            var frames = {num:15, height:1000, width:1000},
                slices = {num:8, width: 416, height:500, startDeg:30},
                animation = {
                    width : element.width(),
                    height : element.height(),
                    offsetTop: (frames.height-element.height())/2
                },

                // This will calculate the rotate difference between the
                // slices of the shutter. (2*Math.PI equals 360 degrees in radians):

                rotateStep = 2*Math.PI/slices.num,
                rotateDeg = 30;

            // Calculating the offset
            slices.angleStep = ((90 - slices.startDeg)/frames.num)*Math.PI/180;

            // The shutter slice image:
            var img = new Image();

            // Defining the callback before setting the source of the image:
            img.onload = function(){

                window.console && console.time && console.time("Generating Frames");

                // The film div holds 15 canvas elements (or frames).

                var film = $('<div>',{
                    className: 'film',
                    css:{
                        height: frames.num*frames.height,
                        width: frames.width,
                        marginLeft: -frames.width/2, // Centering horizontally
                        top: -animation.offsetTop
                    }
                });

                // The animation holder hides the film with overflow:hidden,
                // exposing only one frame at a time.

                var animationHolder = $('<div>',{
                    className: 'shutterAnimationHolder',
                    css:{
                        width:animation.width,
                        height:animation.height
                    }
                });

                for(var z=0;z<frames.num;z++){

                    // Creating 15 canvas elements.

                    var canvas  = document.createElement('canvas'),
                        c       = canvas.getContext("2d");

                    canvas.width=frames.width;
                    canvas.height=frames.height;

                    c.translate(frames.width/2,frames.height/2);

                    for(var i=0;i<slices.num;i++){

                        // For each canvas, generate the different
                        // states of the shutter by drawing the shutter
                        // slices with a different rotation difference.

                        // Rotating the canvas with the step, so we can
                        // paint the different slices of the shutter.
                        c.rotate(-rotateStep);

                        // Saving the current rotation settings, so we can easily revert
                        // back to them after applying an additional rotation to the slice.

                        c.save();

                        // Moving the origin point (around which we are rotating
                        // the canvas) to the bottom-center of the shutter slice.
                        c.translate(0,frames.height/2);

                        // This rotation determines how widely the shutter is opened.
                        c.rotate((frames.num-1-z)*slices.angleStep);

                        // An additional offset, applied to the last five frames,
                        // so we get a smoother animation:

                        var offset = 0;
                        if((frames.num-1-z) <5){
                            offset = (frames.num-1-z)*5;
                        }

                        // Drawing the shutter image
                        c.drawImage(img,-slices.width/2,-(frames.height/2 + offset));

                        // Reverting back to the saved settings above.
                        c.restore();
                    }

                    // Adding the canvas (or frame) to the film div.
                    film.append(canvas);
                }

                // Appending the film to the animation holder.
                animationHolder.append(film);

                if(options.hideWhenOpened){
                    animationHolder.hide();
                }

                element.css('position','relative').append(animationHolder);

                var animating = false;

                // Binding custom open and close events, which trigger
                // the shutter animations.

                element.bind('shutterClose',function(){

                    if(animating) return false;
                    animating = true;

                    var count = 0;

                    var close = function(){

                        (function animate(){
                            if(count>=frames.num){
                                animating=false;

                                // Calling the user provided callback.
                                options.closeCallback.call(element);

                                return false;
                            }

                            film.css('top',-frames.height*count - animation.offsetTop);
                            count++;
                            setTimeout(animate,20);
                        })();
                    }

                    if(options.hideWhenOpened){
                        animationHolder.fadeIn(60,close);
                    }
                    else close();
                });

                element.bind('shutterOpen',function(){

                    if(animating) return false;
                    animating = true;

                    var count = frames.num-1;

                    (function animate(){
                        if(count<0){

                            var hide = function(){
                                animating=false;
                                // Calling the user supplied callback:
                                options.openCallback.call(element);
                            };

                            if(options.hideWhenOpened){
                                animationHolder.fadeOut(60,hide);
                            }
                            else{
                                hide();
                            }

                            return false;
                        }

                        film.css('top',-frames.height*count - animation.offsetTop);
                        count--;

                        setTimeout(animate,20);
                    })();
                });

                // Writing the timing information if the
                // firebug/web development console is opened:

                window.console && console.timeEnd && console.timeEnd("Generating Frames");
                options.loadCompleteCallback();
            };

            img.src = options.imgSrc;

        },0);

        return element;
    };

})(jQuery);

El único inconveniente de este método es que la tarea intensiva del procesador de generar los elementos del lienzo se realiza cuando se carga la página. Esto puede hacer que la interfaz del navegador deje de responder durante un breve período de tiempo. Alternativamente, podría usar imágenes PNG reales en su lugar, pero esto agregaría más de 1 mb de peso a sus páginas (en comparación con los 12 KB actuales).

Ahora veamos cómo se usa el complemento.

secuencia de comandos.js

$(document).ready(function(){

    var container = $('#container'),
        li = container.find('li');

    // Using the tzShutter plugin. We are giving the path
    // to he shutter.png image (located in the plugin folder), and two
    // callback functions.

    container.tzShutter({
        imgSrc: 'assets/jquery.shutter/shutter.png',
        closeCallback: function(){

            // Cycling the visibility of the li items to
            // create a simple slideshow.

            li.filter(':visible:first').hide();

            if(li.filter(':visible').length == 0){
                li.show();
            }

            // Scheduling a shutter open in 0.1 seconds:
            setTimeout(function(){container.trigger('shutterOpen')},100);
        },
        loadCompleteCallback:function(){
            setInterval(function(){
                container.trigger('shutterClose');
            },4000);

            container.trigger('shutterClose');
        }
    });

});

Cuando el complemento termina de generar los elementos del lienzo, activa la función loadCompleteCallback. Lo usamos para programar una animación del obturador cada cuatro segundos, acompañada de un cambio de la foto visible en la lista desordenada.

¡Con esto, nuestro complemento de efecto de obturador está completo!

Conclusión

La etiqueta de lienzo brinda a los desarrolladores una amplia gama de posibilidades y les permite crear nuevas y emocionantes interfaces de usuario, animaciones e incluso juegos. Comparta sus pensamientos en la sección de comentarios a continuación. Si te gustó este tutorial, asegúrate de suscribirte a nuestro canal RSS y síguenos en Twitter.