Introduction
En 2010, F-i.com et l'équipe Google Chrome ont collaboré sur une application Web éducative basée sur HTML5 appelée 20 Things I Learned about Browsers and the Web (www.20thingsilearned.com). L'une des idées clés de ce projet était qu'il serait préférable de le présenter dans le contexte d'un livre. Le contenu du livre portant principalement sur les technologies du Web ouvert, nous avons jugé important de rester fidèles à cet esprit en faisant du conteneur lui-même un exemple de ce que ces technologies nous permettent d'accomplir aujourd'hui.

Nous avons décidé que la meilleure façon de recréer la sensation d'un livre physique était de simuler les aspects positifs de l'expérience de lecture analogique, tout en tirant parti des avantages du numérique dans des domaines tels que la navigation. Nous avons déployé beaucoup d'efforts pour le traitement graphique et interactif du flux de lecture, en particulier pour la façon dont les pages des livres se tournent.
Premiers pas
Ce tutoriel vous explique comment créer votre propre effet de page tournée à l'aide de l'élément canvas et de nombreux éléments JavaScript. Une partie du code rudimentaire, comme les déclarations de variables et l'abonnement aux écouteurs d'événements, a été omise des extraits de code de cet article. N'oubliez donc pas de vous référer à l'exemple fonctionnel.
Avant de commencer, il est judicieux de consulter la démo pour savoir ce que nous allons créer.
Annoter
Il est important de se rappeler que ce que nous dessinons sur le canevas ne peut pas être indexé par les moteurs de recherche, sélectionné par un visiteur ni trouvé par les recherches dans le navigateur. C'est pourquoi le contenu avec lequel nous allons travailler est placé directement dans le DOM, puis manipulé par JavaScript s'il est disponible. Le balisage requis est minimal :
<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>
Nous avons un élément de conteneur principal pour le livre, qui contient à son tour les différentes pages de notre livre et l'élément canvas
sur lequel nous allons dessiner les pages qui se tournent. Dans l'élément section
, il existe un wrapper div
pour le contenu. Nous en avons besoin pour pouvoir modifier la largeur de la page sans affecter la mise en page de son contenu. La largeur de div
est fixe et section
est défini pour masquer son contenu qui dépasse. La largeur de section
sert donc de masque horizontal pour div
.

Logique
Le code nécessaire pour faire tourner la page n'est pas très complexe, mais il est assez étendu, car il implique de nombreux graphiques générés de manière procédurale. Commençons par examiner la description des valeurs constantes que nous utiliserons tout au long du code.
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;
Le CANVAS_PADDING
est ajouté autour du canevas afin que le papier puisse s'étendre au-delà du livre lors du changement de page. Notez que certaines constantes définies ici sont également définies dans le CSS. Par conséquent, si vous souhaitez modifier la taille du livre, vous devrez également mettre à jour les valeurs correspondantes.

Nous devons ensuite définir un objet d'inversion pour chaque page. Ces objets seront constamment mis à jour à mesure que nous interagissons avec le livre pour refléter l'état actuel de l'inversion.
// 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
});
}
Nous devons d'abord nous assurer que les pages sont correctement superposées en organisant les z-index des éléments de section de sorte que la première page soit en haut et la dernière en bas. Les propriétés les plus importantes des objets d'inversion sont les valeurs progress
et target
.
Elles permettent de déterminer le degré de pliage de la page. -1 signifie que la page est pliée complètement à gauche, 0 qu'elle est pliée au centre du livre et +1 qu'elle est pliée complètement à droite.

Maintenant que nous avons défini un objet de retournement pour chaque page, nous devons commencer à capturer et à utiliser les saisies des utilisateurs pour mettre à jour l'état du retournement.
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 fonction mouseMoveHandler
met à jour l'objet mouse
afin que nous travaillions toujours sur l'emplacement du curseur le plus récent.
Dans mouseDownHandler
, nous commençons par vérifier si la souris a été enfoncée sur la page de gauche ou de droite afin de savoir dans quelle direction nous voulons commencer à tourner les pages. Nous nous assurons également qu'une autre page existe dans cette direction, car nous pouvons être sur la première ou la dernière page. Si une option d'inversion valide est disponible après ces vérifications, nous définissons l'indicateur dragging
de l'objet d'inversion correspondant sur true
.
Une fois que nous avons atteint le mouseUpHandler
, nous parcourons tous les flips
et vérifions si l'un d'eux a été signalé comme dragging
et doit maintenant être libéré. Lorsqu'un flip est relâché, nous définissons sa valeur cible pour qu'elle corresponde au côté vers lequel il doit basculer en fonction de la position actuelle de la souris.
Le numéro de page est également mis à jour pour refléter cette navigation.
Affichage
Maintenant que la majeure partie de notre logique est en place, nous allons voir comment afficher le papier plié sur l'élément canvas. La plupart de ces opérations se déroulent dans la fonction render()
, qui est appelée 60 fois par seconde pour mettre à jour et dessiner l'état actuel de tous les flips actifs.
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 );
}
}
}
Avant de commencer le rendu de flips
, nous réinitialisons le canevas à l'aide de la méthode clearRect(x,y,w,h)
. Effacer l'intégralité du canevas a un coût important en termes de performances. Il serait beaucoup plus efficace d'effacer uniquement les régions sur lesquelles nous dessinons. Pour rester concentrés sur le sujet de ce tutoriel, nous allons nous contenter d'effacer l'intégralité du canevas.
Si un flip est en cours de déplacement, nous mettons à jour sa valeur target
pour qu'elle corresponde à la position de la souris, mais sur une échelle de -1 à 1 plutôt qu'en pixels réels.
Nous incrémentons également progress
d'une fraction de la distance par rapport à target
. Cela permet une progression fluide et animée de l'inversion, car elle est mise à jour à chaque frame.
Comme nous parcourons tous les flips
à chaque frame, nous devons nous assurer de ne redessiner que ceux qui sont actifs. Si un retournement de page n'est pas très proche du bord du livre (à moins de 0,3 % de BOOK_WIDTH
) ou s'il est signalé comme dragging
, il est considéré comme actif.
Maintenant que toute la logique est en place, nous devons dessiner la représentation graphique d'un flip en fonction de son état actuel. Il est temps d'examiner la première partie de la fonction 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';
Cette section du code commence par calculer un certain nombre de variables visuelles dont nous avons besoin pour dessiner le pli de manière réaliste. La valeur progress
de l'inversion que nous dessinons joue un rôle important ici, car c'est là que nous voulons que le pli de la page apparaisse. Pour ajouter de la profondeur à l'effet de page qui se tourne, nous faisons en sorte que le papier dépasse des bords supérieur et inférieur du livre. Cet effet est à son apogée lorsqu'une page est proche de la reliure du livre.

Maintenant que toutes les valeurs sont préparées, il ne reste plus qu'à dessiner le papier.
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();
La méthode translate(x,y)
de l'API Canvas est utilisée pour décaler le système de coordonnées afin que nous puissions dessiner notre page en utilisant le haut de la reliure comme position 0,0. Notez que nous devons également save()
la matrice de transformation actuelle du canevas et restore()
à celle-ci lorsque nous avons terminé de dessiner.

Le foldGradient
est ce qui remplira la forme du papier plié pour lui donner des reflets et des ombres réalistes. Nous ajoutons également une ligne très fine autour du dessin sur papier pour que le papier ne disparaisse pas sur les arrière-plans clairs.
Il ne reste plus qu'à dessiner la forme du papier plié à l'aide des propriétés que nous avons définies ci-dessus. Les côtés gauche et droit de notre feuille sont dessinés sous forme de lignes droites, tandis que les côtés supérieur et inférieur sont incurvés pour donner l'impression d'une feuille pliée. La résistance de ce pliage du papier est déterminée par la valeur verticalOutdent
.
Et voilà ! Vous disposez désormais d'une navigation par page entièrement fonctionnelle.
Démonstration du retournement de page
L'effet de changement de page vise à communiquer la bonne sensation interactive. Par conséquent, le fait de regarder des images de cet effet ne lui rend pas vraiment justice.
Étapes suivantes

Il ne s'agit que d'un exemple de ce qui peut être accompli en utilisant les fonctionnalités HTML5 telles que l'élément canvas. Je vous recommande de consulter l'expérience de livre plus raffinée dont cette technique est un extrait sur www.20thingsilearned.com. Vous y verrez comment les pages peuvent être retournées dans une application réelle et à quel point cette technique devient puissante lorsqu'elle est associée à d'autres fonctionnalités HTML5.
Références
- Spécification de l'API Canvas