offscreencanvas.html 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>OffscreenCanvas</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – OffscreenCanvas">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="../resources/lesson.css">
  12. <link rel="stylesheet" href="../resources/lang.css">
  13. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. </head>
  24. <body>
  25. <div class="container">
  26. <div class="lesson-title">
  27. <h1>OffscreenCanvas</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas"><code class="notranslate" translate="no">OffscreenCanvas</code></a>
  32. is a relatively new browser feature currently only available in Chrome but apparently
  33. coming to other browsers. <code class="notranslate" translate="no">OffscreenCanvas</code> allows a web worker to render
  34. to a canvas. This is a way to offload heavy work, like rendering a complex 3D scene,
  35. to a web worker so as not to slow down the responsiveness of the browser. It
  36. also means data is loaded and parsed in the worker so possibly less jank while
  37. the page loads.</p>
  38. <p>Getting <em>started</em> using it is pretty straight forward. Let's port the 3 spinning cube
  39. example from <a href="responsive.html">the article on responsiveness</a>.</p>
  40. <p>Workers generally have their code separated
  41. into another script file whereas most of the examples on this site have had
  42. their scripts embedded into the HTML file of the page they are on.</p>
  43. <p>In our case we'll make a file called <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> and
  44. copy all the JavaScript from <a href="responsive.html">the responsive example</a> into it. We'll then
  45. make the changes needed for it to run in a worker.</p>
  46. <p>We still need some JavaScript in our HTML file. The first thing
  47. we need to do there is look up the canvas and then transfer control of that
  48. canvas to be offscreen by calling <code class="notranslate" translate="no">canvas.transferControlToOffscreen</code>.</p>
  49. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  50. const canvas = document.querySelector('#c');
  51. const offscreen = canvas.transferControlToOffscreen();
  52. ...
  53. </pre>
  54. <p>We can then start our worker with <code class="notranslate" translate="no">new Worker(pathToScript, {type: 'module'})</code>.
  55. and pass the <code class="notranslate" translate="no">offscreen</code> object to it.</p>
  56. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  57. const canvas = document.querySelector('#c');
  58. const offscreen = canvas.transferControlToOffscreen();
  59. const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
  60. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  61. }
  62. main();
  63. </pre>
  64. <p>It's important to note that workers can't access the <code class="notranslate" translate="no">DOM</code>. They
  65. can't look at HTML elements nor can they receive mouse events or
  66. keyboard events. The only thing they can generally do is respond
  67. to messages sent to them and send messages back to the page.</p>
  68. <p>To send a message to a worker we call <a href="https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage"><code class="notranslate" translate="no">worker.postMessage</code></a> and
  69. pass it 1 or 2 arguments. The first argument is a JavaScript object
  70. that will be <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">cloned</a>
  71. and sent to the worker. The second argument is an optional array
  72. of objects that are part of the first object that we want <em>transferred</em>
  73. to the worker. These objects will not be cloned. Instead they will be <em>transferred</em>
  74. and will cease to exist in the main page. Cease to exist is the probably
  75. the wrong description, rather they are neutered. Only certain types of
  76. objects can be transferred instead of cloned. They include <code class="notranslate" translate="no">OffscreenCanvas</code>
  77. so once transferred the <code class="notranslate" translate="no">offscreen</code> object back in the main page is useless.</p>
  78. <p>Workers receive messages from their <code class="notranslate" translate="no">onmessage</code> handler. The object
  79. we passed to <code class="notranslate" translate="no">postMessage</code> arrives on <code class="notranslate" translate="no">event.data</code> passed to the <code class="notranslate" translate="no">onmessage</code>
  80. handler on the worker. The code above declares a <code class="notranslate" translate="no">type: 'main'</code> in the object it passes
  81. to the worker. This object has no meaning to the browser. It's entirely for
  82. our own usage. We'll make a handler that based on <code class="notranslate" translate="no">type</code> calls
  83. a different function in the worker. Then we can add functions as
  84. needed and easily call them from the main page.</p>
  85. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const handlers = {
  86. main,
  87. };
  88. self.onmessage = function(e) {
  89. const fn = handlers[e.data.type];
  90. if (typeof fn !== 'function') {
  91. throw new Error('no handler for type: ' + e.data.type);
  92. }
  93. fn(e.data);
  94. };
  95. </pre>
  96. <p>You can see above we just look up the handler based on the <code class="notranslate" translate="no">type</code> pass it the <code class="notranslate" translate="no">data</code>
  97. that was sent from the main page.</p>
  98. <p>So now we just need to start changing the <code class="notranslate" translate="no">main</code> we pasted into
  99. <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> from <a href="responsive.html">the responsive article</a>.</p>
  100. <p>Instead of looking up the canvas from the DOM we'll receive it from the
  101. event data.</p>
  102. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function main() {
  103. - const canvas = document.querySelector('#c');
  104. +function main(data) {
  105. + const {canvas} = data;
  106. const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
  107. ...
  108. </pre>
  109. <p>Remembering that workers can't see the DOM at all the first problem
  110. we run into is <code class="notranslate" translate="no">resizeRendererToDisplaySize</code> can't look at <code class="notranslate" translate="no">canvas.clientWidth</code>
  111. and <code class="notranslate" translate="no">canvas.clientHeight</code> as those are DOM values. Here's the original code</p>
  112. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  113. const canvas = renderer.domElement;
  114. const width = canvas.clientWidth;
  115. const height = canvas.clientHeight;
  116. const needResize = canvas.width !== width || canvas.height !== height;
  117. if (needResize) {
  118. renderer.setSize(width, height, false);
  119. }
  120. return needResize;
  121. }
  122. </pre>
  123. <p>Instead we'll need to send sizes as they change to the worker.
  124. So, let's add some global state and keep the width and height there.</p>
  125. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const state = {
  126. width: 300, // canvas default
  127. height: 150, // canvas default
  128. };
  129. </pre>
  130. <p>Then let's add a <code class="notranslate" translate="no">'size'</code> handler to update those values. </p>
  131. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function size(data) {
  132. + state.width = data.width;
  133. + state.height = data.height;
  134. +}
  135. const handlers = {
  136. main,
  137. + size,
  138. };
  139. </pre>
  140. <p>Now we can change <code class="notranslate" translate="no">resizeRendererToDisplaySize</code> to use <code class="notranslate" translate="no">state.width</code> and <code class="notranslate" translate="no">state.height</code></p>
  141. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  142. const canvas = renderer.domElement;
  143. - const width = canvas.clientWidth;
  144. - const height = canvas.clientHeight;
  145. + const width = state.width;
  146. + const height = state.height;
  147. const needResize = canvas.width !== width || canvas.height !== height;
  148. if (needResize) {
  149. renderer.setSize(width, height, false);
  150. }
  151. return needResize;
  152. }
  153. </pre>
  154. <p>and where we compute the aspect we need similar changes</p>
  155. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  156. time *= 0.001;
  157. if (resizeRendererToDisplaySize(renderer)) {
  158. - camera.aspect = canvas.clientWidth / canvas.clientHeight;
  159. + camera.aspect = state.width / state.height;
  160. camera.updateProjectionMatrix();
  161. }
  162. ...
  163. </pre>
  164. <p>Back in the main page we'll send a <code class="notranslate" translate="no">size</code> event anytime the page changes size.</p>
  165. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
  166. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  167. +function sendSize() {
  168. + worker.postMessage({
  169. + type: 'size',
  170. + width: canvas.clientWidth,
  171. + height: canvas.clientHeight,
  172. + });
  173. +}
  174. +
  175. +window.addEventListener('resize', sendSize);
  176. +sendSize();
  177. </pre>
  178. <p>We also call it once to send the initial size.</p>
  179. <p>And with just those few changes, assuming your browser fully supports <code class="notranslate" translate="no">OffscreenCanvas</code>
  180. it should work. Before we run it though let's check if the browser actually supports
  181. <code class="notranslate" translate="no">OffscreenCanvas</code> and if not display an error. First let's add some HTML to display the error.</p>
  182. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  183. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  184. + &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  185. + &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  186. + &lt;/div&gt;
  187. &lt;/body&gt;
  188. </pre>
  189. <p>and some CSS for that</p>
  190. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#noOffscreenCanvas {
  191. display: flex;
  192. width: 100%;
  193. height: 100%;
  194. align-items: center;
  195. justify-content: center;
  196. background: red;
  197. color: white;
  198. }
  199. </pre>
  200. <p>and then we can check for the existence of <code class="notranslate" translate="no">transferControlToOffscreen</code> to see
  201. if the browser supports <code class="notranslate" translate="no">OffscreenCanvas</code></p>
  202. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  203. const canvas = document.querySelector('#c');
  204. + if (!canvas.transferControlToOffscreen) {
  205. + canvas.style.display = 'none';
  206. + document.querySelector('#noOffscreenCanvas').style.display = '';
  207. + return;
  208. + }
  209. const offscreen = canvas.transferControlToOffscreen();
  210. const worker = new Worker('offscreencanvas-picking.js', {type: 'module});
  211. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  212. ...
  213. </pre>
  214. <p>and with that, if your browser supports <code class="notranslate" translate="no">OffscreenCanvas</code> this example should work</p>
  215. <p></p><div translate="no" class="threejs_example_container notranslate">
  216. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas.html"></iframe></div>
  217. <a class="threejs_center" href="/manual/examples/offscreencanvas.html" target="_blank">click here to open in a separate window</a>
  218. </div>
  219. <p></p>
  220. <p>So that's great but since not every browser supports <code class="notranslate" translate="no">OffscreenCanvas</code> at the moment
  221. let's change the code to work with both <code class="notranslate" translate="no">OffscreenCanvas</code> and if not then fallback to using
  222. the canvas in the main page like normal.</p>
  223. <blockquote>
  224. <p>As an aside, if you need OffscreenCanvas to make your page responsive then
  225. it's not clear what the point of having a fallback is. Maybe based on if
  226. you end up running on the main page or in a worker you might adjust the amount
  227. of work done so that when running in a worker you can do more than when
  228. running in the main page. What you do is really up to you.</p>
  229. </blockquote>
  230. <p>The first thing we should probably do is separate out the three.js
  231. code from the code that is specific to the worker. That way we can
  232. use the same code on both the main page and the worker. In other words
  233. we will now have 3 files</p>
  234. <ol>
  235. <li><p>our html file.</p>
  236. <p><code class="notranslate" translate="no">threejs-offscreencanvas-w-fallback.html</code></p>
  237. </li>
  238. <li><p>a JavaScript that contains our three.js code.</p>
  239. <p><code class="notranslate" translate="no">shared-cubes.js</code></p>
  240. </li>
  241. <li><p>our worker support code</p>
  242. <p><code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code></p>
  243. </li>
  244. </ol>
  245. <p><code class="notranslate" translate="no">shared-cubes.js</code> and <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code> are basically
  246. the split of our previous <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> file. First we
  247. copy all of <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> to <code class="notranslate" translate="no">shared-cube.js</code>. Then
  248. we rename <code class="notranslate" translate="no">main</code> to <code class="notranslate" translate="no">init</code> since we already have a <code class="notranslate" translate="no">main</code> in our
  249. HTML file and we need to export <code class="notranslate" translate="no">init</code> and <code class="notranslate" translate="no">state</code></p>
  250. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  251. -const state = {
  252. +export const state = {
  253. width: 300, // canvas default
  254. height: 150, // canvas default
  255. };
  256. -function main(data) {
  257. +export function init(data) {
  258. const {canvas} = data;
  259. const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
  260. </pre>
  261. <p>and cut out the just the non three.js relates parts</p>
  262. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function size(data) {
  263. - state.width = data.width;
  264. - state.height = data.height;
  265. -}
  266. -
  267. -const handlers = {
  268. - main,
  269. - size,
  270. -};
  271. -
  272. -self.onmessage = function(e) {
  273. - const fn = handlers[e.data.type];
  274. - if (typeof fn !== 'function') {
  275. - throw new Error('no handler for type: ' + e.data.type);
  276. - }
  277. - fn(e.data);
  278. -};
  279. </pre>
  280. <p>Then we copy those parts we just deleted to <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code>
  281. and import <code class="notranslate" translate="no">shared-cubes.js</code> as well as call <code class="notranslate" translate="no">init</code> instead of <code class="notranslate" translate="no">main</code>.</p>
  282. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {init, state} from './shared-cubes.js';
  283. function size(data) {
  284. state.width = data.width;
  285. state.height = data.height;
  286. }
  287. const handlers = {
  288. - main,
  289. + init,
  290. size,
  291. };
  292. self.onmessage = function(e) {
  293. const fn = handlers[e.data.type];
  294. if (typeof fn !== 'function') {
  295. throw new Error('no handler for type: ' + e.data.type);
  296. }
  297. fn(e.data);
  298. };
  299. </pre>
  300. <p>Similarly we need to include <code class="notranslate" translate="no">shared-cubes.js</code> in the main page</p>
  301. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;script type="module"&gt;
  302. +import {init, state} from './shared-cubes.js';
  303. </pre>
  304. <p>We can remove the HTML and CSS we added previously</p>
  305. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  306. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  307. - &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  308. - &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  309. - &lt;/div&gt;
  310. &lt;/body&gt;
  311. </pre>
  312. <p>and some CSS for that</p>
  313. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">-#noOffscreenCanvas {
  314. - display: flex;
  315. - width: 100%;
  316. - height: 100%;
  317. - align-items: center;
  318. - justify-content: center;
  319. - background: red;
  320. - color: white;
  321. -}
  322. </pre>
  323. <p>Then let's change the code in the main page to call one start
  324. function or another depending on if the browser supports <code class="notranslate" translate="no">OffscreenCanvas</code>.</p>
  325. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  326. const canvas = document.querySelector('#c');
  327. - if (!canvas.transferControlToOffscreen) {
  328. - canvas.style.display = 'none';
  329. - document.querySelector('#noOffscreenCanvas').style.display = '';
  330. - return;
  331. - }
  332. - const offscreen = canvas.transferControlToOffscreen();
  333. - const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
  334. - worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  335. + if (canvas.transferControlToOffscreen) {
  336. + startWorker(canvas);
  337. + } else {
  338. + startMainPage(canvas);
  339. + }
  340. ...
  341. </pre>
  342. <p>We'll move all the code we had to setup the worker inside <code class="notranslate" translate="no">startWorker</code></p>
  343. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  344. const offscreen = canvas.transferControlToOffscreen();
  345. const worker = new Worker('offscreencanvas-worker-cubes.js', {type: 'module'});
  346. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  347. function sendSize() {
  348. worker.postMessage({
  349. type: 'size',
  350. width: canvas.clientWidth,
  351. height: canvas.clientHeight,
  352. });
  353. }
  354. window.addEventListener('resize', sendSize);
  355. sendSize();
  356. console.log('using OffscreenCanvas');
  357. }
  358. </pre>
  359. <p>and send <code class="notranslate" translate="no">init</code> instead of <code class="notranslate" translate="no">main</code></p>
  360. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  361. + worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
  362. </pre>
  363. <p>for starting in the main page we can do this</p>
  364. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  365. init({canvas});
  366. function sendSize() {
  367. state.width = canvas.clientWidth;
  368. state.height = canvas.clientHeight;
  369. }
  370. window.addEventListener('resize', sendSize);
  371. sendSize();
  372. console.log('using regular canvas');
  373. }
  374. </pre>
  375. <p>and with that our example will run either in an OffscreenCanvas or
  376. fallback to running in the main page.</p>
  377. <p></p><div translate="no" class="threejs_example_container notranslate">
  378. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-fallback.html"></iframe></div>
  379. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-fallback.html" target="_blank">click here to open in a separate window</a>
  380. </div>
  381. <p></p>
  382. <p>So that was relatively easy. Let's try picking. We'll take some code from
  383. the <code class="notranslate" translate="no">RayCaster</code> example from <a href="picking.html">the article on picking</a>
  384. and make it work offscreen.</p>
  385. <p>Let's copy the <code class="notranslate" translate="no">shared-cube.js</code> to <code class="notranslate" translate="no">shared-picking.js</code> and add the
  386. picking parts. We copy in the <code class="notranslate" translate="no">PickHelper</code> </p>
  387. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  388. constructor() {
  389. this.raycaster = new THREE.Raycaster();
  390. this.pickedObject = null;
  391. this.pickedObjectSavedColor = 0;
  392. }
  393. pick(normalizedPosition, scene, camera, time) {
  394. // restore the color if there is a picked object
  395. if (this.pickedObject) {
  396. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  397. this.pickedObject = undefined;
  398. }
  399. // cast a ray through the frustum
  400. this.raycaster.setFromCamera(normalizedPosition, camera);
  401. // get the list of objects the ray intersected
  402. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  403. if (intersectedObjects.length) {
  404. // pick the first object. It's the closest one
  405. this.pickedObject = intersectedObjects[0].object;
  406. // save its color
  407. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  408. // set its emissive color to flashing red/yellow
  409. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  410. }
  411. }
  412. }
  413. const pickPosition = {x: 0, y: 0};
  414. const pickHelper = new PickHelper();
  415. </pre>
  416. <p>We updated <code class="notranslate" translate="no">pickPosition</code> from the mouse like this</p>
  417. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  418. const rect = canvas.getBoundingClientRect();
  419. return {
  420. x: (event.clientX - rect.left) * canvas.width / rect.width,
  421. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  422. };
  423. }
  424. function setPickPosition(event) {
  425. const pos = getCanvasRelativePosition(event);
  426. pickPosition.x = (pos.x / canvas.width ) * 2 - 1;
  427. pickPosition.y = (pos.y / canvas.height) * -2 + 1; // note we flip Y
  428. }
  429. window.addEventListener('mousemove', setPickPosition);
  430. </pre>
  431. <p>A worker can't read the mouse position directly so just like the size code
  432. let's send a message with the mouse position. Like the size code we'll
  433. send the mouse position and update <code class="notranslate" translate="no">pickPosition</code></p>
  434. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function size(data) {
  435. state.width = data.width;
  436. state.height = data.height;
  437. }
  438. +function mouse(data) {
  439. + pickPosition.x = data.x;
  440. + pickPosition.y = data.y;
  441. +}
  442. const handlers = {
  443. init,
  444. + mouse,
  445. size,
  446. };
  447. self.onmessage = function(e) {
  448. const fn = handlers[e.data.type];
  449. if (typeof fn !== 'function') {
  450. throw new Error('no handler for type: ' + e.data.type);
  451. }
  452. fn(e.data);
  453. };
  454. </pre>
  455. <p>Back in our main page we need to add code to pass the mouse
  456. to the worker or the main page.</p>
  457. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let sendMouse;
  458. function startWorker(canvas) {
  459. const offscreen = canvas.transferControlToOffscreen();
  460. const worker = new Worker('offscreencanvas-worker-picking.js', {type: 'module'});
  461. worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
  462. + sendMouse = (x, y) =&gt; {
  463. + worker.postMessage({
  464. + type: 'mouse',
  465. + x,
  466. + y,
  467. + });
  468. + };
  469. function sendSize() {
  470. worker.postMessage({
  471. type: 'size',
  472. width: canvas.clientWidth,
  473. height: canvas.clientHeight,
  474. });
  475. }
  476. window.addEventListener('resize', sendSize);
  477. sendSize();
  478. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  479. }
  480. function startMainPage(canvas) {
  481. init({canvas});
  482. + sendMouse = (x, y) =&gt; {
  483. + pickPosition.x = x;
  484. + pickPosition.y = y;
  485. + };
  486. function sendSize() {
  487. state.width = canvas.clientWidth;
  488. state.height = canvas.clientHeight;
  489. }
  490. window.addEventListener('resize', sendSize);
  491. sendSize();
  492. console.log('using regular canvas'); /* eslint-disable-line no-console */
  493. }
  494. </pre>
  495. <p>Then we can copy in all the mouse handling code to the main page and
  496. make just minor changes to use <code class="notranslate" translate="no">sendMouse</code></p>
  497. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setPickPosition(event) {
  498. const pos = getCanvasRelativePosition(event);
  499. - pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
  500. - pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
  501. + sendMouse(
  502. + (pos.x / canvas.clientWidth ) * 2 - 1,
  503. + (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
  504. }
  505. function clearPickPosition() {
  506. // unlike the mouse which always has a position
  507. // if the user stops touching the screen we want
  508. // to stop picking. For now we just pick a value
  509. // unlikely to pick something
  510. - pickPosition.x = -100000;
  511. - pickPosition.y = -100000;
  512. + sendMouse(-100000, -100000);
  513. }
  514. window.addEventListener('mousemove', setPickPosition);
  515. window.addEventListener('mouseout', clearPickPosition);
  516. window.addEventListener('mouseleave', clearPickPosition);
  517. window.addEventListener('touchstart', (event) =&gt; {
  518. // prevent the window from scrolling
  519. event.preventDefault();
  520. setPickPosition(event.touches[0]);
  521. }, {passive: false});
  522. window.addEventListener('touchmove', (event) =&gt; {
  523. setPickPosition(event.touches[0]);
  524. });
  525. window.addEventListener('touchend', clearPickPosition);
  526. </pre>
  527. <p>and with that picking should be working with <code class="notranslate" translate="no">OffscreenCanvas</code>.</p>
  528. <p></p><div translate="no" class="threejs_example_container notranslate">
  529. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-picking.html"></iframe></div>
  530. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-picking.html" target="_blank">click here to open in a separate window</a>
  531. </div>
  532. <p></p>
  533. <p>Let's take it one more step and add in the <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>.
  534. This will be little more involved. The <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> use
  535. the DOM pretty extensively checking the mouse, touch events,
  536. and the keyboard.</p>
  537. <p>Unlike our code so far we can't really use a global <code class="notranslate" translate="no">state</code> object
  538. without re-writing all the OrbitControls code to work with it.
  539. The OrbitControls take an <code class="notranslate" translate="no">HTMLElement</code> to which they attach most
  540. of the DOM events they use. Maybe we could pass in our own
  541. object that has the same API surface as a DOM element.
  542. We only need to support the features the OrbitControls need.</p>
  543. <p>Digging through the <a href="https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js">OrbitControls source code</a>
  544. it looks like we need to handle the following events.</p>
  545. <ul>
  546. <li>contextmenu</li>
  547. <li>pointerdown</li>
  548. <li>pointermove</li>
  549. <li>pointerup</li>
  550. <li>touchstart</li>
  551. <li>touchmove</li>
  552. <li>touchend</li>
  553. <li>wheel</li>
  554. <li>keydown</li>
  555. </ul>
  556. <p>For the pointer events we need the <code class="notranslate" translate="no">ctrlKey</code>, <code class="notranslate" translate="no">metaKey</code>, <code class="notranslate" translate="no">shiftKey</code>,
  557. <code class="notranslate" translate="no">button</code>, <code class="notranslate" translate="no">pointerType</code>, <code class="notranslate" translate="no">clientX</code>, <code class="notranslate" translate="no">clientY</code>, <code class="notranslate" translate="no">pageX</code>, and <code class="notranslate" translate="no">pageY</code>, properties.</p>
  558. <p>For the keydown events we need the <code class="notranslate" translate="no">ctrlKey</code>, <code class="notranslate" translate="no">metaKey</code>, <code class="notranslate" translate="no">shiftKey</code>,
  559. and <code class="notranslate" translate="no">keyCode</code> properties.</p>
  560. <p>For the wheel event we only need the <code class="notranslate" translate="no">deltaY</code> property.</p>
  561. <p>And for the touch events we only need <code class="notranslate" translate="no">pageX</code> and <code class="notranslate" translate="no">pageY</code> from
  562. the <code class="notranslate" translate="no">touches</code> property.</p>
  563. <p>So, let's make a proxy object pair. One part will run in the main page,
  564. get all those events, and pass on the relevant property values
  565. to the worker. The other part will run in the worker, receive those
  566. events and pass them on using events that have the same structure
  567. as the original DOM events so the OrbitControls won't be able to
  568. tell the difference.</p>
  569. <p>Here's the code for the worker part.</p>
  570. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {EventDispatcher} from 'three';
  571. class ElementProxyReceiver extends EventDispatcher {
  572. constructor() {
  573. super();
  574. }
  575. handleEvent(data) {
  576. this.dispatchEvent(data);
  577. }
  578. }
  579. </pre>
  580. <p>All it does is if it receives a message it dispatches it.
  581. It inherits from <a href="/docs/#api/en/core/EventDispatcher"><code class="notranslate" translate="no">EventDispatcher</code></a> which provides methods like
  582. <code class="notranslate" translate="no">addEventListener</code> and <code class="notranslate" translate="no">removeEventListener</code> just like a DOM
  583. element so if we pass it to the OrbitControls it should work.</p>
  584. <p><code class="notranslate" translate="no">ElementProxyReceiver</code> handles 1 element. In our case we only need
  585. one but it's best to think head so lets make a manager to manage
  586. more than one of them.</p>
  587. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ProxyManager {
  588. constructor() {
  589. this.targets = {};
  590. this.handleEvent = this.handleEvent.bind(this);
  591. }
  592. makeProxy(data) {
  593. const {id} = data;
  594. const proxy = new ElementProxyReceiver();
  595. this.targets[id] = proxy;
  596. }
  597. getProxy(id) {
  598. return this.targets[id];
  599. }
  600. handleEvent(data) {
  601. this.targets[data.id].handleEvent(data.data);
  602. }
  603. }
  604. </pre>
  605. <p>We can make a instance of <code class="notranslate" translate="no">ProxyManager</code> and call its <code class="notranslate" translate="no">makeProxy</code>
  606. method with an id which will make an <code class="notranslate" translate="no">ElementProxyReceiver</code> that
  607. responds to messages with that id.</p>
  608. <p>Let's hook it up to our worker's message handler.</p>
  609. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const proxyManager = new ProxyManager();
  610. function start(data) {
  611. const proxy = proxyManager.getProxy(data.canvasId);
  612. init({
  613. canvas: data.canvas,
  614. inputElement: proxy,
  615. });
  616. }
  617. function makeProxy(data) {
  618. proxyManager.makeProxy(data);
  619. }
  620. ...
  621. const handlers = {
  622. - init,
  623. - mouse,
  624. + start,
  625. + makeProxy,
  626. + event: proxyManager.handleEvent,
  627. size,
  628. };
  629. self.onmessage = function(e) {
  630. const fn = handlers[e.data.type];
  631. if (typeof fn !== 'function') {
  632. throw new Error('no handler for type: ' + e.data.type);
  633. }
  634. fn(e.data);
  635. };
  636. </pre>
  637. <p>In our shared three.js code we need to import the <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> and set them up.</p>
  638. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  639. +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  640. export function init(data) {
  641. - const {canvas} = data;
  642. + const {canvas, inputElement} = data;
  643. const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
  644. + const controls = new OrbitControls(camera, inputElement);
  645. + controls.target.set(0, 0, 0);
  646. + controls.update();
  647. </pre>
  648. <p>Notice we're passing the OrbitControls our proxy via <code class="notranslate" translate="no">inputElement</code>
  649. instead of passing in the canvas like we do in other non-OffscreenCanvas
  650. examples.</p>
  651. <p>Next we can move all the picking event code from the HTML file
  652. to the shared three.js code as well while changing
  653. <code class="notranslate" translate="no">canvas</code> to <code class="notranslate" translate="no">inputElement</code>.</p>
  654. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  655. - const rect = canvas.getBoundingClientRect();
  656. + const rect = inputElement.getBoundingClientRect();
  657. return {
  658. x: event.clientX - rect.left,
  659. y: event.clientY - rect.top,
  660. };
  661. }
  662. function setPickPosition(event) {
  663. const pos = getCanvasRelativePosition(event);
  664. - sendMouse(
  665. - (pos.x / canvas.clientWidth ) * 2 - 1,
  666. - (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
  667. + pickPosition.x = (pos.x / inputElement.clientWidth ) * 2 - 1;
  668. + pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1; // note we flip Y
  669. }
  670. function clearPickPosition() {
  671. // unlike the mouse which always has a position
  672. // if the user stops touching the screen we want
  673. // to stop picking. For now we just pick a value
  674. // unlikely to pick something
  675. - sendMouse(-100000, -100000);
  676. + pickPosition.x = -100000;
  677. + pickPosition.y = -100000;
  678. }
  679. *inputElement.addEventListener('mousemove', setPickPosition);
  680. *inputElement.addEventListener('mouseout', clearPickPosition);
  681. *inputElement.addEventListener('mouseleave', clearPickPosition);
  682. *inputElement.addEventListener('touchstart', (event) =&gt; {
  683. // prevent the window from scrolling
  684. event.preventDefault();
  685. setPickPosition(event.touches[0]);
  686. }, {passive: false});
  687. *inputElement.addEventListener('touchmove', (event) =&gt; {
  688. setPickPosition(event.touches[0]);
  689. });
  690. *inputElement.addEventListener('touchend', clearPickPosition);
  691. </pre>
  692. <p>Back in the main page we need code to send messages for
  693. all the events we enumerated above.</p>
  694. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let nextProxyId = 0;
  695. class ElementProxy {
  696. constructor(element, worker, eventHandlers) {
  697. this.id = nextProxyId++;
  698. this.worker = worker;
  699. const sendEvent = (data) =&gt; {
  700. this.worker.postMessage({
  701. type: 'event',
  702. id: this.id,
  703. data,
  704. });
  705. };
  706. // register an id
  707. worker.postMessage({
  708. type: 'makeProxy',
  709. id: this.id,
  710. });
  711. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  712. element.addEventListener(eventName, function(event) {
  713. handler(event, sendEvent);
  714. });
  715. }
  716. }
  717. }
  718. </pre>
  719. <p><code class="notranslate" translate="no">ElementProxy</code> takes the element who's events we want to proxy. It
  720. then registers an id with the worker by picking one and sending it
  721. via the <code class="notranslate" translate="no">makeProxy</code> message we setup earlier. The worker will make
  722. an <code class="notranslate" translate="no">ElementProxyReceiver</code> and register it to that id.</p>
  723. <p>We then have an object of event handlers to register. This way
  724. we can pass handlers only for these events we want to forward to
  725. the worker.</p>
  726. <p>When we start the worker we first make a proxy and pass in our event handlers.</p>
  727. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  728. const offscreen = canvas.transferControlToOffscreen();
  729. const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', {type: 'module'});
  730. + const eventHandlers = {
  731. + contextmenu: preventDefaultHandler,
  732. + mousedown: mouseEventHandler,
  733. + mousemove: mouseEventHandler,
  734. + mouseup: mouseEventHandler,
  735. + pointerdown: mouseEventHandler,
  736. + pointermove: mouseEventHandler,
  737. + pointerup: mouseEventHandler,
  738. + touchstart: touchEventHandler,
  739. + touchmove: touchEventHandler,
  740. + touchend: touchEventHandler,
  741. + wheel: wheelEventHandler,
  742. + keydown: filteredKeydownEventHandler,
  743. + };
  744. + const proxy = new ElementProxy(canvas, worker, eventHandlers);
  745. worker.postMessage({
  746. type: 'start',
  747. canvas: offscreen,
  748. + canvasId: proxy.id,
  749. }, [offscreen]);
  750. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  751. }
  752. </pre>
  753. <p>And here are the event handlers. All they do is copy a list of properties
  754. from the event they receive. They are passed a <code class="notranslate" translate="no">sendEvent</code> function to which they pass the data
  755. they make. That function will add the correct id and send it to the worker.</p>
  756. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mouseEventHandler = makeSendPropertiesHandler([
  757. 'ctrlKey',
  758. 'metaKey',
  759. 'shiftKey',
  760. 'button',
  761. 'pointerType',
  762. 'clientX',
  763. 'clientY',
  764. 'pageX',
  765. 'pageY',
  766. ]);
  767. const wheelEventHandlerImpl = makeSendPropertiesHandler([
  768. 'deltaX',
  769. 'deltaY',
  770. ]);
  771. const keydownEventHandler = makeSendPropertiesHandler([
  772. 'ctrlKey',
  773. 'metaKey',
  774. 'shiftKey',
  775. 'keyCode',
  776. ]);
  777. function wheelEventHandler(event, sendFn) {
  778. event.preventDefault();
  779. wheelEventHandlerImpl(event, sendFn);
  780. }
  781. function preventDefaultHandler(event) {
  782. event.preventDefault();
  783. }
  784. function copyProperties(src, properties, dst) {
  785. for (const name of properties) {
  786. dst[name] = src[name];
  787. }
  788. }
  789. function makeSendPropertiesHandler(properties) {
  790. return function sendProperties(event, sendFn) {
  791. const data = {type: event.type};
  792. copyProperties(event, properties, data);
  793. sendFn(data);
  794. };
  795. }
  796. function touchEventHandler(event, sendFn) {
  797. const touches = [];
  798. const data = {type: event.type, touches};
  799. for (let i = 0; i &lt; event.touches.length; ++i) {
  800. const touch = event.touches[i];
  801. touches.push({
  802. pageX: touch.pageX,
  803. pageY: touch.pageY,
  804. });
  805. }
  806. sendFn(data);
  807. }
  808. // The four arrow keys
  809. const orbitKeys = {
  810. '37': true, // left
  811. '38': true, // up
  812. '39': true, // right
  813. '40': true, // down
  814. };
  815. function filteredKeydownEventHandler(event, sendFn) {
  816. const {keyCode} = event;
  817. if (orbitKeys[keyCode]) {
  818. event.preventDefault();
  819. keydownEventHandler(event, sendFn);
  820. }
  821. }
  822. </pre>
  823. <p>This seems close to running but if we actually try it we'll see
  824. that the <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> need a few more things.</p>
  825. <p>One is they call <code class="notranslate" translate="no">element.focus</code>. We don't need that to happen
  826. in the worker so let's just add a stub.</p>
  827. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  828. constructor() {
  829. super();
  830. }
  831. handleEvent(data) {
  832. this.dispatchEvent(data);
  833. }
  834. + focus() {
  835. + // no-op
  836. + }
  837. }
  838. </pre>
  839. <p>Another is they call <code class="notranslate" translate="no">event.preventDefault</code> and <code class="notranslate" translate="no">event.stopPropagation</code>.
  840. We're already handling that in the main page so those can also be a noop.</p>
  841. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function noop() {
  842. +}
  843. class ElementProxyReceiver extends THREE.EventDispatcher {
  844. constructor() {
  845. super();
  846. }
  847. handleEvent(data) {
  848. + data.preventDefault = noop;
  849. + data.stopPropagation = noop;
  850. this.dispatchEvent(data);
  851. }
  852. focus() {
  853. // no-op
  854. }
  855. }
  856. </pre>
  857. <p>Another is they look at <code class="notranslate" translate="no">clientWidth</code> and <code class="notranslate" translate="no">clientHeight</code>. We
  858. were passing the size before but we can update the proxy pair
  859. to pass that as well.</p>
  860. <p>In the worker...</p>
  861. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  862. constructor() {
  863. super();
  864. }
  865. + get clientWidth() {
  866. + return this.width;
  867. + }
  868. + get clientHeight() {
  869. + return this.height;
  870. + }
  871. + getBoundingClientRect() {
  872. + return {
  873. + left: this.left,
  874. + top: this.top,
  875. + width: this.width,
  876. + height: this.height,
  877. + right: this.left + this.width,
  878. + bottom: this.top + this.height,
  879. + };
  880. + }
  881. handleEvent(data) {
  882. + if (data.type === 'size') {
  883. + this.left = data.left;
  884. + this.top = data.top;
  885. + this.width = data.width;
  886. + this.height = data.height;
  887. + return;
  888. + }
  889. data.preventDefault = noop;
  890. data.stopPropagation = noop;
  891. this.dispatchEvent(data);
  892. }
  893. focus() {
  894. // no-op
  895. }
  896. }
  897. </pre>
  898. <p>back in the main page we need to send the size and the left and top positions as well.
  899. Note that as is we don't handle if the canvas moves, only if it resizes. If you wanted
  900. to handle moving you'd need to call <code class="notranslate" translate="no">sendSize</code> anytime something moved the canvas.</p>
  901. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxy {
  902. constructor(element, worker, eventHandlers) {
  903. this.id = nextProxyId++;
  904. this.worker = worker;
  905. const sendEvent = (data) =&gt; {
  906. this.worker.postMessage({
  907. type: 'event',
  908. id: this.id,
  909. data,
  910. });
  911. };
  912. // register an id
  913. worker.postMessage({
  914. type: 'makeProxy',
  915. id: this.id,
  916. });
  917. + sendSize();
  918. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  919. element.addEventListener(eventName, function(event) {
  920. handler(event, sendEvent);
  921. });
  922. }
  923. + function sendSize() {
  924. + const rect = element.getBoundingClientRect();
  925. + sendEvent({
  926. + type: 'size',
  927. + left: rect.left,
  928. + top: rect.top,
  929. + width: element.clientWidth,
  930. + height: element.clientHeight,
  931. + });
  932. + }
  933. +
  934. + window.addEventListener('resize', sendSize);
  935. }
  936. }
  937. </pre>
  938. <p>and in our shared three.js code we no longer need <code class="notranslate" translate="no">state</code></p>
  939. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-export const state = {
  940. - width: 300, // canvas default
  941. - height: 150, // canvas default
  942. -};
  943. ...
  944. function resizeRendererToDisplaySize(renderer) {
  945. const canvas = renderer.domElement;
  946. - const width = state.width;
  947. - const height = state.height;
  948. + const width = inputElement.clientWidth;
  949. + const height = inputElement.clientHeight;
  950. const needResize = canvas.width !== width || canvas.height !== height;
  951. if (needResize) {
  952. renderer.setSize(width, height, false);
  953. }
  954. return needResize;
  955. }
  956. function render(time) {
  957. time *= 0.001;
  958. if (resizeRendererToDisplaySize(renderer)) {
  959. - camera.aspect = state.width / state.height;
  960. + camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
  961. camera.updateProjectionMatrix();
  962. }
  963. ...
  964. </pre>
  965. <p>A few more hacks. The OrbitControls add <code class="notranslate" translate="no">pointermove</code> and <code class="notranslate" translate="no">pointerup</code> events to the
  966. <code class="notranslate" translate="no">ownerDocument</code> of the element to handle mouse capture (when the mouse goes
  967. outside the window).</p>
  968. <p>Further the code references the global <code class="notranslate" translate="no">document</code> but there is no global document
  969. in a worker. </p>
  970. <p>We can solve all of these with a 2 quick hacks. In our worker
  971. code we'll re-use our proxy for both problems.</p>
  972. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function start(data) {
  973. const proxy = proxyManager.getProxy(data.canvasId);
  974. + proxy.ownerDocument = proxy; // HACK!
  975. + self.document = {} // HACK!
  976. init({
  977. canvas: data.canvas,
  978. inputElement: proxy,
  979. });
  980. }
  981. </pre>
  982. <p>This will give the <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> something to inspect which
  983. matches their expectations.</p>
  984. <p>I know that was kind of hard to follow. The short version is:
  985. <code class="notranslate" translate="no">ElementProxy</code> runs on the main page and forwards DOM events
  986. to <code class="notranslate" translate="no">ElementProxyReceiver</code> in the worker which
  987. masquerades as an <code class="notranslate" translate="no">HTMLElement</code> that we can use both with the
  988. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> and with our own code.</p>
  989. <p>The final thing is our fallback when we are not using OffscreenCanvas.
  990. All we have to do is pass the canvas itself as our <code class="notranslate" translate="no">inputElement</code>.</p>
  991. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  992. - init({canvas});
  993. + init({canvas, inputElement: canvas});
  994. console.log('using regular canvas');
  995. }
  996. </pre>
  997. <p>and now we should have OrbitControls working with OffscreenCanvas</p>
  998. <p></p><div translate="no" class="threejs_example_container notranslate">
  999. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-orbitcontrols.html"></iframe></div>
  1000. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-orbitcontrols.html" target="_blank">click here to open in a separate window</a>
  1001. </div>
  1002. <p></p>
  1003. <p>This is probably the most complicated example on this site. It's a
  1004. little hard to follow because there are 3 files involved for each
  1005. sample. The HTML file, the worker file, the shared three.js code.</p>
  1006. <p>I hope it wasn't too difficult to understand and that it provided some
  1007. useful examples of working with three.js, OffscreenCanvas and web workers.</p>
  1008. </div>
  1009. </div>
  1010. </div>
  1011. <script src="../resources/prettify.js"></script>
  1012. <script src="../resources/lesson.js"></script>
  1013. </body></html>