Как мне перебрать значения в этом цикле (например, оттенок или поворот)?

25

пример совместной анимации

Посмотреть демо

Я пытаюсь заставить сустав вращаться плавно вокруг центра холста, к углу указателя мыши. То, что у меня есть, работает, но я хочу, чтобы оно анимировало кратчайшее расстояние, чтобы добраться до угла мыши. Проблема возникает, когда значение зацикливается на горизонтальной линии ( 3.14и -3.14). Наведите указатель мыши на эту область, чтобы увидеть, как меняется направление, и он проходит долгий путь назад.

Соответствующий код

// ease the current angle to the target angle
joint.angle += ( joint.targetAngle - joint.angle ) * 0.1;

// get angle from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;  
joint.targetAngle = Math.atan2( dy, dx );

Как я могу заставить его вращаться на кратчайшее расстояние, даже «через зазор»?

jackrugile
источник
Используйте по модулю. : D en.wikipedia.org/wiki/Modular_arithmetic
Вон Хилтс
1
@VaughanHilts Не уверен, как бы я использовал это в моей ситуации. Можете ли вы уточнить дальше?
негодяй

Ответы:

11

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

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

При переносе значений оттенков вы можете заменить hueих [cos(hue), sin(hue)]вектором.

В вашем случае, нормализованное направление суставов:

// get normalised direction from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;
var len = Math.sqrt(dx * dx + dy * dy);
dx /= len ? len : 1.0; dy /= len ? len : 1.0;
// get current direction
var dirx = cos(joint.angle),
    diry = sin(joint.angle);
// ease the current direction to the target direction
dirx += (dx - dirx) * 0.1;
diry += (dy - diry) * 0.1;

joint.angle = Math.atan2(diry, dirx);

Код может быть короче, если вы можете использовать 2D векторный класс. Например:

// get normalised direction from joint to mouse
var desired_dir = normalize(vec2(e.clientX, e.clientY) - joint);
// get current direction
var current_dir = vec2(cos(joint.angle), sin(joint.angle));
// ease the current direction to the target direction
current_dir += (desired_dir - current_dir) * 0.1;

joint.angle = Math.atan2(current_dir.y, current_dir.x);
Сэм Хоцевар
источник
Спасибо, это прекрасно работает здесь: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Я понимаю большую часть того, что вы делаете, но можете ли вы немного подробнее рассказать о 5-й строке вашего кода во время нормализации? Не уверен, что именно там происходит dx /= len...
наркомания
1
Деление вектора на его длину называется нормализацией . Он гарантирует, что он имеет длину 1. len ? len : 1.0Деталь просто избегает деления на ноль, в редком случае, когда мышь находится точно в положении соединения. Это могло быть написано: if (len != 0) dx /= len;.
Сэм Хочевар
-1. Этот ответ далеко не оптимален в большинстве случаев. Что если вы интерполируете между и 180°? В векторном виде: [1, 0]а [-1, 0]. Интерполирующие векторы дадут вам либо , 180°или ошибку деления на 0, в случае t=0.5.
Густаво Масиэль
@GustavoMaciel, это не «большинство случаев», это один очень специфический случай, который никогда не случается на практике. Также нет деления на ноль, проверьте код.
Сэм Хоцевар
@GustavoMaciel еще раз проверив код, он на самом деле чрезвычайно безопасен и работает точно так же, как и в случае, который вы описываете.
Сэм Хоцевар
11

Хитрость заключается в том, чтобы помнить, что углы (по крайней мере, в евклидовом пространстве) периодичны на 2 * пи. Если разница между текущим углом и целевым углом слишком велика (т. Е. Курсор пересек границу), просто отрегулируйте текущий угол, добавив или вычтя 2 * пи соответственно.

В этом случае вы можете попробовать следующее: (Я никогда раньше не программировал в Javascript, так что простите мой стиль кодирования.)

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;
  joint.angle += ( joint.targetAngle - joint.angle ) * joint.easing;

РЕДАКТИРОВАТЬ : В этой реализации слишком быстрое перемещение курсора вокруг центра соединения приводит к его рывку. Это предполагаемое поведение, так как угловая скорость сустава всегда пропорциональна dtheta. Если это поведение нежелательно, проблему можно легко решить, установив крышку на угловое ускорение сустава.

Для этого нам нужно отслеживать скорость соединения и наложить максимальное ускорение:

  joint = {
    // snip
    velocity: 0,
    maxAccel: 0.01
  },

Затем для нашего удобства введем функцию отсечения:

function clip(x, min, max) {
  return x < min ? min : x > max ? max : x
}

Теперь наш код движения выглядит следующим образом. Сначала мы рассчитываем, dthetaкак и прежде, корректируя joint.angleпри необходимости:

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;

Затем, вместо того, чтобы немедленно перемещать соединение, мы вычисляем целевую скорость и используем ее, clipчтобы заставить ее в пределах нашего приемлемого диапазона.

  var targetVel = ( joint.targetAngle - joint.angle ) * joint.easing;
  joint.velocity = clip(targetVel,
                        joint.velocity - joint.maxAccel,
                        joint.velocity + joint.maxAccel);
  joint.angle += joint.velocity;

Это обеспечивает плавное движение даже при переключении направлений при выполнении расчетов только в одном измерении. Кроме того, это позволяет независимо регулировать скорость и ускорение соединения. Смотрите демо здесь: http://codepen.io/anon/pen/HGnDF/

Дэвид Чжан
источник
Этот метод очень близок, но если моя мышь слишком быстро, она начинает слегка подпрыгивать. Демонстрация здесь, дайте мне знать, если я не реализовал это правильно: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile
1
Я не уверен, что ты имеешь в виду, прыгнув в неправильном направлении; для меня сустав всегда движется в правильном направлении. Конечно, если вы слишком быстро перемещаете указатель мыши по центру, вы обгоняете сустав, и он дергается, когда переключается с движения в одном направлении на другое. Это предполагаемое поведение, так как скорость вращения всегда пропорциональна dtheta. Вы намеревались, чтобы у сустава был некоторый импульс?
Дэвид Чжан
Смотрите мое последнее редактирование, чтобы узнать способ устранения резких движений, создаваемых путем обгонения сустава.
Дэвид Чжан
Привет, попробовал вашу демонстрационную ссылку, но похоже, она просто указывает на мою оригинальную. Вы сохранили новый? Если это не так, я смогу реализовать это позже сегодня вечером и посмотреть, как это работает. Я думаю, что вы описали в своем редактировании больше, чем я искал. Вернемся к вам, спасибо!
негодяй
1
Ах да, ты прав. Извините моя ошибка. Я исправлю это.
Дэвид Чжан
3

Я люблю другие ответы. Очень технично!

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

double MAX_ANGLE = 360.0;
double startAngle = 300.0;
double endAngle = 15.0;
double distanceForward = 0.0;  // Clockwise
double distanceBackward = 0.0; // Counter-Clockwise

// Calculate both distances, forward and backward:
distanceForward = endAngle - startAngle;    // -285.00
distanceBackward = startAngle - endAngle;   // +285.00

// Which direction is shortest?
// Forward? (normalized to 75)
if (NormalizeAngle(distanceForward) < NormalizeAngle(distanceBackward)) {
    // Adjust for 360/0 degree wrap
    if (endAngle < startAngle) endAngle += MAX_ANGLE; // Will be above 360
}

// Backward? (normalized to 285)
else {
    // Adjust for 360/0 degree wrap
    if (endAngle > startAngle) endAngle -= MAX_ANGLE; // Will be below 0
}

// Now, Lerp between startAngle and endAngle. 

// EndAngle can be above 360 if wrapping clockwise past 0, or
// EndAngle can be below 0 if wrapping counter-clockwise before 0.
// Normalize each returned Lerp value to bring angle in range of 0 to 360 if required.  Most engines will automatically do this for you.


double NormalizeAngle(double angle) {
    while (angle < 0) 
        angle += MAX_ANGLE;
    while (angle >= MAX_ANGLE) 
        angle -= MAX_ANGLE;
    return angle;
}

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

[Редактировать] 2017/06/02 - немного прояснил логику.

Начните с вычисления distanceForward и distanceBackwards, и позвольте результатам выйти за пределы диапазона (0-360).

Нормализация углов возвращает эти значения в диапазон (0-360). Чтобы сделать это, вы добавляете 360, пока значение не станет выше нуля, и вычтите 360, пока значение не превысит 360. Результирующие начальный / конечный углы будут эквивалентны (-285 равно 75).

Затем вы найдете наименьший нормализованный угол либо distanceForward, либо distanceBackward. distanceForward в примере становится 75, что меньше нормализованного значения distanceBackward (300).

Если distanceForward является наименьшим AND endAngle <startAngle, увеличьте endAngle за пределы 360, добавив 360. (в примере это 375).

Если distanceBackward является наименьшим AND endAngle> startAngle, увеличьте endAngle до значения ниже 0, вычитая 360.

Теперь вы переходите от startAngle (300) к новому endAngle (375). Двигатель должен автоматически отрегулировать значения выше 360, вычитая 360 для вас. В противном случае вам пришлось бы подскочить с 300 до 360, ТО - с 0 до 15, если двигатель не нормализует значения для вас.

Doug.McFarlane
источник
Ну, хотя atan2основанная идея проста, она может сэкономить немного места и немного производительности (нет необходимости хранить x и y, а также слишком часто вычислять sin, cos и atan2). Я думаю, что стоит немного углубиться в вопрос о том, почему это решение является правильным (то есть, выбирая кратчайший путь по кругу или сфере, как это делает SLERP для кватерионов). Этот вопрос и ответы должны быть помещены в вики сообщества, поскольку это действительно распространенная проблема, с которой сталкиваются большинство программистов геймплея.
Теодрон
Отличный ответ. Мне также нравится другой ответ, данный @David Zhang. Однако я всегда получаю странный джиттер, используя его отредактированное решение, когда я делаю быстрый поворот. Но ваш ответ отлично вписывается в мою игру. Меня интересует теория математики, стоящая за вашим ответом. Хотя это выглядит просто, но не очевидно, почему мы должны сравнивать нормализованное угловое расстояние в разных направлениях.
Новый
Я рад, что мой код работает. Как уже упоминалось, это было просто набрано в браузере, а не проверено в реальном проекте. Я даже не помню, как отвечал на этот вопрос (три года назад)! Но, глядя на это, похоже, что я просто расширяю диапазон (0-360), чтобы позволить тестовым значениям выйти за пределы / перед этим диапазоном, чтобы сравнить общую разницу градусов и принять кратчайшую разницу. При нормализации эти значения снова попадают в диапазон (0-360). Таким образом, distanceForward становится 75 (-285 + 360), что меньше, чем distanceBackward (285), поэтому это самое короткое расстояние.
Doug.McFarlane
Так как distanceForward - это кратчайшее расстояние, используется первая часть предложения IF. Поскольку endAngle (15) меньше startAngle (300), мы добавляем 360, чтобы получить 375. Таким образом, вы перешли бы от startAngle (300) к новому endAngle (375). Двигатель должен автоматически отрегулировать значения выше 360, вычитая 360 для вас.
Doug.McFarlane
Я отредактировал ответ, чтобы добавить больше ясности.
Doug.McFarlane