Animaciones de primavera en Vue

Hola Habr!

Durante mucho tiempo he querido agregar animaciones de primavera a cualquier proyecto. Pero hice esto solo para proyectos React usando react-spring, porque no sabía nada más.

¡Pero finalmente, decidí descubrir cómo funciona todo y escribir mi implementación!

Si también quieres usar animaciones de primavera en todas partes, ve debajo del gato. Allí encontrará algo de teoría, implementando Spring en JS puro e implementando la animación Spring en Vue usando componentes y composición-api.


TL; DR


Implementación de Spring Animation en JS: https://playcode.io/590645/ . Debe

agregar animaciones Spring a su proyecto: tome Spring de la biblioteca Popmotion .

Debe agregar animaciones Spring a su proyecto Vue: tome Spring de la biblioteca Popmotion y escriba un contenedor sobre él.

¿Para qué son las animaciones de primavera?


Las animaciones CSS convencionales en la web utilizan una duración y una función para calcular la posición de vez en cuando.

Las animaciones de primavera utilizan un enfoque diferente: la física de primavera.

Para la animación, establecemos las propiedades físicas del sistema: la masa del resorte, el coeficiente de elasticidad del resorte, el coeficiente de fricción del medio. Luego estiramos el resorte a la distancia que necesitamos animar y lo liberamos.

En los proyectos de front-end, veo tales ventajas en las animaciones de Spring:

1. Puede establecer la velocidad de animación inicial.

Si el sitio procesa deslizamientos con los dedos o arrastra con el mouse, las animaciones de los objetos se verán más naturales si las configura con la velocidad inicial como la velocidad de deslizamiento o la velocidad del mouse.


Animación CSS que no retiene la velocidad del mouse


Spring-animation, en la que la velocidad del mouse se transmite al spring

2. Puede cambiar el punto final de la animación

Si, por ejemplo, desea animar un objeto al cursor del mouse, no puede ayudar con CSS; cuando actualiza la posición del cursor, la animación se inicializa nuevamente, lo que provocará sacudidas es visible.

No hay tal problema con las animaciones de Spring: si necesita cambiar el punto final de la animación, estiramos o comprimimos el resorte hasta la sangría deseada.


Un ejemplo de animación de un elemento a un cursor del mouse; arriba - Animación CSS, abajo - Animación de primavera

Física de primavera


Tome un resorte ordinario con una carga. Ella todavía está en reposo.

Tírala un poco hacia abajo. Una fuerza elástica (F=kΔX), que busca devolver el resorte a su posición original. Cuando soltemos el resorte, comenzará a oscilar a lo largo de la sinusoide:


Añadir fuerza de fricción. Deje que sea directamente proporcional a la velocidad del resorte (F=αV) En este caso, las oscilaciones decaerán con el tiempo:


Queremos que nuestras animaciones de primavera se animen de la misma manera que un resorte oscila cuando se expone a la elasticidad y la fricción.

Consideramos la posición del resorte para la animación.


En el contexto de las animaciones, la diferencia entre el valor inicial y final de la animación es la tensión inicial de la primavera. X0.

Antes del comienzo de la animación, se nos da: misa de primaveram, coeficiente de elasticidad de resorte k, coeficiente de fricción del medio αtensión de primavera X0, velocidad inicial V0.

La posición de la animación se calcula varias decenas de veces por segundo. El intervalo de tiempo entre los cálculos de animación se llamaΔt.

Durante cada nuevo cálculo, tenemos la velocidad del resorteV y la tensión del resorte Xen el último intervalo de animación. Para el primer cálculo de animación.X=X0, V=V0.

¡Comenzamos a calcular la posición de la animación para el intervalo!

Según la segunda ley de Newton, la suma de todas las fuerzas aplicadas es igual a la masa del cuerpo multiplicada por su aceleración:

F=ma


Dos fuerzas actúan sobre un resorte: la fuerza elástica y la fuerza de fricción:

F+F=ma


Anotaremos las fuerzas con más detalle y encontraremos la aceleración:

kXαV=ma


a=kXαVm


Después de eso podemos encontrar una nueva velocidad. Es igual a la suma de la velocidad en el intervalo de animación anterior y la aceleración multiplicada por el intervalo de tiempo:

V=V+aΔt


Conociendo la velocidad, puede encontrar la nueva posición del resorte:

X=X+VΔt



Tenemos la posición correcta, ¡muéstrela al usuario!

Una vez que se muestra, comience un nuevo intervalo de animación. Para un nuevo intervaloX=X, V=V.

Vale la pena parar la animación cuandoX y Vse vuelven muy pequeños, en este momento las oscilaciones de primavera son casi invisibles.

JS Spring Animation


Tenemos las fórmulas necesarias, queda por escribir nuestra implementación:

class Spring {
  constructor({ mass, tension, friction, initVelocity, from, to, onUpdate }) {
    this.mass = mass;                 //  
    this.tension = tension;           //  
    this.friction = friction;         //  
    this.initVelocity = initVelocity; //  

    this.from = from;                 //    
    this.to = to;                     //    
    this.onUpdate = onUpdate;         // ,    
  }

  startAnimation() {
    const callDoAnimationTick = () => {
      const isFinished = this.doAnimationTick();

      if (isFinished) {
        return;
      }

      this.nextTick = window.requestAnimationFrame(callDoAnimationTick);
    };

    callDoAnimationTick();
  }

  stopAnimation() {
    const { nextTick } = this;

    if (nextTick) {
      window.cancelAnimationFrame(nextTick);
    }

    this.isFinished = true;
  }

  doAnimationTick() {
    const {
      mass, tension, friction, initVelocity, from, to,
      previousTimestamp, prevVelocity, prevValue, onUpdate,
    } = this;

    //  Δt
    const currentTimestamp = performance.now();
    const deltaT = (currentTimestamp - (previousTimestamp || currentTimestamp))
      / 1000;
    //   Δt 46 
    const normalizedDeltaT = Math.min(deltaT, 0.046);

    let prevSafeVelocity = prevVelocity || initVelocity || 0;
    let prevSafeValue = prevValue || from;

    //   ,    
    const springRestoringForce = -1 * tension * (prevSafeValue - to);
    const dampingForce = -1 * prevSafeVelocity * friction;
    const acceleration = (springRestoringForce + dampingForce) / mass;

    //       
    const newVelocity = prevSafeVelocity + acceleration * normalizedDeltaT;
    const newValue  = prevSafeValue + newVelocity * normalizedDeltaT;

    //    
    const precision = 0.001;
    const isFinished = Math.abs(newVelocity) < precision
      && Math.abs(newValue - to) < precision;

    onUpdate({ value: newValue, isFinished });

    this.previousTimestamp = currentTimestamp;
    this.prevValue = newValue;
    this.prevVelocity = newVelocity;
    this.isFinished = isFinished;

    return isFinished;
  }
}

Sandbox con código de clase Spring y un ejemplo de su uso:
https://playcode.io/590645

Mejora de animación de primavera menor


Hay un pequeño problema con nuestra clase de primavera: Δtserá diferente cada vez con nuevos comienzos de la animación.

Abrir en un teléfono celular viejoΔtserá igual a 46 milisegundos, nuestro límite máximo. Abrir en una computadora poderosa -Δtserá de 16-17 milisegundos.

CambiandoΔtsignifica que la duración de la animación y los cambios en el valor animado serán ligeramente diferentes cada vez.

Para evitar que esto suceda, podemos hacer esto:

tomarΔtcomo un valor fijo Con un nuevo intervalo de animación, tendremos que calcular cuánto tiempo ha pasado desde el último intervalo y cuántos valores fijos hay en él.Δt. Si el tiempo no es divisible porΔt, luego transfiera el resto al siguiente intervalo de animación.

Entonces calculamosX y Vtantas veces como obtuvimos valores fijos. Último valorXmostrando al usuario.

Ejemplo:

Δttomemos como 1 milisegundo, 32 milisegundos pasados ​​entre el último y el nuevo intervalo de animación.

Tenemos que calcular la física 32 veces,X y V; últimoXdebe mostrarse al usuario.

Así es como se verá el método doAnimationTick:

class Spring {
  /* ... */

  doAnimationTick() {
    const {
      mass, tension, friction, initVelocity, from, to,
      previousTimestamp, prevVelocity, prevValue, onUpdate,
    } = this;

    const currentTimestamp = performance.now();
    const fractionalDiff = currentTimestamp - (previousTimestamp || currentTimestamp);
    const naturalDiffPart = Math.floor(fractionalDiff);
    const decimalDiffPart = fractionalDiff % 1;
    const normalizedDiff = Math.min(naturalDiffPart, 46);

    let safeVelocity = prevVelocity || initVelocity || 0;
    let safeValue = prevValue || from;

    //     1-  
    for (let i = 0; i < normalizedDiff; i++) {
      const springRestoringForce = -1 * tension * (safeValue - to);
      const dampingForce = -1 * safeVelocity * friction;
      const acceleration = (springRestoringForce + dampingForce) / mass;

      safeVelocity = safeVelocity + acceleration / 1000;
      safeValue  = safeValue + safeVelocity / 1000;
    }

    const precision = 0.001;
    const isFinished = Math.abs(safeVelocity) < precision
      && Math.abs(safeValue - to) < precision;

    onUpdate({ value: safeValue, isFinished });

    //       ,
    //      
    this.previousTimestamp = currentTimestamp - decimalDiffPart;
    this.prevValue = safeValue;
    this.prevVelocity = safeVelocity;
    this.isFinished = isFinished;

    return isFinished;
  }
}

Este método de cálculo físico usa react-spring, en este artículo puedes leer más.

Sobre el coeficiente de atenuación y el período de oscilación intrínseca


Al observar las propiedades del sistema (la masa de la carga, el coeficiente de elasticidad y el coeficiente de fricción del medio), no está completamente claro cómo se comporta el resorte. Al crear animaciones de Spring, estas variables se seleccionan aleatoriamente hasta que el programador esté satisfecho con el "springiness" de la animación.

Sin embargo, en física hay un par de variables para un resorte amortiguado, al observarlas se puede saber cómo se comporta el resorte.

Para encontrarlos, volvamos a nuestra ecuación de la segunda ley de Newton:

kXαV=ma


Lo escribimos como una ecuación diferencial:

kxαdxdt=md2xdt2


Se puede reescribir como:

d2xdt2+2ζω0dxdt+ω02x=0,


Dónde ω0=km- la frecuencia de oscilación del resorte sin tener en cuenta la fuerza de fricción, ζ=α2mk- coeficiente de atenuación.

Si resolvemos esta ecuación, obtenemos varias funciones.x(t)que determinan la posición de la primavera frente al tiempo. Pondré la solución debajo del spoiler para no estirar demasiado el artículo, iremos directamente al resultado.

Solución de ecuaciones
, :
https://www.youtube.com/watch?v=uI2xt8nTOlQ

:

x+2ζω0x+ω02x=0,ω0>0,ζ0


:

g2+2ζω0g+ω02=0


D=(2ζω0)24ω02=4ω02(ζ21)



:

1. D>0

D>0ζ>1.

D>0:

x(t)=C1er1t+C2er2t,


r1,2— , :

r1,2=2ζω0±4ω02(ζ21)2=ω0(ζ±ζ21)


:

x(t)=C1eω0β1t+C2eω0β2t,


β1,2=ζ±ζ21.

, 0.

2. D=0

D=0ζ=1.

D=0:

x(t)=(C1+C2t)ert,


r— , :

r=ζω0


:

x(t)=(C1+C2t)eζω0t



, 0. , D>0.

, , , 0 , D>0. , ?

3. D<0

D<0ζ<1.

D<0:

x(t)=eαt(C1cos(βt)+C2sin(βt))


αβ— , :

r1,2=α±βi=ω0ζ±ω0ζ21)=ω0ζ±ω01ζ2i


α=ω0ζ,β=ω01ζ2


:

x(t)=eω0ζt(C1cos(ω01ζ2t)+C2sin(ω01ζ2t))



, 0.

ζ, eω0ζt0, .. .

ζ=0.


A ζ<1obtenemos una ecuación que describe las vibraciones amortiguadas de un resorte. Menosζ, cuanto menor es la fuerza de fricción y las vibraciones más visibles.

Aζ=1obtenemos la ecuación con fricción crítica, es decir La ecuación en la que el sistema vuelve a la posición de equilibrio sin dudarlo, de la manera más rápida.

Aζ>1obtenemos una ecuación en la que el sistema vuelve a la posición de equilibrio sin dudarlo. Esto será más lento que conζ=1; con aumentoζ la tasa de convergencia a la posición de equilibrio disminuye.


Ejemplos de animaciones para varios ζ. Velocidad de animación reducida en 4 veces.

Además deζ, hay otra característica comprensible de las oscilaciones de resorte: el período de las oscilaciones de resorte sin tener en cuenta la fricción:

T=2πmk



Tenemos dos parámetros:

ζ, que determina cuánto fluctuará la animación.

Valores posiblesζ0 a 1.

menosζ, las vibraciones más visibles tendrá la primavera. En 0, el sistema no tendrá fricción, el resorte oscilará sin atenuación; en 1, el resorte convergerá a la posición de equilibrio sin dudarlo.

Los valores superiores a 1 no son necesarios para nosotros: funcionan como 1, solo en vano arrastran nuestra animación.

Tque dirá cuánto durará la animación.

Hay que tener en cuenta el parámetroThabla sobre la duración de una sola vibración del resorte sin tener en cuenta la fuerza de fricción, y no sobre la duración total de la animación.

Ahora podemos derivar el coeficiente de elasticidad y el coeficiente de fricción del medio, sabiendoζ y Ty suponiendo que m=1:

ζ=α2mk,T=2πmk


k=(2πT)2,α=4πζT



Implementamos esta lógica en el código:

const getSpringValues = ({ dampingRatio, period }) => {
  const mass = 1;
  const tension = Math.pow(2 * Math.PI / period, 2);
  const friction = 4 * Math.PI * dampingRatio / period;

  return {
    mass,
    tension,
    friction,
  };
};

//  

new Spring({
  ...getSpringValues({
    dampingRatio: 0.3, //   - 
    period: 0.6, //   600 
  }),
  from: 0,
  to: 20,
  onUpdate: () => { /* ... */ },
});

new Spring({
  ...getSpringValues({
    dampingRatio: 1, //   
    period: 0.2, //   200 
  }),
  from: 0,
  to: 20,
  onUpdate: () => { /* ... */ },
});

Ahora tenemos un método getSpringValues ​​que acepta el coeficiente de atenuación legible por humanos ζy periodo de oscilación Ty devuelve la masa del resorte, el coeficiente de fricción y el coeficiente de elasticidad.

Este método está en la caja de arena en el siguiente enlace, puede intentar usarlo en lugar de las propiedades habituales del sistema:
https://playcode.io/590645

Vue Spring Animation


Para usar convenientemente las animaciones de Spring en Vue, puede hacer dos cosas: escribir un componente contenedor o el método de composición-api .

Animación de primavera a través del componente


Escribamos un componente que abstraiga el uso de la clase Spring.

Antes de escribirlo, imagina cómo queremos usarlo:

<template>
  <div>
    <SpringWrapper
      :mass="1"
      :tension="170"
      :friction="14"
      :animationProps="{ x, y }" 
      :animate="true" 
      v-slot="{ x: newX, y: newY }"
    >
      <div :style="{ transform: `translate(${newX}px, ${newY}px)` }" />
    </SpringWrapper>
  </div>
</template>

<script>
import SpringWrapper from 'path/to/SpringWrapper';

export default {
  components: {
    SpringWrapper,
  },

  data() {
    return {
      x: 0,
      y: 0,
    };
  },

  mounted() {
    setTimeout(() => {
      this.x = 100;
      this.y = 100;
    }, 1000);
  },
};
</script>

Nos gustaría:

  • Podrías establecer las propiedades de la primavera
  • En el parámetro animationProps, fue posible especificar los campos que queremos animar
  • El parámetro animado pasó el valor de si animationProps debería ser animado
  • Cada vez que los campos cambian en animationProps, se cambian usando la animación Spring y se transfieren a la ranura del alcance

Para una funcionalidad completa, el componente aún debe poder cambiar la velocidad del resorte, pero no haremos esto para no complicar el código.

Comenzamos el desarrollo:

const SpringWrapper = {
  props: {
    friction: { type: Number, default: 10 },
    tension: { type: Number, default: 270 },
    mass: { type: Number, default: 1 },
    animationProps: { type: Object, required: true },
    animate: { type: Boolean, required: true },
  },

  data: () => ({
    //    
    actualAnimationProps: null,
  }),

  created() {
    this.actualAnimationProps = this.animationProps;
    // ,     Spring-
    this._springs = {};
  },

  watch: {
    animationProps: {
      deep: true,

      //    animationProps
      handler(animationProps) {
        const {
          friction, tension, mass,
          animate, actualAnimationProps, _springs,
        } = this;

        //     ,    
        if (!animate) {
          this.actualAnimationProps = animationProps;

          return;
        }

        //      
        Object.entries(animationProps).forEach((([key, value]) => {
          const _spring = _springs[key];
          const actualValue = actualAnimationProps[key];

          //      Spring-,
          //      
          if (_spring && !_spring.isFinished) {
            _spring.to = value;

            return;
          }

          const spring = new Spring({
            friction,
            tension,
            mass,
            initVelocity: 0,
            from: actualValue,
            to: value,
            onUpdate: ({ value }) => {
              actualAnimationProps[key] = value;
            },
          });

          spring.startAnimation();

          _springs[key] = spring;
        }));
      }
    },

    animate(val) {
      const { _springs } = this;

      //     ,
      //    Spring-
      if (!val) {
        Object.values(_springs).forEach((spring) => {
          spring.stopAnimation();
        })

        this.actualAnimationProps = this.animationProps;
      }
    },
  },

  render() {
    const { $scopedSlots, actualAnimationProps } = this;

    //     scopedSlot
    return $scopedSlots.default(actualAnimationProps)[0];
  },
};

Sandbox con un componente y un ejemplo de su uso:
https://playcode.io/591686/

Animación de primavera a través del método composición-api


Para usar composición-api, que aparecerá en Vue 3.0, necesitamos el paquete composición-api .

Agrégalo a nosotros mismos:

npm i @vue/composition-api

import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api';

Vue.use(VueCompositionApi);

Ahora pensemos cómo nos gustaría ver nuestro método de animación:

<template>
  <div>
    <div :style="{ transform: `translate(${x}px, ${y}px)` }" />
  </div>
</template>

<script>
import useSpring from 'path/to/useSpring';

export default {
  setup() {
    const { x, y, animate } = useSpring({
      mass: 1, 
      tension: 170, 
      friction: 10,
      x: 0,
      y: 0,
    });

    setTimeout(() => {
      x.value = 100;
      y.value = 100;
    }, 1000);

    return {
      x,
      y,
      animate,
    };
  },
};
</script>

Nos gustaría:

  • Podrías establecer las propiedades de la primavera
  • La función tomó un objeto con los campos que queremos animar como entrada.
  • La función devolvió contenedores calculados. Los setters aceptarán los valores que queremos animar como entrada; getters devolverán un valor animado
  • La función devolvió la variable animada, que será responsable de si necesitamos reproducir la animación o no.

Como en el caso del componente, no agregaremos personalización de la velocidad del resorte para no complicar el código.

Comenzamos a hacer un método:

import { reactive, computed, ref, watch } from '@vue/composition-api';
import { Spring } from '@cag-animate/core';

const useSpring = (initialProps) => {
  const {
    mass, tension, friction,
    animate: initialAnimate,
    ...restInitialProps
  } = initialProps;

  //  ,      
  const actualProps = reactive(restInitialProps);

  const springs = {};
  const mirrorProps = {};

  Object.keys(initialProps).forEach(key => {
    //     computed-
    // 
    // Getter     actualProps
    // Setter  Spring-
    mirrorProps[key] = computed({
      get: () => actualProps[key],
      set: val => {
        const _spring = springs[key];
        const actualValue = actualProps[key];

        if (!mirrorProps.animate.value) {
          actualProps[key] = val;

          return
        }

        if (_spring && !_spring.isFinished) {
          _spring.to = val;

          return;
        }

        const spring = new Spring({
          friction,
          tension,
          mass,
          initVelocity: 0,
          from: actualValue,
          to: val,
          onUpdate: ({ value }) => {
            actualProps[key] = value;
          },
        });

        spring.startAnimation();

        springs[key] = spring;
      },
    });
  });

  mirrorProps.animate = ref(initialAnimate || true);

  watch(() => mirrorProps.animate.value, (val) => {
    if (!val) {
      Object.entries(springs).forEach(([key, spring]) => {
        spring.stopAnimation();
        actualProps[key] = spring.to;
      });
    }
  });

  return mirrorProps;
};

export default useSpring;

Sandbox con el método de composición-api y un ejemplo de su uso:
https://playcode.io/591812/

La última palabra


En el artículo, examinamos las características básicas de las animaciones de Spring y las implementamos en Vue. Pero todavía tenemos margen para mejoras: en los componentes Vue puede agregar la capacidad de agregar nuevos campos después de la inicialización, agregar un cambio en la velocidad de la primavera.

Es cierto, escribir su Spring en proyectos reales no es necesario en absoluto: la biblioteca Popmotion ya tiene una implementación Spring preparada . Puede escribir un contenedor para Vue, como lo hicimos anteriormente.

¡Gracias por leer hasta el final!

Materiales usados



All Articles