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 fue que se presentaría mejor en el contexto de un libro. Dado que el contenido del libro se centra en gran medida en las tecnologías de la Web abierta, consideramos que era importante mantenernos fieles a ese concepto y hacer que el contenedor en sí mismo fuera un ejemplo de lo que estas tecnologías nos permiten lograr hoy en día.

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 ámbito digital en áreas como la navegación. Se dedicó mucho esfuerzo al tratamiento gráfico e interactivo del flujo de lectura, en especial, a cómo se pasan las páginas de los libros.
Comenzar
En este instructivo, se explica el proceso para crear tu propio efecto de cambio de página con el elemento canvas y mucho JavaScript. Parte del código rudimentario, como las declaraciones de variables y la suscripción al objeto de escucha de eventos, se omitió de los fragmentos de este artículo, por lo que te recomendamos que consultes el ejemplo de trabajo.
Antes de comenzar, es recomendable que veas la demostración para que sepas qué queremos crear.
Marca
Siempre es importante recordar que los motores de búsqueda no pueden indexar lo que dibujamos en el lienzo, ni los visitantes pueden seleccionarlo ni se puede encontrar con las búsquedas en el navegador. Por ese motivo, el contenido con el que trabajaremos se coloca directamente en el DOM y, luego, se manipula con 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 que se voltean. 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. El div
tiene un ancho fijo y el section
está configurado para ocultar su desbordamiento, lo que hace que el ancho del section
actúe como una máscara horizontal para el div
.

Lógica
El código necesario para activar el cambio de página no es muy complejo, pero sí bastante extenso, ya que incluye muchos gráficos generados de forma procedimental. Comencemos por analizar 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;
El CANVAS_PADDING
se agrega alrededor del lienzo para que el papel se extienda fuera del libro cuando se lo da vuelta. Ten en cuenta que algunas de las constantes definidas aquí también se establecen en CSS, por lo que, si deseas cambiar el tamaño del libro, también deberás actualizar los valores allí.

A continuación, debemos definir un objeto de cambio de página para cada página. Estos se actualizarán constantemente a medida que interactuemos con el libro para reflejar el estado actual del cambio 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
});
}
Primero, debemos asegurarnos de que las páginas estén dispuestas correctamente organizando 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 en la parte inferior. Las propiedades más importantes de los objetos de inversión son los valores progress
y target
.
Se usan para determinar qué tan doblada debería estar la página actualmente. -1 significa que está doblada por completo hacia la izquierda, 0 significa el centro exacto del libro y +1 significa el borde más a la derecha del libro.

Ahora que tenemos un objeto de cambio definido para cada página, debemos comenzar a capturar y usar la entrada del usuario para actualizar el estado del cambio.
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 del cursor más reciente.
En mouseDownHandler
, primero verificamos si se presionó el mouse en la página izquierda o derecha para saber en qué dirección queremos comenzar a voltear la página. También nos aseguramos de que exista otra página en esa dirección, ya que podríamos estar en la primera o la última. Si hay una opción de cambio válida disponible después de estas verificaciones, establecemos la marca dragging
del objeto de cambio correspondiente en true
.
Una vez que llegamos al mouseUpHandler
, revisamos todos los flips
y verificamos si alguno de ellos se marcó como dragging
y ahora se debe lanzar. Cuando se suelta un flip, establecemos su valor objetivo para que coincida con el lado hacia el que debe voltearse 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á en su lugar, veremos cómo renderizar el papel plegado en el elemento canvas. La mayor parte de esto sucede dentro de la función render()
, a la 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 tiene un costo de rendimiento muy alto, y sería mucho más eficiente borrar solo las regiones en las que estamos dibujando. Para mantener este instructivo sobre el tema, lo dejaremos en borrar todo el lienzo.
Si se arrastra un giro, actualizamos su valor de 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 a target
. Esto generará una progresión fluida y animada del giro, ya que se actualiza en cada fotograma.
Dado que revisamos todos los flips
en cada fotograma, debemos asegurarnos de volver a dibujar solo los que están activos. Si un pliegue no está muy cerca del borde del libro (a menos del 0.3% de BOOK_WIDTH
) o si se marca como dragging
, se considera activo.
Ahora que toda la lógica está en su lugar, debemos dibujar la representación gráfica de un giro según su estado actual. Es hora de analizar 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';
En esta sección del código, se comienza por calcular una cantidad de variables visuales que necesitamos para dibujar el pliegue de manera realista. El valor progress
del giro que dibujamos juega un papel importante aquí, ya que es donde queremos que aparezca el pliegue de la página. Para agregar profundidad al efecto de cambio de página, hacemos que el papel se extienda fuera de los bordes superior e inferior del libro. Este efecto alcanza su punto máximo cuando el cambio está cerca del lomo del libro.

Ahora que todos los valores están preparados, 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)
de la API de Canvas 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 save()
la matriz de transformación actual del lienzo y restore()
a ella cuando terminemos de dibujar.

El foldGradient
es lo que usaremos para rellenar la forma del papel doblado y darle reflejos y sombras realistas. También agregamos una línea muy delgada alrededor del dibujo en papel para que no desaparezca cuando se coloque sobre fondos claros.
Todo lo que resta ahora es dibujar la forma del papel doblado con las propiedades que definimos anteriormente. Los lados izquierdo y derecho del papel se dibujan como líneas rectas, y los lados superior e inferior se curvan para transmitir la sensación de un papel doblado. La resistencia de este doblez de papel se determina con el valor de verticalOutdent
.
Eso es todo. 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 se trata de comunicar la sensación interactiva correcta, por lo que mirar imágenes de él no le hace justicia.
Próximos pasos

Este es solo un ejemplo de lo que se puede lograr con las funciones de HTML5, como el elemento canvas. Te recomiendo que veas la experiencia de libro más refinada de la que se extrajo esta técnica en www.20thingsilearned.com. Allí verás cómo se pueden aplicar los cambios de página en una aplicación real y lo potente que se vuelve cuando se combina con otras funciones de HTML5.
Referencias
- Especificación de la API de Canvas