web-dev-qa-db-fra.com

Zoom avant sur un point (en utilisant l'échelle et la translation)

Je souhaite pouvoir effectuer un zoom avant sur le point situé sous la souris dans un canevas HTML 5, comme pour le zoom sur Google Maps . Comment puis-je y arriver?

138
csiz

Finalement résolu le:

var zoomIntensity = 0.2;

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var width = 600;
var height = 200;

var scale = 1;
var originx = 0;
var originy = 0;
var visibleWidth = width;
var visibleHeight = height;


function draw(){
    // Clear screen to white.
    context.fillStyle = "white";
    context.fillRect(originx,originy,800/scale,600/scale);
    // Draw the black square.
    context.fillStyle = "black";
    context.fillRect(50,50,100,100);
}
// Draw loop at 60FPS.
setInterval(draw, 1000/60);

canvas.onwheel = function (event){
    event.preventDefault();
    // Get mouse offset.
    var mousex = event.clientX - canvas.offsetLeft;
    var mousey = event.clientY - canvas.offsetTop;
    // Normalize wheel to +1 or -1.
    var wheel = event.deltaY < 0 ? 1 : -1;

    // Compute zoom factor.
    var zoom = Math.exp(wheel*zoomIntensity);
    
    // Translate so the visible Origin is at the context's Origin.
    context.translate(originx, originy);
  
    // Compute the new visible Origin. Originally the mouse is at a
    // distance mouse/scale from the corner, we want the point under
    // the mouse to remain in the same place after the zoom, but this
    // is at mouse/new_scale away from the corner. Therefore we need to
    // shift the Origin (coordinates of the corner) to account for this.
    originx -= mousex/(scale*zoom) - mousex/scale;
    originy -= mousey/(scale*zoom) - mousey/scale;
    
    // Scale it (centered around the Origin due to the trasnslate above).
    context.scale(zoom, zoom);
    // Offset the visible Origin to it's proper position.
    context.translate(-originx, -originy);

    // Update scale and others.
    scale *= zoom;
    visibleWidth = width / scale;
    visibleHeight = height / scale;
}
<canvas id="canvas" width="600" height="200"></canvas>

La clé, comme @ Tatarize a souligné , consiste à calculer la position de l’axe de telle sorte que le point de zoom (pointeur de la souris) reste au même endroit après le zoom.

À l’origine, la souris est éloignée mouse/scale du coin, nous voulons que le point situé sous la souris reste au même endroit après le zoom, mais cela se trouve à mouse/new_scale loin du coin. Par conséquent, nous devons décaler le Origin (coordonnées du coin) pour en tenir compte.

originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
scale *= zomm

Le code restant doit ensuite appliquer la mise à l'échelle et être traduit dans le contexte de dessin afin que son origine coïncide avec le coin de la zone de dessin.

53
csiz

La meilleure solution consiste simplement à déplacer la position de la fenêtre en fonction de la modification du zoom. Le point de zoom est simplement le point de l'ancien zoom et le nouveau zoom pour lequel vous souhaitez rester le même. Ce qui revient à dire que la fenêtre d'affichage pré-zoomée et la fenêtre d'affichage post-zoomée ont le même point de zoom par rapport à la fenêtre d'affichage. Étant donné que nous mettons à l'échelle par rapport à l'origine. Vous pouvez ajuster la position de la fenêtre en conséquence:

scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);

En réalité, vous pouvez simplement vous déplacer vers le bas et vers la droite lorsque vous effectuez un zoom avant, en fonction du facteur de zoom que vous avez utilisé, par rapport au point sur lequel vous avez effectué un zoom.

enter image description here

107
Tatarize

C'est en fait un problème très difficile (mathématiquement), et je travaille presque sur la même chose. J'ai posé une question similaire sur Stackoverflow mais je n'ai pas eu de réponse, mais j'ai posté dans DocType (StackOverflow pour HTML/CSS) et j'ai reçu une réponse. Découvrez-le http://doctype.com/javascript-image-zoom-css3-transforms-calculate-Origin-example

Je suis en train de construire un plugin jQuery qui le fait (zoom de style Google Maps avec CSS3 Transforms). Le bit de zoom du curseur de la souris fonctionne bien, essayant toujours de comprendre comment permettre à l'utilisateur de faire glisser la toile, comme vous pouvez le faire dans Google Maps. Quand cela fonctionne, je poste le code ici, mais consultez le lien ci-dessus pour la partie zoom de la souris.

Je n'avais pas réalisé qu'il existait des méthodes d'échelle et de traduction en contexte Canvas, vous pouvez obtenir la même chose avec CSS3, par exemple. en utilisant jQuery:

$('div.canvasContainer > canvas')
    .css('-moz-transform', 'scale(1) translate(0px, 0px)')
    .css('-webkit-transform', 'scale(1) translate(0px, 0px)')
    .css('-o-transform', 'scale(1) translate(0px, 0px)')
    .css('transform', 'scale(1) translate(0px, 0px)');

Assurez-vous de définir l’origine CSS3 transform-Origin sur 0, 0 (-moz-transform-Origin: 0 0). L'utilisation de la transformation CSS3 vous permet d'effectuer un zoom avant sur n'importe quoi. Assurez-vous simplement que le conteneur DIV est défini sur débordement: masqué pour empêcher les bords zoomés de se répandre sur les côtés.

Que vous utilisiez des transformations CSS3 ou des méthodes d'échelle et de traduction propres à canvas, vous en avez la responsabilité, mais vérifiez le lien ci-dessus pour connaître les calculs.


Mise à jour: Meh! Je vais simplement poster le code ici plutôt que de vous faire suivre un lien:

$(document).ready(function()
{
    var scale = 1;  // scale of the image
    var xLast = 0;  // last x location on the screen
    var yLast = 0;  // last y location on the screen
    var xImage = 0; // last x location on the image
    var yImage = 0; // last y location on the image

    // if mousewheel is moved
    $("#mosaicContainer").mousewheel(function(e, delta)
    {
        // find current location on screen 
        var xScreen = e.pageX - $(this).offset().left;
        var yScreen = e.pageY - $(this).offset().top;

        // find current location on the image at the current scale
        xImage = xImage + ((xScreen - xLast) / scale);
        yImage = yImage + ((yScreen - yLast) / scale);

        // determine the new scale
        if (delta > 0)
        {
            scale *= 2;
        }
        else
        {
            scale /= 2;
        }
        scale = scale < 1 ? 1 : (scale > 64 ? 64 : scale);

        // determine the location on the screen at the new scale
        var xNew = (xScreen - xImage) / scale;
        var yNew = (yScreen - yImage) / scale;

        // save the current screen location
        xLast = xScreen;
        yLast = yScreen;

        // redraw
        $(this).find('div').css('-moz-transform', 'scale(' + scale + ')' + 'translate(' + xNew + 'px, ' + yNew + 'px' + ')')
                           .css('-moz-transform-Origin', xImage + 'px ' + yImage + 'px')
        return false;
    });
});

Vous devrez bien sûr l’adapter pour utiliser l’échelle de la toile et traduire les méthodes.


Mise à jour 2: Je viens juste de remarquer que j'utilise transform-Origin avec translate. J'ai réussi à implémenter une version qui utilise simplement scale et à traduire, allez voir ici http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Attendre les images à télécharger puis utilisez la molette de la souris pour zoomer, prend également en charge le panoramique en faisant glisser l’image. Il utilise les transformations CSS3, mais vous devriez pouvoir utiliser les mêmes calculs pour votre canevas.

25
Sunday Ironfoot

J'aime réponse de Tatarize , mais je proposerai une alternative. C'est un problème trivial d'algèbre linéaire, et la méthode que je présente fonctionne bien avec les panoramiques, le zoom, l'inclinaison, etc. C'est-à-dire que cela fonctionne bien si votre image est déjà transformée.

Lorsqu'une matrice est mise à l'échelle, l'échelle est au point (0, 0). Ainsi, si vous avez une image et que vous l’échelonnez par un facteur 2, le point situé en bas à droite double dans les directions x et y (en utilisant la convention selon laquelle [0, 0] est le coin supérieur gauche de l’image).

Si au lieu de cela vous souhaitez agrandir l'image autour du centre, la solution est la suivante: (1) traduire l'image de telle sorte que son centre soit à (0, 0); (2) redimensionne l'image par des facteurs x et y; (3) traduire l'image en arrière. c'est à dire.

myMatrix
  .translate(image.width / 2, image.height / 2)    // 3
  .scale(xFactor, yFactor)                         // 2
  .translate(-image.width / 2, -image.height / 2); // 1

Plus abstraitement, la même stratégie fonctionne pour n'importe quel point. Si, par exemple, vous souhaitez redimensionner l'image en un point P:

myMatrix
  .translate(P.x, P.y)
  .scale(xFactor, yFactor)
  .translate(-P.x, -P.y);

Enfin, si l'image est déjà transformée d'une manière ou d'une autre (par exemple, si elle est tournée, inclinée, traduite ou mise à l'échelle), la transformation en cours doit être conservée. Plus précisément, la transformation définie ci-dessus doit être post-multipliée (ou multipliée à droite) par la transformation en cours.

myMatrix
  .translate(P.x, P.y)
  .scale(xFactor, yFactor)
  .translate(-P.x, -P.y)
  .multiply(myMatrix);

Voilà. Voici un tableau qui montre cela en action. Faites défiler avec la molette à souris sur les points et vous verrez qu'ils restent toujours en place. (Testé dans Chrome uniquement.) http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview

7
avejidah

Je me suis heurté à ce problème en utilisant c ++, ce que je n'aurais probablement pas dû faire. Je viens d'utiliser des matrices OpenGL pour commencer ... de toute façon, si vous utilisez un contrôle dont Origin est en haut à gauche et que vous souhaitez un panoramique/zoom comme google maps, voici la mise en page (en utilisant allegro comme gestionnaire d'événements):

// initialize
double originx = 0; // or whatever its base offset is
double originy = 0; // or whatever its base offset is
double zoom = 1;

.
.
.

main(){

    // ...set up your window with whatever
    //  tool you want, load resources, etc

    .
    .
    .
    while (running){
        /* Pan */
        /* Left button scrolls. */
        if (mouse == 1) {
            // get the translation (in window coordinates)
            double scroll_x = event.mouse.dx; // (x2-x1) 
            double scroll_y = event.mouse.dy; // (y2-y1) 

            // Translate the Origin of the element (in window coordinates)      
            originx += scroll_x;
            originy += scroll_y;
        }

        /* Zoom */ 
        /* Mouse wheel zooms */
        if (event.mouse.dz!=0){    
            // Get the position of the mouse with respect to 
            //  the Origin of the map (or image or whatever).
            // Let us call these the map coordinates
            double mouse_x = event.mouse.x - originx;
            double mouse_y = event.mouse.y - originy;

            lastzoom = zoom;

            // your zoom function 
            zoom += event.mouse.dz * 0.3 * zoom;

            // Get the position of the mouse
            // in map coordinates after scaling
            double newx = mouse_x * (zoom/lastzoom);
            double newy = mouse_y * (zoom/lastzoom);

            // reverse the translation caused by scaling
            originx += mouse_x - newx;
            originy += mouse_y - newy;
        }
    }
}  

.
.
.

draw(originx,originy,zoom){
    // NOTE:The following is pseudocode
    //          the point is that this method applies so long as
    //          your object scales around its top-left corner
    //          when you multiply it by zoom without applying a translation.

    // draw your object by first scaling...
    object.width = object.width * zoom;
    object.height = object.height * zoom;

    //  then translating...
    object.X = originx;
    object.Y = originy; 
}
7
Aleksandr Albert

Voici ma solution pour une image centrée:

var MIN_SCALE = 1;
var MAX_SCALE = 5;
var scale = MIN_SCALE;

var offsetX = 0;
var offsetY = 0;

var $image     = $('#myImage');
var $container = $('#container');

var areaWidth  = $container.width();
var areaHeight = $container.height();

$container.on('wheel', function(event) {
    event.preventDefault();
    var clientX = event.originalEvent.pageX - $container.offset().left;
    var clientY = event.originalEvent.pageY - $container.offset().top;

    var nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale - event.originalEvent.deltaY / 100));

    var percentXInCurrentBox = clientX / areaWidth;
    var percentYInCurrentBox = clientY / areaHeight;

    var currentBoxWidth  = areaWidth / scale;
    var currentBoxHeight = areaHeight / scale;

    var nextBoxWidth  = areaWidth / nextScale;
    var nextBoxHeight = areaHeight / nextScale;

    var deltaX = (nextBoxWidth - currentBoxWidth) * (percentXInCurrentBox - 0.5);
    var deltaY = (nextBoxHeight - currentBoxHeight) * (percentYInCurrentBox - 0.5);

    var nextOffsetX = offsetX - deltaX;
    var nextOffsetY = offsetY - deltaY;

    $image.css({
        transform : 'scale(' + nextScale + ')',
        left      : -1 * nextOffsetX * nextScale,
        right     : nextOffsetX * nextScale,
        top       : -1 * nextOffsetY * nextScale,
        bottom    : nextOffsetY * nextScale
    });

    offsetX = nextOffsetX;
    offsetY = nextOffsetY;
    scale   = nextScale;
});
body {
    background-color: orange;
}
#container {
    margin: 30px;
    width: 500px;
    height: 500px;
    background-color: white;
    position: relative;
    overflow: hidden;
}
img {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    max-width: 100%;
    max-height: 100%;
    margin: auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<div id="container">
    <img id="myImage" src="http://s18.postimg.org/eplac6dbd/mountain.jpg">
</div>
5
Chris

Voici une autre façon de le faire qui utilise setTransform () à la place de scale () et translate (). Tout est stocké dans le même objet. Le canevas est supposé être à 0,0 sur la page, sinon vous devrez soustraire sa position aux coordonnées de la page.

this.zoomIn = function (pageX, pageY) {
    var zoomFactor = 1.1;
    this.scale = this.scale * zoomFactor;
    this.lastTranslation = {
        x: pageX - (pageX - this.lastTranslation.x) * zoomFactor,
        y: pageY - (pageY - this.lastTranslation.y) * zoomFactor
    };
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    this.lastTranslation.x,
                                    this.lastTranslation.y);
};
this.zoomOut = function (pageX, pageY) {
    var zoomFactor = 1.1;
    this.scale = this.scale / zoomFactor;
    this.lastTranslation = {
        x: pageX - (pageX - this.lastTranslation.x) / zoomFactor,
        y: pageY - (pageY - this.lastTranslation.y) / zoomFactor
    };
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    this.lastTranslation.x,
                                    this.lastTranslation.y);
};

Code d'accompagnement pour gérer le panoramique:

this.startPan = function (pageX, pageY) {
    this.startTranslation = {
        x: pageX - this.lastTranslation.x,
        y: pageY - this.lastTranslation.y
    };
};
this.continuePan = function (pageX, pageY) {
    var newTranslation = {x: pageX - this.startTranslation.x,
                          y: pageY - this.startTranslation.y};
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    newTranslation.x, newTranslation.y);
};
this.endPan = function (pageX, pageY) {
    this.lastTranslation = {
        x: pageX - this.startTranslation.x,
        y: pageY - this.startTranslation.y
    };
};

Pour obtenir la réponse vous-même, considérez que les mêmes coordonnées de page doivent correspondre aux mêmes coordonnées de zone de travail avant et après le zoom. Ensuite, vous pouvez faire de l’algèbre à partir de cette équation:

(pageCoords - translation)/scale = canvasCoords

3
chad

Je tiens à mettre ici quelques informations pour ceux qui font séparément le dessin et le déplacent.

Cela peut être utile lorsque vous souhaitez stocker des zooms et la position de la fenêtre.

Voici le tiroir:

function redraw_ctx(){
   self.ctx.clearRect(0,0,canvas_width, canvas_height)
   self.ctx.save()
   self.ctx.scale(self.data.zoom, self.data.zoom) // 
   self.ctx.translate(self.data.position.left, self.data.position.top) // position second
   // Here We draw useful scene My task - image:
   self.ctx.drawImage(self.img ,0,0) // position 0,0 - we already prepared
   self.ctx.restore(); // Restore!!!
}

Avis L'échelle DOIT être la première.

Et voici zoomer:

function zoom(zf, px, py){
    // zf - is a zoom factor, which in my case was one of (0.1, -0.1)
    // px, py coordinates - is point within canvas 
    // eg. px = evt.clientX - canvas.offset().left
    // py = evt.clientY - canvas.offset().top
    var z = self.data.zoom;
    var x = self.data.position.left;
    var y = self.data.position.top;

    var nz = z + zf; // getting new zoom
    var K = (z*z + z*zf) // putting some magic

    var nx = x - ( (px*zf) / K ); 
    var ny = y - ( (py*zf) / K);

    self.data.position.left = nx; // renew positions
    self.data.position.top = ny;   
    self.data.zoom = nz; // ... and zoom
    self.redraw_ctx(); // redraw context
    }

et, bien sûr, il nous faudrait un dragueur:

this.my_cont.mousemove(function(evt){
    if (is_drag){
        var cur_pos = {x: evt.clientX - off.left,
                       y: evt.clientY - off.top}
        var diff = {x: cur_pos.x - old_pos.x,
                    y: cur_pos.y - old_pos.y}

        self.data.position.left += (diff.x / self.data.zoom);  // we want to move the point of cursor strictly
        self.data.position.top += (diff.y / self.data.zoom);

        old_pos = cur_pos;
        self.redraw_ctx();

    }


})
3
Vasiliy Stavenko

Voici une implémentation en code de la réponse de @ tatarize à l'aide de PIXI.js. J'ai une fenêtre qui regarde une partie d'une très grande image (par exemple, le style Google Maps).

$canvasContainer.on('wheel', function (ev) {

    var scaleDelta = 0.02;
    var currentScale = imageContainer.scale.x;
    var nextScale = currentScale + scaleDelta;

    var offsetX = -(mousePosOnImage.x * scaleDelta);
    var offsetY = -(mousePosOnImage.y * scaleDelta);

    imageContainer.position.x += offsetX;
    imageContainer.position.y += offsetY;

    imageContainer.scale.set(nextScale);

    renderer.render(stage);
});
  • $canvasContainer est mon conteneur HTML.
  • imageContainer est mon conteneur PIXI qui contient l'image.
  • mousePosOnImage est la position de la souris par rapport à l'image entière (pas seulement le port d'affichage).

Voici comment j'ai obtenu la position de la souris:

  imageContainer.on('mousemove', _.bind(function(ev) {
    mousePosOnImage = ev.data.getLocalPosition(imageContainer);
    mousePosOnViewport.x = ev.data.originalEvent.offsetX;
    mousePosOnViewport.y = ev.data.originalEvent.offsetY;
  },self));
2
Mirror318
if(wheel > 0) {
    this.scale *= 1.1; 
    this.offsetX -= (mouseX - this.offsetX) * (1.1 - 1);
    this.offsetY -= (mouseY - this.offsetY) * (1.1 - 1);
}
else {
    this.scale *= 1/1.1; 
    this.offsetX -= (mouseX - this.offsetX) * (1/1.1 - 1);
    this.offsetY -= (mouseY - this.offsetY) * (1/1.1 - 1);
}
2
Daniel

Vous devez obtenir le point dans l'espace mondial (opposé à l'espace d'écran) avant et après le zoom, puis le traduire par le delta.

mouse_world_position = to_world_position(mouse_screen_position);
zoom();
mouse_world_position_new = to_world_position(mouse_screen_position);
translation += mouse_world_position_new - mouse_world_position;

La position de la souris est dans l’écran, vous devez donc la transformer en espace mondial. La transformation simple devrait ressembler à ceci:

world_position = screen_position / scale - translation
0
Enhex

vous pouvez utiliser la fonction scrollto (x, y) pour gérer la position de la barre de défilement jusqu'au point à afficher après le zoom. Pour trouver la position de la souris, utilisez event.clientX et event.clientY. cela vous aidera

0
Mamo Ghandi