Подтвердить что ты не робот

Как получить координаты MouseEvent для элемента с преобразованием CSS3?

Я хочу обнаружить , где произошел MouseEvent, в координатах относительно щелкнутого элемента. Зачем? Потому что я хочу добавить абсолютно позиционированный дочерний элемент в выбранном месте.

Я знаю, как его обнаружить, когда нет преобразований CSS3 (см. описание ниже). Однако, когда я добавляю CSS3 Transform, тогда мой алгоритм прерывается, и я не знаю, как его исправить.

Я не использую какую-либо библиотеку JavaScript, и я хочу понять, как все работает в простом JavaScript. Поэтому, пожалуйста, не отвечайте "просто используйте jQuery".

Кстати, я хочу решение, которое работает для всех MouseEvents, а не только "click". Не то чтобы это важно, потому что я считаю, что все события мыши имеют одни и те же свойства, поэтому одно и то же решение должно работать для всех.


Фоновая информация

Согласно спецификация DOM уровня 2, MouseEvent имеет несколько свойств, связанных с получением координат события:

  • screenX и screenY вернуть координаты экрана (начало координат находится в верхнем левом углу пользовательского монитора)
  • clientX и clientY вернуть координаты относительно окна просмотра документа.

Таким образом, чтобы найти положение MouseEvent относительно содержимого элемента clicked, я должен выполнить эту математику:

ev.clientX - this.getBoundingClientRect().left - this.clientLeft + this.scrollLeft
  • ev.clientX - это координата относительно окна просмотра документа.
  • this.getBoundingClientRect().left - это позиция элемента относительно окна просмотра документа.
  • this.clientLeft - количество границы (и полосы прокрутки) между границей элемента и внутренними координатами
  • this.scrollLeft - количество прокрутки внутри элемента

getBoundingClientRect(), clientLeft и scrollLeft указаны в CSSOM View Module.

Эксперимент без CSS Transform (работает)

Непонятный? Попробуйте следующий фрагмент 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.

В этом примере единственным преобразованием было масштабирование, поэтому я мог бы умножить коэффициент масштабирования для вычисления правильной координаты. Однако, если мы думаем о произвольных преобразованиях (масштабирование, поворот, перекос, перевод, матрица) и даже вложенные преобразования (преобразованный элемент внутри другого преобразованного элемента), то нам действительно нужен лучший способ вычисления координат.

4b9b3361

Ответ 1

Поведение, которое вы испытываете, является правильным, и ваш алгоритм не нарушается. Во-первых, CSS3 Transforms предназначены для того, чтобы не мешать коробке.

Попробовать и объяснить...

При применении CSS3 Transform на элементе. Элемент предполагает своеобразное относительное позиционирование. При этом окружающие элементы не будут выполняться преобразованным элементом.

например. представьте себе три div в горизонтальной строке. Если вы применяете масштабное преобразование для уменьшения размера центра div. Окружающий div не будет перемещаться внутрь, чтобы занять пространство, которое когда-то занимало преобразованный элемент.

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

Итак, в модели ящика элемент фактически не изменяет размер. Только это изменило размер.

Вы также должны иметь в виду, что вы применяете масштаб Transform, поэтому ваши "реальные" размеры на самом деле совпадают с оригинальным размером. Вы меняете только воспринимаемый размер.

Объяснить..

Представьте, что вы создаете div с шириной 1000 пикселей и масштабируете его до 1/2 размера. Внутренний размер div по-прежнему составляет 1000 пикселей, а не 500 пикселей.

Таким образом, положение ваших точек правильное относительно реального размера div.

Я изменил ваш пример, чтобы проиллюстрировать.

Инструкции

  • Нажмите div и держите мышь в том же положении.
  • Найдите точку в неправильном положении.
  • Нажмите Q, размер div станет правильным.
  • Переместите мышь, чтобы найти точку в правильной позиции, где вы нажали.

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

Итак, чтобы координаты щелчков мыши соответствовали видимому местоположению на div, вам нужно понять, что мышь возвращает координаты на основе окна, а ваши смещения div также основаны на его "реальный" размер.

Поскольку размер вашего объекта относительно окна, единственным решением является масштабирование координат смещения по тому же значению шкалы, что и ваш div.

Однако это может стать сложным на основе того, где вы задаете свойство Transform-origin вашего div. Поскольку это приведет к смещениям.

Смотрите здесь.

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

Надеюсь, что это поможет.

Ответ 2

если элемент является контейнером и расположен абсолютным или относительным, вы можете разместить внутри него элемент, расположите его относительно родителя и width = 1px, height = 1px и перемещаться внутрь контейнера, и после каждого перемещения используйте document.elementFromPoint(event.clientX, event.clientY) =))))

Вы можете использовать бинарный поиск, чтобы сделать его быстрее. выглядит ужасно, но он работает

http://jsfiddle.net/3VT5N/3/ - демо

Ответ 3

Кроме того, для 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;

Ответ 4

Чтобы получить координаты a MouseEvent относительно выбранного элемента, используйте offsetX/layerX.

Пробовали ли вы использовать ev.layerX или ev.offsetX?

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

См. также:

Ответ 5

BY FAR - самый быстрый. Принятый ответ занимает около 40-70 мс на моем сайте 3d transforms, обычно это занимает менее 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 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 маленьких квадратов и т.д., Пока он не станет точным с точностью до пикселя. Я знаю, что я прокомментировал это. Принимаемый ответ в несколько раз медленнее этого.

EDIT: теперь он еще быстрее, и если пользователь находится в Chrome или Safari, он будет использовать встроенную функцию, предназначенную для этого, вместо 9 секторов, и может сделать это последовательно в МЕНЬШЕ 2 МИЛЛИОНАХ!

Ответ 6

другим способом является место 3 divs в углах этого элемента, чем найти матрицу преобразования... но также работает только для позиционируемых контейнерных элементов - 4esn0k

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

Ответ 7

Мне кажется, что это действительно хорошо работает

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

Ответ 8

Работает нормально, независимо от относительного или абсолютного:) простое решение

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

Ответ 9

Я работаю над polyfill для преобразования координат DOM. GeometryUtils api пока недоступен (@see https://drafts.csswg.org/cssom-view/). Я создал "простой" код в 2014 году, чтобы преобразовать координаты, такие как localToGlobal, globalToLocal и localToLocal. Он еще не закончен, но его работа:) Я думаю, что закончу его в ближайшие месяцы (05.07.2017), поэтому, если вам все еще нужен API для выполнения преобразования координат, попробуйте: https://github.com/jsidea/jsidea базовая библиотека jsidea. Его нестабильный (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.

Загрузите первую версию здесь: Загрузить TypeScript код

Не стесняйтесь менять код, я никогда не ставил его под любую лицензию:)

Ответ 10

У меня есть эта проблема и я начал пытаться вычислить матрицу. Я создал вокруг него библиотеку: 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])

Как вы думаете?

Ответ 11

EDIT: мой ответ не проверен, WIP, я обновлю, когда я его заработаю.

Я реализую 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 будет точкой относительно поверхности элемента, на которую вы нажали.

A w3c DOMMatrix может быть сконструирован с помощью строкового аргумента CSS-преобразования, что делает его очень простым в использовании в JavaScript.

К сожалению, пока это не работает (только Firefox имеет реализацию геометрических интерфейсов, а мой polyfill еще не принимает строку преобразования CSS). См.: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18

Я обновлю это, как только я его реализую, и у меня есть рабочий пример. Приглашаем вас приветствовать!

EDIT: значение 800 - это перспектива сцены, я не уверен, что это то, что должно быть четвертым значением для конструктора DOMPoint, когда мы намереваемся сделать что-то подобное. Кроме того, я не уверен, следует ли использовать preMultiplySelf или postMultiplySelf. Я узнаю, как только я получу хотя бы работу (сначала значения могут быть неверными) и обновит мой ответ.