Browse Source

Japanese translation of offscreencanvas (#136)

* doc: translate ja of offscreencanvas

* doc: translate ja of offscreencanvas completed
naotaro 4 years ago
parent
commit
16143b00c2
1 changed files with 1144 additions and 0 deletions
  1. 1144 0
      threejs/lessons/ja/threejs-offscreencanvas.md

+ 1144 - 0
threejs/lessons/ja/threejs-offscreencanvas.md

@@ -0,0 +1,1144 @@
+Title: Three.jsのOffscreenCanvas
+Description: three.jsでweb workerを使う方法
+TOC: Web WorkerでOffscreenCanvasを使用する
+
+[`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas)は新しいブラウザの機能で現在はChromeでしか利用できませんが、他のブラウザにも来るようです。
+`OffscreenCanvas` はWeb Workerでキャンバスにレンダリングできます。
+複雑な3Dシーンのレンダリングなど重い作業をWeb Workerで行い負荷を軽減させ、ブラウザのレスポンスを低下させない方法です。
+また、データが読み込まれWorkerで解析されてるのでページ読み込み中にページ表示の途切れは少ないでしょう。
+
+OffscreenCanvasの利用を*開始*するのは非常に簡単です。
+[レスポンシブデザインの記事](threejs-responsive.html)から3つのキューブを回転させるコードに修正してみましょう。
+
+通常はWorkerのコードを別ファイルに分離しますが、このサイトのほとんどのサンプルコードではスクリプトをHTMLファイルに埋め込んでいます。
+
+ここでは `offscreencanvas-cubes.js` というファイルを作成し、[レスポンシブデザインの例](threejs-responsive.html)から全てのJavaScriptをコピーして下さい。
+そして、Workerで実行するために必要な変更を行います。
+
+HTMLファイルにはJavaScriptのいくつかの処理が必要です。
+まず最初に行う必要があるのはキャンバスを検索し、`canvas.transferControlToOffscreen` 呼び出してキャンバスのコントロールをオフスクリーンに転送します。
+
+```js
+function main() {
+  const canvas = document.querySelector('#c');
+  const offscreen = canvas.transferControlToOffscreen();
+
+  ...
+```
+
+`new Worker(pathToScript, {type: 'module'})`でWorkerを起動し、`offscreen` オブジェクトを渡します。
+
+```js
+function main() {
+  const canvas = document.querySelector('#c');
+  const offscreen = canvas.transferControlToOffscreen();
+  const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
+  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
+}
+main();
+```
+
+ここで重要なのはWorkerが `DOM` にアクセスできない事です。
+HTML要素の参照やマウスイベントやキーボードイベントを受け取る事もできません。
+Workerは、送られたメッセージに返信してWebページにメッセージを送り返す事だけです。
+
+Workerにメッセージを送信するには[`worker.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage)を呼び出し、1つまたは2つの引数を渡します。
+1つ目の引数は[クローン](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)されるJavaScriptオブジェクトでWorkerに送ります。
+2番目の引数は任意でWorkerに *転送* したい最初のオブジェクトです。
+このオブジェクトはクローンされません。
+その代わりに *転送* され、メインページには存在しなくなります。
+存在しなくなるというのはおそらく間違った説明であり、むしろ取り除かれます。
+クローンではなく、特定のタイプのオブジェクトのみを転送する事ができます。
+転送するオブジェクトには `OffscreenCanvas` が含まれているので、1度転送した `offscreen` オブジェクトをメインページに戻しても意味がありません。
+
+Workerは `onmessage` ハンドラからメッセージを受け取ります。
+`postMessage` に渡したオブジェクトはWorkerの `onmessage` ハンドラに渡され `event.data` を更新します。
+上記のコードではWorkerに渡すオブジェクトに `type: 'main'` を宣言しています。
+このオブジェクトはブラウザには何の意味もありません。Workerで使うためだけのものです。
+`type` に基づいて、Worker内で別の関数を呼び出すハンドラを作成します。
+あとは必要に応じて関数を追加し、メインページから簡単に呼び出す事ができます。
+
+```js
+const handlers = {
+  main,
+};
+
+self.onmessage = function(e) {
+  const fn = handlers[e.data.type];
+  if (!fn) {
+    throw new Error('no handler for type: ' + e.data.type);
+  }
+  fn(e.data);
+};
+```
+
+上記コードのように `type` に基づいてハンドラを検索し、メインページから送られてきた `data` を渡します。
+あとは[レスポンシブデザインの記事](threejs-responsive.html)から `offscreencanvas-cubes.js` に貼り付けた `main` を変更するだけです。
+
+DOMからキャンバスを探すのではなく、イベントデータからキャンバスを受け取ります。
+
+```js
+-function main() {
+-  const canvas = document.querySelector('#c');
++function main(data) {
++  const {canvas} = data;
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  ...
+
+```
+
+最初の問題はWorkerからDOMを参照できず、`resizeRendererToDisplaySize` が `canvas.clientWidth` と `canvas.clientHeight` を参照できない事です。
+`clientWidth` と `canvas.clientHeight` はDOMの値です。
+
+元のコードは以下の通りです。
+
+```js
+function resizeRendererToDisplaySize(renderer) {
+  const canvas = renderer.domElement;
+  const width = canvas.clientWidth;
+  const height = canvas.clientHeight;
+  const needResize = canvas.width !== width || canvas.height !== height;
+  if (needResize) {
+    renderer.setSize(width, height, false);
+  }
+  return needResize;
+}
+```
+
+DOMを参照できないため、変更したサイズの値をWorkerに送る必要があります。
+そこでグローバルな状態を追加し、幅と高さを維持するようにしましょう。
+
+```js
+const state = {
+  width: 300,  // canvas default
+  height: 150,  // canvas default
+};
+```
+
+これらの値を更新するための `'size'` ハンドラを追加してみます。
+
+```js
++function size(data) {
++  state.width = data.width;
++  state.height = data.height;
++}
+
+const handlers = {
+  main,
++  size,
+};
+```
+
+これで `resizeRendererToDisplaySize` を変更すると `state.width` と `state.height` が使えるようになりました。
+
+```js
+function resizeRendererToDisplaySize(renderer) {
+  const canvas = renderer.domElement;
+-  const width = canvas.clientWidth;
+-  const height = canvas.clientHeight;
++  const width = state.width;
++  const height = state.height;
+  const needResize = canvas.width !== width || canvas.height !== height;
+  if (needResize) {
+    renderer.setSize(width, height, false);
+  }
+  return needResize;
+}
+```
+
+以下も同様の変更が必要です。
+
+```js
+function render(time) {
+  time *= 0.001;
+
+  if (resizeRendererToDisplaySize(renderer)) {
+-    camera.aspect = canvas.clientWidth / canvas.clientHeight;
++    camera.aspect = state.width / state.height;
+    camera.updateProjectionMatrix();
+  }
+
+  ...
+```
+
+メインページに戻りページのリサイズの度に `size` イベントを送信します。
+
+```js
+const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
+worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
+
++function sendSize() {
++  worker.postMessage({
++    type: 'size',
++    width: canvas.clientWidth,
++    height: canvas.clientHeight,
++  });
++}
++
++window.addEventListener('resize', sendSize);
++sendSize();
+```
+
+初期サイズを送るために1度sendSizeを呼んでいます。
+
+ブラウザが `OffscreenCanvas` を完全にサポートしていると仮定して、これらの変更を行うだけで動作するはずです。
+実行する前にブラウザが `OffscreenCanvas` を実際にサポートしているか確認し、サポートしていない場合はエラーを表示してみましょう。
+まずはエラーを表示するためのHTMLを追加します。
+
+```html
+<body>
+  <canvas id="c"></canvas>
++  <div id="noOffscreenCanvas" style="display:none;">
++    <div>no OffscreenCanvas support</div>
++  </div>
+</body>
+```
+
+そして、CSSを追加します。
+
+```css
+#noOffscreenCanvas {
+    display: flex;
+    width: 100vw;
+    height: 100vh;
+    align-items: center;
+    justify-content: center;
+    background: red;
+    color: white;
+}
+```
+
+ブラウザが `OffscreenCanvas` をサポートしているか確認するためには `transferControlToOffscreen` を呼びます。
+
+```js
+function main() {
+  const canvas = document.querySelector('#c');
++  if (!canvas.transferControlToOffscreen) {
++    canvas.style.display = 'none';
++    document.querySelector('#noOffscreenCanvas').style.display = '';
++    return;
++  }
+  const offscreen = canvas.transferControlToOffscreen();
+  const worker = new Worker('offscreencanvas-picking.js', {type: 'module});
+  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
+
+  ...
+```
+
+ブラウザが `OffscreenCanvas` をサポートしていれば、このサンプルは動作するはずです。
+
+{{{example url="../threejs-offscreencanvas.html" }}}
+
+これは素晴らしい事ですが、今の所は全てのブラウザが `OffscreenCanvas` をサポートしている訳ではなく、
+`OffscreenCanvas` サポートありとサポートなしの両方で動作するコードに変更し、サポートなしの場合はメインページのキャンバスを通常のように表示します。
+
+> 余談ですがページをレスポンシブにするためにOffscreenCanvasが必要な場合、フォールバックを持つ意味がよくわかりません。
+> メインページで実行するかWorkerで実行するかには、Workerで実行している時にメインページで実行している時よりも多くの事ができるように
+> 調整するかもしれません。何をするかは本当にあなた次第です。
+
+まず最初にthree.jsのコードとWorkerの固有コードを分離しましょう。
+これでメインページとWorkerの両方で同じコードを使う事ができます。
+つまり、3つのファイルを持つ事になります。
+
+1. htmlファイル
+
+   `threejs-offscreencanvas-w-fallback.html`
+
+2. three.jsを含むJavaScriptコード
+
+   `shared-cubes.js`
+
+3. workerをサポートするコード
+
+   `offscreencanvas-worker-cubes.js`
+
+`shared-cubes.js` と `offscreencanvas-worker-cubes.js` は前の `offscreencanvas-cubes.js` ファイルを分割したものです。
+
+まず `offscreencanvas-cubes.js` を全て `shared-cube.js` にコピーします。
+次にHTMLファイルには既に `main` があり、`init` と `state` をエクスポートする必要があるため `main` の名前を `init` に変更します。
+
+```js
+import * as THREE from './resources/threejs/r119/build/three.module.js';
+
+-const state = {
++export const state = {
+  width: 300,   // canvas default
+  height: 150,  // canvas default
+};
+
+-function main(data) {
++export function init(data) {
+  const {canvas} = data;
+  const renderer = new THREE.WebGLRenderer({canvas});
+```
+
+そして、three.js関連以外の部分だけを切り取ります。
+
+```js
+-function size(data) {
+-  state.width = data.width;
+-  state.height = data.height;
+-}
+-
+-const handlers = {
+-  main,
+-  size,
+-};
+-
+-self.onmessage = function(e) {
+-  const fn = handlers[e.data.type];
+-  if (!fn) {
+-    throw new Error('no handler for type: ' + e.data.type);
+-  }
+-  fn(e.data);
+-};
+```
+
+削除した部分を `offscreencanvas-worker-cubes.js` にコピーして `shared-cubes.js` をインポートし、`main` の代わりに `init` を呼び出します。
+
+```js
+import {init, state} from './shared-cubes.js';
+
+function size(data) {
+  state.width = data.width;
+  state.height = data.height;
+}
+
+const handlers = {
+-  main,
++  init,
+  size,
+};
+
+self.onmessage = function(e) {
+  const fn = handlers[e.data.type];
+  if (!fn) {
+    throw new Error('no handler for type: ' + e.data.type);
+  }
+  fn(e.data);
+};
+```
+
+同様にメインページに `shared-cubes.js` を含める必要があります。
+
+```html
+<script type="module">
++import {init, state} from './shared-cubes.js';
+```
+前に追加したHTMLとCSSを削除します。
+
+```html
+<body>
+  <canvas id="c"></canvas>
+-  <div id="noOffscreenCanvas" style="display:none;">
+-    <div>no OffscreenCanvas support</div>
+-  </div>
+</body>
+```
+
+そして、CSSは以下のようになります。
+
+```css
+-#noOffscreenCanvas {
+-    display: flex;
+-    width: 100vw;
+-    height: 100vh;
+-    align-items: center;
+-    justify-content: center;
+-    background: red;
+-    color: white;
+-}
+```
+
+次にブラウザが `OffscreenCanvas` をサポートありなしに応じて、メインページのコードを変更して起動関数を呼び出すようにしてみましょう。
+
+```js
+function main() {
+  const canvas = document.querySelector('#c');
+-  if (!canvas.transferControlToOffscreen) {
+-    canvas.style.display = 'none';
+-    document.querySelector('#noOffscreenCanvas').style.display = '';
+-    return;
+-  }
+-  const offscreen = canvas.transferControlToOffscreen();
+-  const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
+-  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
++  if (canvas.transferControlToOffscreen) {
++    startWorker(canvas);
++  } else {
++    startMainPage(canvas);
++  }
+  ...
+```
+
+Workerのセットアップコードを全て `startWorker` の中に移動します。
+
+```js
+function startWorker(canvas) {
+  const offscreen = canvas.transferControlToOffscreen();
+  const worker = new Worker('offscreencanvas-worker-cubes.js', {type: 'module'});
+  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
+
+  function sendSize() {
+    worker.postMessage({
+      type: 'size',
+      width: canvas.clientWidth,
+      height: canvas.clientHeight,
+    });
+  }
+
+  window.addEventListener('resize', sendSize);
+  sendSize();
+
+  console.log('using OffscreenCanvas');
+}
+```
+
+そして `main` の代わりに `init` を送信します。
+
+```js
+-  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
++  worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
+```
+
+メインページで開始するには次のようにします。
+
+```js
+function startMainPage(canvas) {
+  init({canvas});
+
+  function sendSize() {
+    state.width = canvas.clientWidth;
+    state.height = canvas.clientHeight;
+  }
+  window.addEventListener('resize', sendSize);
+  sendSize();
+
+  console.log('using regular canvas');
+}
+```
+
+このサンプルコードではOffscreenCanvasで実行、またはメインページで実行されるようにフォールバックしています。
+
+{{{example url="../threejs-offscreencanvas-w-fallback.html" }}}
+
+比較的簡単でした。ピッキングしてみましょう。
+[ピッキングの記事](threejs-picking.html)にある `RayCaster` の例からコードをいくつか取り出し、画面外でオフスクリーンが動作するようにします。
+
+`shared-cube.js` を `shared-picking.js` にコピーし、ピッキング部分を追加してみましょう。
+この例では `PickHelper` をコピーします。
+
+```js
+class PickHelper {
+  constructor() {
+    this.raycaster = new THREE.Raycaster();
+    this.pickedObject = null;
+    this.pickedObjectSavedColor = 0;
+  }
+  pick(normalizedPosition, scene, camera, time) {
+    // restore the color if there is a picked object
+    if (this.pickedObject) {
+      this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
+      this.pickedObject = undefined;
+    }
+
+    // cast a ray through the frustum
+    this.raycaster.setFromCamera(normalizedPosition, camera);
+    // get the list of objects the ray intersected
+    const intersectedObjects = this.raycaster.intersectObjects(scene.children);
+    if (intersectedObjects.length) {
+      // pick the first object. It's the closest one
+      this.pickedObject = intersectedObjects[0].object;
+      // save its color
+      this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
+      // set its emissive color to flashing red/yellow
+      this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
+    }
+  }
+}
+
+const pickPosition = {x: 0, y: 0};
+const pickHelper = new PickHelper();
+```
+
+マウスの `pickPosition` を以下のように更新しました。
+
+```js
+function getCanvasRelativePosition(event) {
+  const rect = canvas.getBoundingClientRect();
+  return {
+    x: (event.clientX - rect.left) * canvas.width  / rect.width,
+    y: (event.clientY - rect.top ) * canvas.height / rect.height,
+  };
+}
+
+function setPickPosition(event) {
+  const pos = getCanvasRelativePosition(event);
+  pickPosition.x = (pos.x / canvas.width ) *  2 - 1;
+  pickPosition.y = (pos.y / canvas.height) * -2 + 1;  // note we flip Y
+}
+window.addEventListener('mousemove', setPickPosition);
+```
+
+Workerではマウスの位置を直接読み取れないので、サイズのコードと同じようにマウスの位置を指定してメッセージを送信してみましょう。
+サイズのコードと同様にマウスの位置を送信して `pickPosition` を更新します。
+
+```js
+function size(data) {
+  state.width = data.width;
+  state.height = data.height;
+}
+
++function mouse(data) {
++  pickPosition.x = data.x;
++  pickPosition.y = data.y;
++}
+
+const handlers = {
+  init,
++  mouse,
+  size,
+};
+
+self.onmessage = function(e) {
+  const fn = handlers[e.data.type];
+  if (!fn) {
+    throw new Error('no handler for type: ' + e.data.type);
+  }
+  fn(e.data);
+};
+```
+
+メインページに戻ってマウスをWorkerやメインページに渡すコードを追加します。
+
+```js
++let sendMouse;
+
+function startWorker(canvas) {
+  const offscreen = canvas.transferControlToOffscreen();
+  const worker = new Worker('offscreencanvas-worker-picking.js', {type: 'module'});
+  worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
+
++  sendMouse = (x, y) => {
++    worker.postMessage({
++      type: 'mouse',
++      x,
++      y,
++    });
++  };
+
+  function sendSize() {
+    worker.postMessage({
+      type: 'size',
+      width: canvas.clientWidth,
+      height: canvas.clientHeight,
+    });
+  }
+
+  window.addEventListener('resize', sendSize);
+  sendSize();
+
+  console.log('using OffscreenCanvas');  /* eslint-disable-line no-console */
+}
+
+function startMainPage(canvas) {
+  init({canvas});
+
++  sendMouse = (x, y) => {
++    pickPosition.x = x;
++    pickPosition.y = y;
++  };
+
+  function sendSize() {
+    state.width = canvas.clientWidth;
+    state.height = canvas.clientHeight;
+  }
+  window.addEventListener('resize', sendSize);
+  sendSize();
+
+  console.log('using regular canvas');  /* eslint-disable-line no-console */
+}
+
+```
+
+全てのマウス操作コードをメインページにコピーし、`sendMouse` を使用するようにマイナーチェンジを加えます。
+
+```js
+function setPickPosition(event) {
+  const pos = getCanvasRelativePosition(event);
+-  pickPosition.x = (pos.x / canvas.clientWidth ) *  2 - 1;
+-  pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1;  // note we flip Y
++  sendMouse(
++      (pos.x / canvas.clientWidth ) *  2 - 1,
++      (pos.y / canvas.clientHeight) * -2 + 1);  // note we flip Y
+}
+
+function clearPickPosition() {
+  // unlike the mouse which always has a position
+  // if the user stops touching the screen we want
+  // to stop picking. For now we just pick a value
+  // unlikely to pick something
+-  pickPosition.x = -100000;
+-  pickPosition.y = -100000;
++  sendMouse(-100000, -100000);
+}
+window.addEventListener('mousemove', setPickPosition);
+window.addEventListener('mouseout', clearPickPosition);
+window.addEventListener('mouseleave', clearPickPosition);
+
+window.addEventListener('touchstart', (event) => {
+  // prevent the window from scrolling
+  event.preventDefault();
+  setPickPosition(event.touches[0]);
+}, {passive: false});
+
+window.addEventListener('touchmove', (event) => {
+  setPickPosition(event.touches[0]);
+});
+
+window.addEventListener('touchend', clearPickPosition);
+```
+
+これでこのピッキングは `OffscreenCanvas` で動作するはずです。
+
+{{{example url="../threejs-offscreencanvas-w-picking.html" }}}
+
+もう1歩踏み込んで `OrbitControls` を追加してみましょう。
+これはもう少し複雑です。
+`OrbitControls` はマウス、タッチイベント、キーボードなどDOMをかなり広範囲にチェックしています。
+
+これまでのコードとは異なり、グローバルな `state` オブジェクトを使う事はできません。
+これを使用して動作するようにOrbitControlsのコードを全て書き換える必要はありません。
+OrbitControlsは `HTMLElement` を取り、それに使用するDOMイベントのほとんどをアタッチします。
+OrbitControlsが必要とする機能をサポートする必要があります。
+
+[OrbitControlsのソースコード](https://github.com/gfxfundamentals/threejsfundamentals/blob/master/threejs/resources/threejs/r119/examples/js/controls/OrbitControls.js)を掘り下げてみると、次のイベントを処理する必要があるように見えます。
+
+* contextmenu
+* mousedown
+* mousemove
+* mouseup
+* touchstart
+* touchmove
+* touchend
+* wheel
+* keydown
+
+マウスイベントには `ctrlKey`、 `metaKey`、 `shiftKey`、 `button`、 `clientX`、 `clientY`、 `pageX`、 `pageY` プロパティが必要です。
+
+キーダウンイベントには `ctrlKey`, `metaKey`, `shiftKey`, `keyCode` プロパティが必要です。
+
+ホイールイベントに必要なのは `deltaY` プロパティだけです。
+
+また、タッチイベントに必要なのは `touches` プロパティの `pageX` と `pageY` だけです。
+
+そこでproxyオブジェクトのペアを作ってみましょう。
+ある時はメインページで実行され、全てのイベント、関連するプロパティ値をWorkerに渡します。
+また、ある時はWorkerで実行され、全てのイベント、DOMイベントと同じ構造をもつイベントをメインページに渡すので、OrbitControlsは違いを見分けられません。
+
+ここにWorker部分のコードがあります。
+
+```js
+import {EventDispatcher} from './resources/threejs/r119/build/three.module.js';
+
+class ElementProxyReceiver extends EventDispatcher {
+  constructor() {
+    super();
+  }
+  handleEvent(data) {
+    this.dispatchEvent(data);
+  }
+}
+```
+
+メッセージを受信した場合にdataを送信するだけです。
+これは `EventDispatcher` を継承しており、DOM要素のように `addEventListener` や `removeEventListener` のようなメソッドを提供しているので、OrbitControlsに渡せば動作するはずです。
+
+`ElementProxyReceiver` は1つの要素を扱います。
+私たちの場合は1つの頭しか必要ありませんが、頭で考えるのがベストです。
+つまり、マネージャーを作って複数のElementProxyReceiverを管理するようにしましょう。
+
+```js
+class ProxyManager {
+  constructor() {
+    this.targets = {};
+    this.handleEvent = this.handleEvent.bind(this);
+  }
+  makeProxy(data) {
+    const {id} = data;
+    const proxy = new ElementProxyReceiver();
+    this.targets[id] = proxy;
+  }
+  getProxy(id) {
+    return this.targets[id];
+  }
+  handleEvent(data) {
+    this.targets[data.id].handleEvent(data.data);
+  }
+}
+```
+
+`ProxyManager`のインスタンスを作成し `makeProxy` メソッドにidを指定して呼び出す事で、そのidを持つメッセージに応答する `ElementProxyReceiver` を作成できます。
+
+Workerのメッセージハンドラに接続してみましょう。
+
+```js
+const proxyManager = new ProxyManager();
+
+function start(data) {
+  const proxy = proxyManager.getProxy(data.canvasId);
+  init({
+    canvas: data.canvas,
+    inputElement: proxy,
+  });
+}
+
+function makeProxy(data) {
+  proxyManager.makeProxy(data);
+}
+
+...
+
+const handlers = {
+-  init,
+-  mouse,
++  start,
++  makeProxy,
++  event: proxyManager.handleEvent,
+   size,
+};
+
+self.onmessage = function(e) {
+  const fn = handlers[e.data.type];
+  if (!fn) {
+    throw new Error('no handler for type: ' + e.data.type);
+  }
+  fn(e.data);
+};
+```
+
+共有のthree.jsコードでは `OrbitControls` をインポートして設定する必要があります。
+
+```js
+import * as THREE from './resources/threejs/r119/build/three.module.js';
++import {OrbitControls} from './resources/threejs/r119/examples/jsm/controls/OrbitControls.js';
+
+export function init(data) {
+-  const {canvas} = data;
++  const {canvas, inputElement} = data;
+  const renderer = new THREE.WebGLRenderer({canvas});
+
++  const controls = new OrbitControls(camera, inputElement);
++  controls.target.set(0, 0, 0);
++  controls.update();
+```
+
+OffscreenCanvas以外のサンプルコード例のようにキャンバスを渡すのではなく、
+`inputElement` を介してOrbitControlsをProxyに渡している事に注目して下さい。
+
+次に `canvas` を `inputElement` に変更し、HTMLファイルから全てのピッキングイベントのコードを共有のthree.jsコードに移動させます。
+
+```js
+function getCanvasRelativePosition(event) {
+-  const rect = canvas.getBoundingClientRect();
++  const rect = inputElement.getBoundingClientRect();
+  return {
+    x: event.clientX - rect.left,
+    y: event.clientY - rect.top,
+  };
+}
+
+function setPickPosition(event) {
+  const pos = getCanvasRelativePosition(event);
+-  sendMouse(
+-      (pos.x / canvas.clientWidth ) *  2 - 1,
+-      (pos.y / canvas.clientHeight) * -2 + 1);  // note we flip Y
++  pickPosition.x = (pos.x / inputElement.clientWidth ) *  2 - 1;
++  pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1;  // note we flip Y
+}
+
+function clearPickPosition() {
+  // unlike the mouse which always has a position
+  // if the user stops touching the screen we want
+  // to stop picking. For now we just pick a value
+  // unlikely to pick something
+-  sendMouse(-100000, -100000);
++  pickPosition.x = -100000;
++  pickPosition.y = -100000;
+}
+
+*inputElement.addEventListener('mousemove', setPickPosition);
+*inputElement.addEventListener('mouseout', clearPickPosition);
+*inputElement.addEventListener('mouseleave', clearPickPosition);
+
+*inputElement.addEventListener('touchstart', (event) => {
+  // prevent the window from scrolling
+  event.preventDefault();
+  setPickPosition(event.touches[0]);
+}, {passive: false});
+
+*inputElement.addEventListener('touchmove', (event) => {
+  setPickPosition(event.touches[0]);
+});
+
+*inputElement.addEventListener('touchend', clearPickPosition);
+```
+
+メインページに戻り、上記で列挙した全てのイベントにメッセージを送信するコードが必要です。
+
+```js
+let nextProxyId = 0;
+class ElementProxy {
+  constructor(element, worker, eventHandlers) {
+    this.id = nextProxyId++;
+    this.worker = worker;
+    const sendEvent = (data) => {
+      this.worker.postMessage({
+        type: 'event',
+        id: this.id,
+        data,
+      });
+    };
+
+    // register an id
+    worker.postMessage({
+      type: 'makeProxy',
+      id: this.id,
+    });
+    for (const [eventName, handler] of Object.entries(eventHandlers)) {
+      element.addEventListener(eventName, function(event) {
+        handler(event, sendEvent);
+      });
+    }
+  }
+}
+```
+
+`ElementProxy` はProxyしたいイベントの要素を受け取ります。
+次にWorkerにidを登録し、先ほど設定した `makeProxy` メッセージを使って送信します。
+Workerは `ElementProxyReceiver` を作成しそのidに登録します。
+
+そして登録するイベントハンドラのオブジェクトを用意します。
+このようにして、Workerに転送したいイベントにハンドラを渡す事ができます。
+
+Workerを起動する時はまずProxyを作成しイベントハンドラを渡します。
+
+```js
+function startWorker(canvas) {
+  const offscreen = canvas.transferControlToOffscreen();
+  const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', {type: 'module'});
+
++  const eventHandlers = {
++    contextmenu: preventDefaultHandler,
++    mousedown: mouseEventHandler,
++    mousemove: mouseEventHandler,
++    mouseup: mouseEventHandler,
++    touchstart: touchEventHandler,
++    touchmove: touchEventHandler,
++    touchend: touchEventHandler,
++    wheel: wheelEventHandler,
++    keydown: filteredKeydownEventHandler,
++  };
++  const proxy = new ElementProxy(canvas, worker, eventHandlers);
+  worker.postMessage({
+    type: 'start',
+    canvas: offscreen,
++    canvasId: proxy.id,
+  }, [offscreen]);
+  console.log('using OffscreenCanvas');  /* eslint-disable-line no-console */
+}
+```
+
+以下はイベントハンドラです。
+受信したイベントからプロパティのリストをコピーするだけです。
+`sendEvent` 関数に渡され作成したデータを渡します。
+この関数は正しいidを追加してWorkerに送信します。
+
+```js
+const mouseEventHandler = makeSendPropertiesHandler([
+  'ctrlKey',
+  'metaKey',
+  'shiftKey',
+  'button',
+  'clientX',
+  'clientY',
+  'pageX',
+  'pageY',
+]);
+const wheelEventHandlerImpl = makeSendPropertiesHandler([
+  'deltaX',
+  'deltaY',
+]);
+const keydownEventHandler = makeSendPropertiesHandler([
+  'ctrlKey',
+  'metaKey',
+  'shiftKey',
+  'keyCode',
+]);
+
+function wheelEventHandler(event, sendFn) {
+  event.preventDefault();
+  wheelEventHandlerImpl(event, sendFn);
+}
+
+function preventDefaultHandler(event) {
+  event.preventDefault();
+}
+
+function copyProperties(src, properties, dst) {
+  for (const name of properties) {
+      dst[name] = src[name];
+  }
+}
+
+function makeSendPropertiesHandler(properties) {
+  return function sendProperties(event, sendFn) {
+    const data = {type: event.type};
+    copyProperties(event, properties, data);
+    sendFn(data);
+  };
+}
+
+function touchEventHandler(event, sendFn) {
+  const touches = [];
+  const data = {type: event.type, touches};
+  for (let i = 0; i < event.touches.length; ++i) {
+    const touch = event.touches[i];
+    touches.push({
+      pageX: touch.pageX,
+      pageY: touch.pageY,
+    });
+  }
+  sendFn(data);
+}
+
+// The four arrow keys
+const orbitKeys = {
+  '37': true,  // left
+  '38': true,  // up
+  '39': true,  // right
+  '40': true,  // down
+};
+function filteredKeydownEventHandler(event, sendFn) {
+  const {keyCode} = event;
+  if (orbitKeys[keyCode]) {
+    event.preventDefault();
+    keydownEventHandler(event, sendFn);
+  }
+}
+```
+
+これで動くと思われるが、実際に試してみると `OrbitControls` がもう少し必要なものがあると分かります。
+
+1つは `element.focus` です。Workerには必要ないのでStubを追加しておきましょう。
+
+```js
+class ElementProxyReceiver extends THREE.EventDispatcher {
+  constructor() {
+    super();
+  }
+  handleEvent(data) {
+    this.dispatchEvent(data);
+  }
++  focus() {
++    // no-op
++  }
+}
+```
+
+もう1つは `event.preventDefault` と `event.stopPropagation` を呼び出す事です。
+メインページでは既に対応してるのでそれらも不要になります。
+
+```js
++function noop() {
++}
+
+class ElementProxyReceiver extends THREE.EventDispatcher {
+  constructor() {
+    super();
+  }
+  handleEvent(data) {
++    data.preventDefault = noop;
++    data.stopPropagation = noop;
+    this.dispatchEvent(data);
+  }
+  focus() {
+    // no-op
+  }
+}
+```
+
+もう1つは `clientWidth` と `clientHeight` を見る事です。
+以前はサイズを渡してましたが、Proxyペアを更新してそれも渡すようにします。
+
+Workerの中では
+
+```js
+class ElementProxyReceiver extends THREE.EventDispatcher {
+  constructor() {
+    super();
+  }
++  get clientWidth() {
++    return this.width;
++  }
++  get clientHeight() {
++    return this.height;
++  }
++  getBoundingClientRect() {
++    return {
++      left: this.left,
++      top: this.top,
++      width: this.width,
++      height: this.height,
++      right: this.left + this.width,
++      bottom: this.top + this.height,
++    };
++  }
+  handleEvent(data) {
++    if (data.type === 'size') {
++      this.left = data.left;
++      this.top = data.top;
++      this.width = data.width;
++      this.height = data.height;
++      return;
++    }
+    data.preventDefault = noop;
+    data.stopPropagation = noop;
+    this.dispatchEvent(data);
+  }
+  focus() {
+    // no-op
+  }
+}
+```
+
+メインページに戻るにはサイズと左と上の位置も送信する必要があります。
+このままではキャンバスを移動しても処理されず、サイズを変更しても処理されないです。
+移動を処理したい場合は何かがキャンバスを移動する度に `sendSize` を呼び出す必要があります。
+
+```js
+class ElementProxy {
+  constructor(element, worker, eventHandlers) {
+    this.id = nextProxyId++;
+    this.worker = worker;
+    const sendEvent = (data) => {
+      this.worker.postMessage({
+        type: 'event',
+        id: this.id,
+        data,
+      });
+    };
+
+    // register an id
+    worker.postMessage({
+      type: 'makeProxy',
+      id: this.id,
+    });
++    sendSize();
+    for (const [eventName, handler] of Object.entries(eventHandlers)) {
+      element.addEventListener(eventName, function(event) {
+        handler(event, sendEvent);
+      });
+    }
+
++    function sendSize() {
++      const rect = element.getBoundingClientRect();
++      sendEvent({
++        type: 'size',
++        left: rect.left,
++        top: rect.top,
++        width: element.clientWidth,
++        height: element.clientHeight,
++      });
++    }
++
++    window.addEventListener('resize', sendSize);
+  }
+}
+```
+
+そして共有のthree.jsコードでは `state` は不要になりました。
+
+```js
+-export const state = {
+-  width: 300,   // canvas default
+-  height: 150,  // canvas default
+-};
+
+...
+
+function resizeRendererToDisplaySize(renderer) {
+  const canvas = renderer.domElement;
+-  const width = state.width;
+-  const height = state.height;
++  const width = inputElement.clientWidth;
++  const height = inputElement.clientHeight;
+  const needResize = canvas.width !== width || canvas.height !== height;
+  if (needResize) {
+    renderer.setSize(width, height, false);
+  }
+  return needResize;
+}
+
+function render(time) {
+  time *= 0.001;
+
+  if (resizeRendererToDisplaySize(renderer)) {
+-    camera.aspect = state.width / state.height;
++    camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
+    camera.updateProjectionMatrix();
+  }
+
+  ...
+```
+
+他にもいくつかのハックがあります。
+OrbitControlsは `mousemove` と `mouseup` イベントをマウスキャプチャ(マウスがウィンドウの外に出た時)を処理するための要素の `ownerDocument` です。
+
+さらにコードはグローバルな `document` を参照していますが、Workerにはグローバルなdocumentはありません。
+
+これは2つの簡単なハックで全て解決できます。
+Workerコードでは両方の問題に対してProxyを再利用します。
+
+```js
+function start(data) {
+  const proxy = proxyManager.getProxy(data.canvasId);
++  proxy.ownerDocument = proxy; // HACK!
++  self.document = {} // HACK!
+  init({
+    canvas: data.canvas,
+    inputElement: proxy,
+  });
+}
+```
+
+これで `OrbitControls` が期待に沿った検査を行うための機能を提供します。
+
+難しいのは分かっていますが手短に言うと:
+
+`ElementProxy` はメインページ上で動作し、DOMイベントを転送します。
+Worker内の `ElementProxyReceiver` は一緒に使うことができる `HTMLElement` を装っています。
+`OrbitControls` と独自のコードを使用しています。
+
+最後にOffscreenCanvasを使用していない時のフォールバックです。
+必要なのはcanvas自体を `inputElement` として渡す事です。
+
+```js
+function startMainPage(canvas) {
+-  init({canvas});
++  init({canvas, inputElement: canvas});
+  console.log('using regular canvas');
+}
+```
+
+これでOrbitControlsがOffscreenCanvasで動作するようになりました。
+
+{{{example url="../threejs-offscreencanvas-w-orbitcontrols.html" }}}
+
+これはおそらくこのサイトで最も複雑な例です。
+各サンプルには3つのファイルが含まれているので少しわかりにくいです。
+HTMLファイル、Workerファイル、共有のthree.jsコードなどです。
+
+理解する事が難し過ぎず、少しでも参考になれば幸いです。
+three.js、OffscreenCanvas、Web Workerを使った動作の便利な例を紹介しました。