Эта статья была спонсирована WRLD 3D . Спасибо за поддержку партнеров, которые делают возможным использование SitePoint.
Следующее происходит с 7:00 до 8:00, в канун Рождества. События происходят в режиме реального времени.
Для всех наших возможностей по сбору данных мы все еще безнадежны, когда дело доходит до визуализации этих данных в трехмерном мире, в котором мы живем. Мы смотрим на двухмерные диаграммы и записи в журналах, но большая часть данных, которые мы извлекаем из мира, имеет смысл в трехмерном контексте. И это может быть полезно для визуализации этих данных, когда они применяются обратно в 3D-модель.
Эту проблему пытается решить дополненная реальность. В отличие от вымышленной среды виртуальной реальности, дополненная реальность может помочь нам решить многие реальные проблемы; применяя данные, которые мы иначе использовали бы через 2D-среду, к реальному миру вокруг нас. Картография — первенец среди детей дополненной реальности.
Когда WRLD подошел к нам, чтобы написать о своей платформе, меня сразу поразили графика и производительность их платформы. Тем не менее, чем больше я использую их платформу; тем более я очарован полезностью их API и точностью их картографических данных.
Мы собираемся опубликовать серию учебных пособий, которые демонстрируют, как использовать эту платформу для передачи информации в мир, к которому она относится. Каждый урок тематический в соответствии с популярным телешоу. Как вы уже догадались, этот первый около 24 .
В этом уроке мы узнаем, как начать работу с платформой WRLD. Мы будем следовать примерам документации, чтобы сделать простейшую карту. Затем мы создадим локальную среду для компиляции нашего кода; и начать рассказывать историю с этим.
Мы рассмотрим эти темы:
- Рендеринг карт по названию места
- Перемещение по карте, для последовательности событий
- Подсветка зданий и разработка мероприятий в каждом здании
- Воспроизведение звуковых файлов с помощью HTML5 Audio API
- Изменение погодных условий и времени суток на карте
Код для этого урока можно найти на Github . Он был протестирован с современными версиями Firefox, Node и macOS.
Начиная
Самый простой способ начать работу — следовать первому примеру в документации. Прежде чем мы сможем это сделать, нам нужен аккаунт. Зайдите на https://www.wrld3d.com и нажмите «Зарегистрироваться».
После входа нажмите «Разработчики» и «Ключи API доступа».
Создайте новый ключ API для вашего приложения. Вы можете назвать это как угодно, но вам нужно будет скопировать сгенерированный ключ позже …
Мы можем получить код, для первого примера, с официального сайта документации . Я поместил его в CodePen и заменил координаты координатами для Нью-Йорка:
WRLD.js основан на Leaflet.js , что делает его знакомым для всех, кто раньше работал с картами. Это также означает, что карты являются мобильными и интерактивными.
Нажмите и перетащите левой кнопкой мыши, чтобы перемещаться по карте. Нажмите и перетащите правой кнопкой мыши, чтобы повернуть карту. Нажмите и перетащите с помощью средней кнопки мыши, чтобы изменить угол перспективы. Прокрутка колесика мыши повлияет на масштабирование. Картой также можно управлять на сенсорных устройствах.
Помимо включения Javascript SDK и таблицы стилей; нам потребовалось всего около 5 строк отформатированного кода, чтобы сделать красивую карту Нью-Йорка! Первый параметр map
— это идентификатор элемента, в который WRLD должен отобразить карту. Второй — это ключ API, который мы сгенерировали. Третий — это объект конфигурации. Этот объект содержит координаты центра карты и необязательный уровень масштабирования.
Настройка цепочки сборки
CodePen отлично подходит для быстрой демонстрации; но нам нужно что-то более надежное и презентабельное. Давайте настроим что-то простое, которое скомпилирует весь наш современный Javascript в версию, понятную большинству браузеров.
ParcelJS был недавно анонсирован; как быстрый веб-пакет с нулевой конфигурацией. Давайте проверим это. Во-первых, нам нужно установить Parcel как глобальное приложение через NPM:
npm install -g parcel-bundler
Далее мы можем создать несколько файлов для нашего проекта. Нам понадобится файл Javascript, файл CSS и файл точки входа HTML:
const Wrld = require("wrld.js") const map = Wrld.map("map", "[your API key here]", { center: [40.73061, -73.935242], zoom: 16, })
Это из
tutorial/app.js
@import "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.1/leaflet.css"; html, body { margin: 0; padding: 0; width: 100%; height: 100%; } #map { width: 100%; height: 100%; background-color: #000000; }
Это из
tutorial/app.css
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="stylesheet" href="./app.css" /> <title>Getting started with WRLD</title> </head> <body> <div id="map"></div> <script src="./app.js"></script> </body> </html>
Это из
tutorial/index.html
Обратите внимание, что app.js
требует wrld.js
? Нам нужно установить WRLD Javascript SDK:
npm init -y npm install --save wrld.js
Затем мы можем начать собирать и запускать локальные файлы, используя Parcel:
parcel index.html
Это запускает локальный сервер разработки и связывает файлы JS и CSS. Процесс выглядит примерно так:
Откройте URL-адрес в браузере, и вы должны еще раз увидеть карту Нью-Йорка. Когда мы вносим изменения в файлы JS и CSS, они автоматически перекомпилируются и перезагружаются в браузере. Посылка, безусловно, оправдывает свои требования.
И это именно то, что нам нужно — цепочка сборки с минимальными усилиями, которая позволит нам сосредоточиться на том, чтобы что-то сделать с WRLD!
Посылка еще совсем новая. Вы можете столкнуться с трудностями при работе с настроенными рабочими процессами или требованиями к сборке; и в документации еще есть способы объяснить, что делать в таких ситуациях. Тем не менее, я думаю, что эта простая цепочка сборки удовлетворит наши потребности, и Parcel выполнил свое обещание здесь.
Преобразование имен в координаты
Иногда мы знаем точные координаты места, о котором думаем. Иногда мы знаем только название места. Давайте быстро уйдем в сторону и посмотрим, как работать, обнаруживая координаты места, когда мы знаем только его название.
Это один из немногих сервисов, еще не доступных на платформе WRLD. Итак, давайте воспользуемся API Google, чтобы решить это. Нам понадобится еще один ключ API, поэтому перейдите на страницу https://developers.google.com/maps/documentation/geocoding/get-api-key и нажмите «Получить ключ»:
Далее, мы можем использовать сервис геокодирования Google, чтобы найти координаты для адреса, немного изменив наш Javascript:
const Wrld = require("wrld.js") const keys = { wrld: "[your WRLD API key]", google: "[your Google API key]", } window.addEventListener("load", async () => { const address = encodeURIComponent("empire state building, new york") const endpoint = "https://maps.googleapis.com/maps/api/geocode/json?" + "key=" + keys.google + "&address=" + address // console.log(endpoint) const response = await fetch(endpoint) const lookup = await response.json() // console.log(lookup) const { lat, lng } = lookup.results[0].geometry.location const map = Wrld.map("map", keys.wrld, { center: [lat, lng], zoom: 12, }) })
Это из
tutorial/app.js
Я переработал ключи в объект. Мы могли бы даже переместить их в файл переменных среды и исключить этот файл из Git. Таким образом, ключи могут быть полезны, но скрыты от общественности. Я также переместил свой код в асинхронную функцию коротких стрелок, чтобы я мог использовать async
и await
; и так, чтобы это произошло после загрузки документа.
Далее мы можем определить адрес для поиска. Лучше всего кодировать адрес, чтобы его можно было использовать в качестве параметра строки запроса. Мы можем передать это в конечную точку API геокодирования вместе с ключом API Google, чтобы получить результат.
Идите дальше и раскомментируйте операторы журнала консоли, чтобы увидеть, как выглядит закодированный URI и как выглядит результат, который Google возвращает нам. Мы получаем довольно подробный результат от Google, но results[0].geometry.location
нам фрагменты находятся внутри results[0].geometry.location
. Используя деструктуризацию объекта, мы можем извлечь только ключи lat
и lng
этого объекта.
Наконец, мы можем передать их в функцию map
, и карта отобразит Эмпайр Стейт Билдинг. Как я уже сказал, мы часто будем знать координаты центра карты. Но когда мы этого не сделаем: этот сервис и код помогут нам их выяснить.
Перемещение по карте
Давайте начнем работать над нашим погружением в карты. Мы хотим провести кого-то через серию событий и переместить карту к каждому новому событию, чтобы мы могли рассказать им историю. Хороший способ отделить содержание истории от механики истории — создать отдельный импорт Javascript «данных»:
module.exports = [ { // start at Empire State Building lat: 40.7484405, lng: -73.98566439999999, seconds: 15, image: ".jack-1", text: "What a lovely day...<phone rings>", }, { // stay in the same place but update story lat: 40.7484405, lng: -73.98566439999999, seconds: 15, image: ".chloe-1", text: "Jack, we have a problem...", }, // ...more events ]
Это из
tutorial/story.js
Мы можем разделить историю на события на карте. Каждый из них даже имеет lng
и lng
, хотя некоторые события могут происходить в предыдущем месте. Для каждого события мы покажем фотографию говорящего, а также то, что он говорит. Через несколько секунд мы переместим камеру на новое место и / или динамик.
Мы можем импортировать этот файл в наш основной файл Javascript и изменить карту, чтобы показать первое событие истории. Мы можем даже выделить здание, в котором проходит мероприятие:
const story = require("./story") window.addEventListener("load", async () => { // ...old code commented out here const { lat, lng } = story[0] const map = Wrld.map("map", keys.wrld, { center: [lat, lng], zoom: 15, }) map.on("initialstreamingcomplete", () => { Wrld.buildings .buildingHighlight( Wrld.buildings .buildingHighlightOptions() .highlightBuildingAtLocation([lat, lng]) .color([125, 255, 125, 128]), ) .addTo(map) }) })
Это из
tutorial/app.js
Этот код демонстрирует, как выделить здание после завершения начального рендеринга / потоковой передачи карты. Wrld.buildings.buildingHighlightOptions
создает объект параметров шаблона, к которому мы добавляем расположение и цвет подсветки. Мы передаем этот объект параметров в Wrld.buildings.buildingHighlight
чтобы создать выделение и добавить его на карту. Цветовой массив представляет собой значение RGBA, что означает, что четвертое целое число является значением непрозрачности ( 128
составляет примерно половину предела 255
или 50% прозрачности).
Это не единственный способ выделить здания. Мы также могли бы использовать лучевую отборку, чтобы выбрать здание, но это гораздо сложнее, чем нам нужно. Вы можете найти документацию для этого по адресу https://wrld3d.com/wrld.js/latest/docs/api/L.Wrld.buildings.BuildingHighlightOptions
На самом деле, пока мы на это; мы можем абстрагировать это здание в многократно используемую функцию. Мы можем даже добавить определенные цвета подсветки для каждого события и удалять предыдущие выделения здания каждый раз, когда добавляем новый:
const { lat, lng, color } = story[0] const map = Wrld.map("map", keys.wrld, { center: [lat, lng], zoom: 15, }) map.on("initialstreamingcomplete", () => { highlightBuildingAt(lat, lng, color) }) let highlight = null const highlightBuildingAt = (lat, lng, color) => { if (highlight) { highlight.remove() } highlight = Wrld.buildings .buildingHighlight( Wrld.buildings .buildingHighlightOptions() .highlightBuildingAtLocation([lat, lng]) .color(color), ) .addTo(map) }
Это из
tutorial/app.js
Таким образом, Джек и Хлоя могут иметь свои собственные яркие цвета, чтобы показать, когда они говорят. Удаление бликов здания еще проще, чем их добавление. Нам просто нужно сохранить ссылку на созданное нами выделение и вызвать метод remove
.
Перемещение карты
Хорошо, теперь нам нужно переместить карту к каждому новому событию. Мы будем выделять здание для каждого события, поэтому мы знаем, какое из них мы рассматриваем:
const { lat, lng, zoom, color, seconds } = story[0] const map = Wrld.map("map", keys.wrld, { center: [lat, lng], zoom, }) map.on("initialstreamingcomplete", () => { highlightBuildingAt(lat, lng, color) if (story.length > 1) { setTimeout(() => showNextEvent(1), seconds * 1000) } }) let highlight = null const highlightBuildingAt = (lat, lng, color) => { if (highlight) { highlight.remove() } highlight = Wrld.buildings .buildingHighlight( Wrld.buildings .buildingHighlightOptions() .highlightBuildingAtLocation([lat, lng]) .color(color), ) .addTo(map) } const showNextEvent = index => { const { lat, lng, zoom, degrees, color, seconds } = story[index] map.setView([lat, lng], zoom, { headingDegrees: degrees, animate: true, durationSeconds: 2.5, }) setTimeout(() => { highlightBuildingAt(lat, lng, color) if (story.length > index + 1) { setTimeout(() => showNextEvent(index + 1), seconds * 1000) } }, 2.5 * 1000) }
Это из
tutorial/app.js
Здесь много чего происходит, поэтому давайте разберемся с этим:
- Мы добавили свойство
zoom
к каждому событию. Это означает, что мы можем анимировать уровень масштабирования между событиями, что добавляет истории динамичности. Мы также добавили свойствоdegrees
ко всем, кроме первого события. Мы могли бы изменить направление камеры первого события, но я в порядке с тем, как оно выглядит по умолчанию (360 градусов). Добавление градусов к событиям позволяет нам анимировать заголовок так же, как масштабирование. - Если есть несколько событий (это можно предположить, но я все равно добавил проверку), то мы используем свойство
seconds
первого события, чтобы задержать переход к событию # 2. Мы создаем функциюshowNextEvent
с жестко закодированным значением индекса1
. - В
showNextEvent
мы используем методsetView
для анимации положения, масштабирования и направления камеры. Анимация займет2.5
секунды, поэтому мы установили тайм-аут на это время. В функции обратного вызова тайм-аута мы выделяем новое здание (так что выделение происходит только после того, как камера закончила движение) и ставим в очередь следующее событие.
Не стесняйтесь добавлять больше событий и / или полностью изменить историю. Сделайте это самостоятельно и получайте удовольствие!
Добавление аудио
Наша история немного тихая. Нам нужна некоторая напряженная фоновая музыка, чтобы поместить нас в зону. Зайдите на сайт вроде Epidemic Sound и найдите несколько интересных музыкальных треков для вашей истории. Я скачал несколько и поместил их в папку tutorial/tracks
.
Теперь давайте создадим невидимый аудиоплеер, и он будет воспроизводить треки в произвольном порядке. Чтобы это работало, нам нужен список треков:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="stylesheet" href="./app.css" /> <title>Getting started with WRLD</title> </head> <body> <div id="map"></div> <audio class="track-1" src="./tracks/track-1.mp3" /> <audio class="track-2" src="./tracks/track-2.mp3" /> <audio class="track-3" src="./tracks/track-3.mp3" /> <audio class="track-4" src="./tracks/track-4.mp3" /> <audio class="track-5" src="./tracks/track-5.mp3" /> <audio class="track-6" src="./tracks/track-6.mp3" /> <audio class="track-7" src="./tracks/track-7.mp3" /> <audio class="track-8" src="./tracks/track-8.mp3" /> <audio class="track-9" src="./tracks/track-9.mp3" /> <audio class="track-10" src="./tracks/track-10.mp3" /> <script src="./app.js"></script> </body> </html>
Это из
tutorial/index.html
Parcel просматривает index.html
и переписывает все статические ссылки на файлы, которые он копирует в папку dist
. Если мы создадим audio
теги HTML4 в этом HTML-файле, Parcel скопирует эти файлы в папку списка и предоставит их через сервер разработки. Нам не нужно так поступать, но это проще для тестирования по мере разработки.
Одной из альтернатив будет ссылка на эти файлы из Интернета. Другим было бы не использовать сервер разработки.
module.exports = [ ".track-1", ".track-2", ".track-3", ".track-4", ".track-5", ".track-6", ".track-7", ".track-8", ".track-9", ".track-10", ]
Это из
tutorial/tracks.js
Мы можем использовать этот список, чтобы найти элементы HTML, связанные с каждым *.mp3
файлом, который мы хотим воспроизвести. Мы собираемся использовать этот список в нашем основном файле JS:
const nextTrack = () => { const index = Math.floor(Math.random() * tracks.length) const audio = new Audio(document.querySelector(tracks[index]).src) audio.addEventListener("ended", () => nextTrack()) audio.play() } nextTrack()
Это из
tutorial/app.js
Мы хотим воспроизвести трек в случайном порядке, поэтому мы находим случайный индекс. Затем мы выбираем audio
элемент, соответствующий этому индексу, и создаем новый Audio
объект со значением атрибута src
. Когда трек закончится, мы снова nextTrack
функцию nextTrack
(так, чтобы следующий случайный трек начал воспроизводиться в цикле) и запускаем случайно выбранный трек.
К сожалению, я не могу включить треки, которые я использую, в репозиторий Github. Во-первых, они значительно увеличат размер репо. Во-вторых, я имею право использовать их для производства YouTube, но не распространять их по любой другой причине. Если вы хотите получить треки, которые я использовал, вы можете найти их на странице результатов поиска Epidemic Sound .
Добавление информационных карт для событий
Я упоминал ранее; WRLD.js основан на LeafletJS. Это здорово, потому что мы можем делать все, что позволяет Leaflet, работая с картами WRLD. Фактически, мы можем использовать всплывающие окна Leaflet, чтобы рассказывать о событиях в истории. Всплывающее окно Leaflet выглядит так:
L.popup() .setLatLng(latlng) .setContent("I am a popup!") .openOn(map)
Мы собираемся встроить изображение и текст каждого события во всплывающее окно. Также было бы здорово, если бы мы могли расположить всплывающее окно относительно высоты здания. Не прямо наверху, но … скажем … на полпути вверх по зданию. Мы могли бы использовать что-то вроде этого:
let popup = null const showPopup = (lat, lng, image, text, elevation) => { const src = document.querySelector(image).src const element1 = "<img class='image' src='" + src + "' />" const element2 = "<span class='text'>" + text + "</span>" const element3 = "<div class='popup'>" + element1 + element2 + "</div>" popup = L.popup({ closeButton: false, autoPanPaddingTopLeft: 100, elevation: Math.max(20, elevation / 2), }) .setLatLng(L.latLng(lat, lng)) .setContent(element3) .openOn(map) }
Это из
tutorial/app.js
L.popup
принимает объект параметров. Параметры, которые мы устанавливаем:
- Мы хотим скрыть кнопку закрытия, которая обычно отображается во всплывающих окнах Leaflet.
- Мы хотим, чтобы камера оставляла достаточно места между верхним / левым краем экрана, когда камера заканчивает панорамирование, чтобы показать всплывающее окно.
- Мы хотим, чтобы всплывающее окно было не менее 20 метров от первого этажа и не более половины другого фасада здания.
Мы также создаем строку HTML; который помещает изображение и текст .popup
элемент .popup
. Мы можем использовать следующие стили для этих элементов:
.hidden { display: none; } .image { display: flex; width: auto; height: 100px; } .text { display: flex; padding-left: 10px; font-size: 16px; } .popup { display: flex; flex-direction: row; align-items: flex-start; }
Это из
tutorial/app.css
.popup
является контейнерным элементом .popup
. Применяемые к нему гибкие стили заключаются в том, что дочерние элементы должны отображаться в строке и должны быть выровнены по верху контейнера. Есть много замечательных гидов Flexbox. Взгляните на Flexbox Zombies для интересного способа узнать …
Обратите внимание, что мы также определяем стиль
.hidden
для изображений вindex.html
. Мы не хотим, чтобы они отображались — они есть, чтобы Parcel правильно копировал и ссылался на них.
Вопрос в том, как мы можем получить высоту каждого здания? Мы можем прислушиваться к информационным событиям в зданиях и определять высоту оттуда. К сожалению, для каждого из них нет способа сделать это, поэтому нам нужно подключиться к «глобальным» событиям и незаметно добавить / удалить слушателей:
let elevation = 0 const waitForElevation = onElevation => { const listener = event => { map.buildings.off("buildinginformationreceived", listener) const information = event.buildingHighlight.getBuildingInformation() if (!information) { onElevation(0) } else { const dimensions = information.getBuildingDimensions() const ground = dimensions.getBaseAltitude() const elevation = dimensions.getTopAltitude() - ground onElevation(elevation) } } map.buildings.on("buildinginformationreceived", listener) }
Это из
tutorial/app.js
waitForElevation
создает и добавляет функцию слушателя к событию buildinginformationreceived
полученной информации карты. В тот момент, когда слушатель срабатывает, он удаляется. Таким образом, мы можем инициировать событие для каждого выделения: добавить слушателя → здание выделено → слушатель вызван → слушатель удален.
buildinginformationreceived
получает событие, которое имеет метод getBuildingInformation
. Если в здании есть какая-либо информация, мы получаем высоту земли и определяем ее высоту. Если нет, мы onElevation
параметр функции onElevation
. Итак, onElevation
с целым числом 0
или больше.
onElevation
добавить onElevation
вызов onElevation
к каждому вызову highlightBuildingAt
; и вызовите waitForElevation
внутри этой функции:
map.on("initialstreamingcomplete", () => { highlightBuildingAt( lat, lng, color, elevation => showPopup(lat, lng, image, text, elevation) ) if (story.length > 1) { setTimeout(() => showNextEvent(1), seconds * 1000) } }) let highlight = null const highlightBuildingAt = (lat, lng, color, onElevation) => { waitForElevation(onElevation) // ...rest of highlightBuildingAt } const showNextEvent = index => { // ...rest of showNextEvent setTimeout(() => { highlightBuildingAt( lat, lng, color, elevation => showPopup(lat, lng, image, text, elevation) ) if (story.length > index + 1) { setTimeout(() => showNextEvent(index + 1), seconds * 1000) } }, 2.5 * 1000) }
Это из
tutorial/app.js
Изменение погоды и времени суток
История Джека разыгрывается зимой; но карта солнечная и яркая. Давайте изменим погоду, чтобы она больше соответствовала сезону:
map.themes.setWeather(Wrld.themes.weather.Snowy)
Это из
tutorial/app.js
Смехотворно легко изменить погоду. Здесь мы делаем это снежным; но мы можем сделать это одним из следующих:
-
Wrld.themes.weather.Clear
-
Wrld.themes.weather.Overcast
-
Wrld.themes.weather.Foggy
-
Wrld.themes.weather.Rainy
-
Wrld.themes.weather.Snowy
Точно так же мы хотим сделать ход времени более реалистичным. Каждый 24 эпизод должен происходить в течение 1 часа. Было бы замечательно, если бы мы могли сделать каждое место с интервалом в 1 час, но у нас есть только эти времена для работы:
-
Wrld.themes.time.Dawn
-
Wrld.themes.time.Day
-
Wrld.themes.time.Dusk
-
Wrld.themes.time.Night
Давайте изменим время суток в зависимости от каждого события:
const { lat, lng, zoom, color, seconds, image, text, time } = story[0] const map = Wrld.map("map", keys.wrld, { center: [lat, lng], zoom, }) if (time) { map.themes.setTime(time) } // ...later const showNextEvent = index => { const { lat, lng, zoom, degrees, color, seconds, image, text, time } = story[index] map.setView(...) setTimeout(() => { if (time) { map.themes.setTime(time) } highlightBuildingAt(...) if (story.length > index + 1) { setTimeout(...) } }, 2.5 * 1000) }
Это из
tutorial/app.js
Резюме
Мы сделали на сегодня. Я надеюсь, что вы получили столько же удовольствия, сколько и я, и все это вместе. Потратьте некоторое время, чтобы украсить свою историю; добавление новых персонажей, новой музыки и всего, что вы думаете, сделает вашу историю великолепной. Мы хотели бы увидеть, что вы придумали.
Вот видео о конечном продукте. Я буду добавлять больше событий к этому, но я очень горжусь тем, что нам удалось:
В следующий раз мы узнаем больше о презентации, анимации и автоматизации, которые позволяет платформа WRLD. Фактически, мы собираемся использовать WRLD для создания полезного, доступного для продажи мобильного приложения. Увидимся в следующий раз!