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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 23:07:36  来源:igfitidea点击:

How to get the MouseEvent coordinates for an element that has CSS3 Transform?

javascripthtmlcsstransform

提问by Denilson Sá Maia

I want to detect wherea MouseEventhas 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 MouseEventhas few properties related to getting the event coordinates:

根据DOM Level 2 规范,aMouseEvent有一些与获取事件坐标相关的属性:

  • screenXand screenYreturn the screen coordinates (the origin is the top-left corner of user's monitor)
  • clientXand clientYreturn the coordinates relative the document viewport.
  • screenXscreenY返回屏幕坐标(原点为用户显示器的左上角)
  • clientXclientY返回相对于文档视口的坐标。

Thus, in order to find the position of the MouseEventrelative to the clicked element content, I must do this math:

因此,为了找到MouseEvent相对于点击元素内容的位置,我必须做这个数学运算:

ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
  • ev.clientXis the coordinate relative to the document viewport
  • this.getBoundingClientRect().leftis the position of the element relative to the document viewport
  • this.clientLeftis the amount of border (and scrollbar) between the element boundary and the inner coordinates
  • this.scrollLeftis the amount of scrolling inside the element
  • ev.clientX是相对于文档视口的坐标
  • this.getBoundingClientRect().left是元素相对于文档视口的位置
  • this.clientLeft是元素边界和内部坐标之间的边界(和滚动条)的数量
  • this.scrollLeft是元素内部的滚动量

getBoundingClientRect(), clientLeftand scrollLeftare specified at CSSOM View Module.

getBoundingClientRect()clientLeftscrollLeftCSSOM 视图模块中指定。

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:

现在,尝试添加一个 CSStransform

#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

指示

  1. Click the div and keep you mouse in the same position.
  2. Find the dot in the wrong position.
  3. Press Q, the div will become the correct size.
  4. Move your mouse to find the dot in the correct position to where you clicked.
  1. 单击 div 并将鼠标保持在同一位置。
  2. 在错误的位置找到点。
  3. 按Q,div会变成正确的大小。
  4. 移动鼠标以在您单击的位置找到正确位置的点。

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

您可以使用二分搜索来加快速度。看起来很糟糕,但它有效

http://jsfiddle.net/3VT5N/3/- demo

http://jsfiddle.net/3VT5N/3/- 演示

回答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

demo: http://jsfiddle.net/dAwfF/3/

演示:http: //jsfiddle.net/dAwfF/3/

回答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 MouseEventrelative to the clicked element, use offsetX/ layerX.

要获取MouseEvent相对于单击元素的坐标,请使用offsetX/ layerX

Have you tried using ev.layerXor ev.offsetX?

你试过使用ev.layerXev.offsetX吗?

var offsetX = (typeof ev.offsetX == "number") ? ev.offsetX : ev.layerX || 0;

See also:

也可以看看:

回答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.matrixTransformmethod 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-interfacespolyfillDOMPoint.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)

relativePointwill 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 800is the scene's perspective, I'm not sure if this is what the fourth value for the DOMPointconstructor should be when we intend to do something like this. Also, I'm not sure if I should use preMultiplySelfor 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当我们打算做这样的事情时,我不确定这是否是构造函数的第四个值应该是什么。另外,我不确定是否应该使用preMultiplySelfpostMultiplySelf。一旦我得到它至少工作(一开始可能不正确),我会发现并更新我的答案。

回答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.c​​sswg.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 ?

你怎么认为 ?