Как получить координаты MouseEvent для элемента с CSS3 Transform? - PullRequest
45 голосов
/ 21 июля 2011

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

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

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

Кстати, я хочу решение, которое работает для всех MouseEvents, а не просто «щелкает».Это не имеет значения, потому что я считаю, что все события мыши имеют одинаковые свойства, поэтому для всех них должно работать одно и то же решение.


Справочная информация

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

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

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

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.

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

Ответы [ 11 ]

14 голосов
/ 21 июля 2011

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

Чтобы попытаться объяснить ...

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

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

пример: http://jsfiddle.net/AshMokhberi/bWwkC/

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

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

Объяснить ..

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

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

Я изменил ваш пример для иллюстрации.

Инструкция

  1. Нажмите div и держите мышку в том же положении.
  2. Найдите точку в неправильном положении.
  3. Нажмите Q, div станет правильного размера.
  4. Наведите указатель мыши, чтобы найти точку в правильном положении в месте, где вы щелкнули.

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

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

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

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

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

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

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

9 голосов
/ 09 августа 2011

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

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

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

3 голосов
/ 29 июля 2013

FAR самый быстрый.Принятый ответ занимает около 40-70 мс на моем сайте 3d-преобразований, обычно это занимает менее 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 МИЛЛИСЕКОНД!

3 голосов
/ 16 августа 2011

Также для 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 голосов
/ 10 августа 2011

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

демонстрация: http://jsfiddle.net/dAwfF/3/

2 голосов
/ 14 августа 2011

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

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

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

См. Также:

1 голос
/ 22 января 2015

Мне кажется, это очень хорошо работает

var elementNewXPosition = (event.offsetX != null) ? event.offsetX : event.originalEvent.layerX;
var elementNewYPosition = (event.offsetY != null) ? event.offsetY : event.originalEvent.layerY; 
0 голосов
/ 18 сентября 2017

У меня есть эта проблема, и я начал пытаться вычислить матрицу.Я основал библиотеку вокруг этого: 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 голосов
/ 05 июля 2017

Я работаю над полифиллом для преобразования координат DOM.API GeometryUtils пока недоступен (@see https://drafts.csswg.org/cssom-view/).. Я создал «простой» код в 2014 году для преобразования координат, например localToGlobal, globalToLocal и localToLocal. Он еще не закончен, но работает :) Я думаю, что будузавершите его в ближайшие месяцы (05.07.2017), поэтому, если вам все еще нужен API для выполнения преобразования координат, попробуйте: https://github.com/jsidea/jsidea jsidea core library .Его еще не стабильно (до альфа).Вы можете использовать его следующим образом:

Создайте свой экземпляр преобразования:

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

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

0 голосов
/ 24 апреля 2017

РЕДАКТИРОВАТЬ: мой ответ не проверен, 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 есть реализация геометрических интерфейсов, а мой polyfill еще не принимает строку преобразования CSS).См .: https://github.com/trusktr/geometry-interfaces/blob/7872f1f78a44e6384529e22505e6ca0ba9b30a4d/src/DOMMatrix.js#L15-L18

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...