【问题标题】:How to get the MouseEvent coordinates for an element that has CSS3 Transform?如何获取具有 CSS3 变换的元素的 MouseEvent 坐标?
【发布时间】:2011-10-10 01:12:41
【问题描述】:

我想在相对于被点击元素的坐标中检测MouseEvent 发生的位置。为什么?因为我想在点击的位置添加一个绝对定位的子元素。

我知道在不存在 CSS3 转换时如何检测它(参见下面的描述)。但是,当我添加一个 CSS3 变换时,我的算法会中断,我不知道如何修复它。

我没有使用任何 JavaScript 库,我想了解在纯 JavaScript 中是如何工作的。所以,请不要回答“只使用 jQuery”。

顺便说一句,我想要一个适用于所有鼠标事件的解决方案,而不仅仅是“点击”。这并不重要,因为我相信所有鼠标事件都具有相同的属性,因此相同的解决方案应该适用于所有事件。


背景资料

根据DOM Level 2 specificationMouseEvent 几乎没有与获取事件坐标相关的属性:

  • screenXscreenY 返回屏幕坐标(原点是用户显示器的左上角)
  • clientXclientY 返回相对于文档视口的坐标。

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

ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
  • ev.clientX 是相对于文档视口的坐标
  • this.getBoundingClientRect().left 是元素相对于文档视口的位置
  • this.clientLeft 是元素边界和内部坐标之间的边框(和滚动条)量
  • this.scrollLeft 是元素内部的滚动量

getBoundingClientRect()clientLeftscrollLeftCSSOM View Module 中指定。

没有 CSS 变换的实验(它有效)

令人困惑? Try the following piece of JavaScript and 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>

添加 CSS 变换的实验(失败)

现在,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. */
}

算法不知道变换,因此计算出错误的位置。更重要的是,Firefox 3.6 和 Chrome 12 的结果不同。Opera 11.50 的行为与 Chrome 一样。

在这个例子中,唯一的转换是缩放,所以我可以乘以缩放因子来计算正确的坐标。但是,如果我们考虑任意变换(缩放、旋转、倾斜、平移、矩阵),甚至是嵌套变换(另一个变换元素中的变换元素),那么我们确实需要一种更好的方法来计算坐标。

【问题讨论】:

  • '请不要回答“只使用 jQuery”' - 我会单独为你投票...
  • 在我看来,如果你有办法获得描述原始变换的 3D 变换矩阵,你就可以通过使它们经历相同的变换来获得坐标,对吧? getComputedStyle(someElement).getPropertyValue('transform') 怎么样?它似乎恰好返回了这样一个矩阵。现在我并不是说矩阵计算很容易,但它们准确且快速......
  • @StijndeWitt 在浏览器中出现了一种官方方式,请参阅我的回答。

标签: javascript html css transform


【解决方案1】:

您遇到的行为是正确的,并且您的算法没有破坏。首先,CSS3 变换被设计为不会干扰盒子模型。

试着解释一下……

当您对元素应用 CSS3 变换时。元素假定一种相对定位。因为周围的元素不受转换后的元素的影响。

例如想象三个 div 在水平行中。如果您应用缩放变换来减小中心 div 的大小。周围的 div 不会向内移动以占据曾经占据转换元素的空间。

示例:http://jsfiddle.net/AshMokhberi/bWwkC/

所以在盒子模型中,元素实际上并没有改变大小。只是它的渲染大小发生了变化。

您还必须记住,您正在应用缩放变换,因此您的元素“真实”大小实际上与其原始大小相同。你只是在改变它的感知大小。

解释一下..

假设您创建了一个宽度为 1000 像素的 div,并将其缩小到原来的 1/2。 div的内部大小仍然是1000px,而不是500px。

所以点的位置相对于 div 的“真实”大小是正确的。

我修改了你的例子来说明。

说明

  1. 单击 div 并将鼠标保持在同一位置。
  2. 在错误的位置找到点。
  3. 按Q,div会变成正确的大小。
  4. 移动鼠标,在您点击的正确位置找到点。

http://jsfiddle.net/AshMokhberi/EwQLX/

所以为了使鼠标点击的坐标与div上的可见位置相匹配,你需要了解鼠标是根据窗口返回坐标的,而你的div偏移量也是基于它的"实际”大小。

由于您的对象大小是相对于窗口的,唯一的解决方案是将偏移坐标缩放与您的 div 相同的缩放值。

但是,根据您设置 div 的 Transform-origin 属性的位置,这可能会变得很棘手。因为这会影响偏移量。

看这里。

http://jsfiddle.net/AshMokhberi/KmDxj/

希望这会有所帮助。

【讨论】:

  • 这是一个很好的解释,但实际上并没有回答这个问题。它很好地解释了为什么浏览器行为是正确的,但没有回答如何获得所需的行为(考虑任意转换)。
  • 我知道我可以在算法中缩放坐标;如果它是如此简单,我会这样做的。您可能没有注意到的是,当您混合旋转、倾斜、平移或任意矩阵时,它会变得更加棘手。如果我们在另一个转换元素中包含一个转换元素,它会变得更加棘手。这就是我的问题所在。而且我不喜欢在 JavaScript 中重新实现所有转换的想法。
  • 你是对的,它会变得非常棘手。不幸的是,除非他们更改 css3 转换规范,否则没有其他方法可以做到这一点。 css3 转换的规范仍在草稿中,我认为您提出了一个很好的观点。我会考虑加入工作草案邮件列表,并提出您的担忧。此处有详细信息w3.org/TR/css3-2d-transforms 可能值得加入 3d 转换邮件列表,因为这适用于规范的两个部分。 w3.org/TR/css3-3d-transforms
  • 按照您的建议发布了消息。谢谢! lists.w3.org/Archives/Public/www-style/2011Jul/0387.html
  • 欢迎您,您似乎得到了一些积极的回应。我希望他们确实实现了所需的更改,否则 CSS 转换的生活将变得非常复杂,非常快。
【解决方案2】:

如果元素是容器并且定位绝对或相对, 你可以把它放在元素里面, 相对于父母定位它和 宽度 = 1px,高度 = 1px,然后移动到容器内部, 并在每次移动后使用 document.elementFromPoint(event.clientX, event.clientY) =))))

您可以使用二分搜索使其更快。 看起来很糟糕,但它确实有效

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

【讨论】:

  • 另一种方法是在该元素的角落放置 3 个 div,而不是查找变换矩阵...但也仅适用于定位的可容器元素
  • 你能试着提高你的英语水平吗?因为我仔细阅读了你的演示源代码,我才明白。
  • 我稍微修改了一下以获得 20-40% 的性能提升。以前,在我的 3d 转换站点上需要 50-70 毫秒,现在需要 30-45 毫秒。 jsfiddle.net/3VT5N/49 是更新版本。我还将返回对象中的属性名称从 left 和 top 更改为 x 和 y,它返回像素坐标而不是百分比。我还无缘无故地取出了 toString 函数,并在返回的对象中添加了一个 time 属性,该属性告诉您操作花费了多少毫秒。在我的计算机上,新示例大约需要 20 毫秒,即 1/50 秒。过去需要 40 毫秒。
  • 继续上一条评论,即1/25秒,速度的1/2。这是因为旧的,它一直在设置样式,创建一个新元素,运行 document.elementFromPoint,然后再次删除元素,只是为了重复很多次。而是在开头创建元素,然后修改元素的样式并多次运行 document.elementFromPoint,然后在末尾删除元素。因此,与其多次创建和删除元素,不如只执行一次。
  • @trusktr 你确实看到我的评论来自 2014 年 :-)
【解决方案3】:

另一种方法是在该元素的角落放置 3 个 div, 比查找变换矩阵...但也仅适用于定位容器 元素 - 4esn0k

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

【讨论】:

  • 您的用户似乎没有在 Stackoverflow 中注册,因此您的两个答案似乎来自不同的用户。我认为您应该登录/注册,以便您的所有答案都正确链接到您的用户。
  • 在角落使用 3 个 DIV 将适用于任意 2D CSS 转换。那太棒了。但是 3D 转换呢?
  • 是的,3d 无法使用这种方法,尽管使用这种方法我们可以找到元素之外的位置
  • 这个方法在 Firefox 中也不起作用,因为bugzilla.mozilla.org/show_bug.cgi?id=591718
  • 我想可以使用 4 个元素而不是 3 个元素来重建 3D 变换矩阵。这会有点困难,需要考虑透视。不确定是否值得努力。
【解决方案4】:

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

您是否尝试过使用ev.layerXev.offsetX

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

另见:

【讨论】:

  • 我猜你错过了我谈论 TRANSFORMED 元素(使用 2D 或 3D CSS 转换)的部分
  • 不,我读到了。我在发布之前尝试了这种方法,使用转换,它适用于我在这里拥有的几个浏览器。如果你去:songthaimassage.com/massage 并尝试document.images[1].onclick = function(ev) { var offsetX = (typeof ev.offsetX == "number") ? ev.offsetX : ev.layerX || 0; console.log(offsetX); }; 然后单击第一个画廊图像的左边缘,然后,放大后,单击左边缘附近(你必须移动鼠标),你应该查看 0 附近的结果。
  • 这太棒了,帮助我解决了由于 3d 转换而遇到的问题,其中 pageX 和 clientX 需要大量其他数学才能使它们正确!谢谢。
  • 谢谢加勒特,它就像一个魅力!帮了我很多。这么少的代码,太棒了!
  • @Garrett 这不起作用。也许您尝试的转换太简单了。制作涉及具有旋转、缩放、倾斜、平移的多级嵌套元素的复杂 3D 场景,然后您会看到这些值在屏幕像素空间上,而不是在元素空间上。
【解决方案5】:

另外,对于 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;

【讨论】:

  • 你有这个函数的文档链接吗?
  • 此功能已弃用。因此,让我们编写大量代码来检测鼠标的局部坐标或 3d 旋转元素上的触摸事件,即:画布用于:查找该点是否透明?或者在那儿画点什么……天哪。傻傻傻傻。
【解决方案6】:

目前最快。在我的 3d 转换站点上,接受的答案大约需要 40-70 毫秒,这通常需要不到 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;
}

并且还在同一页面中包含以下 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%;
}

它本质上将元素分成 9 个方块,通过document.elementFromPoint 确定点击在哪一个。然后将其拆分为 9 个较小的正方形等,直到精确到一个像素内。我知道我评论过头了。接受的答案比这慢几倍。

编辑:它现在更快了,如果用户在 Chrome 或 Safari 中,它将使用为此设计的本机功能,而不是 9 个扇区的东西,并且可以在不到 2 毫秒的时间内始终如一地完成!

【讨论】:

  • 我希望我可以试试这个,但似乎 webkitConvertPointFromPageToNode 已从浏览器中删除(或者只是在 Chrome 中不存在)。我发布了一个答案,你能检查一下它是否可以工作(一旦我完成了 DOMMatrix/DOMPoint polyfill,我可以对其进行测试,但目前它是理论上的)。我的回答:stackoverflow.com/a/43576626/454780
  • @trusktr webkitConvertPointFromPageToNode 首先是非标准的。此代码旨在尽可能使用它,否则回退。即使没有它也应该可以工作。
  • 你能发布一个小提琴或一些活生生的例子吗?
  • 太棒了!
【解决方案7】:

无论是相对还是绝对都可以正常工作:) 简单的解决方案

var p = $( '.divName' );
var position = p.position();  
var left = (position.left  / 0.5);
var top =  (position.top  / 0.5);

【讨论】:

  • 这似乎无法回答所提出的问题。
【解决方案8】:

这对我来说似乎非常有效

var elementNewXPosition = (event.offsetX != null) ? event.offsetX : event.originalEvent.layerX;
var elementNewYPosition = (event.offsetY != null) ? event.offsetY : event.originalEvent.layerY; 

【讨论】:

  • 这个答案与@Garrett 之前的答案相同,因此对这个问题没有任何用处。
【解决方案9】:

编辑:我的答案未经测试,WIP,当我得到它的工作时我会更新。

我正在实现 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 将是相对于您单击的元素表面的点。

可以使用 CSS 转换字符串参数构造 w3c DOMMatrix,这使得它在 JavaScript 中非常易于使用。

不幸的是,这还不行(只有 Firefox 有几何接口实现,我的 polyfill 还不接受 CSS 转换字符串)。见:https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18

一旦我实现它并有一个工作示例,我将更新它。欢迎请求请求!

编辑:值800 是场景的视角,我不确定当我们打算做这样的事情时,这是否是DOMPoint 构造函数的第四个值。另外,我不确定是否应该使用preMultiplySelfpostMultiplySelf。一旦我得到它至少工作(一开始值可能不正确),我会发现并更新我的答案。

【讨论】:

    【解决方案10】:

    我正在开发一个 polyfill 来转换 DOM 坐标。 GeometryUtils api 尚不可用 (@see https://drafts.csswg.org/cssom-view/)。我在 2014 年创建了一个“简单”代码来转换坐标,例如 localToGlobal、globalToLocal 和 localToLocal。它还没有完成,但它正在工作 :) 我想我会在接下来的几个月(05.07.2017)完成它,所以如果你仍然需要一个 API 来完成坐标转换,请尝试一下:https://github.com/jsidea/jsideajsidea core library。它的还不稳定(pre alpha)。 你可以这样使用它:

    创建您的转换实例:

    var transformer = jsidea.geom.Transform.create(yourElement);
    

    你要转换成的盒子模型(默认:“border”,后面会被ENUM替换):

    var toBoxModel = "border";
    

    输入坐标的来源框模型(默认:“border”):

    var fromBoxModel = "border";
    

    将您的全局坐标(此处为 {x:50, y:100, z: 0})转换为本地空间。结果点有 4 个分量:x、y、z 和 w。

    var local = transformer.globalToLocal(50, 100, 0, toBoxModel, fromBoxModel);
    

    我已经实现了一些其他功能,例如 localToGlobal 和 localToLocal。 如果您想尝试一下,只需下载发布版本并使用 jsidea.min.js。

    在此处下载第一个版本:Download TypeScript code

    随意更改代码,我从未将其置于任何许可下:)

    【讨论】:

      【解决方案11】:

      我遇到了这个问题并开始尝试计算矩阵。 我围绕它创建了一个库: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])
      

      你怎么看?

      【讨论】:

        猜你喜欢
        • 2013-06-09
        • 1970-01-01
        • 2014-01-04
        • 2013-10-09
        • 1970-01-01
        • 2019-09-06
        • 2021-12-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多