offscreencanvas.html 54 KB

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