Étude de cas : Effet de page tournée sur 20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

Présentation

En 2010, F-i.com et l'équipe Google Chrome ont collaboré sur une application Web éducative HTML5 appelée "20 Things I Learned about Browsers and the Web" (www.20thingsilearned.com). L'une des principales idées derrière ce projet était qu'il serait préférable de le présenter dans le contexte d'un livre. Étant donné que ce livre traite essentiellement des technologies Web ouvertes, nous avons estimé qu'il était important d'y rester fidèle en faisant du conteneur lui-même un exemple de ce que ces technologies nous permettent d'accomplir aujourd'hui.

Couverture du livre et page d'accueil du livre "20 choses à savoir sur les navigateurs et le Web"
Couverture du livre et page d'accueil du livre "20 choses à savoir sur les navigateurs et le Web" (www.20thingsilearned.com)

Nous avons décidé que la meilleure façon de donner l'impression d'un livre réel est de simuler les bonnes parties de l'expérience de lecture analogique tout en tirant parti des avantages du domaine numérique dans des domaines tels que la navigation. Le traitement graphique et interactif du flux de lecture a nécessité beaucoup d'efforts, en particulier la façon dont les pages des livres passent d'une page à l'autre.

Premiers pas

Ce tutoriel vous explique comment créer votre propre effet de retournement de page à l'aide de l'élément canevas et d'une grande quantité de code JavaScript. Une partie du code rudimentaire, comme les déclarations de variable et l'abonnement à l'écouteur d'événements, a été omise des extraits de cet article. N'oubliez donc pas de consulter l'exemple de travail.

Avant de commencer, il peut être judicieux de vérifier la version de démonstration afin de comprendre ce que nous nous efforçons de créer.

Markup

N'oubliez pas que ce que nous dessinons sur la toile ne peut pas être indexé par les moteurs de recherche, sélectionnés par un visiteur ni trouvé lors d'une recherche effectuée dans un navigateur. Pour cette raison, le contenu avec lequel nous allons travailler est placé directement dans le DOM, puis manipulé par JavaScript s'il est disponible. Le balisage requis pour cela 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 du livre et l'élément canvas sur lequel nous allons dessiner les pages à retourner. L'élément section contient 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. div a une largeur fixe et section est configuré pour masquer son dépassement. La largeur de section sert donc de masque horizontal pour la div.

Ouvrez le livre.
Une image de fond contenant la texture du papier et la pochette de livre marron est ajoutée à l'élément de livre.

Logique

Le code requis pour alimenter le saut de page n'est pas très complexe, mais il est assez conséquent, 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 dans le 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;

L'élément CANVAS_PADDING est ajouté autour de la toile afin que nous puissions étendre le papier à l'extérieur du livre lors du retournement. Notez que certaines des constantes définies ici sont également définies en CSS. Par conséquent, si vous souhaitez modifier la taille du livre, vous devez également mettre à jour les valeurs à cet endroit.

Constantes
Valeurs constantes utilisées dans le code pour suivre les interactions et faire défiler la page.

Nous devons ensuite définir un objet "flip" pour chaque page, qui sera constamment mis à jour à mesure que nous interagirons avec le livre afin de refléter l'état actuel du retournement.

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

Tout d'abord, nous devons nous assurer que les pages sont superposées correctement en organisant les z-index des éléments de la 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 "flip" sont les valeurs progress et target. Ces valeurs permettent de déterminer jusqu'à quelle distance la page doit être pliée : -1 correspond à l'extrémité gauche, 0 correspond au centre mort du livre et +1 à la bordure la plus à droite du livre.

Progression.
La progression et les valeurs cibles des retournements permettent de déterminer où la page pliée doit être dessinée sur une échelle -1 à +1.

Maintenant que nous avons défini un objet flip pour chaque page, nous devons commencer à capturer et à utiliser les entrées utilisateur pour mettre à jour l'état du flip.

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 fonction mouseMoveHandler met à jour l'objet mouse afin que nous travaillions toujours vers l'emplacement le plus récent du curseur.

Dans mouseDownHandler, nous commençons par vérifier si l'utilisateur a appuyé sur la gauche ou la droite de la page afin de savoir dans quelle direction nous allons commencer à retourner. 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 de retournement valide est disponible après ces vérifications, nous définissons l'indicateur dragging de l'objet de retournement correspondant sur true.

Une fois que nous avons atteint mouseUpHandler, nous parcourons tous les flips et vérifions si l'un d'entre eux a été signalé comme dragging et doit maintenant être publié. Lorsqu'un flipper est lancé, 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 la 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 pliant sur l'élément canevas. La plupart de ces opérations se produisent dans la fonction render(), qui est appelée 60 fois par seconde pour mettre à jour et dessiner l'état actuel de tous les inverses 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 à afficher flips, nous réinitialisons le canevas à l'aide de la méthode clearRect(x,y,w,h). Le nettoyage de l'ensemble de la toile entraîne des coûts de performances importants. Il serait beaucoup plus efficace de n'effacer que les régions sur lesquelles nous nous appuyons. Pour que ce tutoriel reste pertinent, nous allons le faire pour effacer l'ensemble du canevas.

Si vous faites glisser un flipper, nous mettons à jour sa valeur target pour qu'elle corresponde à la position de la souris, mais sur une échelle de -1 à 1 plutôt que sur les pixels réels. Nous incrémentons également la progress d'une fraction de la distance par rapport au target. Cela se traduit par une progression fluide et animée du retournement, car il se met à jour à chaque image.

Étant donné que nous passons en revue tous les flips sur chaque frame, nous devons nous assurer de ne redessiner que ceux qui sont actifs. Si un retournement 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 retournement en fonction de son état actuel. Examinons maintenant 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 du retournement 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 retournement de page, nous faisons en sorte que le papier s'étend en dehors des bords supérieur et inférieur du livre. Cet effet est à son maximum lorsqu'un retournement est proche de la tranche du livre.

Inverser
Voici à quoi ressemble le pli de la page lorsque la page est retournée ou déplacée.

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 &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();

La méthode translate(x,y) de l'API Canevas permet de décaler le système de coordonnées afin de pouvoir dessiner le retournement de page, le haut de la colonne représentant la position 0,0. Notez que nous devons également appliquer la fonction save() à la matrice de transformation actuelle du canevas et y appliquer restore() une fois le dessin terminé.

Traduction
C'est à partir de ce point que nous allons commencer à effectuer le saut de page. Le point 0,0 d'origine se trouve en haut à gauche de l'image, mais en le modifiant, via translation(x,y), nous simplifions la logique de dessin.

foldGradient est ce que nous allons remplir dans la forme du papier plié pour lui donner des tons clairs et des ombres réalistes. Nous ajoutons également une ligne très fine autour du dessin sur papier afin que celui-ci ne disparaisse pas lorsque vous le placez sur des fonds 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 papier sont dessinés sous forme de lignes droites, et les côtés supérieur et inférieur sont incurvés pour donner l'impression de plier un papier plié. L'intensité de ce pli du papier est déterminée par la valeur verticalOutdent.

Et voilà ! Vous disposez désormais d'une navigation de retournement de page entièrement fonctionnelle.

Démo du retournement de page

L'effet de page tournée consiste à communiquer la bonne sensation d'interaction. Regarder des images de celui-ci n'est donc pas vraiment justifié.

Étapes suivantes

Lancer le retournement dur
Le retournement de page de ce tutoriel est encore plus efficace lorsqu'il est associé à d'autres fonctionnalités semblables à celles d'un livre, comme une couverture rigide interactive.

Ceci n'est qu'un exemple de ce qu'il est possible d'accomplir en utilisant les fonctionnalités HTML5, telles que l'élément canevas. Nous vous recommandons de découvrir l'expérience de livre plus sophistiquée dont vous pouvez tirer un extrait sur le site www.20thingsilearned.com. Vous y découvrirez comment appliquer des changements de page dans une application réelle et à quel point elle s'avère efficace lorsqu'elle est associée à d'autres fonctionnalités HTML5.

Références

  • Spécification de l'API Canvas