Quiero poder acercar el punto debajo del mouse en un lienzo HTML 5, como hacer zoom en Google Maps . ¿Cómo puedo lograr eso?
Quiero poder acercar el punto debajo del mouse en un lienzo HTML 5, como hacer zoom en Google Maps . ¿Cómo puedo lograr eso?
Respuestas:
La mejor solución es simplemente mover la posición de la ventana gráfica en función del cambio en el zoom. El punto de zoom es simplemente el punto en el zoom anterior y el nuevo zoom que desea que permanezca igual. Es decir, la vista ampliada previamente y la vista ampliada tienen el mismo punto de zoom relativo a la vista. Dado que estamos escalando en relación con el origen. Puede ajustar la posición de la vista en consecuencia:
scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);
Entonces, realmente puedes desplazarte hacia abajo y hacia la derecha cuando acercas, por un factor de cuánto acercaste, en relación con el punto en el que hiciste zoom.
Finalmente lo resolvió:
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 clave, como señaló @Tatarize , es calcular la posición del eje de manera que el punto de zoom (puntero del mouse) permanezca en el mismo lugar después del zoom.
Originalmente el mouse está a una distancia mouse/scale
de la esquina, queremos que el punto debajo del mouse permanezca en el mismo lugar después del zoom, pero esto está mouse/new_scale
lejos de la esquina. Por lo tanto, necesitamos cambiar las origin
(coordenadas de la esquina) para dar cuenta de esto.
originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
scale *= zoom
El código restante debe aplicar la escala y traducir al contexto de dibujo para que su origen coincida con la esquina del lienzo.
Este es en realidad un problema muy difícil (matemáticamente), y estoy trabajando en lo mismo casi. Hice una pregunta similar en Stackoverflow pero no obtuve respuesta, pero publiqué en DocType (StackOverflow para HTML / CSS) y obtuve una respuesta. Échale un vistazo http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example
Estoy en medio de la construcción de un complemento jQuery que hace esto (zoom de estilo de Google Maps usando transformaciones CSS3). Tengo el zoom en el bit del cursor del mouse funcionando bien, aún tratando de descubrir cómo permitir que el usuario arrastre el lienzo como lo puede hacer en Google Maps. Cuando lo haga funcionar, publicaré el código aquí, pero revise el enlace de arriba para la parte del mouse-zoom-to-point.
No me di cuenta de que había métodos de escala y traducción en el contexto de Canvas, puedes lograr lo mismo usando CSS3, por ejemplo. usando 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)');
Asegúrese de establecer el CSS3 transform-origin en 0, 0 (-moz-transform-origin: 0 0). El uso de CSS3 transform le permite acercar cualquier cosa, solo asegúrese de que el contenedor DIV esté configurado para desbordarse: oculto para evitar que los bordes con zoom se derramen por los lados.
Depende de usted si utiliza transformaciones CSS3, o los métodos de escala y traducción propios del lienzo, pero consulte el enlace anterior para ver los cálculos.
Actualización: Meh! Voy a publicar el código aquí en lugar de hacer que sigas un enlace:
$(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;
});
});
Por supuesto, deberá adaptarlo para usar la escala del lienzo y los métodos de traducción.
Actualización 2: Acabo de notar que estoy usando transform-origin junto con translate. Me las arreglé para implementar una versión que solo usa la escala y la traducción por su cuenta, compruébelo aquí http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Espere a que se descarguen las imágenes y luego use su rueda del mouse para hacer zoom, también es compatible con la panorámica arrastrando la imagen. Está utilizando transformaciones CSS3, pero debería poder usar los mismos cálculos para su lienzo.
Me encontré con este problema usando c ++, que probablemente no debería haber tenido, solo comencé a usar matrices OpenGL para empezar ... de todos modos, si estás usando un control cuyo origen es la esquina superior izquierda, y quieres paneo / zoom Al igual que Google Maps, aquí está el diseño (usando Allegro como mi controlador de eventos):
// 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;
}
Me gusta la respuesta de Tatarize , pero proporcionaré una alternativa. Este es un problema trivial de álgebra lineal, y el método que presento funciona bien con panorámica, zoom, sesgo, etc. Es decir, funciona bien si su imagen ya está transformada.
Cuando se escala una matriz, la escala está en el punto (0, 0). Entonces, si tiene una imagen y la escala en un factor de 2, el punto inferior derecho se duplicará en las direcciones x e y (usando la convención de que [0, 0] es la esquina superior izquierda de la imagen).
Si, en cambio, desea ampliar la imagen sobre el centro, entonces la solución es la siguiente: (1) traduzca la imagen de modo que su centro esté en (0, 0); (2) escalar la imagen por factores x e y; (3) traduzca la imagen de nuevo. es decir
myMatrix
.translate(image.width / 2, image.height / 2) // 3
.scale(xFactor, yFactor) // 2
.translate(-image.width / 2, -image.height / 2); // 1
Más abstracto, la misma estrategia funciona para cualquier punto. Si, por ejemplo, desea escalar la imagen en un punto P:
myMatrix
.translate(P.x, P.y)
.scale(xFactor, yFactor)
.translate(-P.x, -P.y);
Y, por último, si la imagen ya está transformada de alguna manera (por ejemplo, si está rotada, sesgada, traducida o escalada), entonces la transformación actual necesita ser preservada. Específicamente, la transformación definida anteriormente necesita ser multiplicada posteriormente (o multiplicada a la derecha) por la transformación actual.
myMatrix
.translate(P.x, P.y)
.scale(xFactor, yFactor)
.translate(-P.x, -P.y)
.multiply(myMatrix);
Ahí tienes. Aquí hay un plunk que muestra esto en acción. Desplácese con la rueda del mouse sobre los puntos y verá que permanecen consistentemente. (Probado solo en Chrome.) Http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview
Aquí está mi solución para una imagen orientada al centro:
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>
Aquí hay una forma alternativa de hacerlo que usa setTransform () en lugar de scale () y translate (). Todo se almacena en el mismo objeto. Se supone que el lienzo está en 0,0 en la página, de lo contrario, deberá restar su posición de los códigos de la página.
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);
};
Código de acompañamiento para manejar la panorámica:
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
};
};
Para obtener la respuesta usted mismo, considere que las mismas coordenadas de la página deben coincidir con las mismas coordenadas del lienzo antes y después del zoom. Entonces puedes hacer algo de álgebra a partir de esta ecuación:
(pageCoords - traducción) / scale = canvasCoords
Quiero poner aquí información para aquellos que hacen un dibujo separado de la imagen y la mueven para acercarla.
Esto puede ser útil cuando desea almacenar los zooms y la posición de la ventana gráfica.
Aquí está el cajón:
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!!!
}
La escala de aviso DEBE ser la primera .
Y aquí está el 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
}
y, por supuesto, necesitaríamos un arrastrador:
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();
}
})
Aquí hay una implementación de código de la respuesta de @ tatarize, usando PIXI.js. Tengo una ventana gráfica que mira parte de una imagen muy grande (por ejemplo, el estilo de 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
es mi contenedor htmlimageContainer
es mi contenedor PIXI que tiene la imagen en él.mousePosOnImage
es la posición del mouse con respecto a la imagen completa (no solo el puerto de vista).Así es como obtuve la posición del mouse:
imageContainer.on('mousemove', _.bind(function(ev) {
mousePosOnImage = ev.data.getLocalPosition(imageContainer);
mousePosOnViewport.x = ev.data.originalEvent.offsetX;
mousePosOnViewport.y = ev.data.originalEvent.offsetY;
},self));
Debe obtener el punto en el espacio mundial (en oposición al espacio de la pantalla) antes y después del zoom, y luego traducir por el 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 posición del mouse está en el espacio de la pantalla, por lo que debe transformarla en espacio mundial. La transformación simple debería ser similar a esto:
world_position = screen_position / scale - translation
puede usar la función scrollto (x, y) para manejar la posición de la barra de desplazamiento hasta el punto que necesita que se muestre después del zoom. para encontrar la posición del mouse, use event.clientX y event.clientY. Esto te ayudara
Una cosa importante ... si tienes algo como:
body {
zoom: 0.9;
}
Necesitas hacer lo mejor en el lienzo:
canvas {
zoom: 1.1;
}