Советы

Эта статья представляет собой набор небольших проблем, с которыми вы можете столкнуться при использовании three.js, которые кажутся слишком маленькими, чтобы иметь собственную статью.


Делаем скриншот холста

В браузере фактически есть 2 функции, которые сделают скриншот. Старая canvas.toDataURL и новая улучшенная canvas.toBlob

Таким образом, вы думаете, что было бы легко сделать снимок экрана, просто добавив такой код, как

<canvas id="c"></canvas>
+<button id="screenshot" type="button">Save...</button>
const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

const saveBlob = (function() {
  const a = document.createElement('a');
  document.body.appendChild(a);
  a.style.display = 'none';
  return function saveData(blob, fileName) {
     const url = window.URL.createObjectURL(blob);
     a.href = url;
     a.download = fileName;
     a.click();
  };
}());

Вот пример из статьи об отзывчивости с добавленным выше кодом и некоторым CSS для размещения кнопки

Когда я попробовал это, я получил этот скриншот

Да, это просто черное изображение.

Возможно, это сработало для вас в зависимости от вашего браузера / ОС, но в целом это вряд ли сработает.

Проблема заключается в том, что по соображениям производительности и совместимости браузер по умолчанию очищает буфер рисования WebGL-холста после его отрисовки.

Решение состоит в том, чтобы вызвать ваш код рендеринга непосредственно перед захватом.

В нашем коде нам нужно настроить несколько вещей. Сначала давайте выделим код рендеринга.

+const state = {
+  time: 0,
+};

-function render(time) {
-  time *= 0.001;
+function render() {
  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

  cubes.forEach((cube, ndx) => {
    const speed = 1 + ndx * .1;
-    const rot = time * speed;
+    const rot = state.time * speed;
    cube.rotation.x = rot;
    cube.rotation.y = rot;
  });

  renderer.render(scene, camera);

-  requestAnimationFrame(render);
}

+function animate(time) {
+  state.time = time * 0.001;
+
+  render();
+
+  requestAnimationFrame(animate);
+}
+requestAnimationFrame(animate);

Теперь этот render касается только фактического рендеринга, мы можем вызвать его непосредственно перед захватом холста.

const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
+  render();
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

И теперь это должно работать.

Для другого решения см. Следующий пункт.


Предотвращение очистки холста

Допустим, вы хотели, чтобы пользователь рисовал анимированный объект. Вам нужно передать preserveDrawingBuffer: true при создании WebGLRenderer. Это мешает браузеру очистить холст. Вы также должны сказать three.js не очищать холст.

const canvas = document.querySelector('#c');
-const renderer = new THREE.WebGLRenderer({canvas});
+const renderer = new THREE.WebGLRenderer({
+  canvas,
+  preserveDrawingBuffer: true,
+  alpha: true,
+});
+renderer.autoClearColor = false;

Обратите внимание, что если вы серьезно относитесь к созданию программы для рисования, это не будет решением, так как браузер будет очищать холст каждый раз, когда мы меняем его разрешение. Мы меняем разрешение в зависимости от размера дисплея. Размер дисплея изменяется при изменении размера окна. Это включает в себя, когда пользователь загружает файл, даже в другой вкладке, и браузер добавляет строку состояния. Это также включает в себя, когда пользователь поворачивает свой телефон и браузер переключается с портретного на альбомный.

Если вы действительно хотите создать программу для рисования, вы должны . визуализировать текстуру, используя цель визуализации.


Ввод с клавиатуры

В этих уроках мы часто прикрепляли слушателей событий canvas. Хотя многие события работают, по умолчанию не работают события клавиатуры.

Чтобы получить события клавиатуры, установите для холста tabindex значение 0 или более. Например.

<canvas tabindex="0"></canvas>

Это в конечном итоге вызывает новую проблему, хотя. Все, что имеет установленный tabindex будет выделено, когда оно будет в фокусе. Чтобы исправить это, установите фокус CSS outline:none.

canvas:focus {
  outline:none;
}

Для демонстрации здесь представлены 3 холста

<canvas id="c1"></canvas>
<canvas id="c2" tabindex="0"></canvas>
<canvas id="c3" tabindex="1"></canvas>

и немного CSS только для последнего холста

#c3:focus {
    outline: none;
}

Давайте прикрепим к ним всех слушателей событий

document.querySelectorAll('canvas').forEach((canvas) => {
  const ctx = canvas.getContext('2d');

  function draw(str) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(str, canvas.width / 2, canvas.height / 2);
  }
  draw(canvas.id);

  canvas.addEventListener('focus', () => {
    draw('has focus press a key');
  });

  canvas.addEventListener('blur', () => {
    draw('lost focus');
  });

  canvas.addEventListener('keydown', (e) => {
    draw(`keyCode: ${e.keyCode}`);
  });
});

Обратите внимание, что вы не можете получить первый холст, чтобы принять ввод с клавиатуры. Второе полотно вы можете, но оно подсвечивается. На третьем холсте применяются оба решения.


Делаем холст прозрачным

По умолчанию THREE.js делает холст непрозрачным. Если вы хотите, чтобы холст был прозрачным, передайте alpha:true при создании WebGLRenderer

const canvas = document.querySelector('#c');
-const renderer = new THREE.WebGLRenderer({canvas});
+const renderer = new THREE.WebGLRenderer({
+  canvas,
+  alpha: true,
+});

Вы, вероятно, также хотите сказать, что ваши результаты не используют предварительно умноженную альфа

const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({
  canvas,
  alpha: true,
+  premultipliedAlpha: false,
});

Three.js по умолчанию использует холст с использованием premultipliedAlpha: true но по умолчанию используется для материалов, которые выводят premultipliedAlpha: false.

Если вы хотите лучше понять, когда и когда не следует использовать предварительно умноженную альфу, вот хорошая статья об этом.

В любом случае давайте настроим простой пример с прозрачным холстом.

Мы применили настройки выше к примеру из статьи об отзывчивости. Давайте также сделаем материалы более прозрачными

function makeInstance(geometry, color, x) {
-  const material = new THREE.MeshPhongMaterial({color});
+  const material = new THREE.MeshPhongMaterial({
+    color,
+    opacity: 0.5,
+  });

...

И давайте добавим немного HTML-контента

<body>
  <canvas id="c"></canvas>
+  <div id="content">
+    <div>
+      <h1>Cubes-R-Us!</h1>
+      <p>We make the best cubes!</p>
+    </div>
+  </div>
</body>

а также немного CSS, чтобы поставить холст впереди

body {
    margin: 0;
}
#c {
    width: 100%;
    height: 100%;
    display: block;
+    position: fixed;
+    left: 0;
+    top: 0;
+    z-index: 2;
+    pointer-events: none;
}
+#content {
+  font-size: 7vw;
+  font-family: sans-serif;
+  text-align: center;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}

обратите внимание, что pointer-events: none делает холст невидимым для мыши и сенсорных событий, поэтому вы можете выделить текст под ним.


Создание анимированного фона в three.js

Распространенный вопрос - как сделать анимацию three.js фоном веб-страницы.

Есть 2 очевидных способа.

  • Установите у холста CSS position fixed как в
#c {
 position: fixed;
 left: 0;
 top: 0;
 ...
}

Вы можете в основном увидеть это точное решение на предыдущем примере. Просто установите z-index на -1, и кубы появятся за текстом.

Небольшим недостатком этого решения является то, что ваш JavaScript должен интегрироваться со страницей, и если у вас сложная страница, вам нужно убедиться, что ни один из JavaScript в вашей визуализации three.js не конфликтует с JavaScript, выполняющим другие действия на странице.

  • Используйте iframe

Это решение используется на главной странице этого сайта .

На вашей веб-странице просто вставьте iframe, например

<iframe id="background" src="responsive.html">
<div>
  Your content goes here.
</div>

Затем создайте стиль iframe, чтобы заполнить окно и оказаться в фоновом режиме, который в основном является тем же кодом, который мы использовали выше для холста, за исключением того, что нам также нужно установить значение none, поскольку у iframes по умолчанию есть граница.

#background {
    position: fixed;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    z-index: -1;
    border: none;
    pointer-events: none;
}