viernes, 21 de junio de 2013

Rollovers, precarga de imágenes y sprites

Cuando necesitamos mostrar imágenes que no están en la página inicial, la petición de la imagen y la carga tardan un tiempo que puede ser apreciable por el usuario. En algunas ocasiones este tiempo de espera no es aceptable. Por ejemplo cuando un botón cambia al pulsarlo. No es lógico que el usuario haga click y el botón reaccione medio segundo despues.

Hay varias técnicas para evitar esta espera:

  • Precargar las imágenes y cambiarlas dinámicamente
  • Crear varias imagenes dentro del elemento y ocultar/mostrar según sea necesario
  • Utilizar sprites para cargar todas las imágenes juntas

El problema

Como ejemplo vamos a trabajar con un botón tipo interruptor:

Tenemos dos imágenes separadas para representar los estados on y off del botón:

Queremos que cuando el usuario pulse el botón, la imagen cambie instantáneamente. En el evento click podemos asignar una función que sustituya una imagen por otra:

//html
//JavaScript (function () { //save img DOM element for quick access var $img = $("#switch img"); //define click handler to switch images $("#switch").click( function () { if ( $img.hasClass("on") ) { $img.attr("src", "images/switchOff.png"); } else { $img.attr("src", "images/switchOn.png"); } $img.toggleClass("on"); }); })();

En el HTML sólo creamos el elemento con la imagen inicial, con el interruptor a on (switchOn.png). Utilizaremos una clase "on" para averiguar facilmente el estado de nuestro botón.

Con el JavaScript simplemente asignamos una función que se lanzará en el evento click. La función averigua el estado actual del botón leyendo la clase, cambia la imagen a la correspondiente al estado contrario y cambia la clase para indicar el nuevo estado.

Esto funciona pero presenta un problema. La primera vez que se pulsa el botón la imagen no está cargada. Perdir y cargar la imagen lleva un tiempo y hace que la respuesta no sea inmediata. El click habrá terminado y el botón aún no ha cambiado. Cuanto peor sea la conexión o el servidor, mayor será el retraso y peor la experiencia del usuario.

La segunda vez que se pulse el botón la respuesta sí será inmediata, porque la imagen ya está cargada. Está en la caché del navegador. La solución entonces, para tener una buena respuesta inicial, sería pre-cachear la segunda imagen al cargar la página.

Precarga de imágenes

La idea es cargar las imágenes en la caché del navegador antes de que las necesitemos. Cuando las mostremos aparecerán inmediatamente.

Para pre-cachear la imagen podemos crear un objeto image en memoria:

var imgOff = new Image();
imgOff.src = "images/switchOff.png";

La primera línea crea un objeto Image y la segunda le asigna el path o URL de la imagen para que la carge. Opcionalmente podemos pasarle la altura y la anchura al constructor.

Con esto ya tendríamos la imagen disponible en la caché del navegador y su uso sería inmediato. Sólo tenemos que asignar imgOff.src al src de la imagen que estamos cambiando:

      $("#switch img").attr("src", imgOff.src);

El código completo precargando la imagen sería:

(function () {

  //preload second image
  var imgOff = new Image();
  imgOff.src = "images/switchOff.png";

  //save img DOM element for quick access
  var $img = $("#switch img");

  //define click handler to switch images
  $("#switch").click( function () { 
    if ( $img.hasClass("on") ) {
      $img.attr("src", imgOff.src);    // <-- assigned from our object
    }
    else {
      $img.attr("src", "images/switchOn.png");
    }
    $img.toggleClass("on");
  });

 })(); 

new Image() vs createElement("img")

En el ejemplo que hemos visto antes creábamos un objeto Image para cargar la imagen. En realidad este objeto es una reliquia del DOM Level 0, que es como se conoce al DOM previo a la primera estandarización (DOM level 1). Todos los navegadores proporcionan el API definido inicialmente en este DOM pre-estandar por compatibilidad.

La forma 'actual' de hacer la precarga sería:

  var imgOff = document.createElement("img");
  imgOff.src = "images/switchOff.png";

Las dos formas son equivalentes y son correctas. La que más se utiliza y la que me parece más elegante es new Image(), aunque sea parte de un API antiguo.

Precargando varias imágenes

Si tenemos varias imagenes para cachear podemos hacer:

var images = "one.png, two.png, three.png".split(",");
var tempImg = [];

for(var i=0; i < images.length; i++) {
    tempImg[i] = new Image();
    tempImg[i].src = images[x]
}

Ocultar un elemento y mostrar otro

Para hacer rollovers con sólo dos imágenes, como el ejemplo que estamos tratando, podemos crear el elemento con las dos imágenes inicialmente. Una estará oculta (switchOff.png) y la otra visible (switchOn.png). Al hacer click ocultamos On y mostramos Off. Al estar las dos imagenes en el documento desde el principio, las dos se cargarán en el inicio.

Para implementarlo crearemos una clase "hidden" con display: none. Colocaremos esa clase a la imagen que queramos ocultar:


//html
//CSS .hidden { display: none; } //JavaScript (function(){ //store all img elements for quick access var $imgs = $("#switch img"); $("#switch").click( function () { for (var i = 0; i < $imgs.length; i++) { $imgs.eq(i).toggleClass("hidden"); } }); })();

Ahora la variable $imgs contiene un array con dos imágenes. En el click recorremos las dos imagenes y cambiamos la clase 'hidden' en cada una de ellas. A la imagen que no la tiene se la ponemos para ocultarla y a la que la tenía se la quitamos para mostrarla.

La instrucción $imgs.eq(i).toggleClass("hidden"); selecciona, del array de imágenes, la que esté en el index 'i' y permuta la clase 'hidden' ( si la tiene se la quita y si no la tiene se la añade).

Utilizando Sprites

Un sprite son varias imágenes juntas en un solo fichero. Se utiliza mucho para imágenes pequeñas, tipo iconos, botones o símbolos del UI.

Este es un sprite que utiliza Google:

Es una sóla imagen .png que contiene todos los iconos que utilizan en su interfaz. Sólo se hace una petición HTTP y todas las imágenes quedan ya disponibles en la caché.

En nuestro ejemplo tenemos que construir un sprite con las dos imagenes que utilizamos:

Cuando el navegador cargue la imagen para la posición inicial del botón, ya tenemos cargada la otra (es la misma imagen). Evidentemente necesitamos alguna manera de mostrar sólo la parte que nos interesa.

Cuando la imagen que necesitamos forma parte de un sprite no podemos utilizarla en una etiqueta <img>, tenemos que colocarla siempre como imagen de fondo de un elemento (background-image). Es muy importante conocer la posición exacta de cada imagen en la composición total, así como su anchura y altura porque jugaremos con la propiedad CSS background-position para colocarla.

Como ahora el <div> del botón está vacío (no contiene una <img>), tenemos que definir su anchura y su altura en CSS para que se vea justo la parte de la imagen de fondo que nos interesa:


//html
//CSS #switch { width: 70px; height: 35px; background-image: url("images/switch_Sprite.png"); background-position: 0 35px; }

Utilizamos background-position para definir la coordenada (x, y) desde la que queremos que se muestre la imagen. En nuestro caso la imagen en off empieza en (x=0, y=0) y la imagen en on en (x=0, y=35px). Por lo que inicialmente mostraremos la imagen en background-position: 0 35px. Al hacer click cambiaremos a background-position: 0 0. A partir de la posición especificada se mostrará lo que quepa en los 70x35 pixels que hemos definido como contenedor.

En la siguiente imagen hemos colocado un borde naranja alrededor del *div* para apreciar mejor la zona donde se coloca el *background*:

El event handler que tenemos que crear en JavaScript para hacer el cambio sería:

(function(){

  var state = "on";

  $("#switch").click( function () { 
    if ( state === "on" ) {
       $("#switch").css("background-position","0 0");
       state = "off";
    }
    else {
       $("#switch").css("background-position","0 35px");
       state = "on";
    }
  });
})();

Cuando hay un click cambiamos la posición del background para que se vea el botón contrario:

Hay que tener cuidado al definir la anchura y altura del div porque si lo hacemos mal se verán más imagenes del sprite. En el siguiente CSS aumentamos la altura:

//demasiada altura, se ve el segundo botón  
#switch3 {
  border: 1px solid orange;
  width: 70px;
  height: 50px;
  background-image: url("images/switch_Sprite.png");
  background-position: 0 0;
}

El resultado es:

Al utilizar sprites es muy importante ajustar el tamaño del contenedor para que quepa justo la imagen que queremos presentar.

1 comentario: