一,过渡动画的实现

1,无过渡,直接变化

<template>
  <div class="testBox">
    <div class="testCon" :style="{ width: width + 'px' }"></div>
    <div class="testBtn" @click="clickBtn">点击按钮</div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const width = ref(10);
const clickBtn = () => (width.value += 100);
</script>

<style lang="scss" scoped>
.testBox {
  overflow: hidden;
  height: 100vh;
  padding: 100px 50px;
  box-sizing: border-box;
  .testCon {
    height: 200px;
    background: pink;
  }
  .testBtn {
    margin-top: 10px;
    height: 60px;
    line-height: 60px;
    border: 1px solid #35aeff;
    background: #35aeff;
    border-radius: 10px;
  }
}
</style>

为了好讲述,只看一次点击。也就是开始时width:10px,结束时width:110px。在没有设置过渡的时候,变化是突兀的,生硬的。如下图:

vue 动画特效用gpu渲染 vue实现动画效果_动画

2,设置过渡,设置时间和速率

过渡的实现。可以理解为:记录起始状态,然后添加一个从开始到结束的时间和速度。
主要就是利用css3的transition来设置。需要监听谁的属性变化,就写在谁的样式中

.testCon {
    height: 200px;
    background: pink;
    transition: width 2s linear;//监听这个元素的width属性的变化,
  }

于是就会监听这个width属性,从开始到结束的变化,会被拉长到2s来线性变化。

vue 动画特效用gpu渲染 vue实现动画效果_vue.js_02


当想要监听该元素所有元素的变动的时候,只要把属性改成all即可:

<template>
  <div class="testBox">
    <div :class="['testCon', btnClick ? 'btnClick' : '']"></div>
    <div class="testBtn" @click="clickBtn">点击按钮</div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const btnClick = ref(false);
const clickBtn = () => {
  btnClick.value = true;
};
</script>

<style lang="scss" scoped>
.testBox {
  overflow: hidden;
  height: 100vh;
  padding: 100px 50px;
  box-sizing: border-box;
  .testCon {
    width: 100px;
    height: 100px;
    background: pink;
  }
  .btnClick {
    height: 200px;
    width: 200px;
    background: red;
    transition: all 2s linear;
  }
  .testBtn {
    margin-top: 10px;
    height: 60px;
    line-height: 60px;
    border: 1px solid #35aeff;
    background: #35aeff;
    border-radius: 10px;
  }
}
</style>

主要是从这个样式:

.testCon {
    width: 100px;
    height: 100px;
    background: pink;
}

变成:

.btnClick {
    height: 200px;
    width: 200px;
    background: red;
    transition: all 2s linear;
  }

这样一来,就会给该元素所有变动的属性增加过渡变化的效果:

vue 动画特效用gpu渲染 vue实现动画效果_css3_03


也就是说,过渡主要是transition这个css样式产生的效果,

transition :transition-property(执行变换的属性) || transition-duration(执行变换的时间) || transition-timing-function(执行变换的速率) || transition-delay(延迟变换的时间);

transition-property(执行变换的属性)

transition-property

可取的值

none

没有属性会获得过渡效果

all

所有属性都将获得过渡效果,也是其默认值

property

CSS 属性名称列表,逗号隔开

transition-duration(执行变换的时间

过渡的持续时间,取值time为数值,单位为s(秒)或者ms(毫秒),其默认值是0,也就是变换时是即时的。

transition-timing-function(执行变换的速率)


描述

linear

相同速度开始至结束

ease

慢速开始,然后变快,然后慢速结束

ease-in

慢速开始

ease-out

慢速结束

ease-in-out

慢速开始和结束

cubic-bezier(n,n,n,n)

在 cubic-bezier 函数中定义自己的值。可能的值是 0 至 1 之间的数值。

transition-delay(延迟变换的时间)

默认是0,也就是立即执行,若有设置值,则是延迟执行过渡。
需要注意的是,不是所有的CSS属性都支持transition,可以支持的具体的效果可以看这里

二,animation 和 keyframe 的组合实现动画

过渡动画,只能定义首尾两个状态。而animation可以自定义多个帧的状态之间的过渡效果。从而实现真正的动画。

1,animation的取值


描述

animation-name

规定需要绑定到选择器的 keyframe 名称

animation-duration

规定完成动画所花费的时间,以秒或毫秒计

animation-timing-function

规定动画的速度曲线

animation-delay

规定在动画开始之前的延迟,默认值为0

animation-iteration-count

规定动画应该播放的次数,默认值为1

animation-direction

规定是否应该轮流反向播放动画,默认值是正向

2,keyframe

@keyframes 就是让程序员设置一定序列的动画帧,然后提供给animation使用的。
使用方法:
当使用百分比用法时:

@keyframes 动画名字{
  0% { top: 0; left: 0px}
  50% { top: 30px; left: 20px; }
  100% { top: 0; left: 30px;}
}

使用的方法如下:

<template>
  <div class="testBox">
    <div class="testCon"></div>
  </div>
</template>

<script setup></script>

<style lang="scss" scoped>
.testBox {
  overflow: hidden;
  height: 100vh;
  padding: 100px 50px;
  box-sizing: border-box;
  .testCon {
    animation: testAni 3s ease-in-out infinite;
  }
}
@keyframes testAni {
  0% {
    width: 50px;
    height: 50px;
    background: #cd4a48;
    border-radius: 50px;
  }
  50% {
    width: 100px;
    height: 100px;
    background: #e72365;
    border-radius: 0;
  }
  100% {
    width: 50px;
    height: 50px;
    background: #04aef1;
    border-radius: 50px;
  }
}
</style>

实现的效果:

vue 动画特效用gpu渲染 vue实现动画效果_css3_04


当使用from-to用法时:

@keyframes preloader {
  from {
    transform: rotate(0deg);//其实这里写的就是支持过渡动画的属性啦
  }
  to {
    transform: rotate(360deg);
  }
}

不同于transition,使用animation自动触发,无需事件触发。

三,transform变换

在制作动画的时候,常常要使用到属性变化。transform就是变形,主要包括旋转rotate、扭曲skew、缩放scale和移动translate以及矩阵变形matrix。

transform: none || transform-functions

none:表示不进么变换;transform-function表示一个或多个变换函数,用空格分开。
transform-function变换函数有以下几种:

1,旋转rotate

通过指定的角度参数对原元素指定一个2D rotation(2D 旋转),需先有transform-origin属性的定义(默认dom元素的中心)。transform-origin定义的是旋转的基点,其中angle是指旋转角度,如果设置的值为正数表示顺时针旋转,如果设置的值为负数,表示逆时针旋转。 如:transform:rotate(30deg):

<template>
  <div class="testBox">
    <div class="testCon"></div>
  </div>
</template>

<script setup></script>

<style lang="scss" scoped>
.testBox {
  overflow: hidden;
  height: 100vh;
  padding: 100px 50px;
  box-sizing: border-box;
  .testCon {
    width: 100px;
    height: 100px;
    animation: testAni 3s ease-in-out infinite;
  }
}
@keyframes testAni {
  0% {
    background: #04aef1;
    border-radius: 50px;
  }
  50% {
    background: #e72365;
    border-radius: 0;
    transform: rotate(180deg);
  }
  100% {
    background: #04aef1;
    border-radius: 50px;
  }
}
</style>

vue 动画特效用gpu渲染 vue实现动画效果_vue.js_05

2,移动translate

移动translate我们分为三种情况:translate(x,y)水平方向和垂直方向同时移动(也就是X轴和Y轴同时移动);translateX(x)仅水平方向移动(X轴移动);translateY(Y)仅垂直方向移动(Y轴移动),如果是负值,则是反方向移动,当然,也可以用transform-origin来控制基准。

@keyframes testAni {
  0% {
    background: #04aef1;
    border-radius: 50px;
  }
  50% {
    background: #e72365;
    border-radius: 0;
    transform: rotate(180deg);
    transform: translate(50px, 50px);
  }
  100% {
    background: #04aef1;
    border-radius: 50px;
  }
}

实现的便是这样的效果:

vue 动画特效用gpu渲染 vue实现动画效果_css3_06

3,scale缩放

scale(x,y)使元素水平方向和垂直方向同时缩放(也就是X轴和Y轴同时缩放);scaleX(x)元素仅水平方向缩放(X轴缩放);scaleY(y)元素仅垂直方向缩放(Y轴缩放),但它们具有相同的缩放中心点和基数,其中心点就是元素的中心位置,缩放基数为1,如果其值大于1元素就放大,反之其值小于1,元素缩小。同样的,它可以用transform-origin来控制基准。

50% {
    background: #e72365;
    border-radius: 0;
    transform: scale(1.5, 2);
  }

vue 动画特效用gpu渲染 vue实现动画效果_vue 动画特效用gpu渲染_07

4,扭曲skew

扭曲skew和translate、scale一样同样具有三种情况:skew(x,y)使元素在水平和垂直方向同时扭曲(X轴和Y轴同时按一定的角度值进行扭曲变形);skewX(x)仅使元素在水平方向扭曲变形(X轴扭曲变形);skewY(y)仅使元素在垂直方向扭曲变形(Y轴扭曲变形)。

50% {
    background: #e72365;
    border-radius: 0;
    transform: skewX(30deg);
  }

vue 动画特效用gpu渲染 vue实现动画效果_动画_08


单个的动画看起来有些死板,但是多种结合在一起,就会有神效:

<div id="preloader">
      <span></span>
      <span></span>
      <span></span>
      <span></span>
</div>

#preloader {
  position: relative;
  width: 42px;
  height: 42px;
  animation: preloader 5s infinite linear;
}
#preloader span {
  width: 20px;
  height: 20px;
  position: absolute;
  background: red;
  // display: block;
  animation: preloader_6_span 1s infinite linear;
}
#preloader span:nth-child(1) {
  background: #2ecc71;
}
#preloader span:nth-child(2) {
  left: 22px;
  background: #9b59b6;
  animation-delay: 0.2s; //延迟0.2s后开始动画
}
#preloader span:nth-child(3) {
  top: 22px;
  background: #3498db;
  animation-delay: 0.4s; //延迟0.4s后开始动画
}
#preloader span:nth-child(4) {
  top: 22px;
  left: 22px;
  background: #f1c40f;
  animation-delay: 0.6s; //延迟0.6s后开始动画
}
@keyframes preloader {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
@keyframes preloader_6_span {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(0.5);
  }
  100% {
    transform: scale(1);
  }
}

vue 动画特效用gpu渲染 vue实现动画效果_动画_09

四,vue中的动画效果

1,普通的过渡动画

Vue 3 中提供了一些动画的封装,使用内置的 transition 组件来控制组件的动画。

vue 动画特效用gpu渲染 vue实现动画效果_vue 动画特效用gpu渲染_10


按照官网的说法,就是被transition包裹的组件,会有这六种class的切换:

1,v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
2,v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
3,v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
4,v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
5,v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
6,v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。

具体 class 的名字,Vue 的官网有一个图给出了很好的解释,图里的 v-enter-from 中的 v,就是我们设置的 name 属性。

实际上,这个过程就是过渡动画的体现。当元素进入的那一帧,会添加v-enter-from的class,设置了初始状态。而v-enter-to则是元素开始进入到完成会添加的class,用来设置元素过渡的结束状态。v-enter-active则是整个过渡时间内会存在的class。

也就是上面那张图实际上是这样的:

vue 动画特效用gpu渲染 vue实现动画效果_动画_11


于是,v-enter-active和v-leave-active就可以被用来定义进入过渡的过程时间,延迟和曲线函数。因为这两个类在过渡的时间内一直存在,过渡完成后才删除。

而v-enter-from和v-leave-to则可以用来设置动画的开始和结束。

另外两个可以不使用,而用元素自身的class替代:

<template>
  <div id="demo">
    <button @click="data.noActivated = !data.noActivated">Toggle</button>
    <transition name="fade">
      <p v-if="data.noActivated" class="test">hello</p>
    </transition>
  </div>
</template>

<script setup>
import { reactive } from "vue";
const data = reactive({
  noActivated: false
});
</script>

<style lang="scss" scoped>
.test {
  color: green;
  opacity: 1;
}
.fade-enter-active,
.fade-leave-active {
  transition: all 5s ease;
}
.fade-enter-from,
.fade-leave-to {
  color: black;
  opacity: 0;
}
</style>

实现的效果:

vue 动画特效用gpu渲染 vue实现动画效果_vue.js_12


也就是在元素渲染的那段动画中:

1,fade-enter-from设置了初始状态
2,fade-enter-active设置了过渡的动画和时间
3,元素本身的class:test充当设置了最终状态。

从而实现了开始渲染的过渡动画效果。
而在元素移除的那段动画中:

1,元素本身的class:test充当设置了初始状态。
2,fade-leave-active设置了过渡的动画和时间
3,fade-leave-to设置了最终状态。

2,列表的进入/离开过渡动画

transition 元素作为单个元素/组件的过渡效果。transition 只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。它的这六个class就是放在这个子元素上的。所以列表渲染就不用这个,而是使用transition-grou来包裹,至于六种class则是添加在item上。用法和上文说的一样,我习惯用元素本身的class,来取代v-enter-to和v-leave-from。所以没设置这两个。

<template>
  <div id="demo">
    <button @click="clickBtn">增加按钮</button>
    <transition-group name="fade" tag="ul">
      <li v-for="(item, index) in data.testList" :key="index" class="test">
        {{ item.content }}
      </li>
    </transition-group>
  </div>
</template>

<script setup>
import { reactive } from "vue";
const data = reactive({
  testList: [
    {
      content: "第一个"
    }
  ]
});
function clickBtn() {
  data.testList.push({
    content: "下一个"
  });
}
</script>

<style lang="scss" scoped>
.test {
  color: red;
  font-size: 18px;
}
.fade-enter-active,
.fade-leave-active {
  transition: all 2s ease;
}
.fade-enter-from,
.fade-leave-to {
  color: black;
  opacity: 0;
  transform: translateX(30px);
}
</style>

vue 动画特效用gpu渲染 vue实现动画效果_动画_13

3,js动画配合transition

<template>
  <div class="title">
    <input type="text" @keypress.enter="add" v-model="title" />
    <button @click="clear">清理</button>
  </div>
  <div class="content">
    <transition-group name="flip-list" tag="ul">
      <li v-for="(item, index) in todoList" :key="item.content">
        <input type="checkbox" v-model="item.done" />
        <span :class="[item.done ? 'inactive' : 'active']">
          {{ item.content }}
        </span>
        <span class="remove-btn" @click="removeTodo($event, index)">❌</span>
      </li>
    </transition-group>
  </div>

  <span class="dustbin">🗑</span>
  <div class="animate-wrap">
    <transition
      @before-enter="beforeEnter"
      @enter="enter"
      @after-enter="afterEnter"
    >
      <div class="animate" v-show="animate.show">📋</div>
    </transition>
  </div>
</template>

<script setup>
import { ref, reactive } from "vue";
//把这个useTodo解耦出来
function useTodos() {
  let title = ref("");
  let todoList = ref([
    {
      content: "五湖四海皆一望",
      done: false
    },
    {
      content: "千江有水千江月",
      done: true
    }
  ]);
  function add() {
    const obj = {
      content: title.value,
      done: false
    };
    todoList.value.push(obj);
    title.value = "";
  }
  function clear() {
    todoList.value = todoList.value.filter((v) => !v.done);
  }
  return { title, todoList, add, clear };
}

//其实你可以把组件内部的任何一段代码,从组件文件里抽离出一个独立的文件进行维护。
//再在这个组件中使用,于是这些东西就不依赖于this上下文了,这里再解构赋值出来
let { title, todoList, add, clear } = useTodos();

const { animate, beforeEnter, enter, afterEnter, removeTodo } = useAnimation();

function useAnimation() {
  let animate = reactive({
    show: false,
    el: null
  });
  const dustbin = {
    el: null,
    pos: [],
    init(queryStr) {
      this.el = document.querySelector(queryStr);
      this.getPos();
    },
    getPos() {
      const { left, top } = this.el.getBoundingClientRect();
      this.pos[0] = left;
      this.pos[1] = top;
    }
  };
  function beforeEnter(el) {
    let dom = animate.el;
    let rect = dom.getBoundingClientRect();
    const aniEl = document.querySelector(".animate");
    //动画元素 调整到dustbin的位置,也可以css直接写精准位置
    aniEl.style.left = `${dustbin.pos[0]}px`;
    aniEl.style.top = `${dustbin.pos[1]}px`;
    //计算并赋值偏移量
    let dx = dustbin.pos[0] - rect.left;
    let dy = dustbin.pos[1] - rect.top;
    el.style.transform = `translate(-${dx}px, ${dy * -1}px)`;
  }
  function enter(el, done) {
    document.body.offsetHeight;
    el.style.transform = `translate(0,0)`;
    el.addEventListener("transitionend", done);
  }
  function afterEnter(el) {
    animate.show = false;
    el.style.display = "none";
  }
  function removeTodo(e, i) {
    animate.el = e.target;
    animate.show = true;
    todoList.value.splice(i, 1);
    dustbin.init(".dustbin");
  }
  return { animate, beforeEnter, enter, afterEnter, removeTodo };
}
</script>

<style lang="scss" scoped>
.active {
  color: v-bind(color);
}
.flip-list-move {
  transition: transform 0.8s ease;
}
.flip-list-enter-active,
.flip-list-leave-active {
  transition: all 1s ease;
}
.flip-list-enter-from,
.flip-list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
.dustbin {
  position: absolute;
  top: 0;
  right: 0;
}
.animate-wrap .animate {
  position: fixed;
  right: 10px;
  top: 10px;
  z-index: 100;
  transition: all 0.5s linear;
}
</style>

实现的效果:

vue 动画特效用gpu渲染 vue实现动画效果_vue 动画特效用gpu渲染_14