EtchMark 后台揭密:构建能够处理触控、鼠标、手写笔以及设备摇晃操作的网站

EtchMark 是以经典的 Etch-A-Sketch 绘图玩具为原型构建的全新游戏,充分展示了 IE11 在触控和最新 Web 标准(包括指针事件设备方向)支持方面的改进。在本博文中,我们将介绍一些创新功能,您可以轻松地将它们添加到自己的网站中,构建平滑、自然的触控、鼠标、手写笔和键盘操作体验,甚至可响应设备摇晃操作。

该演示的结构

EtchMark 允许您使用触控、鼠标、手写笔或箭头键在屏幕上绘制任何图形。绘图表面为一个 HTML5 canvas 元素,当旋转旋钮时,我们会立即更新该元素。在基准模式下,我们使用 requestAnimationFrame API 提供每秒 60 帧、平滑的循环动画,延长电池使用时间。使用 SVG 滤镜创建旋钮的阴影效果。IE11 的硬件加速功能将此项工作的大部分任务转交给 GPU 来完成,从而实现惊艳的快速体验。请观看下面的视频了解这些功能的应用效果,随后我们将深入剖析这些功能,了解它们是如何构建的。

EtchMark 使用 HTML5 canvas、requestAnimationFrame、SVG 滤镜、指针事件和设备方向 API 以经典玩具为原型构建全新的游戏

使用指针事件实现触控、鼠标、键盘和手写笔操作

利用指针事件,您可以针对单一 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 属性。这样我们就可以支持多个指针,这意味着用户可以使用手指同时操控两个旋钮,两个旋钮的事件互不干涉。多旋钮支持意味着当用户同时旋转两个旋钮时,将能够随意地绘制出任何形式的线条,而不是只能绘制垂直和水平线条。

我们还希望为 this 设置两个标记,指向我们的旋钮对象(每个旋钮设置一对标记):

  • 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 可以通过两种方式来触发。第一种方式是,当系统判断指针不再继续生成事件(例如,由于硬件事件)时,将会触发该事件。第二种方式是,在发生 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;

        }

    }

通过实现四个简单的事件处理程序,我们现在创建了一种贴合手指操控的自然触控体验。它支持多个指针,允许用户同时操控两个旋钮来绘制任意图形。最重要的是,由于我们使用了指针事件,相同的代码同样适用于鼠标、手写笔和键盘操作。

在游戏中使用更多手指:添加手势支持

当用户使用单个手指旋转旋钮时,我们在上文中编写的指针事件代码可以有效地处理用户操作,但是,如果用户使用两个手指旋转旋钮,此时应该如何高效地处理用户操作?我们必须使用三角函数计算旋转角度,但是,在加入第二个手指的移动轨迹后,要计算出正确的角度将变得更加复杂。我们没有再为此编写复杂的代码,而是考虑利用 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 为我们提供了一个简单的旋转属性,该属性使用弧度来表示角度,因此,我们不必手动实现所有数学算法。利用该属性,即使使用两个手指来旋转旋钮,我们也能够提供贴合手指操控的自然体验。

设备运动:添加一些摇晃操作

IE11 支持 W3C DeviceOrientation 事件规范,允许我们访问设备的物理方向和运动信息。当设备移动或旋转(更精确地说是加速)时,将在窗口中触发 devicemotion 事件,并提供 x、y 和 z 轴方向的加速度(同时提供含与不含设备重力加速度的两组数据,以米/平方秒为单位)。它还提供了 alpha、beta 和 gamma 旋转角度的变化率(以度/秒为单位)。

假设我们希望在用户摇晃设备时擦除屏幕。为此,我们首先需要关联 devicemotion 事件(在本例中使用 jQuery):

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

接下来,我们检测用户是否沿任意方向移动设备,并且加速度大于我们设定的阈值。由于我们需要检测摇晃操作,因此我们设置了一个计数器来确保存在两次连续的快速运动。如果检测到两次快速运动,我们将擦除屏幕:

    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 还引入了对屏幕方向 API 的支持以及方向锁定等功能。由于 EtchMark 还将用作性能基准,因此我们希望在不同的屏幕分辨率下保持相同的 canvas 尺寸,这样我们只需对每一种设备完成同样的工作即可。在较小的屏幕上(特别是在纵向模式下),这可能会导致屏幕元素的布局非常密集。为了确保提供最佳的体验,我们将屏幕方向锁定为横向模式:

    window.screen.setOrientationLock("landscape");

这样一来,无论用户如何旋转设备,屏幕将始终以横向模式显示。您也可以使用 screen.unlockOrientation 来取消方向锁定。

展望未来

指针事件和设备方向事件等基于标准、可互操作的技术为您的网站带来了令人耳目一新的用户体验改善机会。IE11 的卓越触控支持提供了一种贴合手指操控的平滑体验和优异的互操作性。利用 IE11 和 MSGesture,您甚至还能够创建更绚丽的互动体验,例如,只需访问一个属性,就可以完成两个手指旋转角度的计算,一切都变得非常简单。请在您自己的网站上尝试采用这些技术,我们期待您的反馈。

Jon Aneja
Internet Explorer 项目经理