EtchMark – Hinter den Kulissen: Erstellen einer Website mit Unterstützung für Touch-, Maus- und Stifteingabe sowie Gerätebewegungen

EtchMark ist eine Neuauflage der klassischen Etch-A-Sketch-Zeichentafel, die die verbesserte Unterstützung für Toucheingabe und neue Webstandards (einschließlich Zeigerereignisse und Geräteausrichtung) in IE11 demonstriert. In diesem Beitrag werden mehrere Funktionen erläutert, die Sie auf einfache Weise Ihren Websites hinzufügen können und dort für eine reibungslose und natürliche Eingabe per Touch, Maus, Stift und Tastatur und sogar die Verarbeitung von Gerätebewegungen sorgen können.

Aufbau der Demo

Mit EtchMark per Toucheingabe, Maus, Stift oder Pfeiltasten frei auf dem Bildschirm zeichnen.  Die Zeichenoberfläche ist ein HTML5-Canvaselement, das bei jedem Drehen eines Knopfs aktualisiert wird.  Im Benchmarkmodus wird die requestAnimationFrame-API verwendet, die eine flüssige Animationsschleife mit 60 Frames pro Sekunde sowie eine längere Akkulaufzeit ermöglicht.  Die Schlagschatten der Knöpfe werden mit SVG-Filtern erstellt. Die Hardwarebeschleunigung von IE11 überträgt einen Großteil dieser Arbeit an die GPU, sodass ein schnelles und reibungsloses Erlebnis erzielt wird.  Im nachfolgenden Video können Sie diese Funktionen in Aktion sehen. Anschließend schauen wir uns genauer an, was dahinter steckt.

EtchMark verwendet HTML5-Canvas, requestAnimationFrame, SVG-Filter, Zeigerereignisse und Geräteausrichtungs-APIs, um eine Neuauflage der klassischen Zeichentafel zu erstellen.

Toucheingabe, Maus, Tastatur und Stift unter Verwendung von Zeigerereignissen

Mit Zeigerereignissen können Sie Oberflächen erzeugen, die mit Maus, Tastatur, Stift oder Toucheingabe gleichermaßen funktionieren – alles durch Codierung für eine einzige API. Zeigerereignisse werden von einem breiten Spektrum an Windows-Geräten unterstützt und bald auch in anderen Browsern eingeführt.  Die Pointer Events-Spezifikation ist derzeit Candidate Recommendation beim W3C, und IE11 unterstützt bereits eine Version des Standards ohne Präfix.

Als Erstes müssen die Zeigerereignisse in Knob.js eingerichtet werden. Zunächst wird nach der Standardversion ohne Präfix gesucht, und wenn die Suche fehlschlägt, wird auf die Version mit Präfix zurückgegriffen, die zur Aktivierung der IE10-Unterstützung erforderlich ist.  Im nachfolgenden Beispiel ist „hitTarget“ ein einfaches DIV, das das Bild vom Knopf enthält – etwas vergrößert, damit der Benutzer es leicht mit dem Finger anvisieren kann: 

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

    }

Auf die gleiche Weise wird der entsprechende Fallback für setPointerCapture auf Element.prototype hinzugefügt, um sicherzustellen, dass es auch in IE10 funktioniert:

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

Als Nächstes wird das „pointerDown“-Ereignis behandelt.  Zuerst wird setPointerCapture für „this.hitTarget“ aufgerufen.  Der Zeiger soll erfasst werden, sodass alle nachfolgenden Zeigerereignisse von diesem Element behandelt werden. Außerdem wird dadurch sichergestellt, dass andere Elemente keine Ereignisse auslösen, auch wenn der Zeiger über deren Rand bewegt wird.  Ohne diese Maßnahme würden Probleme auftreten, wenn sich der Finger des Benutzers auf der Grenze zwischen dem Bild und dem enthaltenden DIV befindet: manchmal würde das Zeigerereignis vom Bild und manchmal vom DIV empfangen werden.  Dies würde zu einem ruckeligen Verhalten führen, bei dem der Knopf hin und her springt. Das Erfassen des Zeigers ist eine einfache Möglichkeit, dies zu beheben.

Die Zeigererfassung funktioniert auch gut, wenn der Benutzer den Finger auf dem Knopf aufsetzt und dann allmählich über das Trefferziel hinaus bewegt, während der Knopf weiter gedreht wird.  Selbst wenn der Finger mehrere Zentimeter aus dem Trefferziel hinaus bewegt und nicht angehoben wird, fühlt sich die Drehung noch flüssig und natürlich an.

Als Letztes ist über „setPointerCapture“ anzumerken, dass wir die „pointerId“-Eigenschaft des Ereignisses übergeben.  Dadurch können mehrere Zeiger unterstützt werden. Der Benutzer kann also beide Knöpfe mit je einem Finger steuern, ohne dass sich die Ereignisse der Knöpfe gegenseitig stören. Die Unterstützung für mehrere Knöpfe bedeutet, dass der Benutzer beim gleichzeitigen Drehen beider Knöpfe eine freihändige Zeichnung und nicht nur vertikale und horizontale Linien erhält.

Wir möchten dafür auch zwei Flags festlegen, die auf das „Knob“-Objekt zeigen (die Flags werden für jeden Knopf festgelegt):

  • pointerEventInProgress – gibt an, ob der Zeiger aktiv ist oder nicht
  • firstContact – gibt an, ob der Benutzer gerade seinen Finger aufgesetzt hat

    function pointerDown(evt)

    {

        this.hitTarget.setPointerCapture(evt.pointerId);

        this.pointerEventInProgress = true;

        this.firstContact = true;

    }

Zum Schluss möchten wir das „pointerEventInProgress“-Flag zurücksetzen, wenn der Benutzer den Finger (bzw. Maus/Stift) absetzt:

    function pointerUp(evt)

    {

        this.pointerEventInProgress = false;

    }

 

    function pointerCancel(evt)

    {

        this.pointerEventInProgress = false;

    }

„PointerCancel“ kann auf zwei Arten auftreten. Zum einen tritt es auf, wenn das System erkennt, dass ein Zeiger wahrscheinlich keine weiteren Ereignisse erzeugt (z. B. aufgrund eines Hardwareereignisses). Das Ereignis wird auch ausgelöst, wenn das „pointerDown“-Ereignis bereits aufgetreten ist und der Zeiger dann zum Bearbeiten des Viewports der Seite verwendet wird (z. B. durch Verschieben oder Zoomen).  Der Vollständigkeit wegen sollten sowohl „pointerUp“ als auch „pointerCancel“ implementiert werden.

Mit den eingerichteten Ereignissen für „Up“, „Down“ und „Cancel“ können wir nun die Unterstützung für „pointerMove“ implementieren.  Wir verwenden das firstContact-Flag, damit der Knopf nicht überdreht, wenn der Benutzer den Finger zum ersten Mal ansetzt. Nachdem „firstContact“ gelöscht wurde, werden einfach die Bewegungsdeltas des Fingers berechnet.  Mithilfe von Trigonometrie werden die Start- und Endkoordinaten in einen Drehwinkel umgewandelt, der dann an unsere Zeichenfunktion übergeben wird:

    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;

        }

    }

Durch die Implementierung von vier einfachen Ereignishandlern haben wir nun ein Toucheingabeverhalten erzeugt, das sich natürlich anfühlt und dem Finger reibungslos folgt.  Es unterstützt mehrere Zeiger und lässt den Benutzer beide Knöpfe gleichzeitig drehen, um eine freihändige Zeichnung zu erzeugen.  Da wir Zeigerereignisse verwenden, funktioniert derselbe Code sogar für Maus, Stift und Tastatur.

Noch mehr Finger im Spiel: Hinzufügen von Unterstützung für Gesten

Der von uns geschriebene Code mit den Zeigerereignissen funktioniert gut, wenn der Benutzer den Knopf mit einem einzigen Finger dreht. Was passiert aber, wenn er ihn mit zwei Fingern dreht?  Wir mussten Trigonometrie verwenden, um den Drehwinkel zu berechnen, und die Berechnung des richtigen Winkels mit einem zweiten Finger ist noch etwas komplexer.  Anstatt diesen komplexen Code selbst zu schreiben, greifen wir einfach auf die MSGesture-Unterstützung von IE11 zurück.

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

        });

    }

Mit diesen eingerichteten Ereignissen können wir nun Gestenereignisse behandeln:

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

        }

    }

Wie Sie sehen, bietet uns MSGesture eine einfache Rotationseigenschaft, die den Winkeln im Bogenmaß darstellt, sodass wir die ganze Mathematik nicht selbst anwenden müssen.  Dadurch erhalten wir nun Unterstützung für eine Drehung mit zwei Fingern, die sich natürlich anfühlt und dem Finger reibungslos folgt.

Gerätebewegung: Noch ein Shake gefällig?

IE11 unterstützt die DeviceOrientation-Ereignisspezifikation von W3C, mit der wir auf Informationen über die physische Ausrichtung und Bewegung eines Geräts zugreifen können.  Wird ein Gerät bewegt oder gedreht (genauer gesagt: beschleunigt), wird das devicemotion-Ereignis für das Fenster ausgelöst, und eine Beschleunigung (mit und ohne Einfluss der Erdbeschleunigung auf das Gerät (in m/s2)) wird auf der x-, y- und z-Achse ausgeführt.  Außerdem wird die Änderung der Drehwinkel „Alpha“, „Beta“ und „Gamma“ (in Grad/Sek.) bereitgestellt.

In diesem Fall möchten wir den Bildschirm jedes Mal löschen, wenn der Benutzer das Gerät schüttelt.  Dazu richten wir zunächst das devicemotion-Ereignis ein (in diesem Fall verwenden wir jQuery):

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

Als Nächstes überprüfen wir, ob der Benutzer das Gerät in irgendeine Richtung mit einer Beschleunigung bewegt hat, die über unserem Schwellenwert liegt.  Da wir ein Schütteln erkennen möchten, haben wir einen Zähler, mit dem sichergestellt wird, dass zwei solcher schnellen Bewegungen hintereinander erfolgen.  Wenn zwei schnelle Bewegungen erkannt werden, wird der Bildschirm gelöscht:

    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;

        }

    }

Weitere Informationen zu Geräteausrichtung und -bewegung finden Sie in diesem Beitrag im IE-Blog.

Ausrichtungssperre

Mit IE11 werden außerdem Unterstützung für die Screen Orientation API und Features wie die Ausrichtungssperre eingeführt.  Da es sich bei EtchMark auch um einen Benchmark-Test zur Leistung handelt, möchten wir die Canvas-Größe zwischen verschiedenen Bildschirmauflösungen beibehalten, sodass auf jedem Gerät die gleiche Menge Arbeit verrichtet wird.  Dadurch kann es auf kleineren Bildschirmen gerade in der Hochformatansicht ziemlich eng werden.  Daher wird die Ausrichtung einfach in der Querformatansicht gesperrt:

    window.screen.setOrientationLock("landscape");

Auf diese Weise wird immer die Querformatansicht angezeigt, unabhängig davon, in welche Richtung der Benutzer das Gerät dreht.  Sie können auch „screen.unlockOrientation“, um die Ausrichtungssperre zu entfernen.

Blick in die Zukunft

Interoperable und auf Standard basierende Techniken wie Zeigerereignisse und Geräteausrichtungsereignisse eröffnen neue aufregende Möglichkeiten für Ihre Websites. Die komfortable Toucheingabeunterstützung von IE11 bietet ein flüssiges und interaktives Erlebnis. Mit IE11 und MSGesture können Sie sogar noch einen Schritt weiter gehen und Szenarien erzeugen, wie die Berechnung von Drehwinkeln mit zwei Fingern nur durch Zugreifen auf eine Eigenschaft. Testen Sie diese Techniken auf Ihrer eigenen Website. Wir freuen uns auf Ihr Feedback.

Jon Aneja
Programmmanager, Internet Explorer