Как получить координаты 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.
В этом примере единственным преобразованием было масштабирование, поэтому я мог бы умножить коэффициент масштабирования для вычисления правильной координаты. Однако, если мы думаем о произвольных преобразованиях (масштабирование, поворот, перекос, перевод, матрица) и даже вложенные преобразования (преобразованный элемент внутри другого преобразованного элемента), то нам действительно нужен лучший способ вычисления координат.
Ответы
Ответ 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
. Я узнаю, как только я получу хотя бы работу (сначала значения могут быть неверными) и обновит мой ответ.