javascript - rotate - ¿Cómo obtener las coordenadas de MouseEvent para un elemento que tiene CSS3 Transform?
translate css animation (11)
Quiero detectar dónde ha ocurrido un MouseEvent
, en coordenadas relativas al elemento cliqueado . ¿Por qué? Porque quiero agregar un elemento secundario absolutamente posicionado en la ubicación clicada.
Sé cómo detectarlo cuando no existen transformaciones CSS3 (ver descripción a continuación). Sin embargo, cuando agrego una Transformación CSS3, mi algoritmo se rompe y no sé cómo solucionarlo.
No estoy usando ninguna biblioteca de JavaScript, y quiero entender cómo funcionan las cosas en JavaScript. Entonces, por favor, no responda con "solo use jQuery".
Por cierto, quiero una solución que funcione para todos los MouseEvents, no solo para "hacer clic". No es que importe, porque creo que todos los eventos de mouse comparten las mismas propiedades, por lo que la misma solución debería funcionar para todos ellos.
Información de fondo
Según la especificación DOM Level 2 , un MouseEvent
tiene pocas propiedades relacionadas con obtener las coordenadas del evento:
-
screenX
yscreenY
devuelven las coordenadas de la pantalla (el origen es la esquina superior izquierda del monitor del usuario) -
clientX
yclientY
devuelven las coordenadas relativas a la ventana del documento.
Por lo tanto, para encontrar la posición de MouseEvent
relativa al contenido del elemento cliqueado, debo hacer esto en matemáticas:
ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
-
ev.clientX
es la coordenada relativa a la ventana del documento -
this.getBoundingClientRect().left
es la posición del elemento relativo a la ventana del documento -
this.clientLeft
es la cantidad de borde (y barra de desplazamiento) entre el límite del elemento y las coordenadas interiores -
this.scrollLeft
es la cantidad de desplazamiento dentro del elemento
getBoundingClientRect()
, clientLeft
y scrollLeft
se especifican en CSSOM View Module .
Experimento sin CSS Transform (funciona)
¿Confuso? Pruebe la siguiente pieza de JavaScript y HTML . Al hacer clic, un punto rojo debe aparecer exactamente donde ha ocurrido el clic. Esta versión es "bastante simple" y funciona como se esperaba.
function click_handler(ev) {
var rect = this.getBoundingClientRect();
var left = ev.clientX - rect.left - this.clientLeft + this.scrollLeft;
var top = ev.clientY - rect.top - this.clientTop + this.scrollTop;
var dot = document.createElement(''div'');
dot.setAttribute(''style'', ''position:absolute; width: 2px; height: 2px; top: ''+top+''px; left: ''+left+''px; background: red;'');
this.appendChild(dot);
}
document.getElementById("experiment").addEventListener(''click'', click_handler, false);
<div id="experiment" style="border: 5px inset #AAA; background: #CCC; height: 400px; position: relative; overflow: auto;">
<div style="width: 900px; height: 2px;"></div>
<div style="height: 900px; width: 2px;"></div>
</div>
Experimento agregando una Transformación CSS (falla)
Ahora, intente agregar una transform
CSS :
#experiment {
transform: scale(0.5);
-moz-transform: scale(0.5);
-o-transform: scale(0.5);
-webkit-transform: scale(0.5);
/* Note that this is a very simple transformation. */
/* Remember to also think about more complex ones, as described below. */
}
El algoritmo no conoce las transformaciones y, por lo tanto, calcula una posición incorrecta. Además, los resultados son diferentes entre Firefox 3.6 y Chrome 12. Opera 11.50 se comporta como Chrome.
En este ejemplo, la única transformación fue la escala, por lo que pude multiplicar el factor de escala para calcular la coordenada correcta. Sin embargo, si pensamos en transformaciones arbitrarias (escala, rotar, sesgar, traducir, matriz) e incluso transformaciones anidadas (un elemento transformado dentro de otro elemento transformado), entonces realmente necesitamos una mejor manera de calcular las coordenadas.
Otra forma es colocar 3 divs en esquinas de ese elemento, que encontrar la matriz de transformación ... pero también funciona solo para elementos containerables posicionados - 4esn0k
Funciona bien si es relativo o absoluto :) solución simple
var p = $( ''.divName'' );
var position = p.position();
var left = (position.left / 0.5);
var top = (position.top / 0.5);
Además, para Webkit se puede usar el método webkitConvertPointFromPageToNode :
var div = document.createElement(''div''), scale, point;
div.style.cssText = ''position:absolute;left:-1000px;top:-1000px'';
document.body.appendChild(div);
scale = webkitConvertPointFromNodeToPage(div, new WebKitPoint(0, 0));
div.parentNode.removeChild(div);
scale.x = -scale.x / 1000;
scale.y = -scale.y / 1000;
point = webkitConvertPointFromPageToNode(element, new WebKitPoint(event.pageX * scale.x, event.pageY * scale.y));
point.x = point.x / scale.x;
point.y = point.y / scale.x;
De lejos, el más rápido. La respuesta aceptada demora alrededor de 40-70 ms en mi sitio de transformaciones 3D, por lo general requiere menos de 20 ( fiddle ):
function getOffset(event,elt){
var st=new Date().getTime();
var iterations=0;
//if we have webkit, then use webkitConvertPointFromPageToNode instead
if(webkitConvertPointFromPageToNode){
var webkitPoint=webkitConvertPointFromPageToNode(elt,new WebKitPoint(event.clientX,event.clientY));
//if it is off-element, return null
if(webkitPoint.x<0||webkitPoint.y<0)
return null;
return {
x: webkitPoint.x,
y: webkitPoint.y,
time: new Date().getTime()-st
}
}
//make full-size element on top of specified element
var cover=document.createElement(''div'');
//add styling
cover.style.cssText=''height:100%;width:100%;opacity:0;position:absolute;z-index:5000;'';
//and add it to the document
elt.appendChild(cover);
//make sure the event is in the element given
if(document.elementFromPoint(event.clientX,event.clientY)!==cover){
//remove the cover
cover.parentNode.removeChild(cover);
//we''ve got nothing to show, so return null
return null;
}
//array of all places for rects
var rectPlaces=[''topleft'',''topcenter'',''topright'',''centerleft'',''centercenter'',''centerright'',''bottomleft'',''bottomcenter'',''bottomright''];
//function that adds 9 rects to element
function addChildren(elt){
iterations++;
//loop through all places for rects
rectPlaces.forEach(function(curRect){
//create the element for this rect
var curElt=document.createElement(''div'');
//add class and id
curElt.setAttribute(''class'',''offsetrect'');
curElt.setAttribute(''id'',curRect+''offset'');
//add it to element
elt.appendChild(curElt);
});
//get the element form point and its styling
var eltFromPoint=document.elementFromPoint(event.clientX,event.clientY);
var eltFromPointStyle=getComputedStyle(eltFromPoint);
//Either return the element smaller than 1 pixel that the event was in, or recurse until we do find it, and return the result of the recursement
return Math.max(parseFloat(eltFromPointStyle.getPropertyValue(''height'')),parseFloat(eltFromPointStyle.getPropertyValue(''width'')))<=1?eltFromPoint:addChildren(eltFromPoint);
}
//this is the innermost element
var correctElt=addChildren(cover);
//find the element''s top and left value by going through all of its parents and adding up the values, as top and left are relative to the parent but we want relative to teh wall
for(var curElt=correctElt,correctTop=0,correctLeft=0;curElt!==cover;curElt=curElt.parentNode){
//get the style for the current element
var curEltStyle=getComputedStyle(curElt);
//add the top and left for the current element to the total
correctTop+=parseFloat(curEltStyle.getPropertyValue(''top''));
correctLeft+=parseFloat(curEltStyle.getPropertyValue(''left''));
}
//remove all of the elements used for testing
cover.parentNode.removeChild(cover);
//the returned object
var returnObj={
x: correctLeft,
y: correctTop,
time: new Date().getTime()-st,
iterations: iterations
}
return returnObj;
}
y también incluye el siguiente CSS en la misma página:
.offsetrect{
position: absolute;
opacity: 0;
height: 33.333%;
width: 33.333%;
}
#topleftoffset{
top: 0;
left: 0;
}
#topcenteroffset{
top: 0;
left: 33.333%;
}
#toprightoffset{
top: 0;
left: 66.666%;
}
#centerleftoffset{
top: 33.333%;
left: 0;
}
#centercenteroffset{
top: 33.333%;
left: 33.333%;
}
#centerrightoffset{
top: 33.333%;
left: 66.666%;
}
#bottomleftoffset{
top: 66.666%;
left: 0;
}
#bottomcenteroffset{
top: 66.666%;
left: 33.333%;
}
#bottomrightoffset{
top: 66.666%;
left: 66.666%;
}
Básicamente divide el elemento en 9 cuadrados, determina en cuál estaba el clic a través de document.elementFromPoint
. Luego divide eso en 9 cuadrados más pequeños, etc. hasta que sea preciso dentro de un píxel. Sé que lo comenté demasiado. La respuesta aceptada es varias veces más lenta que esta.
EDITAR: ahora es incluso más rápido, y si el usuario está en Chrome o Safari usará una función nativa diseñada para esto en lugar de los 9 sectores y puede hacerlo de manera consistente en MENOS DE 2 MILISEGUNDOS.
El comportamiento que estás experimentando es correcto y tu algoritmo no se está rompiendo. En primer lugar, las transformaciones CSS3 están diseñadas para no interferir con el modelo de caja.
Para tratar de explicar ...
Cuando aplica una Transformación CSS3 en un elemento. el elemento asume un tipo de posicionamiento relativo. En que los elementos circundantes no se ven afectados por el elemento transformado.
por ejemplo, imagine tres div en una fila horizontal. Si aplica una transformación de escala para disminuir el tamaño del centro div. Los div''s circundantes no se moverán hacia adentro para ocupar el espacio que una vez ocupó el elemento transformado.
ejemplo: http://jsfiddle.net/AshMokhberi/bWwkC/
Entonces, en el modelo de caja, el elemento en realidad no cambia de tamaño. Solo se muestran los cambios de tamaño.
También debe tener en cuenta que está aplicando una Transformación de escala, por lo que el tamaño "real" de sus elementos es en realidad el mismo que su tamaño original. Solo está cambiando su tamaño percibido.
Para explicar..
Imagine que crea un div con un ancho de 1000 px y lo reduce a la mitad del tamaño. El tamaño interno de div es todavía 1000px, no 500px.
Entonces, la posición de tus puntos es correcta en relación con el tamaño "real" del div.
Modifiqué tu ejemplo para ilustrar.
Instrucciones
- Haz clic en la div y mantén el mouse en la misma posición.
- Encuentra el punto en la posición incorrecta.
- Presione Q, el div se convertirá en el tamaño correcto.
- Mueva su mouse para encontrar el punto en la posición correcta al lugar donde hizo clic.
http://jsfiddle.net/AshMokhberi/EwQLX/
Por lo tanto, para hacer que las coordenadas del clic del ratón coincidan con la ubicación visible en el div, debe comprender que el mouse está devolviendo coordenadas basadas en la ventana, y sus desplazamientos div también se basan en su tamaño "real" .
Como el tamaño de su objeto es relativo a la ventana, la única solución es escalar las coordenadas de desplazamiento con el mismo valor de escala que su div.
Sin embargo, esto puede ser complicado en función de dónde establezca la propiedad de origen de transformación de su div. Como eso va a afectar las compensaciones.
Mira aquí.
http://jsfiddle.net/AshMokhberi/KmDxj/
Espero que esto ayude.
Esto parece funcionar muy bien para mí
var elementNewXPosition = (event.offsetX != null) ? event.offsetX : event.originalEvent.layerX;
var elementNewYPosition = (event.offsetY != null) ? event.offsetY : event.originalEvent.layerY;
Estoy trabajando en un polyfill para transfromar las coordenadas DOM. La aplicación GeometryUtils todavía no está disponible (@see https://drafts.csswg.org/cssom-view/ ). Creé un código "simple" en 2014 para transformar coordenadas, como localToGlobal, globalToLocal y localToLocal. Aún no está terminado, pero está funcionando :) Creo que lo terminaré en los próximos meses (05.07.2017), así que si todavía necesitas una API para lograr la transformación de coordenadas pruébalo: https://github.com/jsidea/jsidea https://github.com/jsidea/jsidea . Aún no es estable (pre alfa). Puedes usarlo así:
Crea tu instancia de transformación:
var transformer = jsidea.geom.Transform.create(yourElement);
El modelo de caja que desea transformar (predeterminado: "borde", será reemplazado por ENUM más adelante):
var toBoxModel = "border";
El modelo de caja de donde provienen las coordenadas de entrada (por defecto: "borde"):
var fromBoxModel = "border";
Transforme sus coordenadas globales (aquí {x: 50, y: 100, z: 0}) al espacio local. El punto resultante tiene 4 componentes: x, y, z y w.
var local = transformer.globalToLocal(50, 100, 0, toBoxModel, fromBoxModel);
He implementado algunas otras funciones como localToGlobal y localToLocal. Si quiere probar, simplemente descargue la versión de lanzamiento y use jsidea.min.js.
Descargue la primera versión aquí: descargue el código TypeScript
Siéntase libre de cambiar el código, nunca lo puse bajo ninguna licencia :)
Para obtener las coordenadas de un MouseEvent
relativo al elemento offsetX
, use offsetX
/ layerX
.
¿Has probado usar ev.layerX
o ev.offsetX
?
var offsetX = (typeof ev.offsetX == "number") ? ev.offsetX : ev.layerX || 0;
Ver también:
- Módulo de vista de CSSOM: 9 extensiones a la interfaz MouseEvent
- IE 8 mide
clientX
desde el borde de relleno del elemento en lugar del borde del contenido: GTalbot MSIE 8 Browser Bugs: event.offsetX, event.offsetY como coordenadas del mouse dentro de la caja de relleno del objetivo del elemento - MSDN: Propiedad offsetX
Si el elemento es contenedor y se coloca absoluto o relativo, puede colocarlo dentro del elemento, colocarlo relativo al elemento principal y ancho = 1px, alto = 1px, y moverse al interior del contenedor, y después de cada movimiento usar document.elementFromPoint (event. clientX, event.clientY) =))))
Puede usar la búsqueda binaria para hacerlo más rápido. se ve terrible, pero funciona
http://jsfiddle.net/3VT5N/3/ - demo
Tengo este problema y comencé a tratar de calcular la matriz. Empecé una biblioteca a su alrededor: https://github.com/ombr/referentiel
$(''.referentiel'').each ->
ref = new Referentiel(this)
$(this).on ''click'', (e)->
input = [e.pageX, e.pageY]
p = ref.global_to_local(input)
$pointer = $(''.pointer'', this)
$pointer.css(''left'', p[0])
$pointer.css(''top'', p[1])
Qué piensas ?
EDITAR: mi respuesta no ha sido probada, WIP, la actualizaré cuando la ponga en funcionamiento.
Estoy implementando un polyfill de geomtetry-interfaces . El método DOMPoint.matrixTransform
lo haré a continuación, lo que significa que deberíamos poder escribir algo como lo siguiente para asignar una coordenada de clic a un elemento DOM transformado (posiblemente anidado):
// target is the element nested somewhere inside the scene.
function multiply(target) {
let result = new DOMMatrix;
while (target && /* insert your criteria for knowing when you arrive at the root node of the 3D scene*/) {
const m = new DOMMatrix(target.style.transform)
result.preMultiplySelf(m) // see w3c DOMMatrix (geometry-interfaces)
target = target.parentNode
}
return result
}
// inside click handler
// traverse from nested node to root node and multiply on the way
const matrix = multiply(node)
const relativePoint = DOMPoint(clickX, clickY, 0, 800).matrixTransform(matrix)
relativePoint
será el punto relativo a la superficie del elemento sobre el que hizo clic.
Un DOMMatrix w3c se puede construir con un argumento de cadena de transformación CSS, lo que lo hace muy fácil de usar en JavaScript.
Desafortunadamente, esto no funciona todavía (solo Firefox tiene una implementación de interfaces geométricas, y mi polyfill aún no acepta una cadena de transformación CSS). Ver: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18
Voy a actualizar esto una vez que implemente eso y tenga un ejemplo de trabajo. ¡Solicitudes de extracción bienvenidas!
EDITAR: el valor 800
es la perspectiva de la escena, no estoy seguro de si este es el cuarto valor para el constructor DOMPoint
cuando pretendemos hacer algo como esto. Además, no estoy seguro de si debería usar preMultiplySelf
o postMultiplySelf
. Lo averiguaré una vez que lo tenga al menos funcionando (los valores pueden ser incorrectos al principio) y actualizaré mi respuesta.