Реализация интерактивного масштабирования изображений (pinch/pan) на мобильных устройствах

Показывать справа: 0

В современной веб-разработке, особенно при создании адаптивных интерфейсов для мобильных устройств, возможность детально рассмотреть изображения — важная часть пользовательского опыта. Этот урок покажет, как реализовать интуитивно понятное pinch zoom (увеличение двумя пальцами) и pan (перемещение) изображений, создавая эффект, подобный работе с галереями в нативных приложениях.

Зачем нужны pinch zoom и pan?

На мобильных устройствах, где экраны меньше, чем на настольных компьютерах, пользователям часто требуется рассмотреть детали изображений. Функция pinch zoom позволяет масштабировать картинку, а pan — перемещать ее в увеличенном состоянии. Реализация этих жестов значительно повышает удобство использования вашего сайта или веб-приложения.

  • Улучшенный UX: Пользователи могут свободно изучать содержимое, не теряя деталей.
  • Привычный интерфейс: Эффект "как в приложении" делает взаимодействие более естественным.
  • Интерактивность: Добавляет динамичности и современности вашему проекту.

Необходимые инструменты

Для реализации этой функциональности мы будем использовать:

  • JavaScript: Основной язык программирования для логики.
  • Hammer.js: Мощная JavaScript-библиотека для работы с сенсорными жестами (touch events), которая значительно упрощает реализацию pinch-to-zoom и drag-and-drop двумя пальцами.
  • HTML: Для структуры и размещения элементов.

Подключение Hammer.js

Прежде всего, подключите библиотеку Hammer.js. Это можно сделать, добавив следующую строку в секцию <head> или перед закрывающим тегом </body> вашего HTML-файла:


<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>

        

Основной JavaScript-скрипт для pinch/pan

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


 // Объект для хранения экземпляров Hammer.js, связанных с уникальными ID элементов.
// Это позволяет нам управлять ими (например, удалять).
const hammerInstances = {};
// Объект для хранения самих HTML-элементов, с которыми работает Hammer.js.
const hammerelms = {};

// --- Вспомогательные функции ---

// Генерирует или получает уникальный ID для HTML-элемента.
// Это важно для корректного управления экземплярами Hammer.js.
function getElementUniqueId(elm) {
    if (!elm.id) {
        // Генерируем ID, если у элемента его нет, чтобы связать с ним экземпляр Hammer.js.
        elm.id = 'hammer-generated-id-' + Math.random().toString(36).substr(2, 9);
    }
    return elm.id;
}

// Функция для полного удаления всех примененных обработчиков Hammer.js
// и сброса стилей элементов. Полезна при смене состояний (например, переход на десктоп).
function removeHammerFromElementAll() {
    // Уничтожаем все активные экземпляры Hammer.js.
    Object.keys(hammerInstances).forEach(function(key) {
        const hammertimeInstance = hammerInstances[key];
        if (hammertimeInstance) {
            hammertimeInstance.destroy(); // Освобождаем ресурсы.
        }
    });
    // Сбрасываем примененные CSS-трансформации и другие стили,
    // чтобы вернуть элемент в исходное состояние.
    Object.keys(hammerelms).forEach(function(key) {
        let elm = document.getElementById(key);
        if (elm) {
            elm.style.webkitTransform = ''; // Сбрасываем трансформацию.
            elm.style.touchAction = 'manipulation'; // Возвращаем стандартное поведение касаний.
            elm.style.userSelect = ''; // Сбрасываем выбор текста.
            elm.style.webkitUserDrag = ''; // Сбрасываем перетаскивание.
        }
    });
    // Очищаем объекты, чтобы избежать утечек памяти.
    Object.keys(hammerInstances).forEach(key => delete hammerInstances[key]);
    Object.keys(hammerelms).forEach(key => delete hammerelms[key]);
}

// --- Основная функция для применения pinch/pan ---

// Применяет интерактивную функциональность (pinch zoom и pan) к указанному HTML-элементу.
function pinch_web(elm) {
    // Создаем новый экземпляр Hammer.js для элемента.
    const hammertime = new Hammer(elm, {});
    // Включаем поддержку жеста 'pinch' (масштабирование двумя пальцами).
    hammertime.get('pinch').set({
        enable: true
    });

    // Получаем или генерируем уникальный ID для элемента.
    const elementId = getElementUniqueId(elm);
    // Сохраняем экземпляр Hammer.js и сам элемент для последующего управления.
    hammerInstances[elementId] = hammertime;
    hammerelms[elementId] = elm;

    // Инициализируем переменные для отслеживания текущего состояния трансформации:
    var posX = 0,            // Текущая позиция по оси X.
        posY = 0,            // Текущая позиция по оси Y.
        scale = 1,           // Текущий масштаб.
        last_scale = 1,      // Предыдущий масштаб (для расчета изменения).
        last_posX = 0,       // Предыдущая позиция по X (для расчета изменения).
        last_posY = 0,       // Предыдущая позиция по Y (для расчета изменения).
        max_pos_x = 0,       // Максимально допустимое смещение по X.
        max_pos_y = 0,       // Максимально допустимое смещение по Y.
        transform = "",      // Строка с CSS-трансформацией.
        el = elm;            // Ссылка на текущий элемент.

    // Обработчик различных жестов (событий) от Hammer.js.
    hammertime.on('doubletap pan pinch panend pinchend', function(ev) {
        // --- Обработка двойного касания (doubletap) ---
        // При двойном касании мы либо увеличиваем изображение в 2 раза,
        // либо сбрасываем его к исходному масштабу (1x).
        if (ev.type == "doubletap") {
            // Проверяем текущее состояние трансформации.
            // Если оно не равно исходному (matrix(1, 0, 0, 1, 0, 0)), то сбрасываем.
            if (window.getComputedStyle(el, null).getPropertyValue('-webkit-transform').toString() != "matrix(1, 0, 0, 1, 0, 0)") {
                transform = "translate3d(0, 0, 0) scale3d(1, 1, 1)"; // Сброс к 1x.
                scale = 1;
                last_scale = 1;
            } else { // Иначе, устанавливаем масштаб 2x.
                transform = "translate3d(0, 0, 0) scale3d(2, 2, 1)"; // Увеличение до 2x.
                scale = 2;
                last_scale = 2;
            }
        }

        // --- Обработка перемещения (pan) ---
        // Этот блок выполняется, только если изображение увеличено (scale != 1).
        if (scale != 1) {
            // Рассчитываем новую позицию элемента, добавляя смещение текущего жеста (ev.deltaX, ev.deltaY)
            // к последней сохраненной позиции.
            posX = last_posX + ev.deltaX;
            posY = last_posY + ev.deltaY;

            // Ограничиваем перемещение, чтобы контент не выходил за пределы видимой области.
            // Максимальное смещение рассчитывается на основе текущего масштаба и размеров элемента.
            max_pos_x = Math.ceil((scale - 1) * el.clientWidth / 2);
            max_pos_y = Math.ceil((scale - 1) * el.clientHeight / 2);

            // Применяем ограничения: если posX/posY превышает максимум, устанавливаем максимум.
            if (posX > max_pos_x) posX = max_pos_x;
            if (posX < -max_pos_x) posX = -max_pos_x;
            if (posY > max_pos_y) posY = max_pos_y;
            if (posY < -max_pos_y) posY = -max_pos_y;
        }

        // --- Обработка масштабирования (pinch) ---
        // Этот блок выполняется только при жесте 'pinch'.
        if (ev.type == "pinch") {
            // Рассчитываем новый масштаб: берем предыдущий масштаб (last_scale)
            // и умножаем на коэффициент изменения текущего жеста (ev.scale).
            // Ограничиваем масштаб значением от 0.999 (чтобы избежать полного сброса при минимальном движении)
            // до 4 (максимальное значение масштабирования).
            scale = Math.max(.999, Math.min(last_scale * (ev.scale), 4));
        }
        // Когда жест 'pinch' заканчивается (pinchend), сохраняем текущий масштаб
        // как последний масштаб для следующего события.
        if(ev.type == "pinchend"){
            last_scale = scale;
        }

        // --- Обработка завершения перемещения (panend) ---
        // Когда жест 'pan' заканчивается, сохраняем текущую позицию (posX, posY)
        // как последнюю позицию (last_posX, last_posY), учитывая примененные ограничения.
        if(ev.type == "panend"){
            last_posX = posX < max_pos_x ? posX : max_pos_x;
            last_posY = posY < max_pos_y ? posY : max_pos_y;
        }

        // Формируем строку CSS-трансформации, если масштаб отличается от 1.
        if (scale != 1) {
            transform = `translate3d(${posX}px,${posY}px, 0) scale3d(${scale}, ${scale}, 1)`;
        } else {
            // Если масштаб вернулся к 1, сбрасываем трансформацию.
            transform = "";
        }

        // Применяем сформированную трансформацию к элементу.
        if (transform) {
            el.style.webkitTransform = transform;
        } else {
             el.style.webkitTransform = ''; // Явный сброс, если transform пуст.
        }
    });
}

// --- Дополнительная функция для определения мобильной ширины ---

// Простая функция, возвращающая true, если ширина окна браузера <= 768px (типичная мобильная ширина).
function isMobileWidth() {
    return window.innerWidth <= 768;
}
 

Пример использования

Для применения функциональности pinch zoom и pan к вашим изображениям, вам нужно найти нужные элементы на странице и передать их в функцию pinch_web.

HTML-структура (пример):

        
      
        <div class="product-page__image">
    <div class="product-page__image-main">
        <div class="product-page__image-main-carousel owl-carousel owl-loaded owl-drag">
            <div class="owl-stage-outer" style="overflow: inherit">
                <div class="owl-stage">
                    <div class="owl-item">
                        <img src="/" alt="" title="" data-thumb="" data-full="" width="500" height="500" class="" />
                    </div>
                    <div class="owl-item">
                        <img src="/" alt="" title="" data-thumb="" data-full="" width="500" height="500" class="" />
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

JavaScript для применения pinch_web:

Привязываем элемнты .owl-item к функции pinch_web


        
        
let parentBlock_js = document.querySelector(".product-page__image");

// 2. Проверяем, найден ли родительский блок
if (parentBlock_js) {
    // 3. Ищем все элементы .owl-item внутри найденного родительского блока
    let imageContainers = parentBlock_js.querySelectorAll(".owl-item");

    // 4. Проверяем, есть ли найденные элементы
    if (imageContainers.length > 0) {
        // 5. Перебираем найденные элементы и вызываем pinch_web для каждого
        imageContainers.forEach((container) => {
            pinch_web(container);
        });
    } else {
        let imageContainers2 = parentBlock_js.querySelectorAll(".product-page__image-main-img");

        imageContainers2.forEach((container) => {
            pinch_web(container);
        });
    }
}
        

Удаляем элементы .owl-item от функции pinch_web

 //webalan
setTimeout(() => {
    //webalan
    let parentBlock_js = document.querySelector(".product-page__image");
    $(parentBlock).find(".owl-stage-outer").css("overflow", "hidden");
    // 2. Проверяем, найден ли родительский блок
    if (parentBlock_js) {
        // 3. Ищем все элементы .owl-item внутри найденного родительского блока
        let imageContainers = parentBlock_js.querySelectorAll(".owl-item");

        // 4. Проверяем, есть ли найденные элементы
        if (imageContainers.length > 0) {
            removeHammerFromElementAll();
        } else {
            let imageContainers2 = parentBlock_js.querySelectorAll(".product-page__image-main-img");

            imageContainers2.forEach((container) => {
                container.style = "";
                removeHammerFromElementAll();
            });
        }
    }
}, 500);


        

Важные замечания

  • CSS touch-action: Для корректной работы Hammer.js, свойство CSS touch-action на масштабируемых элементах часто устанавливают в 'manipulation' или 'none'. Это предотвращает конфликты с нативными жестами браузера.
  • Производительность: На страницах с большим количеством интерактивных изображений важно следить за производительностью.
  • Управление состоянием: При разработке SPA, убедитесь, что вы корректно удаляете экземпляры Hammer.js при уничтожении компонента, чтобы избежать утечек памяти.
  • Адаптивность: Используйте проверку isMobileWidth() или медиа-запросы, чтобы применять интерактивность только на устройствах, где это действительно нужно.

Заключение

Реализация pinch zoom и pan с помощью Hammer.js — это мощный инструмент для создания интерактивных и интуитивно понятных интерфейсов на мобильных устройствах. Этот урок предоставляет вам основу для добавления функциональности "drag and drop двумя пальцами как в приложении" к вашим изображениям, улучшая пользовательский опыт и делая ваш сайт более современным.

Покупка готового скрипта joomla 3

или просто напишите в телеграмм https://t.me/webalan