Escribir una función de relajación; una historia un poco interesante

Debo decir desde el principio que no hay un punto real en esta publicación de blog. Tenía algo que hacer, lo hice y me sentí moderadamente satisfecho con el resultado. Me gusta leer dichos cuentos de otros desarrolladores, así que pensé en compartir mi historia.

Combinación de música recomendada para esta publicación: el nuevo álbum de Awolnation.

Version corta

Mover algo suavemente de un lugar a otro es algo que debo hacer en la mayoría de los sitios en los que trabajo. La mayoría de las veces no es más que desplazar suavemente la página a alguna posición o deslizar un menú de navegación.

El tamaño y la complejidad de las soluciones preempaquetadas parecían, para mí, desproporcionadas con la simplicidad de la tarea. Así que hice mi propio.

Aquí está el resultado en npm y GitHub y CodePen .

Versión larga

Hace mucho tiempo, cuando esta historia se llevó a cabo por primera vez, asumí que las funciones de relajación eran bastante pesadas en matemáticas, y me alegré de dejarlas en manos de personas más inteligentes que yo.

Al principio usé jQuery y su función animate() . Me da vergüenza decir que en un sitio cargué 60 KB de jQuery sin otro propósito que desplazar la página sin problemas.

Entonces npm se convirtió en algo y utilicé lo que aparecía en los resultados de búsqueda de npm para 'easing'.

Luego aprendí que el rendimiento web era algo que a los usuarios les importaba, así que realicé algunas pruebas contrarreloj y lancé una 'eek' tímida.

Aquí están los tiempos de carga de un sitio en el que estoy trabajando con un paquete de expansión típico y uno casero que revelaré muy lentamente en esta publicación.

La mediana es de cinco carreras cada una

No sé ustedes, pero considero que agregar ~ 100ms a mi tiempo de carga es una gran cosa. Recuerde que Amazon calculó que agregar 100ms costaba 1% en las ventas.

(Si los 100ms le importan o no deben ser un factor de los ingresos de su sitio web. Si recibe 20 visitas al día en un sitio sobre técnicas de meditación, entonces 100ms probablemente no es nada de qué preocuparse, si es una buena práctica).

Ah y 20 KB – en un sitio de 150 KB – solo para desplazarse por la página es indignante.

Sin embargo, déjenme ser claro, no es tanto culpa de los paquetes, están diseñados para hacer mucho más. El problema es que el código que necesito es de unos pocos cientos de bytes, pero viene integrado inextricablemente con decenas de miles de bytes de otro código creado para otras personas. Si utilizara un paquete como este, sería mi maldita culpa que mi tiempo de carga aumentara.

Comenzando: una transición lineal

Sé que mi función de aceleración necesitará al menos tres parámetros: un valor inicial, un valor final y una duración. Me imagino que este es un buen comienzo:

Un día, AI completará automáticamente ese comentario de "hacer algo" y todos nos iremos a casa.

Esto va a necesitar algún tipo de bucle que realice una operación una y otra vez desde el valor de inicio hasta el valor final. requestAnimationFrame para esto, de modo que cada ciclo del código se ejecute una vez por cuadro.

Primero, una cartilla de animación de un solo párrafo para que todos estén al tanto. La pantalla a la que se apunta actualmente es probablemente de 60Hz, lo que significa que se actualiza 60 veces por segundo. Esa es una característica del hardware real. Los sistemas operativos y navegadores no calculan constantemente el color que debe tener cada píxel de la pantalla, solo "lo hacen" cuando lo necesitan, lo que es 60 veces por segundo. O una vez cada 16 milisegundos. Esto se llama un marco o cuadro de animación, y requestAnimationFrame dice explícitamente al navegador, haga esto la próxima vez que vaya a actualizar los píxeles en la pantalla. Al llamarlo una y otra vez, ha creado un bucle que solo se ejecuta una vez por cada vez que el navegador actualiza la pantalla, por lo que es el trabajo mínimo para la velocidad de fotogramas máxima.

En el fragmento anterior, tengo una función ( step ) para hacer el trabajo real, al que llamo repetidamente (pasándolo a requestAnimationFrame ) siempre que alguna condición sea verdadera. Esto es más o menos como un while de bucle (con el mismo riesgo de un bucle infinito si se obtiene algo malo).

Incremento currentValue en cada ciclo, por lo que puedo estar bastante seguro de que eventualmente será más que endValue y eso romperá el ciclo.

Quiero que esta función sea utilizable en todo tipo de situaciones. Entonces, en lugar de tener una lógica de actualización integrada en la función, solo pasaré una devolución de llamada que se llamará en cada ciclo / paso / tick / lo que sea. Entonces, el "qué" de la aceleración se deja al código que llama a la función.

Tengo una regla personal (solo una) que dice que si una función tiene más de tres parámetros, utilizo 'parámetros nombrados'. Luego puedo hacer un seguimiento de lo que estoy pasando, y hace que sea más fácil manejar los parámetros opcionales (ya que el usuario no necesita pasar null valores null marcador de posición para satisfacer el orden de los parámetros).

Aquí hay cinco puntos sobre el fragmento de código debajo de los puntos:

  • La función ahora toma un solo parámetro, un objeto. Ese objeto puede tener las propiedades startValue , endValue , durationMs y onStep .
  • La onStep llamada onStep que se llama para cada paso con el valor actual.
  • Estoy usando la sintaxis de desestructuración de objetos para descomprimir estas propiedades en variables.
  • Estoy usando la sintaxis del parámetro predeterminado ( = ) para establecer algunos valores predeterminados. (El Dr. Axel describió los parámetros predeterminados y nombrados dos años antes de que comenzara a aprender JavaScript, pero de alguna manera todavía se sienten como The Future).
  • Cuando el 'loop' ha finalizado, onStep una vez más con endValue . Esto garantiza que la cosa termine donde lo solicitó, incluso si hay problemas de redondeo.

Los lectores de Eagle verán el gran defecto. Lo abordaré más tarde.

Ahora tengo una función de funcionamiento a la que puedo llamar así:

Efectivamente, esto arroja valores entre 1,000 y 2,000

Tenga en cuenta que no pasé en durationMs , por lo que tendrá un valor predeterminado de 200 milisegundos. Una duración hermosa y sensata.

Si me sentía juguetón, podría imprimir estos valores como una bonita tabla en la consola:

Me da:

Finalmente encontré un uso para el formateo de la consola

Desafortunadamente, la función de relajación anterior ahora está en la cárcel del código por romper la primera ley de Newton.

Aliviando las cosas duras

Para parecer más natural, quiero que mi transición simule un poco de inercia e impulso; Quiero que tenga la misma física que deslizar una cerveza a lo largo de una barra (física normal, no física de Ted Danson ).

Específicamente, quiero que se mueva lentamente al principio, más rápido en el medio, luego gradualmente se detenga al final.

Como el tiempo para cada paso es fijo (~ 16ms), y la velocidad es una ilusión perpetrada por nuestro sistema visual, podemos centrarnos únicamente en la distancia recorrida en cada paso.

Un ejemplo: digamos que quiero deslizar algo por 100 píxeles en más de 100 pasos. En el ejemplo lineal, cada paso sería una distancia de 1 píxel.

El eje y es la distancia o la velocidad.

Para obtener un efecto de relajación, necesito ajustar cada uno de estos pasos para que sean números más pequeños en cada extremo.

Por suerte, JavaScript tiene una función incorporada que me dará exactamente este comportamiento.

Math.sin() es la función de la que hablo. Si paso en cero , obtengo cero . Cuando estoy a la mitad de la animación paso por la mitad de PI y obtendré uno. Cuando estoy 100% hecho con la animación, paso el 100% de PI y obtendré cero nuevamente.

Así que puedo modificar mi función lineal y simplemente multiplicar cada paso con Math.sin(progress * Math.PI) .

Auge.

Pero hay un problema. En el momento en que mi pequeña cosa movió cada uno de esos pasos, solo habrá recorrido unos 63.657 píxeles. Eso no es suficiente. No lo suficientemente lejos en absoluto.

Sé lo que estás pensando: David, ¡simplemente multiplica cada paso por la mitad de PI!

Aunque aprecio los consejos que me imaginé que me dabas, y que de hecho hacen que todos los pasos sumen 100, realmente me gustaría un poco más de empuje en la aceleración y la desaceleración.

Por lo tanto, rechazaré respetuosamente su consejo y, en su lugar, cuadraré y doblaré cada uno de estos valores.

Tengo una confesión que hacer (perdóneme, padre, porque tengo Math.sinned): esto solo funcionó porque tomé un camión lleno de peyote y trepé a un abeto, donde un salmón hecho de electricidad nadó por la corriente de mi alma y me dijo qué suma hacer.

Ahora, he estado hablando de estas barras como si representaran píxeles, en un escenario donde algo necesita moverse 100 píxeles, más de 100 pasos. Esa fue una simplificación (probablemente innecesaria) de la realidad de que estos son en realidad multiplicadores de cada paso. No necesitan agregar 100, o 100% de la distancia, y generalmente no habrá exactamente 100 de ellos.

La parte importante es que después de aplicar el multiplicador a cada paso, de modo que los pasos intermedios sean más grandes y los inicios y los finales sean más pequeños, la suma de todos los pasos equivale a la distancia total que la cosa necesita para viajar.

Ahora, para convertir esta teoría en JavaScript …

Texto original en inglés.