الرسوم المتحركة الربيع في Vue

مرحبا يا هابر!

لطالما أردت إضافة رسوم متحركة للربيع إلى أي مشروع. لكنني فعلت ذلك فقط لمشاريع React التي تستخدم الربيع التفاعلي ، لأنني لم أكن أعرف أي شيء آخر.

لكن أخيرًا ، قررت معرفة كيف يعمل كل شيء وكتابة التنفيذ الخاص بي!

إذا كنت تريد أيضًا استخدام الرسوم المتحركة للربيع في كل مكان ، فابحث تحت القطة. ستجد هناك بعض النظريات ، وتطبيق Spring في JS النقي ، وتنفيذ الرسوم المتحركة Spring في Vue باستخدام المكونات وتكوين api.


TL ؛ د


تنفيذ الرسوم المتحركة الربيع في شبيبة: https://playcode.io/590645/ .

تحتاج إلى إضافة الرسوم المتحركة في الربيع إلى مشروعك - خذ الربيع من مكتبة Popmotion .

تحتاج إلى إضافة رسوم متحركة الربيع إلى مشروع Vue الخاص بك - خذ الربيع من مكتبة Popmotion واكتب غلافًا فوقه.

ما هي الرسوم المتحركة في الربيع؟


تستخدم الرسوم المتحركة التقليدية لـ css على الويب مدة ووظيفة لحساب الموقع من وقت لآخر.

تستخدم الرسوم المتحركة الربيعية نهجًا مختلفًا - فيزياء الربيع.

بالنسبة للرسوم المتحركة ، قمنا بتعيين الخصائص الفيزيائية للنظام: كتلة الربيع ، ومعامل مرونة الربيع ، ومعامل الاحتكاك للوسط. ثم نمد الزنبرك إلى المسافة التي نحتاجها لتحريكها وإطلاقها.

في مشاريع الواجهة الأمامية ، أرى مثل هذه المزايا في الرسوم المتحركة الربيعية:

1. يمكنك تعيين سرعة الرسوم المتحركة الأولية.

إذا كان الموقع يعالج الضربات الشديدة بأصابعك أو يسحب بالماوس ، فإن الرسوم المتحركة للكائن ستبدو أكثر طبيعية إذا قمت بتعيينها بالسرعة الأولية مثل سرعة التمرير أو سرعة الماوس.


الرسوم المتحركة CSS التي لا تحتفظ بسرعة الماوس


Spring-animation ، التي تنتقل فيها سرعة الماوس إلى الربيع

2. يمكنك تغيير نقطة نهاية الرسم المتحرك

إذا كنت تريد ، على سبيل المثال ، تحريك كائن ما إلى مؤشر الماوس ، فلا يمكنك المساعدة في CSS - عند تحديث موضع المؤشر ، تتم تهيئة الرسوم المتحركة مرة أخرى ، مما سيؤدي إلى الرجيج مرئي.

لا توجد مثل هذه المشكلة مع الرسوم المتحركة للربيع - إذا كنت بحاجة إلى تغيير نقطة نهاية الرسوم المتحركة ، فإننا نقوم بتمديد أو ضغط الربيع إلى المسافة البادئة المطلوبة.


مثال على تحريك عنصر إلى مؤشر الماوس ؛ أعلى - الرسوم المتحركة CSS ، الرسوم المتحركة في الأسفل - الربيع

فيزياء الربيع


خذ زنبركًا عاديًا بحمل. ما زالت في حالة راحة.

اسحبها إلى أسفل قليلاً. قوة مرنة (F=kΔX) ، الذي يسعى لإعادة الربيع إلى موقعه الأصلي. عندما نطلق الربيع ، سيبدأ في التأرجح على طول الجيوب الأنفية:


أضف قوة احتكاك. فليكن متناسبًا بشكل مباشر مع سرعة الربيع (F=αV) في هذه الحالة ، ستنحل التذبذبات بمرور الوقت:


نريد أن تكون صورنا المتحركة متحركة بنفس الطريقة التي يتذبذب بها الربيع عند تعرضه للمرونة والاحتكاك.

نعتبر موقف الربيع للرسوم المتحركة


في سياق الرسوم المتحركة ، فإن الفرق بين القيمة الأولية والقيمة النهائية للرسوم المتحركة هو التوتر الأولي للربيع X0.

قبل بدء الرسوم المتحركة ، يتم إعطاؤنا: كتلة الربيعmمعامل الربيع للمرونة kمعامل الاحتكاك للوسط αالتوتر في الربيع X0، سرعة البدء V0.

يتم احتساب موضع الرسوم المتحركة عدة عشرات المرات في الثانية. يسمى الفاصل الزمني بين حسابات الرسوم المتحركةΔt.

خلال كل عملية حسابية جديدة ، لدينا سرعة الربيعV وتوتر الربيع Xفي آخر فاصل للرسوم المتحركة. للحساب الأول للرسوم المتحركةX=X0، V=V0.

نبدأ في حساب موضع الرسوم المتحركة للفاصل الزمني!

وفقًا لقانون نيوتن الثاني ، فإن مجموع جميع القوى المطبقة يساوي كتلة الجسم مضروبًا في تسارعه:

F=ma


تعمل قوتان على زنبرك - قوة مرنة وقوة احتكاك:

F+F=ma


سنكتب القوى بمزيد من التفصيل ونجد التسارع:

kXαV=ma


a=kXαVm


بعد ذلك يمكننا العثور على سرعة جديدة. وهو يساوي مجموع السرعة في فاصل الرسوم المتحركة السابق والتسارع مضروبًا في الفاصل الزمني:

V=V+aΔt


معرفة السرعة ، يمكنك العثور على الوضع الجديد للربيع:

X=X+VΔt



لقد حصلنا على الموضع الصحيح ، عرضه للمستخدم!

بمجرد عرضها ، ابدأ فاصل زمني جديد للرسوم المتحركة. لفترة جديدةX=X، V=V.

الرسوم المتحركة تستحق التوقف عندX و Vتصبح صغيرة جدًا - في هذه اللحظة تكون تذبذبات الربيع غير مرئية تقريبًا.

شبيبة الربيع الرسوم المتحركة


لدينا الصيغ اللازمة ، يبقى أن نكتب تنفيذنا:

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 مع رمز فصل الربيع ومثال على استخدامه:
https://playcode.io/590645

تحسين الرسوم المتحركة الطفيفة في الربيع


هناك مشكلة صغيرة في فصل الربيع لدينا - Δtستختلف في كل مرة مع بداية جديدة للرسوم المتحركة.

فتح على هاتف خلوي قديم -Δtستساوي 46 مللي ثانية ، الحد الأقصى المسموح به لدينا. فتح على كمبيوتر قوي -Δtسيكون 16-17 مللي ثانية.

التغييرΔtيعني أن مدة الرسوم المتحركة والتغييرات في القيمة المتحركة ستكون مختلفة قليلاً في كل مرة.

لمنع هذا من الحدوث، يمكننا أن نفعل هذا:

خذواΔtكقيمة ثابتة. باستخدام فاصل زمني جديد للرسوم المتحركة ، سيتعين علينا حساب مقدار الوقت المنقضي منذ آخر فاصل زمني وعدد القيم الثابتة فيهΔt. إذا كان الوقت غير قابل للقسمة علىΔt، ثم انقل الباقي إلى فاصل الرسوم المتحركة التالي.

ثم نحسبX و Vعدة مرات حصلنا على قيم ثابتة. القيمة الأخيرةXتظهر للمستخدم.

مثال:

Δtلنأخذ 1 ميلي ثانية ، مرت 32 مللي ثانية بين الفاصل الزمني الأخير والجديد للرسوم المتحركة.

علينا أن نحسب الفيزياء 32 مرة ،X و V؛ الاخيرXيجب أن تظهر للمستخدم.

هذا ما ستبدو عليه طريقة 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;
  }
}

تستخدم هذه الطريقة في حساب الفيزياء الربيع التفاعلي ، في هذه المقالة يمكنك قراءة المزيد.

حول معامل التوهين وفترة التذبذب الجوهري


بالنظر إلى خصائص النظام - كتلة الحمل ، ومعامل المرونة ومعامل الاحتكاك للوسيط - من غير الواضح تمامًا كيف يتصرف الربيع. عند إنشاء Spring-animations ، يتم تحديد هذه المتغيرات بشكل عشوائي حتى يرضي المبرمج "ربيع" الرسوم المتحركة.

ومع ذلك ، في الفيزياء ، هناك زوجان من المتغيرات لربيع مغمور ، بالنظر إليهما ، يمكنك معرفة كيف يتصرف الربيع.

للعثور عليهم ، دعنا نعود إلى معادلتنا لقانون نيوتن الثاني:

kXαV=ma


نكتبها كمعادلة تفاضلية:

kxαdxdt=md2xdt2


يمكن إعادة كتابته على النحو التالي:

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


أين ω0=km- تواتر تذبذبات الزنبرك دون مراعاة قوة الاحتكاك ، ζ=α2mk- معامل التوهين.

إذا قمنا بحل هذه المعادلة ، نحصل على عدة وظائفx(t)التي تحدد موضع الربيع مقابل الزمن. سأضع الحل تحت المفسد حتى لا أمد المقالة أكثر من اللازم ، سننتقل مباشرة إلى النتيجة.

حل المعادلة
, :
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.


في ζ<1نحصل على معادلة تصف الاهتزازات الرطبة للربيع. أقلζ، كلما انخفضت قوة الاحتكاك والاهتزازات الأكثر وضوحا.

فيζ=1نحصل على المعادلة مع الاحتكاك النقدي ، أي المعادلة التي يعود فيها النظام إلى وضع التوازن دون تردد ، بأسرع طريقة.

فيζ>1نحصل على معادلة يعود فيها النظام إلى وضع التوازن دون تردد. سيكون هذا أبطأ منζ=1؛ مع الزيادةζ ينخفض ​​معدل التقارب إلى وضع التوازن.


أمثلة للرسوم المتحركة لمختلف ζ. تم تقليل سرعة الرسوم المتحركة 4 مرات

بالإضافة إلىζ، هناك خاصية أخرى مفهومة لتذبذبات الربيع - فترة تذبذبات الربيع دون مراعاة الاحتكاك:

T=2πmk



لدينا معلمتان:

ζ، والذي يحدد مقدار تقلب الرسوم المتحركة.

القيم الممكنةζ0 إلى 1.

أقلζ، كلما كانت الاهتزازات أكثر وضوحًا في الربيع. عند 0 ، لن يكون للنظام احتكاكًا ، وسوف يتأرجح الربيع بدون توهين ؛ عند 1 ، سوف يتقارب الربيع إلى وضع التوازن دون تردد.

القيم الأكبر من 1 ليست ضرورية بالنسبة لنا - فهي تعمل 1 ، ولكن دون جدوى اسحب الرسوم المتحركة الخاصة بنا.

Tوالذي سيحدد مدة الرسوم المتحركة.

يجب أن يؤخذ في الاعتبار المعلمةTيتحدث عن مدة اهتزاز واحد للربيع دون مراعاة قوة الاحتكاك ، وليس عن إجمالي مدة الرسوم المتحركة.

الآن يمكننا اشتقاق معامل المرونة ومعامل الاحتكاك للوسط ، مع العلمζ و Tوافتراض ذلك m=1:

ζ=α2mk,T=2πmk


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



نقوم بتطبيق هذا المنطق في الكود:

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

الآن لدينا طريقة getSpringValues ​​التي تقبل معامل التوهين الذي يمكن قراءته بواسطة الإنسان ζوفترة التذبذب Tويعيد كتلة الربيع ومعامل الاحتكاك ومعامل المرونة.

هذه الطريقة موجودة في وضع الحماية على الرابط أدناه ، يمكنك محاولة استخدامها بدلاً من خصائص النظام المعتادة:
https://playcode.io/590645

Vue Spring الرسوم المتحركة


لاستخدام الرسوم المتحركة الربيعية بشكل ملائم في Vue ، يمكنك القيام بأمرين: كتابة مكون مجمّع أو طريقة تكوين واجهة برمجة التطبيقات .

الرسوم المتحركة الربيع من خلال المكون


دعونا نكتب مكونًا يلخص استخدام فصل الربيع.

قبل كتابته ، تخيل كيف نريد استخدامه:

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

نود أن:

  • يمكنك تعيين خصائص الربيع
  • في معلمة animationProps ، كان من الممكن تحديد الحقول التي نريد تحريكها
  • اجتازت المعلمة animate القيمة سواء كانت animationProps يجب أن تكون متحركة
  • عندما تتغير الحقول في animationProps ، يتم تغييرها باستخدام الرسوم المتحركة الربيعية ويتم نقلها إلى الفتحة المحددة

للحصول على الوظائف الكاملة ، يجب أن يظل المكون قادرًا على تغيير سرعة الزنبرك ، لكننا لن نقوم بذلك حتى لا نعقد الكود.

نبدأ التنمية:

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

وضع الحماية مع مكون ومثال على استخدامه:
https://playcode.io/591686/

الرسوم المتحركة الربيع عبر طريقة تكوين API


من أجل استخدام تكوين واجهة برمجة التطبيقات ، والذي سيظهر في Vue 3.0 ، نحتاج إلى حزمة تكوين واجهة برمجة التطبيقات .

أضفه إلى أنفسنا:

npm i @vue/composition-api

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

Vue.use(VueCompositionApi);

الآن دعونا نفكر في الكيفية التي نود أن نرى بها طريقة الرسوم المتحركة:

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

نود أن:

  • يمكنك تعيين خصائص الربيع
  • أخذت الدالة كائنًا مع الحقول التي نريد تحريكها كمدخل.
  • قامت الدالة بإرجاع الأغلفة المحسوبة. ستقبل الرسائل القيم التي نريد تحريكها كمدخلات ؛ ستعيد getters قيمة متحركة
  • عادت الوظيفة متغير الحركة ، والتي ستكون مسؤولة عما إذا كنا بحاجة إلى تشغيل الرسوم المتحركة أم لا.

كما هو الحال في المكون ، لن نضيف تخصيصًا لسرعة الزنبرك حتى لا نعقد الكود.

نبدأ في صنع طريقة:

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;

وضع الحماية مع طريقة تكوين api ومثال على استخدامه:
https://playcode.io/591812/

الكلمة الأخيرة


في المقالة ، درسنا الميزات الأساسية للرسوم المتحركة الربيعية وقمنا بتنفيذها في Vue. ولكن لا يزال لدينا مجال للتحسينات - في مكونات Vue ، يمكنك إضافة القدرة على إضافة حقول جديدة بعد التهيئة ، وإضافة تغيير في سرعة الربيع.

صحيح ، كتابة ربيعك في مشاريع حقيقية ليس ضروريًا على الإطلاق: مكتبة Popmotion لديها بالفعل تطبيق ربيع جاهز . يمكنك كتابة غلاف لـ Vue ، كما فعلنا أعلاه.

شكرا لقراءتك حتى النهاية!

المواد المستعملة



All Articles