Javascript 如何获取具有 CSS3 变换的元素的 MouseEvent 坐标?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/6773481/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to get the MouseEvent coordinates for an element that has CSS3 Transform?
提问by Denilson Sá Maia
I want to detect wherea MouseEvent
has occurred, in coordinates relative to the clicked element. Why? Because I want to add an absolutely positioned child element at the clicked location.
我想检测,其中一个MouseEvent
已经发生,在相对于点击的元素坐标。为什么?因为我想在点击的位置添加一个绝对定位的子元素。
I know how to detect it when no CSS3 transformations exist (see description below). However, when I add a CSS3 Transform, then my algorithm breaks, and I don't know how to fix it.
当不存在 CSS3 转换时,我知道如何检测它(见下面的描述)。但是,当我添加 CSS3 转换时,我的算法会中断,我不知道如何修复它。
I'm not using any JavaScript library, and I want to understand how things work in plain JavaScript. So, please, don't answer with "just use jQuery".
我没有使用任何 JavaScript 库,我想了解纯 JavaScript 中的工作原理。所以,请不要回答“只使用 jQuery”。
By the way, I want a solution that works for all MouseEvents, not just "click". Not that it matters, because I believe all mouse events share the same properties, thus the same solution should work for all of them.
顺便说一句,我想要一个适用于所有 MouseEvents 的解决方案,而不仅仅是“点击”。这并不重要,因为我相信所有鼠标事件都具有相同的属性,因此相同的解决方案应该适用于所有鼠标事件。
Background information
背景资料
According to DOM Level 2 specification, a MouseEvent
has few properties related to getting the event coordinates:
根据DOM Level 2 规范,aMouseEvent
有一些与获取事件坐标相关的属性:
screenX
andscreenY
return the screen coordinates (the origin is the top-left corner of user's monitor)clientX
andclientY
return the coordinates relative the document viewport.
screenX
并screenY
返回屏幕坐标(原点为用户显示器的左上角)clientX
并clientY
返回相对于文档视口的坐标。
Thus, in order to find the position of the MouseEvent
relative to the clicked element content, I must do this math:
因此,为了找到MouseEvent
相对于点击元素内容的位置,我必须做这个数学运算:
ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
ev.clientX
is the coordinate relative to the document viewportthis.getBoundingClientRect().left
is the position of the element relative to the document viewportthis.clientLeft
is the amount of border (and scrollbar) between the element boundary and the inner coordinatesthis.scrollLeft
is the amount of scrolling inside the element
ev.clientX
是相对于文档视口的坐标this.getBoundingClientRect().left
是元素相对于文档视口的位置this.clientLeft
是元素边界和内部坐标之间的边界(和滚动条)的数量this.scrollLeft
是元素内部的滚动量
getBoundingClientRect()
, clientLeft
and scrollLeft
are specified at CSSOM View Module.
getBoundingClientRect()
,clientLeft
并scrollLeft
在CSSOM 视图模块中指定。
Experiment without CSS Transform (it works)
没有 CSS 转换的实验(它有效)
Confusing? Try the following piece of JavaScript and HTML. Upon clicking, a red dot should appear exactly where the click has happened. This version is "quite simple" and works as expected.
令人困惑?尝试以下 JavaScript 和 HTML 片段。点击后,应该会在点击发生的地方出现一个红点。这个版本“非常简单”并且按预期工作。
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>
Experiment adding a CSS Transform (it fails)
尝试添加 CSS 转换(失败)
Now, try adding a CSS transform
:
#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. */
}
The algorithm doesn't know about the transformations, and thus calculates a wrong position. What's more, the results are different between Firefox 3.6 and Chrome 12. Opera 11.50 behaves just like Chrome.
该算法不知道转换,因此计算出错误的位置。更重要的是,Firefox 3.6 和 Chrome 12 之间的结果是不同的。Opera 11.50 的行为就像 Chrome。
In this example, the only transformation was scaling, so I could multiply the scaling factor to calculate the correct coordinate. However, if we think about arbitrary transformations (scale, rotate, skew, translate, matrix), and even nested transformations (a transformed element inside another transformed element), then we really need a better way to calculate the coordinates.
在这个例子中,唯一的转换是缩放,所以我可以乘以缩放因子来计算正确的坐标。然而,如果我们考虑任意变换(缩放、旋转、倾斜、平移、矩阵),甚至嵌套变换(另一个变换元素内的变换元素),那么我们真的需要一种更好的方法来计算坐标。
回答by AshHeskes
The behaviour you are experiencing is correct, and your algorithm isn't breaking. Firstly CSS3 Transforms are designed not to interfere with the box model.
您遇到的行为是正确的,并且您的算法没有中断。首先,CSS3 变换旨在不干扰盒模型。
To try and explain...
尝试解释...
When you apply a CSS3 Transform on an element. the Element assumes a kind of relative positioning. In that the surrounding elements are not effected by the transformed element.
当您在元素上应用 CSS3 转换时。元素假定一种相对定位。因为周围的元素不受转换元素的影响。
e.g. imagine three div's in a horizontal row. If you apply a scale transform to decrease the size of the centre div. The surrounding div's will not move inwards to occupy the space that was once occupied the transformed element.
例如,想象一下水平行中的三个 div。如果您应用缩放变换来减小中心 div 的大小。周围的 div 不会向内移动以占据曾经占据变换元素的空间。
example: http://jsfiddle.net/AshMokhberi/bWwkC/
示例:http: //jsfiddle.net/AshMokhberi/bWwkC/
So in the box model, the element does not actually change size. Only it's rendered size changes.
所以在盒模型中,元素实际上并没有改变大小。只有它呈现的大小发生变化。
You also have to keep in mind that you are applying a scale Transform, so your elements "real" size is actually the same as it's original size. You are only changing it's perceived size.
您还必须记住,您正在应用缩放变换,因此您的元素“真实”大小实际上与其原始大小相同。你只是改变它的感知大小。
To explain..
解释..
Imagine you create a div with a width of 1000px and scale it down to 1/2 the size. The internal size of the div is still 1000px, not 500px.
想象一下,您创建了一个宽度为 1000px 的 div 并将其缩小到 1/2 的大小。div 的内部大小仍然是 1000px,而不是 500px。
So the position of your dots are correct relative to the div's "real" size.
所以你的点的位置相对于 div 的“真实”大小是正确的。
I modified your example to illustrate.
我修改了你的例子来说明。
Instructions
指示
- Click the div and keep you mouse in the same position.
- Find the dot in the wrong position.
- Press Q, the div will become the correct size.
- Move your mouse to find the dot in the correct position to where you clicked.
- 单击 div 并将鼠标保持在同一位置。
- 在错误的位置找到点。
- 按Q,div会变成正确的大小。
- 移动鼠标以在您单击的位置找到正确位置的点。
http://jsfiddle.net/AshMokhberi/EwQLX/
http://jsfiddle.net/AshMokhberi/EwQLX/
So in order to make the mouse clicks co-ordinates match the visible location on the div, you need to understand that the mouse is giving back co-ordinates based on the window, and your div offsets are also based on its "real" size.
因此,为了使鼠标点击坐标与 div 上的可见位置匹配,您需要了解鼠标是根据窗口返回坐标,而您的 div 偏移量也基于其“真实”大小.
As your object size is relative to the window the only solution is to scale the offset co-ordinates by the same scale value as your div.
由于您的对象大小是相对于窗口的,唯一的解决方案是按与 div 相同的比例值缩放偏移坐标。
However this can get tricky based on where you set the Transform-origin property of your div. As that is going to effect the offsets.
但是,根据您设置 div 的 Transform-origin 属性的位置,这可能会变得棘手。因为这会影响偏移量。
See here.
看这里。
http://jsfiddle.net/AshMokhberi/KmDxj/
http://jsfiddle.net/AshMokhberi/KmDxj/
Hope this helps.
希望这可以帮助。
回答by 4esn0k
if element is container and positioned absolute or relative, you can place inside of it element, position it relative to parent and width = 1px, height = 1px, and move to inside of container, and after each move use document.elementFromPoint(event.clientX, event.clientY) =))))
如果元素是容器并且绝对或相对定位,则可以将元素放入其中,相对于父元素进行定位,宽度= 1px,高度= 1px,然后移动到容器内部,每次移动后使用 document.elementFromPoint(event.elementFromPoint(event. clientX, event.clientY) =))))
You can use binary search to make it faster. looks terrible, but it works
您可以使用二分搜索来加快速度。看起来很糟糕,但它有效
回答by 4esn0k
another way is place 3 divs in corners of that element, than find transform matrix ... but is also works only for positioned containerable elements – 4esn0k
另一种方法是在该元素的角落放置 3 个 div,而不是找到变换矩阵......但也仅适用于定位的容器元素 – 4esn0k
回答by 4esn0k
Also, for Webkit webkitConvertPointFromPageToNodemethod can be used:
此外,对于 Webkit,可以使用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;
回答by markasoftware
BY FAR the fastest. The accepted answer takes about 40-70 ms on my 3d transforms site, this usually takes less than 20 (fiddle):
迄今为止最快的。在我的 3d 转换站点上,接受的答案大约需要 40-70 毫秒,这通常需要不到 20(小提琴):
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;
}
and also include the following CSS in the same page:
并在同一页面中包含以下 CSS:
.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%;
}
It essentially splits the element into 9 squares, determines which one the click was in via document.elementFromPoint
. It then splits that into 9 smaller squares, etc until it is accurate to within a pixel. I know I over-commented it. The accepted answer is several times slower than this.
它本质上将元素分成 9 个方块,确定点击是通过哪个方块document.elementFromPoint
。然后将其拆分为 9 个较小的正方形等,直到精确到一个像素内。我知道我过度评论了。接受的答案比这慢几倍。
EDIT: It is now even faster, and if the user is in Chrome or Safari it will use a native function designed for this instead of the 9 sectors thingy and can do it consistently in LESS THAN 2 MILLISECONDS!
编辑:它现在更快,如果用户在 Chrome 或 Safari 中,它将使用为此设计的本机功能而不是 9 个扇区,并且可以在不到 2 毫秒内始终如一地完成!
回答by Garrett
To get the coordinates of a MouseEvent
relative to the clicked element, use offsetX
/ layerX
.
要获取MouseEvent
相对于单击元素的坐标,请使用offsetX
/ layerX
。
Have you tried using ev.layerX
or ev.offsetX
?
你试过使用ev.layerX
或ev.offsetX
吗?
var offsetX = (typeof ev.offsetX == "number") ? ev.offsetX : ev.layerX || 0;
See also:
也可以看看:
- CSSOM View Module: 9 Extensions to the MouseEvent Interface
- IE 8 measures
clientX
from the element's padding edge instead of the content edge: GTalbot MSIE 8 Browser Bugs: event.offsetX, event.offsetY as mouse coordinates inside element target's padding-box - MSDN: offsetX Property
- CSSOM 视图模块:MouseEvent 接口的 9 个扩展
- IE 8
clientX
从元素的填充边缘而不是内容边缘进行测量:GTalbot MSIE 8 浏览器错误:event.offsetX、event.offsetY 作为元素目标填充框内的鼠标坐标 - MSDN:offsetX 属性
回答by bryan
This seems to work really well for me
这对我来说似乎非常有效
var elementNewXPosition = (event.offsetX != null) ? event.offsetX : event.originalEvent.layerX;
var elementNewYPosition = (event.offsetY != null) ? event.offsetY : event.originalEvent.layerY;
回答by trusktr
EDIT: my answer is untested, WIP, I will update when I get it working.
编辑:我的答案未经测试,WIP,当我开始工作时我会更新。
I'm implementing a polyfill of the geomtetry-interfaces. The DOMPoint.matrixTransform
method I will make next, which means we should be able to write something like the following in order to map a click coordinate onto a transformed (possiblly nested) DOM element:
我正在实现geomtetry-interfaces的polyfill。DOMPoint.matrixTransform
我接下来将使用的方法,这意味着我们应该能够编写如下内容,以便将点击坐标映射到转换后的(可能是嵌套的)DOM 元素上:
// 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
will be the point relative to the element's surface that you clicked on.
relativePoint
将是相对于您单击的元素表面的点。
A w3c DOMMatrix can be constructed with a CSS transform string argument, which makes it super easy to use in JavaScript.
w3c DOMMatrix 可以用 CSS 转换字符串参数构造,这使得它在 JavaScript 中非常容易使用。
Unfortunately, this isn't working yet (only Firefox has a geometry-interfaces implementation, and my polyfill does not yet accept a CSS transform string). See: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18
不幸的是,这还不起作用(只有 Firefox 有一个几何接口实现,我的 polyfill 还不接受 CSS 转换字符串)。参见:https: //github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18
I will update this once I implement that and have a working example. Pull requests welcome!
一旦我实现它并有一个工作示例,我将更新它。欢迎拉取请求!
EDIT: the value 800
is the scene's perspective, I'm not sure if this is what the fourth value for the DOMPoint
constructor should be when we intend to do something like this. Also, I'm not sure if I should use preMultiplySelf
or postMultiplySelf
. I'll find out once I get it at least working (values may be incorrect at first) and will update my answer.
编辑:该值800
是场景的视角,DOMPoint
当我们打算做这样的事情时,我不确定这是否是构造函数的第四个值应该是什么。另外,我不确定是否应该使用preMultiplySelf
或postMultiplySelf
。一旦我得到它至少工作(一开始可能不正确),我会发现并更新我的答案。
回答by jbenker
I am working on a polyfill to transfrom DOM coordinates. The GeometryUtils api is not available yet (@see https://drafts.csswg.org/cssom-view/). I created a "simple" code in 2014 to transform coordinates, like localToGlobal, globalToLocal and localToLocal. Its not finished yet, but its working :) I think I will finish it in the coming months (05.07.2017), so if you still need a API to accomplish coordinate transformation give it a try: https://github.com/jsidea/jsideajsidea core library. Its not stable yet(pre alpha). You can use it like that:
我正在研究一个 polyfill 来转换 DOM 坐标。GeometryUtils api 尚不可用(@see https://drafts.csswg.org/cssom-view/)。我在 2014 年创建了一个“简单”代码来转换坐标,如 localToGlobal、globalToLocal 和 localToLocal。它还没有完成,但它的工作:) 我想我会在未来几个月(05.07.2017)完成它,所以如果你仍然需要一个 API 来完成坐标转换,请试一试:https: //github.com/ jsidea/jsidea jsidea 核心库。它还不稳定(pre alpha)。你可以这样使用它:
Create your transform instance:
创建您的转换实例:
var transformer = jsidea.geom.Transform.create(yourElement);
The box model you want to transform to (default:"border", will be replaced by ENUM's later on):
你想要转换的盒子模型(默认:“border”,稍后将被 ENUM 替换):
var toBoxModel = "border";
The box model where your input coordinates coming from (default:"border"):
您的输入坐标来自的框模型(默认:“边框”):
var fromBoxModel = "border";
Transform your global coordinates (here {x:50, y:100, z: 0}) to local space. The resulting point has 4 components: x, y, z and w.
将您的全局坐标(此处为 {x:50, y:100, z: 0})转换为本地空间。结果点有 4 个分量:x、y、z 和 w。
var local = transformer.globalToLocal(50, 100, 0, toBoxModel, fromBoxModel);
I have implemented some other functions like localToGlobal and localToLocal. If you want to give a try, just download the release build and use the jsidea.min.js.
我已经实现了一些其他功能,如 localToGlobal 和 localToLocal。如果您想尝试一下,只需下载发布版本并使用 jsidea.min.js。
Download the first release here: Download TypeScript code
在此处下载第一个版本:下载 TypeScript 代码
Feel free to change the code, I never put it under any license :)
随意更改代码,我从未将其置于任何许可之下:)
回答by Luc Boissaye
I have this issue and started trying to compute the matrix. I started a library around it: https://github.com/ombr/referentiel
我有这个问题并开始尝试计算矩阵。我围绕它创建了一个库: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])
What do you think ?
你怎么认为 ?