¿Cómo paso entre los valores de ese ciclo (como el tono o la rotación)?

25

ejemplo de animación conjunta

Ver demostración

Estoy tratando de hacer que la articulación gire suavemente alrededor del centro del lienzo, hacia el ángulo del puntero del mouse. Lo que tengo funciona, pero quiero que anime la distancia más corta posible para llegar al ángulo del mouse. El problema ocurre cuando el valor gira alrededor de la línea horizontal ( 3.14y -3.14). Pase el mouse sobre esa área para ver cómo cambia la dirección y se tarda mucho en regresar.

Código relevante

// 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 );

¿Cómo puedo hacer que gire la distancia más corta, incluso "a través del espacio"?

Jackrugile
fuente
Use modulo. : D en.wikipedia.org/wiki/Modular_arithmetic
Vaughan Hilts
1
@VaughanHilts No estoy seguro de cómo lo usaría en mi situación. ¿Puedes elaborar más?
Jackrugile

Respuestas:

11

No siempre es el mejor método, y puede ser más costoso desde el punto de vista computacional (aunque esto depende en última instancia de cómo almacene sus datos), pero haré el argumento de que los valores bidimensionales funcionan razonablemente bien en la mayoría de los casos. En lugar de elevar un ángulo deseado, puede elevar el vector de dirección normalizado deseado .

Una ventaja de este método sobre el método de "elegir la ruta más corta al ángulo" es que funciona cuando necesita interpolar entre más de dos valores.

Al extraer valores de matiz, puede reemplazar huecon un [cos(hue), sin(hue)]vector.

En su caso, siguiendo la dirección común normalizada:

// 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);

El código puede ser más corto si puede usar una clase de vector 2D. Por ejemplo:

// 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);
sam hocevar
fuente
Gracias, esto está funcionando muy bien aquí: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Entiendo la mayor parte de lo que está haciendo, pero ¿puede explicar un poco más en la línea 5 de su código durante la normalización? No estoy seguro de lo que está sucediendo allí exactamente dx /= len...
jackrugile
1
Dividir un vector por su longitud se llama normalización . Asegura que tiene longitud 1. La len ? len : 1.0parte simplemente evita una división por cero, en el raro caso de que el mouse se coloque exactamente en la posición conjunta. Podría haber sido escrito: if (len != 0) dx /= len;.
sam hocevar
-1. Esta respuesta está lejos de ser óptima en la mayoría de los casos. ¿Qué pasa si estás interpolando entre y 180°? En forma vectorial: [1, 0]y [-1, 0]. Vectores de interpolación le dará cualquiera , 180°o una división por 0 errores, en caso de t=0.5.
Gustavo Maciel
@GustavoMaciel no es "la mayoría de los casos", ese es un caso de esquina muy específico que nunca sucede en la práctica. Además, no hay división por cero, verifique el código.
sam hocevar
@GustavoMaciel ha vuelto a comprobar el código, en realidad es extremadamente seguro y funciona exactamente como debería incluso en el caso de la esquina que describe.
sam hocevar
11

El truco es recordar que los ángulos (al menos en el espacio euclidiano) son periódicos por 2 * pi. Si la diferencia entre el ángulo actual y el ángulo objetivo es demasiado grande (es decir, el cursor ha cruzado el límite), simplemente ajuste el ángulo actual sumando o restando 2 * pi en consecuencia.

En este caso, puede intentar lo siguiente: (Nunca he programado en Javascript antes, así que perdone mi estilo de codificación).

  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;

EDITAR : en esta implementación, mover el cursor demasiado rápido alrededor del centro de la articulación hace que se mueva. Este es el comportamiento previsto, ya que la velocidad angular de la articulación es siempre proporcional adtheta . Si este comportamiento no es deseado, el problema se puede solucionar fácilmente colocando una tapa en la aceleración angular de la articulación.

Para hacer esto, necesitaremos hacer un seguimiento de la velocidad de la articulación e imponer una aceleración máxima:

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

Luego, para nuestra conveniencia, presentaremos una función de recorte:

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

Ahora, nuestro código de movimiento se ve así. Primero, calculamos dthetacomo antes, ajustando joint.anglesegún sea necesario:

  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;

Luego, en lugar de mover la articulación de inmediato, calculamos una velocidad objetivo y la usamos clippara forzarla dentro de nuestro rango aceptable.

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

Esto produce un movimiento suave, incluso al cambiar de dirección, mientras se realizan cálculos en una sola dimensión. Además, permite que la velocidad y la aceleración de la articulación se ajusten independientemente. Ver demostración aquí: http://codepen.io/anon/pen/HGnDF/

David Zhang
fuente
Este método se acerca mucho, pero si mi mouse es demasiado rápido, comienza a saltar ligeramente hacia el lado equivocado. Demostración aquí, avíseme si no lo implementé correctamente: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile
1
No estoy seguro de lo que quieres decir saltando un poco hacia el lado equivocado; Para mí, la articulación siempre se mueve en la dirección correcta. Por supuesto, si mueve el mouse por el centro demasiado rápido, supera la articulación y se sacude cuando cambia de moverse en una dirección a la otra. Este es el comportamiento previsto, ya que la velocidad de rotación siempre es proporcional a dtheta. ¿Pretendía que la articulación tuviera algo de impulso?
David Zhang
Vea mi última edición para ver una forma de eliminar el movimiento desigual creado al sobrepasar la articulación.
David Zhang
Oye, probé tu enlace de demostración, pero parece que solo apunta a mi enlace original. ¿Guardaste uno nuevo? Si no está bien, debería poder implementar esto más tarde esta noche y ver cómo funciona. Creo que lo que describiste en tu edición es más lo que estaba buscando. Nos pondremos en contacto con usted, gracias!
jackrugile
1
Oh sí, tienes razón. Perdón mi error. Lo arreglaré
David Zhang
3

Me encantan las otras respuestas dadas. Muy técnico!

Si quieres, tengo un método muy simple para lograr esto. Asumiremos ángulos para estos ejemplos. El concepto se puede extrapolar a otros tipos de valores, como los colores.

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;
}

Acabo de crear esto en el navegador y nunca he sido probado. Espero tener la lógica correcta el primer intento.

[Editar] 2017/06/02 - Aclaró un poco la lógica.

Comience calculando distanceForward y distanceBackwards, y permita que los resultados se extiendan más allá del rango (0-360).

La normalización de ángulos devuelve esos valores al rango de (0-360). Para hacer esto, sumas 360 hasta que el valor esté por encima de cero y restas 360 mientras el valor está por encima de 360. Los ángulos de inicio / final resultantes serán equivalentes (-285 es lo mismo que 75).

A continuación, encontrará el ángulo normalizado más pequeño de distanceForward o distanceBackward. distanceForward en el ejemplo se convierte en 75, que es más pequeño que el valor normalizado de distanceBackward (300).

Si distanceForward es el AND final más pequeño <startAngle, extienda endAngle más allá de 360 ​​agregando 360. (se convierte en 375 en el ejemplo).

Si distanceBackward es el AND final más pequeño> startAngle, extienda endAngle por debajo de 0 restando 360.

Ahora pasaría de startAngle (300) al nuevo endAngle (375). El motor debería ajustar automáticamente los valores por encima de 360 ​​restando 360 por usted. De lo contrario, tendría que subir de 300 a 360, LUEGO subir de 0 a 15 si el motor no normaliza los valores por usted.

Doug.McFarlane
fuente
Bueno, aunque la atan2idea basada es sencilla, esta podría ahorrar algo de espacio y un poco de rendimiento (no es necesario almacenar x e y, ni calcular sin, cos y atan2 con demasiada frecuencia). Creo que vale la pena ampliar un poco sobre por qué esta solución es correcta (es decir, elegir el camino más corto en un círculo o esfera, al igual que SLERP hace para los quaterions). Esta pregunta y las respuestas deben incluirse en el wiki de la comunidad, ya que este es un problema muy común que enfrentan la mayoría de los programadores de juegos.
teodron
Gran respuesta. También me gusta la otra respuesta dada por @David Zhang. Sin embargo, siempre obtengo una inquietud extraña al usar su solución editada cuando doy una vuelta rápida. Pero tu respuesta encaja perfectamente en mi juego. Estoy interesado en la teoría matemática detrás de su respuesta. Aunque parece simple, no es obvio por qué deberíamos comparar la distancia de ángulo normalizada de diferentes direcciones.
newguy
Me alegra que mi código funcione. Como se mencionó, esto simplemente se escribió en el navegador, no se probó en un proyecto real. ¡Ni siquiera recuerdo haber respondido esta pregunta (hace tres años)! Pero, al mirarlo, parece que solo estaba extendiendo el rango (0-360) para permitir que los valores de la prueba vayan más allá / antes de este rango para comparar la diferencia total de grados y tomar la diferencia más corta. La normalización solo trae esos valores al rango de (0-360) nuevamente. Entonces distanceForward se convierte en 75 (-285 + 360), que es más pequeño que distanceBackward (285), por lo que es la distancia más corta.
Doug.McFarlane
Como distanceForward es la distancia más corta, se utiliza la primera parte de la cláusula IF. Como endAngle (15) es menor que startAngle (300), agregamos 360 para obtener 375. Por lo tanto, pasaría de startAngle (300) al nuevo endAngle (375). El motor debería ajustar automáticamente los valores por encima de 360 ​​restando 360 por usted.
Doug.McFarlane
Edité la respuesta para agregar más claridad.
Doug.McFarlane