Animations printaniĂšres dans Vue

Bonjour, Habr!

Je souhaite depuis longtemps ajouter des animations Spring Ă  tous les projets. Mais je ne l'ai fait que pour les projets React utilisant react-spring, car je ne savais rien d'autre.

Mais finalement, j'ai décidé de comprendre comment tout cela fonctionne et d'écrire mon implémentation!

Si vous souhaitez également utiliser des animations Spring partout, passez sous le chat. Vous y trouverez une théorie, l'implémentation de Spring en JS pur et l'implémentation de l'animation Spring dans Vue en utilisant les composants et la composition-api.


TL; DR


Implémentation de Spring Animation dans JS: https://playcode.io/590645/ .

Vous devez ajouter des animations Spring Ă  votre projet - prenez Spring dans la bibliothĂšque Popmotion .

Vous devez ajouter des animations Spring Ă  votre projet Vue - prenez Spring dans la bibliothĂšque Popmotion et Ă©crivez un wrapper dessus.

À quoi servent les animations de printemps?


Les animations CSS classiques sur le Web utilisent une durée et une fonction pour calculer la position de temps en temps.

Les animations de printemps utilisent une approche différente - la physique du printemps.

Pour l'animation, nous définissons les propriétés physiques du systÚme: la masse du ressort, le coefficient d'élasticité du ressort et le coefficient de frottement du milieu. Ensuite, nous étendons le ressort à la distance dont nous avons besoin pour l'animer et le relùchons.

Dans les projets front-end, je vois de tels avantages dans les animations Spring:

1. Vous pouvez définir la vitesse d'animation initiale.

Si le site traite les balayages avec vos doigts ou faites glisser avec la souris, les animations d'objet seront plus naturelles si vous les définissez avec la vitesse initiale comme la vitesse de balayage ou la vitesse de la souris.


Animation CSS qui ne conserve pas la vitesse de la souris


Spring-animation, dans laquelle la vitesse de la souris est transmise au printemps

2. Vous pouvez changer le point final de l'animation

Si, par exemple, vous souhaitez animer un objet avec le curseur de la souris, vous ne pouvez pas aider avec CSS - lorsque vous mettez à jour la position du curseur, l'animation est à nouveau initialisée, ce qui entraßnera des secousses sont visibles.

Il n'y a pas un tel problÚme avec Spring-animations - si vous devez changer le point final de l'animation, nous étirons ou compressons le ressort au retrait souhaité.


Un exemple d'animation d'un élément à un curseur de souris; haut - animation CSS, bas - animation Spring

Physique du printemps


Prenez un ressort ordinaire avec une charge. Elle est toujours au repos.

Tirez-la un peu. Une force Ă©lastique (F→=−kΔX→), qui cherche Ă  remettre le ressort dans sa position d'origine. Lorsque nous relĂąchons le ressort, il commencera Ă  se balancer le long de la sinusoĂŻde:


Ajoutez une force de friction. Soit directement proportionnelle Ă  la vitesse du ressort (F→=−αV→) Dans ce cas, les oscillations se dĂ©sintĂšgrent avec le temps:


Nous voulons que nos animations de ressort soient animĂ©es de la mĂȘme maniĂšre qu'un ressort oscille lorsqu'il est exposĂ© Ă  l'Ă©lasticitĂ© et au frottement.

Nous considérons la position du ressort pour l'animation


Dans le cadre des animations, la diffĂ©rence entre la valeur initiale et finale de l'animation est la tension initiale du ressort X0→.

Avant le dĂ©but de l'animation, on nous donne: masse de ressortm, coefficient d'Ă©lasticitĂ© du ressort k, coefficient de frottement du fluide αla tension du ressort X0→, vitesse de dĂ©marrage V0→.

La position d'animation est calculĂ©e plusieurs dizaines de fois par seconde. L'intervalle de temps entre les calculs d'animation est appeléΔt.

Lors de chaque nouveau calcul, nous avons la vitesse du ressortV→ et la tension du ressort X→sur le dernier intervalle d'animation. Pour le premier calcul d'animationX→=X0→, V→=V0→.

Nous commençons à calculer la position de l'animation pour l'intervalle!

Selon la deuxiÚme loi de Newton, la somme de toutes les forces appliquées est égale à la masse du corps multipliée par son accélération:

F→=ma→


Deux forces agissent sur un ressort - force Ă©lastique et force de friction:

F→+F→=ma→


Nous noterons les forces plus en détail et trouverons l'accélération:

−kX→−αV→=ma→


a→=−kX→−αV→m


AprÚs cela, nous pouvons trouver une nouvelle vitesse. Elle est égale à la somme de la vitesse dans l'intervalle d'animation précédent et de l'accélération multipliée par l'intervalle de temps:

V→=V→+a→∗Δt


Connaissant la vitesse, vous pouvez trouver la nouvelle position du ressort:

X→=X→+V→∗Δt



Nous avons obtenu la bonne position, affichez-la Ă  l'utilisateur!

Une fois affichĂ©, dĂ©marrez un nouvel intervalle d'animation. Pour un nouvel intervalleX→=X→, V→=V→.

L'animation mĂ©rite d'ĂȘtre arrĂȘtĂ©e lorsque‖X→‖ et ‖V→‖deviennent trĂšs petites - en ce moment les oscillations du ressort sont presque invisibles.

JS Spring Animation


Nous avons les formules nécessaires, il reste à écrire notre implémentation:

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 avec le code de classe Spring et un exemple de son utilisation:
https://playcode.io/590645

Amélioration mineure de l'animation printaniÚre


Il y a un petit problĂšme avec notre classe de printemps - Δtsera diffĂ©rent Ă  chaque fois avec de nouveaux dĂ©parts de l'animation.

Ouvert sur un vieux tĂ©lĂ©phone portable -Δtsera Ă©gal Ă  46 millisecondes, notre limite maximale. Ouvert sur un ordinateur puissant -Δtsera de 16 Ă  17 millisecondes.

En changeantΔtsignifie que la durĂ©e de l'animation et les modifications de la valeur animĂ©e seront lĂ©gĂšrement diffĂ©rentes Ă  chaque fois.

Pour Ă©viter que cela se produise, nous pouvons le faire:

PrenezΔtcomme valeur fixe. Avec un nouvel intervalle d'animation, nous devrons calculer combien de temps s'est Ă©coulĂ© depuis le dernier intervalle et combien de valeurs fixes s'y trouventΔt. Si le temps n'est pas divisible parΔt, puis transfĂ©rez le reste Ă  l'intervalle d'animation suivant.

Ensuite, nous calculonsX→ et V→autant de fois que nous avons obtenu des valeurs fixes. Derniùre valeurX→montrer à l'utilisateur.

Exemple:

Δtprenons comme 1 milliseconde, 32 millisecondes passĂ©es entre le dernier et le nouvel intervalle d'animation.

Nous devons calculer la physique 32 fois,X→ et V→; dernierX→doit ĂȘtre montrĂ© Ă  l'utilisateur.

Voici à quoi ressemblera la méthode 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;
  }
}

Cette méthode de calcul de la physique utilise le ressort de réaction, dans cet article, vous pouvez en savoir plus.

A propos du coefficient d'atténuation et de la période d'oscillation intrinsÚque


En regardant les propriétés du systÚme - la masse de la charge, le coefficient d'élasticité et le coefficient de frottement du milieu - on ne sait pas trÚs bien comment se comporte le ressort. Lors de la création d'animations Spring, ces variables sont sélectionnées de maniÚre aléatoire jusqu'à ce que le programmeur soit satisfait de la "réactivité" de l'animation.

Cependant, en physique, il existe quelques variables pour un ressort amorti, en les regardant, vous pouvez dire comment le ressort se comporte.

Pour les trouver, revenons Ă  notre Ă©quation de la deuxiĂšme loi de Newton:

−kX→−αV→=ma→


Nous l'écrivons comme une équation différentielle:

−kx−αdxdt=md2xdt2


Il peut ĂȘtre rĂ©Ă©crit comme:

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


OĂč ω0=km- la frĂ©quence des oscillations du ressort sans tenir compte de la force de frottement, ζ=α2mk- coefficient d'attĂ©nuation.

Si nous résolvons cette équation, alors nous obtenons plusieurs fonctionsx(t)qui déterminent la position du ressort en fonction du temps. Je vais mettre la solution sous le spoiler pour ne pas trop étirer l'article, on ira directement au résultat.

Solution d'Ă©quation
, :
https://www.youtube.com/watch?v=uI2xt8nTOlQ

:

x″+2ζω0xâ€Č+ω02x=0,ω0>0,ζ≄0


:

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


D=(2ζω0)2−4ω02=4ω02(ζ2−1)



:

1. D>0

D>0ζ>1.

D>0:

x(t)=C1er1t+C2er2t,


r1,2— , :

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


:

x(t)=C1e−ω0ÎČ1t+C2e−ω0ÎČ2t,


ÎČ1,2=ζ±ζ2−1.

, 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)+C2∗sin(ÎČt))


αÎČ— , :

r1,2=α±ÎČi=−ω0ζ±ω0ζ2−1)=−ω0ζ±ω01−ζ2i


α=−ω0ζ,ÎČ=ω01−ζ2


:

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



, 0.

ζ, e−ω0ζt0, .. .

ζ=0.


À ζ<1nous obtenons une Ă©quation qui dĂ©crit les vibrations amorties d'un ressort. Moinsζ, plus la force de friction est faible et plus les vibrations sont visibles.

Àζ=1nous obtenons l'Ă©quation avec le frottement critique, c'est-Ă -dire l'Ă©quation dans laquelle le systĂšme revient Ă  la position d'Ă©quilibre sans hĂ©sitation, de la maniĂšre la plus rapide.

Àζ>1on obtient une Ă©quation dans laquelle le systĂšme revient sans hĂ©sitation Ă  la position d'Ă©quilibre. Ce sera plus lent qu'avecζ=1; avec augmentationζ le taux de convergence vers la position d'Ă©quilibre diminue.


Exemples d'animations pour divers ζ. Vitesse d'animation réduite de 4 fois.

En plus deζ, il existe une autre caractéristique compréhensible des oscillations du ressort - la période des oscillations du ressort sans tenir compte du frottement:

T=2πmk



Nous avons obtenu deux paramĂštres:

→ζ, qui dĂ©termine la variation de l'animation.

Valeurs possiblesζ0 à 1.

moinsζ, plus les vibrations du ressort seront visibles. A 0, le systÚme n'aura pas de frottement, le ressort oscillera sans atténuation; à 1, le ressort convergera sans hésitation vers la position d'équilibre.

Les valeurs supérieures à 1 ne sont pas nécessaires pour nous - elles fonctionnent comme 1, seulement en vain faites glisser notre animation.

→Tqui indiquera la durĂ©e de l'animation.

Il convient de garder à l'esprit le paramÚtreTparle de la durée d'une seule vibration du ressort sans tenir compte de la force de frottement, et non de la durée totale de l'animation.

Maintenant, nous pouvons dériver le coefficient d'élasticité et le coefficient de frottement du milieu, sachantζ et Tet en supposant que m=1:

ζ=α2mk,T=2πmk


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



Nous implémentons cette logique dans le code:

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: () => { /* ... */ },
});

Nous avons maintenant une mĂ©thode getSpringValues ​​qui accepte un coefficient d'attĂ©nuation lisible par l'homme ζet pĂ©riode d'oscillation Tet renvoie la masse du ressort, le coefficient de frottement et le coefficient d'Ă©lasticitĂ©.

Cette méthode est dans le bac à sable sur le lien ci-dessous, vous pouvez essayer de l'utiliser à la place des propriétés systÚme habituelles:
https://playcode.io/590645

Vue Spring Animation


Pour utiliser facilement les animations Spring dans Vue, vous pouvez faire deux choses: écrire un composant wrapper ou la méthode composition-api .

Animation de printemps via le composant


Écrivons un composant qui rĂ©sume l'utilisation de la classe Spring.

Avant de l'Ă©crire, imaginez comment nous voulons l'utiliser:

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

Nous voudrions:

  • Vous pouvez dĂ©finir les propriĂ©tĂ©s du ressort
  • Dans le paramĂštre animationProps, il Ă©tait possible de spĂ©cifier les champs que nous voulons animer
  • Le paramĂštre animate a transmis la valeur si animationProps doit ĂȘtre animĂ©e
  • Chaque fois que les champs changent dans animationProps, ils sont modifiĂ©s Ă  l'aide de l'animation Spring et transfĂ©rĂ©s vers l'emplacement dĂ©limitĂ©

Pour une fonctionnalité complÚte, le composant doit encore pouvoir changer la vitesse du ressort, mais nous ne le ferons pas afin de ne pas compliquer le code.

Nous commençons le développement:

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 avec un composant et un exemple de son utilisation:
https://playcode.io/591686/

Animation de printemps via la méthode de composition-api


Pour utiliser composition-api, qui apparaĂźtra dans Vue 3.0, nous avons besoin du package composition-api .

Ajoutez-le Ă  nous:

npm i @vue/composition-api

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

Vue.use(VueCompositionApi);

Voyons maintenant comment nous aimerions voir notre méthode d'animation:

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

Nous voudrions:

  • Vous pouvez dĂ©finir les propriĂ©tĂ©s du ressort
  • La fonction a pris un objet avec les champs que nous voulons animer en entrĂ©e.
  • La fonction a renvoyĂ© des wrappers calculĂ©s. Les setters accepteront les valeurs que nous voulons animer en entrĂ©e; les getters retourneront une valeur animĂ©e
  • La fonction a renvoyĂ© la variable animate, qui sera responsable de savoir si nous devons jouer l'animation ou non.

Comme dans le cas du composant, nous n'ajouterons pas de personnalisation de la vitesse du ressort afin de ne pas compliquer le code.

Nous commençons à faire une méthode:

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 avec la méthode de composition-api et un exemple de son utilisation:
https://playcode.io/591812/

Le dernier mot


Dans l'article, nous avons examiné les fonctionnalités de base des animations Spring et les avons implémentées dans Vue. Mais nous avons encore de la place pour des améliorations - dans les composants Vue, vous pouvez ajouter la possibilité d'ajouter de nouveaux champs aprÚs l'initialisation, d'ajouter un changement dans la vitesse du ressort.

Certes, Ă©crire votre Spring dans de vrais projets n'est pas du tout nĂ©cessaire: la bibliothĂšque Popmotion a dĂ©jĂ  une implĂ©mentation Spring prĂȘte Ă  l'emploi . Vous pouvez Ă©crire un wrapper pour Vue, comme nous l'avons fait ci-dessus.

Merci d'avoir lu jusqu'au bout!

Matériaux utilisés



All Articles