Caso de éxito: Efecto de cambio de página de 20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

Introducción

En 2010, F-i.com y el equipo de Google Chrome colaboraron en una app web educativa basada en HTML5 llamada 20 Things I Learned about Browsers and the Web (www.20thingsilearned.com). Una de las ideas clave detrás de este proyecto era que se presentaría mejor en el contexto de un libro. Dado que el contenido del libro se relaciona, en gran medida, con tecnologías web abiertas, creemos que era importante ser fieles a eso haciendo del contenedor un ejemplo de lo que estas tecnologías nos permiten lograr en la actualidad.

Portada del libro y página de inicio de "20 Things I Learned About navegadors and the Web" (20 cosas que aprendí de los navegadores y la web)
Portada del libro y página principal de "20 Things I Learned About Browsers and the Web" (www.20thingsilearned.com)

Decidimos que la mejor manera de lograr la sensación de un libro del mundo real es simular las partes buenas de la experiencia de lectura analógica y, al mismo tiempo, aprovechar los beneficios del dominio digital en áreas como la navegación. Se dedicó mucho esfuerzo al tratamiento interactivo y gráfico del flujo de lectura, especialmente en cómo las páginas de los libros pasan de una página a otra.

Cómo comenzar

En este instructivo, te explicaremos el proceso de crear tu propio efecto de cambio de página con el elemento lienzo y mucho JavaScript. Parte del código rudimentario, como las declaraciones de variables y la suscripción a objetos de escucha de eventos, se excluyeron de los fragmentos de este artículo, así que recuerda consultar el ejemplo funcional.

Antes de comenzar, te recomendamos consultar la demostración para que sepas lo que nuestro objetivo es crear.

Marca

Siempre es importante recordar que lo que dibujamos en el lienzo no puede ser indexado por los motores de búsqueda, seleccionados por un visitante ni encontrados por las búsquedas en el navegador. Por esa razón, el contenido con el que trabajaremos se coloca directamente en el DOM y, luego, JavaScript, si está disponible. El lenguaje de marcado requerido para esto 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>

Tenemos un elemento contenedor principal para el libro que, a su vez, contiene las diferentes páginas del libro y el elemento canvas en el que dibujaremos las páginas. Dentro del elemento section hay un wrapper div para el contenido. Lo necesitamos para poder cambiar el ancho de la página sin afectar el diseño de su contenido. div tiene un ancho fijo y section está configurado para ocultar su desbordamiento, lo que hace que el ancho de section actúe como una máscara horizontal para div.

Abrir Libro.
Al elemento de libro se agregó una imagen de fondo que contiene la textura de papel y la sobrecubierta marrón.

Lógica

El código necesario para activar el cambio de página no es muy complejo, pero es bastante extenso, ya que involucra muchos gráficos generados mediante procedimientos. Comencemos por revisar la descripción de los valores constantes que usaremos en todo 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;

Se agrega el objeto CANVAS_PADDING alrededor del lienzo para que el papel se extienda fuera del libro cuando se gire. Ten en cuenta que algunas constantes definidas aquí también se configuran en CSS, por lo que si quieres cambiar el tamaño del libro, también deberás actualizar los valores allí.

Constantes.
Los valores constantes que se usan en todo el código para hacer un seguimiento de la interacción y dibujar el cambio de página.

A continuación, debemos definir un objeto de vuelta para cada página. Estos se actualizarán constantemente a medida que interactúemos con el libro para reflejar el estado actual de la vuelta.

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

Primero, debemos asegurarnos de que las páginas estén en capas correctamente. Para ello, debemos organizar los índices z de los elementos de la sección de modo que la primera página esté en la parte superior y la última esté en la parte inferior. Las propiedades más importantes de los objetos de vuelta son los valores progress y target. Estos se usan para determinar hasta qué punto se debe doblar la página actualmente, -1 significa toda la orientación hacia la izquierda, 0 significa el centro muerto del libro y +1 significa el borde más derecho del libro.

Progreso.
El progreso y los valores objetivo de las vueltas se usan para determinar dónde se debe dibujar la página plegable en una escala de -1 a +1.

Ahora que tenemos un objeto de giro definido para cada página, debemos comenzar a capturar y usar la entrada del usuario para actualizar el estado de la vuelta.

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 &amp;&amp; page - 1 >= 0) {
    // We are on the left side, drag the previous page
    flips[page - 1].dragging = true;
}
else if (mouse.x > 0 &amp;&amp; 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 hacia la ubicación más reciente del cursor.

En mouseDownHandler, comenzamos por verificar si se presionó el mouse en la página izquierda o derecha para saber en qué dirección queremos comenzar a girar. También nos aseguramos de que otra página exista en esa dirección, ya que es posible que estemos en la primera o la última página. Si hay una opción de giro válida disponible después de estas verificaciones, establecemos la marca dragging del objeto de giro correspondiente en true.

Una vez que llegamos al elemento mouseUpHandler, revisamos todos los flips y verificamos si alguno se marcó como dragging y ahora debería lanzarse. Cuando se suelta un giro, configuramos su valor objetivo para que coincida con el lado al que debería girar, según la posición actual del mouse. El número de página también se actualiza para reflejar esta navegación.

Renderización

Ahora que la mayor parte de nuestra lógica está implementada, veremos cómo renderizar el papel plegable en el elemento de lienzo. La mayor parte de esto sucede dentro de la función render(), que se llama 60 veces por segundo para actualizar y dibujar el estado actual de todos los giros 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 comenzar a renderizar el flips, restablecemos el lienzo con el método clearRect(x,y,w,h). Borrar todo el lienzo implica un gran gasto de rendimiento, y sería mucho más eficiente borrar solo las regiones que necesitamos. Para mantener este instructivo en el tema, se quitará todo el lienzo.

Si se arrastra un giro, actualizamos su valor target para que coincida con la posición del mouse, pero en una escala de -1 a 1 en lugar de píxeles reales. También incrementamos progress en una fracción de la distancia al target, lo que generará una progresión fluida y animada del cambio, ya que se actualiza en cada fotograma.

Dado que revisaremos todos los flips de cada fotograma, debemos asegurarnos de volver a dibujar solo los que están activos. Si un giro no está muy cerca del borde del libro (dentro del 0.3% de BOOK_WIDTH) o si está marcado como dragging, se considera activo.

Ahora que toda la lógica está implementada, debemos dibujar la representación gráfica de una vuelta según su estado actual. Es hora de ver 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 comienza calculando una serie de variables visuales que necesitamos para dibujar el pliegue de manera realista. El valor progress de la vuelta que estamos dibujando juega un papel importante aquí, ya que es ahí donde queremos que aparezca el pliegue de página. Para agregar profundidad al efecto de giro de página, hacemos que el papel se extienda fuera de los bordes inferior y superior del libro. Este efecto se encuentra en su punto máximo cuando una vuelta está cerca del lomo del libro.

Cambiar
Así se ve el pliegue de página cuando la página se da vuelta o se arrastra.

Ahora que todos los valores están preparados, ¡lo único que queda es 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 &amp; 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) de la API de lienzo se usa para desplazar el sistema de coordenadas, de modo que podamos dibujar el cambio de página con la parte superior del lomo como la posición 0,0. Ten en cuenta que también debemos aplicar save() la matriz de transformación actual del lienzo y restore() cuando terminemos de dibujar.

Traductor
Este es el punto desde el que dibujamos el salto de página. El punto 0,0 original se encuentra en la parte superior izquierda de la imagen, pero, si lo cambias con translate(x,y), simplificamos la lógica de dibujo.

foldGradient es con lo que llenaremos la forma del papel plegado para darle sombras y reflejos realistas. También agregamos una línea muy delgada alrededor del dibujo en papel para que el papel no desaparezca cuando se lo coloque sobre fondos claros.

Lo que resta ahora es dibujar la forma del papel plegado con las propiedades que definimos anteriormente. Los lados izquierdo y derecho de nuestro papel se dibujan como líneas rectas, y la parte superior y la inferior son curvas para transmitir la sensación de doblaje de papel. La resistencia de esta curva de papel se determina mediante el valor verticalOutdent.

Listo. Ahora tienes una navegación de cambio de página completamente funcional.

Demostración de cambio de página

El efecto de cambio de página consiste en comunicar el sentimiento interactivo adecuado, por lo que mirar imágenes no le hace justicia.

Próximos pasos

Funda de tapa dura
El cambio de página virtual de este instructivo es aún más eficaz cuando se combina con otras funciones similares a las de los libros, como la tapa dura interactiva.

Este es solo un ejemplo de lo que se puede lograr mediante el uso de funciones HTML5, como el elemento de lienzo. Te recomendamos que consultes la experiencia de libros más refinada de la que esta técnica es un extracto en: www.20thingsilearned.com. Allí verás cómo se pueden aplicar los giros de página en una aplicación real y la eficacia que resulta cuando se combina con otras funciones HTML5.

Referencias

  • Especificación de la API de Canvas