Поворот элемента с ограничениями

Итак, как следует из названия, я вращаю элемент, ничего особенного. Для этого я использую 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>
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
0
83
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ваше решение зависит от расположения указателя относительно центра поля. Чем ближе вы находитесь к центру, тем больше кривизна (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>

Честно говоря, у меня не было проблем с вращением коробки, когда указатель приближался к центру. Вам удалось остановить вращение на 0 °, но примерно на 90 ° элемент больше не может быть правильно повернут, и в зависимости от того, где я поместил указатель в вашем фрагменте, чтобы начать вращение, элемент вращается в неправильном направлении. Спасибо за ваш ответ до сих пор, но это не очень помогает мне с моей проблемой.

DavidDomain 01.04.2023 10:22

Я попробую дросселирование и посмотрю, поможет ли это.

DavidDomain 01.04.2023 10:28

Что означает, что при 90 градусах элемент больше нельзя правильно повернуть? Каково желаемое ожидание поворота вокруг 90 градусов? . Такое ограничение нигде не написано в первых трех ограничениях, которые вы упомянули. С этим алгоритмом нет такой вещи, как зависимость от того, куда вы поместите мышь. Перетаскивание по x вправо поворачивает по часовой стрелке, перетаскивание вниз по y поворачивает против часовой стрелки независимо от положения мыши. Это позволяет избавиться как от getBoundingClientRect, так и от atan2, которые являются дорогостоящими. Если вы хотите изменить направление, сделайте rotStep отрицательным.

ibrahim tanyalcin 01.04.2023 23:58
Ответ принят как подходящий

Можно ограничить угол "Север" до -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>

Другие вопросы по теме