web-dev-qa-db-ja.com

CSS3トランスフォームを持つ要素のMouseEvent座標を取得する方法は?

whereMouseEventが発生したことを、クリックした要素に対する座標で検出したい。どうして?クリックした場所に絶対位置の子要素を追加したいからです。

CSS3変換が存在しないときにそれを検出する方法を知っています(以下の説明を参照)。ただし、CSS3 Transformを追加すると、アルゴリズムが壊れてしまい、修正方法がわかりません。

JavaScriptライブラリを使用していません。プレーンJavaScriptでの動作を理解したいと思います。したがって、「jQueryを使用するだけ」で答えないでください。

ところで、「クリック」だけでなく、すべてのMouseEventで機能するソリューションが必要です。すべてのマウスイベントが同じプロパティを共有していると考えているため、重要ではありません。したがって、すべてのマウスイベントで同じソリューションが機能するはずです。


背景情報

DOMレベル2仕様 によると、 MouseEvent には、イベント座標の取得に関連するプロパティがほとんどありません。

  • screenXおよびscreenYは画面座標を返します(Originはユーザーのモニターの左上隅です)
  • clientXおよびclientYは、ドキュメントビューポートに相対的な座標を返します。

したがって、クリックされた要素のコンテンツに関連するMouseEventの位置を見つけるには、次の計算を行う必要があります。

_ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
_
  • _ev.clientX_は、ドキュメントビューポートに相対的な座標です
  • this.getBoundingClientRect().leftは、ドキュメントビューポートに対する要素の位置です。
  • _this.clientLeft_は、要素の境界と内部座標の間の境界線(およびスクロールバー)の量です。
  • _this.scrollLeft_は、要素内のスクロール量です

getBoundingClientRect()clientLeft および scrollLeftCSSOMモジュールを表示

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>
_

CSSトランスフォームを追加する実験(失敗)

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と同じように動作します。

この例では、変換のみがスケーリングであるため、スケーリング係数を乗算して正しい座標を計算できます。ただし、任意の変換(スケール、回転、傾斜、平行移動、行列)、さらにはネストされた変換(別の変換された要素内の変換された要素)について考える場合、座標を計算するためのより良い方法が本当に必要です。

45

あなたが経験している振る舞いは正しく、あなたのアルゴリズムは壊れていません。まず、CSS3トランスフォームは、ボックスモデルに干渉しないように設計されています。

試して説明するには...

要素にCSS3変換を適用する場合。要素は、一種の相対的な配置を想定しています。その点で、周囲の要素は変換された要素の影響を受けません。

例えば横一列に3つのdivがあるとします。スケール変換を適用して、中央のdivのサイズを小さくする場合。周囲のdivは、内側に移動して、変換された要素を占有していたスペースを占有しません。

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

そのため、ボックスモデルでは、要素のサイズは実際には変わりません。レンダリングされたサイズの変更のみです。

また、スケール変換を適用しているため、要素の「実際の」サイズは実際には元のサイズと同じであることに留意する必要があります。認識されるサイズのみを変更しています。

説明する..

1000pxの幅の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/

お役に立てれば。

14
AshHeskes

要素がコンテナであり、絶対または相対に配置されている場合、その要素の内側に配置し、親および幅= 1px、高さ= 1pxを基準にして配置し、コンテナの内側に移動し、各移動後にdocument.elementFromPoint(eventを使用します。 clientX、event.clientY)=))))

バイナリ検索を使用して、高速化することができます。ひどいが、それは動作します

http://jsfiddle.net/3VT5N/3/ -デモ

9
4esn0k

最速で。受け入れられた答えは私の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セクターの代わりにこのために設計されたネイティブ関数を使用し、LESS THAN 2で一貫して実行できますミリセコンズ!

3
markasoftware

また、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;
3
4esn0k

別の方法は、変換行列を見つけるよりも、その要素の隅に3 divを配置することですが、配置されたコンテナ可能な要素に対してのみ機能します

デモ: http://jsfiddle.net/dAwfF/3/

3
4esn0k

これは私にとって本当にうまくいくようです

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

編集:私の答えはテストされていません、WIP、動作するようになったら更新します。

geomtetry-interfaces のポリフィルを実装しています。 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は、クリックした要素の表面に相対的なポイントになります。

W3c DOMMatrixは、CSS変換文字列引数を使用して構築できます。これにより、JavaScriptで非常に使いやすくなります。

残念ながら、これはまだ動作していません(Firefoxのみにジオメトリインターフェースの実装があり、ポリフィルはまだCSS変換文字列を受け入れません)。参照: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18

これを実装し、実用的な例を入手したら、これを更新します。プルリクエストを歓迎します!

編集:値800はシーンのパースペクティブです。このようなことをしようとするときに、DOMPointコンストラクターの4番目の値がこれであるべきかどうかはわかりません。また、preMultiplySelfpostMultiplySelfのどちらを使用すべきかわかりません。少なくとも機能するようになったら(最初は値が間違っている可能性があります)、答えを更新します。

0
trusktr

私はこの問題を抱えており、マトリックスの計算を試み始めました。それを中心にライブラリを開始しました: 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])

どう思いますか ?

0
Luc Boissaye

相対でも絶対でも問題なく動作します:)簡単な解決策

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

私は、DOM座標から変換するポリフィルに取り組んでいます。 GeometryUtils APIはまだ利用できません(@see https://drafts.c​​sswg.org/cssom-view/ )。 2014年にlocalToGlobal、globalToLocal、localToLocalなどの座標を変換するための「単純な」コードを作成しました。まだ完成していませんが、機能します:)今後数か月(2017年7月5日)に完成させると思うので、座標変換を達成するためにAPIがまだ必要な場合は試してみてください: https:// github.com/jsidea/jsideajsideaコアライブラリ 。そのまだ安定していません(プレアルファ)。次のように使用できます。

変換インスタンスを作成します。

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

変換先のボックスモデル(デフォルト: "border"。後でENUMに置き換えられます):

var toBoxModel = "border";

入力座標が由来するボックスモデル(デフォルト: "border"):

var fromBoxModel = "border";

グローバル座標(ここでは{x:50、y:100、z:0})をローカル空間に変換します。結果のポイントには、x、y、z、wの4つのコンポーネントがあります。

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

LocalToGlobalやlocalToLocalのような他の関数を実装しました。試してみたい場合は、リリースビルドをダウンロードしてjsidea.min.jsを使用してください。

最初のリリースをここからダウンロードします。 TypeScriptコードをダウンロード

コードを自由に変更してください、私は決してライセンスの下にそれを置きません:)

0
jbenker