如何获取具有CSS3 Transform的元素的MouseEvent坐标?

Den*_*aia 47 javascript css html5 transform css3

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

我知道在没有CSS3转换时如何检测它(见下面的描述).但是,当我添加一个CSS3转换,然后我的算法中断,我不知道如何解决它.

我没有使用任何JavaScript库,我想了解简单JavaScript中的工作原理.所以,请不要回答"只使用jQuery".

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


背景资料

根据DOM Level 2规范,a MouseEvent几乎没有与获取事件坐标相关的属性:

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

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

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

getBoundingClientRect(),clientLeftscrollLeftCSSOM View Module中指定.

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

混乱?尝试以下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>
Run Code Online (Sandbox Code Playgroud)

实验添加CSS转换(失败)

现在,尝试添加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. */
}
Run Code Online (Sandbox Code Playgroud)

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

在这个例子中,唯一的变换是缩放,所以我可以乘以缩放因子来计算正确的坐标.但是,如果我们想任意变换(缩放,旋转,倾斜,翻译,矩阵),甚至嵌套转换(另一转化元素中变换元件),那么我们真的需要一个更好的方式来计算坐标.

Ash*_*kes 14

您遇到的行为是正确的,您的算法不会中断.首先,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/

希望这可以帮助.

  • 这是一个很好的解释,但实际上并没有回答问题。它很好地解释了为什么浏览器行为是正确的,但没有回答如何获得所需的行为(考虑任意转换)。 (2认同)

4es*_*n0k 9

如果元素是容器和位于绝对或相对,则可以在其内部放置相对于亲和宽度= 1px的,高度= 1像素,并移动到容器的内部,并经过元件,它的位置移动各使用document.elementFromPoint(事件. clientX,event.clientY)=))))

您可以使用二进制搜索来加快速度.看起来很糟糕,但它确实有效

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

  • 你可以尝试在答案中提高英语水平吗?我只能理解它,因为我仔细阅读了您的演示源代码. (2认同)

mar*_*are 5

迄今为止最快的。在我的 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;
}
Run Code Online (Sandbox Code Playgroud)

并在同一页面中包含以下 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%;
}
Run Code Online (Sandbox Code Playgroud)

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

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