CSS3 масштабирование курсора мыши

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

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

Для этого я использую матричные преобразования CSS3 на лету. Единственное, но обязательное ограничение - это то, что я не могу использовать ничего, кроме преобразования CSS3 translate & scale, с источником преобразования 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="Stackru.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 mouse wheel от Брэндона Аарона, который можно найти здесь: https://github.com/brandonaaron/jquery-mousewheel/

Вот содержимое файла Stackru.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);
}

2 ответа

С помощью transform чтобы получить поведение масштабирования карт Google на 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 и translateY для сдвига рамки масштабирования (здесь я представил две новые переменные, которые помогают нам добиться цели - так что теперь у нас есть 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);
    }

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

Теперь мы увеличиваем и уменьшаем курсор мыши с любого уровня масштабирования. Но из-за смещения кадра мы в конечном итоге перемещаем исходный элемент 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;
        }
    }

Другой (немного дрянной) вариант - просто сбросить перевод кадра при полном уменьшении масштаба (scale==1).

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

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

Для этого мы создали библиотеку реагирования: 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

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

Другие вопросы по тегам