webentwicklung-frage-antwort-db.com.de

Wie erhalte ich die MouseEvent-Koordinaten für ein Element mit CSS3-Transformation?

Ich möchte erkennen, wo eine MouseEvent aufgetreten ist, in Koordinaten relativ zu dem angeklickten Element. Warum? Weil ich an der angeklickten Stelle ein absolut positioniertes untergeordnetes Element hinzufügen möchte.

Ich kann es erkennen, wenn keine CSS3-Transformationen vorhanden sind (siehe Beschreibung unten). Wenn ich jedoch eine CSS3-Transformation hinzufüge, bricht mein Algorithmus, und ich weiß nicht, wie ich ihn reparieren kann.

Ich verwende keine JavaScript-Bibliothek und möchte verstehen, wie Dinge in reinem JavaScript funktionieren. Antworten Sie also bitte nicht mit "nur jQuery verwenden".

Ich möchte übrigens eine Lösung, die für alle MouseEvents funktioniert, und nicht nur "klicken". Nicht, dass es von Bedeutung ist, weil ich glaube, dass alle Mausereignisse dieselben Eigenschaften haben. Daher sollte die gleiche Lösung für alle von ihnen funktionieren.


Hintergrundinformation

Gemäß DOM Level 2-Spezifikation hat ein MouseEvent einige Eigenschaften, die mit dem Abrufen der Ereigniskoordinaten zusammenhängen:

  • screenX und screenY geben die Bildschirmkoordinaten zurück (der Ursprung ist die linke obere Ecke des Bildschirms des Benutzers).
  • clientX und clientY geben die Koordinaten relativ zum Dokument-Ansichtsfenster zurück.

Um die Position der MouseEvent relativ zu dem angeklickten Elementinhalt zu finden, muss ich also Folgendes tun:

ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
  • ev.clientX ist die Koordinate relativ zum Dokument-Ansichtsfenster
  • this.getBoundingClientRect().left ist die Position des Elements relativ zum Dokument-Ansichtsfenster
  • this.clientLeft ist der Grenzwert (und die Bildlaufleiste) zwischen der Elementgrenze und den inneren Koordinaten
  • this.scrollLeft ist der Bildlauf innerhalb des Elements

getBoundingClientRect() , clientLeft und scrollLeft werden bei CSSOM View Module angegeben.

Experimentieren Sie ohne CSS-Transformation (es funktioniert)

Verwirrend? Versuchen Sie folgendes JavaScript und HTML }. Beim Klicken sollte ein roter Punkt genau dort erscheinen, wo der Klick stattgefunden hat. Diese Version ist "ziemlich einfach" und funktioniert wie erwartet.

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 beim Hinzufügen einer CSS-Transformation (schlägt fehl)

Nun, füge ein CSS hinzu, 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. */
}

Der Algorithmus kennt die Transformationen nicht und berechnet somit eine falsche Position. Darüber hinaus unterscheiden sich die Ergebnisse zwischen Firefox 3.6 und Chrome 12. Opera 11.50 verhält sich wie Chrome.

In diesem Beispiel war die einzige Transformation die Skalierung, sodass ich den Skalierungsfaktor multiplizieren konnte, um die korrekte Koordinate zu berechnen. Wenn wir jedoch über willkürliche Transformationen (Skalieren, Drehen, Neigen, Übersetzen, Matrix) und sogar verschachtelte Transformationen (ein transformiertes Element in einem anderen transformierten Element) nachdenken, brauchen wir wirklich eine bessere Methode zur Berechnung der Koordinaten.

45

Das Verhalten, das Sie feststellen, ist korrekt und Ihr Algorithmus ist nicht fehlerhaft. Erstens sind CSS3-Transformationen so gestaltet, dass sie das Boxmodell nicht beeinträchtigen.

Versuchen und erklären ...

Wenn Sie eine CSS3-Transformation auf ein Element anwenden. Das Element nimmt eine Art relativer Positionierung an. Die umgebenden Elemente werden dadurch nicht durch das transformierte Element beeinflusst.

z.B. Stellen Sie sich drei Divs in einer horizontalen Reihe vor. Wenn Sie eine Skalentransformation anwenden, um die Größe des mittleren Bereichs zu verringern. Die umgebenden divs werden sich nicht nach innen bewegen, um den Raum einzunehmen, der das transformierte Element einmal besetzt hat. 

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

Im Boxmodell ändert das Element also nicht wirklich die Größe. Nur die Größe wird gerendert.

Sie müssen auch bedenken, dass Sie eine Skalentransformation anwenden, sodass die "echte" Größe Ihrer Elemente tatsächlich der Originalgröße entspricht. Sie ändern nur die wahrgenommene Größe.

Erklären..

Stellen Sie sich vor, Sie erstellen ein Div mit einer Breite von 1000px und verkleinern es auf die Hälfte. Die interne Größe des Div beträgt immer noch 1000px, nicht 500px. 

Die Position Ihrer Punkte ist also relativ zur "echten" Größe des Dict korrekt.

Ich habe dein Beispiel geändert, um es zu veranschaulichen. 

Anleitung

  1. Klicken Sie auf das div und halten Sie Ihre Maus an derselben Position.
  2. Finden Sie den Punkt an der falschen Position.
  3. Drücke Q, das div wird zur richtigen Größe.
  4. Bewegen Sie Ihre Maus, um den Punkt an der richtigen Stelle zu finden, an der Sie geklickt haben.

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

Damit die Mausklicks mit der sichtbaren Position des div übereinstimmen, müssen Sie wissen, dass die Maus basierend auf dem Fenster Koordinaten zurückgibt, und Ihre div-Offsets basieren ebenfalls auf der "tatsächlichen" Größe .

Da Ihre Objektgröße relativ zum Fenster ist, besteht die einzige Lösung darin, die Versatzkoordinaten mit demselben Skalierungswert wie Ihr div zu skalieren.

Dies kann jedoch schwierig werden, je nachdem, wo Sie die Transform-Origin-Eigenschaft Ihres div festlegen. Da dies die Offsets beeinflussen wird.

Siehe hier.

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

Hoffe das hilft.

14
AshHeskes

wenn das Element Container ist und absolut oder relativ positioniert ist, können Sie innerhalb des Elements platzieren Positionieren Sie es relativ zu übergeordneten Elementen und width = 1px, height = 1px, und bewegen Sie sich innerhalb von container. .und verwenden Sie nach jeder Verschiebung document.elementFromPoint (event.clientX, event.clientY) =))))

Sie können die binäre Suche verwenden, um den Vorgang zu beschleunigen. Sieht zwar schlecht aus, aber es funktioniert

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

9
4esn0k

Für die Webkit webkitConvertPointFromPageToNode -Methode kann auch verwendet werden:

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

eine andere Möglichkeit ist, 3 Divs in den Ecken des Elements zu platzieren, als find transform matrix ... funktioniert aber auch nur für positionierbare Container Elemente - 4esn0k

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

3
4esn0k
2
Garrett

Mit Abstand der Schnellste. Die akzeptierte Antwort dauert auf meiner 3D-Transformations-Site etwa 40-70 ms. Dies dauert normalerweise weniger als 20 ( Geige ):

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;
}

und fügen Sie auch das folgende CSS auf derselben Seite ein:

.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%;
}

Es teilt das Element im Wesentlichen in 9 Quadrate auf und bestimmt über document.elementFromPoint, in welchem ​​der Klick sich befand. Anschließend wird das in 9 kleinere Quadrate usw. aufgeteilt, bis es auf ein Pixel genau ist. Ich weiß, dass ich es überkommentiert habe. Die akzeptierte Antwort ist einige Male langsamer als diese.

BEARBEITEN: Es ist jetzt noch schneller, und wenn der Benutzer in Chrome oder Safari ist, verwendet er eine native Funktion, die für diese Funktion vorgesehen ist, anstelle der 9-Sektoren-Sache und kann dies in WENIGER ALS 2 MILLEKONDERN konsistent tun!

2
markasoftware

Das scheint für mich sehr gut zu funktionieren

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

EDIT: Meine Antwort ist nicht getestet, WIP, ich werde es aktualisieren, wenn ich es bekomme.

Ich implementiere eine Polyfill der Geomtetry-Interfaces . Die DOMPoint.matrixTransform-Methode, die ich als nächstes erstellen werde, was bedeutet, dass wir in der Lage sein sollten, etwa Folgendes zu schreiben, um eine Klick-Koordinate auf ein transformiertes (möglicherweise verschachteltes) DOM-Element abzubilden:

// 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 ist der Punkt relativ zu der Elementoberfläche, auf die Sie geklickt haben.

Eine w3c DOMMatrix kann mit einem CSS-Transformationsstringargument erstellt werden, was die Verwendung in JavaScript sehr einfach macht.

Leider funktioniert das noch nicht (nur Firefox verfügt über eine Geometrie-Schnittstellen-Implementierung, und mein Polyfill akzeptiert noch keine CSS-Transformationszeichenfolge). Siehe: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18

Ich werde dies aktualisieren, sobald ich das implementiert habe und ein funktionierendes Beispiel habe. Ziehwünsche willkommen!

BEARBEITEN: Der Wert 800 ist die Perspektive der Szene. Ich bin nicht sicher, ob dies der vierte Wert für den DOMPoint-Konstruktor sein soll, wenn wir so etwas tun wollen. Ich bin mir auch nicht sicher, ob ich preMultiplySelf oder postMultiplySelf verwenden sollte. Ich werde es herausfinden, sobald ich es zumindest funktioniere (die Werte können anfangs falsch sein) und werde meine Antwort aktualisieren.

0
trusktr

Ich habe dieses Problem und habe versucht, die Matrix zu berechnen ... __ Ich habe eine Bibliothek darum herum gestartet: 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])

Was denkst du ?

0
Luc Boissaye

Funktioniert gut, ob relativ oder absolut :) einfache Lösung

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

Ich arbeite an einer Polyfill, um DOM-Koordinaten zu übertragen. Die GeometryUtils-API ist noch nicht verfügbar (@see https://drafts.csswg.org/cssom-view/ ). Ich habe 2014 einen "einfachen" Code erstellt, um Koordinaten wie localToGlobal, globalToLocal und localToLocal umzuwandeln. Es ist noch nicht fertig, aber es funktioniert :) Ich denke, ich werde es in den kommenden Monaten (05.07.2017) beenden. Wenn Sie also noch eine API für die Koordinatentransformation benötigen, versuchen Sie es: https: // github. com/jsidea/jsideajsidea-Kernbibliothek . Sein ist noch nicht stabil (pre alpha) . Sie können es so verwenden:

Erstellen Sie Ihre Transformationsinstanz:

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

Das Boxmodell, in das Sie transformieren möchten (Standard: "border", wird später durch ENUMs ersetzt):

var toBoxModel = "border";

Das Boxmodell, von dem die Eingabekoordinaten stammen (Standard: "border"):

var fromBoxModel = "border";

Transformieren Sie Ihre globalen Koordinaten (hier {x: 50, y: 100, z: 0}) in den lokalen Raum. Der resultierende Punkt besteht aus 4 Komponenten: x, y, z und w.

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

Ich habe einige andere Funktionen wie localToGlobal und localToLocal implementiert. Wenn Sie es versuchen möchten, laden Sie einfach den Release-Build herunter und verwenden Sie die Datei jsidea.min.js. 

Laden Sie die erste Version hier herunter: TypeScript-Code herunterladen

Fühlen Sie sich frei, den Code zu ändern, ich habe ihn niemals unter eine Lizenz gestellt :)

0
jbenker