Рис 1. Zoom в редакторе блок-схем dgrm.net
Рис 1. Zoom в редакторе блок-схем dgrm.net

dgrm.net | GitHub

Как сделан zoom в редакторе блок-схем dgrm.net.
Zoom-ить можно:

  • колесиком мышки,

  • touchpad-ом

  • и двумя пальцами на телефонах и планшетах.

Готовая функция zoom-а SVG для ваших проектов прилагается. Для HTML можно переделать.

Алгоритм zoom-а

В SVG и HTML есть масштабирование. Масштабирование в HTML.

Только менять масштаб не достаточно. Изображение будет уезжать.
При увеличении круг в центре экрана съезжает вниз вправо.

Рис 2. Не правильно: при увеличении круг в центре экрана съезжает вниз вправо.
Рис 2. Не правильно: при увеличении круг в центре экрана съезжает вниз вправо.
Рис 3. При увеличении круг в центре экрана съезжает вниз вправо - схема.
Рис 3. При увеличении круг в центре экрана съезжает вниз вправо - схема.

Черный прямоугольник это экран. Синий это увеличенное изображение. Увеличенный круг находится в центре увеличенного изображения, но съехал относительно экрана.

Нужно увеличивать и сдвигать, тогда центр изображения не уедет.

Рис 4. Увеличиваем и сдвигаем вверх влево. Круг остается в центре экрана.
Рис 4. Увеличиваем и сдвигаем вверх влево. Круг остается в центре экрана.

На рисунке 4 центр изображения не съезжает. Но карты работают не так. Карты зумятся не в центр. Карты зумятся относительно курсора. Место, куда указывает курсор, не сдвигается относительно экрана.

Здание в центре карты уезжает вниз. Оно выделено красным. Здание под курсором остается на месте. Выделено синим.

Рис 5. Карта зумятся не в центр, а относительно курсора.
Место, куда указывает курсор, не сдвигается относительно экрана.
Рис 5. Карта зумятся не в центр, а относительно курсора. Место, куда указывает курсор, не сдвигается относительно экрана.

Функция зума должна сдвигать изображение так, чтобы точка под курсором оставалась на месте.

/**
 * @param {SVGGraphicsElement} svgEl
 * @param {Point} fixedPoint this point will not change position while scale
 * @param {number} scale
 * @param {number} nextScale
 */
export function svgScale(svgEl, fixedPoint, scale, nextScale) {
    const position = svgPositionGet(svgEl);
 
    svgPositionSet(svgEl, {
        x: nextScale / scale * (position.x - fixedPoint.x) + fixedPoint.x,
        y: nextScale / scale * (position.y - fixedPoint.y) + fixedPoint.y
    });
 
    ensureTransform(svgEl, SVGTransform.SVG_TRANSFORM_SCALE)
        .setScale(nextScale, nextScale);
}

Листинг 1. Функция зума. Сдвигает изображение так, что fixedPoint остается на месте.
Вспомогательные функции svgPositionGet, svgPositionSet, ensureTransform смотрите на
GitHub.

Zoom колесиком мышки и touchpad-ом

Подписываемся на событие колесика мышки “wheel”. Для щипка двумя пальцами на touchpad отдельного события нет. Щипок использует это же событие “wheel”.

Для колесика масштаб изменяется с шагом 0.25, а для touchpad 0.05. Значения подобраны так чтобы:

  • колесико мышки  не нужно было долго крутить,

  • а на touchpad изображение не скакало.

// 'svg' is type of {SVGSVGElement}

let scale = 1;
// mouse wheel, trackpad pitch
svg.addEventListener('wheel', /** @param {WheelEvent} evt */ evt => {
    evt.preventDefault();
 
 
    // calc nextScale
 
    const delta = evt.deltaY || evt.deltaX;
    const scaleStep = Math.abs(delta) < 50
        ? 0.05  // touchpad pitch
        : 0.25; // mouse wheel
 
    const scaleDelta = delta < 0 ? scaleStep : -scaleStep;
    const nextScale = scale + scaleDelta; // 'scale' is previous scale
 
 
    // calc fixedPoint
    const fixedPoint = { x: evt.clientX, y: evt.clientY };
 
 
    // scale
    // 'svgEl' is element to scale
    svgScale(svgEl, fixedPoint, scale, nextScale);
    scale = nextScale;
});

Листинг 2. Подписываемся на событие колесика мышки. Щипок touchpad запускает это же событие. Полный код смотрите на GitHub.

Zoom двумя пальцам на телефонах и планшетах

Для zoom-а пальцами фиксированная точка - это точка посередине между пальцами. Изменение масштаба зависит от изменения расстояния между пальцами.

Еще нужно учитывать что изображение можно одновременно зумить и двигать.

// calc nextScale
 
// distance between fingers
const distanceNew = Math.hypot(
    firstFinger.x - secondFinger.x,
    firstFinger.y - secondFinger.y);
 
// 'distance' is previous distance between fingers
// 'scale' is previous scale
const nextScale = scale / distance * distanceNew;
 
 
// calc fixedPoint
const fixedPoint = {
    x: (firstFinger.x + secondFinger.x) / 2,
    y: (firstFinger.y + secondFinger.y) / 2
};
 
 
// scale
// 'svgEl' is element to scale
svgScale(svgEl, fixedPoint, scale, nextScale);
 
 
// don't forget to also move the canvas behind your fingers

Листинг 3. При zoom-е пальцами фиксированная точка это точка посередине между пальцами. Изменение масштаба зависит от изменения расстояния между пальцами. Полный код смотрите на GitHub.

Другие статьи про dgrm.net

Как поддержать проект

  • Начните использовать, расскажите что думаете.
    Любым способом: комментарии, личные сообщения, на GitHub.
    Все читаю, веду список предложений.

  • Расскажите знакомым.

  • Ставьте звездочки на GitHub.

Комментарии (7)


  1. mayorovp
    30.10.2022 10:08
    +2

    const distanceNew = Math.hypot(
        firstFinger.x - secondFinger.x,
        firstFinger.y - secondFinger.y);
    
    // 'distance' variable is previous distance between fingers
    const scaleDelta = (distanceNew - distance) * 0.01;
    const nextScale = scale + scaleDelta; // 'scale' is previous scale

    Что это вообще за формулы? В них есть хоть какой-то математический смысл, или они подбирались пока результат не стал хоть немного напоминать то что нужно?


    Вот правильная формула, дающая наиболее интуитивное управление масштабом:


    const distanceNew = Math.hypot(
        firstFinger.x - secondFinger.x,
        firstFinger.y - secondFinger.y);
    
    // 'distance' variable is previous distance between fingers
    const nextScale = scale / distance * distanceNew;


    1. Alex_BBB Автор
      30.10.2022 15:09

      Да, так лучше. Спасибо.


  1. mayorovp
    30.10.2022 11:07
    +2

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


    /**
     * @param {Point} p1 старая позиция первого пальца
     * @param {Point} p2 старая позиция второго пальца
     * @param {Point} q1 новая позиция первого пальца
     * @param {Point} q2 новая позиция второго пальца
     */
    function getMatrix(p1, p2, q1, q2) {
        const // межпальцевые вектора
            px = p1.x - p2.x, py = p1.y - p2.y, 
            qx = q1.x - q2.x, qy = q1.2 - q2.y;
    
        const // коэффициенты масштаба и поворота
            a = (px * qx + py * qy) / (px * px + py * py),
            b = (qx * py - px * qy) / (px * px + py * py);
    
        const // коэффициенты перемещения
            e = q1.x - a * p1.x - b * p1.y,
            f = q1.y + b * p1.x - a * p1.y;
    
        return new DOMMatrix([a, b, -b, a, e, f]);
    }

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


    Но чтобы такой способ работал — надо все преобразования делать только матрицами, никаких svgPositionSet, только ensureTransform.


    1. DWM
      01.11.2022 18:33

      qy = q1.2 + q2.y ?

      может быть так:

      qy = q1.y + q2.y ?


      1. mayorovp
        01.11.2022 19:06

        Разумеется, только всё-таки минус:


        const // межпальцевые вектора
                px = p1.x - p2.x, py = p1.y - p2.y, 
                qx = q1.x - q2.x, qy = q1.y - q2.y;

        Если что, код я проверял, но я его проверял на C#, а не на Javascript. Вот при переносе с одного языка на другой опечатка и вылезла...


  1. dom1n1k
    31.10.2022 01:57

    Не очень хорошая идея называть переменную fixedPoint — в контексте информатики сразу лезет посторонний смысл. Тем более, что есть специальный устоявшийся термин — origin.


  1. BooooBka
    31.10.2022 10:46

    Спасибо!

    Решал похожую задачу, необходимо было сделать просматривалку планов здания. В итоге уперся в размер svg, они могли быть 50-100мб, возникали тормоза.

    Как решение написал сервис на SharpJS, резал картинку на тайлы, для просмотра использовал любые карты - leaflet/mapbox/etc.