Je veux pouvoir zoomer sur le point sous la souris dans un canevas HTML 5, comme un zoom sur Google Maps . Comment puis-je y parvenir?
Je veux pouvoir zoomer sur le point sous la souris dans un canevas HTML 5, comme un zoom sur Google Maps . Comment puis-je y parvenir?
Réponses:
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 du nouveau zoom que vous souhaitez conserver. C'est-à-dire que la fenêtre pré-zoomée et la fenêtre post-zoomée ont le même point de zoom par rapport à la fenêtre. É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);
Donc, vraiment, vous pouvez simplement faire un panoramique vers le bas et vers la droite lorsque vous effectuez un zoom avant, en fonction du facteur de zoom avant, par rapport au point sur lequel vous avez zoomé.
Enfin résolu:
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 l' a souligné @Tatarize , est de 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 à une distance mouse/scale
du coin, nous voulons que le point sous la souris reste au même endroit après le zoom, mais celui-ci est mouse/new_scale
éloigné du coin. Par conséquent, nous devons décaler les origin
(coordonnées du coin) pour en tenir compte.
originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
scale *= zoom
Le code restant doit ensuite appliquer la mise à l'échelle et se traduire dans le contexte de dessin afin que son origine coïncide avec le coin du canevas.
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 obtenu aucune réponse, mais j'ai posté dans DocType (StackOverflow pour HTML / CSS) et j'ai obtenu une réponse. Découvrez-le http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example
Je suis en train de créer un plugin jQuery qui fait cela (zoom de style Google Maps à l'aide des transformations CSS3). J'ai le zoom sur le curseur de la souris qui fonctionne bien, en essayant toujours de comprendre comment permettre à l'utilisateur de faire glisser la toile comme vous pouvez le faire dans Google Maps. Quand je le ferai fonctionner, je publierai le code ici, mais consultez le lien ci-dessus pour la partie souris-zoom-point.
Je ne savais pas qu'il y avait des méthodes de mise à l'échelle et de traduction sur le contexte de Canvas, vous pouvez obtenir la même chose en utilisant 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 de la transformation CSS3 sur 0, 0 (-moz-transform-origin: 0 0). L'utilisation de la transformation CSS3 vous permet de zoomer sur n'importe quoi, assurez-vous simplement que le conteneur DIV est défini sur overflow: hidden pour empêcher les bords zoomés de se répandre sur les côtés.
Que vous utilisiez les transformations CSS3 ou les propres méthodes de mise à l'échelle et de traduction du canevas dépend de vous, mais vérifiez le lien ci-dessus pour 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 du canevas et les méthodes de traduction.
Mise à jour 2: Je viens de remarquer que j'utilise transform-origin avec translate. J'ai réussi à mettre en œuvre une version qui utilise simplement l'échelle et la traduction seule, vérifiez-la ici http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Attendez que les images soient téléchargées puis utilisez votre molette de la souris pour zoomer, prend également en charge le panoramique en faisant glisser l'image autour. Il utilise des transformations CSS3, mais vous devriez pouvoir utiliser les mêmes calculs pour votre canevas.
J'ai rencontré ce problème en utilisant C ++, que je n'aurais probablement pas dû avoir Je viens d'utiliser des matrices OpenGL pour commencer ... de toute façon, si vous utilisez un contrôle dont l'origine est le coin supérieur gauche, et que vous voulez 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;
}
J'aime la réponse de Tatarize , mais je vais proposer une alternative. C'est un problème trivial d'algèbre linéaire, et la méthode que je présente fonctionne bien avec le panoramique, le zoom, l'inclinaison, etc. Autrement dit, 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). Donc, si vous avez une image et que vous la mettez à l'échelle d'un facteur 2, le point en bas à droite doublera dans les directions x et y (en utilisant la convention selon laquelle [0, 0] est le coin supérieur gauche de l'image).
Si, à la place, vous souhaitez agrandir l'image autour du centre, la solution est la suivante: (1) traduisez l'image de telle sorte que son centre soit à (0, 0); (2) mettre l'image à l'échelle 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 mettre l'image à l'échelle en un point P:
myMatrix
.translate(P.x, P.y)
.scale(xFactor, yFactor)
.translate(-P.x, -P.y);
Et 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 actuelle doit être préservée. Plus précisément, la transformation définie ci-dessus doit être post-multipliée (ou multipliée à droite) par la transformation actuelle.
myMatrix
.translate(P.x, P.y)
.scale(xFactor, yFactor)
.translate(-P.x, -P.y)
.multiply(myMatrix);
Voilà. Voici un plunk qui montre cela en action. Faites défiler avec la molette de la souris sur les points et vous verrez qu'ils restent toujours en place. (Testé dans Chrome uniquement.) Http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview
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>
Voici une autre façon de le faire qui utilise setTransform () au lieu 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 des 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 canevas avant et après le zoom. Ensuite, vous pouvez faire de l'algèbre à partir de cette équation:
(pageCoords - translation) / scale = canvasCoords
Je veux mettre ici quelques informations pour ceux qui font séparément le dessin de l'image et le zoom en mouvement.
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!!!
}
L' échelle de notification 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, nous aurions besoin d'un dragger:
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();
}
})
Voici une implémentation de code de la réponse de @ tatarize, en utilisant 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 de vue).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));
Vous devez obtenir le point dans l'espace mondial (par opposition à l'espace écran) avant et après le zoom, puis 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'espace d'écran, vous devez donc la transformer en espace mondial. La transformation simple devrait être similaire à ceci:
world_position = screen_position / scale - translation
vous pouvez utiliser la fonction scrollto (x, y) pour gérer la position de la barre de défilement jusqu'au point que vous devez afficher après le zoom.pour trouver la position de la souris, utilisez event.clientX et event.clientY. cela t'aidera
Une chose importante ... si vous avez quelque chose comme:
body {
zoom: 0.9;
}
Vous devez faire la chose équivalente dans la toile:
canvas {
zoom: 1.1;
}