Caso práctico: el efecto de pasar la página en el sitio 20thingsilearned.com

HTML5 Rocks

Introducción

En 2010, la empresa F-i.com colaboró con el equipo de Google Chrome en la creación de una aplicación web educativa basada en HTML5 llamada "20 cosas que he aprendido sobre Internet y los navegadores" (www.20thingsilearned.com). Una de las ideas fundamentales de este proyecto es que el proyecto se debía presentar con la apariencia de un libro. Dado que el contenido del libro trata principalmente sobre tecnologías web de código abierto, consideramos que era importante mantenernos fieles a esa idea creando algo que fuera un ejemplo de lo que estas tecnologías nos permiten hacer actualmente.

Cubierta del libro y página de inicio de
Cubierta del libro y página de inicio de "20 cosas que he aprendido sobre Internet y los navegadores" (www.20thingsilearned.com)

Decidimos que la mejor forma de conseguir la apariencia de un libro real era simular las partes positivas de la experiencia de lectura análoga y aprovechar al mismo tiempo las ventajas del mundo digital en determinados aspectos, como el de la navegación. Pusimos mucho esfuerzo en el tratamiento interactivo y gráfico de la fluidez de lectura, especialmente en la forma de pasar las páginas.

Introducción

Este tutorial te guiará a través del proceso de creación del efecto de pasar una página mediante el elemento canvas y una cantidad considerable de código JavaScript. Hay cierto código básico que no hemos incluido en los fragmentos de código de este artículo como, por ejemplo, las declaraciones de variables y las suscripciones de detectores de eventos, por lo que te recomendamos que consultes el ejemplo práctico.

Antes de empezar, te recomendamos que consultes la demostración para que te hagas una idea de lo que vamos a crear.

Marcado

Siempre es importante recordar que el contenido que se crea con el elemento canvas no lo pueden indexar los motores de búsqueda, no pueden seleccionarlo los usuarios y no se puede localizar mediante búsquedas en el navegador. Por esta razón, el contenido con el que trabajaremos se coloca directamente en el DOM y, posteriormente, se manipula con JavaScript, si está disponible. El marcado que se requiere para esta operación es mínimo.

<div id="book">
  <canvas id="pageflip-canvas"></canvas>
  <div id="pages">
    <section>
      <div> <!-- Any type of contents here --> </div>
    </section>
    <!-- More <section>'s here -->
  </div>
</div>

Contamos con un elemento de contenedor principal para el libro, que a su vez incluye las diferentes páginas del libro y el elemento canvas sobre el que dibujaremos las páginas que pasaremos. El elemento section incluye una etiqueta div para el contenido, que necesitamos para poder cambiar el ancho de la página sin que el diseño del contenido se vea afectado. La etiqueta div tiene un ancho fijo y el elemento section se define para ocultar el desbordamiento, por lo que el ancho del elemento section actúa como una máscara horizontal para el envoltorio div.

Libro abierto
Al elemento de libro se le añade una imagen de fondo que incluye la textura de papel y una sobrecubierta marrón.

Lógica

El código necesario para pasar páginas no es muy complejo pero sí bastante extenso, ya que incluye una gran cantidad de gráficos generados de forma procedimental. Empezaremos describiendo los valores de constantes que vamos a utilizar en el código.

var BOOK_WIDTH = 830;
var BOOK_HEIGHT = 260;
var PAGE_WIDTH = 400;
var PAGE_HEIGHT = 250;
var PAGE_Y = ( BOOK_HEIGHT - PAGE_HEIGHT ) / 2;
var CANVAS_PADDING = 60;

La variable CANVAS_PADDING se añade alrededor del elemento canvas para que podamos extender el papel fuera del libro al pasar las páginas. Ten en cuenta que algunas de las constantes que se definen en este documento también se establecen en el código CSS. Por tanto, si quieres cambiar el tamaño del libro, también tendrás que actualizar esos valores.

Constantes
Valores de constantes que se utilizan en el código para realizar un seguimiento de la interacción y dibujar el paso de página

A continuación, se debe definir un objeto de paso de página para cada página, que se actualizará constantemente a medida que interactuemos con el libro para reflejar el estado actual de paso de página.

// Create a reference to the book container element
var book = document.getElementById( "book" );

// Grab a list of all section elements (pages) within the book
var pages = book.getElementsByTagName( "section" );

for( var i = 0, len = pages.length; i < len; i++ ) {
    pages[i].style.zIndex = len - i;

    flips.push( {
    progress: 1,
    target: 1,
    page: pages[i],
    dragging: false
  });
}

En primer lugar, debemos asegurarnos de que las páginas estén superpuestas correctamente. Para ello, se deben organizar los índices z de los elementos de sección de forma que la primera página quede en la parte superior y la última página en la parte inferior. Las propiedades más importantes de los objetos de paso de página son los valores de progress y target. Estos valores se utilizan para determinar hasta dónde se debe doblar la página: -1 indica que se debe doblar totalmente hacia la izquierda, 0 indica que el libro se debe abrir por la mitad y +1 indica que se debe doblar totalmente hacia la derecha.

Progress
Los valores de "progress" y "target" de los objetos de paso de página se utilizan para determinar dónde se debe dibujar el doblez de página en una escala de -1 a +1.

Una vez que hemos definido un objeto de paso de página para cada página, debemos empezar a capturar y a utilizar la información que introducen los usuarios para actualizar el estado de paso de página.

function mouseMoveHandler( event ) {
  // Offset mouse position so that the top of the book spine is 0,0
  mouse.x = event.clientX - book.offsetLeft - ( BOOK_WIDTH / 2 );
  mouse.y = event.clientY - book.offsetTop;
}

function mouseDownHandler( event ) {
  // Make sure the mouse pointer is inside of the book
  if (Math.abs(mouse.x) < PAGE_WIDTH) {
    if (mouse.x < 0 && page - 1 >= 0) {
      // We are on the left side, drag the previous page
      flips[page - 1].dragging = true;
    }
    else if (mouse.x > 0 && page + 1 < flips.length) {
      // We are on the right side, drag the current page
      flips[page].dragging = true;
    }
  }

  // Prevents the text selection
  event.preventDefault();
}

function mouseUpHandler( event ) {
  for( var i = 0; i < flips.length; i++ ) {
    // If this flip was being dragged, animate to its destination
    if( flips[i].dragging ) {
      // Figure out which page we should navigate to
      if( mouse.x < 0 ) {
        flips[i].target = -1;
        page = Math.min( page + 1, flips.length );
      }
      else {
        flips[i].target = 1;
        page = Math.max( page - 1, 0 );
      }
    }

    flips[i].dragging = false;
  }
}

La función mouseMoveHandler actualiza el objeto mouse para que siempre trabajemos con la ubicación más reciente del cursor.

Con la función mouseDownHandler, comprobamos si el ratón se ha pulsado en la página izquierda o derecha para saber hacia qué dirección queremos empezar a pasar la página. También debemos asegurarnos de que haya alguna otra página en esa dirección, ya que podríamos encontrarnos en la primera página o en la última. Si hay una opción de paso de página válida disponible después de efectuar estas comprobaciones, establecemos el indicador dragging del objeto de paso de página correspondiente en true.

Una vez que hemos definido la función mouseUpHandler, comprobamos todos los valores de objetos flip para ver si hay alguno que esté marcado como dragging y que, por tanto, se deba liberar. Cuando se libera un objeto de paso de página, se define su valor de "target" de forma que coincida con la dirección hacia la que se debe pasar la página en función de la posición actual del ratón. El número de página también se actualiza para reflejar este desplazamiento.

Representación

Una vez que hemos establecido la mayor parte de la lógica, vamos a ver cómo se representa el papel que se dobla en el elemento canvas. La mayor parte de este proceso se desarrolla dentro de la función render(), que se activa 60 veces por segundo para actualizar y dibujar el estado actual de todos los objetos de paso de página activos.

function render() {
  // Reset all pixels in the canvas
  context.clearRect( 0, 0, canvas.width, canvas.height );

  for( var i = 0, len = flips.length; i < len; i++ ) {
    var flip = flips[i];

    if( flip.dragging ) {
      flip.target = Math.max( Math.min( mouse.x / PAGE_WIDTH, 1 ), -1 );
    }

    // Ease progress towards the target value
    flip.progress += ( flip.target - flip.progress ) * 0.2;

    // If the flip is being dragged or is somewhere in the middle
    // of the book, render it
    if( flip.dragging || Math.abs( flip.progress ) < 0.997 ) {
      drawFlip( flip );
    }

  }
}

Antes de iniciar la representación de los objetos flip, restablecemos el elemento canvas con el método clearRect(x,y,w,h). Restablecer todo el elemento canvas supone demasiado trabajo, así que puede resultar mucho más eficaz eliminar únicamente las regiones en las que estamos dibujando. Sin embargo, para no desviarnos del tema de este tutorial, vamos a restablecer todo el elemento canvas.

Si se efectúa un movimiento de paso de página, actualizamos el valor de target de forma que coincida con la posición del ratón en una escala de -1 a 1 (en lugar de referirnos a píxeles reales). También incrementamos el valor de progress en una fracción de la distancia al valor de target. De esta forma, se obtiene una progresión suave y animada del movimiento de paso de página, ya que este se actualiza en cada marco.

Como estamos revisando todos los objetos flip de cada marco, debemos asegurarnos de volver a dibujar únicamente los que estén activos. Se considera que el objeto "flip" está activo si no se encuentra demasiado próximo al borde del libro (dentro de un 0,3% del valor de BOOK_WIDTH) o si está marcado como dragging.

Una vez establecida toda la lógica, tenemos que dibujar la representación gráfica del movimiento de paso de página en función de su estado actual. Veamos la primera parte de la función drawFlip(flip).

// Determines the strength of the fold/bend on a 0-1 range
var strength = 1 - Math.abs( flip.progress );

// Width of the folded paper
var foldWidth = ( PAGE_WIDTH * 0.5 ) * ( 1 - flip.progress );

// X position of the folded paper
var foldX = PAGE_WIDTH * flip.progress + foldWidth;

// How far outside of the book the paper is bent due to perspective
var verticalOutdent = 20 * strength;

// The maximum widths of the three shadows used
var paperShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(1 - flip.progress, 0.5), 0);
var rightShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);
var leftShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);

// Mask the page by setting its width to match the foldX
flip.page.style.width = Math.max(foldX, 0) + "px";

Esta sección del código empieza con el cálculo de un número de variables visuales que necesitamos para representar el plegado de una forma realista. El valor de progress del movimiento de paso de página que estamos dibujando desempeña un papel importante, ya que representa el lugar en el que queremos que se efectúe el plegado de la página. Para añadir profundidad al efecto de pasar la página, hacemos que el papel se extienda fuera de los bordes superior e inferior del libro. La parte más importante de este efecto se produce cuando la página que se pasa está cerca de la parte central del libro.

Paso de página
Aspecto de la página cuando se pasa o se arrastra

Una vez preparados todos los valores, solo queda dibujar el papel.

context.save();
context.translate( CANVAS_PADDING + ( BOOK_WIDTH / 2 ), PAGE_Y + CANVAS_PADDING );

// Draw a sharp shadow on the left side of the page
context.strokeStyle = 'rgba(0,0,0,'+(0.05 * strength)+')';
context.lineWidth = 30 * strength;
context.beginPath();
context.moveTo(foldX - foldWidth, -verticalOutdent * 0.5);
context.lineTo(foldX - foldWidth, PAGE_HEIGHT + (verticalOutdent * 0.5));
context.stroke();

// Right side drop shadow
var rightShadowGradient = context.createLinearGradient(foldX, 0,
              foldX + rightShadowWidth, 0);
rightShadowGradient.addColorStop(0, 'rgba(0,0,0,'+(strength*0.2)+')');
rightShadowGradient.addColorStop(0.8, 'rgba(0,0,0,0.0)');

context.fillStyle = rightShadowGradient;
context.beginPath();
context.moveTo(foldX, 0);
context.lineTo(foldX + rightShadowWidth, 0);
context.lineTo(foldX + rightShadowWidth, PAGE_HEIGHT);
context.lineTo(foldX, PAGE_HEIGHT);
context.fill();

// Left side drop shadow
var leftShadowGradient = context.createLinearGradient(
    foldX - foldWidth - leftShadowWidth, 0, foldX - foldWidth, 0);
leftShadowGradient.addColorStop(0, 'rgba(0,0,0,0.0)');
leftShadowGradient.addColorStop(1, 'rgba(0,0,0,'+(strength*0.15)+')');

context.fillStyle = leftShadowGradient;
context.beginPath();
context.moveTo(foldX - foldWidth - leftShadowWidth, 0);
context.lineTo(foldX - foldWidth, 0);
context.lineTo(foldX - foldWidth, PAGE_HEIGHT);
context.lineTo(foldX - foldWidth - leftShadowWidth, PAGE_HEIGHT);
context.fill();

// Gradient applied to the folded paper (highlights & shadows)
var foldGradient = context.createLinearGradient(
    foldX - paperShadowWidth, 0, foldX, 0);
foldGradient.addColorStop(0.35, '#fafafa');
foldGradient.addColorStop(0.73, '#eeeeee');
foldGradient.addColorStop(0.9, '#fafafa');
foldGradient.addColorStop(1.0, '#e2e2e2');

context.fillStyle = foldGradient;
context.strokeStyle = 'rgba(0,0,0,0.06)';
context.lineWidth = 0.5;

// Draw the folded piece of paper
context.beginPath();
context.moveTo(foldX, 0);
context.lineTo(foldX, PAGE_HEIGHT);
context.quadraticCurveTo(foldX, PAGE_HEIGHT + (verticalOutdent * 2),
                         foldX - foldWidth, PAGE_HEIGHT + verticalOutdent);
context.lineTo(foldX - foldWidth, -verticalOutdent);
context.quadraticCurveTo(foldX, -verticalOutdent * 2, foldX, 0);

context.fill();
context.stroke();

context.restore();

El método translate(x,y) del API de canvas se utiliza para adaptar el sistema de coordenadas de forma que podamos dibujar el paso de página tomando la parte superior de la parte central del libro como posición 0,0. Ten en cuenta que también tenemos que guardar (método save()) la matriz de transformación actual del elemento canvas y restablecerla (método restore()) cuando acabemos de dibujar.

Translate
A partir de este punto, empezamos a dibujar el movimiento de paso de página. El punto 0,0 original se encuentra en la parte superior izquierda de la imagen, pero si lo cambiamos con el método translate(x,y), simplificamos la lógica del dibujo.

La variable foldGradient representa aquello con lo que rellenamos la forma del papel doblado para darle un efecto realista de luces y sombras. También añadimos una línea muy fina alrededor del dibujo del papel para que este no desaparezca cuando se contraponga a un fondo claro.

Ya solo nos queda dibujar la forma del papel doblado con las propiedades anteriormente definidas. Los lados derecho e izquierdo del papel se representan como líneas rectas, mientras que los lados superior e inferior son curvos para crear el efecto de un papel que se dobla. La curvatura del papel se determina con el valor de verticalOutdent.

Eso es todo. Ahora ya puedes utilizar el efecto de pasar la página de forma totalmente funcional.

Demostración de paso de página

Para comprobar el efecto de pasar la página, se debe experimentar la sensación interactiva adecuada, por lo que ver imágenes de este efecto no es suficiente. Por ello, te recomendamos que utilices los enlaces que aparecen a continuación para probar el resultado final.

Ver ejemplo práctico

Descargar código fuente (archivo .zip de 75 k)

Siguientes pasos

Pasar una cubierta dura
Para experimentar aún más la sensación de movimiento, te recomendamos que combines el suave efecto de paso de una página con el movimiento de paso de otros elementos de los libros como, por ejemplo, una cubierta dura interactiva.

Este es solo un ejemplo de lo que se puede hacer con funciones de HTML5 (como la del elemento canvas). Te recomendamos que consultes la página que intenta recrear lo más fielmente posible la experiencia de lectura (y de donde hemos extraído esta técnica): www.20thingsilearned.com. En esta página podrás comprobar cómo se aplica el efecto de pasar páginas a una aplicación real y la potencia de esta práctica cuando se utiliza con otras funciones de HTML5.

Referencias

  • Especificación del API de canvas

Comments

0