Итак, как следует из названия, я вращаю элемент, ничего особенного. Для этого я использую PointerEvents. Я вычисляю смещение и обновляю начальное значение степени, чтобы убедиться, что элемент не перескакивает при последовательных действиях пользователя, например. когда отпускаешь элемент и снова начинаешь вращаться.
Я борюсь с тем, чтобы добавить несколько ограничений. Например, я хотел бы убедиться, что:
N
указывает на север, разрешается вращение только против часовой стрелки.N
не указывает на север, разрешите вращение в обоих направлениях, пока N
не укажет на север.Проблема не столько в том, чтобы настроить условия для этих ограничений, сколько в том, что они, кажется, не работают последовательно, пока запускается событие pointermove
.
Например, для первого правила я пытался сделать что-то вроде этого:
if (currentDegree + initialDegree - offsetDegree <= 0) {
box.style.transform = `rotate(${
currentDegree + initialDegree - offsetDegree
}deg)`;
}
Это отчасти работает, но если я перемещаю указатель достаточно быстро, я могу легко преодолеть ограничение. Иногда элемент даже останавливается до того, как N
указывает на север, поэтому мне интересно, правильный ли это подход к проблеме.
Я публикую скрипку без ограничений, чтобы вы могли понять, что я пытаюсь сделать.
Я был бы признателен за любой совет, но предлагать библиотеку, которая помогла бы мне реализовать эти правила, мне не нужно. Заранее спасибо.
const box = document.querySelector('.box');
const { x, y, width } = box.getBoundingClientRect();
let boxCenter = width / 2;
let boxXPos = x;
let boxYPos = y;
let initialDegree = 0;
let offsetDegree = 0;
let currentDegree = 0;
function calcDegree(x, y) {
const degree = Math.floor(Math.atan2(y, x) * (180 / Math.PI) + 90);
return degree < 0 ? 360 + degree : degree;
}
function onPointerMove(event) {
const x = event.clientX - boxXPos - boxCenter;
const y = event.clientY - boxYPos - boxCenter;
currentDegree = calcDegree(x, y);
box.style.transform = `rotate(${
currentDegree + initialDegree - offsetDegree
}deg)`;
}
box.addEventListener('pointerdown', (event) => {
box.setPointerCapture(event.pointerId);
const x = event.clientX - boxXPos - boxCenter;
const y = event.clientY - boxYPos - boxCenter;
offsetDegree = calcDegree(x, y);
box.addEventListener('pointermove', onPointerMove);
box.addEventListener(
'pointerup',
() => {
initialDegree += currentDegree - offsetDegree;
box.removeEventListener('pointermove', onPointerMove);
},
{ once: true }
);
});
window.addEventListener('resize', () => {
const { x, y } = box.getBoundingClientRect();
boxXPos = x;
boxYPos = y;
});
body {
margin: 0;
height: 100vh;
}
#app {
display: grid;
place-items: center;
height: inherit;
}
.box {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
align-items: center;
justify-items: center;
width: 200px;
height: 200px;
background-color: #bbd6b8;
border: 1px solid #94af9f;
cursor: move;
}
.direction {
width: 50px;
height: 50px;
background-color: #333;
border-radius: 50%;
color: white;
text-align: center;
line-height: 50px;
user-select: none;
}
.north {
grid-column: 2;
grid-row: 1;
}
.east {
grid-column: 3;
grid-row: 2;
}
.south {
grid-column: 2;
grid-row: 3;
}
.west {
grid-column: 1;
grid-row: 2;
}
<div id = "app">
<div class = "box">
<div class = "direction north">N</div>
<div class = "direction east">E</div>
<div class = "direction south">S</div>
<div class = "direction west">W</div>
</div>
</div>
Ваше решение зависит от расположения указателя относительно центра поля. Чем ближе вы находитесь к центру, тем больше кривизна (dS/dTheta) следа указателя, поэтому вы очень быстро пересекаете квадранты (0-90, 90-180, 180-270, 270-360), и коробка будет вращаться хаотично. Вы, безусловно, можете исправить свой код, чтобы наложить надлежащие ограничения на эти значения.
Я предпочитаю действовать поэтапно в таких задачах. Это делает его немного проще и не зависит от пикселей. Я также уменьшил mouseover
до 60 кадров в секунду. Если хотите, замените событие мыши на событие указателя.
В приведенном ниже примере вы можете настроить dRotMax
, dRotMin
и rotStep
внутри события resize
, если хотите.
!function(){
const box = document.querySelector('.box');
let rotStarted = false,
busy = false,
prevX = null,
prevY = null,
currRot = 360,
rotStep = 1,
dRotMax = 10,
dRotMin = -10;
window.addEventListener("mousedown", () => rotStarted = true)
window.addEventListener("mouseup", () => {
prevX = null;
prevY = null;
rotStarted = false;
})
window.addEventListener("mousemove", function(e) {
if (!rotStarted){return}
if (busy){return}
busy = true;
window.requestAnimationFrame(()=> busy = false)
prevX = prevX ?? e.clientX;
prevY = prevY ?? e.clientY;
let dX = -prevX + (prevX = e.clientX),
dY = -prevY + (prevY = e.clientY),
dRot = Math.max(dRotMin, Math.min(dRotMax, (dX - dY) * rotStep));
currRot = Math.max(0, Math.min(360, dRot + currRot))
box.style.transform = `rotate(${currRot}deg)`
//console.info(dX, dY, dRot)
})
}()
body {
margin: 0;
height: 100vh;
}
#app {
display: grid;
place-items: center;
height: inherit;
}
.box {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
align-items: center;
justify-items: center;
width: 200px;
height: 200px;
background-color: #bbd6b8;
border: 1px solid #94af9f;
cursor: move;
}
.direction {
width: 50px;
height: 50px;
background-color: #333;
border-radius: 50%;
color: white;
text-align: center;
line-height: 50px;
user-select: none;
}
.north {
grid-column: 2;
grid-row: 1;
}
.east {
grid-column: 3;
grid-row: 2;
}
.south {
grid-column: 2;
grid-row: 3;
}
.west {
grid-column: 1;
grid-row: 2;
}
<div id = "app">
<div class = "box">
<div class = "direction north">N</div>
<div class = "direction east">E</div>
<div class = "direction south">S</div>
<div class = "direction west">W</div>
</div>
</div>
Если вы хотите изменить способ управления вращением оси на основе угловых квадрантов, преобразуйте rotStep
в функцию. Главное преимущество в том, что мы избавляемся от getBoundingClientRect
и atan2
вместе.
!function(){
const box = document.querySelector('.box');
let rotStarted = false,
busy = false,
prevX = null,
prevY = null,
currRot = 360,
epsilon = 10e-1,
rotStep = (r, axis) => { //0 for x, 1 for y
let quad = r / 90,
//at transition points, disable 1 axis to prevent jitter
isTransition = Math.abs(Math.round(quad) - quad) <= epsilon;
switch(quad | 0) {
case 0:
return 1
case 1: //3 o'clock,
return axis
? 1
: isTransition ? 0 : -1
case 2: //6 o'clock
return axis
? isTransition ? 0 : -1
: -1
case 3: //9 o'clock
case 4:
return axis
? -1
: isTransition ? 0 : 1
}
},
dRotMax = 10,
dRotMin = -10;
window.addEventListener("mousedown", () => rotStarted = true)
window.addEventListener("mouseup", () => {
prevX = null;
prevY = null;
rotStarted = false;
})
window.addEventListener("mousemove", function(e) {
if (!rotStarted){return}
if (busy){return}
busy = true;
window.requestAnimationFrame(()=> busy = false)
prevX = prevX ?? e.clientX;
prevY = prevY ?? e.clientY;
let dX = -prevX + (prevX = e.clientX),
dY = -prevY + (prevY = e.clientY),
dRot = Math.max(dRotMin, Math.min(dRotMax, dX * rotStep(currRot, 0) + dY * rotStep(currRot, 1)));
currRot = Math.max(0, Math.min(360, dRot + currRot))
box.style.transform = `rotate(${currRot}deg)`
//console.info(dX, dY, dRot)
})
}()
body {
margin: 0;
height: 100vh;
}
#app {
display: grid;
place-items: center;
height: inherit;
}
.box {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
align-items: center;
justify-items: center;
width: 200px;
height: 200px;
background-color: #bbd6b8;
border: 1px solid #94af9f;
cursor: move;
}
.direction {
width: 50px;
height: 50px;
background-color: #333;
border-radius: 50%;
color: white;
text-align: center;
line-height: 50px;
user-select: none;
}
.north {
grid-column: 2;
grid-row: 1;
}
.east {
grid-column: 3;
grid-row: 2;
}
.south {
grid-column: 2;
grid-row: 3;
}
.west {
grid-column: 1;
grid-row: 2;
}
<div id = "app">
<div class = "box">
<div class = "direction north">N</div>
<div class = "direction east">E</div>
<div class = "direction south">S</div>
<div class = "direction west">W</div>
</div>
</div>
Я попробую дросселирование и посмотрю, поможет ли это.
Что означает, что при 90 градусах элемент больше нельзя правильно повернуть? Каково желаемое ожидание поворота вокруг 90 градусов? . Такое ограничение нигде не написано в первых трех ограничениях, которые вы упомянули. С этим алгоритмом нет такой вещи, как зависимость от того, куда вы поместите мышь. Перетаскивание по x вправо поворачивает по часовой стрелке, перетаскивание вниз по y поворачивает против часовой стрелки независимо от положения мыши. Это позволяет избавиться как от getBoundingClientRect, так и от atan2, которые являются дорогостоящими. Если вы хотите изменить направление, сделайте rotStep отрицательным.
Можно ограничить угол "Север" до -360 и 0 градусов (зажим). Это отрицательно, потому что вы хотите разрешить движение против часовой стрелки из исходного положения, которое является движением в отрицательном направлении. Если бы это было по часовой стрелке, диапазон должен был бы быть [0, 360].
Чтобы узнать, как обновить текущий угол, вы можете проверить разницу, которую произвело последнее перемещение указателя, и интерпретировать разницу менее 180 градусов как движение по часовой стрелке, а все остальное - как движение против часовой стрелки. Это определит, как текущий угол должен быть обновлен (путем увеличения или уменьшения). Это означает, что угол может превышать диапазон, в котором мы хотим оставаться, то есть [-360, 0]. Если это значение будет превышено, вам потребуется повторно откалибровать вовлеченные глобальные переменные.
Чтобы помочь себе понять значение задействованных глобальных переменных, я назвал одну pointerToNorthDegree
, которая представляет собой угол между положением «Север» и указателем. Этот угол должен оставаться постоянным, когда пользователи перетаскивают указатель (поскольку север будет перемещаться соответственно). Только при переполнении этот угол необходимо обновить, не перемещаясь на север.
У меня было одно сомнение: если пользователь делает полный поворот против часовой стрелки, снова помещая север наверх, отпускает указатель, а затем хочет повторить эту операцию снова, будет ли ему по-прежнему разрешено поворачиваться только против часовой стрелки или теперь им нужно «раскрутиться» и фактически двигаться по часовой стрелке. Разница в одной строчке кода, которую я пометил комментарием.
Я также добавил некоторые другие комментарии в обновленный код ниже:
const box = document.querySelector('.box');
const { x, y, width } = box.getBoundingClientRect();
const boxCenter = width / 2;
let boxXPos = x;
let boxYPos = y;
let pointerToNorthDegree; // degrees of where north is relative to where pointer is
let northDegree = 0; // degrees of where north is relative to top
// Map any angle to range 0..359
const mod = ang => ((ang % 360) + 360) % 360;
// Clamp a value between min/max range
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
const calcDegree = (x, y) => mod(Math.floor(Math.atan2(y, x) * (180 / Math.PI) + 90));
const getPos = (e) => [e.clientX - boxXPos - boxCenter, e.clientY - boxYPos - boxCenter];
function onPointerMove(event) {
const pointerDegree = calcDegree(...getPos(event));
// Calculate a relative angle change in the range -180..179,
// and determine where north should be. It will get an
// angle that might exceed 0..359 range, which can be used to
// detect an overrun:
northDegree += mod(pointerDegree + pointerToNorthDegree - northDegree + 180) - 180;
// Should not rotate beyond home position in either direction:
const clampedNorthDegree = clamp(northDegree, -360, 0); // This range allows a full CCW turn from 0 degrees
if (northDegree != clampedNorthDegree) {
// If cursor was moving further than allowed, then use the current degree
// as the new offset, so that if the cursor moves in the opposite
// direction, rotation will happen immediately from that reference onwards.
northDegree = clampedNorthDegree;
pointerToNorthDegree = northDegree - pointerDegree;
}
box.style.transform = `rotate(${northDegree}deg)`;
}
box.addEventListener('pointerdown', (event) => {
const pointerDegree = calcDegree(...getPos(event));
// Remove next line if you don't want to allow another CCW turn from the home position
// when the previous operation ended at north via a CCW turn.
if (northDegree == -360) northDegree = 0;
pointerToNorthDegree = northDegree - pointerDegree;
box.setPointerCapture(event.pointerId);
box.addEventListener('pointermove', onPointerMove);
box.addEventListener('pointerup', () => box.removeEventListener('pointermove', onPointerMove));
});
window.addEventListener('resize', () => ({ x: boxXPos, y: boxYPos } = box.getBoundingClientRect()));
body {
margin: 0;
height: 100vh;
}
#app {
display: grid;
place-items: center;
height: inherit;
}
.box {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
align-items: center;
justify-items: center;
width: 200px;
height: 200px;
background-color: #bbd6b8;
border: 1px solid #94af9f;
cursor: move;
}
.direction {
width: 50px;
height: 50px;
background-color: #333;
border-radius: 50%;
color: white;
text-align: center;
line-height: 50px;
user-select: none;
}
.north {
grid-column: 2;
grid-row: 1;
}
.east {
grid-column: 3;
grid-row: 2;
}
.south {
grid-column: 2;
grid-row: 3;
}
.west {
grid-column: 1;
grid-row: 2;
}
<div id = "app">
<div class = "box">
<div class = "direction north">N</div>
<div class = "direction east">E</div>
<div class = "direction south">S</div>
<div class = "direction west">W</div>
</div>
</div>
Честно говоря, у меня не было проблем с вращением коробки, когда указатель приближался к центру. Вам удалось остановить вращение на 0 °, но примерно на 90 ° элемент больше не может быть правильно повернут, и в зависимости от того, где я поместил указатель в вашем фрагменте, чтобы начать вращение, элемент вращается в неправильном направлении. Спасибо за ваш ответ до сих пор, но это не очень помогает мне с моей проблемой.