Feuilleter un livre avec l'API canvas - partie 2

Dans l’article précédent, nous avons mis en place la plupart des éléments qui vont nous permettre de créer l’effet désiré. Dans cette seconde partie, nous allons nous concentrer sur la réalisation de l’effet proprement dit.

Mettons-nous deux secondes dans la peau de celui qui veut créer cet effet et posons-nous ce problème: “J’aimerais donner aux utilisateurs de mon site l’impression qu’ils feuillettent les pages d’un livre. Comment faire ?”. Eh bien, prenez tout simplement un livre, ouvert en son milieu, et placez-vous au dessus de lui. Prenez la page de droite par son extrémité droite et tournez-la lentement. Une fois n’est pas coutume, ne regardez pas le contenu de la page mais la manière dont sa forme évolue au fur et a mesure que vous la tournez:

  1. A moins que vous ayez pris un livre cartonné (dans ce cas rendez le vite à votre enfant) vous constatez que la page ne reste pas rectangulaire mais qu’elle s’incurve.
  2. Si vous êtes particulièrement observateur, vous remarquez aussi qu’elle projette des ombres pendant le processus.

Voilà, nous avons toutes les idées qui vont nous servir à créer l’effet ! Evidemment, nous ne recréerons pas l’effet tel que nous pouvons le voir exactement, ce serait bien trop complexe. Notre objectif est simplement d’en faire assez pour donner à un cerveau humain l’illusion que les pages tournent, et nous savons que notre cerveau est très complaisant pour ce genre de choses ! Nous verrons dans un premier temps comment construire l’effet de courbure de la page, puis nous nous intéresserons aux ombres.

Une démo vaut mieux que mille mots

Pour que vous puissiez visualiser le résultat que nous obtiendrons, j’ai créé cette petite démonstration de l’effet, avec laquelle vous pouvez déjà vous amuser. Faites varier progress entre +1 et -1 et observez l’effet sur le canevas. Selon que le navigateur que vous utilisez gère ou pas les champs de type range, vous pourrez utiliser un slider bien agréable pour cela ou bien vous devrez saisir une valeur à l’ancienne.

Contour du pli de la page

Avant de commencer à regarder le code, rappelons quelques constantes qui serviront de base aux calculs.

  • BOOK_WIDTH et BOOK_HEIGHT donnent les dimensions du livre. Ce sont également les dimensions du canevas sur lequel nous allons dessiner.
  • PAGE_WIDTH et PAGE_HEIGHT sont les dimensions d’une page. Comme nous voulons laisser un espace entre les bords du livre (la couverture) et les pages, on ne se contente pas diviser les dimensions du livre par deux.
  • CANVAS_PADDING est la marge verticale que nous allons placer entre le livre et les bords effectifs du canevas. C’est nécessaire car nous aurons besoin, pour dessiner le pli de la page, de “sortir” du livre.

Il faut également préciser le système de coordonnées que nous utiliserons dans le canevas. Au moyen d’une translation, nous allons déplacer l’origine pour qu’elle se trouve comme illustrée ci-dessous:

Système de coordonnées du canevas

  • Les abcisses (x) partent milieu du livre. On aura donc des abscisses positives dans la partie droite et négatives dans la partie gauche.
  • Les ordonnées (y) prennent en compte le fait que la page est centrée en hauteur dans le livre. La constante PAGE_Y représente l’écart entre le haut du livre (et donc du canevas), et le haut de la page, où nous nous déplacerons pour dessiner.

Nous allons beaucoup utiliser la variable progress qui représente l’état d’avancement de l’effet. Si vous voulez jouer avec la démo pour visualiser son comportement, je vous conseille de décocher toutes les cases pour vous concentrer sur le coeur de l’effet et de modifier la valeur de progress. Voici ce qu’on constate:

VALEUR DE PROGRESS EFFET OBTENU
+1 La page n’est pas tournée du tout
0 La page présente un pli maximum
-1 La page est complètement tournée

Parlons maintenant de ce qui se passe au fur et à mesure que la valeur de progress change. A chaque modification, on recalcule un certain nombre de variables, qui nous permettront de dessiner dans le canevas. Voici la signification de ces variable et l’idée qui se trouve derrière chaque formule:

  • strength: mesure de l’ampleur verticale du pli. On souhaite qu’elle soit nulle au départ, qu’elle augmente au fur et à mesure que l’on tourne la page pour atteindre un pic quand l’extrémité de la page est au centre, puis qu’elle décroisse pour revenir à 0 quand la page est complètement tournée.
  • foldWidth: largeur du pli. On souhaite qu’elle soit nulle au départ et qu’elle progresse linéairement pour atteindre PAGE_WIDTH
  • foldX: position du bord droit du pli. Il doit se trouver sur le bord droit au départ, puis progresser pour se retrouver au centre.
  • verticalOutdent: mesure en pixels de la hauteur du pli. Cette variable n’est qu’un multiplicateur de strength.
    Voici le code qui correspond au calcul de ces valeurs:
var strength = 1 - Math.abs(progress);
var foldWidth = (PAGE_WIDTH * 0.5) * (1 - progress);
var foldX = PAGE_WIDTH * progress + foldWidth;
var verticalOutdent = 20 * strength;

Si ces formules ne vous parlent pas, pas de panique ! Le principal est de comprendre le rôle des variables et la manière dont elles évoluent. Pour information, les représentations graphiques de ces équations (en donnant à PAGE_WIDTH la valeur 300 comme dans la fiddle) donnent ceci:

strength foldWidth foldX
Progression de l'ampleur verticale du pli Progression de la largeur du pli Progression du bord du pli

Une fois que ces variables sont calculées, on peut commencer à dessiner le pli. Voici le code de la fonction drawFoldedPaper() :

function drawFoldedPaper(foldX, foldWidth, verticalOutdent, fill) {
    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);

    if (fill) {
        // Gradient applied to the folded paper (highlights & shadows)
        var paperShadowWidth = (PAGE_WIDTH * 0.5) * Math.max(Math.min(1 - progress, 0.5), 0);
        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.fill();
    }
    context.strokeStyle = 'rgba(0,0,0,0.06)';
    context.lineWidth = 2;
    context.stroke();
}

Outre les paramètres foldX, foldWidth et verticalOutdent dont nous venons de parler, cette fonction prend aussi le paramètres fill, qui nous servira par la suite à rendre le dessin du pli plus réaliste. Si on laisse donc cette portion de code de côté pour l’instant, la fonction utilise simplement quelques primitives graphiques de context pour dessiner la forme suivante:

  • beginPath() : commencer à tracer une nouvelle forme.
  • moveTo(dest) : déplacer le “crayon” vers la destination sans rien tracer.
  • lineTo(dest) : tracer une ligne du point courant vers la destination.
  • quadraticCurveTo(ctrl, dest) : tracer une courbe du point courant vers la destination en utilisant le point de contrôle pour déterminer l’allure de la courbe. J’aime bien imaginer qu’il y a sur le point de contrôle un petit bonhomme qui tire la quadratique vers lui.
  • stroke() : dessin effectif du tracé déterminé à l’aide des primitives précédentes.

Sur la copie d’écran suivante, j’ai fait apparaitre en bleu les points de destination et en rouge les points de contrôle des quadratiques:

Dessin de la quadratique

Nous avons bien avancé ! Intéressons-nous maintenant à ce fameux paramètre fill et à ce qui se passe quand il vaut true, c’est-à-dire quand on a coché la case “Fill paper with gradient”.

Ombres

L’ombre du pli de la page

Il s’agit ici de créer un dégradé permettant d’améliorer l’impression que la page s’arrondit quand on la tourne. On voudrait dessiner une ombre là ou la page est pliée, ombre qui disparaitrait progressivement de chaque côté du pli.

Il faudrait que la largeur de cette ombre:

  • augmente en même temps que la largeur du pli quand on commence à tourner la page
  • atteigne ensuite un seuil (allez, disons PAGE_WIDTH / 4) qu’elle ne dépassera pas.

Eh bien croyez le ou non, cela donne la formule (PAGE_WIDTH * 0.5) * Math.max(Math.min(1 - progress, 0.5), 0). Bon cette formule est franchement compliquée et peut être simplifiée un peu (par exemple le Math.max() ne sert à rien vu la fourchette dans laquelle évolue progress), mais encore une fois c’est l’idée qu’il faut retenir. La courbe correspondante ressemble à ceci:

Largeur de l'ombre du pli

On constate en la rapprochant de la courbe de foldWidth que cela répond aux deux souhaits que nous avons formulé un peu plus haut. Reste à créer le dégradé !

On peut voir un dégradé linéaire comme un vecteur le long duquel on passe progressivement d’une couleur à une autre, et cela autant de fois qu’on le souhaite. Ce dégradé sert ensuite de motif de remplissage à une zone donnée. La création d’un dégradé linéaire avec l’API Canvas se fait de la manière suivante:

  1. Création de l’objet gradient lui-même: context.createLinearGradient(orig, dest). Ici, nous allons créer un gradient de la gauche vers la droite entre le point (foldX - paperShadowWidth, 0) et le point (foldX, 0). Le vecteur aura donc effectivement une longueur égale à paperShadowWidth.
  2. On détermine les couleurs que l’on ajoute au dégradé: gradient.addColorStop(offset, color). offset est une valeur comprise entre 0 (origine du vecteur) et 1 (extrémité du vecteur). Les valeurs que vous pouvez voir dans le code produisent le résultat suivant:

Dégradé appliqué au pli de la page

Vous conviendrez que cela donne effectivement l’impression que la page est souple, qu’elle s’arrondit. Là ça ne s’invente pas, il faut un peu de sensibilité artistique pour arriver à ce résultat. Si vous voulez jouer avec ce dégradé, vous pouvez le modifier facilement ici. On a déjà un résultat plus que correct, mais les créateurs de l’effet étant des stakhanovistes du Web Design, ils ont ajouté d’autres petites fioritures…

Les ombres portées sur les autres pages

Dire que nous allions oublier que le pli de la page projette une ombre sur le reste du livre ! Si si, regardez à nouveau le livre que vous avez devant vous si vous ne le croyez pas. Rassurez-vous, il ne s’agit désormais que de recycler des idées et des techniques que nous avons déjà utilisées.

Le code créant ces ombres se trouve dans la fonction drawDroppedShadow(foldX, foldWidth, strength). Il crée une ombre à gauche du pli et une à droite. Comme le principe est le même, je n’expliquerai que le code pour l’ombre de droite:

var rightShadowWidth = (PAGE_WIDTH * 0.5) * Math.max(Math.min(strength, 0.5), 0);
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();

L’idée est ici de simuler l’ombre portée à droite en dessinant un rectangle dont la largeur augmentera à mesure que le pli prend de l’ampleur, jusqu’à un certain seuil, puis décroitra sur la fin. Je vous laisse regarder quelle formule cela donne dans le code ci-dessus. Voilà le graphique de la fonction, qui prouve que c’est bien ce que l’on attend:

Ombre portée sur les autres pages

Le petit plus de cette ombre portée, c’est que sa profondeur est proportionnelle à l’amplitude du pli. On ne se contente donc pas de faire varier sa largeur, mais on applique également un dégradé dont dont l’opacité est calculée à partir de strength.

La touche finale

Enfin, nous terminerons cette démo en superposant une ombre plus vive autour du bord gauche du pli. Cela permet de renforcer encore l’impression de volume en donnant l’illusion que la page que l’on tourne se trouve au-dessus des autres. Là encore, on fera en sorte que la largeur et l’intensité de cette ombre soit proportionnelles à l’ampleur du pli. Voici le code correspondant:

function drawSharpShadow(foldX, foldWidth, verticalOutdent, strength) {
    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();
}

Concrètement, on dessine cette fois un simple trait, dont on fait varier l’épaisseur et l’opacité.

La suite du menu

Nous avons vu dans cet article comment différentes techniques ont été combinées pour donner l’illusion que, quand on fait varier progress, une page est tournée. Mais ce n’est pas fini ! Il nous reste:

  • à combiner cela avec ce que nous avons commencé dans l’article précédent pour que les pages ne restent pas vides
  • à faire en sorte de pouvoir animer plusieurs pages à la fois (pour les lecteurs pressés).

Nous verrons cela dans le prochain (et dernier) article de cette série.

comments powered by Disqus