2011/07/27

jQuery FRoll plugin - Preview de vídeos sin necesidad de cargarlos


La carga de un objeto de tipo vídeo es una operación realmente costosa y pesada respecto a rendimiento de una página. Máxime si estamos haciendo una galería de vídeos donde se muestran varios vídeos a la vez.

Una primera aproximación para aligerar esa carga es lo que se ha hecho toda la vida mediante la utilización de thumbnails de los vídeos, pero yo al menos siempre me quedo con la sensación de que quiero algo más... un triste fotograma de un vídeo no me saca de pobre y más si es un fotograma automatizado que puede que coincida con una pantalla en negro.

Pero... y si tuviéramos varios fotogramas de un mismo vídeo. Se podrían poner uno a continuación del otro para hacer una secuencia, pero es algo que se antoja "artificial"... lo ideal sería que dichos fotogramas se reprodujeran en sucesión antes de la carga real del vídeo de modo que solo se muestre el vídeo en sí bajo petición por parte del usuario.

Ahí es donde entra FRoll, un plugin 100% hecho por mí, haciéndonos la vida un poco más sencilla y más si el vídeo en cuestión está alojado en youtube, que hace el trabajo de sacar fotogramas de los vídeos por nosotros (sólo 3, pero qué le vamos a hacer).

Una vez incluido este plugin, tendremos disponible el método .froll() que podremos llamar sobre las imágenes que deseemos convertir en previsualizaciones de vídeos. El nombre de los fotogramas se deriva a partir del nombre de la imagen original mediante sencillas reglas de transformación y ya se proporcionan reglas para los escenarios más comunes que son el de imágenes alojadas en youtube, thumbnail en local y fotogramas en remoto en youtube e imagen + fotogramas en local. Usando dichos ejemplos es fácil adaptar el comportamiento a las necesidades de cada uno.

También soporta el la delegación del evento de click para permitirnos completar acciones sobre la imagen original y que el uso del plugin sea totalmente transparente tanto para el usuario como para el desarrollador.

Para ver el plugin en acción, como es costumbre, podéis hacerlo en mi fiddle o en la página del plugin en el sitio de jQuery.


Y ahora, el código del plugin, que está comentado hasta el aburrimiento para que no se pierda nadie, y ya sabéis, si lo usáis, mejorais, arregláis, hacédmelo saber ;)
/**
 * jquery.froll-0.1.js - Fancy Image Roll
 * ==========================================================
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 *
 * http://3nibbles.blogspot.com/2011/07/plugin-jquery-sliding-buttons.html
 * http://plugins.jquery.com/project/froll
 *
 * FRoll is a jQuery plugin to simplify the task of providing a simple and
 * efficient way to expand the image information of a picture using a sequence
 * of images.
 *
 * The direct use of this plugin is to provide a preview of a youtube video
 * using the Google API. In that case it presents a sucession of three
 * different moments of the video in a smooth sucession when mouse enters into
 * the image.
 *
 * Everything is self-contained. No need of extra CSS or complex controls,
 * just call the method over the image and you are ready.
 *
 * INSTANTIATION
 * Just call the ".froll()" method over the images selector.
 *
 *     $('.videoCaptionImg').froll( [ options_object ] );
 *
 * OPTIONS
 *     - transform  $.froll.youtube  Array that defines [0] as the regex to apply
 *                                   to src and [1] as the resulting string with
 *                                   {number} placeholder for the animation images.
 *     - frames     [1, 2, 3],       Array with the {number} of each frame of the animation
 *     - width      null,            Width of the animation frames.  Null = automatic
 *     - height     null,            Height of the animation frames. Null = automatic
 *     - speed      750,             Fade animation speed
 *     - time       1500,            Time between frames
 *     - click      function(taget)  Click callback function. Defaults to trigger click
 *                                   event over the container <a> of the img.
 *
 * PUBLIC API
 *     -  $.froll.stop()    Stops current animation and hides the preview
 *
 * HELPER CONSTANTS
 *     $.froll.youtube       Transform array that converts origin src stored in youtube
 *                           (http://img.youtube.com/vi/<video_ID>/0.jpg) into youtube previews.
 *     $.froll.youtubeLocal  Transform array that converts local stored captions with name the
 *                           id of the video, into youtube previews.
 *     $.froll.local         Transform array that converts local stored captions into local
 *                           stored previews with the same name but ended with "_<number>"
 *                           in the same directory than caption.
 *
 * CSS CLASSES
 *     - Container: #froll-overlay
 *     - Frames:    .froll-frame
 *
 * Legal stuff
 *     You are free to use this code, but you must give credit and/or keep header intact.
 *     Please, tell me if you find it useful. An email will be enough.
 *     If you enhance this code or correct a bug, please, tell me.
 */
(function( $ ) {

    ///////////////////////////////////////////////////////////////////////////////
    // Private members
    ///////////////////////////////////////////////////////////////////////////////

    var busy      = false,
        overlay   = null,
        frames    = [],
        frame     = -1,
        lframe    = 0,
        options   = {},
        target    = null,
        src       = "",
        tickTimer = null,
        imgPreloader = new Image(),
        //isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest,

        // ========================================================================
        // Starts the animation
        _start = function() {
            _stop(); // Hides current animation (if any)

            if( !target.attr('src') ) return; // No image src

            // Gets the options and src transform
            options = target.data('froll');
            src = target.attr('src').replace( options.transform[0], options.transform[1] );

            // Moves the overlay to the target position
            var pos = _getPos(target);
            overlay.css({
                'position'  : 'absolute',
                'left'      : pos.left,
                'top'       : pos.top,
                'width'     : pos.width,
                'height'    : pos.height,
                'zIndex'    : 9999,
                'background': 'transparent'
                //,'border': '1px solid red'
            }).show();

            // ========================================================================
            // Starts the frames preload chain loading first frame
            lframe = 0; // Frame being loaded
            if(!imgPreloader) imgPreloader = new Image();
            imgPreloader.onerror = function() { _error(); };
            imgPreloader.onload  = _preloadCompleted;

            imgPreloader.src = src.replace( /\{number\}/, ""+options.frames[lframe] );
            if(imgPreloader.complete) _preloadCompleted(); // Cached images don't fire onload events
        },

        // ========================================================================
        // Gracefully stops the animation
        _stop = function() {
            clearInterval(tickTimer); // Disables the timer
            imgPreloader.onerror = imgPreloader.onload = null;
            if( target && overlay.is( ':visible' ) )
                overlay.hide().empty();   // Hides the overlay and deletes the frames

            frames = [];
            frame  = -1;
            //target = options = null;
            busy   = false;
        },

        // ========================================================================
        // Frame image load error
        _error = function() {
            alert( "Error loading image at " + src );
        },

        // ========================================================================
        // Function called on animation click
        _click = function(e) {
            if( typeof options.click !== 'undefined' )
                options.click(target);
        },

        // ========================================================================
        // Image preload complete event
        _preloadCompleted = function() {
            // Gets default image dimensions
            if( !options.width )  options.width  = overlay.width();  //imgPreloader.width;
            if( !options.height ) options.height = overlay.height(); //imgPreloader.height;

            // Creates frame ima
            $("<img />").attr({
                'id'   : 'froll-frame-' + lframe,
                'class': 'froll-frame',
                'src'  : imgPreloader.src
            }).css({
                'position'  : 'absolute',
                'display'   : 'block',
                'left'      : '0px',
                'top'       : '0px',
                'width'     : options.width+'px',
                'height'    : options.height+'px',
                'zIndex'    : lframe+1,
                'opacity'   : 0
                //,'visibility': 'hidden'
            }).appendTo( overlay );

            // Shows first frame
            if( lframe == 0 )
            {
                _tick();
                tickTimer = setInterval( _tick, options.time );
            }

            // Preloads next frame
            frames[ lframe++ ] = 1; // Marks frame as done
            if( lframe < options.frames.length )
            {
                // Intermediate frame
                imgPreloader.src = src.replace( /\{number\}/i, options.frames[lframe] );
                if(imgPreloader.complete) _preloadCompleted(); // Cached images don't fire onload events
            }
            else
                imgPreloader.onerror = imgPreloader.onload = null; // Last frame
        },

        // ========================================================================
        // Shows next frame
        _tick = function() {

            var l = options.frames.length - 1;
            var children = overlay.children();

            // Animates next frame
            if( frame == -1 )
            {
                // First run
                children.eq( 0 ).css('opacity', 0).stop( true, true ).animate( { 'opacity': 1 }, options.speed );
                frame = 0;
            }
            else if( frame == 0 )
            {
                // First frame (after a full run)
                for(var i = 1; i < l; i++) children.eq( i ).css( 'opacity' , 0); // Hides all but first and last frames
                children.eq( 0 ).css( 'opacity', 1 ).show(); // Shows first frame (bellow last frame)
                children.eq( l ).stop( true, true ).animate( { 'opacity': 0 }, options.speed );
            }
            else if( frame <= l )
            {
                // Intermediate frame
                var next = children.eq( frame );
                if(next)
                    next.css('opacity', 0).stop( true, true ).animate( { 'opacity': 1 }, options.speed );
            }
            else
            {
                // The last frame. Resets animation to show first frame and hide the last one
                children.eq( 0 ).css( 'opacity', 1 );
                children.eq( l ).stop( true, true ).animate( { 'opacity': 0 }, options.speed );
            }
            frame = (frame+1) % (l+1);
        },

        // ========================================================================
        // Helper function to get the exact obj position in the page
        _getPos = function(obj) {

            var pos = obj.offset();

            pos.top   += parseInt( obj.css( 'paddingTop' )       , 10 ) || 0;
            pos.left  += parseInt( obj.css( 'paddingLeft' )      , 10 ) || 0;

            pos.top   += parseInt( obj.css( 'border-top-width' ) , 10 ) || 0;
            pos.left  += parseInt( obj.css( 'border-left-width' ), 10 ) || 0;

            pos.width  = obj.width();
            pos.height = obj.height();

            return pos;
        };


    ///////////////////////////////////////////////////////////////////////////////
    // Public members
    ///////////////////////////////////////////////////////////////////////////////

    // ========================================================================
    // Instantiation. Called on every object of the supplied selector
    $.fn.froll = function( obj ) {
        if (!$(this).length) {
            return this;
        }

        if( $(this).data( 'froll' ) )
        {
            // Object already initialized. Starts the animation over it simulating a click
            if($(this).click) $(this).click();
        }
        else
        {
            // Object not initialized. Sets data and binds events
            $(this)
                .data( 'froll', $.extend( $.fn.froll.defaults, obj ) )
                .unbind( 'mouseenter' )
                .bind( 'mouseenter', function(e) {
                    var self = $(this);
                    e.preventDefault();

                    if (busy && self !== target) _stop(); // Stops current animation
                    busy = true;
                    //var rel = self.attr('rel') || '';
                    target = self;

                    _start(); // Starts the animation over target element
                    return;
                });
        }

        return this;
    };


    // ========================================================================
    // Container class for the public interface
    $.froll = function(obj) {  };

    // ========================================================================
    // Inits the components needed for the animation overlay
    $.froll.init = function() {
        if ( $( '#froll-overlay' ).length ) {
            return;
        }

        // Components
        $('body').append(
            overlay    = $( '<div id="froll-overlay"></div>' )
        );

        // Animation controls events
        overlay.mouseleave( _stop );
        overlay.click( _click );

        return this;
    };

    // ========================================================================
    // Stops current animation and hides overlay
    $.froll.stop = function() { _stop(); };

    // ========================================================================
    // Sample transformation arrays
    $.froll.youtube      = [ /.*\/(.*)\/0\.(jpg|gif|png|bmp|jpeg)(.*)?/i, 'http://img.youtube.com/vi/$1/{number}.$2' ]; // Caption image is located in youtube (http://img.youtube.com/vi/<video_ID>/0.jpg)
    $.froll.youtubeLocal = [ /.*\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i   , 'http://img.youtube.com/vi/$1/{number}.$2' ]; // Caption image is located elsewhere but caption image name is the youtube video_ID
    $.froll.local        = [ /(.*)\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i , '$1/$2_{number}.$3$4' ];                      // Caption image is located elsewhere and frames are in format "originalImage_<frame>.jpg" in the same directory

    // ========================================================================
    // Froll options defaults
    $.fn.froll.defaults = {
        transform  : $.froll.youtube, // Array that defines [0] as the regex to apply to src and [1] as the resulting string with {number} placeholder for the animation images
        frames     : [1, 2, 3],       // Array with the {number} of each frame of the animation
        width      : null,            // Width of the animation frames.  Null = automatic frame image width
        height     : null,            // Height of the animation frames. Null = automatic frame image height
        speed      : 750,             // Fade animation speed
        time       : 1500,            // Time between frames

        click      : function(taget) { $(target).closest('a').click(); } // Click callback function
    };

    // ========================================================================
    // Inits the animation overlay on DOM ready
    $(document).ready(function() {
        $.froll.init();
    });

})( jQuery );

Y ahora, la versión minimizada, que se queda en 2.5Kb (aún menos gzipeada, claro)
/**
 * jquery.froll-0.1.js - Fancy Image Roll - (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 * http://3nibbles.blogspot.com/2011/07/jquery-froll-plugin-preview-de-videos.html
 * http://plugins.jquery.com/project/froll
 * You are free to use this code, but you must give credit and/or keep header intact.
 */
(function(a){var k=!1,d=null,g=-1,h=0,b={},e=null,i="",m=null,c=new Image,p=function(){j();if(e.attr("src")){b=e.data("froll");i=e.attr("src").replace(b.transform[0],b.transform[1]);var a=o(e);d.css({position:"absolute",left:a.left,top:a.top,width:a.width,height:a.height,zIndex:9999,background:"transparent"}).show();h=0;c||(c=new Image);c.onerror=function(){alert("Error loading image at "+i)};c.onload=l;c.src=i.replace(/\{number\}/,""+b.frames[h]);c.complete&&l()}},j=function(){clearInterval(m);c.onerror= c.onload=null;e&&d.is(":visible")&&d.hide().empty();g=-1;k=!1},q=function(){typeof b.click!=="undefined"&&b.click(e)},l=function(){if(!b.width)b.width=d.width();if(!b.height)b.height=d.height();a("<img />").attr({id:"froll-frame-"+h,"class":"froll-frame",src:c.src}).css({position:"absolute",display:"block",left:"0px",top:"0px",width:b.width+"px",height:b.height+"px",zIndex:h+1,opacity:0}).appendTo(d);h==0&&(n(),m=setInterval(n,b.time));h++;h<b.frames.length?(c.src=i.replace(/\{number\}/i,b.frames[h]), c.complete&&l()):c.onerror=c.onload=null},n=function(){var a=b.frames.length-1,f=d.children();if(g==-1)f.eq(0).css("opacity",0).animate({opacity:1},b.speed),g=0;else if(g==0){for(var c=1;c<a;c++)f.eq(c).css("opacity",0);f.eq(0).css("opacity",1).show();f.eq(a).animate({opacity:0},b.speed)}else g<=a?(f=f.eq(g))&&f.css("opacity",0).animate({opacity:1},b.speed):(f.eq(0).css("opacity",1),f.eq(a).animate({opacity:0},b.speed));g=(g+1)%(a+1)},o=function(a){var b=a.offset();b.top+=parseInt(a.css("paddingTop"), 10)||0;b.left+=parseInt(a.css("paddingLeft"),10)||0;b.top+=parseInt(a.css("border-top-width"),10)||0;b.left+=parseInt(a.css("border-left-width"),10)||0;b.width=a.width();b.height=a.height();return b};a.fn.froll=function(b){if(!a(this).length)return this;a(this).data("froll")?a(this).click&&a(this).click():a(this).data("froll",a.extend(a.fn.froll.defaults,b)).unbind("mouseenter").bind("mouseenter",function(b){var c=a(this);b.preventDefault();k&&c!==e&&j();k=!0;e=c;p()});return this};a.froll=function(){}; a.froll.init=function(){if(!a("#froll-overlay").length)return a("body").append(d=a('<div id="froll-overlay"></div>')),d.mouseleave(j),d.click(q),this};a.froll.stop=function(){j()};a.froll.youtube=[/.*\/(.*)\/0\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"http://img.youtube.com/vi/$1/{number}.$2"];a.froll.youtubeLocal=[/.*\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"http://img.youtube.com/vi/$1/{number}.$2"];a.froll.local=[/(.*)\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"$1/$2_{number}.$3$4"];a.fn.froll.defaults={transform:a.froll.youtube, frames:[1,2,3],width:null,height:null,speed:750,time:1500,click:function(){a(e).closest("a").click()}};a(document).ready(function(){a.froll.init()})})(jQuery);

Actualización. Añadida la clase CSS para cambiar el cursor a tipo "mano" al hacer hover. Ver en el jsfiddle.

7 comentarios:

  1. Hi, great plugin, how i can click in image and load the link?

    ResponderEliminar
    Respuestas
    1. Hi Eduardo. To achieve the click you want, just look in the documentation at the begining of the plugin. THere is a property named 'click' where you can define your own callback function receiving the original IMG element.

      In the demo, replace the instantiation with this one to see it in action:
      $('#demo').froll( { 'click': function( target ) {
      console.log( 'click', target );
      console.log( $(target).attr('src').match( $.froll.youtube[0] )[1] );
      } } );

      The second console.log uses the plugin pre-built regular expresions and shows the video id that you need to build the youtube video URL to redirect al user.

      The redirect URL is in the format: "http://www.youtube.com/watch?v=" + videoId;

      Hope it helps ;)

      Eliminar
  2. Thanks! But dont work, see http://jsfiddle.net/zRPJd/ whatever can you showme how make this with no youtube video.

    A lot of thanks, my jquery skills it's really bad.

    ResponderEliminar
  3. Mmm... are you sure that you are doing it right? Just tried on the fiddle and works as expected...
    What do you want exactly? redirect the user to youtube from the image. In that case you need the videoId using the method of the previous comment parsing the image src. If you want to redirect the user to another place, you can store the id (or whatever you want) for example in the rel attribute of the original image using:

    Creation: < img id="demo" rel="Value I want for this element" src="http://thumbnailImage"/>
    On click rel read: var myValue = $('#demo').attr('rel');

    On Froll instantiation remember to replace: $('#demo').froll();
    With this (it's the same than before but a bit simplified):

    $('#demo').froll( { 'click': function( target ) {
    var videoId = $(target).attr('src').match( $.froll.youtube[0] )[1]; // Gets videoId
    alert( videoId ); // Shows the original videoId of the image
    document.location = "http://www.youtube.com/watch?v=" + videoId; // Redirects the user
    } } );

    To redirect the user to the video URL, just use standar javascript DOM methods (no need of jQuery) using:
    document.location = "http://www.youtube.com/watch?v=" + videoId;

    ResponderEliminar
  4. Hola mira tengo este codigo, funciona perfecto, solo que cuando pongo el mouse sobre el la imagen (el video preview) el mouse no cambia a la manito tipica de cuando algo es clickeable.

    Si me peudes dar una mano, sería ideal y muchas gracias.
    $(document).ready(function() {
    $('#demo').froll( {
    "frames": [4, 5, 6, 7, 8, 9, 10, 12, 14, 16],
    "click": function( target ) {
    document.location.href = "http://www.example.com";
    }
    } );
    });

    ResponderEliminar
    Respuestas
    1. Tu mejor opción es usar CSS estándar para hacerlo. En el caso del ejemplo, suponiendo que el selector es #demo, añade la siguiente regla:

      .froll-frame:hover {
      cursor: pointer; cursor: hand;
      }

      Espero que te sirva. Un saludo.

      Eliminar
    2. Y como es normal, al responder sin releer, como habrás podido deducir, el selector #demo no afecta para lo que quieres conseguir :)

      Eliminar