Перенос кубиков LEGO® в многофункциональный Интернет
Build with Chrome, забавный эксперимент для пользователей Chrome на настольных компьютерах, изначально запущенный в Австралии , был переиздан в 2014 году с глобальной доступностью, связями с THE LEGO® MOVIE™ и недавно добавленной поддержкой мобильных устройств. В этой статье мы поделимся некоторыми выводами из проекта, особенно в отношении перехода от опыта только на настольном компьютере к многоэкранному решению, которое поддерживает как мышь, так и сенсорный ввод.
История сборки с Chrome
Первая версия Build with Chrome была запущена в Австралии в 2012 году. Мы хотели продемонстрировать возможности Интернета совершенно по-новому и представить Chrome совершенно новой аудитории.
Сайт состоял из двух основных частей: режима «Строительство», в котором пользователи могли создавать творения из кубиков LEGO, и режима «Исследование», в котором можно было просматривать творения на LEGO-фицированной версии Google Карт.
Интерактивное 3D было необходимо для предоставления пользователям наилучшего опыта сборки LEGO. В 2012 году WebGL был доступен только в браузерах для настольных компьютеров, поэтому Build был нацелен на опыт только для настольных компьютеров. Explore использовал Google Maps для отображения творений, но при достаточном увеличении он переключался на реализацию WebGL карты, показывая творения в 3D, по-прежнему используя карты Google в качестве текстуры базовой пластины. Мы надеялись создать среду, в которой энтузиасты LEGO всех возрастов могли бы легко и интуитивно выражать свое творчество и исследовать творения друг друга.
В 2013 году мы решили расширить Build with Chrome на новые веб-технологии. Среди этих технологий был WebGL в Chrome для Android, который, естественно, позволил бы Build with Chrome развиться в мобильный опыт. Для начала мы сначала разработали сенсорные прототипы, прежде чем подвергнуть сомнению аппаратное обеспечение для «Builder Tool», чтобы понять поведение жестов и тактильную отзывчивость, с которыми мы можем столкнуться через браузер по сравнению с мобильным приложением.
Отзывчивый интерфейс
Нам нужно было поддерживать устройства с сенсорным и мышиным вводом. Однако использование того же пользовательского интерфейса на небольших сенсорных экранах оказалось неоптимальным решением из-за ограничений по пространству.
В Build происходит много интерактивности: увеличение и уменьшение масштаба, изменение цвета кирпичей и, конечно, выбор, вращение и размещение кирпичей. Это инструмент, на котором пользователи часто проводят много времени, поэтому важно, чтобы у них был быстрый доступ ко всему, что они часто используют, и им должно быть комфортно взаимодействовать с ним.
При проектировании высокоинтерактивного сенсорного приложения вы обнаружите, что экран быстро становится маленьким, а пальцы пользователя, как правило, покрывают большую часть экрана во время взаимодействия. Это стало очевидным для нас при работе с Builder. При проектировании вам действительно нужно учитывать физический размер экрана, а не пиксели в графике. Становится важным минимизировать количество кнопок и элементов управления, чтобы получить как можно больше экранного пространства, выделенного для фактического контента.
Наша цель состояла в том, чтобы сделать Build естественным на сенсорных устройствах, не просто добавляя сенсорный ввод к оригинальной реализации настольного компьютера, а делая его таким, как будто он действительно предназначен для сенсорного ввода. В итоге мы получили два варианта пользовательского интерфейса: один для настольных компьютеров и планшетов с большими экранами и один для мобильных устройств с меньшими экранами. Когда это возможно, лучше всего использовать одну реализацию и плавный переход между режимами. В нашем случае мы определили, что между этими двумя режимами существует такая существенная разница в опыте, что решили положиться на определенную точку останова. У этих двух версий много общих функций, и мы попытались сделать большинство вещей с помощью одной реализации кода, но некоторые аспекты пользовательского интерфейса работают по-разному в этих двух режимах.
Мы используем данные user-agent для обнаружения мобильных устройств, а затем проверяем размер области просмотра, чтобы решить, следует ли использовать мобильный пользовательский интерфейс для маленького экрана. Немного сложно выбрать контрольную точку для того, каким должен быть «большой экран», потому что сложно получить надежное значение физического размера экрана. К счастью, в нашем случае не имеет значения, отображаем ли мы пользовательский интерфейс для маленького экрана на сенсорном устройстве с большим экраном, потому что инструмент все равно будет работать нормально, просто некоторые кнопки могут показаться немного слишком большими. В конце концов, мы устанавливаем контрольную точку на 1000 пикселей; если вы загружаете сайт из окна шире 1000 пикселей (в альбомной ориентации), вы получите версию для большого экрана.
Давайте немного поговорим о двух размерах экрана и их особенностях:
Большой экран с поддержкой мыши и сенсорного ввода
Версия для большого экрана предоставляется всем настольным компьютерам с поддержкой мыши и сенсорным устройствам с большими экранами (например, Google Nexus 10). Эта версия близка к оригинальному решению для настольных компьютеров по типу доступных элементов управления навигацией, но мы добавили поддержку сенсорного экрана и некоторые жесты. Мы настраиваем пользовательский интерфейс в зависимости от размера окна, поэтому, когда пользователь изменяет размер окна, он может удалить или изменить размер части пользовательского интерфейса. Мы делаем это с помощью медиазапросов CSS.
Пример: если доступная высота меньше 730 пикселей, элемент управления ползунком масштабирования в режиме «Исследование» скрыт:
@media only screen and (max-height: 730px) {
.zoom-slider {
display: none;
}
}
Маленький экран, только сенсорная поддержка
Эта версия предназначена для мобильных устройств и небольших планшетов (целевые устройства Nexus 4 и Nexus 7). Для этой версии требуется поддержка multi-touch.
На устройствах с небольшим экраном нам необходимо предоставить контенту как можно больше места на экране, поэтому мы внесли несколько изменений, чтобы максимально увеличить пространство, в основном путем перемещения редко используемых элементов за пределы поля зрения:
- Во время строительства окно выбора кирпичиков сворачивается в окно выбора цвета.
- Мы заменили элементы управления масштабированием и ориентацией на мультисенсорные жесты.
- Функциональность полноэкранного режима Chrome также полезна для получения дополнительного пространства на экране.


Производительность и поддержка WebGL
Современные сенсорные устройства оснащены довольно мощными графическими процессорами, но они все еще далеки от своих настольных аналогов, поэтому мы знали, что у нас возникнут некоторые проблемы с производительностью, особенно в режиме Explore 3D, где нам нужно одновременно визуализировать множество творений.
Творчески мы хотели добавить несколько новых типов кирпичей со сложными формами и даже прозрачностью — функции, которые обычно очень сильно нагружают графический процессор. Однако нам нужно было обеспечить обратную совместимость и продолжить поддержку творений из первой версии, поэтому мы не могли установить никаких новых ограничений, таких как значительное сокращение общего количества кирпичей в творениях.
В первой версии Build у нас был максимальный лимит кирпичей, которые можно было использовать в одном творении. Был «метр кирпичей», показывающий, сколько кирпичей осталось. В новой реализации некоторые из новых кирпичей влияли на счетчик кирпичей больше, чем стандартные кирпичи, таким образом, немного уменьшая общее максимальное количество кирпичей. Это был один из способов включить новые кирпичи, сохраняя при этом приличную производительность.
В режиме Explore 3D одновременно происходит довольно много всего: загрузка текстур базовой пластины, загрузка созданий, анимация и рендеринг созданий и т. д. Это требует много ресурсов как от GPU, так и от CPU, поэтому мы провели много профилирования кадров в Chrome DevTools, чтобы максимально оптимизировать эти части. На мобильных устройствах мы решили немного приблизить творения, чтобы нам не приходилось рендерить так много созданий одновременно.
Некоторые устройства заставляли нас пересматривать и упрощать некоторые шейдеры WebGL, но мы всегда находили способ решить эту проблему и двигаться вперед.
Поддержка не-WebGL устройств
Мы хотели, чтобы сайт был в какой-то степени пригоден для использования, даже если устройство посетителя не поддерживает WebGL. Иногда есть способы представить 3D упрощенным способом с помощью решения Canvas или функций CSS3D. К сожалению, мы не нашли достаточно хорошего решения для воспроизведения функций Build и Explore 3D без использования WebGL.
Для единообразия визуальный стиль творений должен быть одинаковым на всех платформах. Мы могли бы потенциально попробовать решение 2.5D , но это заставило бы творения выглядеть по-разному в некоторых отношениях. Нам также пришлось подумать, как сделать так, чтобы творения, созданные с помощью первой версии Build with Chrome, выглядели одинаково и работали так же гладко в новой версии сайта, как и в первой.
Режим Explore 2D по-прежнему доступен для устройств без поддержки WebGL, хотя вы не можете создавать новые творения или исследовать в 3D. Таким образом, пользователи все равно могут получить представление о глубине проекта и о том, что они могли бы создать с помощью этого инструмента, если бы они были на устройстве с поддержкой WebGL. Сайт может быть не таким ценным для пользователей без поддержки WebGL, но он должен, по крайней мере, послужить тизером и вовлечь их в его тестирование.
Сохранение резервных версий для решений WebGL иногда просто невозможно. Существует множество возможных причин: производительность, визуальный стиль, затраты на разработку и обслуживание и т. д. Однако, если вы решили не внедрять резервную версию, вы должны по крайней мере позаботиться о посетителях, не поддерживающих WebGL, объяснить, почему они не могут получить полный доступ к сайту, и дать инструкции о том, как они могут решить проблему, используя браузер с поддержкой WebGL.
Управление активами
В 2013 году Google представила новую версию Google Maps с самыми значительными изменениями пользовательского интерфейса с момента запуска. Поэтому мы решили перепроектировать Build with Chrome, чтобы он соответствовал новому пользовательскому интерфейсу Google Maps, и при этом учли другие факторы при перепроектировании. Новый дизайн относительно плоский с чистыми сплошными цветами и простыми формами. Это позволило нам использовать чистый CSS во многих элементах пользовательского интерфейса, минимизировав использование изображений.
В Explore нам нужно загрузить много изображений; миниатюрные изображения для творений, текстуры карт для базовых пластин и, наконец, сами 3D-творения. Мы уделяем особое внимание тому, чтобы не было утечки памяти, когда мы постоянно загружаем новые изображения.
3D-творения хранятся в пользовательском формате файла, упакованном как изображение PNG. Хранение данных 3D-творений в виде изображения позволило нам в основном передавать данные напрямую шейдерам, которые визуализируют творения.
Для всех созданных пользователями изображений дизайн позволил нам использовать одинаковые размеры изображений на всех платформах, что минимизировало использование хранилища и полосы пропускания.
Управление ориентацией экрана
Легко забыть, насколько сильно меняется соотношение сторон экрана при переходе из портретного в ландшафтный режим и наоборот. Вам нужно учитывать это с самого начала при адаптации для мобильных устройств.
На традиционном веб-сайте с включенной прокруткой вы можете применить правила CSS, чтобы получить адаптивный сайт, который перестраивает контент и меню. Пока вы можете использовать функциональность прокрутки, это довольно управляемо.
Мы также использовали этот метод с Build, но мы были немного ограничены в том, как мы могли решить эту проблему, потому что нам нужно было, чтобы контент был виден все время, и при этом иметь быстрый доступ к ряду элементов управления и кнопок. Для сайтов с чистым контентом, таких как новостные сайты, резиновый макет имеет смысл, но для игрового приложения, такого как наше, это было проблемой. Стало проблемой найти макет, который работал бы как в альбомной, так и в портретной ориентации, сохраняя при этом хороший обзор контента и удобный способ взаимодействия. В конце концов мы решили оставить Build только в альбомной ориентации и просим пользователя повернуть свое устройство.
Explore было намного проще решить в обеих ориентациях. Нам просто нужно было настроить уровень масштабирования 3D в зависимости от ориентации, чтобы получить согласованный опыт.
Большая часть макета контента контролируется CSS, но некоторые вещи, связанные с ориентацией, необходимо было реализовать в JavaScript. Мы обнаружили, что не было хорошего решения для кросс-устройства, чтобы использовать window.orientation для определения ориентации, поэтому в итоге мы просто сравнивали window.innerWidth и window.innerHeight для определения ориентации устройства.
if( window.innerWidth > window.innerHeight ){
//landscape
} else {
//portrait
}
Добавление поддержки сенсорного управления
Добавление поддержки сенсорного ввода в веб-контент достаточно просто. Базовая интерактивность, такая как событие щелчка, работает одинаково на настольных компьютерах и устройствах с сенсорным вводом, но когда дело доходит до более сложных взаимодействий, вам также необходимо обрабатывать события сенсорного ввода: touchstart, touchmove и touchend. В этой статье рассматриваются основы использования этих событий. Internet Explorer не поддерживает события сенсорного ввода, но вместо этого использует события указателя (pointerdown, pointermove, pointerup). События указателя были отправлены в W3C для стандартизации, но на данный момент реализованы только в Internet Explorer.
В режиме Explore 3D мы хотели такую же навигацию, как и в стандартной реализации Google Maps; использование одного пальца для перемещения по карте и сведение двух пальцев для масштабирования. Поскольку творения находятся в 3D, мы также добавили жест вращения двумя пальцами. Обычно это требует использования сенсорных событий.
Хорошей практикой является избегание тяжелых вычислений, таких как обновление или рендеринг 3D в обработчиках событий. Вместо этого сохраняйте сенсорный ввод в переменной и реагируйте на ввод в цикле рендеринга requestAnimationFrame. Это также упрощает реализацию мыши в то же время, вы просто сохраняете соответствующие значения мыши в тех же переменных.
Начните с инициализации объекта для хранения ввода и добавьте прослушиватель событий touchstart. В каждом обработчике событий мы вызываем event.preventDefault(). Это делается для того, чтобы браузер не продолжал обрабатывать событие касания, что может вызвать неожиданное поведение, например прокрутку или масштабирование всей страницы.
var input = {dragStartX:0, dragStartY:0, dragX:0, dragY:0, dragDX:0, dragDY:0, dragging:false};
plateContainer.addEventListener('touchstart', onTouchStart);
function onTouchStart(event) {
event.preventDefault();
if( event.touches.length === 1){
handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
//start listening to all needed touchevents to implement the dragging
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', onTouchEnd);
document.addEventListener('touchcancel', onTouchEnd);
}
}
function onTouchMove(event) {
event.preventDefault();
if( event.touches.length === 1){
handleDragging(event.touches[0].clientX, event.touches[0].clientY);
}
}
function onTouchEnd(event) {
event.preventDefault();
if( event.touches.length === 0){
handleDragStop();
//remove all eventlisteners but touchstart to minimize number of eventlisteners
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
//also listen to touchcancel event to avoid unexpected behavior when switching tabs and some other situations
document.removeEventListener('touchcancel', onTouchEnd);
}
}
Мы не занимаемся фактическим сохранением ввода в обработчиках событий, а вместо этого в отдельных обработчиках: handleDragStart, handleDragging и handleDragStop. Это потому, что мы хотим иметь возможность вызывать их также из обработчиков событий мыши. Имейте в виду, что, хотя это маловероятно, пользователь может использовать касание и мышь одновременно. Вместо того, чтобы обрабатывать этот случай напрямую, мы просто убеждаемся, что ничего не взорвется.
function handleDragStart(x ,y ){
input.dragging = true;
input.dragStartX = input.dragX = x;
input.dragStartY = input.dragY = y;
}
function handleDragging(x ,y ){
if(input.dragging) {
input.dragDX = x - input.dragX;
input.dragDY = y - input.dragY;
input.dragX = x;
input.dragY = y;
}
}
function handleDragStop(){
if(input.dragging) {
input.dragging = false;
input.dragDX = 0;
input.dragDY = 0;
}
}
При создании анимаций на основе touchmove часто бывает полезно также сохранять дельта-движение с момента последнего события. Например, мы использовали это как параметр скорости камеры при перемещении по всем базовым пластинам в Explore, поскольку вы не перетаскиваете базовые пластины, а фактически перемещаете камеру.
function onAnimationFrame() {
requestAnimationFrame( onAnimationFrame );
//execute animation based on input.dragDX, input.dragDY, input.dragX or input.dragY
/*
/
*/
//because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
input.dragDX=0;
input.dragDY=0;
}
Встроенный пример: Перетаскивание объекта с помощью сенсорных событий. Похожая реализация, как при перетаскивании карты Explore 3D в Build with Chrome: http://cdpn.io/qDxvo
Мультисенсорные жесты
Существует несколько фреймворков и библиотек, таких как Hammer или QuoJS , которые могут упростить управление мультисенсорными жестами, но если вы хотите объединить несколько жестов и получить полный контроль, иногда лучше сделать это с нуля.
Для управления жестами сжатия и поворота мы сохраняем расстояние и угол между двумя пальцами, когда второй палец находится на экране:
//variables representing the actual scale/rotation of the object we are affecting
var currentScale = 1;
var currentRotation = 0;
function onTouchStart(event) {
event.preventDefault();
if( event.touches.length === 1){
handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
}else if( event.touches.length === 2 ){
handleGestureStart(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
}
}
function handleGestureStart(x1, y1, x2, y2){
input.isGesture = true;
//calculate distance and angle between fingers
var dx = x2 - x1;
var dy = y2 - y1;
input.touchStartDistance=Math.sqrt(dx*dx+dy*dy);
input.touchStartAngle=Math.atan2(dy,dx);
//we also store the current scale and rotation of the actual object we are affecting. This is needed to support incremental rotation/scaling. We can't assume that an object is always the same scale when gesture starts.
input.startScale=currentScale;
input.startAngle=currentRotation;
}
В событии touchmove мы затем непрерывно измеряем расстояние и угол между этими двумя пальцами. Разница между начальным расстоянием и текущим расстоянием затем используется для установки масштаба, а разница между начальным углом и текущим углом используется для установки угла.
function onTouchMove(event) {
event.preventDefault();
if( event.touches.length === 1){
handleDragging(event.touches[0].clientX, event.touches[0].clientY);
}else if( event.touches.length === 2 ){
handleGesture(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
}
}
function handleGesture(x1, y1, x2, y2){
if(input.isGesture){
//calculate distance and angle between fingers
var dx = x2 - x1;
var dy = y2 - y1;
var touchDistance = Math.sqrt(dx*dx+dy*dy);
var touchAngle = Math.atan2(dy,dx);
//calculate the difference between current touch values and the start values
var scalePixelChange = touchDistance - input.touchStartDistance;
var angleChange = touchAngle - input.touchStartAngle;
//calculate how much this should affect the actual object
currentScale = input.startScale + scalePixelChange*0.01;
currentRotation = input.startAngle+(angleChange*180/Math.PI);
//upper and lower limit of scaling
if(currentScale<0.5) currentScale = 0.5;
if(currentScale>3) currentScale = 3;
}
}
Потенциально вы могли бы использовать изменение расстояния между каждым событием touchmove аналогично примеру с перетаскиванием, но такой подход часто более полезен, когда вам требуется непрерывное движение.
function onAnimationFrame() {
requestAnimationFrame( onAnimationFrame );
//execute transform based on currentScale and currentRotation
/*
/
*/
//because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
input.dragDX=0;
input.dragDY=0;
}
Вы также можете включить перетаскивание объекта во время выполнения жестов сжатия и поворота, если хотите. В этом случае вы будете использовать центральную точку между двумя пальцами в качестве входных данных для обработчика перетаскивания.
Встроенный пример: Вращение и масштабирование объекта в 2D. Аналогично тому, как реализована карта в Explore: http://cdpn.io/izloq
Поддержка мыши и сенсорного экрана на одном оборудовании
Сегодня есть несколько ноутбуков, например Chromebook Pixel, которые поддерживают как мышь, так и сенсорный ввод. Это может привести к неожиданному поведению, если вы не будете осторожны.
Важно то, что вы не должны просто определять поддержку сенсорного ввода и игнорировать ввод с помощью мыши, а вместо этого поддерживать оба способа одновременно.
Если вы не используете event.preventDefault()
в обработчиках событий касания, также будут срабатывать некоторые эмулированные события мыши, чтобы большинство оптимизированных для несенсорного управления сайтов продолжали работать. Например, для одного нажатия на экран эти события могут срабатывать в быстрой последовательности и в следующем порядке:
- сенсорный старт
- touchmove
- тачэнд
- наведение мыши
- перемещение мыши
- mousedown
- мышь вверх
- нажмите
Если у вас немного более сложные взаимодействия, эти события мыши могут вызвать неожиданное поведение и испортить вашу реализацию. Часто лучше использовать event.preventDefault()
в обработчиках событий касания и управлять вводом мыши в отдельных обработчиках событий. Вам нужно знать, что использование event.preventDefault()
в обработчиках событий касания также предотвратит некоторое поведение по умолчанию, такое как прокрутка и событие щелчка.
«В Build with Chrome мы не хотели, чтобы масштабирование происходило при двойном касании на сайте, хотя это стандартно для большинства браузеров. Поэтому мы используем метатег viewport, чтобы сообщить браузеру, что не следует масштабировать, когда пользователь делает двойное касание. Это также устраняет задержку нажатия в 300 мс, что улучшает отзывчивость сайта. (Задержка нажатия нужна для того, чтобы провести различие между одинарным и двойным касанием, когда включено масштабирование двойным касанием.)
<meta name="viewport" content="width=device-width,user-scalable=no">
Помните, что при использовании этой функции вы должны сделать сайт читабельным на экранах всех размеров, поскольку пользователь не сможет увеличить масштаб.
Ввод с помощью мыши, сенсорного экрана и клавиатуры
В режиме Explore 3D мы хотели, чтобы было три способа навигации по карте: мышь (перетаскивание), касание (перетаскивание, сведение пальцев для масштабирования и поворота) и клавиатура (перемещение с помощью клавиш со стрелками). Все эти методы навигации работают немного по-разному, но мы использовали один и тот же подход для всех них; устанавливая переменные в обработчиках событий и действуя на них в цикле requestAnimationFrame. Цикл requestAnimationFrame не должен знать, какой метод используется для навигации.
В качестве примера мы могли бы задать движение карты (dragDX и dragDY) со всеми тремя методами ввода. Вот реализация клавиатуры:
document.addEventListener('keydown', onKeyDown );
document.addEventListener('keyup', onKeyUp );
function onKeyDown( event ) {
input.keyCodes[ "k" + event.keyCode ] = true;
input.shiftKey = event.shiftKey;
}
function onKeyUp( event ) {
input.keyCodes[ "k" + event.keyCode ] = false;
input.shiftKey = event.shiftKey;
}
//this needs to be called every frame before animation is executed
function handleKeyInput(){
if(input.keyCodes.k37){
input.dragDX = -5; //37 arrow left
} else if(input.keyCodes.k39){
input.dragDX = 5; //39 arrow right
}
if(input.keyCodes.k38){
input.dragDY = -5; //38 arrow up
} else if(input.keyCodes.k40){
input.dragDY = 5; //40 arrow down
}
}
function onAnimationFrame() {
requestAnimationFrame( onAnimationFrame );
//because keydown events are not fired every frame we need to process the keyboard state first
handleKeyInput();
//implement animations based on what is stored in input
/*
/
*/
//because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
input.dragDX = 0;
input.dragDY = 0;
}
Встроенный пример: использование мыши, сенсорного экрана и клавиатуры для навигации: http://cdpn.io/catlf
Краткое содержание
Адаптация Build с Chrome для поддержки сенсорных устройств с различными размерами экрана стала обучающим опытом. У команды не было большого опыта в реализации такого уровня интерактивности на сенсорных устройствах, и мы многому научились по ходу дела.
Самой большой проблемой оказалось то, как решить пользовательский опыт и дизайн. Технические проблемы заключались в управлении множеством размеров экрана, событий касания и проблем производительности.
Несмотря на некоторые проблемы с шейдерами WebGL на сенсорных устройствах, это сработало почти лучше, чем ожидалось. Устройства становятся все более мощными, а реализации WebGL быстро улучшаются. Мы чувствуем, что будем использовать WebGL на устройствах гораздо больше в ближайшем будущем.
А теперь, если вы еще этого не сделали, идите и постройте что-нибудь потрясающее !