Инерция в JavaScript на примере OL3

    Современный мир диктует разработчикам и дизайнерам довольно высокие стандарты качества и удобства использования веб-приложений. Как правило, хорошее впечатление о приложении складывается из множества мелочей, которые должны гармонично сочетаться между собой. Одной из таких мелочей может являться инерционное движение “драгабельных” (перетаскиваемых) объектов на странице — об этом мы и поговорим в рамках сегодняшней небольшой статьи. Фича особенно актуальна при взаимодействии пользователя с приложением посредством тач-устройств, так как размер экранов таких устройств ограничен, и передвигать объект из одной точки в другую хочется одним “легким движением руки”, а не многократными касаниями экрана.

    Подобным вопросом мы когда-то задались в рамках разработки API карт 2GIS, а сегодня решили поделиться нашим скромным опытом.

    Прежде чем рассматривать детали реализации инерции, давайте взглянем на уже готовые примеры:

    Хорошо, теперь к делу.

    Пространство для экспериментов


    В качестве тестового примера добавим инерционное перемещение к одному из картографических API, у которого еще нет такой фичи. Таким образом мы: а) чему-то научимся; б) нанесем кому-то необратимую пользу.

    Давайте искать столь “обделенное” API. Bing, Google? Нет, у этих ребят с перетаскиванием карты все в порядке. Посмотрим, как обстоят дела у OpenLayers 3. Да, наш “пациент”, над ним и будем ставить эксперименты.

    Несмотря на молодость “подопытного”, установка трудностей не вызвала, все как в документации:



    Исходный код всех html, js и css файлов примеров можно найти в папке ol3/examples.

    Наброски алгоритма


    На самом деле, все довольно просто.

    Подготовка к инерционному движению:
    • отлавливаем момент начала перетаскивания карты и записываем текущее время;
    • отлавливаем момент окончания перетаскивания (пользователь отпустил кнопку мышки) и в этот момент:
      • меряем продолжительность перетаскивания (в мс.);
      • меряем дистанцию, которая была пройдена по оси Х и по оси Y (в пикс.);
      • считаем скорость перетаскивания карты по оси Х и по оси Y (в пикс. за мс.):
        • скорость = дистанция / продолжительность.
      • считаем начальный импульс по оси Х и по оси Y:
        • импульс = масса * скорость, где масса — это просто константа.
    • вызываем метод, который отвечает за инерционное движение карты по окончанию перетаскивания, назовем его inertiaMove.

    Метод inertiaMove вызывается раз в определенный период (каждых 16 мс., например). В этом методе мы:
    • сдвигаем центр карты на значение импульса по оси Х и по оси Y;
    • уменьшаем импульс, разделив его на коэффициент затухания (константа);
    • проверяем, не “скатился” ли импульс по оси Х и по оси Y до определенного предела (тоже константа), если “скатился” — останавливаем движение.



    “Экстренная” остановка перетаскивания. Карта должна принудительно перестать двигаться в двух случаях:
    • если пользователь кликнул мышкой по карте;
    • если пользователь изменил масштаб карты.

    Код


    Так как OpenLayers 3 сделан на основе Closure library, будем писать код по его правилам.

    Взглянув на файловую структуру, мы увидим, что в каталоге interactions находятся все объекты, которые отвечают за взаимодействие с картой. Среди них есть объект ol.interaction.DragPan, отвечающий за перетаскивание, создадим и отнаследуем от него наш ol.interaction.DragPanInertia и реализуем в нем инерционное движение карты.

    Также для удобства была добавлена реализация polyfill-а для метода requestAnimationFrame от Erik Möller.

    Что касается принудительной остановки движения карты, делается это весьма простой подпиской на соответствующие события:

    var eventType = ol.BrowserFeature.HAS_TOUCH ? goog.events.EventType.TOUCHEND : goog.events.EventType.MOUSEDOWN;
    
    // mouse down
    goog.events.listen(this._map.getViewport(), eventType, this.stopInertiaMove, false, this);
    
    // zoom changed
    goog.events.listen(this._map, ol.Object.getChangedEventType(ol.MapProperty.RESOLUTION), this.stopInertiaMove, false, this);
    

    Для того, чтобы карта начала использовать реализованные нами объекты, их необходимо зарегистрировать в методе createInteractions объекта ol.Map.

    Сначала подключаем нашу реализацию, заменив
    goog.require('ol.interaction.DragPan');
    
    на
    goog.require('ol.interaction.DragPanInertia');
    

    А потом строку
    interactions.push(new ol.interaction.DragPan(ol.interaction.condition.noModifierKeys));
    
    заменяем на
    interactions.push(new ol.interaction.DragPanInertia(ol.interaction.condition.noModifierKeys));
    

    Как вы уже наверное заметили, реализованный нами код лежит на github-е, а, благодаря github pages, вы можете посмотреть в действии пример карты с инерцией при передвижении. Пример лучше всего смотреть в Google Chrome, так как под различные браузеры и устройства он не оптимизировался, это не является основной целью статьи. Что же касается самого OpenLayers 3, то библиотека находится в стадии активной разработки и использовать ее в “боевых” приложениях пока не стоит (имеются проблемы при работе с тач-устройствами, а также со стабильностью самого API). Тем не менее, пожелаем разработчикам выдержки, вдохновения и хороших pull request-ов в столь полезный для пользователей проект.
    Метки:
    2ГИС 345,39
    Карта города и справочник предприятий
    Поделиться публикацией
    Комментарии 18
    • +4
      Невиданная гладкость для OL :)
      • 0
        Хм. Я помню, ещё с год назад включал в OL инерциальное тягание. Или его выпилили из новой версии?
      • +13
        Вы неправильно считаете инерцию, не учитывая, что drag может долго не заканчиваться.
        Use case:
        — пользователь начинает drag, водит нажатой мышой по карте.
        — насмотревшись окрестностей/найдя нужный ракурс, пользователь отпускает кнопку мыши

        Ожидаемый результат:
        — карта никуда не едет, т.к. «импульса» не было (скорость мыши в момент отпускания мала)

        Наблюдаемый результат:
        — карта «улетает» под большим импульсом, пропорционально пути мыши от начала drag-a.

        Предлагается:
        — считать импульс за интервал (например за последние 50-100мс).
        • +1
          Спасибо за дельное замечание.

          Действительно, этот кейс в статье не учтен. В API карт 2GIS-а мы считаем дистанцию (и, как следствие, импульс) не всего пути drag-а, а лишь последнего отрезка пути. Последний отрезок пути считаем исходя из разницы координат последнего события DgDrag и события DgDragStop.

          Более того, если импульс очень маленький, тогда вообще нет смысла инерционно передвигать карту, мы запускаем инерцию только в том случае, если импульс больше определенной константы.
        • 0
          прикольно, так надо и к 2гис-у прикрутить интерцию
          • +2
            Так ведь уже. ;)
            • 0
              да, точно, ступил.

              тогда было бы круто, если бы был плавный зум)
        • +3
          Ткнул на «посмотреть в действии», увеличил Кремль московский (сильно), решил чуть сдвинуть карту, по инерции оказался в Твери :)
          Нужен допилинг, имхо

          Ubuntu 11.04
          Chrome 18
          • +1
            Может не в тему конечно, но чего Хром такой старенький то?
            • 0
              «Допилинг» нужен, однозначно. Пример реализации не стоит рассматривать как окончательно готовое и единственно правильное решение, пример лучше рассматривать в обучающих целях, для реализации подобных задач в своих проектах. Для полноценного решения в OL3 было бы неплохо дождаться «устаканивания» основных «классов» библиотеки.
            • 0
              Я правильно понял, что точный адрес этот: github.com/AndreyGeonya/ol3/blob/master/src/ol/interaction/dragpaninertiainteraction.js?
              • 0
                Это адрес fork-а от основного репозитория. Сам основной репозиторий OpenLayers 3 находится здесь: github.com/openlayers/ol3/
                • 0
                  Я, собственно, спрашивал про исходник вашего компонента
            • 0
              Еще глюк:
              1. Начинаем драг
              2. Не двигая мышу, зумим
              3. Двигаем мышу, не заканчивая драг
              Результат — резкий скачок

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

              Самое читаемое