Dans les coulisses d'EtchMark : créer un site Web capable de gérer l'interaction tactile, la souris et le clavier, ainsi que les mouvements consistant à secouer l'appareil

EtchMark est une version remaniée du fameux jouet de dessin Télécran (connu dans le monde anglophone sous le nom « Etch-A-Sketch »), qui montre qu'IE11 offre une prise en charge améliorée des interactions tactiles et des standards du Web (dont les événements de pointeur et l'orientation des appareils). Dans ce billet, nous détaillons quelques-unes des fonctionnalités que vous pouvez ajouter facilement à vos propres sites pour créer une expérience utilisateur à la fois fluide et naturelle avec les écrans tactiles, les souris, les stylets et les claviers, et même réagir aux mouvements consistant à secouer l'appareil.

Structure de la démonstration

EtchMark vous permet de dessiner ce que vous souhaitez à l'écran en utilisant l'écran tactile, la souris, le clavier ou les touches de direction.  La surface de dessin est un élément HTML5 Canvas qui est mis à jour à chaque fois que l'un des boutons est actionné.  En mode banc d'essai, nous utilisons l'API requestAnimationFrame, qui offre une boucle d'animation fluide (60 images par seconde) et une meilleure autonomie.  Les ombres portées des boutons sont créées à l'aide de filtres SVG. L'accélération matérielle d'IE11 décharge une bonne partie de ce travail sur le GPU, ce qui permet de profiter d'une expérience utilisateur extrêmement rapide.  Regardez la vidéo ci-dessous pour voir ces fonctionnalités en action. Nous nous pencherons ensuite sur les détails pour mieux comprendre comment la démonstration est conçue.

EtchMark utilise l'élément HTML5 Canvas, requestAnimationFrame, des filtres SVG, ainsi que les API d'événement de pointeur et d'orientation de l'appareil, pour proposer une nouvelle vision d'un jouet très célèbre

Tactile, souris, clavier et stylet avec les événements de pointeur

Les événements de pointeur vous permettent de créer des expériences utilisateur qui fonctionnent aussi bien avec une souris et un clavier, mais aussi avec un stylet ou un écran tactile, en ne codant qu'avec une seule API. Les événements de pointeur sont pris en charge sur l'ensemble des appareils Windows et le seront bientôt sur d'autres navigateurs.  La spécification Pointer Events a maintenant atteint le statut de « Candidate Recommendation » auprès du W3C, et IE11 prend en charge une version sans préfixe du standard.

Pour commencer, nous devons relier nos événements de pointeur dans Knob.js. Il nous faut tout d'abord vérifier la version standard sans préfixe. En cas d'échec de la vérification, nous devons nous rabattre sur la version préfixée, nécessaire pour permettre la prise en charge d'IE10.  Dans l'exemple ci-dessous, hitTarget est une simple balise div contenant l'image du bouton, légèrement surdimensionnée pour permettre à l'utilisateur de poser facilement son doigt dessus : 

    if (navigator.pointerEnabled)

    {

        this.hitTarget.addEventListener("pointerdown", pointerDown.bind(this));

        this.hitTarget.addEventListener("pointerup", pointerUp.bind(this));

        this.hitTarget.addEventListener("pointercancel", pointerCancel.bind(this));

        this.hitTarget.addEventListener("pointermove", pointerMove.bind(this));

    }

    else if (navigator.msPointerEnabled)

    {

        this.hitTarget.addEventListener("MSPointerDown", pointerDown.bind(this));

        this.hitTarget.addEventListener("MSPointerUp", pointerUp.bind(this));

        this.hitTarget.addEventListener("MSPointerCancel", pointerCancel.bind(this));

        this.hitTarget.addEventListener("MSPointerMove", pointerMove.bind(this));

    }

De même, nous ajoutons l'action de secours correcte pour setPointerCapture à Element.prototype, pour que tout fonctionne également avec IE10 :

    Element.prototype.setPointerCapture = Element.prototype.setPointerCapture || Element.prototype.msSetPointerCapture;

Intéressons-nous maintenant à notre événement pointerDown.  Nous devons tout d'abord appeler setPointerCapture sur this.hitTarget.  Nous souhaitons capturer le pointeur de telle sorte que tous les événements de pointeur suivants soient gérés par cet élément. Cela nous permet également de faire en sorte que les autres éléments ne déclenchent pas d'événement si le pointeur entre dans leurs limites.  Sans cela, nous rencontrerions des problèmes lorsque le doigt de l'utilisateur est sur le bord de l'image et de la balise div qui la contient : parfois l'image capterait l'événement de pointeur et à d'autres reprises il serait capté par la balise div.  Au final, l'expérience utilisateur serait saccadée et le bouton changerait de position inopinément. Pour résoudre ce problème facilement, nous pouvons capturer le pointeur.

La capture du pointeur est également très utile lorsque l'utilisateur place son doigt sur le bouton, puis s'éloigne peu à peu de la cible de détection tout en continuant à tourner le bouton.  Même s'il ne lève pas son doigt tant qu'il ne s'est pas éloigné de plusieurs centimètres de la cible, la rotation reste fluide et naturelle.

Dernière chose à noter concernant setPointerCapture : nous transmettons la propriété pointerId de l'événement.  Ceci nous permet de prendre en charge plusieurs pointeurs. Ainsi, l'utilisateur peut utiliser un doigt sur chaque bouton simultanément sans que cela n'interfère avec les autres événements du bouton. Comme plusieurs boutons sont pris en charge, lorsque l'utilisateur fait pivoter les deux boutons simultanément, il obtient un tracé à main levée et non pas uniquement des lignes verticales et horizontales.

Nous voulons également définir deux indicateurs sur this, qui pointe vers notre objet de bouton Knob (les indicateurs sont propres à chaque bouton) :

  • pointerEventInProgress : indique si le pointeur est enfoncé ou non
  • firstContact : indique si l'utilisateur vient juste d'abaisser son doigt

    function pointerDown(evt)

    {

        this.hitTarget.setPointerCapture(evt.pointerId);

        this.pointerEventInProgress = true;

        this.firstContact = true;

    }

Enfin, nous souhaitons réinitialiser l'indicateur pointerEventInProgress lorsque l'utilisateur lève son doigt (ou sa souris ou encore son stylet) :

    function pointerUp(evt)

    {

        this.pointerEventInProgress = false;

    }

 

    function pointerCancel(evt)

    {

        this.pointerEventInProgress = false;

    }

L'événement PointerCancel peut se produire de deux manières différentes. Il peut notamment se produire lorsque le système détecte qu'un pointeur a peu de chances de continuer à produire des événements (en raison d'un événement matériel, par exemple). L'événement se déclenche également si l'événement pointerDown s'est déjà produit et que le pointeur est ensuite utilisé pour manipuler la fenêtre d'affichage de la page (déplacement latéral ou zoom, par exemple).  Pour plus d'exhaustivité, nous vous recommandons dans tous les cas d'implémenter à la fois pointerUp et pointerCancel.

En connectant les événements Up, Down et Cancel, nous pouvons maintenant implémenter la prise en charge de pointerMove.  Nous utilisons l'indicateur firstContact pour ne pas provoquer une rotation excessive lorsque l'utilisateur abaisse pour la première fois son doigt. Une fois que firstContact est vide, nous calculons simplement les deltas de mouvement du doigt.  Nous faisons appel aux principes de trigonométrie pour transformer nos coordonnées de début et de fin en angle de rotation, que nous transmettons ensuite à notre fonction de dessin :

    function pointerMove(evt)

    {

        //centerX and centerY are the centers of the hit target (div containing the knob)

        evt.x -= this.centerX;

        evt.y -= this.centerY;

 

        if (this.pointerEventInProgress)

        {

            //Trigonometry calculations to figure out rotation angle

 

            var startXDiff = this.pointerEventInitialX - this.centerX;

            var startYDiff = this.pointerEventInitialY - this.centerY;

 

            var endXDiff = evt.x - this.centerX;

            var endYDiff = evt.y - this.centerY;

 

            var s1 = startYDiff / startXDiff;

            var s2 = endYDiff / endXDiff;

 

            var smoothnessFactor = 2;

            var rotationAngle = -Math.atan((s1 - s2) / (1 + s1 * s2)) / smoothnessFactor;

 

            if (!isNaN(rotationAngle) && rotationAngle !== 0 && !this.firstContact)

            {

                //it’s a real rotation value, so rotate the knob and draw to the screen

                this.doRotate({ rotation: rotationAngle, nonGesture: true });

            }

 

            //current x and y values become initial x and y values for the next event

            this.pointerEventInitialX = evt.x;

            this.pointerEventInitialY = evt.y;

            this.firstContact = false;

        }

    }

En implémentant quatre gestionnaires d'événement simples, nous avons créé une expérience tactile qui semble naturelle et qui est facile à manipuler.  Elle prend en charge plusieurs pointeurs et permet à l'utilisateur de manipuler les deux boutons simultanément pour créer un dessin à main levée.  Cerise sur le gâteau, comme nous utilisons des événements de pointeur, le même code fonctionne pour les souris, les stylets et les claviers.

Quand plus de doigts entrent en jeu : ajout de la prise en charge des mouvements tactiles

Le code d'événements de pointeur que nous avons écrit ci-dessus fonctionne parfaitement si l'utilisateur tourne le bouton avec un doigt. Cependant, que se passe-t-il s'il utilise deux doigts ?  Nous avons dû utiliser la trigonométrie pour calculer l'angle de rotation. Avec un deuxième doigt qui bouge, il est encore plus difficile de calculer l'angle adéquat.  Plutôt que d'essayer d'écrire nous-mêmes ce code complexe, profitons du fait qu'IE11 prenne en charge MSGesture.

    if (window.MSGesture)

    {

        var gesture = new MSGesture();

        gesture.target = this.hitTarget;

 

        this.hitTarget.addEventListener("MSGestureChange", handleGesture.bind(this));

        this.hitTarget.addEventListener("MSPointerDown", function (evt)

        {

            // adds the current mouse, pen, or touch contact for gesture recognition

            gesture.addPointer(evt.pointerId);

        });

    }

Une fois les événements reliés, nous pouvons maintenant gérer les événements de mouvement :

    function handleGesture(evt)

    {

        if (evt.rotation !== 0)

        {

            //evt.nonGesture is a flag we defined in the pointerMove method above.

            //It will be true when we’re handling a pointer event, and false when

            //we’re handling an MSGestureChange event

            if (!evt.nonGesture)

            {

                //set to false if we got here via Gesture so flag is in correct state

                this.pointerEventInProgress = false;

            }

 

            var angleInDegrees = evt.rotation * 180 / Math.PI;

 

            //rotate the knob visually

            this.rotate(angleInDegrees);

 

            //draw based on how much we rotated

            this.imageSketcher.draw(this.elementName, angleInDegrees);

        }

    }

Comme vous pouvez le constater, MSGesture nous fournit une propriété de rotation simple qui représente l'angle en radians. Par conséquent, nous n'avons pas besoin de réaliser les calculs manuellement de notre côté.  Désormais, nous pouvons prendre en charge les rotations à deux doigts naturelles et faciles à manipuler.

Déplacement de l'appareil : secouez, secouez-moi !

IE11 prend en charge la spécification W3C DeviceOrientation Event du W3C, qui nous permet d'accéder à des informations concernant l'orientation physique et les mouvements d'un appareil.  Lorsqu'un utilisateur déplace ou fait pivoter l'appareil (ou plus précisément, lorsqu'il l'accélère), l'événement devicemotion est déclenché et fournit l'accélération (à la fois avec et sans les effets de l'accélération gravitationnelle de l'appareil, en mètres/s2) sur l'axe x, y et z.  Il fournit aussi le taux de changement dans les angles de rotation alpha, bêta et gamma (exprimés en degrés/seconde).

Dans ce cas, nous souhaitons effacer l'écran à chaque fois que l'utilisateur secoue l'appareil.  Pour cela, la première chose à faire est de connecter l'événement devicemotion (dans ce cas, nous utilisons jQuery) :

    $(window).on("devicemotion", detectShaking);

Nous devons ensuite détecter si l'utilisateur a déplacé l'appareil dans l'une ou l'autre des directions avec une accélération supérieure à notre valeur de seuil.  Comme nous devons détecter les mouvements consistant à secouer l'appareil, nous mettons en place un compteur pour vérifier si au moins deux de ces mouvements rapides sont effectués d'affilée.  Si nous détections deux mouvements rapides, nous effaçons l'écran :

    var nAccelerationsInARow = 0;

 

    var detectShaking = function (evt)

    {

        var accl = evt.originalEvent.acceleration;

 

        var threshold = 6;

        if (accl.x > threshold || accl.y > threshold || accl.z > threshold)

        {

            nAccelerationsInARow++;

            if (nAccelerationsInARow > 1)

            {

                eraseScreen();

                nAccelerationsInARow = 0;

            }

        }

        else

        {

            nAccelerationsInARow = 0;

        }

    }

Pour plus d'informations sur l'orientation de l'appareil et les déplacements, consultez ce billet sur le blog IE.

Verrouillage d'orientation

IE11 introduit également la prise en charge de l'API d'orientation d'écran et de fonctionnalités telles que le verrouillage d'orientation.  EtchMark étant destiné à permettre l'évaluation des performances, nous souhaitons que l'élément Canvas soit de même taille quelle que soit la résolution d'écran, de sorte que nous réalisions la même quantité de travail sur chaque appareil.  Ainsi, les éléments peuvent devenir très resserrés sur les écrans de petite taille, en particulier en mode portrait.  Pour offrir la meilleure expérience possible, nous verrouillons simplement l'orientation en mode paysage :

    window.screen.setOrientationLock("landscape");

Ainsi, quelle que soit la façon dont l'utilisateur fait pivoter l'appareil, il le voit toujours en mode paysage.  Vous pouvez également utiliser screen.unlockOrientation pour désactiver le verrouillage d'orientation.

Perspectives

Les techniques interopérables et basées sur standards, telles que les événements de pointeur ou d'orientation de l'appareil, vous offrent des possibilités passionnantes pour vos sites Web. L'excellente prise en charge du tactile dans IE11 offre une expérience fluide et naturelle, pour une interactivité sans faille. Vous pouvez même aller plus loin avec IE11 et MSGesture, et simplifier grandement certains scénarios. Ainsi, il suffit par exemple d'accéder à une propriété pour calculer des angles de rotation à deux doigts. Essayez ces techniques sur votre propre site et n'hésitez pas à nous faire part de vos commentaires.

Jon Aneja
Chef de projet, Internet Explorer