Vue中的春季动画

哈Ha!

我一直想将Spring动画添加到任何项目中。但是我只对使用react-spring的React项目执行了此操作,因为我什么都不知道。

但是最后,我决定弄清所有工作原理并编写实现!

如果您还想在任何地方使用Spring动画,请放手一搏。在那里,您会发现一些理论,可以在纯JS中实现Spring,并在Vue中使用component和composition-api实现Spring动画。


TL; DR


在JS中实现Spring Animation:https//playcode.io/590645/

您需要将Spring动画添加到您的项目中-从Popmotion库中获取Spring

您需要将Spring动画添加到Vue项目中-从Popmotion库中获取Spring并为其编写包装。

Spring动画有什么作用?


网络上的常规CSS动画使用持续时间和不时计算位置的功能。

春季动画使用不同的方法-春季物理。

对于动画,我们设置系统的物理属性:弹簧的质量,弹簧的弹性系数,介质的摩擦系数。然后,将弹簧拉伸到需要设置动画的距离,然后释放它。

在前端项目中,我看到了Spring动画的这些优点:

1.您可以设置初始动画速度,

如果站点用手指处理滑动或用鼠标拖动,则如果以初始速度(例如滑动速度或鼠标速度)进行设置,则对象动画将看起来更加自然。


无法保持鼠标速度的CSS动画


弹簧动画,其中鼠标的速度传递到弹簧

2。您可以更改动画的端点

,例如,如果要将对象动画化为鼠标光标,则无法使用CSS-在更新光标位置时,动画会再次初始化,这将导致抽搐是可见的。

Spring动画没有这种问题-如果您需要更改动画的端点,我们可以将spring拉伸或压缩到所需的缩进。


将元素动画化为鼠标光标的示例;顶部-CSS动画,底部-Spring动画

春季物理学


带负载的普通弹簧。她还在休息。

把她拉下来一点。弹力(F=kΔX),以使弹簧恢复到原始位置。当我们释放弹簧时,它将开始沿正弦曲线摆动:


增加摩擦力。使其与弹簧的速度成正比(F=αV在这种情况下,振荡会随着时间而衰减:


我们希望以与弹簧在受到弹性和摩擦力的作用下振动相同的方式对弹簧动画进行动画处理。

我们考虑动画的弹簧位置


在动画的上下文中,动画的初始值和最终值之间的差是弹簧的初始张力 X0

在动画开始之前,我们得到:弹簧质量m,弹簧弹性系数 k,介质的摩擦系数 α弹簧拉力 X0,启动速度 V0

每秒计算动画位置数十次。动画计算之间的时间间隔称为Δt

在每次新计算中,我们都有弹簧速度V 和弹簧张力 X在最后一个动画间隔上。对于动画的第一次计算X=X0V=V0

我们开始计算该间隔的动画位置!

根据牛顿第二定律,所有施加力的总和等于人体质量乘以其加速度:

F=ma


两个力作用在弹簧上-弹力和摩擦力:

F+F=ma


我们将更详细地记录力并找到加速度:

kXαV=ma


a=kXαVm


之后,我们可以找到新的速度。它等于上一个动画间隔中的速度与加速度乘以时间间隔之和:

V=V+aΔt


了解速度后,您可以找到弹簧的新位置:

X=X+VΔt



我们位置正确,将其显示给用户!

一旦显示,开始一个新的动画间隔。对于新的间隔X=XV=V

动画值得停止的时候XV变得非常小-此时,弹簧振动几乎是看不见的。

JS Spring动画


我们有必要的公式,剩下的就是编写我们的实现:

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

带有Spring类代码的沙盒及其使用示例:https :
//playcode.io/590645

春季小动画改进


我们的Spring课程有一个小问题- Δt每次动画的新开始都会有所不同。

用旧手机打开-Δt将等于46毫秒(我们的最大限制)。在功能强大的计算机上打开-Δt将是16-17毫秒。

改变中Δt表示动画的持续时间和动画值的更改每次都会略有不同。

为了防止这种情况发生,我们可以这样做:

Δt作为固定值。使用新的动画间隔,我们将必须计算自上一个间隔以来经过了多少时间以及其中有多少固定值Δt如果时间不能被时间整除Δt,然后将其余部分转移到下一个动画间隔。

然后我们计算XV得到固定值的次数就很多。最后值X向用户展示。

例:

Δt我们以1毫秒为单位,在最后一个动画间隔和新的动画间隔之间经过了32毫秒。

我们必须计算32次物理XV; 持续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;
  }
}

这种计算物理的方法使用反应弹簧,在本文中您可以阅读更多。

关于衰减系数和固有振荡周期


观察系统的特性-负载的质量,弹性系数和介质的摩擦系数-完全不清楚弹簧的行为。创建弹簧动画时,将随机选择这些变量,直到程序员对动画的“弹性”感到满意为止。

但是,在物理学中,阻尼弹簧有几个变量,查看它们可以知道弹簧的行为。

为了找到它们,让我们回到牛顿第二定律的方程式中:

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中使用Spring动画,您可以做两件事:编写包装器组件或composition-api方法

通过组件的春季动画


让我们编写一个抽象化Spring类用法的组件。

在编写它之前,请想象我们要如何使用它:

<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中的字段发生更改时,它们就会使用Spring动画进行更改并转移到有作用域的插槽中

为了获得完整的功能,组件必须仍然能够更改弹簧的速度,但是为了不使代码复杂化,我们将不会这样做。

我们开始开发:

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/

通过composition-api方法的春季动画


为了使用在Vue 3.0中出现的composition-api,我们需要composition-api

添加到我们自己:

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>

我们想要:

  • 您可以设置弹簧的属性
  • 该函数接受了一个对象,其中包含我们要设置动画的字段作为输入。
  • 该函数返回了计算的包装器。设置者将接受我们要设置动画的值作为输入; 吸气剂将返回动画值
  • 该函数返回了animate变量,它将负责我们是否需要播放动画。

与组件一样,我们不会添加弹簧速度的自定义设置,以免使代码复杂化。

我们开始制作一个方法:

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;

带有composition-api方法的沙盒及其使用示例:https :
//playcode.io/591812/

最后一个字


在本文中,我们检查了Spring动画的基本功能,并在Vue中实现了这些动画。但是我们仍有改进的余地-在Vue组件中,您可以添加以下功能:在初始化后添加新字段,增加弹簧速度的变化。

没错,根本不需要在实际项目中编写Spring:Popmotion已经具有现成的Spring实现您可以像上面一样为Vue编写包装。

感谢您阅读到底!

二手材料



All Articles