Использование WebVR API - Интерфейсы веб API

Использование WebVR API
=======================

WebVR API - фантастическое дополнение к набору инструментов веб-разработчика, позволяющее отображать WebGL сцены на устройствах виртуальной реальности, таких как Oculus Rift и HTC Vive. Но как же начать разработку VR для Веба? Эта статья познакомит вас с основами.

**Примечание:** Наиболее стабильная версия WebVR API — 1.1 — была недавно реализована в Firefox 55 (для Windows в релизной версии, для Mac OS X только в Nightly версии). Так же она доступна в Chrome при использовании Google Daydream. Существует спецификация для следующей версии — 2.0 — но процесс разработки всё ещё находится на ранней стадии. Последнюю информацию можно найти на WebVR Spec Version List.

С чего начать
-------------

Для начала вам понадобится:

* Устройство с поддержкой VR.
    + Самым дешёвым вариантом будет использовать мобильное устройство, какое-нибудь крепление (например, Google Cardboard) и подходящий браузер. Ощущения будут не такие яркие, как от специализированного устройства, но зато вам не понадобится приобретать мощный компьютер или отдельный VR дисплей.
    + Специализированное устройство может быть дорогим, но зато ощущения будут лучше. Наиболее WebVR-совместимые устройства на данный момент: HTC VIVE, The Oculus Rift. Дополнительную информацию о доступных устройствах и их поддержке браузерами можно найти на webvr.info.
* Компьютер с мощностью, достаточной для соответствующего рендеринга/отображения VR сцен, если вы всё-таки решите воспользоваться специализированным устройством. Чтобы получить некоторое представление о системных требованиях, поищите соответствующие руководства для VR платформы, которую вы думаете использовать. В качестве примера: VIVE READY Computers.
* Подходящий браузер. Последние версии Firefox Nightly и Chrome на данные момент являются лучшими вариантами, как для ПК, так и для мобильных устройств.

После того, как вы всё подготовили, можете проверить всё ли работает как следует. Для этого надо открыть простой A-Frame пример и убедиться, что сцена отображается и что можно перейти в VR режим по нажатию на кнопку в правом нижнем углу.

Фреймворк A-Frame является лучшим вариантом, если вы хотите быстро создать WebVR-совместимую сцену, не углубляясь в огромное количество JavaScript-кода. Однако, пользуясь им, вы не узнаете о работе с чистым WebVR API, а именно этим мы сейчас и займёмся.

Introducing our demo
--------------------

Рассмотрим как работает WebVR API на примере, проекте raw-webgl-example, который выглядит похожим образом:

![](/ru/docs/Web/API/WebVR_API/Using_the_WebVR_API/capture1.png)

**Примечание:** вы можете найти исходный код примера на GitHub, а так же посмотреть как он работает прямо в браузере.

Примечание: если вы решили просто посмотреть на этот пример, не подключая VR устройство и не используя Firefox , то есть вероятность, что он не будет корректно отображаться при том, что простой A-Frame пример работает. В таком случае установите одну из последних версий Firefox, и пример должен отобразиться как на рисунке выше, плюс должна присутствовать анимация.

**Примечание:** Если WebVR не работает в вашем браузере, то возможно вам следует убедиться, что работа идёт через видеокарту. Например, для видеокарт фирмы NVIDIA, если у вас успешно установлена панель управления NVIDIA, по клику правой кнопки на ярлык Firefox появится контекстное меню, в котором можно будет выбрать *Run with graphics processor > High-performance NVIDIA processor*.

Наш пример это своеобразный святой Грааль примеров WebGL — вращающийся куб в 3D. Реализован этот пример с помощью чистого WebGL API. Мы не будем объяснять основы JavaScript или WebGL, а только то, что касается WebVR.

Так же в нашем примере присутствуют:

* Кнопка для начала (и окончания) отображения сцены на VR дисплее.
* Кнопка, показывающая и скрывающая обновляемые в реальном времени данные о положении и направлении VR устройства.

При просмотре исходного кода основного JavaScript файла нашего примера, вы легко сможете найти части, относящиеся к WebVR, по предваряющим их комментариям. Просто забейте в строку поиска по документу строчку "WebVR".

**Примечание:** Чтобы узнать больше об основах JavaScript и WebGL, обратитесь к нашим учебным материалам по JavaScript и нашему руководству по WebGL.

Как это работает?
-----------------

Пока что, давайте взглянем на части кода, связанные с WebVR.

Типичное (простое) WebVR приложение работает следующим образом:

1. `Navigator.getVRDisplays()` используется для получения ссылки на ваше VR устройство.
2. `VRDisplay.requestPresent()` используется для начала отображения сцены на VR устройстве.
3. `VRDisplay.requestAnimationFrame()` используется при создании цикла отображения кадров анимации с правильной для выбранного устройства частотой обновления.
4. Внутри цикла сначала, для получения данных необходимых для отображения кадра, используется (`VRDisplay.getFrameData()` (en-US)). Затем сцена отрисовывается дважды (по разу для каждого глаза в соответствующей ему области видимости), а потом отображается с помощью функции (`VRDisplay.submitFrame()` (en-US)).

In the below sections we'll look at our raw-webgl-demo in detail, and see where exactly the above features are used.

### Starting with some variables

The first WebVR-related code you'll meet is this following block:

```js
// WebVR variables

var frameData = new VRFrameData();
var vrDisplay;
var btn = document.querySelector(".stop-start");
var normalSceneFrame;
var vrSceneFrame;

var poseStatsBtn = document.querySelector(".pose-stats");
var poseStatsSection = document.querySelector("section");
poseStatsSection.style.visibility = "hidden"; // hide it initially

var posStats = document.querySelector(".pos");
var orientStats = document.querySelector(".orient");
var linVelStats = document.querySelector(".lin-vel");
var linAccStats = document.querySelector(".lin-acc");
var angVelStats = document.querySelector(".ang-vel");
var angAccStats = document.querySelector(".ang-acc");
var poseStatsDisplayed = false;
```

Let's briefly explain these:

* `frameData` contains a `VRFrameData` object, created using the `VRFrameData()` (en-US) constructor. This is initially empty, but will later contain the data required to render each frame to show in the VR display, updated constantly as the rendering loop runs.
* `vrDisplay` starts uninitialized, but will later on hold a reference to our VR headset (`VRDisplay` (en-US) — the central control object of the API).
* `btn` and `poseStatsBtn` hold references to the two buttons we are using to control our app.
* `normalSceneFrame` and `vrSceneFrame` start uninitialized, but later on will hold references to `Window.requestAnimationFrame()` and `VRDisplay.requestAnimationFrame()` calls — these will initiate the running of a normal rendering loop, and a special WebVR rendering loop; we'll explain the difference between these two later on.
* The other variables store references to different parts of the VR pose data display box, which you can see in the bottom right hand corner of the UI.
### Getting a reference to our VR display

One of the major functions inside our code is `start()` — we run this function when the body has finished loading:

```js
// start
//
// Called when the body has loaded is created to get the ball rolling.

document.body.onload = start;
```

To begin with, `start()` retrieves a WebGL context to use to render 3D graphics into the `<canvas>` element in our HTML. We then check whether the `gl` context is available — if so, we run a number of functions to set up the scene for display.

```js
function start() {
  canvas = document.getElementById("glcanvas");

  initWebGL(canvas);      // Initialize the GL context

  // WebGL setup code here
```

Next, we start the process of actually rendering the scene onto the canvas, by setting the canvas to fill the entire browser viewport, and running the rendering loop (`drawScene()`) for the first time. This is the non-WebVR — normal — rendering loop.

```js
// draw the scene normally, without WebVR - for those who don't have it and want to see the scene in their browser

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
drawScene();
```

Now onto our first WebVR-specific code. First of all, we check to see if `Navigator.getVRDisplays` exists — this is the entry point into the API, and therefore good basic feature detection for WebVR. You'll see at the end of the block (inside the `else` clause) that if this doesn't exist, we log a message to indicate that WebVR 1.1 isn't supported by the browser.

```js
    // WebVR: Check to see if WebVR is supported
    if(navigator.getVRDisplays) {
      console.log('WebVR 1.1 supported');
```

Inside our `if() { ... }` block, we run the `Navigator.getVRDisplays()` function. This returns a promise, which is fulfilled with an array containing all the VR display devices connected to the computer. If none are connected, the array will be empty.

```js
      // Then get the displays attached to the computer
      navigator.getVRDisplays().then(function(displays) {
```

Inside the promise `then()` block, we check whether the array length is more than 0; if so, we set the value of our `vrDisplay` variable to the 0 index item inside the array. `vrDisplay` now contains a `VRDisplay` (en-US) object representing our connected display!

```js
        // If a display is available, use it to present the scene
        if(displays.length > 0) {
          vrDisplay = displays[0];
          console.log('Display found');
```

**Примечание:** It is unlikely that you'll have multiple VR displays connected to your computer, and this is just a simple demo, so this will do for now.

### Starting and stopping the VR presentation

Now we have a `VRDisplay` (en-US) object, we can use it do a number of things. The next thing we want to do is wire up functionality to start and stop presentation of the WebGL content to the display.

Continuing on with the previous code block, we now add an event listener to our start/stop button (`btn`) — when this button is clicked we want to check whether we are presenting to the display already (we do this in a fairly dumb fashion, by checking what the button `textContent` contains).

If the display is not already presenting, we use the `VRDisplay.requestPresent()` method to request that the browser start presenting content to the display. This takes as a parameter an array of the `VRLayerInit` (en-US) objects representing the layers you want to present in the display.

Since the maximum number of layers you can display is currently 1, and the only required object member is the `VRLayerInit.source` (en-US) property (which is a reference to the `<canvas>` you want to present in that layer; the other parameters are given sensible defaults — see `leftBounds` (en-US) and `rightBounds` (en-US))), the parameter is simply [{ source: canvas }].

`requestPresent()` returns a promise that is fulfilled when the presentation begins successfully.

```js
          // Starting the presentation when the button is clicked: It can only be called in response to a user gesture
          btn.addEventListener('click', function() {
            if(btn.textContent === 'Start VR display') {
              vrDisplay.requestPresent([{ source: canvas }]).then(function() {
                console.log('Presenting to WebVR display');
```

With our presentation request successful, we now want to start setting up to render content to present to the VRDisplay. First of all we set the canvas to the same size as the VR display area. We do this by getting the `VREyeParameters` (en-US) for both eyes using `VRDisplay.getEyeParameters()` (en-US).

We then do some simple math to calculate the total width of the VRDisplay rendering area based on the eye `VREyeParameters.renderWidth` (en-US) and `VREyeParameters.renderHeight` (en-US).

```js
// Set the canvas size to the size of the vrDisplay viewport

var leftEye = vrDisplay.getEyeParameters("left");
var rightEye = vrDisplay.getEyeParameters("right");

canvas.width = Math.max(leftEye.renderWidth, rightEye.renderWidth) \* 2;
canvas.height = Math.max(leftEye.renderHeight, rightEye.renderHeight);
```

Next, we `cancel the animation loop` previously set in motion by the `Window.requestAnimationFrame()` call inside the `drawScene()` function, and instead invoke `drawVRScene()`. This function renders the same scene as before, but with some special WebVR magic going on. The loop inside here is maintained by WebVR's special `VRDisplay.requestAnimationFrame` method.

```js
// stop the normal presentation, and start the vr presentation
window.cancelAnimationFrame(normalSceneFrame);
drawVRScene();
```

Finally, we update the button text so that the next time it is pressed, it will stop presentation to the VR display.

```js
                btn.textContent = 'Exit VR display';
              });
```

To stop the VR presentation when the button is subsequently pressed, we call `VRDisplay.exitPresent()` (en-US). We also reverse the button's text content, and swap over the `requestAnimationFrame` calls. You can see here that we are using `VRDisplay.cancelAnimationFrame` (en-US) to stop the VR rendering loop, and starting the normal rendering loop off again by calling `drawScene()`.

```js
            } else {
              vrDisplay.exitPresent();
              console.log('Stopped presenting to WebVR display');

              btn.textContent = 'Start VR display';

              // Stop the VR presentation, and start the normal presentation
              vrDisplay.cancelAnimationFrame(vrSceneFrame);
              drawScene();
            }
          });
        }
      });
    } else {
      console.log('WebVR API not supported by this browser.');
    }
  }
}
```

Once the presentation starts, you'll be able to see the stereoscopic view displayed in the browser:

![](/ru/docs/Web/API/WebVR_API/Using_the_WebVR_API/capture2.png)

You'll learn below how the stereoscopic view is actually produced.

### Why does WebVR have its own requestAnimationFrame()?

This is a good question. The reason is that for smooth rendering inside the VR display, you need to render the content at the display's native refresh rate, not that of the computer. VR display refresh rates are greater than PC refresh rates, typically up to 90fps. The rate will be differ from the computer's core refresh rate.

Note that when the VR display is not presenting, `VRDisplay.requestAnimationFrame` runs identically to `Window.requestAnimationFrame`, so if you wanted, you could just use a single rendering loop, rather than the two we are using in our app. We have used two because we wanted to do slightly different things depending on whether the VR display is presenting or not, and keep things separated for ease of comprehension.

### Rendering and display

At this point, we've seen all the code required to access the VR hardware, request that we present our scene to the hardware, and start running the rending loop. Let's now look at the code for the rendering loop, and explain how the WebVR-specific parts of it work.

First of all, we begin the definition of our rendering loop function — `drawVRScene()`. The first thing we do inside here is make a call to `VRDisplay.requestAnimationFrame()` to keep the loop running after it has been called once (this occurred earlier on in our code when we started presenting to the VR display). This call is set as the value of the global `vrSceneFrame` variable, so we can cancel the loop with a call to `VRDisplay.cancelAnimationFrame()` (en-US) once we exit VR presenting.

```js
function drawVRScene() {
  // WebVR: Request the next frame of the animation
  vrSceneFrame = vrDisplay.requestAnimationFrame(drawVRScene);
```

Next, we call `VRDisplay.getFrameData()` (en-US), passing it the name of the variable that we want to use to contain the frame data. We initialized this earlier on — `frameData`. After the call completes, this variable will contain the data need to render the next frame to the VR device, packaged up as a `VRFrameData` object. This contains things like projection and view matrices for rendering the scene correctly for the left and right eye view, and the current `VRPose` (en-US) object, which contains data on the VR display such as orientation, position, etc.

This has to be called on every frame so the rendered view is always up-to-date.

```js
// Populate frameData with the data of the next frame to display
vrDisplay.getFrameData(frameData);
```

Now we retrieve the current `VRPose` (en-US) from the `VRFrameData.pose` (en-US) property, store the position and orientation for use later on, and send the current pose to the pose stats box for display, if the `poseStatsDisplayed` variable is set to true.

```js
// You can get the position, orientation, etc. of the display from the current frame's pose

var curFramePose = frameData.pose;
var curPos = curFramePose.position;
var curOrient = curFramePose.orientation;
if (poseStatsDisplayed) {
  displayPoseStats(curFramePose);
}
```

We now clear the canvas before we start drawing on it, so that the next frame is clearly seen, and we don't also see previous rendered frames:

```js
// Clear the canvas before we start drawing on it.

gl.clear(gl.COLOR\_BUFFER\_BIT | gl.DEPTH\_BUFFER\_BIT);
```

We now render the view for both the left and right eyes. First of all we need to create projection and view locations for use in the rendering. these are `WebGLUniformLocation` (en-US) objects, created using the `WebGLRenderingContext.getUniformLocation()` (en-US) method, passing it the shader program's identifier and an identifying name as parameters.

```js
// WebVR: Create the required projection and view matrix locations needed
// for passing into the uniformMatrix4fv methods below

var projectionMatrixLocation = gl.getUniformLocation(
  shaderProgram,
  "projMatrix",
);
var viewMatrixLocation = gl.getUniformLocation(shaderProgram, "viewMatrix");
```

The next rendering step involves:

* Specifying the viewport size for the left eye, using `WebGLRenderingContext.viewport` (en-US) — this is logically the first half of the canvas width, and the full canvas height.
* Specifying the view and projection matrix values to use to render the left eye — this is done using the `WebGLRenderingContext.uniformMatrix4fv` (en-US) method, which is passed the location values we retrieved above, and the left matrices obtained from the `VRFrameData` object.
* Running the `drawGeometry()` function, which renders the actual scene — because of what we specified in the previous two steps, we will render it for the left eye only.

```js
// WebVR: Render the left eye’s view to the left half of the canvas
gl.viewport(0, 0, canvas.width \* 0.5, canvas.height);
gl.uniformMatrix4fv(
  projectionMatrixLocation,
  false,
  frameData.leftProjectionMatrix,
);
gl.uniformMatrix4fv(viewMatrixLocation, false, frameData.leftViewMatrix);
drawGeometry();
```

We now do exactly the same thing, but for the right eye:

```js
// WebVR: Render the right eye’s view to the right half of the canvas
gl.viewport(canvas.width \* 0.5, 0, canvas.width \* 0.5, canvas.height);
gl.uniformMatrix4fv(
  projectionMatrixLocation,
  false,
  frameData.rightProjectionMatrix,
);
gl.uniformMatrix4fv(viewMatrixLocation, false, frameData.rightViewMatrix);
drawGeometry();
```

Next we define our `drawGeometry()` function. Most of this is just general WebGL code required to draw our 3D cube. You'll see some WebVR-specific parts in the `mvTranslate()` and `mvRotate()` function calls — these pass matrices into the WebGL program that define the translation and rotation of the cube for the current frame

You'll see that we are modifying these values by the position (`curPos`) and orientation (`curOrient`) of the VR display we got from the `VRPose` (en-US) object. The result is that, for example, as you move or rotate your head left, the x position value (`curPos[0]`) and y rotation value (`[curOrient[1]`) are added to the x translation value, meaning that the cube will move to the right, as you'd expect when you are looking at something and then move/turn your head left.

This is a quick and dirty way to use VR pose data, but it illustrates the basic principle.

```js
function drawGeometry() {
  // Establish the perspective with which we want to view the
  // scene. Our field of view is 45 degrees, with a width/height
  // ratio of 640:480, and we only want to see objects between 0.1 units
  // and 100 units away from the camera.

  perspectiveMatrix = makePerspective(45, 640.0 / 480.0, 0.1, 100.0);

  // Set the drawing position to the "identity" point, which is
  // the center of the scene.

  loadIdentity();

  // Now move the drawing position a bit to where we want to start
  // drawing the cube.

  mvTranslate([
    0.0 - curPos[0] \* 25 + curOrient[1] \* 25,
    5.0 - curPos[1] \* 25 - curOrient[0] \* 25,
    -15.0 - curPos[2] \* 25,
  ]);

  // Save the current matrix, then rotate before we draw.

  mvPushMatrix();
  mvRotate(cubeRotation, [0.25, 0, 0.25 - curOrient[2] \* 0.5]);

  // Draw the cube by binding the array buffer to the cube's vertices
  // array, setting attributes, and pushing it to GL.

  gl.bindBuffer(gl.ARRAY\_BUFFER, cubeVerticesBuffer);
  gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);

  // Set the texture coordinates attribute for the vertices.

  gl.bindBuffer(gl.ARRAY\_BUFFER, cubeVerticesTextureCoordBuffer);
  gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);

  // Specify the texture to map onto the faces.

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE\_2D, cubeTexture);
  gl.uniform1i(gl.getUniformLocation(shaderProgram, "uSampler"), 0);

  // Draw the cube.

  gl.bindBuffer(gl.ELEMENT\_ARRAY\_BUFFER, cubeVerticesIndexBuffer);
  setMatrixUniforms();
  gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED\_SHORT, 0);

  // Restore the original matrix

  mvPopMatrix();
}
```

The next bit of the code has nothing to do with WebVR — it just updates the rotation of the cube on each frame:

```js
// Update the rotation for the next draw, if it's time to do so.

var currentTime = new Date().getTime();
if (lastCubeUpdateTime) {
  var delta = currentTime - lastCubeUpdateTime;

  cubeRotation += (30 \* delta) / 1000.0;
}

lastCubeUpdateTime = currentTime;
```

The last part of the rendering loop involves us calling `VRDisplay.submitFrame()` (en-US) — now all the work has been done and we've rendered the display on the `<canvas>`, this method then submits the frame to the VR display so it is displayed on there as well.

```js
  // WebVR: Indicate that we are ready to present the rendered frame to the VR display
  vrDisplay.submitFrame();
}
```

### Displaying the pose (position, orientation, etc.) data

In this section we'll discuss the `displayPoseStats()` function, which displays our updated pose data on each frame. The function is fairly simple.

First of all, we store the six different property values obtainable from the `VRPose` (en-US) object in their own variables — each one is a `Float32Array` (en-US).

```js
function displayPoseStats(pose) {
  var pos = pose.position;
  var orient = pose.orientation;
  var linVel = pose.linearVelocity;
  var linAcc = pose.linearAcceleration;
  var angVel = pose.angularVelocity;
  var angAcc = pose.angularAcceleration;
```

We then write out the data into the information box, updating it on every frame. We've clamped each value to three decimal places using `toFixed()`, as the values are hard to read otherwise.

You should note that we've used a conditional expression to detect whether the linear acceleration and angular acceleration arrays are successfully returned before we display the data. These values are not reported by most VR hardware as yet, so the code would throw an error if we did not do this (the arrays return `null` if they are not successfully reported).

```js
  posStats.textContent = 'Position: x ' + pos[0].toFixed(3) + ', y ' + pos[1].toFixed(3) + ', z ' + pos[2].toFixed(3);
  orientStats.textContent = 'Orientation: x ' + orient[0].toFixed(3) + ', y ' + orient[1].toFixed(3) + ', z ' + orient[2].toFixed(3);
  linVelStats.textContent = 'Linear velocity: x ' + linVel[0].toFixed(3) + ', y ' + linVel[1].toFixed(3) + ', z ' + linVel[2].toFixed(3);
  angVelStats.textContent = 'Angular velocity: x ' + angVel[0].toFixed(3) + ', y ' + angVel[1].toFixed(3) + ', z ' + angVel[2].toFixed(3);

  if(linAcc) {
    linAccStats.textContent = 'Linear acceleration: x ' + linAcc[0].toFixed(3) + ', y ' + linAcc[1].toFixed(3) + ', z ' + linAcc[2].toFixed(3);
  } else {
    linAccStats.textContent = 'Linear acceleration not reported';
  }

  if(angAcc) {
    angAccStats.textContent = 'Angular acceleration: x ' + angAcc[0].toFixed(3) + ', y ' + angAcc[1].toFixed(3) + ', z ' + angAcc[2].toFixed(3);
  } else {
    angAccStats.textContent = 'Angular acceleration not reported';
  }
}
```

WebVR events
------------

The WebVR spec features a number of events that are fired, allowing our app code to react to changes in the state of the VR display (see Window events). For example:

* `vrdisplaypresentchange` — Fires when the presenting state of a VR display changes — i.e. goes from presenting to not presenting, or vice versa.
* `vrdisplayconnect` — Fires when a compatible VR display has been connected to the computer.
* `vrdisplaydisconnect` — Fires when a compatible VR display has been disconnected from the computer.

To demonstrate how they work, our simple demo includes the following example:

```js
window.addEventListener("vrdisplaypresentchange", function (e) {
  console.log(
    "Display " +
      e.display.displayId +
      " presentation has changed. Reason given: " +
      e.reason +
      ".",
  );
});
```

As you can see, the `event object` provides two useful properties — `VRDisplayEvent.display`, which contains a reference to the `VRDisplay` (en-US) the event was fired in response to, and `VRDisplayEvent.reason` (en-US), which contains a human-readable reason why the event was fired.

This is a very useful event; you could use it to handle cases where the display gets disconnected unexpectedly, stopping errors from being thrown and making sure the user is aware of the situation. In Google's Webvr.info presentation demo, the event is used to run an `onVRPresentChange()` function, which updates the UI controls as appropriate and resizes the canvas.

Summary
-------

This article has given you the very basics of how to create a simple WebVR 1.1 app, to help you get started.


KeyboardEvent.key - Интерфейсы веб API

KeyboardEvent.key
=================

Ключ события `KeyboardEvent` **`key`** доступен только для чтения, возвращает значение клавиши, нажатой пользователем, принимая во внимание состояние клавиш-модификаторов, таких как `Shift` , а также локаль и раскладку клавиатуры. Его значение определяется следующим образом:

#### Key values

See a full list of key values.

* If the pressed key has a printed representation, the returned value is a non-empty Unicode character string containing the printable representation of the key.
* If the pressed key is a control or special character, the returned value is one of the pre-defined key values.
* If the `KeyboardEvent` represents the press of a dead key, the key value must be "`Dead`".
* Some specialty keyboard keys (such as the extended keys for controlling media on multimedia keyboards) don't generate key codes on Windows; instead, they trigger `WM_APPCOMMAND` events. These events get mapped to DOM keyboard events, and are listed among the "Virtual key codes" for Windows, even though they aren't actually key codes.
* If the key cannot be identified, the returned value is `"Unidentified"`.
KeyboardEvent sequence
----------------------

Every `KeyboardEvent` is fired in a pre-determined sequence. For a given key press, the sequence of `KeyboardEvent`s fired is as follows assuming that `Event.preventDefault` is not called:

1. A `keydown (en-US)` event is first fired. If the key is held down further and the key produces a character key, then the event continues to be emitted in a platform implementation dependent interval and the `KeyboardEvent.repeat` (en-US) read only property is set to `true`.
2. If the key produces a character key that would result in a character being inserted into possibly an `<input>`, `<textarea>` (en-US) or an element with `HTMLElement.contentEditable` set to true, the `beforeinput` and `input (en-US)` event types are fired in that order. Note that some other implementations may fire `keypress (en-US)` event if supported. The events will be fired repeatedly while the key is held down.
3. A `keyup (en-US)` event is fired once the key is released. This completes the process.

In sequence 1 & 3, the `KeyboardEvent.key` attribute is defined and is set appropriately to a value according to the rules defined ealier.

KeyboardEvent sequence example
------------------------------

Consider the event sequence generated when we interact with the `Shift` and the `2` key using a U.S keyboard layout as compared to when we do so using a UK keyboard layout.

Try experimenting using the following two test cases:

1. Press and hold the
 `Shift`
 key, then press
 `2`
 and release it. Next, release the
 `Shift`
 key.
2. Press and hold the
 `Shift`
 key, then press and hold
 `2`
 . Release the
 `Shift`
 key. Finally, release
 `2`
 .
### HTML

```html
<div class="fx">
  <div>
    <textarea rows="5" name="test-target" id="test-target"></textarea>
    <button type="button" name="btn-clear-console" id="btn-clear-console">clear console</button>
  </div>
  <div class="flex">
    <pre id="console-log"></div>
  </div>
</div>
```

### CSS

```css
.fx {
  -webkit-display: flex;
  display: flex;
  margin-left: -20px;
  margin-right: -20px;
}

.fx > div {
  padding-left: 20px;
  padding-right: 20px;
}

.fx > div:first-child {
  width: 30%;
}

.flex {
  -webkit-flex: 1;
  flex: 1;
}

#test-target {
  display: block;
  width: 100%;
  margin-bottom: 10px;
}
```

### JavaScript

```js
let textarea = document.getElementById("test-target"),
  consoleLog = document.getElementById("console-log"),
  btnClearConsole = document.getElementById("btn-clear-console");

function logMessage(message) {
  document.getElementById("console-log").innerHTML += message + "<br>";
}

textarea.addEventListener("keydown", (e) => {
  if (!e.repeat) logMessage(`Key "${e.key}" pressed [event: keydown]`);
  else logMessage(`Key "${e.key}" repeating [event: keydown]`);
});

textarea.addEventListener("beforeinput", (e) => {
  logMessage(`Key "${e.data}" about to be input [event: beforeinput]`);
});

textarea.addEventListener("input", (e) => {
  logMessage(`Key "${e.data}" input [event: input]`);
});

textarea.addEventListener("keyup", (e) => {
  logMessage(`Key "${e.key}" released [event: keyup]`);
});

btnClearConsole.addEventListener("click", (e) => {
  let child = consoleLog.firstChild;
  while (child) {
    consoleLog.removeChild(child);
    child = consoleLog.firstChild;
  }
});
```

### Result

**Примечание:** On browsers that don't fully implement the `InputEvent` (en-US) interface which is used for the `beforeinput` and `input (en-US)` events, you may get incorrect output on those lines of the log output.

### Case 1

When the shift key is pressed, a `keydown (en-US)` event is first fired, and the `key` property value is set to the string `"Shift"`. As we keep holding this key, the `keydown (en-US)` event does not continue to fire repeatedly because it does not produce a character key.

When `key 2` is pressed, another `keydown (en-US)` event is fired for this new key press, and the `key` property value for the event is set to the string `"@"` for the U.S keyboard type and `"""` for the UK keyboard type, because of the active modifier `shift` key. The `beforeinput` and `input (en-US)` events are fired next because a character key has been produced.

As we release the `key 2`, a `keyup (en-US)` event is fired and the `key` property will maintain the string values `"@"` and `"""` for the different keyboard layouts respectively.

As we finally release the `shift` key, another `keyup (en-US)` event is fired for it, and the key attribute value remains `"Shift"`.

### Case 2

When the shift key is pressed, a `keydown (en-US)` event is first fired, and the `key` property value is set to be the string `"Shift"`. As we keep holding this key, the keydown event does not continue to fire repeatedly because it produced no character key.

When `key 2` is pressed, another `keydown (en-US)` event is fired for this new key press, and the `key` property value for the event is set to be the string `"@"` for the U.S keyboard type and `"""` for the UK keyboard type, because of the active modifier `shift` key. The `beforeinput` and `input (en-US)` events are fired next because a character key has been produced. As we keep holding the key, the `keydown (en-US)` event continues to fire repeatedly and the `KeyboardEvent.repeat` (en-US) property is set to `true`. The `beforeinput` and `input (en-US)` events are fired repeatedly as well.

As we release the `shift` key, a `keyup (en-US)` event is fired for it, and the key attribute value remains `"Shift"`. At this point, notice that the `key` property value for the repeating keydown event of the `key 2` key press is now "2" because the modifier `shift` key is no longer active. The same goes for the `InputEvent.data` (en-US) property of the `beforeinput` and `input (en-US)` events.

As we finally release the `key 2`, a `keyup (en-US)` event is fired but the `key` property will be set to the string value `"2"` for both keyboard layouts because the modifier `shift` key is no longer active.

Example
-------

This example uses `EventTarget.addEventListener()` to listen for `keydown (en-US)` events. When they occur, the key's value is checked to see if it's one of the keys the code is interested in, and if it is, it gets processed in some way (possibly by steering a spacecraft, perhaps by changing the selected cell in a spreadsheet).

```js
window.addEventListener(
  "keydown",
  function (event) {
    if (event.defaultPrevented) {
      return; // Do nothing if the event was already processed
    }

    switch (event.key) {
      case "Down": // IE/Edge specific value
      case "ArrowDown":
        // Do something for "down arrow" key press.
        break;
      case "Up": // IE/Edge specific value
      case "ArrowUp":
        // Do something for "up arrow" key press.
        break;
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        // Do something for "left arrow" key press.
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        // Do something for "right arrow" key press.
        break;
      case "Enter":
        // Do something for "enter" or "return" key press.
        break;
      case "Esc": // IE/Edge specific value
      case "Escape":
        // Do something for "esc" key press.
        break;
      default:
        return; // Quit when this doesn't handle the key event.
    }

    // Cancel the default action to avoid it being handled twice
    event.preventDefault();
  },
  true,
);
```

Specification
-------------

| Specification |
| --- |
| UI Events # dom-keyboardevent-key |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


KeyboardEvent.altKey - Интерфейсы веб API

KeyboardEvent.altKey
====================

**`KeyboardEvent.altKey`** свойство только для чтения, возвращает `Boolean`, если клавиша `alt` (`Option` или `⌥` на OS X) была нажата (`true`) или не нажата (`false`), когда событие произошло.

Синтаксис
---------
```

var altKeyPressed = instanceOfKeyboardEvent.altKey
```

Пример
------

```js
<html>
<head>
<title>altKey example</title>

<script type="text/javascript">

function showChar(e){
  alert(
    "Нажата клавиша: " + String.fromCharCode(e.charCode) + "\n"
    + "charCode: " + e.charCode + "\n"
    + "клавиша ALT нажата: " + e.altKey + "\n"
  );
}

</script>
</head>

<body onkeypress="showChar(event);">
<p>
Нажмите любую клавишу с зажатой клавишей ALT или без неё<br />
Вы можете также использовать SHIFT вместе с ALT.
</p>
</body>
</html>
```

Спецификация
------------

| Specification |
| --- |
| UI Events # dom-keyboardevent-altkey |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

Смотрите также
--------------

* `KeyboardEvent`


Применение Web Speech API - Интерфейсы веб API

Применение Web Speech API
=========================

Web Speech API предоставляет 2 основных типа функциональности — распознавание речи пользователя и речевое воспроизведение текста (en-US). Это предоставляет новые возможности для взаимодействия с интерфейсом и открывает перед нами новые горизонты создания уникального пользовательского опыта. Эта статья даёт краткое описание обоих направлений с примерами кода и ссылкой на работающее приложение онлайн.

Распознавание речи
------------------

Механизм распознавания речи способен принимать речевой поток через микрофон устройства, а затем проверять его, используя свои внутренние алгоритмы. Для более точной работы рекомендуется использовать интерфейс SpeechGrammar, предоставляющий контейнер для определённого набора грамматики, которое ваше приложение должно использовать. Грамматика определяется с помощью JSpeech Grammar Format(JSGF.).

После того, как пользовательская речь была распознана, алгоритм возвращает результат (список результатов) в качестве текстовой строки, с которой мы можем продолжить работу.

**Примечание:** В Chrome распознавание речи на веб-странице завязано на взаимодействие с сервером. Ваш звук отправляется на веб-службу для обработки распознавания, поэтому приложение не будет работать в офлайн-режиме.

### Демо

Для запуска демо достаточно перейти по ссылке на приложение или скачать репозиторий, установить зависимости (`npm install`) и запустить приложение (`npm run start`), после чего открыть **localhost:4001** в браузере.

![](https://pp.userapi.com/c831409/v831409509/1c0226/S_tm-BfW-U8.jpg)

после озвучки команды

![](https://pp.userapi.com/c831409/v831409509/1c022e/uWRjlOvjopk.jpg)

### HTML и CSS

Разметка и стили предельно просты. У нас есть значок микрофона, на который мы можем кликнуть для начала записи, анимация звукозаписи, которая включается после клика, и фоновый контейнер, который будет изменять свой цвет, в зависимости от того, что озвучит пользователь.

CSS задаёт простые отзывчивые стили, для корректного отображения и работы на всех устройствах

### JavaScript

А вот на реализацию логики давайте обратим более пристальное внимание.

#### Поддержка Chrome

Как уже упоминалось ранее, в настоящее время Chrome поддерживает интерфейс распознавания речи с указанными префиксами, поэтому в начале нашего кода мы включаем строки префиксов для использования нужных объектов в Chrome и ссылки на объекты без префиксов для Firefox.
```

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const SpeechGrammarList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
const SpeechRecognitionEvent = window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
```

#### Грамматика

Следующая часть нашего кода определяет грамматику, которую мы хотим, применять для поиска соответствий.

Определяем следующие переменные:
```

const colors = {
  красный: 'red',
  оранжевый: 'orange',
  жёлтый: 'yellow',
  зелёный: 'green',
  голубой: 'blue',
  синий: 'darkblue',
  фиолетовый: 'violet'
};

const colorsList = Object.keys(colors);

const grammar = '#JSGF V1.0; grammar colors; public <color> = ' + colorsList.join(' | ') + ' ;';

Формат “грамматики“ используемой нами - это JSpeech Grammar Format (JSGF) - по ссылке можете почитать про это больше.
```

Быстро пробежимся по основным принципам:

* Линии разделены точкой с запятой, как и в JavaScript.
* Первая строка - `#JSGF V1.0;` - указывает формат и версию. Это всегда необходимо включать в первую очередь.
* Вторая строка указывает значение, которое мы хотим распознать. public объявляет, что это общедоступное правило, строка в угловых скобках определяет распознанное имя для этого значения (цвет), а список элементов, следующих за знаком равенства, - это альтернативные варианты, которые будут распознаны и могут быть приняты в качестве возможного значения. Обратите внимание, как каждый из них разделяется вертикальной линией ("|" - "pipe character").
* У вас может быть множество значений, определённых отдельно, как указано выше, и содержащих довольно сложные определения грамматики. Для нашего демонстрационного примера мы делаем все просто.

#### Подключение грамматики к нашему распознаванию речи

Следующее, что нужно сделать, это определить экземпляр объекта распознавания речи для управления записью нашего приложения.

Это делается с помощью конструктора `SpeechRecognition()`. Мы также создаём новый речевой грамматический список, чтобы содержать нашу грамматику, используя конструктор `SpeechGrammarList()`.
```

const recognition = new SpeechRecognition();
const speechRecognitionList = new SpeechGrammarList();
```

Добавляем нашу "грамматику" в список, используя метод `SpeechGrammarList.addFromString()`. Он принимает в качестве параметров строку, плюс необязательное значение веса, которое указывает важность этой грамматики по отношению к другим грамматикам, доступным в списке (может быть от 0 до 1 включительно). Добавленная грамматика доступна в списке как экземпляр объекта `SpeechGrammar`.

```js
speechRecognitionList.addFromString(grammar, 1);
```

Затем мы добавляем `SpeechGrammarList` (en-US) к уже созданному объекту распознавания речи, присваивая его значение свойству `SpeechRecognition.grammars` (en-US). Также зададим ещё несколько свойств объекту, прежде чем двигаться дальше:

* `SpeechRecognition.lang` (en-US): устанавливает язык распознавания. Его установка - это хорошая практика, поэтому рекомендуется не пропускать.
* `SpeechRecognition.interimResults` (en-US): определяет, должна ли система распознавания речи возвращать промежуточные результаты или только конечные результаты. Только конечные результаты подойдут для этой нашего простого приложения.
* `SpeechRecognition.maxAlternatives` (en-US): устанавливает количество альтернативных потенциальных совпадений, которые должны быть возвращены на каждый результат. Иногда это может быть полезно, скажем, если результат распознан не точно, и вы хотите отобразить пользователю список вариантов. Но это для простого примера это не нужно, поэтому мы просто указываем один (который по сути является вариантом по умолчанию).

```js
recognition.grammars = speechRecognitionList;
//recognition.continuous = false;
recognition.lang = "ru-RU";
recognition.interimResults = false;
recognition.maxAlternatives = 1;
```

**Примечание:** `SpeechRecognition.continuous` (en-US) задаёт, отслеживаются ли продолжающиеся результаты или только 1 результат, каждый раз, когда запись начата. Это закомментировано, поскольку данное свойство в ещё не реализовано в Gecko.

Вы можете получить аналогичный результат, просто прекратив распознавание после получения первого результата.

#### Запуск распознавания речи

После получения ссылок на DOM-элементы, необходимые нам для обработки пользовательских событий и обновления цвета фона приложения, мы реализуем обработчик `onclick`, чтобы при нажатии на значок микрофона сервис распознавания речи начинал работу. Запуск происходит путём вызова функции `SpeechRecognition.start()`.
```

microphoneIcon.onclick = function() {
  recognition.start();
  console.log('Ready to receive a color command.');
};

recognition.onaudiostart = function() {
  microphoneWrapper.style.visibility = 'hidden';
  audioRecordAnimation.style.visibility = 'visible';
};
```

#### Получение и обработка результата

После того, как процесс распознавания речи был запущен, есть много обработчиков событий, которые могут быть использованы для работы с результатом и другой сопутствующей информацией (см. Список обработчиков событий SpeechRecognition.) Наиболее распространённый, который вы, вероятно, и будете использовать, это SpeechRecognition.onresult (en-US), который запускается сразу после получения успешного результата. Значение цвета получаем вызовом функции `getColor()`
```

function getColor(speechResult) {
  for (let index = 0; index < colorsList.length; index += 1) {
    if (speechResult.indexOf(colorsList[index]) !== -1) {
      const colorKey = colorsList[index];
      return [colorKey, colors[colorKey]];
    }
  }
  return null;
}

recognition.onresult = function(event) {
  const last = event.results.length - 1;
  const colors = getColor(event.results[last][0].transcript);
  recognitionTextResult.textContent = 'Результат: ' + colors[0];
  speechRecognitionSection.style.backgroundColor = colors[1];
  console.log('Confidence: ' + event.results[0][0].confidence);
};
```

Третья строка здесь выглядит немного усложнённой, поэтому давайте разберёмся с ней подробнее. Свойство `SpeechRecognitionEvent.results` (en-US) возвращает объект `SpeechRecognitionResultList` (en-US), содержащий в себе другие объекты типа `SpeechRecognitionResult` (en-US). У него есть геттер, поэтому он может быть доступен как массив, поэтому переменная `last` определяет ссылку на `SpeechRecognitionResult` из списка. Каждый объект `SpeechRecognitionResult` содержит объекты `SpeechRecognitionAlternative` (en-US), которые содержат отдельные распознанные слова. Они также имеют геттеры, поэтому к ним можно получить доступ как к массивам, поэтому логично, что [0] возвращает значение `SpeechRecognitionAlternative` по индексу 0. Затем мы возвращаем строку, содержащую индивидуально распознанный результат, используя который и можем установить цвет фона.

Мы также используем свойство `SpeechRecognition.speechend` (en-US), чтобы задать обработчик на завершение работы распознавателя речи (вызов `SpeechRecognition.stop()` (en-US) ), как только одно слово было распознано, и входящий речевой поток был остановлен.
```

recognition.onspeechend = function() {
  recognition.stop();
  microphoneWrapper.style.visibility = 'visible';
  audioRecordAnimation.style.visibility = 'hidden';
};
```

#### Обработка ошибок

Последние два обработчика используются для отлова ошибок: когда речь была признана не в соответствии с определённой грамматикой или произошла ошибка. По логике, `SpeechRecognition.onnomatch` (en-US), должен обрабатывать первый случай, но обратите внимание, что на данный момент он не срабатывает правильно в Firefox или Chrome, он просто возвращает все, что было распознано в любом случае:
```

recognition.onnomatch = function(event) {
  alert("I didn't recognise that color.");
};
```

`SpeechRecognition.onerror` (en-US) обрабатывает случаи, когда имела место быть фактическая ошибка при распознавании. Свойство `SpeechRecognitionError.error` (en-US) содержит возвращаемую фактическую ошибку:
```

recognition.onerror = function(event) {
  alert(`Error occurred in recognition: ${event.error}`);
};
```

Синтез речи
-----------

Синтез речи (text-to-speech или tts) подразумевает получение синтезированного текста приложения и его речевое воспроизведение.

 Для этой цели Web Speech API предоставляет интерфейс - `SpeechSynthesis` (en-US) - плюс ряд близких интерфейсов для нужного нам воспроизведения текста (utterances - "дикция"), набор голосов, которыми приложение будет "говорить", и т. д.
 Опять же, большинство ОС имеют некоторые встроенные системы синтеза речи, которые будут задействованы нашим API для этой цели.

### Демо

 То же самое приложение из предыдущего примера.
 Ссылка на приложение или репозиторий (клонируем, затем `npm install && npm run start` в терминале, после чего открыть **localhost:4001** в браузере).

Пользовательский интерфейс включает в себя набор элементов для ввода текста, задания высоты тона, скорости воспроизведения и непосредственного выбора голоса, которым будет текст произнесён.

После ввода текста вы можете нажать **Play** для запуска.

![](https://pp.userapi.com/c847220/v847220505/1103b9/Jlnq5hDThyQ.jpg)

### HTML и CSS

 HTML и CSS снова достаточно тривиальны.
 Заголовок и форму с некоторыми простыми элементами управления.
 Элемент `<select>` изначально пуст, но заполняется с помощью `<option>` через JavaScript (см. ниже).

CSS задаёт простые отзывчивые стили, для корректного отображения и работы на всех устройствах
```

<section>
  <h1>Синтез речи</h1>
  <p>Введите текст в поле ниже и нажмите кнопку "Play", чтобы прослушать запись. Выбирайте возможные голоса из списка ниже</p>

  <form>
    <input type="text" class="text">
    <div class="row">
      <div class="values-box">
        <div class="value-box">
          <div>Темп (Rate)</div>
          <div class="value value--rate-value">1</div>
        </div>

        <div class="value-box">
          <div>Диапазон (Pitch)</div>
          <div class="value value--pitch-value">1</div>
        </div>
      </div>

      <div class="ranges-box">
        <input type="range" min="0.5" max="2" value="1" step="0.1" id="rate">
        <input type="range" min="0" max="2" value="1" step="0.1" id="pitch">
      </div>
    </div>

    <select>
    </select>

    <button id="play" type="submit">Play</button>

  </form>
```

### JavaScript

Давайте более детально рассмотрим скрипт, задающий логику нашему приложения.

#### Задание переменных

Прежде всего, создаём ссылки на все нужные нам DOM-элементы.

Входная точка API - `window.speechSynthesis` (en-US), возвращает экземпляр `SpeechSynthesis` (en-US), интерфейс контроллера для синтеза речи в вебе.
```

const synth = window.speechSynthesis;
const inputForm = document.querySelector('form');
const inputTxt = document.querySelector('.text');
const voicesList = document.querySelector('select');
const pitch = document.querySelector('#pitch');
const pitchValue = document.querySelector('.value--pitch-value');
const rate = document.querySelector('#rate');
const rateValue = document.querySelector('.value--rate-value');
let voices = [];
```

#### Заполнение выпадающего списка

Чтобы заполнить элемент `<select>` различными вариантами голоса, доступных на устройстве, напишем функцию `populateVoiceList()` (en-US). Сначала мы вызываем `SpeechSynthesis.getVoices()` (en-US), который возвращает список всех доступных вариантов голосов, представленных объектами `SpeechSynthesisVoice` (en-US). Затем мы проходимся по списку, создавая элемент `<option>` для каждого отдельного случая, задаём его текстовое содержимое, соответствующее названию голоса (взято из `SpeechSynthesisVoice.name` (en-US)), языка голоса (из `SpeechSynthesisVoice.lang` (en-US)), и "по умолчанию", если голос является голосом по умолчанию для механизма синтеза (проверяется, если функция `SpeechSynthesisVoice.default` (en-US) возвращает значение `true`.)

Мы также задаём `data-` атрибуты для каждого варианта, содержащие имя и язык связанного голоса, благодаря чему мы можем легко их собрать их позже, а затем вложить все варианты в качестве дочерних элементов нашего списка (`<select>`).
```

function populateVoiceList() {
  voices = synth.getVoices();
  const selectedIndex =
  voicesList.selectedIndex < 0 ? 0 : voicesList.selectedIndex;
  voicesList.innerHTML = '';

  for(i = 0; i < voices.length ; i++) {
    const option = document.createElement('option');
    option.textContent = voices[i].name + ' (' + voices[i].lang + ')';

    if(voices[i].default) {
      option.textContent += ' -- DEFAULT';
    }

    option.setAttribute('data-lang', voices[i].lang);
    option.setAttribute('data-name', voices[i].name);
    voiceSelect.appendChild(option);
  }
  voicesList.selectedIndex = selectedIndex;
}
```

Когда мы собираемся запустить функцию, мы делаем следующее. Это связано с тем, что Firefox не поддерживает свойство `SpeechSynthesis.onvoiceschanged` (en-US) и будет только возвращать список голосов при запуске `SpeechSynthesis.getVoices()` (en-US). Однако, в Chrome вам нужно дождаться триггера события перед заполнением списка, следовательно, нужно условие, описанное в блоке с `if` ниже.
```

populateVoiceList();
  if (speechSynthesis.onvoiceschanged !== undefined) {
  speechSynthesis.onvoiceschanged = populateVoiceList;
}
```

#### Озвучка введённого текста

Затем мы создаём обработчик событий, чтобы начать "произносить" текст, введённый в текстовом поле, при нажатии на кнопку `Enter/Return` или на `Play`. Для этого используем обработчик `onsubmit` в html-формы. В функции-обработчике `speak()` мы создаём новый экземпляр `SpeechSynthesisUtterance()` (en-US), передавая значение текстового поля в конструктор.

Затем нам нужно выяснить, какой голос использовать. Мы используем свойство `HTMLSelectElement` (en-US) `selectedOptions` для получения выбранного элемента `<option>`, у которого берём атрибут data-name, и находим объект `SpeechSynthesisVoice` (en-US), имя которого соответствует значению имеющегося атрибута. После этого устанавливаем соответствующий "голосовой" объект как значение свойства `SpeechSynthesisUtterance.voice` (en-US).

Наконец, мы устанавливаем `SpeechSynthesisUtterance.pitch` (en-US) (высота тона) и `SpeechSynthesisUtterance.rate` (en-US) (скорость) в соответствии со значениями соответствующих элементов формы. Затем, после всего проделанного, мы запускаем произношение речи, вызывая `SpeechSynthesis.speak()` (en-US), и передавая ему экземпляр `SpeechSynthesisUtterance` в качестве аргумента.

 Внутри функции `speak()` мы выполняем проверку на то, воспроизводится ли речь в данный момент, с помощью свойства `SpeechSynthesis.speaking` (en-US)
 Если да, то останавливаем процесс функцией `SpeechSynthesis.cancel()` (en-US) и запускаем рекурсивно заново.

В последней части функции мы включаем обработчик `SpeechSynthesisUtterance.onpause` (en-US), чтобы показать пример применения `SpeechSynthesisEvent` (en-US) в различных ситуациях. Вызов `SpeechSynthesis.pause()` (en-US)возвращает сообщение с информацией о номере символа и слове, на котором была вызвана пауза.

Наконец, мы назовём `blur()` у текстового поля. Это, прежде всего, для того, чтобы скрыть клавиатуру в ОС Firefox.

```js
function speak() {
  if (synth.speaking) {
    console.error("speechSynthesis.speaking");
    synth.cancel();
    setTimeout(speak, 300);
  } else if (inputTxt.value !== "") {
    const utterThis = new SpeechSynthesisUtterance(inputTxt.value);
    utterThis.onend = function (event) {
      console.log("SpeechSynthesisUtterance.onend");
    };

    utterThis.onerror = function (event) {
      console.error("SpeechSynthesisUtterance.onerror");
    };
    const selectedOption =
      voicesList.selectedOptions[0].getAttribute("data-name");

    for (i = 0; i < voices.length; i++) {
      if (voices[i].name === selectedOption) {
        utterThis.voice = voices[i];
      }
    }

    utterThis.onpause = function (event) {
      const char = event.utterance.text.charAt(event.charIndex);
      console.log(
        "Speech paused at character " +
          event.charIndex +
          ' of "' +
          event.utterance.text +
          '", which is "' +
          char +
          '".',
      );
    };

    utterThis.pitch = pitch.value;
    utterThis.rate = rate.value;
    synth.speak(utterThis);
  }
}

inputForm.onsubmit = function (event) {
  event.preventDefault();
  speak();
  inputTxt.blur();
};
```

#### Обновление отображаемых значений высоты тона и скорости

Последний пример кода просто обновляет значения высоты тона/скорости, отображаемые в пользовательском интерфейсе, каждый раз, когда позиции ползунка перемещаются.

```js
pitch.onchange = function () {
  pitchValue.textContent = pitch.value;
};

rate.onchange = function () {
  rateValue.textContent = rate.value;
};
```


CSSStyleDeclaration.getPropertyValue() - Интерфейсы веб API

CSSStyleDeclaration.getPropertyValue()
======================================

**CSSStyleDeclaration.getPropertyValue()** интерфейс метода, который возвращает `DOMString`, указанного CSS-свойства.

Синтаксис
---------
```

var value = style.getPropertyValue(property);
```

### Параметры

* *`property`* - это `DOMString` представляющий имя свойства, которое должно быть обработано.
### Возвращаемое значение

* `value` - это `DOMString` содержащий значение свойства. Если не задано, возвращается пустая строка.
Пример
------

Следующий JavaScript-код проверяет отмечен ли margin как important в CSS-правиле селектора:

```js
var declaration = document.styleSheets[0].rules[0].style;
var value = declaration.getPropertyValue("margin"); // "1px 2px"
```

Спецификации
------------

| Specification |
| --- |
| CSS Object Model (CSSOM) # dom-cssstyledeclaration-getpropertyvalue |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


BackgroundFetchUpdateUIEvent() - Интерфейсы веб API

BackgroundFetchUpdateUIEvent()
==============================

Limited availability
--------------------

This feature is not Baseline because it does not work in some of the most widely-used browsers.

* Learn more
* See full compatibility
* Report feedback
**Экспериментальная возможность:** **Это экспериментальная технология**
Так как спецификация этой технологии ещё не стабилизировалась, смотрите таблицу совместимости по поводу использования в различных браузерах. Также заметьте, что синтаксис и поведение экспериментальной технологии может измениться в будущих версиях браузеров, вслед за изменениями спецификации.

Конструктор **`BackgroundFetchUpdateUIEvent()`** создаёт новый объект `BackgroundFetchUpdateUIEvent`. Этот конструктор обычно не используется, так как браузер сам создаёт эти объекты и предоставляет их для колбэков фонового запроса.

Синтаксис
---------

```js
new BackgroundFetchEvent(type, options)
```

### Параметры

`type`

 Строка с именем события.
 Эта строка чувствительна к регистру и браузеры устанавливают её значение в `backgroundfetchsuccess` или `backgroundfetchfail`.

`options`

Объект, который , *в дополнение к свойствам, определённым в`ExtendableEvent()` (en-US)*, имеет следующие свойства:

`registration`

Объект `BackgroundFetchRegistration`.

### Возвращаемое значение

Новый объект `BackgroundFetchUpdateUIEvent`.

Спецификации
------------

| Specification |
| --- |
| Background Fetch # dom-backgroundfetchupdateuievent-backgroundfetchupdateuievent |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


BackgroundFetchUpdateUIEvent.updateUI() - Интерфейсы веб API

BackgroundFetchUpdateUIEvent.updateUI()
=======================================

Limited availability
--------------------

This feature is not Baseline because it does not work in some of the most widely-used browsers.

* Learn more
* See full compatibility
* Report feedback
**Экспериментальная возможность:** **Это экспериментальная технология**
Так как спецификация этой технологии ещё не стабилизировалась, смотрите таблицу совместимости по поводу использования в различных браузерах. Также заметьте, что синтаксис и поведение экспериментальной технологии может измениться в будущих версиях браузеров, вслед за изменениями спецификации.

Метод **`updateUI()`** интерфейса `BackgroundFetchUpdateUIEvent` обновляет заголовок и иконку в пользовательском интерфейсе, для того чтобы показать статус фонового запроса.

Этот метод может быть вызван только один раз, чтобы уведомить пользователя об успехе или провале фонового запроса.

Синтаксис
---------

```js
updateUI(options)
```

### Параметры

`options` Необязательный

Объект, содержащий любое из следующих значений:

`icons` Необязательный

Список из одного или более ресурсов изображений, содержащих иконки для использования в интерфейсе пользователя. Ресурс изображения это объект, содержащий:

`src`

Строка с URL изображения.

`sizes` Необязательный

Строка, которая эквивалентна аттрибуту `sizes` элемента `<link>`.

`type` Необязательный

Строка, содержащая MIME-тип изображения.

`label` Необязательный

Строка, содержащая имя для изображения.

`title` Необязательный

Строка, содержащая новый заголовок для интерфейса пользователя.

### Возвращаемое значение

`Promise`.

### Исключения

`InvalidStateError` `DOMException` (en-US)

Возвращается, если любое из следующих утверждений верно:

* Свойство `isTrusted` - `false`.
* `BackgroundFetchUpdateUIEvent` флаг обновения UI уже установлен, указывая на то, что метод `updateUI()` уже был вызван.
* `BackgroundFetchUpdateUIEvent` не активен.

Примеры
-------

Следующий пример демонстрирует обновление в UI заголовка и иконки, при успешном завершении запроса.

```js
addEventListener("backgroundfetchsuccess", (event) => {
  event.updateUI({
    title: "Эпизод 5 готов к прослушиванию!",
    icon: {
      src: "path/to/success.ico",
      sizes: "16x16 32x32 64x64",
    },
  });
});
```

Спецификации
------------

| Specification |
| --- |
| Background Fetch # background-fetch-update-ui-event-update-ui |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


Using the Gamepad API - Интерфейсы веб API

Using the Gamepad API
=====================

HTML5 представляет большое количество необходимых компонентов для полной и интерактивной разработки игр. Такие технологии, как `<canvas>`, WebGL, `<audio>`, и `<video>`, вместе с Javascript сейчас поддерживают большое количество задач, для которых раньше требовались нативные программы. Gamepad API - это способ получения данных с геймпада и других игровых контроллеров.

Gamepad API добавляет в объект `Window` новые события для получения событий контроллера. Дополнительно к этим событиям, API также добавляет объект `Gamepad`, который позволяет получить состояние подключённого контроллера, и метод `navigator.getGamepads()` который позволяет получить все контроллеры, определённые на странице браузера.

Подключение геймпада
--------------------

Когда новый геймпад подключается, на странице срабатывают события `gamepadconnected`. Если геймпад уже был подключён к моменту загрузки страницы, события `gamepadconnected` сработают, когда пользователь нажмёт на любую кнопку или передвинет стики.

**Примечание:** В Firefox геймпад определяется только тогда, когда пользователь взаимодействует с ним, и при этом страница видна и в фокусе. Это помогает предотвратить использование геймпадов для идентификации пользователя. После взаимодействия с одним геймпадом другие подключённые геймпады будут автоматически видны.

Вы можете использовать `gamepadconnected` как в примере:

```js
window.addEventListener("gamepadconnected", function (e) {
  console.log(
    "Gamepad connected at index %d: %s. %d buttons, %d axes.",
    e.gamepad.index,
    e.gamepad.id,
    e.gamepad.buttons.length,
    e.gamepad.axes.length,
  );
});
```

Каждый геймпад имеет уникальный ID, который доступен в свойстве `gamepad`.

Отключение геймпада
-------------------

When a gamepad is disconnected, and if a page has previously received data for that gamepad (e.g. `gamepadconnected`), a second event is dispatched to the focused window, `gamepaddisconnected (en-US)`:

```js
window.addEventListener("gamepaddisconnected", function (e) {
  console.log(
    "Gamepad disconnected from index %d: %s",
    e.gamepad.index,
    e.gamepad.id,
  );
});
```

The gamepad's `index` property will be unique per-device connected to the system, even if multiple controllers of the same type are used. The `index` property also functions as the index into the `Array` returned by `Navigator.getGamepads()`.

```js
var gamepads = {};

function gamepadHandler(event, connecting) {
  var gamepad = event.gamepad;
  // Note:
  // gamepad === navigator.getGamepads()[gamepad.index]

  if (connecting) {
    gamepads[gamepad.index] = gamepad;
  } else {
    delete gamepads[gamepad.index];
  }
}

window.addEventListener(
  "gamepadconnected",
  function (e) {
    gamepadHandler(e, true);
  },
  false,
);
window.addEventListener(
  "gamepaddisconnected",
  function (e) {
    gamepadHandler(e, false);
  },
  false,
);
```

This previous example also demonstrates how the `gamepad` property can be held after the event has completed — a technique we will use for device state querying later.

Querying the Gamepad object
---------------------------

As you can see, the **gamepad** events discussed above include a `gamepad` property on the event object, which returns a `Gamepad` object. We can use this in order to determine which gamepad (i.e., its ID) had caused the event, since multiple gamepads might be connected at once. We can do much more with the `Gamepad` object, including holding a reference to it and querying it to find out which buttons and axes are being pressed at any one time. Doing so is often desirable for games or other interactive web pages that need to know the state of a gamepad now vs. the next time an event fires.

Performing such checks tends to involve using the `Gamepad` object in conjunction with an animation loop (e.g., `requestAnimationFrame`), where developers want to make decisions for the current frame based on the state of the gamepad or gamepads.

The `Navigator.getGamepads()` method returns an array of all devices currently visible to the webpage, as `Gamepad` objects (the first value is always `null`, so `null` will be returned if there are no gamepads connected.) This can then be used to get the same information. For example, the first code example above you be rewritten as shown below:

```js
window.addEventListener("gamepadconnected", function (e) {
  var gp = navigator.getGamepads()[e.gamepad.index];
  console.log(
    "Gamepad connected at index %d: %s. %d buttons, %d axes.",
    gp.index,
    gp.id,
    gp.buttons.length,
    gp.axes.length,
  );
});
```

The `Gamepad` object's properties are as follows:

* `id`: A string containing some information about the controller. This is not strictly specified, but in Firefox it will contain three pieces of information separated by dashes (`-`): two 4-digit hexadecimal strings containing the USB vendor and product id of the controller, and the name of the controller as provided by the driver. This information is intended to allow you to find a mapping for the controls on the device as well as display useful feedback to the user.
* `index`: An integer that is unique for each gamepad currently connected to the system. This can be used to distinguish multiple controllers. Note that disconnecting a device and then connecting a new device may reuse the previous index.
* `mapping`: A string indicating whether the browser has remapped the controls on the device to a known layout. Currently there is only one supported known layout — the standard gamepad. If the browser is able to map controls on the device to that layout the `mapping` property will be set to the string `standard`.
* `connected`: A boolean indicating whether the gamepad is still connected to the system. If this is so the value is `True`; if not, it is `False`.
* `buttons`: An array of `GamepadButton` objects representing the buttons present on the device. Each `GamepadButton` has a `pressed` and a `value` property:
    + The `pressed` property is a boolean indicating whether the button is currently pressed (`true`) or unpressed (`false`).
    + The `value` property is a floating point value used to enable representing analog buttons, such as the triggers on many modern gamepads. The values are normalized to the range 0.0..1.0, with 0.0 representing a button that is not pressed, and 1.0 representing a button that is fully pressed.
* `axes`: An array representing the controls with axes present on the device (e.g. analog thumb sticks). Each entry in the array is a floating point value in the range -1.0 - 1.0, representing the axis position from the lowest value (-1.0) to the highest value (1.0).
* `timestamp`: This returns a `DOMHighResTimeStamp` representing the last time the data for this gamepad was updated, allowing developers to determine if the `axes` and `button` data have been updated from the hardware. The value must be relative to the `navigationStart` attribute of the `PerformanceTiming` (en-US) interface. Values are monotonically increasing, meaning that they can be compared to determine the ordering of updates, as newer values will always be greater than or equal to older values. Note that this property is not currently supported in Firefox.

**Примечание:** The Gamepad object is available on the `gamepadconnected` event rather than the `Window` object itself, for security reasons. Once we have a reference to it, we can query its properties for information about the current state of the gamepad. Behind the scenes, this object will be updated every time the gamepad's state changes.

### Using button information

Let's look at a simple example that displays connection information for one gamepad (it ignores subsequent gamepad connections) and allows you to move a ball around the screen using the four gamepad buttons on the right hand side of the gamepad. You can view the demo live, and find the source code on Github.

To start with, we declare some variables: The `gamepadInfo` paragraph that the connection info is written into, the `ball` that we want to move, the `start` variable that acts as the ID for `requestAnimation Frame`, the `a` and `b` variables that act as position modifiers for moving the ball, and the shorthand variables that will be used for the `requestAnimationFrame()` and `cancelAnimationFrame()` cross browser forks.

```js
var gamepadInfo = document.getElementById("gamepad-info");
var ball = document.getElementById("ball");
var start;
var a = 0;
var b = 0;
```

Next we use the `gamepadconnected` event to check for a gamepad being connected. When one is connected, we grab the gamepad using `Navigator.getGamepads()` `[0]`, print information about the gamepad into our gamepad info `div`, and fire the `gameLoop()` function that starts the whole ball movement process up.

```js
window.addEventListener("gamepadconnected", function (e) {
  var gp = navigator.getGamepads()[e.gamepad.index];
  gamepadInfo.innerHTML =
    "Gamepad connected at index " +
    gp.index +
    ": " +
    gp.id +
    ". It has " +
    gp.buttons.length +
    " buttons and " +
    gp.axes.length +
    " axes.";

  gameLoop();
});
```

Now we use the `gamepaddisconnected` event to check if the gamepad is disconnected again. If so, we stop the `requestAnimationFrame()` loop (see below) and revert the gamepad information back to what it was originally.

```js
window.addEventListener("gamepaddisconnected", function (e) {
  gamepadInfo.innerHTML = "Waiting for gamepad.";

  cancelRequestAnimationFrame(start);
});
```

Chrome does things differently here. Instead of constantly storing the gamepad's latest state in a variable it only stores a snapshot, so to do the same thing in Chrome you have to keep polling it and then only use the `Gamepad` object in code when it is available. We have done this below using `Window.setInterval()` (en-US); once the object is available the gamepad info is outputted, the game loop is started, and the interval is cleared using `Window.clearInterval()` (en-US). Note that in older versions of Chrome `Navigator.getGamepads()` is implemented with a `webkit` prefix. We attempt to detect and handle both the prefixed version and the standard version of the function for backwards compatibility.

```js
var interval;

if (!("ongamepadconnected" in window)) {
  // No gamepad events available, poll instead.
  interval = setInterval(pollGamepads, 500);
}

function pollGamepads() {
  var gamepads = navigator.getGamepads
    ? navigator.getGamepads()
    : navigator.webkitGetGamepads
      ? navigator.webkitGetGamepads
      : [];
  for (var i = 0; i < gamepads.length; i++) {
    var gp = gamepads[i];
    if (gp) {
      gamepadInfo.innerHTML =
        "Gamepad connected at index " +
        gp.index +
        ": " +
        gp.id +
        ". It has " +
        gp.buttons.length +
        " buttons and " +
        gp.axes.length +
        " axes.";
      gameLoop();
      clearInterval(interval);
    }
  }
}
```

Now on to the main game loop. In each execution of the loop we check if one of four buttons is being pressed; if so, we update the values of the `a` and `b` movement variables appropriately, then update the `left` (en-US) and `top` properties, changing their values to the current values of `a` and `b` respectively. This has the effect of moving the ball around the screen. In current versions of Chrome (version 34 as of this writing) the button values are stored as an array of double values, instead of `GamepadButton` objects. This is fixed in development versions.

After all this is done, we use our `requestAnimationFrame()` to request the next animation frame, running `gameLoop()` again.

```js
function buttonPressed(b) {
  if (typeof b == "object") {
    return b.pressed;
  }
  return b == 1.0;
}

function gameLoop() {
  var gamepads = navigator.getGamepads
    ? navigator.getGamepads()
    : navigator.webkitGetGamepads
      ? navigator.webkitGetGamepads
      : [];
  if (!gamepads) {
    return;
  }

  var gp = gamepads[0];
  if (buttonPressed(gp.buttons[0])) {
    b--;
  } else if (buttonPressed(gp.buttons[2])) {
    b++;
  }
  if (buttonPressed(gp.buttons[1])) {
    a++;
  } else if (buttonPressed(gp.buttons[3])) {
    a--;
  }

  ball.style.left = a \* 2 + "px";
  ball.style.top = b \* 2 + "px";

  start = requestAnimationFrame(gameLoop);
}
```

Complete example: Displaying gamepad state
------------------------------------------

This example shows how to use the `Gamepad` object, as well as the `gamepadconnected` and `gamepaddisconnected` events in order to display the state of all gamepads connected to the system. You can find a working demo and look at the full source code on Github.

```js
var haveEvents = "ongamepadconnected" in window;
var controllers = {};

function connecthandler(e) {
  addgamepad(e.gamepad);
}

function addgamepad(gamepad) {
  controllers[gamepad.index] = gamepad;

  var d = document.createElement("div");
  d.setAttribute("id", "controller" + gamepad.index);

  var t = document.createElement("h1");
  t.appendChild(document.createTextNode("gamepad: " + gamepad.id));
  d.appendChild(t);

  var b = document.createElement("div");
  b.className = "buttons";
  for (var i = 0; i < gamepad.buttons.length; i++) {
    var e = document.createElement("span");
    e.className = "button";
    //e.id = "b" + i;
    e.innerHTML = i;
    b.appendChild(e);
  }

  d.appendChild(b);

  var a = document.createElement("div");
  a.className = "axes";

  for (var i = 0; i < gamepad.axes.length; i++) {
    var p = document.createElement("progress");
    p.className = "axis";
    //p.id = "a" + i;
    p.setAttribute("max", "2");
    p.setAttribute("value", "1");
    p.innerHTML = i;
    a.appendChild(p);
  }

  d.appendChild(a);

  // See https://github.com/luser/gamepadtest/blob/master/index.html
  var start = document.getElementById("start");
  if (start) {
    start.style.display = "none";
  }

  document.body.appendChild(d);
  requestAnimationFrame(updateStatus);
}

function disconnecthandler(e) {
  removegamepad(e.gamepad);
}

function removegamepad(gamepad) {
  var d = document.getElementById("controller" + gamepad.index);
  document.body.removeChild(d);
  delete controllers[gamepad.index];
}

function updateStatus() {
  if (!haveEvents) {
    scangamepads();
  }

  var i = 0;
  var j;

  for (j in controllers) {
    var controller = controllers[j];
    var d = document.getElementById("controller" + j);
    var buttons = d.getElementsByClassName("button");

    for (i = 0; i < controller.buttons.length; i++) {
      var b = buttons[i];
      var val = controller.buttons[i];
      var pressed = val == 1.0;
      if (typeof val == "object") {
        pressed = val.pressed;
        val = val.value;
      }

      var pct = Math.round(val \* 100) + "%";
      b.style.backgroundSize = pct + " " + pct;

      if (pressed) {
        b.className = "button pressed";
      } else {
        b.className = "button";
      }
    }

    var axes = d.getElementsByClassName("axis");
    for (i = 0; i < controller.axes.length; i++) {
      var a = axes[i];
      a.innerHTML = i + ": " + controller.axes[i].toFixed(4);
      a.setAttribute("value", controller.axes[i] + 1);
    }
  }

  requestAnimationFrame(updateStatus);
}

function scangamepads() {
  var gamepads = navigator.getGamepads
    ? navigator.getGamepads()
    : navigator.webkitGetGamepads
      ? navigator.webkitGetGamepads()
      : [];
  for (var i = 0; i < gamepads.length; i++) {
    if (gamepads[i]) {
      if (gamepads[i].index in controllers) {
        controllers[gamepads[i].index] = gamepads[i];
      } else {
        addgamepad(gamepads[i]);
      }
    }
  }
}

window.addEventListener("gamepadconnected", connecthandler);
window.addEventListener("gamepaddisconnected", disconnecthandler);

if (!haveEvents) {
  setInterval(scangamepads, 500);
}
```

Specifications
--------------

| Specification |
| --- |
| Gamepad # gamepad-interface |
| Gamepad Extensions # partial-gamepad-interface |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.