offscreencanvas.html 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. <!DOCTYPE html><html lang="ru"><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="/manual/resources/lesson.css">
  12. <link rel="stylesheet" href="/manual/resources/lang.css">
  13. </head>
  14. <body>
  15. <div class="container">
  16. <div class="lesson-title">
  17. <h1>OffscreenCanvas</h1>
  18. </div>
  19. <div class="lesson">
  20. <div class="lesson-main">
  21. <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas"><code class="notranslate" translate="no">OffscreenCanvas</code></a> - это относительно новая функция браузера, которая в настоящее время доступна только в Chrome,
  22. но, очевидно, будет доступна и в других браузерах. <code class="notranslate" translate="no">OffscreenCanvas</code> позволяет веб-воркеру выполнять
  23. рендеринг на холст. Это способ переложить тяжелую работу, такую ​​как рендеринг сложной 3D-сцены, на веб-воркера, чтобы не замедлить скорость отклика браузера.
  24. Это также означает, что данные загружаются и анализируются в воркере, поэтому возможно меньше мусора во время загрузки страницы.</p>
  25. <p>Начать использовать его довольно просто. Давайте разберём пример 3 вращающихся кубов из <a href="responsive.html">статьи об отзывчивости</a>.</p>
  26. <p>Обычно у воркера есть свой код, разделенный в другой файл сценария. Для большинства примеров на этом сайте скрипты встроены в HTML-файл страницы, на которой они находятся.</p>
  27. <p>В нашем случае мы создадим файл с именем <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> и скопируем в него весь JavaScript из <a href="responsive.html">адаптивного примера</a>. Затем мы внесем изменения, необходимые для его работы в воркере.</p>
  28. <p>Нам все еще нужен JavaScript в нашем HTML-файле. Первое, что нам нужно сделать там, это найти холст,
  29. а затем передать управление этим холстом за пределы экрана, вызвав <code class="notranslate" translate="no">canvas.transferControlToOffscreen</code>.</p>
  30. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  31. const canvas = document.querySelector('#c');
  32. const offscreen = canvas.transferControlToOffscreen();
  33. ...
  34. </pre>
  35. <p>Затем мы можем запустить наш воркер с <code class="notranslate" translate="no">new Worker(pathToScript, {type: 'module'})</code>.
  36. и передать ему <code class="notranslate" translate="no">offscreen</code>.</p>
  37. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  38. const canvas = document.querySelector('#c');
  39. const offscreen = canvas.transferControlToOffscreen();
  40. const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
  41. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  42. }
  43. main();
  44. </pre>
  45. <p>Важно отметить, что воркеры не могут получить доступ к <code class="notranslate" translate="no">DOM</code>. Они не могут просматривать элементы HTML, а также получать события мыши или клавиатуры.
  46. Единственное, что они обычно могут делать, - это отвечать на отправленные им сообщения.</p>
  47. <p>Чтобы отправить сообщение воркеру, мы вызываем <a href="https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage"><code class="notranslate" translate="no">worker.postMessage</code></a> and
  48. и передаем ему 1 или 2 аргумента. Первый аргумент - это объект JavaScript, который будет <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">клонирован</a>
  49. и отправлен исполнителю. Второй аргумент - это необязательный массив объектов,
  50. которые являются частью первого объекта, который мы хотим передать воркеру.
  51. Эти объекты не будут клонированы. Вместо этого они будут перенесены и перестанут существовать на главной странице.
  52. Прекращение существования - это, вероятно, неправильное описание, скорее они кастрированы.
  53. Вместо клонирования можно передавать только определенные типы объектов.
  54. Они включают <code class="notranslate" translate="no">OffscreenCanvas</code>, поэтому после переноса <code class="notranslate" translate="no">offscreen</code> обратно на главную страницу он бесполезен.</p>
  55. <p>Воркеры получают сообщения от своего обработчика сообщений <code class="notranslate" translate="no">onmessage</code>. Объект,
  56. который мы передали в <code class="notranslate" translate="no">postMessage</code>, прибывает в объект <code class="notranslate" translate="no">event.data</code>, переданный
  57. обработчику <code class="notranslate" translate="no">onmessage</code> на воркере. В приведенном выше коде объявляется <code class="notranslate" translate="no">type: 'main'</code> в объекте, который он передает воркеру. Мы создадим обработчик,
  58. который на основе типа будет вызывать другую функцию в воркере. Затем мы можем добавлять функции по мере необходимости и легко вызывать их с главной страницы.</p>
  59. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const handlers = {
  60. main,
  61. };
  62. self.onmessage = function(e) {
  63. const fn = handlers[e.data.type];
  64. if (typeof fn !== 'function') {
  65. throw new Error('no handler for type: ' + e.data.type);
  66. }
  67. fn(e.data);
  68. };
  69. </pre>
  70. <p>Вы можете видеть выше, что мы просто ищем обработчик в зависимости от <code class="notranslate" translate="no">type</code>, передаем ему <code class="notranslate" translate="no">data</code>, которые были отправлены с главной страницы.</p>
  71. <p>Итак, теперь нам просто нужно начать изменять основной файл, который мы вставили в <code class="notranslate" translate="no">offscreencanvas-cubes.js</code>
  72. <a href="responsive.html">из адаптивной статьи</a>.</p>
  73. <p>Затем вместо того, чтобы искать холст в DOM, мы получим его из данных события.</p>
  74. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function main() {
  75. - const canvas = document.querySelector('#c');
  76. +function main(data) {
  77. + const {canvas} = data;
  78. const renderer = new THREE.WebGLRenderer({canvas});
  79. ...
  80. </pre>
  81. <p>Помня о том, что воркеры вообще не видят DOM, первая проблема, с которой мы сталкиваемся, -
  82. <code class="notranslate" translate="no">resizeRendererToDisplaySize</code> не может смотреть на <code class="notranslate" translate="no">canvas.clientWidth</code> и <code class="notranslate" translate="no">canvas.clientHeight</code>, поскольку это значения DOM. Вот исходный код</p>
  83. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  84. const canvas = renderer.domElement;
  85. const width = canvas.clientWidth;
  86. const height = canvas.clientHeight;
  87. const needResize = canvas.width !== width || canvas.height !== height;
  88. if (needResize) {
  89. renderer.setSize(width, height, false);
  90. }
  91. return needResize;
  92. }
  93. </pre>
  94. <p>Вместо этого нам нужно будет отправлять размеры по мере их изменения воркеру. Итак, давайте добавим некоторое глобальное состояние и сохраним там ширину и высоту.</p>
  95. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const state = {
  96. width: 300, // canvas default
  97. height: 150, // canvas default
  98. };
  99. </pre>
  100. <p>Затем добавим обработчик <code class="notranslate" translate="no">size</code> для обновления этих значений.</p>
  101. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function size(data) {
  102. + state.width = data.width;
  103. + state.height = data.height;
  104. +}
  105. const handlers = {
  106. main,
  107. + size,
  108. };
  109. </pre>
  110. <p>Теперь мы можем изменить <code class="notranslate" translate="no">resizeRendererToDisplaySize</code>, чтобы использовать <code class="notranslate" translate="no">state.width</code> и <code class="notranslate" translate="no">state.height</code>.</p>
  111. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  112. const canvas = renderer.domElement;
  113. - const width = canvas.clientWidth;
  114. - const height = canvas.clientHeight;
  115. + const width = state.width;
  116. + const height = state.height;
  117. const needResize = canvas.width !== width || canvas.height !== height;
  118. if (needResize) {
  119. renderer.setSize(width, height, false);
  120. }
  121. return needResize;
  122. }
  123. </pre>
  124. <p>и где мы вычисляем аспект, который нам нужен, аналогичные изменения</p>
  125. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  126. time *= 0.001;
  127. if (resizeRendererToDisplaySize(renderer)) {
  128. - camera.aspect = canvas.clientWidth / canvas.clientHeight;
  129. + camera.aspect = state.width / state.height;
  130. camera.updateProjectionMatrix();
  131. }
  132. ...
  133. </pre>
  134. <p>Вернувшись на главную страницу, мы будем отправлять событие <code class="notranslate" translate="no">size</code> каждый раз, когда страница меняет размер.</p>
  135. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
  136. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  137. +function sendSize() {
  138. + worker.postMessage({
  139. + type: 'size',
  140. + width: canvas.clientWidth,
  141. + height: canvas.clientHeight,
  142. + });
  143. +}
  144. +
  145. +window.addEventListener('resize', sendSize);
  146. +sendSize();
  147. </pre>
  148. <p>Мы также вызываем его один раз, чтобы отправить начальный размер.</p>
  149. <p>И всего с этими несколькими изменениями, если ваш браузер полностью
  150. поддерживает <code class="notranslate" translate="no">OffscreenCanvas</code>, он должен работать. Прежде чем запустить его,
  151. давайте проверим, действительно ли браузер поддерживает <code class="notranslate" translate="no">OffscreenCanvas</code>,
  152. и не отобразит ли он ошибку. Сначала добавим HTML-код для отображения ошибки.</p>
  153. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  154. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  155. + &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  156. + &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  157. + &lt;/div&gt;
  158. &lt;/body&gt;
  159. </pre>
  160. <p>и немного CSS для этого</p>
  161. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#noOffscreenCanvas {
  162. display: flex;
  163. width: 100%;
  164. height: 100%;
  165. align-items: center;
  166. justify-content: center;
  167. background: red;
  168. color: white;
  169. }
  170. </pre>
  171. <p>а затем мы можем проверить наличие <code class="notranslate" translate="no">transferControlToOffscreen</code>, чтобы узнать, поддерживает ли браузер <code class="notranslate" translate="no">OffscreenCanvas</code></p>
  172. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  173. const canvas = document.querySelector('#c');
  174. + if (!canvas.transferControlToOffscreen) {
  175. + canvas.style.display = 'none';
  176. + document.querySelector('#noOffscreenCanvas').style.display = '';
  177. + return;
  178. + }
  179. const offscreen = canvas.transferControlToOffscreen();
  180. const worker = new Worker('offscreencanvas-picking.js', {type: 'module});
  181. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  182. ...
  183. </pre>
  184. <p>и при этом, если ваш браузер поддерживает <code class="notranslate" translate="no">OffscreenCanvas</code>, этот пример должен работать</p>
  185. <p></p><div translate="no" class="threejs_example_container notranslate">
  186. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas.html"></iframe></div>
  187. <a class="threejs_center" href="/manual/examples/offscreencanvas.html" target="_blank">нажмите здесь, чтобы открыть в отдельном окне</a>
  188. </div>
  189. <p></p>
  190. <p>Так что это здорово, но поскольку не каждый браузер поддерживает <code class="notranslate" translate="no">OffscreenCanvas</code> на данный момент,
  191. давайте изменим код для работы с <code class="notranslate" translate="no">OffscreenCanvas</code>, а если нет, то вернемся к использованию холста на главной странице, как обычно.</p>
  192. <p>Кстати, если вам нужен OffscreenCanvas, чтобы ваша страница была отзывчивой, тогда неясно,
  193. в чем смысл использования запасного варианта. Возможно, в зависимости от того, выполняете ли
  194. вы в конечном итоге работу на главной странице или в воркере, вы можете настроить объем выполняемой работы так,
  195. чтобы при работе в воркере вы могли делать больше, чем при работе на главной странице. Что вы делаете, действительно зависит от вас.</p>
  196. <p>Первое, что нам, вероятно, следует сделать, - это отделить код three.js от кода,
  197. специфичного для воркера. Что мы можем использовать один и тот же код как на главной странице, так и на рабочем. Другими словами, теперь у нас будет 3 файла</p>
  198. <ol>
  199. <li><p>наш html файл.</p>
  200. <p><code class="notranslate" translate="no">threejs-offscreencanvas-w-fallback.html</code></p>
  201. </li>
  202. <li><p>JavaScript, содержащий наш код three.js.</p>
  203. <p><code class="notranslate" translate="no">shared-cubes.js</code></p>
  204. </li>
  205. <li><p>наш код поддержки воркера </p>
  206. <p><code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code></p>
  207. </li>
  208. </ol>
  209. <p><code class="notranslate" translate="no">shared-cubes.js</code> и <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code> по сути являются разделением нашего
  210. предыдущего файла <code class="notranslate" translate="no">offscreencanvas-cubes.js</code>. Сначала мы копируем весь файл <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> в <code class="notranslate" translate="no">shared-cube.js</code>. Затем мы переименовываем <code class="notranslate" translate="no">main</code> в <code class="notranslate" translate="no">init</code>, так как у нас уже есть <code class="notranslate" translate="no">main</code> в нашем HTML-файле, и нам нужно экспортировать <code class="notranslate" translate="no">init</code> и состояние</p>
  211. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from '../../build/three.module.js';
  212. -const state = {
  213. +export const state = {
  214. width: 300, // canvas default
  215. height: 150, // canvas default
  216. };
  217. -function main(data) {
  218. +export function init(data) {
  219. const {canvas} = data;
  220. const renderer = new THREE.WebGLRenderer({canvas});
  221. </pre>
  222. <p>и вырезать только части, не относящиеся к three.js</p>
  223. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function size(data) {
  224. - state.width = data.width;
  225. - state.height = data.height;
  226. -}
  227. -
  228. -const handlers = {
  229. - main,
  230. - size,
  231. -};
  232. -
  233. -self.onmessage = function(e) {
  234. - const fn = handlers[e.data.type];
  235. - if (typeof fn !== 'function') {
  236. - throw new Error('no handler for type: ' + e.data.type);
  237. - }
  238. - fn(e.data);
  239. -};
  240. </pre>
  241. <p>Затем мы копируем те части, которые мы только что удалили в <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code>.
  242. и импорт <code class="notranslate" translate="no">shared-cubes.js</code>, а также вызов <code class="notranslate" translate="no">init</code> вместо <code class="notranslate" translate="no">main</code>.</p>
  243. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {init, state} from './shared-cubes.js';
  244. function size(data) {
  245. state.width = data.width;
  246. state.height = data.height;
  247. }
  248. const handlers = {
  249. - main,
  250. + init,
  251. size,
  252. };
  253. self.onmessage = function(e) {
  254. const fn = handlers[e.data.type];
  255. if (typeof fn !== 'function') {
  256. throw new Error('no handler for type: ' + e.data.type);
  257. }
  258. fn(e.data);
  259. };
  260. </pre>
  261. <p>Точно так же нам нужно включить three.js и <code class="notranslate" translate="no">shared-cubes.js</code> на главную страницу.</p>
  262. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;script type="module"&gt;
  263. +import {init, state} from './shared-cubes.js';
  264. </pre>
  265. <p>Мы можем удалить HTML и CSS, которые мы добавили ранее</p>
  266. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  267. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  268. - &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  269. - &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  270. - &lt;/div&gt;
  271. &lt;/body&gt;
  272. </pre>
  273. <p>и немного CSS для этого</p>
  274. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">-#noOffscreenCanvas {
  275. - display: flex;
  276. - width: 100%;
  277. - height: 100%;
  278. - align-items: center;
  279. - justify-content: center;
  280. - background: red;
  281. - color: white;
  282. -}
  283. </pre>
  284. <p>Затем давайте изменим код на главной странице для вызова той или иной функции запуска в зависимости от того, поддерживает ли браузер <code class="notranslate" translate="no">OffscreenCanvas</code>.</p>
  285. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  286. const canvas = document.querySelector('#c');
  287. - if (!canvas.transferControlToOffscreen) {
  288. - canvas.style.display = 'none';
  289. - document.querySelector('#noOffscreenCanvas').style.display = '';
  290. - return;
  291. - }
  292. - const offscreen = canvas.transferControlToOffscreen();
  293. - const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
  294. - worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  295. + if (canvas.transferControlToOffscreen) {
  296. + startWorker(canvas);
  297. + } else {
  298. + startMainPage(canvas);
  299. + }
  300. ...
  301. </pre>
  302. <p>Мы переместим весь код, который у нас был для настройки воркера, внутрь <code class="notranslate" translate="no">startWorker</code>.</p>
  303. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  304. const offscreen = canvas.transferControlToOffscreen();
  305. const worker = new Worker('offscreencanvas-worker-cubes.js', {type: 'module'});
  306. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  307. function sendSize() {
  308. worker.postMessage({
  309. type: 'size',
  310. width: canvas.clientWidth,
  311. height: canvas.clientHeight,
  312. });
  313. }
  314. window.addEventListener('resize', sendSize);
  315. sendSize();
  316. console.log('using OffscreenCanvas');
  317. }
  318. </pre>
  319. <p>и отправить <code class="notranslate" translate="no">init</code> вместо <code class="notranslate" translate="no">main</code></p>
  320. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  321. + worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
  322. </pre>
  323. <p>для начала на главной странице мы можем сделать это</p>
  324. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  325. init({canvas});
  326. function sendSize() {
  327. state.width = canvas.clientWidth;
  328. state.height = canvas.clientHeight;
  329. }
  330. window.addEventListener('resize', sendSize);
  331. sendSize();
  332. console.log('using regular canvas');
  333. }
  334. </pre>
  335. <p>и с этим наш пример будет запускаться либо в <code class="notranslate" translate="no">OffscreenCanvas</code>, либо в качестве альтернативы запуску на главной странице.</p>
  336. <p></p><div translate="no" class="threejs_example_container notranslate">
  337. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-fallback.html"></iframe></div>
  338. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-fallback.html" target="_blank">нажмите здесь, чтобы открыть в отдельном окне</a>
  339. </div>
  340. <p></p>
  341. <p>Так что это было относительно легко. Попробуем поковырять.
  342. Мы возьмем код из примера RayCaster из и <a href="picking.html">статьи о выборе</a>
  343. заставим его работать за экраном.</p>
  344. <p>Давайте скопируем <code class="notranslate" translate="no">shared-cube.js</code> в <code class="notranslate" translate="no">shared-picking.js</code> и добавим части выбора. Копируем в <code class="notranslate" translate="no">PickHelper</code></p>
  345. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  346. constructor() {
  347. this.raycaster = new THREE.Raycaster();
  348. this.pickedObject = null;
  349. this.pickedObjectSavedColor = 0;
  350. }
  351. pick(normalizedPosition, scene, camera, time) {
  352. // restore the color if there is a picked object
  353. if (this.pickedObject) {
  354. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  355. this.pickedObject = undefined;
  356. }
  357. // cast a ray through the frustum
  358. this.raycaster.setFromCamera(normalizedPosition, camera);
  359. // get the list of objects the ray intersected
  360. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  361. if (intersectedObjects.length) {
  362. // pick the first object. It's the closest one
  363. this.pickedObject = intersectedObjects[0].object;
  364. // save its color
  365. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  366. // set its emissive color to flashing red/yellow
  367. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  368. }
  369. }
  370. }
  371. const pickPosition = {x: 0, y: 0};
  372. const pickHelper = new PickHelper();
  373. </pre>
  374. <p>Мы обновили <code class="notranslate" translate="no">pickPosition</code> с помощью мыши вот так</p>
  375. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  376. const rect = canvas.getBoundingClientRect();
  377. return {
  378. x: (event.clientX - rect.left) * canvas.width / rect.width,
  379. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  380. };
  381. }
  382. function setPickPosition(event) {
  383. const pos = getCanvasRelativePosition(event);
  384. pickPosition.x = (pos.x / canvas.width ) * 2 - 1;
  385. pickPosition.y = (pos.y / canvas.height) * -2 + 1; // note we flip Y
  386. }
  387. window.addEventListener('mousemove', setPickPosition);
  388. </pre>
  389. <p>Воркер не может напрямую считывать положение мыши, поэтому, как и код размера, давайте отправим сообщение с указанием положения мыши.
  390. Как и код размера, мы отправим позицию мыши и обновим <code class="notranslate" translate="no">pickPosition</code></p>
  391. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function size(data) {
  392. state.width = data.width;
  393. state.height = data.height;
  394. }
  395. +function mouse(data) {
  396. + pickPosition.x = data.x;
  397. + pickPosition.y = data.y;
  398. +}
  399. const handlers = {
  400. init,
  401. + mouse,
  402. size,
  403. };
  404. self.onmessage = function(e) {
  405. const fn = handlers[e.data.type];
  406. if (typeof fn !== 'function') {
  407. throw new Error('no handler for type: ' + e.data.type);
  408. }
  409. fn(e.data);
  410. };
  411. </pre>
  412. <p>Вернувшись на нашу главную страницу, нам нужно добавить код, чтобы передать мышь воркеру или главной странице.</p>
  413. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let sendMouse;
  414. function startWorker(canvas) {
  415. const offscreen = canvas.transferControlToOffscreen();
  416. const worker = new Worker('offscreencanvas-worker-picking.js', {type: 'module'});
  417. worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
  418. + sendMouse = (x, y) =&gt; {
  419. + worker.postMessage({
  420. + type: 'mouse',
  421. + x,
  422. + y,
  423. + });
  424. + };
  425. function sendSize() {
  426. worker.postMessage({
  427. type: 'size',
  428. width: canvas.clientWidth,
  429. height: canvas.clientHeight,
  430. });
  431. }
  432. window.addEventListener('resize', sendSize);
  433. sendSize();
  434. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  435. }
  436. function startMainPage(canvas) {
  437. init({canvas});
  438. + sendMouse = (x, y) =&gt; {
  439. + pickPosition.x = x;
  440. + pickPosition.y = y;
  441. + };
  442. function sendSize() {
  443. state.width = canvas.clientWidth;
  444. state.height = canvas.clientHeight;
  445. }
  446. window.addEventListener('resize', sendSize);
  447. sendSize();
  448. console.log('using regular canvas'); /* eslint-disable-line no-console */
  449. }
  450. </pre>
  451. <p>Затем мы можем скопировать весь код обработки мыши на главную страницу и внести незначительные изменения, чтобы использовать <code class="notranslate" translate="no">sendMouse</code>.</p>
  452. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setPickPosition(event) {
  453. const pos = getCanvasRelativePosition(event);
  454. - pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
  455. - pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
  456. + sendMouse(
  457. + (pos.x / canvas.clientWidth ) * 2 - 1,
  458. + (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
  459. }
  460. function clearPickPosition() {
  461. // unlike the mouse which always has a position
  462. // if the user stops touching the screen we want
  463. // to stop picking. For now we just pick a value
  464. // unlikely to pick something
  465. - pickPosition.x = -100000;
  466. - pickPosition.y = -100000;
  467. + sendMouse(-100000, -100000);
  468. }
  469. window.addEventListener('mousemove', setPickPosition);
  470. window.addEventListener('mouseout', clearPickPosition);
  471. window.addEventListener('mouseleave', clearPickPosition);
  472. window.addEventListener('touchstart', (event) =&gt; {
  473. // prevent the window from scrolling
  474. event.preventDefault();
  475. setPickPosition(event.touches[0]);
  476. }, {passive: false});
  477. window.addEventListener('touchmove', (event) =&gt; {
  478. setPickPosition(event.touches[0]);
  479. });
  480. window.addEventListener('touchend', clearPickPosition);
  481. </pre>
  482. <p>и с этим выбором следует работать с <code class="notranslate" translate="no">OffscreenCanvas</code>.</p>
  483. <p></p><div translate="no" class="threejs_example_container notranslate">
  484. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-picking.html"></iframe></div>
  485. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-picking.html" target="_blank">нажмите здесь, чтобы открыть в отдельном окне</a>
  486. </div>
  487. <p></p>
  488. <p>Сделаем еще один шаг и добавим <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>. Это будет немного больше.
  489. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> довольно широко используют DOM для проверки мыши, событий касания и клавиатуры.</p>
  490. <p>В отличие от нашего кода, мы не можем использовать объект глобального <code class="notranslate" translate="no">state</code>, не переписав весь код <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> для работы с ним. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> принимают элемент, к которому они присоединяют большинство используемых ими событий DOM. Возможно, мы могли бы передать наш собственный объект, имеющий ту же поверхность API, что и элемент DOM. Нам нужно только поддерживать функции, которые необходимы <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>.</p>
  491. <p>Копаясь в <a href="https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js">исходном коде OrbitControls</a>
  492. похоже, что нам нужно обработать следующие события.</p>
  493. <ul>
  494. <li>contextmenu</li>
  495. <li>pointerdown</li>
  496. <li>pointermove</li>
  497. <li>pointerup</li>
  498. <li>touchstart</li>
  499. <li>touchmove</li>
  500. <li>touchend</li>
  501. <li>wheel</li>
  502. <li>keydown</li>
  503. </ul>
  504. <p>Для событий мыши нам нужны свойства <code class="notranslate" translate="no">ctrlKey</code>, <code class="notranslate" translate="no">metaKey</code>, <code class="notranslate" translate="no">shiftKey</code>,
  505. <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>, и <code class="notranslate" translate="no">pageY</code>.</p>
  506. <p>Для событий нажатия клавиатуры нам нужны свойства <code class="notranslate" translate="no">ctrlKey</code>, <code class="notranslate" translate="no">metaKey</code>, <code class="notranslate" translate="no">shiftKey</code>,
  507. и <code class="notranslate" translate="no">keyCode</code>.</p>
  508. <p>Для события wheel нам нужно только свойство <code class="notranslate" translate="no">deltaY</code></p>
  509. <p>А для событий касания нам понадобятся только <code class="notranslate" translate="no">pageX</code> и <code class="notranslate" translate="no">pageY</code> из свойства <code class="notranslate" translate="no">touches</code>.</p>
  510. <p>Итак, создадим пару прокси-объектов. Одна часть будет работать на главной странице, получать все эти события и передавать соответствующие значения свойств воркеру. Другая часть будет запускаться в воркере, получать эти события и передавать их, используя события, которые имеют ту же структуру, что и исходные события DOM, поэтому <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> не сможет определить разницу.</p>
  511. <p>Вот код рабочей части.</p>
  512. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {EventDispatcher} from '../../build/three.module.js';
  513. class ElementProxyReceiver extends EventDispatcher {
  514. constructor() {
  515. super();
  516. }
  517. handleEvent(data) {
  518. this.dispatchEvent(data);
  519. }
  520. }
  521. </pre>
  522. <p>Все, что он делает, - это если он получает сообщение, то отправляет его. Он наследуется от <a href="/docs/#api/en/core/EventDispatcher"><code class="notranslate" translate="no">EventDispatcher</code></a>, который предоставляет такие методы, как <code class="notranslate" translate="no">addEventListener</code> и <code class="notranslate" translate="no">removeEventListener</code>, точно так же, как элемент DOM, поэтому, если мы передадим его в <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>, он должен работать.</p>
  523. <p><code class="notranslate" translate="no">ElementProxyReceiver</code> обрабатывает 1 элемент. В нашем случае нам нужен только один, но лучше думать головой, так что давайте заставим менеджера управлять более чем одним из них.</p>
  524. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ProxyManager {
  525. constructor() {
  526. this.targets = {};
  527. this.handleEvent = this.handleEvent.bind(this);
  528. }
  529. makeProxy(data) {
  530. const {id} = data;
  531. const proxy = new ElementProxyReceiver();
  532. this.targets[id] = proxy;
  533. }
  534. getProxy(id) {
  535. return this.targets[id];
  536. }
  537. handleEvent(data) {
  538. this.targets[data.id].handleEvent(data.data);
  539. }
  540. }
  541. </pre>
  542. <p>Мы можем создать экземпляр <code class="notranslate" translate="no">ProxyManager</code> и вызвать его метод makeProxy с идентификатором, который создаст <code class="notranslate" translate="no">ElementProxyReceiver</code>, который будет отвечать на сообщения с этим идентификатором.</p>
  543. <p>Давайте подключим его к обработчику сообщений нашего воркера.</p>
  544. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const proxyManager = new ProxyManager();
  545. function start(data) {
  546. const proxy = proxyManager.getProxy(data.canvasId);
  547. init({
  548. canvas: data.canvas,
  549. inputElement: proxy,
  550. });
  551. }
  552. function makeProxy(data) {
  553. proxyManager.makeProxy(data);
  554. }
  555. ...
  556. const handlers = {
  557. - init,
  558. - mouse,
  559. + start,
  560. + makeProxy,
  561. + event: proxyManager.handleEvent,
  562. size,
  563. };
  564. self.onmessage = function(e) {
  565. const fn = handlers[e.data.type];
  566. if (typeof fn !== 'function') {
  567. throw new Error('no handler for type: ' + e.data.type);
  568. }
  569. fn(e.data);
  570. };
  571. </pre>
  572. <p>Нам также нужно добавить <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> в начало скрипта.</p>
  573. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from '../../build/three.module.js';
  574. +import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';
  575. export function init(data) {
  576. - const {canvas} = data;
  577. + const {canvas, inputElement} = data;
  578. const renderer = new THREE.WebGLRenderer({canvas});
  579. + const controls = new OrbitControls(camera, inputElement);
  580. + controls.target.set(0, 0, 0);
  581. + controls.update();
  582. </pre>
  583. <p>Обратите внимание, что мы передаем <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> нашему прокси через <code class="notranslate" translate="no">inputElement</code> вместо передачи холста, как в других примерах, отличных от <code class="notranslate" translate="no">OffscreenCanvas</code>.</p>
  584. <p>Затем мы можем переместить весь код события выбора из файла HTML в общий код three.js, а также изменить <code class="notranslate" translate="no">canvas</code> на <code class="notranslate" translate="no">inputElement</code>.</p>
  585. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  586. - const rect = canvas.getBoundingClientRect();
  587. + const rect = inputElement.getBoundingClientRect();
  588. return {
  589. x: event.clientX - rect.left,
  590. y: event.clientY - rect.top,
  591. };
  592. }
  593. function setPickPosition(event) {
  594. const pos = getCanvasRelativePosition(event);
  595. - sendMouse(
  596. - (pos.x / canvas.clientWidth ) * 2 - 1,
  597. - (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
  598. + pickPosition.x = (pos.x / inputElement.clientWidth ) * 2 - 1;
  599. + pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1; // note we flip Y
  600. }
  601. function clearPickPosition() {
  602. // unlike the mouse which always has a position
  603. // if the user stops touching the screen we want
  604. // to stop picking. For now we just pick a value
  605. // unlikely to pick something
  606. - sendMouse(-100000, -100000);
  607. + pickPosition.x = -100000;
  608. + pickPosition.y = -100000;
  609. }
  610. *inputElement.addEventListener('mousemove', setPickPosition);
  611. *inputElement.addEventListener('mouseout', clearPickPosition);
  612. *inputElement.addEventListener('mouseleave', clearPickPosition);
  613. *inputElement.addEventListener('touchstart', (event) =&gt; {
  614. // prevent the window from scrolling
  615. event.preventDefault();
  616. setPickPosition(event.touches[0]);
  617. }, {passive: false});
  618. *inputElement.addEventListener('touchmove', (event) =&gt; {
  619. setPickPosition(event.touches[0]);
  620. });
  621. *inputElement.addEventListener('touchend', clearPickPosition);
  622. </pre>
  623. <p>Вернувшись на главную страницу, нам нужен код для отправки сообщений для всех событий, которые мы перечислили выше.</p>
  624. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let nextProxyId = 0;
  625. class ElementProxy {
  626. constructor(element, worker, eventHandlers) {
  627. this.id = nextProxyId++;
  628. this.worker = worker;
  629. const sendEvent = (data) =&gt; {
  630. this.worker.postMessage({
  631. type: 'event',
  632. id: this.id,
  633. data,
  634. });
  635. };
  636. // register an id
  637. worker.postMessage({
  638. type: 'makeProxy',
  639. id: this.id,
  640. });
  641. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  642. element.addEventListener(eventName, function(event) {
  643. handler(event, sendEvent);
  644. });
  645. }
  646. }
  647. }
  648. </pre>
  649. <p><code class="notranslate" translate="no">ElementProxy</code> берет элемент, события которого мы хотим проксировать. Затем он регистрирует идентификатор у воркера, выбирая его и отправляя через сообщение <code class="notranslate" translate="no">makeProxy</code>, которое мы настроили ранее. Рабочий создаст <code class="notranslate" translate="no">ElementProxyReceiver</code> и зарегистрирует его для этого идентификатора.</p>
  650. <p>Затем у нас есть объект обработчиков событий для регистрации. Таким образом, мы можем передавать обработчики только тех событий, которые мы хотим переслать воркеру.</p>
  651. <p>Когда мы запускаем воркер, мы сначала создаем прокси и передаем наши обработчики событий.</p>
  652. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  653. const offscreen = canvas.transferControlToOffscreen();
  654. const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', {type: 'module'});
  655. + const eventHandlers = {
  656. + contextmenu: preventDefaultHandler,
  657. + mousedown: mouseEventHandler,
  658. + mousemove: mouseEventHandler,
  659. + mouseup: mouseEventHandler,
  660. + pointerdown: mouseEventHandler,
  661. + pointermove: mouseEventHandler,
  662. + pointerup: mouseEventHandler,
  663. + touchstart: touchEventHandler,
  664. + touchmove: touchEventHandler,
  665. + touchend: touchEventHandler,
  666. + wheel: wheelEventHandler,
  667. + keydown: filteredKeydownEventHandler,
  668. + };
  669. + const proxy = new ElementProxy(canvas, worker, eventHandlers);
  670. worker.postMessage({
  671. type: 'start',
  672. canvas: offscreen,
  673. + canvasId: proxy.id,
  674. }, [offscreen]);
  675. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  676. }
  677. </pre>
  678. <p>А вот и обработчики событий. Все, что они делают, - это копируют список свойств из полученного события. Им передается функция <code class="notranslate" translate="no">sendEvent</code>, в которую они передают созданные данные. Эта функция добавит правильный идентификатор и отправит его воркеру.</p>
  679. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mouseEventHandler = makeSendPropertiesHandler([
  680. 'ctrlKey',
  681. 'metaKey',
  682. 'shiftKey',
  683. 'button',
  684. 'pointerType',
  685. 'clientX',
  686. 'clientY',
  687. 'pageX',
  688. 'pageY',
  689. ]);
  690. const wheelEventHandlerImpl = makeSendPropertiesHandler([
  691. 'deltaX',
  692. 'deltaY',
  693. ]);
  694. const keydownEventHandler = makeSendPropertiesHandler([
  695. 'ctrlKey',
  696. 'metaKey',
  697. 'shiftKey',
  698. 'keyCode',
  699. ]);
  700. function wheelEventHandler(event, sendFn) {
  701. event.preventDefault();
  702. wheelEventHandlerImpl(event, sendFn);
  703. }
  704. function preventDefaultHandler(event) {
  705. event.preventDefault();
  706. }
  707. function copyProperties(src, properties, dst) {
  708. for (const name of properties) {
  709. dst[name] = src[name];
  710. }
  711. }
  712. function makeSendPropertiesHandler(properties) {
  713. return function sendProperties(event, sendFn) {
  714. const data = {type: event.type};
  715. copyProperties(event, properties, data);
  716. sendFn(data);
  717. };
  718. }
  719. function touchEventHandler(event, sendFn) {
  720. const touches = [];
  721. const data = {type: event.type, touches};
  722. for (let i = 0; i &lt; event.touches.length; ++i) {
  723. const touch = event.touches[i];
  724. touches.push({
  725. pageX: touch.pageX,
  726. pageY: touch.pageY,
  727. });
  728. }
  729. sendFn(data);
  730. }
  731. // The four arrow keys
  732. const orbitKeys = {
  733. '37': true, // left
  734. '38': true, // up
  735. '39': true, // right
  736. '40': true, // down
  737. };
  738. function filteredKeydownEventHandler(event, sendFn) {
  739. const {keyCode} = event;
  740. if (orbitKeys[keyCode]) {
  741. event.preventDefault();
  742. keydownEventHandler(event, sendFn);
  743. }
  744. }
  745. </pre>
  746. <p>Это кажется близким к запуску, но если мы действительно попробуем, то увидим, что <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> нужно еще кое-что.</p>
  747. <p>Один из них - <code class="notranslate" translate="no">element.focus</code>. Нам не нужно, чтобы это происходило в воркере, поэтому давайте просто добавим заглушку.</p>
  748. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  749. constructor() {
  750. super();
  751. }
  752. handleEvent(data) {
  753. this.dispatchEvent(data);
  754. }
  755. + focus() {
  756. + // no-op
  757. + }
  758. }
  759. </pre>
  760. <p>Другой - они вызывают <code class="notranslate" translate="no">event.preventDefault</code> и <code class="notranslate" translate="no">event.stopPropagation</code>. Мы уже обрабатываем это на главной странице, так что это тоже может быть пустышкой.</p>
  761. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function noop() {
  762. +}
  763. class ElementProxyReceiver extends THREE.EventDispatcher {
  764. constructor() {
  765. super();
  766. }
  767. handleEvent(data) {
  768. + data.preventDefault = noop;
  769. + data.stopPropagation = noop;
  770. this.dispatchEvent(data);
  771. }
  772. focus() {
  773. // no-op
  774. }
  775. }
  776. </pre>
  777. <p>Другой - они смотрят на <code class="notranslate" translate="no">clientWidth</code> и <code class="notranslate" translate="no">clientHeight</code>. Раньше мы передавали размер, но мы можем обновить пару прокси, чтобы передать его.</p>
  778. <p>В воркере...</p>
  779. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  780. constructor() {
  781. super();
  782. }
  783. + get clientWidth() {
  784. + return this.width;
  785. + }
  786. + get clientHeight() {
  787. + return this.height;
  788. + }
  789. + getBoundingClientRect() {
  790. + return {
  791. + left: this.left,
  792. + top: this.top,
  793. + width: this.width,
  794. + height: this.height,
  795. + right: this.left + this.width,
  796. + bottom: this.top + this.height,
  797. + };
  798. + }
  799. handleEvent(data) {
  800. + if (data.type === 'size') {
  801. + this.left = data.left;
  802. + this.top = data.top;
  803. + this.width = data.width;
  804. + this.height = data.height;
  805. + return;
  806. + }
  807. data.preventDefault = noop;
  808. data.stopPropagation = noop;
  809. this.dispatchEvent(data);
  810. }
  811. focus() {
  812. // no-op
  813. }
  814. }
  815. </pre>
  816. <p>обратно на главную страницу нам нужно отправить размер, а также левую и верхнюю позиции. Обратите внимание, что мы не обрабатываем перемещение холста, только если оно меняет размер. Если вы хотите обрабатывать перемещение, вам нужно будет вызывать <code class="notranslate" translate="no">sendSize</code> каждый раз, когда что-то перемещает холст.</p>
  817. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxy {
  818. constructor(element, worker, eventHandlers) {
  819. this.id = nextProxyId++;
  820. this.worker = worker;
  821. const sendEvent = (data) =&gt; {
  822. this.worker.postMessage({
  823. type: 'event',
  824. id: this.id,
  825. data,
  826. });
  827. };
  828. // register an id
  829. worker.postMessage({
  830. type: 'makeProxy',
  831. id: this.id,
  832. });
  833. + sendSize();
  834. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  835. element.addEventListener(eventName, function(event) {
  836. handler(event, sendEvent);
  837. });
  838. }
  839. + function sendSize() {
  840. + const rect = element.getBoundingClientRect();
  841. + sendEvent({
  842. + type: 'size',
  843. + left: rect.left,
  844. + top: rect.top,
  845. + width: element.clientWidth,
  846. + height: element.clientHeight,
  847. + });
  848. + }
  849. +
  850. + window.addEventListener('resize', sendSize);
  851. }
  852. }
  853. </pre>
  854. <p>и в нашем общем коде three.js нам больше не нужно <code class="notranslate" translate="no">state</code></p>
  855. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-export const state = {
  856. - width: 300, // canvas default
  857. - height: 150, // canvas default
  858. -};
  859. ...
  860. function resizeRendererToDisplaySize(renderer) {
  861. const canvas = renderer.domElement;
  862. - const width = state.width;
  863. - const height = state.height;
  864. + const width = inputElement.clientWidth;
  865. + const height = inputElement.clientHeight;
  866. const needResize = canvas.width !== width || canvas.height !== height;
  867. if (needResize) {
  868. renderer.setSize(width, height, false);
  869. }
  870. return needResize;
  871. }
  872. function render(time) {
  873. time *= 0.001;
  874. if (resizeRendererToDisplaySize(renderer)) {
  875. - camera.aspect = state.width / state.height;
  876. + camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
  877. camera.updateProjectionMatrix();
  878. }
  879. ...
  880. </pre>
  881. <p>Еще несколько приемов. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> добавляют события <code class="notranslate" translate="no">pointermove</code> и <code class="notranslate" translate="no">pointerup</code> в <code class="notranslate" translate="no">ownerDocument</code> элемента для обработки захвата мыши (когда мышь выходит за пределы окна).</p>
  882. <p>Далее код ссылается на глобальный <code class="notranslate" translate="no">document</code>, но в воркере нет глобального документа.</p>
  883. <p>Мы можем решить все это с помощью 2 быстрых приемов. В нашем рабочем коде мы повторно используем прокси для обеих задач</p>
  884. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function start(data) {
  885. const proxy = proxyManager.getProxy(data.canvasId);
  886. + proxy.ownerDocument = proxy; // HACK!
  887. + self.document = {} // HACK!
  888. init({
  889. canvas: data.canvas,
  890. inputElement: proxy,
  891. });
  892. }
  893. </pre>
  894. <p>Это даст <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> возможность проверить, что соответствует их ожиданиям.</p>
  895. <p>Я знаю, что это было довольно сложно. Краткая версия:<code class="notranslate" translate="no">ElementProxy</code> запускается на главной странице и пересылает события DOM в <code class="notranslate" translate="no">ElementProxyReceiver</code>
  896. в воркере, который маскируется под <code class="notranslate" translate="no">HTMLElement</code>, который мы можем использовать как с <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>, так и с нашим собственным кодом.</p>
  897. <p>И последнее - это наш запасной вариант, когда мы не используем OffscreenCanvas. Все, что нам нужно сделать, это передать сам холст как наш <code class="notranslate" translate="no">inputElement</code>.</p>
  898. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  899. - init({canvas});
  900. + init({canvas, inputElement: canvas});
  901. console.log('using regular canvas');
  902. }
  903. </pre>
  904. <p>и теперь у нас должен быть OrbitControls, работающий с OffscreenCanvas</p>
  905. <p></p><div translate="no" class="threejs_example_container notranslate">
  906. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-orbitcontrols.html"></iframe></div>
  907. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-orbitcontrols.html" target="_blank">нажмите здесь, чтобы открыть в отдельном окне</a>
  908. </div>
  909. <p></p>
  910. <p>Это, наверное, самый сложный пример на этом сайте.
  911. Это немного сложно понять, потому что для каждого образца задействовано 3 файла. HTML-файл, рабочий файл, общий код three.js.</p>
  912. <p>Я надеюсь, что это было не так уж сложно понять, и что он предоставил несколько полезных примеров работы с three.js, OffscreenCanvas и веб-воркерами.</p>
  913. </div>
  914. </div>
  915. </div>
  916. <script src="/manual/resources/prettify.js"></script>
  917. <script src="/manual/resources/lesson.js"></script>
  918. </body></html>