Хорошее дополнение к вашему следующему веб-приложению

Идея сделать такую ​​штуку пришла мне в голову, когда я увидел похожие демо на официальном сайте Threejs.

Так что я хотел просто скопировать и вставить код и посмотреть, что получилось, но столкнулся с проблемой, что при переносе чужого кода в мой редактор приложение не работало должным образом, и в этих демонстрациях использовалась не стандартная математика. Как-то странно подумал я. В конце концов я решил написать весь код с нуля, а именно получить чужой код и настроить его с помощью надлежащей стандартной математики.

Что такое скайбокс

Эта техника, когда вы находитесь внутри виртуального 3D-мира, состоящего из простых изображений, называется скайбоксом.

Концепция скайбокса очень проста. Скайбокс - это просто ящик или сфера с небом (любым изображением) нарисованным на нем. Мы помещаем камеру внутрь коробки, и кажется, что на заднем плане небо.

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

Настраивать

Единственный обязательный элемент HTML — это элемент ‹div› с атрибутом id.

<div id="app"></div>

Единственный требуемый CSS — сделать элемент canvas черным фоном. Также избавьтесь от полей тела и установите переполнение: скрыто, чтобы настроить границы тела и холста на экране.

body {
  margin: 0;
  overflow: hidden;
}
canvas{
  width: 100%;
  height: 100%
}

Теперь приступим к самой интересной части JavaScript.
Мы инициализируем общие константы, которые нам скоро понадобятся.

const app = document.getElementById('app');
let scene;
let renderer;
let camera;
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let fov = 75;
let lon = 0;
let lat = 0;
let phi = 0;
let theta = 0;
const nearPlane = 1;
const farPlane = 1000;
let aspect = windowWidth / windowHeight;
const dpr = window.devicePixelRatio;
let sphereMaterial;
let isManual = false;
let lastMouseX = 0;
let lastMouseY = 0;
let lastLon = 0;
let lastLat = 0;

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

function init() {
  renderer = new THREE.WebGLRenderer();
  renderer.setSize(windowWidth, windowHeight);
  renderer.setPixelRatio(dpr);
  app.appendChild(renderer.domElement);
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(75, aspect, nearPlane, farPlane);
  camera.target = new THREE.Vector3(0, 0, 0);
  createSphere();
  initEventListeners();
  animate();
}
window.onload = init;

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

function createSphere() {
  const sphere = new THREE.SphereGeometry(100, 100, 40);
  // or we can use just sphere.scale(-1, 1, 1);
  sphere.applyMatrix(new THREE.Matrix4().makeScale(-1, 1, 1));
  sphereMaterial = new THREE.MeshBasicMaterial();
  sphereMaterial.map = new THREE.TextureLoader().load(panoramasArray[panoramaNumber])
  const sphereMesh = new THREE.Mesh(sphere, sphereMaterial);
  scene.add(sphereMesh);
}

Как и в каждом уроке, нам нужно добавить функцию изменения размера холста при изменении размера окна.

function resizeWindow() {
  const width = window.innerWidth;
  const height = window.innerHeight;
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  const canvasPixelWidth = canvas.width / dpr;
  const canvasPixelHeight = canvas.height / dpr;
  if (canvasPixelWidth !== width || canvasPixelHeight !== height) {
    renderer.setSize(width, height, false);
  }
}

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

function initEventListeners() {
  document.addEventListener('mousedown', handleDocumentMouseDown, false);
  document.addEventListener('mousemove', handleDocumentMouseMove,  false);
  document.addEventListener('mouseup', handleDocumentMouseUp, false);
  document.addEventListener('touchstart', handleDocumentMouseDown, false);
  document.addEventListener('touchmove', handleDocumentMouseMove, false);
  document.addEventListener('touchend', handleDocumentMouseUp, false);
  // when a user clicks on space we change panorama
  document.addEventListener('keyup', handleKeyUp);
  // Drag n Drop to load new panorama
  document.addEventListener('dragover', handleDragOver, false);
  document.addEventListener('dragenter', handleDragEnter, false);
  document.addEventListener('dragleave', handleDragLeave, false);
  document.addEventListener('drop', handleDrop, false);
  
  window.addEventListener('resize', resizeWindow);
}

Ключевой частью является наша функция анимации. Здесь мы проверяем, активно ли событие mousemouse, если нет, то суммируем значение lon, преобразуем из сферических в декартовы координаты иобновляем цель камеры.
Мы сделать это из-за использования сферы, и нам нужно это преобразование, потому что наша камера ждет декартовых координат, чтобы наконец отобразить 2D-изображение. В каждом кадре мы добавляем значение широты, чтобы заставить камеру двигаться.

function animate() {
  requestAnimationFrame(animate);
  if(!isManual){
    lat += 0.1;
  }
  phi = (90 - lat) * Math.PI / 180;
  theta = lon * Math.PI / 180;
  // Transform from Spherical to Cartesian coordinates
  camera.target.x = 500 * Math.sin(phi) * Math.cos(theta);
  camera.target.y = 500 * Math.sin(phi) * Math.sin(theta);
  camera.target.z = 500 * Math.cos(phi);
  camera.lookAt(camera.target);
  render();
}
function render() {
  renderer.render(scene, camera);
}

Далее я пишу обработчики событий. Это просто: обновите наши общие константы, используйте preventDefault для перетаскивания, как это показано в MDN, измените непрозрачность холста, когда мы перетаскиваем новое изображение, и сбрасывайте значения мыши и latLng, когда событие выполнено. Мы также определяем коэффициент 0,1 (эмпирически), чтобы замедлить изменение цели камеры, потому что цель камеры onmousemove меняется очень быстро.

function handleKeyUp(event) {
  if (event.code === 'Space') {
    panoramaNumber = (panoramaNumber + 1) % panoramasArray.length;
    sphereMaterial.map = new THREE.TextureLoader().load(panoramasArray[panoramaNumber]);
  }
}
// when the mouse is pressed, we switch to manual control and save current coordinates
function handleDocumentMouseDown(event){
  event.preventDefault();
  isManual = true;
  
  const clientX = event.clientX || event.touches[ 0 ].clientX;
  const clientY = event.clientY || event.touches[ 0 ].clientY;
  
  lastMouseX = clientX;
  lastMouseY = clientY;
  lastLon = lon;
  lastLat = lat;
}
// when the mouse moves, if in manual control we adjust coordinates
function handleDocumentMouseMove(event) {
  if (isManual) {
    const clientX = event.clientX || event.touches[0].clientX;
    const clientY = event.clientY || event.touches[0].clientY;
    lat = (clientX - lastMouseX) * 0.1 + lastLat;
    lon = (clientY - lastMouseY) * 0.1 + lastLon;
  }
}
// when the mouse is released, we turn manual control off
function handleDocumentMouseUp(){
  isManual = false;
  
  lastMouseX = 0;
  lastMouseY = 0;
  lastLon = 0;
  lastLat = 0;
}
function handleDragOver(event) {
  event.preventDefault();
  event.stopPropagation();
  event.dataTransfer.dropEffect = 'copy';
}
function handleDragEnter(event) {
  event.preventDefault();
  event.stopPropagation();
  
  app.style.opacity = 0.5;
}
function handleDragLeave(event) {
  event.preventDefault();
  event.stopPropagation();
  app.style.opacity = 1;
}
function handleDrop(event) {
  event.preventDefault();
  event.stopPropagation();
  const reader = new FileReader();
  
  reader.addEventListener('load', function(event) {
    sphereMaterial.map.image.src = event.target.result;
    sphereMaterial.map.needsUpdate = true;
}, false );
  reader.readAsDataURL(event.dataTransfer.files[0]);
  app.style.opacity = 1;
}

Наконец, мы сбрасываем обработчики событий, это обычная практика.

function cleanup() {
  document.removeEventListener('mousedown', handleDocumentMouseDown, false);
  document.removeEventListener('mousemove', handleDocumentMouseMove, false);
  document.removeEventListener('mouseup', handleDocumentMouseUp, false);
  document.removeEventListener('touchstart', handleDocumentMouseDown, false);
  document.removeEventListener('touchmove', handleDocumentMouseMove, false);
  document.removeEventListener('touchend', handleDocumentMouseUp, false);
  document.removeEventListener('keyup', handleKeyUp);
  document.removeEventListener('dragover', handleDragOver, false);
  document.removeEventListener('dragenter', handleDragEnter, false);
  document.removeEventListener('dragleave', handleDragLeave, false);
  document.removeEventListener('drop', handleDrop, false);
  window.removeEventListener('resize', resizeWindow);
}
window.onunload = cleanup;

Конец

Чтобы посмотреть живую демонстрацию, пожалуйста, нажмите здесь.