Подтвердить что ты не робот

Увеличение CSS3 на курсоре мыши

Моя цель - создать плагин, который позволяет выполнять операции масштабирования и панорамирования в области страницы, точно так же, как в настоящее время работает Google Maps (что означает: прокрутка с помощью мыши = масштабирование в/из области, щелчок и удерживание и перемещение release = панорамирование).

При прокрутке я хочу, чтобы операция масштабирования была центрирована курсором мыши.

Для этого я использую преобразование матрицы CSS3 на лету. Единственное, но обязательное ограничение состоит в том, что я не могу использовать ничего, кроме преобразования CSS3 и масштабирования с возникновением преобразования 0px 0px.

Панинг выходит за рамки моего вопроса, так как я уже работаю. Когда дело доходит до масштабирования, я изо всех сил пытаюсь понять, где глюк в моем javascript-коде.

Проблема должна быть где-то в функции MouseZoom.prototype.zoom при вычислении перевода по оси x и оси y.

Во-первых, вот мой код HTML:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width = device-width, initial-scale = 1.0, user-scalable = no" />
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <script src="jquery.mousewheel.min.js"></script>
    <script src="StackOverflow.js"></script>
    <style type="text/css" media="all">
        #drawing {
            position: absolute;
            top: 0px; 
            left: 0px; 
            right:0; 
            bottom:0;
            z-index: 0;
            background: url(http://catmacros.files.wordpress.com/2009/09/cats_banzai.jpg) no-repeat;
            background-position: 50% 50%;
        }
    </style>
    <title>Test</title>
</head>
<body>
    <div id="drawing"></div>
    <script>
        var renderer = new ZoomPanRenderer("drawing");
    </script>
</body>
</html>

Как вы можете видеть, я использую JQuery и плагин для мыши jQuery от Brandon Aaron, который можно найти здесь: https://github.com/brandonaaron/jquery-mousewheel/

Вот содержимое файла StackOverflow.js:

/***************************************************** 
 * Transformations
 ****************************************************/
function Transformations(translateX, translateY, scale){
    this.translateX = translateX;
    this.translateY = translateY;
    this.scale = scale;
}

/* Getters */
Transformations.prototype.getScale = function(){ return this.scale; }
Transformations.prototype.getTranslateX = function(){ return this.translateX; }
Transformations.prototype.getTranslateY = function(){ return this.translateY; }

/***************************************************** 
 * Zoom Pan Renderer
 ****************************************************/
function ZoomPanRenderer(elementId){
    this.zooming = undefined;
    this.elementId = elementId;
    this.current = new Transformations(0, 0, 1);
    this.last = new Transformations(0, 0, 1);
    new ZoomPanEventHandlers(this);
}

/* setters */
ZoomPanRenderer.prototype.setCurrentTransformations = function(t){ this.current = t; }
ZoomPanRenderer.prototype.setZooming = function(z){ this.zooming = z; }

/* getters */
ZoomPanRenderer.prototype.getCurrentTransformations = function(){ return this.current; }
ZoomPanRenderer.prototype.getZooming = function(){ return this.zooming; }
ZoomPanRenderer.prototype.getLastTransformations = function(){ return this.last; }
ZoomPanRenderer.prototype.getElementId = function(){ return this.elementId; }

/* Rendering */
ZoomPanRenderer.prototype.getTransform3d = function(t){
    var transform3d = "matrix3d(";
    transform3d+= t.getScale().toFixed(10) + ",0,0,0,";
    transform3d+= "0," + t.getScale().toFixed(10) + ",0,0,";
    transform3d+= "0,0,1,0,";
    transform3d+= t.getTranslateX().toFixed(10) + "," + t.getTranslateY().toFixed(10)  + ",0,1)";
    return transform3d;
}

ZoomPanRenderer.prototype.getTransform2d = function(t){
    var transform3d = "matrix(";
    transform3d+= t.getScale().toFixed(10) + ",0,0," + t.getScale().toFixed(10) + "," + t.getTranslateX().toFixed(10) + "," + t.getTranslateY().toFixed(10) + ")";
    return transform3d;
}

ZoomPanRenderer.prototype.applyTransformations = function(t){
    var elem = $("#" + this.getElementId());
    elem.css("transform-origin", "0px 0px");
    elem.css("-ms-transform-origin", "0px 0px");
    elem.css("-o-transform-origin", "0px 0px");
    elem.css("-moz-transform-origin", "0px 0px");
    elem.css("-webkit-transform-origin", "0px 0px");
    var transform2d = this.getTransform2d(t);
    elem.css("transform", transform2d);
    elem.css("-ms-transform", transform2d);
    elem.css("-o-transform", transform2d);
    elem.css("-moz-transform", transform2d);
    elem.css("-webkit-transform", this.getTransform3d(t));
}

/***************************************************** 
 * Event handler
 ****************************************************/
function ZoomPanEventHandlers(renderer){
    this.renderer = renderer;

    /* Disable scroll overflow - safari */
    document.addEventListener('touchmove', function(e) { e.preventDefault(); }, false);

    /* Disable default drag opeartions on the element (FF makes it ready for save)*/
    $("#" + renderer.getElementId()).bind('dragstart', function(e) { e.preventDefault(); });

    /* Add mouse wheel handler */
    $("#" + renderer.getElementId()).bind("mousewheel", function(event, delta) {
        if(renderer.getZooming()==undefined){
            var offsetLeft = $("#" + renderer.getElementId()).offset().left;
            var offsetTop = $("#" + renderer.getElementId()).offset().top;
            var zooming = new MouseZoom(renderer.getCurrentTransformations(), event.pageX, event.pageY, offsetLeft, offsetTop, delta);
            renderer.setZooming(zooming);

            var newTransformation = zooming.zoom();
            renderer.applyTransformations(newTransformation);
            renderer.setCurrentTransformations(newTransformation);
            renderer.setZooming(undefined);
        }
        return false;
    });
}

/***************************************************** 
 * Mouse zoom
 ****************************************************/
function MouseZoom(t, mouseX, mouseY, offsetLeft, offsetTop, delta){
    this.current = t;
    this.offsetLeft = offsetLeft;
    this.offsetTop = offsetTop;
    this.mouseX = mouseX;
    this.mouseY = mouseY;
    this.delta = delta;
}

MouseZoom.prototype.zoom = function(){
    var previousScale = this.current.getScale();
    var newScale = previousScale + this.delta/5;
    if(newScale<1){
        newScale = 1;
    }
    var ratio = newScale / previousScale;

    var imageX = this.mouseX - this.offsetLeft;
    var imageY = this.mouseY - this.offsetTop;

    var previousTx = - this.current.getTranslateX() * previousScale;
    var previousTy = - this.current.getTranslateY() * previousScale;
    var previousDx = imageX * previousScale;
    var previousDy = imageY * previousScale;

    var newTx = (previousTx * ratio + previousDx * (ratio - 1)) / newScale;
    var newTy = (previousTy * ratio + previousDy * (ratio - 1)) / newScale;

    return new Transformations(-newTx, -newTy, newScale);
}
4b9b3361

Ответ 1

Использование transform для получения поведения google maps zooming для элемента div показалось интересной идеей, поэтому я заплатил с ней немного =)

Я бы использовал transform-origin (и его атрибуты сестры для совместимости с браузером), чтобы настроить масштабирование на местоположение мыши на div, который вы масштабируете. Я думаю, что это может сделать то, что вы хотите. Я приведу несколько примеров на скрипке для иллюстрации:

Настройка transform-origin

Итак, в вашей функции applyTransformations мы можем динамически изменять transform-origin из imageX и imageY, если передать эти значения из функции MouseZoom (мышь).

    var orig = t.getTranslateX().toFixed() + "px " + t.getTranslateY().toFixed() + "px";
    elem.css("transform-origin", orig);
    elem.css("-ms-transform-origin", orig);
    elem.css("-o-transform-origin", orig);
    elem.css("-moz-transform-origin", orig);
    elem.css("-webkit-transform-origin", orig);

(В этом первом примере скрипта я просто использовал ваши translateX и translateY в Transformations, чтобы передать местоположение мыши в элементе div - во втором примере я переименовал его в originX и originY, чтобы отличить от переменных перевода.)

Вычисление происхождения преобразования

В MouseZoom мы можем рассчитать исходное местоположение просто с помощью imageX/previousScale.

    MouseZoom.prototype.zoom = function(){
        var previousScale = this.current.getScale();
        var newScale = previousScale + this.delta/10;
        if(newScale<1){
            newScale = 1;
        }
        var ratio = newScale / previousScale;

        var imageX = this.mouseX - this.offsetLeft;
        var imageY = this.mouseY - this.offsetTop;

        var newTx = imageX/previousScale;
        var newTy = imageY/previousScale;

        return new Transformations(newTx, newTy, newScale);
    }

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

Сдвиг рамки масштабирования (расширение моего исходного ответа)

Происхождение преобразования на изображении все равно вычисляется одинаково, но мы используем отдельный translateX и переводим Y для сдвига рамки масштабирования (здесь я представил две новые переменные, которые помогают нам сделать трюк - так что теперь мы имеем originX, originY, translateX и translateY).

    MouseZoom.prototype.zoom = function(){
        // current scale
        var previousScale = this.current.getScale();
        // new scale
        var newScale = previousScale + this.delta/10;
        // scale limits
        var maxscale = 20;
        if(newScale<1){
            newScale = 1;
        }
        else if(newScale>maxscale){
            newScale = maxscale;
        }
        // current cursor position on image
        var imageX = (this.mouseX - this.offsetLeft).toFixed(2);
        var imageY = (this.mouseY - this.offsetTop).toFixed(2);
        // previous cursor position on image
        var prevOrigX = (this.current.getOriginX()*previousScale).toFixed(2);
        var prevOrigY = (this.current.getOriginY()*previousScale).toFixed(2);
        // previous zooming frame translate
        var translateX = this.current.getTranslateX();
        var translateY = this.current.getTranslateY();
        // set origin to current cursor position
        var newOrigX = imageX/previousScale;
        var newOrigY = imageY/previousScale;
        // move zooming frame to current cursor position
        if ((Math.abs(imageX-prevOrigX)>1 || Math.abs(imageY-prevOrigY)>1) && previousScale < maxscale) {
            translateX = translateX + (imageX-prevOrigX)*(1-1/previousScale);
            translateY = translateY + (imageY-prevOrigY)*(1-1/previousScale);
        }
        // stabilize position by zooming on previous cursor position
        else if(previousScale != 1 || imageX != prevOrigX && imageY != prevOrigY) {
            newOrigX = prevOrigX/previousScale;
            newOrigY = prevOrigY/previousScale;
        }
        return new Transformations(newOrigX, newOrigY, translateX, translateY, newScale);
    }

В этом примере я немного скорректировал ваш оригинальный script и добавил второй пример скрипта.

Теперь мы увеличиваем и уменьшаем курсор мыши с любого уровня масштабирования. Но из-за сдвига рамки мы в конечном итоге перемещаем оригинальный div вокруг ( "измерение земли" )... который выглядит забавным, если вы работаете с объектом ограниченной ширины и высоты (с увеличением на одном конце, уменьшением на другой конец, и мы двинулись вперед, как червяк).

Избегать эффекта "червячка"

Чтобы этого избежать, вы можете, например, добавить ограничения, чтобы левая рамка изображения не могла перемещаться вправо от исходной координаты x, верхняя граница изображения не может двигаться ниже, чем ее исходная позиция y, и так далее для другого две границы. Но тогда масштабирование не будет полностью привязано к курсору, но также к краю изображения (вы заметите, что изображение вставлено на место) в примере 3.

    if(this.delta <= 0){
        var width = 500; // image width
        var height = 350; // image height
        if(translateX+newOrigX+(width - newOrigX)*newScale <= width){
            translateX = 0;
            newOrigX = width;
        }
        else if (translateX+newOrigX*(1-newScale) >= 0){
            translateX = 0;
            newOrigX = 0;        
        }
        if(translateY+newOrigY+(height - newOrigY)*newScale <= height){
            translateY = 0;
            newOrigY = height;
        }
        else if (translateY+newOrigY*(1-newScale) >= 0){
            translateY = 0;
            newOrigY = 0;
        }
    }

Другим (немного дерьмовым) вариантом было бы просто reset преобразовать фрейм, когда вы полностью уменьшаете масштаб (масштаб == 1).

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

Чтобы закончить все с приятным прикосновением - мы можем добавить родительский фрейм со скрытым переполнением вокруг нашего масштабирующего объекта. Таким образом, область изображения не изменяется с увеличением. См. пример jsfiddle 4.

Ответ 2

Мы сделали для этого соответствующую библиотеку: https://www.npmjs.com/package/react-map-interaction

Он обрабатывает масштабирование и панорамирование и работает как на мобильном, так и на рабочем столе.

Источник довольно короткий и читаемый, но для более прямого ответа на ваш вопрос мы используем этот CSS-преобразование:

const transform = `translate(${translation.x}px, ${translation.y}px) scale(${scale})`;
const style = {
    transform: transform,
    transformOrigin: '0 0 '
};

// render the div with that style

Один из основных трюков - это правильно рассчитать разницу между начальным состоянием указателя/мыши и текущим состоянием, когда происходит движение касания/мыши. Когда мышь вниз, захватите координаты. Затем на каждом шаге мыши (пока мышь вверху) вычислите разницу на расстоянии. Этот diff - это то, что вам нужно, чтобы компенсировать перевод, чтобы убедиться, что начальная точка под вашим курсором является фокусом масштабирования.