序章

何らかの音声体験を提供するアプリを操作した可能性が非常に高くなります。 テキストメッセージや通知を声に出して読むなど、テキスト読み上げ機能を備えたアプリである可能性があります。 また、SiriやGoogleアシスタントなどの音声認識機能を備えたアプリの場合もあります。

HTML5の登場により、Webプラットフォームで利用可能なAPIの数が急速に増加しました。 Web Speech API と呼ばれるAPIがいくつかあり、Web用のさまざまな種類の音声アプリケーションとエクスペリエンスをシームレスに構築できるように開発されています。 これらのAPIはまだかなり実験的なものですが、最近のすべてのブラウザーでそれらのほとんどのサポートが増えています。

この記事では、ランダムな引用を取得し、引用を表示し、ブラウザーが引用を読み上げるためにテキスト読み上げを使用する機能をユーザーに提供するアプリケーションを構築します。

前提条件

このチュートリアルを完了するには、次のものが必要です。

このチュートリアルは、Nodev14.4.0で検証されました。 npm v6.14.5、 axios v0.19.2、 cors v2.8.5、 express v4.17.1、およびjQueryv3.5.1。

WebSpeechAPIの使用

Web Speech APIには、次の2つの主要なインターフェイスがあります。

  • SpeechSynthesis-テキスト読み上げアプリケーション用。 これにより、アプリはデバイスの音声シンセサイザーを使用してテキストコンテンツを読み上げることができます。 利用可能な音声タイプは、 SpeechSynthesisVoice オブジェクト、発声されるテキストはで表されます SpeechSynthesisUtterance 物体。 サポートテーブルを参照してください。 SpeechSynthesis ブラウザのサポートについて詳しく知るためのインターフェース。

  • SpeechRecognition-非同期音声認識を必要とするアプリケーション向け。 これにより、アプリは音声入力から音声コンテキストを認識できます。 A SpeechRecognition オブジェクトは、コンストラクターを使用して作成できます。 The SpeechGrammar アプリが認識すべき文法のセットを表すためのインターフェースが存在します。 サポートテーブルを参照してください。 SpeechRecognition ブラウザのサポートについて詳しく知るためのインターフェース。

このチュートリアルでは、 SpeechSynthesis.

参照を取得する

への参照を取得する SpeechSynthesis オブジェクトは、1行のコードで実行できます。

var synthesis = window.speechSynthesis;

次のコードスニペットは、ブラウザのサポートを確認する方法を示しています。

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;
} else {
  console.log('Text-to-speech not supported.');
}

次のことを確認すると非常に便利です SpeechSynthesis ブラウザが提供する機能を使用する前に、ブラウザでサポートされています。

利用可能な音声の取得

このステップでは、既存のコードに基づいて、利用可能な音声音声を取得します。 The getVoices() メソッドはのリストを返します SpeechSynthesisVoice デバイスで使用可能なすべての音声を表すオブジェクト。

次のコードスニペットを見てください。

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;

  // Regex to match all English language tags e.g en, en-US, en-GB
  var langRegex = /^en(-[a-z]{2})?$/i;

  // Get the available voices and filter the list to only have English speakers
  var voices = synthesis
    .getVoices()
    .filter((voice) => langRegex.test(voice.lang));

  // Log the properties of the voices in the list
  voices.forEach(function (voice) {
    console.log({
      name: voice.name,
      lang: voice.lang,
      uri: voice.voiceURI,
      local: voice.localService,
      default: voice.default,
    });
  });
} else {
  console.log('Text-to-speech not supported.');
}

コードのこのセクションでは、デバイスで使用可能な音声のリストを取得し、を使用してリストをフィルタリングします langRegex 英語を話す人だけに声を届けるための正規表現。 最後に、リスト内の音声をループして、それぞれのプロパティをコンソールに記録します。

発話の構築

このステップでは、を使用して音声発話を作成します SpeechSynthesisUtterance コンストラクターと使用可能なプロパティの設定値。

次のコードスニペットは、テキストを読むための音声発話を作成します "Hello World":

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;

  // Get the first `en` language voice in the list
  var voice = synthesis.getVoices().filter(function (voice) {
    return voice.lang === 'en';
  })[0];

  // Create an utterance object
  var utterance = new SpeechSynthesisUtterance('Hello World');

  // Set utterance properties
  utterance.voice = voice;
  utterance.pitch = 1.5;
  utterance.rate = 1.25;
  utterance.volume = 0.8;

  // Speak the utterance
  synthesis.speak(utterance);
} else {
  console.log('Text-to-speech not supported.');
}

ここで、あなたは最初のものを手に入れます en 利用可能な音声のリストからの言語音声。 次に、を使用して新しい発話を作成します SpeechSynthesisUtterance コンストラクタ。 次に、発話オブジェクトのいくつかのプロパティを次のように設定します。 voice, pitch, rate、 と volume. 最後に、それは使用して発話を話します speak() の方法 SpeechSynthesis.

注:発話で話すことができるテキストのサイズには制限があります。 各発話で話せるテキストの最大長は32,767文字です。

コンストラクターで発声するテキストを渡したことに注意してください。

発声するテキストを設定することもできます text 発話オブジェクトのプロパティ。

簡単な例を次に示します。

var synthesis = window.speechSynthesis;
var utterance = new SpeechSynthesisUtterance("Hello World");

// This overrides the text "Hello World" and is uttered instead
utterance.text = "My name is Glad.";

synthesis.speak(utterance);

これは、コンストラクターで渡されたテキストをオーバーライドします。

発話を話す

前のコードスニペットでは、 speak() 上のメソッド SpeechSynthesis 実例。 これで、 SpeechSynthesisUtterance の引数としてのインスタンス speak() 発話を話す方法。

var synthesis = window.speechSynthesis;

var utterance1 = new SpeechSynthesisUtterance("Hello World");
var utterance2 = new SpeechSynthesisUtterance("My name is Glad.");
var utterance3 = new SpeechSynthesisUtterance("I'm a web developer from Nigeria.");

synthesis.speak(utterance1);
synthesis.speak(utterance2);
synthesis.speak(utterance3);

あなたがでできる他のいくつかのことがあります SpeechSynthesis 発話の一時停止、再開、キャンセルなどのインスタンス。 従って pause(), resume()、 と cancel() メソッドは、 SpeechSynthesis 実例。

ステップ1—テキスト読み上げアプリを作成する

私たちはの基本的な側面を見てきました SpeechSynthesis インターフェース。 次に、テキスト読み上げアプリケーションの作成を開始します。 始める前に、マシンにNodeとnpmがインストールされていることを確認してください。

端末で次のコマンドを実行して、アプリのプロジェクトをセットアップし、依存関係をインストールします。

新しいプロジェクトディレクトリを作成します。

  1. mkdir web-speech-app

新しく作成されたプロジェクトディレクトリに移動します。

  1. cd web-speech-app

プロジェクトを初期化します。

  1. npm init -y

プロジェクトに必要な依存関係をインストールします- express, cors、 と axios:

  1. npm install express cors axios

を変更します "scripts" のセクション package.json 次のスニペットのように見えるファイル:

package.json
"scripts": {
  "start": "node server.js"
}

アプリケーションのプロジェクトを初期化したので、Expressを使用してアプリのサーバーをセットアップします。

新しいを作成します server.js ファイルを作成し、それに次のコンテンツを追加します。

server.js
const cors = require('cors');
const path = require('path');
const axios = require('axios');
const express = require('express');

const app = express();
const PORT = process.env.PORT || 5000;

app.set('port', PORT);

// Enable CORS (Cross-Origin Resource Sharing)
app.use(cors());

// Serve static files from the /public directory
app.use('/', express.static(path.join(__dirname, 'public')));

// A simple endpoint for fetching a random quote from QuotesOnDesign
app.get('/api/quote', (req, res) => {
  axios
    .get(
      'https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand'
    )
    .then((response) => {
      const [post] = response.data;
      const { title, content } = post || {};

      return title && content
        ? res.json({ status: 'success', data: { title, content } })
        : res
            .status(500)
            .json({ status: 'failed', message: 'Could not fetch quote.' });
    })
    .catch((err) =>
      res
        .status(500)
        .json({ status: 'failed', message: 'Could not fetch quote.' })
    );
});

app.listen(PORT, () => console.log(`> App server is running on port ${PORT}.`));

ここでは、Expressを使用してノードサーバーをセットアップします。 を使用してCORS(クロスオリジンリクエストシェアリング)を有効にしました cors() ミドルウェア。 また、 express.static() から静的ファイルを提供するミドルウェア /public プロジェクトルートのディレクトリ。 これにより、すぐに作成するインデックスページを提供できるようになります。

最後に、 GET /api/quote QuotesOnDesignAPIサービスからランダムな見積もりを取得するためのルート。 axios (PromiseベースのHTTPクライアントライブラリ)を使用してHTTPリクエストを作成しています。

QuotesOnDesignAPIからのサンプル応答は次のようになります。

Output
[ { "title": { "rendered": "Victor Papanek" }, "content": { "rendered": "<p>Any attempt to separate design, to make it a thing-by-itself, works counter to the inherent value of design as the primary, underlying matrix of life.</p>\n", "protected": false } } ]

注: QuotesOnDesignのAPIの変更の詳細については、4.0と5.0の間の変更を文書化したページを参照してください。

見積もりを正常に取得すると、見積もりは titlecontent で返されます data JSON応答のフィールド。 それ以外の場合は、JSON応答の失敗 500 HTTPステータスコードが返されます。

次に、アプリビューのインデックスページを作成します。

まず、新しいを作成します public プロジェクトのルートにあるフォルダー:

  1. mkdir public

次に、新しいを作成します index.html 新しく作成されたファイル public フォルダに次のコンテンツを追加します。

public / index.html
<html>

<head>
    <title>Daily Quotes</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>

<body class="position-absolute h-100 w-100">
    <div id="app" class="d-flex flex-wrap align-items-center align-content-center p-5 mx-auto w-50 position-relative"></div>

    <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
    <script src="main.js"></script>
</body>

</html>

これにより、アプリの基本的なインデックスページが1つだけ作成されます <div id="app"> これは、アプリのすべての動的コンテンツのマウントポイントとして機能します。

また、Bootstrap CDNへのリンクを追加して、アプリのデフォルトの Bootstrap4スタイルを取得しました。 また、DOM操作とAJAXリクエスト用の jQuery と、エレガントなSVGアイコン用のフェザーアイコンも含まれています。

ステップ2—メインスクリプトを作成する

これで、アプリを強化する最後の部分であるメインスクリプトにたどり着きました。 新しいを作成します main.js のファイル public アプリのディレクトリに次のコンテンツを追加します。

public / main.js
jQuery(function ($) {
  let app = $('#app');

  let SYNTHESIS = null;
  let VOICES = null;

  let QUOTE_TEXT = null;
  let QUOTE_PERSON = null;

  let VOICE_SPEAKING = false;
  let VOICE_PAUSED = false;
  let VOICE_COMPLETE = false;

  let iconProps = {
    'stroke-width': 1,
    'width': 48,
    'height': 48,
    'class': 'text-secondary d-none',
    'style': 'cursor: pointer'
  };

  function iconSVG(icon) {}

  function showControl(control) {}

  function hideControl(control) {}

  function getVoices() {}

  function resetVoice() {}

  function fetchNewQuote() {}

  function renderQuote(quote) {}

  function renderVoiceControls(synthesis, voice) {}

  function updateVoiceControls() {}

  function initialize() {}

  initialize();
});

このコードは jQuery DOMがロードされたときに関数を実行します。 あなたはへの参照を取得します #app 要素を作成し、いくつかの変数を初期化します。 また、次のセクションで実装するいくつかの空の関数を宣言します。 最後に、 initialize() アプリケーションを初期化する関数。

The iconProps 変数には、フェザーアイコンをSVGとしてDOMにレンダリングするために使用されるいくつかのプロパティが含まれています。

そのコードを配置すると、関数の実装を開始する準備が整います。 を変更します public/main.js 次の機能を実装するファイル:

public / main.js
// Gets the SVG markup for a Feather icon
function iconSVG(icon) {
  let props = $.extend(iconProps, { id: icon });
  return feather.icons[icon].toSvg(props);
}

// Shows an element
function showControl(control) {
  control.addClass('d-inline-block').removeClass('d-none');
}

// Hides an element
function hideControl(control) {
  control.addClass('d-none').removeClass('d-inline-block');
}

// Get the available voices, filter the list to have only English filters
function getVoices() {
  // Regex to match all English language tags e.g en, en-US, en-GB
  let langRegex = /^en(-[a-z]{2})?$/i;

  // Get the available voices and filter the list to only have English speakers
  VOICES = SYNTHESIS.getVoices()
    .filter(function (voice) {
      return langRegex.test(voice.lang);
    })
    .map(function (voice) {
      return {
        voice: voice,
        name: voice.name,
        lang: voice.lang.toUpperCase(),
      };
    });
}

// Reset the voice variables to the defaults
function resetVoice() {
  VOICE_SPEAKING = false;
  VOICE_PAUSED = false;
  VOICE_COMPLETE = false;
}

The iconSVG(icon) 関数は、引数としてフェザーアイコン名の文字列を取ります(例: 'play-circle')そしてアイコンのSVGマークアップを返します。 フェザーのWebサイトをチェックして、使用可能なフェザーアイコンの完全なリストを確認してください。 APIの詳細については、フェザードキュメントも確認してください。

The getVoices() 関数はを使用します SYNTHESIS デバイスで使用可能なすべての音声のリストをフェッチするオブジェクト。 次に、正規表現を使用してリストをフィルタリングし、英語を話す人だけの声を取得します。

次に、DOMで引用符をフェッチおよびレンダリングするための関数を実装します。 を変更します public/main.js 次の機能を実装するファイル:

public / main.js
function fetchNewQuote() {
  // Clean up the #app element
  app.html('');

  // Reset the quote variables
  QUOTE_TEXT = null;
  QUOTE_PERSON = null;

  // Reset the voice variables
  resetVoice();

  // Pick a voice at random from the VOICES list
  let voice =
    VOICES && VOICES.length > 0
      ? VOICES[Math.floor(Math.random() * VOICES.length)]
      : null;

  // Fetch a quote from the API and render the quote and voice controls
  $.get('/api/quote', function (quote) {
    renderQuote(quote.data);
    SYNTHESIS && renderVoiceControls(SYNTHESIS, voice || null);
  });
}

function renderQuote(quote) {
  // Create some markup for the quote elements
  let quotePerson = $('<h1 id="quote-person" class="mb-2 w-100"></h1>');
  let quoteText = $('<div id="quote-text" class="h3 py-5 mb-4 w-100 font-weight-light text-secondary border-bottom border-gray"></div>');

  // Add the quote data to the markup
  quotePerson.html(quote.title.rendered);
  quoteText.html(quote.content.rendered);

  // Attach the quote elements to the DOM
  app.append(quotePerson);
  app.append(quoteText);

  // Update the quote variables with the new data
  QUOTE_TEXT = quoteText.text();
  QUOTE_PERSON = quotePerson.text();
}

ここで fetchNewQuote() メソッドでは、最初にアプリの要素と変数をリセットします。 次に、を使用してランダムに音声を選択します Math.random() に保存されている音声のリストから VOICES 変数。 あなたが使う $.get() にAJAXリクエストを送信するには /api/quote エンドポイント。ランダムな見積もりを取得し、見積もりデータを音声コントロールと一緒にビューにレンダリングします。

The renderQuote(quote) メソッドは引数としてquoteオブジェクトを受け取り、その内容をDOMに追加します。 最後に、quote変数を更新します。 QUOTE_TEXTQUOTE_PERSON.

あなたが見れば fetchNewQuote() 関数、あなたはあなたが呼び出しを行ったことに気付くでしょう renderVoiceControls() 関数。 この機能は、音声出力の再生、一時停止、および停止のコントロールをレンダリングする役割を果たします。 また、使用中の現在の音声と言語をレンダリングします。

に次の変更を加えます public/main.js を実装するファイル renderVoiceControls() 関数:

public / main.js
function renderVoiceControls(synthesis, voice) {
  let controlsPane = $('<div id="voice-controls-pane" class="d-flex flex-wrap w-100 align-items-center align-content-center justify-content-between"></div>');

  let voiceControls = $('<div id="voice-controls"></div>');

  // Create the SVG elements for the voice control buttons
  let playButton = $(iconSVG('play-circle'));
  let pauseButton = $(iconSVG('pause-circle'));
  let stopButton = $(iconSVG('stop-circle'));

  // Helper function to enable pause state for the voice output
  let paused = function () {
    VOICE_PAUSED = true;
    updateVoiceControls();
  };

  // Helper function to disable pause state for the voice output
  let resumed = function () {
    VOICE_PAUSED = false;
    updateVoiceControls();
  };

  // Click event handler for the play button
  playButton.on('click', function (evt) {});

  // Click event handler for the pause button
  pauseButton.on('click', function (evt) {});

  // Click event handler for the stop button
  stopButton.on('click', function (evt) {});

  // Add the voice controls to their parent element
  voiceControls.append(playButton);
  voiceControls.append(pauseButton);
  voiceControls.append(stopButton);

  // Add the voice controls parent to the controlsPane element
  controlsPane.append(voiceControls);

  // If voice is available, add the voice info element to the controlsPane
  if (voice) {
    let currentVoice = $('<div class="text-secondary font-weight-normal"><span class="text-dark font-weight-bold">' + voice.name + '</span> (' + voice.lang + ')</div>');

    controlsPane.append(currentVoice);
  }

  // Add the controlsPane to the DOM
  app.append(controlsPane);

  // Show the play button
  showControl(playButton);
}

ここでは、音声コントロールとコントロールペインのコンテナ要素を作成します。 あなたは iconSVG() コントロールボタンのSVGマークアップを取得し、ボタン要素も作成するために以前に作成された関数。 あなたは paused()resumed() ボタンのイベントハンドラーを設定するときに使用されるヘルパー関数。

最後に、音声制御ボタンと音声情報をDOMにレンダリングします。 また、最初は再生ボタンのみが表示されるように構成されています。

次に、前のセクションで定義した音声コントロールボタンのクリックイベントハンドラーを実装します。

次のコードスニペットに示すように、イベントハンドラーを設定します。

public / main.js
// Click event handler for the play button
playButton.on('click', function (evt) {
  evt.preventDefault();

  if (VOICE_SPEAKING) {
    // If voice is paused, it is resumed when the playButton is clicked
    if (VOICE_PAUSED) synthesis.resume();
    return resumed();
  } else {
    // Create utterances for the quote and the person
    let quoteUtterance = new SpeechSynthesisUtterance(QUOTE_TEXT);
    let personUtterance = new SpeechSynthesisUtterance(QUOTE_PERSON);

    // Set the voice for the utterances if available
    if (voice) {
      quoteUtterance.voice = voice.voice;
      personUtterance.voice = voice.voice;
    }

    // Set event listeners for the quote utterance
    quoteUtterance.onpause = paused;
    quoteUtterance.onresume = resumed;
    quoteUtterance.onboundary = updateVoiceControls;

    // Set the listener to activate speaking state when the quote utterance starts
    quoteUtterance.onstart = function (evt) {
      VOICE_COMPLETE = false;
      VOICE_SPEAKING = true;
      updateVoiceControls();
    };

    // Set event listeners for the person utterance
    personUtterance.onpause = paused;
    personUtterance.onresume = resumed;
    personUtterance.onboundary = updateVoiceControls;

    // Refresh the app and fetch a new quote when the person utterance ends
    personUtterance.onend = fetchNewQuote;

    // Speak the utterances
    synthesis.speak(quoteUtterance);
    synthesis.speak(personUtterance);
  }
});

// Click event handler for the pause button
pauseButton.on('click', function (evt) {
  evt.preventDefault();

  // Pause the utterance if it is not in paused state
  if (VOICE_SPEAKING) synthesis.pause();
  return paused();
});

// Click event handler for the stop button
stopButton.on('click', function (evt) {
  evt.preventDefault();

  // Clear the utterances queue
  if (VOICE_SPEAKING) synthesis.cancel();
  resetVoice();

  // Set the complete status of the voice output
  VOICE_COMPLETE = true;
  updateVoiceControls();
});

ここでは、音声コントロールボタンのクリックイベントリスナーを設定します。 再生ボタンをクリックすると、 quoteUtterance そして personUtterance. ただし、音声出力が一時停止状態の場合は再開します。

あなたが設定した VOICE_SPEAKINGtrue の中に onstart のイベントハンドラ quoteUtterance. アプリはまた、更新して新しい見積もりを取得します personUtterance 終了します。

Pause ボタンは音声出力を一時停止し、 Stop ボタンは音声出力を終了し、キューからすべての発話を削除します。 cancel() の方法 SpeechSynthesis インターフェース。 コードは updateVoiceControls() 適切なボタンを表示するために毎回機能します。

あなたはいくつかの電話をかけ、 updateVoiceControls() 前のコードスニペットで機能します。 この関数は、音声コントロールを更新して、音声状態変数に基づいて適切なコントロールを表示する役割を果たします。

に次の変更を加えます public/main.js を実装するファイル updateVoiceControls() 関数:

public / main.js
function updateVoiceControls() {
  // Get a reference to each control button
  let playButton = $('#play-circle');
  let pauseButton = $('#pause-circle');
  let stopButton = $('#stop-circle');

  if (VOICE_SPEAKING) {
    // Show the stop button if speaking is in progress
    showControl(stopButton);

    // Toggle the play and pause buttons based on paused state
    if (VOICE_PAUSED) {
      showControl(playButton);
      hideControl(pauseButton);
    } else {
      hideControl(playButton);
      showControl(pauseButton);
    }
  } else {
    // Show only the play button if no speaking is in progress
    showControl(playButton);
    hideControl(pauseButton);
    hideControl(stopButton);
  }
}

コードのこのセクションでは、最初に各音声コントロールボタン要素への参照を取得します。 次に、音声出力のさまざまな状態で表示する音声制御ボタンを指定します。

これで、を実装する準備ができました initialize() 関数。 この関数は、アプリケーションの初期化を担当します。 次のコードスニペットをに追加します public/main.js を実装するファイル initialize() 関数。

public / main.js
function initialize() {
  if ('speechSynthesis' in window) {
    SYNTHESIS = window.speechSynthesis;

    let timer = setInterval(function () {
      let voices = SYNTHESIS.getVoices();

      if (voices.length > 0) {
        getVoices();
        fetchNewQuote();
        clearInterval(timer);
      }
    }, 200);
  } else {
    let message = 'Text-to-speech not supported by your browser.';

    // Create the browser notice element
    let notice = $('<div class="w-100 py-4 bg-danger font-weight-bold text-white position-absolute text-center" style="bottom:0; z-index:10">' + message + '</div>');

    fetchNewQuote();
    console.log(message);

    // Display non-support info on DOM
    $(document.body).append(notice);
  }
}

このコードは最初に speechSynthesis で利用可能です window グローバルオブジェクトに割り当てられ、 SYNTHESIS 利用可能な場合は変数。 次に、使用可能な音声のリストを取得するための間隔を設定します。

で既知の非同期動作があるため、ここでは間隔を使用しています SpeechSynthesis.getVoices() これにより、音声がまだロードされていないため、最初の呼び出しで空の配列が返されます。 間隔は、ランダムな引用をフェッチして間隔をクリアする前に、音声のリストを確実に取得します。

これで、テキスト読み上げアプリが正常に完了しました。 ターミナルで次のコマンドを実行すると、アプリを起動できます。

  1. npm start

アプリはポートで実行されます 5000 利用可能な場合。

訪問 localhost:5000 ブラウザでアプリを観察します。

次に、再生ボタンを操作して、引用が話されているのを聞きます。

結論

このチュートリアルでは、Web Speech APIを使用して、Web用のテキスト読み上げアプリを作成しました。 Web Speech APIの詳細については、MDN WebDocsいくつかの役立つリソースを見つけることができます。

アプリの改良を続けたい場合は、音量コントロール、音声ピッチコントロール、速度/速度コントロール、発声されたテキストの割合など、まだ実装して実験できる興味深い機能がいくつかあります。

このチュートリアルの完全なソースコードは、GitHubで入手できます。