Animasi musim semi di Vue

Halo, Habr!

Saya sudah lama ingin menambahkan animasi Spring ke proyek apa pun. Tetapi saya melakukan ini hanya untuk proyek Bereaksi menggunakan musim semi-reaksi, karena saya tidak tahu apa-apa lagi.

Namun akhirnya, saya memutuskan untuk mencari tahu cara kerjanya dan menulis implementasi saya!

Jika Anda juga ingin menggunakan animasi Musim Semi di mana-mana, pergilah ke bawah kucing. Di sana Anda akan menemukan beberapa teori, menerapkan Spring di JS murni, dan menerapkan animasi Spring di Vue menggunakan komponen dan komposisi-api.


TL; DR


Menerapkan Animasi Musim Semi di JS: https://playcode.io/590645/ .

Anda perlu menambahkan animasi Spring ke proyek Anda - ambil Spring dari perpustakaan Popmotion .

Anda perlu menambahkan animasi Spring ke proyek Vue Anda - ambil Spring dari perpustakaan Popmotion dan tulis pembungkus di atasnya.

Apa itu Animasi Musim Semi?


Animasi css konvensional di web menggunakan durasi dan fungsi untuk menghitung posisi dari waktu ke waktu.

Animasi musim semi menggunakan pendekatan yang berbeda - fisika pegas.

Untuk animasi, kita mengatur sifat fisik sistem: massa pegas, koefisien elastisitas pegas, koefisien gesekan medium. Kemudian kita meregangkan pegas ke jarak yang kita butuhkan untuk menghidupkan, dan melepaskannya.

Dalam proyek front-end, saya melihat keuntungan seperti itu di animasi Spring:

1. Anda dapat mengatur kecepatan animasi

awal.Jika situs memproses gesekan dengan jari-jari Anda atau seret dengan mouse, maka objek animasi akan terlihat lebih alami jika Anda mengaturnya dengan kecepatan awal seperti kecepatan gesek atau kecepatan mouse.


Animasi CSS yang tidak mempertahankan kecepatan mouse


Animasi pegas, di mana kecepatan mouse ditransmisikan ke pegas

2. Anda dapat mengubah titik akhir animasi

Jika, misalnya, Anda ingin menganimasikan objek ke kursor mouse, maka Anda tidak dapat membantu dengan CSS - saat Anda memperbarui posisi kursor, animasi diinisialisasi lagi, yang akan menyebabkan menyentak terlihat.

Tidak ada masalah dengan animasi Pegas - jika Anda perlu mengubah titik akhir animasi, kami merentangkan atau memadatkan pegas ke indentasi yang diinginkan.


Contoh menggerakkan elemen ke kursor mouse; animasi CSS - atas, animasi bawah - Spring

Fisika musim semi


Ambil pegas biasa dengan beban. Dia masih beristirahat.

Tarik sedikit ke bawah. Kekuatan elastis (Fβ†’=βˆ’kΞ”Xβ†’), yang berupaya mengembalikan pegas ke posisi semula. Saat kami melepaskan pegas, ia akan mulai berayun di sepanjang sinusoid:


Tambahkan gaya gesekan. Biarkan berbanding lurus dengan kecepatan pegas (Fβ†’=βˆ’Ξ±Vβ†’) Dalam hal ini, osilasi akan membusuk seiring waktu:


Kami ingin animasi Pegas kami dianimasikan dengan cara yang sama seperti pegas terombang-ambing ketika terkena elastisitas dan gesekan.

Kami mempertimbangkan posisi pegas untuk animasi


Dalam konteks animasi, perbedaan antara nilai awal dan akhir dari animasi adalah ketegangan awal pegas X0β†’.

Sebelum dimulainya animasi, kita diberikan: pegasm, koefisien pegas elastisitas k, koefisien gesekan medium Ξ±ketegangan pegas X0β†’, kecepatan mulai V0β†’.

Posisi animasi dihitung beberapa puluh kali per detik. Interval waktu antara perhitungan animasi disebutΞ”t.

Selama setiap perhitungan baru, kami memiliki kecepatan pegasV→ dan ketegangan pegas X→pada interval animasi terakhir. Untuk perhitungan animasi pertamaX→=X0→, V→=V0→.

Kami mulai menghitung posisi animasi untuk interval!

Menurut hukum kedua Newton, jumlah semua gaya yang diterapkan adalah sama dengan massa tubuh dikalikan dengan akselerasinya:

F→=ma→


Dua gaya bekerja pada pegas - gaya elastis dan gaya gesekan:

F→+F→=ma→


Kami akan menuliskan kekuatan secara lebih rinci dan menemukan akselerasi:

βˆ’kXβ†’βˆ’Ξ±Vβ†’=maβ†’


aβ†’=βˆ’kXβ†’βˆ’Ξ±Vβ†’m


Setelah itu kita dapat menemukan kecepatan baru. Itu sama dengan jumlah kecepatan dalam interval animasi sebelumnya dan percepatan dikalikan dengan interval waktu:

Vβ†’=Vβ†’+aβ†’βˆ—Ξ”t


Mengetahui kecepatan, Anda dapat menemukan posisi baru pegas:

Xβ†’=Xβ†’+Vβ†’βˆ—Ξ”t



Kami mendapat posisi yang tepat, menampilkannya kepada pengguna!

Setelah ditampilkan, mulailah interval animasi baru. Untuk interval baruX→=X→, V→=V→.

Animasi ini layak dihentikan ketika‖X→‖ dan ‖V→‖menjadi sangat kecil - pada saat ini osilasi pegas hampir tidak terlihat.

JS Spring Animation


Kami memiliki formula yang diperlukan, masih menulis implementasi kami:

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 dengan kode kelas Spring dan contoh penggunaannya:
https://playcode.io/590645

Peningkatan Animasi Musim Semi Kecil


Ada masalah kecil dengan kelas Musim Semi kami - Ξ”takan berbeda setiap kali dengan permulaan animasi yang baru.

Buka di ponsel lama -Ξ”takan sama dengan 46 milidetik, batas maksimum kami. Buka di komputer yang kuat -Ξ”takan menjadi 16-17 milidetik.

BerubahΞ”tberarti bahwa durasi animasi dan perubahan nilai animasi akan sedikit berbeda setiap kali.

Untuk mencegah hal ini terjadi, kita dapat melakukan ini:

AmbilΞ”tsebagai nilai tetap. Dengan interval animasi baru, kita harus menghitung berapa banyak waktu yang telah berlalu sejak interval terakhir dan berapa banyak nilai tetap di dalamnyaΞ”t. Jika waktu tidak habis dibagiΞ”t, lalu transfer sisanya ke interval animasi berikutnya.

Lalu kita hitungX→ dan V→sebanyak yang kita dapatkan nilai tetap. Nilai terakhirX→ditampilkan kepada pengguna.

Contoh:

Ξ”tmari kita ambil 1 milidetik, 32 milidetik berlalu antara interval animasi terakhir dan baru.

Kita harus menghitung fisika 32 kali,X→ dan V→; terakhirX→harus ditunjukkan kepada pengguna.

Inilah yang akan terlihat seperti metode 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;
  }
}

Metode perhitungan fisika ini menggunakan reaksi-pegas, dalam artikel ini Anda dapat membaca lebih lanjut.

Tentang koefisien atenuasi dan periode osilasi intrinsik


Melihat sifat-sifat sistem - massa beban, koefisien elastisitas dan koefisien gesekan medium - sama sekali tidak jelas bagaimana musim semi berperilaku. Saat membuat animasi Musim Semi, variabel-variabel ini dipilih secara acak sampai programmer puas dengan "springiness" dari animasi.

Namun, dalam fisika ada beberapa variabel untuk pegas teredam, melihat mereka Anda bisa tahu bagaimana musim semi berperilaku.

Untuk menemukannya, mari kembali ke persamaan kita tentang hukum kedua Newton:

βˆ’kXβ†’βˆ’Ξ±Vβ†’=maβ†’


Kami menulisnya sebagai persamaan diferensial:

βˆ’kxβˆ’Ξ±dxdt=md2xdt2


Itu dapat ditulis ulang sebagai:

d2xdt2+2ΞΆΟ‰0dxdt+Ο‰02x=0,


Dimana Ο‰0=km- Frekuensi osilasi pegas tanpa memperhitungkan gaya gesekan, ΞΆ=Ξ±2mk- koefisien atenuasi.

Jika kita memecahkan persamaan ini, maka kita mendapatkan beberapa fungsix(t)yang menentukan posisi pegas versus waktu. Saya akan meletakkan solusinya di bawah spoiler agar tidak terlalu banyak meregangkan artikel, kami akan langsung menuju hasilnya.

Solusi persamaan
, :
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.


Di ΞΆ<1kita mendapatkan persamaan yang menggambarkan getaran pegas yang teredam. KurangΞΆ, semakin rendah gaya gesek dan getaran yang lebih terlihat.

DiΞΆ=1kita mendapatkan persamaan dengan gesekan kritis, yaitu persamaan di mana sistem kembali ke posisi keseimbangan tanpa ragu-ragu, dengan cara tercepat.

DiΞΆ>1kita mendapatkan persamaan di mana sistem kembali ke posisi keseimbangan tanpa ragu-ragu. Ini akan lebih lambat dibandingkan denganΞΆ=1; dengan peningkatanΞΆ laju konvergensi ke posisi keseimbangan menurun.


Contoh animasi untuk berbagai ΞΆ. Kecepatan animasi dikurangi dengan 4 kali.

SelainΞΆ, ada karakteristik lain dari osilasi pegas - periode osilasi pegas tanpa memperhitungkan gesekan:

T=2Ο€mk



Kami mendapat dua parameter:

β†’ΞΆ, yang menentukan seberapa banyak animasi akan berfluktuasi.

Nilai yang MungkinΞΆ0 hingga 1.

kurangΞΆ, semakin banyak getaran yang terlihat pada musim semi. Pada 0, sistem tidak akan memiliki gesekan, pegas akan berosilasi tanpa pelemahan; pada 1, pegas akan menyatu ke posisi keseimbangan tanpa ragu-ragu.

Nilai yang lebih besar dari 1 tidak diperlukan bagi kami - nilai tersebut berfungsi sebagai 1, hanya sia-sia menyeret animasi kami.

β†’Tyang akan mengatakan berapa lama animasi akan bertahan.

Harus diingat parameternyaTberbicara tentang durasi satu getaran pegas tanpa memperhitungkan gaya gesekan, dan bukan tentang total durasi animasi.

Sekarang kita dapat memperoleh koefisien elastisitas dan koefisien gesekan medium, dengan mengetahuiΞΆ dan Tdan dengan asumsi itu m=1:

ΞΆ=Ξ±2mk,T=2Ο€mk


k=(2Ο€T)2,Ξ±=4π΢T



Kami menerapkan logika ini dalam kode:

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

Sekarang kami memiliki metode getSpringValues ​​yang menerima koefisien atenuasi yang dapat dibaca manusia ΞΆdan periode osilasi Tdan mengembalikan massa pegas, koefisien gesekan dan koefisien elastisitas.

Metode ini ada di kotak pasir di tautan di bawah ini, Anda dapat mencoba menggunakannya alih-alih properti sistem yang biasa:
https://playcode.io/590645

Vue Spring Animation


Untuk menggunakan animasi Spring dengan mudah di Vue, Anda dapat melakukan dua hal: menulis komponen pembungkus atau metode komposisi-api .

Animasi pegas melalui komponen


Mari kita menulis komponen yang mengabstraksi penggunaan kelas Spring.

Sebelum menulis, bayangkan bagaimana kita ingin menggunakannya:

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

Kami ingin:

  • Anda bisa mengatur properti pegas
  • Dalam parameter animationProps, dimungkinkan untuk menentukan bidang yang ingin kita menghidupkan
  • Parameter animasi melewati nilai apakah animasiProp harus dianimasikan
  • Setiap kali bidang berubah di animationProps, bidang tersebut diubah menggunakan animasi Spring dan ditransfer ke slot yang dicakup

Untuk fungsionalitas penuh, komponen masih harus dapat mengubah kecepatan pegas, tetapi kami tidak akan melakukan ini agar tidak menyulitkan kode.

Kami memulai pengembangan:

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 dengan komponen dan contoh penggunaannya:
https://playcode.io/591686/

Animasi musim semi melalui metode komposisi-api


Untuk menggunakan komposisi-api, yang akan muncul di Vue 3.0, kita memerlukan paket komposisi-api .

Tambahkan ke diri kita sendiri:

npm i @vue/composition-api

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

Vue.use(VueCompositionApi);

Sekarang mari kita pikirkan bagaimana kita ingin melihat metode animasi kita:

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

Kami ingin:

  • Anda bisa mengatur properti pegas
  • Fungsi mengambil objek dengan bidang yang ingin kita beri animasi sebagai input.
  • Fungsi mengembalikan pembungkus yang dikomputasi. Setters akan menerima nilai-nilai yang ingin kita animate sebagai input; getter akan mengembalikan nilai animasi
  • Fungsi mengembalikan variabel bernyawa, yang akan bertanggung jawab untuk apakah kita perlu memutar animasi atau tidak.

Seperti dalam kasus komponen, kami tidak akan menambahkan penyesuaian kecepatan pegas agar tidak menyulitkan kode.

Kami mulai membuat metode:

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 dengan metode komposisi-api dan contoh penggunaannya:
https://playcode.io/591812/

Kata terakhir


Dalam artikel tersebut, kami memeriksa fitur dasar animasi Spring dan mengimplementasikannya di Vue. Tetapi kami masih memiliki ruang untuk perbaikan - dalam komponen Vue Anda dapat menambahkan kemampuan untuk menambahkan bidang baru setelah inisialisasi, menambahkan perubahan dalam kecepatan pegas.

Benar, menulis Spring Anda di proyek nyata tidak diperlukan sama sekali: perpustakaan Popmotion sudah memiliki implementasi Musim Semi yang sudah jadi . Anda dapat menulis pembungkus untuk Vue, seperti yang kami lakukan di atas.

Terima kasih sudah membaca sampai akhir!

Bahan bekas



All Articles