offscreencanvas.html 55 KB

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