personalizado - CSS3 haciendo zoom en el cursor del mouse
hover css (2)
Mi objetivo es crear un complemento que permita operaciones de zoom y panorámica en un área de página, al igual que funciona Google Maps actualmente (es decir: desplazarse con el mouse = acercar / alejar el área, hacer clic y mantener y mover y liberar = paneo )
Al desplazarme, deseo tener una operación de zoom centrada en el cursor del mouse.
Para esto, uso transformaciones de matriz CSS3 sobre la marcha. La única restricción, aunque obligatoria, es que no puedo usar otra cosa que las transformaciones de escala y traducción CSS3, con un origen de transformación de 0px 0px.
La panorámica está fuera del alcance de mi pregunta, ya que ya la tengo funcionando. Cuando se trata de hacer zoom, me cuesta descubrir dónde está el problema en mi código de JavaScript.
El problema debe estar en algún lugar de la función MouseZoom.prototype.zoom, en el cálculo de la traducción en el eje xy el eje y.
Primero, aquí está mi código 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>
Como puede ver, estoy usando Jquery y el complemento de jquery mouse wheel de Brandon Aaron, que se puede encontrar aquí: https://github.com/brandonaaron/jquery-mousewheel/
Aquí está el contenido del archivo 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);
}
Utilizar transform
para obtener un comportamiento de zoom de google maps en un elemento div
pareció una idea interesante, así que pagué un poco =)
Utilizaría transform-origin
(y sus atributos hermanos para la compatibilidad del navegador) para ajustar el zoom a la ubicación del mouse en el div que está escalando. Creo que esto podría hacer lo que quieras. Puse algunos ejemplos en violín para ilustración:
- ejemplo 1: acercar y alejar el origen de la transformación
- Ejemplo 2: ampliar el marco de desplazamiento de origen de transformación y desplazamiento con traducción
- Ejemplo 3: ejemplo 2 + alejamiento limitado a los bordes del marco original
- ejemplo 4: ejemplo 3 + marco principal con desbordamiento oculto
Ajustando el transform-origin
la transform-origin
Entonces, en la función applyTransformations
usted puede ajustar el transform-origin
la transform-origin
dinámicamente desde la imageX
y la imageY
, si pasamos estos valores desde la función MouseZoom
(detector de mouse).
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);
(En este primer ejemplo , acabo de usar su translateX
y translateY
en Transformations
para pasar la ubicación del mouse en el elemento div; en el segundo ejemplo, lo originX
a originX
y originY
para diferenciarlo de las variables de traducción).
Cálculo del origen de la transformación
En su MouseZoom
podemos calcular la ubicación del origen simplemente con 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);
}
De modo que esto funcionará perfectamente si aleja completamente el zoom antes de acercarse a una posición diferente. Pero para poder cambiar el origen del zoom en cualquier nivel de zoom, podemos combinar el origen y la funcionalidad de traducción.
Cambiando el marco de acercamiento (extendiendo mi respuesta original)
El origen de la transformación en la imagen todavía se calcula de la misma manera, pero usamos un translateX y translateY separados para cambiar el marco del zoom (aquí originX
dos nuevas variables que nos ayudan a hacer el truco, así que ahora tenemos originX
, originY
, translateX
y 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);
}
Para este ejemplo, ajusté un poco más el guión original y agregué el segundo ejemplo de violín .
Ahora acercamos y alejamos el cursor del mouse desde cualquier nivel de zoom. Pero debido al cambio de marco, terminamos moviendo el div original ("midiendo la tierra") ... lo cual se ve raro si trabajas con un objeto de ancho y alto limitado (acercas un extremo, alejas el zoom en otro extremo, y avanzamos como un inchworm).
Evitando el efecto "inchworm"
Para evitar esto, podría, por ejemplo, agregar limitaciones para que el borde izquierdo de la imagen no se mueva a la derecha de su coordenada x original, el borde superior de la imagen no se mueva más abajo que su posición original y así sucesivamente para los otros dos bordes. Pero luego el zoom / out no estará completamente vinculado al cursor, sino también al borde de la imagen (notará que la imagen se desliza en su lugar) en el ejemplo 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;
}
}
Otra opción (un poco chiflada) sería restablecer la traducción del fotograma cuando se aleja por completo (escala == 1).
Sin embargo, no tendría este problema si va a tratar con elementos continuos (borde izquierdo y derecho y borde superior e inferior unidos) o simplemente con elementos extremadamente grandes.
Para terminar todo con un toque agradable, podemos agregar un cuadro principal con un desbordamiento oculto alrededor de nuestro objeto de escala. Entonces, el área de la imagen no cambia con el zoom. Ver jsfiddle ejemplo 4 .
Hicimos una biblioteca de reacción para esto: https://www.npmjs.com/package/react-map-interaction
Maneja el zoom y la panorámica y funciona en dispositivos móviles y de escritorio.
La fuente es bastante corta y legible, pero para responder a su pregunta aquí más directamente, usamos esta transformación 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
Uno de los principales trucos es calcular correctamente la diferencia entre el estado inicial del puntero / mouse hacia abajo y el estado actual cuando se produce un movimiento de toque / mouse. Cuando el mouse baja, captura las coordenadas. Luego, en cada movimiento del mouse (hasta que el mouse suba), calcule la diferencia en la distancia. Esa diferencia es lo que necesita para compensar la traducción para asegurarse de que el punto inicial debajo del cursor sea el punto focal del zoom.