18

私の目標は、Google マップの現在の動作と同じように、ページ領域でのズームとパン操作を可能にするプラグインを作成することです (つまり、マウスでスクロール = 領域のズームイン/ズームアウト、クリック & ホールド & 移動 & リリース = パニング) )。

スクロール時、マウスカーソルを中心にズーム操作をしたい。

このために、オンザフライの CSS3 マトリックス変換を使用します。唯一の必須の制約は、0px 0px の変換原点を持つ CSS3 の変換とスケール変換以外は使用できないということです。

パンニングは、すでに機能しているため、私の質問の範囲外です。ズームに関しては、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 と Brandon Aaron の jquery マウス ホイール プラグインを使用しています

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);
}
4

2 に答える 2

34

を使用して、要素でGoogle マップのズーム動作transformを取得するのは興味深いアイデアのように思えたので、少しお金を払いました =)div

(およびブラウザーの互換性のための姉妹属性) を使用transform-originして、スケーリングしている div のマウスの位置にズームを調整します。私はこれがあなたが望むことをすることができると思います。説明のためにいくつかの例をフィドルに載せました:

調整するtransform-origin

したがって、applyTransformationsあなたの関数では、 (マウスリスナー)関数からこの値を渡すと、 andtransform-originから動的に調整できます。imageXimageYMouseZoom

    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では、 and translateYinを使用Transformationsして div 要素のマウスの位置を渡しました。2 番目の例では、変換変数と区別するために名前をoriginXandに変更しました。)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 を使用してズーム フレームをシフトします (ここで、このトリックを実行するのに役立つ 2 つの新しい変数を導入したのでoriginX、 、originYtranslateXおよびができました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);
    }

この例では、元のスクリプトをもう少し調整し、2 番目のフィドルの例を追加しました。

ここで、任意のズーム レベルからマウス カーソルをズームインおよびズームアウトします。しかし、フレーム シフトのために、元の div を動かしてしまいます (「地球を測定する」) ... 幅と高さが制限されたオブジェクトを操作すると (一方の端でズームインし、一方の端でズームアウトすると)、おかしく見えます。別の端、そして私たちはインチワームのように前進しました)。

「インチワーム」効果の回避

これを回避するには、たとえば、画像の左側の境界線が元の x 座標の右側に移動できないように、画像の上部の境界線が元の y 位置よりも下に移動できないように、制限を追加することができます。他の 2 つの境界線についても同様です。ただし、ズーム/アウトはカーソルに完全にバインドされるのではなく、例 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;
        }
    }

もう 1 つの (少しくだらない) オプションは、完全にズームアウト (scale==1) したときにフレームの移動を単純にリセットすることです。

ただし、連続した要素 (左右の端と上端と下端が結合されている) を処理する場合、または非常に大きな要素のみを処理する場合は、この問題は発生しません。

素敵なタッチですべてを仕上げるために、スケーリング オブジェクトの周囲にオーバーフローが隠されている親フレームを追加できます。そのため、ズームしても画像領域は変化しません。jsfiddle の例 4を参照してください。

于 2013-03-08T14:29:16.240 に答える