Frühlingsanimationen in Vue

Hallo Habr!

Ich wollte schon lange Frühlingsanimationen zu Projekten hinzufügen. Aber ich habe das nur für React-Projekte mit React-Spring gemacht, weil ich sonst nichts wusste.

Aber schließlich habe ich mich entschlossen herauszufinden, wie das alles funktioniert und meine Implementierung zu schreiben!

Wenn Sie auch überall Frühlingsanimationen verwenden möchten, gehen Sie unter die Katze. Dort finden Sie einige Theorien, die Spring in reinem JS implementieren und Spring-Animationen in Vue mithilfe von Komponenten und Composition-API implementieren.


TL; DR


Implementieren der Frühlingsanimation in JS: https://playcode.io/590645/ .

Sie müssen Ihrem Projekt Spring-Animationen hinzufügen - nehmen Sie Spring aus der Popmotion- Bibliothek .

Sie müssen Ihrem Vue-Projekt Spring-Animationen hinzufügen. Nehmen Sie Spring aus der Popmotion-Bibliothek und schreiben Sie einen Wrapper darüber.

Wofür sind Frühlingsanimationen?


Herkömmliche CSS-Animationen im Web verwenden von Zeit zu Zeit eine Dauer und eine Funktion zur Berechnung der Position.

Frühlingsanimationen verwenden einen anderen Ansatz - die Frühlingsphysik.

Für die Animation legen wir die physikalischen Eigenschaften des Systems fest: die Masse der Feder, den Elastizitätskoeffizienten der Feder, den Reibungskoeffizienten des Mediums. Dann dehnen wir die Feder auf die Entfernung, die wir zum Animieren benötigen, und lassen sie los.

In Front-End-Projekten sehe ich solche Vorteile in Frühlingsanimationen:

1. Sie können die anfängliche Animationsgeschwindigkeit

festlegen . Wenn die Site Wischvorgänge mit Ihren Fingern verarbeitet oder mit der Maus zieht, sehen die Objektanimationen natürlicher aus, wenn Sie sie mit der Anfangsgeschwindigkeit wie Wischgeschwindigkeit oder Mausgeschwindigkeit einstellen.


CSS-Animation, die die Mausgeschwindigkeit nicht beibehält


Federanimation, bei der die Geschwindigkeit der Maus auf die Feder

2 übertragen wird. Sie können den Endpunkt der Animation ändern.

Wenn Sie beispielsweise ein Objekt mit dem Mauszeiger animieren möchten, können Sie nicht mit CSS helfen. Wenn Sie die Cursorposition aktualisieren, wird die Animation erneut initialisiert, was dazu führt, dass die Animation erneut initialisiert wird Ruckeln ist sichtbar.

Bei Federanimationen gibt es kein solches Problem. Wenn Sie den Endpunkt der Animation ändern müssen, dehnen oder komprimieren wir die Feder auf den gewünschten Einzug.


Ein Beispiel für die Animation eines Elements mit einem Mauszeiger. oben - CSS-Animation, unten - Frühlingsanimation

Frühlingsphysik


Nehmen Sie eine gewöhnliche Feder mit einer Last. Sie ist immer noch in Ruhe.

Zieh sie ein bisschen runter. Eine elastische Kraft (F=kΔX), die versucht, die Feder in ihre ursprüngliche Position zurückzubringen. Wenn wir die Feder loslassen, beginnt sie entlang der Sinuskurve zu schwingen:


Reibungskraft hinzufügen. Lassen Sie es direkt proportional zur Geschwindigkeit der Feder sein (F=αV) In diesem Fall nehmen die Schwingungen mit der Zeit ab:


Wir möchten, dass unsere Federanimationen so animiert werden, wie eine Feder schwingt, wenn sie Elastizität und Reibung ausgesetzt ist.

Wir betrachten die Position des Frühlings für die Animation


Im Zusammenhang mit Animationen ist der Unterschied zwischen dem Anfangs- und Endwert der Animation die Anfangsspannung der Feder X0.

Vor dem Start der Animation erhalten wir: FedermassemFederelastizitätskoeffizient kReibungskoeffizient des Mediums αFederspannung X0Startgeschwindigkeit V0.

Die Animationsposition wird mehrere zehn Mal pro Sekunde berechnet. Das Zeitintervall zwischen Animationsberechnungen wird aufgerufenΔt.

Bei jeder neuen Berechnung haben wir die FedergeschwindigkeitV und Federspannung Xim letzten Animationsintervall. Für die erste Berechnung der AnimationX=X0, V=V0.

Wir beginnen die Position der Animation für das Intervall zu berechnen!

Nach dem zweiten Newtonschen Gesetz ist die Summe aller ausgeübten Kräfte gleich der Masse des Körpers multipliziert mit seiner Beschleunigung:

F=ma


Zwei Kräfte wirken auf eine Feder - elastische Kraft und Reibungskraft:

F+F=ma


Wir werden die Kräfte genauer aufschreiben und die Beschleunigung finden:

kXαV=ma


a=kXαVm


Danach können wir eine neue Geschwindigkeit finden. Dies entspricht der Summe der Geschwindigkeit im vorherigen Animationsintervall und der Beschleunigung multipliziert mit dem Zeitintervall:

V=V+aΔt


Wenn Sie die Geschwindigkeit kennen, können Sie die neue Position der Feder finden:

X=X+VΔt



Wir haben die richtige Position, zeigen Sie sie dem Benutzer!

Starten Sie nach der Anzeige ein neues Animationsintervall. Für ein neues IntervallX=X, V=V.

Die Animation ist es wert, angehalten zu werden, wennX und Vsehr klein werden - in diesem Moment sind die Federschwingungen fast unsichtbar.

JS Frühlingsanimation


Wir haben die notwendigen Formeln, es bleibt unsere Implementierung zu schreiben:

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 mit Spring-Klassencode und einem Anwendungsbeispiel:
https://playcode.io/590645

Kleinere Verbesserung der Frühlingsanimation


Es gibt ein kleines Problem mit unserer Frühlingsklasse - Δtwird jedes Mal anders sein, wenn die Animation neu gestartet wird.

Auf einem alten Handy öffnen -Δtentspricht 46 Millisekunden, unserer Höchstgrenze. Öffnen Sie auf einem leistungsstarken Computer -Δtwird 16-17 Millisekunden sein.

ÄndernΔtbedeutet, dass die Dauer der Animation und Änderungen des animierten Werts jedes Mal leicht unterschiedlich sind.

Um dies zu verhindern, können wir Folgendes tun:

NehmenΔtals fester Wert. Mit einem neuen Animationsintervall müssen wir berechnen, wie viel Zeit seit dem letzten Intervall vergangen ist und wie viele feste Werte darin enthalten sindΔt. Wenn die Zeit nicht teilbar ist durchΔtÜbertragen Sie dann den Rest in das nächste Animationsintervall.

Dann berechnen wirX und Vso oft wie wir feste Werte haben. Letzter WertXdem Benutzer zeigen.

Beispiel:

ΔtNehmen wir als 1 Millisekunde 32 Millisekunden zwischen dem letzten und dem neuen Animationsintervall.

Wir müssen die Physik 32 mal berechnen,X und V;; letzteXmuss dem Benutzer gezeigt werden.

So sieht die doAnimationTick-Methode aus:

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

Diese Methode zur Berechnung der Physik verwendet React-Spring. In diesem Artikel erfahren Sie mehr.

Über den Dämpfungskoeffizienten und die Eigenschwingungsperiode


Bei Betrachtung der Eigenschaften des Systems - der Masse der Last, des Elastizitätskoeffizienten und des Reibungskoeffizienten des Mediums - ist völlig unklar, wie sich die Feder verhält. Beim Erstellen von Frühlingsanimationen werden diese Variablen zufällig ausgewählt, bis der Programmierer mit der "Federung" der Animation zufrieden ist.

In der Physik gibt es jedoch einige Variablen für eine gedämpfte Feder. Wenn man sie betrachtet, kann man erkennen, wie sich die Feder verhält.

Um sie zu finden, kehren wir zu unserer Gleichung des zweiten Newtonschen Gesetzes zurück:

kXαV=ma


Wir schreiben es als Differentialgleichung:

kxαdxdt=md2xdt2


Es kann wie folgt umgeschrieben werden:

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


Wo ω0=km- die Schwingungsfrequenz der Feder ohne Berücksichtigung der Reibungskraft ζ=α2mk- Dämpfungskoeffizient.

Wenn wir diese Gleichung lösen, erhalten wir mehrere Funktionenx(t)die die Position der Feder gegen die Zeit bestimmen. Ich werde die Lösung unter den Spoiler stellen, um den Artikel nicht zu stark zu dehnen. Wir werden direkt zum Ergebnis übergehen.

Gleichungslösung
, :
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.


Beim ζ<1Wir erhalten eine Gleichung, die die gedämpften Schwingungen einer Feder beschreibt. WenigerζJe geringer die Reibungskraft und desto sichtbarer die Schwingungen.

Beimζ=1wir erhalten die Gleichung mit kritischer Reibung, d.h. die Gleichung, in der das System auf die schnellste Weise ohne zu zögern in die Gleichgewichtsposition zurückkehrt.

Beimζ>1Wir erhalten eine Gleichung, in der das System ohne zu zögern in die Gleichgewichtsposition zurückkehrt. Dies ist langsamer als mitζ=1;; mit Zunahmeζ Die Konvergenzrate zur Gleichgewichtsposition nimmt ab.


Beispiele für Animationen für verschiedene ζ. Animationsgeschwindigkeit um 4 - fache reduziert.

Nebenζgibt es eine weitere verständliche Eigenschaft von Federschwingungen - die Periode der Federschwingungen ohne Berücksichtigung der Reibung:

T=2πmk



Wir haben zwei Parameter:

ζHiermit wird festgelegt, wie stark die Animation schwankt.

Mögliche Werteζ0 bis 1.

wenigerζJe mehr sichtbare Schwingungen die Feder hat. Bei 0 hat das System keine Reibung, die Feder schwingt ohne Dämpfung; Bei 1 konvergiert die Feder ohne zu zögern in die Gleichgewichtsposition.

Werte größer als 1 sind für uns nicht erforderlich - sie funktionieren als 1, nur vergeblich ziehen Sie unsere Animation heraus.

THier erfahren Sie, wie lange die Animation dauert.

Es lohnt sich, den Parameter zu beachtenTspricht über die Dauer einer einzelnen Vibration der Feder ohne Berücksichtigung der Reibungskraft und nicht über die Gesamtdauer der Animation.

Nun können wir wissentlich den Elastizitätskoeffizienten und den Reibungskoeffizienten des Mediums ableitenζ und Tund unter der Annahme, dass m=1::

ζ=α2mk,T=2πmk


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



Wir implementieren diese Logik im 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: () => { /* ... */ },
});

Jetzt haben wir eine getSpringValues-Methode, die einen vom Menschen lesbaren Dämpfungskoeffizienten akzeptiert ζund Schwingungsdauer Tund gibt die Masse der Feder, den Reibungskoeffizienten und den Elastizitätskoeffizienten zurück.

Diese Methode befindet sich in der Sandbox unter dem folgenden Link. Sie können versuchen, sie anstelle der üblichen Systemeigenschaften zu verwenden:
https://playcode.io/590645

Vue Frühlingsanimation


Um Spring-Animationen bequem in Vue zu verwenden, können Sie zwei Dinge tun: Schreiben einer Wrapper-Komponente oder der Composition-API- Methode .

Frühlingsanimation durch Komponente


Schreiben wir eine Komponente, die die Verwendung der Spring-Klasse abstrahiert.

Stellen Sie sich vor dem Schreiben vor, wie wir es verwenden möchten:

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

Wir möchten:

  • Sie können die Eigenschaften der Feder einstellen
  • Im Parameter animationProps konnten die Felder angegeben werden, die animiert werden sollen
  • Der Animationsparameter hat den Wert übergeben, ob animationProps animiert werden soll
  • Wenn sich Felder in animationProps ändern, werden sie mithilfe der Spring-Animation geändert und in den Bereichsbereich übertragen

Für die volle Funktionalität muss die Komponente weiterhin in der Lage sein, die Geschwindigkeit der Feder zu ändern. Dies wird jedoch nicht durchgeführt, um den Code nicht zu komplizieren.

Wir beginnen mit der Entwicklung:

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 mit einer Komponente und einem Beispiel für ihre Verwendung:
https://playcode.io/591686/

Frühlingsanimation über die Kompositions-API-Methode


Um Composition-API zu verwenden, das in Vue 3.0 erscheinen wird, benötigen wir das Composition-API- Paket .

Fügen Sie es uns hinzu:

npm i @vue/composition-api

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

Vue.use(VueCompositionApi);

Lassen Sie uns nun darüber nachdenken, wie wir unsere Animationsmethode sehen möchten:

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

Wir möchten:

  • Sie können die Eigenschaften der Feder einstellen
  • Die Funktion hat ein Objekt mit den Feldern genommen, die wir als Eingabe animieren möchten.
  • Die Funktion gab berechnete Wrapper zurück. Setter akzeptieren die Werte, die wir animieren möchten, als Eingabe. Getter geben einen animierten Wert zurück
  • Die Funktion hat eine Animationsvariable zurückgegeben, die dafür verantwortlich ist, ob die Animation abgespielt werden muss oder nicht.

Wie im Fall der Komponente werden wir keine Anpassung der Federgeschwindigkeit hinzufügen, um den Code nicht zu komplizieren.

Wir beginnen eine Methode zu machen:

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 mit Composition-API-Methode und ein Beispiel für ihre Verwendung:
https://playcode.io/591812/

Das letzte Wort


In diesem Artikel haben wir die Grundfunktionen von Spring-Animationen untersucht und in Vue implementiert. Wir haben jedoch noch Raum für Verbesserungen - in Vue-Komponenten können Sie die Möglichkeit hinzufügen, nach der Initialisierung neue Felder hinzuzufügen und die Geschwindigkeit der Feder zu ändern.

Das Schreiben Ihres Spring in realen Projekten ist zwar überhaupt nicht erforderlich: Die Popmotion- Bibliothek verfügt bereits über eine vorgefertigte Spring- Implementierung . Sie können wie oben beschrieben einen Wrapper für Vue schreiben.

Vielen Dank für das Lesen bis zum Ende!

Gebrauchte Materialien



All Articles