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

Hakim El Hattab
Hakim El Hattab

Introduction

En 2010, F-i.com et l'équipe Google Chrome ont collaboré sur une application Web éducative basée sur HTML5 intitulé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 mieux présenté dans le contexte d'un livre. Étant donné que le contenu du livre porte principalement sur les technologies du Web ouvert, nous avons jugé important de rester fidèles à ce principe 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 de "20 choses que j'ai apprises sur les navigateurs et le Web"
Couverture du livre et page d'accueil de "20 Things I Learned About Browsers and the Web" (www.20thingsilearned.com)

Nous avons décidé que le meilleur moyen de recréer l'expérience d'un livre physique était de simuler les bons aspects de l'expérience de lecture analogique tout en tirant parti des avantages du monde numérique dans des domaines tels que la navigation. Nous avons beaucoup travaillé sur le traitement graphique et interactif du flux de lecture, en particulier sur la façon dont les pages des livres se tournent.

Premiers pas

Ce tutoriel vous explique comment créer votre propre effet de retournement de page à l'aide de l'élément canvas et de beaucoup de JavaScript. Certains éléments de code rudimentaires, tels que les déclarations de variables et l'abonnement à l'écouteur d'événements, ont été exclus des extraits de code de cet article. N'oubliez donc pas de vous reporter à l'exemple fonctionnel.

Avant de commencer, nous vous conseillons de regarder la démonstration afin de savoir ce que nous voulons créer.

Annoter

Il est toujours 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 des 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 à feuilleter. 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. div a une largeur fixe et section est défini pour masquer son débordement. La largeur de section agit donc comme un masque horizontal pour div.

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

Logique

Le code requis pour alimenter le retournement de 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;

CANVAS_PADDING est ajouté autour du canevas afin que le papier puisse dépasser 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 devrez également mettre à jour les valeurs correspondantes.

Constantes.
Valeurs constantes utilisées dans l'ensemble du code pour suivre les interactions et dessiner le retournement de page.

Nous devons ensuite définir un objet de retournement pour chaque page. Ils seront constamment mis à jour à mesure que nous interagirons avec le livre pour 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
});
}

Nous devons d'abord nous assurer que les pages sont correctement superposées en organisant les z-index des éléments de section afin que la première page soit en haut et la dernière en bas. Les propriétés les plus importantes des objets de retournement sont les valeurs progress et target. Ils servent à déterminer dans quelle mesure la page doit être pliée. -1 signifie tout à gauche, 0 signifie le centre du livre et +1 signifie le bord le plus à droite du livre.

Progression.
Les valeurs de progression et de cible des retournements sont utilisées pour déterminer où la page pliable doit être dessinée sur une échelle allant de -1 à +1.

Maintenant que nous avons défini un objet de retournement pour chaque page, nous devons commencer à capturer et à utiliser l'entrée utilisateur 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 &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 sur la position la plus récente du curseur.

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 à faire pivoter. 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 le mouseUpHandler, nous examinons tous les flips et vérifions si l'un d'eux a été signalé comme dragging et doit maintenant être libéré. Lorsqu'un retournement est relâché, nous définissons sa valeur cible pour qu'elle corresponde au côté vers lequel elle doit se retourner 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 de 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 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 à afficher le 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 impact important sur les performances. Il serait beaucoup plus efficace de ne vider que les régions sur lesquelles nous dessinons. Pour rester dans le sujet de ce tutoriel, nous allons nous contenter d'effacer l'intégralité du canevas.

Si un élément est en cours de glissement, 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 des pixels réels. Nous incrémentons également progress d'une fraction de la distance jusqu'à target. Cela permet d'obtenir une progression fluide et animée du retournement, car il est mis à jour à chaque frame.

Comme nous examinons tous les flips à 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. 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 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 dépasse les bords supérieur et inférieur du livre. Cet effet est à son apogée lorsqu'un retournement est proche de la tranche du livre.

Changer de caméra
Voici à quoi ressemble le pliage de la page lorsqu'elle est tourné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 Canvas permet de décaler le système de coordonnées afin que nous puissions dessiner le retournement de page avec le haut de la colonne vertébrale 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.

Traduire
C'est à partir de ce point que nous dessinons le retournement de page. Le point d'origine 0,0 se trouve en haut à gauche de l'image, mais en le modifiant via translate(x,y), nous simplifions la logique de dessin.

C'est avec foldGradient que nous allons remplir 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 afin qu'il ne disparaisse pas sur un arrière-plan clair.

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 en lignes droites, et les côtés supérieur et inférieur sont incurvés pour donner l'impression de pliage d'un papier. La force de cette pliure de papier est déterminée par la valeur verticalOutdent.

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

Démo de la fonctionnalité de retournement de page

L'effet de retournement de page vise à communiquer la bonne sensation interactive. Par conséquent, regarder des images de cet effet ne lui rend pas vraiment justice.

Étapes suivantes

Retournement forcé
Le retournement de page fluide de ce tutoriel est encore plus efficace lorsqu'il est associé à d'autres fonctionnalités de livre, comme une couverture rigide interactive.

Il ne s'agit là que d'un exemple de ce que vous pouvez accomplir en utilisant des 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 le site www.20thingsilearned.com. Vous verrez comment les retournements de page peuvent être appliqués dans une application réelle et à quel point ils deviennent puissants lorsqu'ils sont associés à d'autres fonctionnalités HTML5.

Références

  • Spécification de l'API Canvas