EtchMark の内部のしくみ: タッチ、マウス、ペン、そしてデバイス シェイクを処理する Web サイトの構築

EtchMark は昔からおなじみのお絵描きおもちゃの新しいバージョンであり、IE11 の向上したタッチ サポートと最新の Web 標準 (ポインター イベントデバイスの方向など) を紹介するサンプルです。この記事では、サイトに簡単に追加できるいくつかの機能について説明します。それらの機能を使用すると、タッチ、マウス、ペン、キーボード、さらにはデバイス シェイクにも対応して、スムーズで自然な操作感を実現できます。

デモの構造

EtchMark では、タッチ、マウス、ペン、または方向キーを使用して、画面上に自由に線を描くことができます。 描画サーフェスは、つまみを回すと更新される HTML5 Canvas 要素です。 ベンチマーク モードでは、requestAnimationFrame API を使用して、60 フレーム/秒のスムーズなアニメーション ループを行い、バッテリ寿命を延ばします。 つまみのドロップ シャドウは、SVG フィルターを使用して作成します。IE11 のハードウェア アクセラレーションによって、この作業の大部分が GPU に移されるので、非常に高速なエクスペリエンスが実現できます。 以下のビデオで、これらの機能の動作を確認してください。その後、実際の構築方法の説明に入ります。

EtchMark では、HTML5 Canvas、requestAnimationFrame、SVG フィルター、ポインター イベント、および Device Orientation API を使用して、昔からおなじみのおもちゃの新しいバージョンを作成します

ポインター イベントを使用するタッチ、マウス、キーボード、およびペン

ポインター イベントを使用すると、マウス、キーボード、ペン、およびタッチのいずれを使用しても、1 つの API に対するコードを記述するだけで、同じようにうまく動作するエクスペリエンスを構築できます。ポインター イベントは、Windows デバイス全般でサポートされており、間もなく他のブラウザーでもサポートされます。 ポインター イベント仕様は W3C の勧告候補となり、IE11 ではプレフィックスを使用しないバージョンのポインター イベントをサポートしています。

まず、最初に行う必要があるのは、Knob.js のポインター イベントの接続です。最初に標準のプレフィックスを使用しないバージョンを確認し、その確認が失敗したら、IE10 をサポートできるようにするために必要な、プレフィックスの付いたバージョンに戻ります。 次の例で、hitTarget はつまみの画像を含む単純な div であり、ユーザーが指を置きやすいように、少し大きめのサイズに設定されています。 

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

    }

同様に、setPointerCapture 用の適切なフォールバックを Element.prototype に追加して、IE10 でも動作するようにします。

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

次に、pointerDown イベントを処理します。 最初に、this.hitTarget の setPointerCapture を呼び出します。 後続のすべてのポインター イベントがこの要素で処理されるように、ポインターをキャプチャします。そうすることで、ポインターが他の要素の範囲内に移動した場合でも、その要素でイベントが発生しないようにすることもできます。 そうしないと、ユーザーの指が画像とそれを含む div の境界にある場合に問題が発生します。画像がポインター イベントを取得したり、別のときには div がイベントを取得したりします。 そうなると、つまみの動きが飛ぶような、ぎくしゃくした操作感になってしまいます。ポインターをキャプチャすると、この問題を簡単に解決できます。

ポインターのキャプチャは、ユーザーがつまみに指を置いた後、回転させながら少しずつヒット ターゲットの外に指を移動させた場合にもうまく動作します。 指がヒット ターゲットから数インチ離れるまで持ち上げられなかった場合でも、回転は滑らかで自然に動作します。

setPointerCapture に関する最後の注意点は、イベントの pointerId プロパティを渡すということです。 そうすることで、複数のポインターをサポートできるので、ユーザーは各つまみで同時に指を使用できます。一方のつまみのイベントが他方に影響することはありません。複数のつまみをサポートできるので、ユーザーが両方のつまみを同時に回転させると、垂直と水平の線だけでなく、自由な形の描画ができます。

Knob オブジェクトを指す this には、次の 2 つのフラグも設定します (各つまみに)。

  • pointerEventInProgress - ポインターがダウン状態かどうかを示します
  • firstContact - ユーザーが指を置いたばかりかどうかを示します

    function pointerDown(evt)

    {

        this.hitTarget.setPointerCapture(evt.pointerId);

        this.pointerEventInProgress = true;

        this.firstContact = true;

    }

最後に、ユーザーが指 (マウス/ペン) を上げたときに pointerEventInProgress フラグをリセットします。

    function pointerUp(evt)

    {

        this.pointerEventInProgress = false;

    }

 

    function pointerCancel(evt)

    {

        this.pointerEventInProgress = false;

    }

PointerCancel は、2 つの場合に発生します。1 つ目は、ポインターがイベントを発生させ続けそうにないとシステムが判断した場合です (たとえばハードウェア イベントによって)。イベントは pointerDown イベントが既に発生し、その後、ページ ビューポートを操作するためにポインターが使用された場合 (たとえばパンやズームによって) にも発生します。 念のために、pointerUp と pointerCancel の両方を実装しておくことをお勧めします。

アップ、ダウン、および取り消しイベントを接続したので、pointerMove のサポートを実装する準備ができました。 firstContact フラグを使用して、ユーザーが最初に指を置いたときに回転し過ぎないようにします。firstContact がクリアされた後で、単に指の移動の差分を計算します。 開始および終了座標を回転角度に変換するには三角法を使用し、これを描画関数に渡します。

    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;

        }

    }

4 つの単純なイベント ハンドラーを実装することで、自然で指にくっつくように感じられるタッチ エクスペリエンスを作成できました。 複数のポインターがサポートされ、ユーザーは両方のつまみを同時に操作して、自由な形を描画できます。 何より良いのは、ポインター イベントを使用したので、同じコードがマウス、ペン、およびキーボードでも機能することです。

多数の指の使用: ジェスチャのサポートの追加

ここまでで記述したポインター イベント コードは、ユーザーが 1 本の指を使用してつまみを回転させる場合にはうまく動作します。しかし、2 本の指を使用して回転させた場合はどうなるでしょう。 回転角を計算するために三角法を使用する必要がありましたが、2 本目の指の動作から正しい角度を計算するのは、より複雑な作業になります。 この複雑なコードを自分で記述しようとするのではなく、IE11 の 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);

        });

    }

イベントを接続したら、ジェスチャ イベントを処理できます。

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

        }

    }

見てわかるように、MSGesture には角度をラジアンで表す単純な回転プロパティが用意されています。そのため、自分で計算を手動で行う必要がありません。 これで、自然で指にくっつくように感じられる、2 本指での回転をサポートできるようになりました。

デバイスの動き: 小さなシェイクの追加

IE11 では W3C DeviceOrientation イベント仕様がサポートされています。そのため、デバイスの物理的な方向と動きについての情報にアクセスできます。 デバイスが動かされたり回転されたりした (つまり加速度が加えられた) 場合は、devicemotion イベントがこのウィンドウで呼び出され、x、y、z それぞれの軸の加速度 (デバイス上での重力加速度を含むものと含まないものをメートル毎秒毎秒 (m/s2) で表現) を提供します。 alpha、beta、gamma それぞれの回転角度での変化率 (度毎秒 (deg/s) で表現) も提供します。

ここでは、ユーザーがデバイスをシェイクするたびに画面を消去します。 そうするには、まず、devicemotion イベントを接続します (ここでは jQuery を使用しています)。

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

次に、ユーザーがしきい値より大きい加速度でいずれかの方向にデバイスを動かしたかどうかを検出します。 シェイクを検出する必要があるので、このような速い動きが連続して 2 つあることを確認するためのカウンターを使います。 2 つの速い動きを検出したら、画面を消去します。

    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;

        }

    }

デバイスの方向と動きの詳細については、IE ブログのこの記事を参照してください。

方向のロック

IE11 では、Screen Orientation API と、方向のロックのような機能のサポートも導入されています。 EtchMark はパフォーマンス ベンチマークでもあるので、Canvas のサイズはどの画面解像度でも同じにして、どのデバイスでも同じ量の作業が行われるようにします。 このため、小さい画面では、特に縦長モードの場合に非常に窮屈になってしまいます。 最高のエクスペリエンスを保つために、単に方向を横向きにロックします。

    window.screen.setOrientationLock("landscape");

こうすることで、ユーザーがどの方向にデバイスを回転させても、画面は横長モードで表示されます。 screen.unlockOrientation を使用して、方向のロックを解除することもできます。

今後を見据えて

ポインター イベント、デバイスの方向のイベントのような、相互運用が可能で標準ベースの手法を使用すると、Web サイトに新しい可能性が開かれます。IE11 の優れたタッチ サポートでは、スムーズで指にくっつくような感覚と操作性を実現できます。さらに、IE11 と MSGesture によって、2 本の指による回転角度の計算を、プロパティにアクセスするだけの単純な操作で行うようなシナリオも可能になります。自分のサイトで、この手法を試してみてください。フィードバックをお待ちしています。

Jon Aneja
Internet Explorer 担当プログラム マネージャー