0


requestAnimationFrame运动框架实现播放中连续变速动画效果

上期已介绍requestAnimationFrame运动框架的基本概念和特性,本期继续了解运动框架的用途,在制作动画效果方面,运动框架可以在播放动画的同时自然、流畅地响应连续变速操作,还能解决画面卡顿的问题。

keyframes关键帧播放同时可调整速度,但是效果不理想

  • 元素的style.animationDuration属性对应keyframes播放速度,连续修改animationDuration属性会使画面出现卡顿,甚至“倒转”的问题。
  • 猜测速度属性的改变触发了动画帧重算,并且重算的方式并不考虑之前已经播放的动作,所以不连贯。这种情况下,使用animation-play-state暂停动画,修改速度之后再继续播放也未能解决。

以上问题解决:requestAnimationFrame运动框架实现播放中连续变速动画效果

  • 运动框架动画的基本原理是连续改变元素的行内属性,产生动态效果。每次视图重绘,运动框架的回调函数可根据时间差与速度计算出需要变化的属性值,然后将元素的属性值修改。
  • 对速度属性设置监听并做好节流,当速度改变时记录当时的元素属性值,并且更新计算属性用的起始时间点。
  • 速度变化以后,依然可以通过时间差与速度计算属性值,只是要将上次变速时记录的属性值作为基础,在原有的动画上再加动作,这样变速之后的动画就是连续的。

以下视频呈现运动框架与关键帧制作相似动画时,响应连续变速的不同效果,关键帧产生了卡顿,运动框架的效果平滑:

运动框架实现播放中连续变速动效

以下是视频中动态效果的实现代码,包含关键帧和运动框架两个动画,同时播放。使用vue3+Element Plus,上代码:

<template>
  <div class="dotDiv noselect">
    <div class="dotInnerDiv">
      <span class="dot" ref="dot"></span>
      <span class="dotAF" ref="dotAF"></span>
      <div class="TexAnimat">关键帧-变速不平滑</div>
      <div class="TexReAF">运动框架-平滑变速</div>
    </div>
    <div class="formDiv noselect">
      <span class="bpmTex">{{ bits.bpmNum }}</span>
      <div class="slider-demo-block">
        <span class="minTex">10bpm</span>
        <el-slider
          v-model="bits.bpmNum"
          :min="10"
          :max="400"
          :step="10"
          size="default"
        />
        <span class="maxTex">400bpm</span>
      </div>
      <div class="btDiv">
        <el-button type="primary" @click="btClick()">
          {{ btText.texts[btText.ind] }}
        </el-button>
        <el-button type="primary" @click="btClick('rest')"> 重置 </el-button>
      </div>
      <span class="Tex">拖动滑块可变速</span>
    </div>
  </div>
</template>

<script>
//element-Plus的slider滑块,touchstart监听未标记被动式,浏览器可能会出现警告,不影响使用,如需去除警告可更换UI。

import { Plus } from "@element-plus/icons-vue";
import { ref } from "vue";

export default {
  name: "speadSmooth",
  components: { Plus },
  data() {
    return {
      btText: {
        texts: ["播放", "暂停"],
        ind: 0,
      },
      bits: {
        bpmNum: ref(80),
        rowNum: 80,
        throttle: false,
      },
      records: {
        rotateTime: null,
        lastRotate: 0,
      },
    };
  },
  watch: {
    "bits.bpmNum"() {
      if (this.btText.ind == 1) {
        if (this.bits.throttle == false) {
          this.bits.throttle = true;
          setTimeout(() => {
            this.bits.throttle = false;
          }, 50);
          this.bits.rowNum = this.bits.bpmNum;

          let dot = this.$refs.dot;
          dot.style.animationDuration = (60 / this.bits.rowNum) * 4 + "s";

          let dotAF = this.$refs.dotAF;
          let reg = RegExp(/.*rotateZ\(([0-9]{1,}\.?[0-9]{1,})deg\).*/g);
          let deg = parseInt(
            dotAF.style.transform.toString().replace(reg, "$1") % 360
          );
          this.records.lastRotate = deg; //运动框架-记录旋转量
          this.records.rotateTime = new Date(); //运动框架-更新上次变速时间
        }
      }
    },
  },
  mounted() {
    //运动框架-编辑定时器
    window.requestAnimationFrame =
      window.requestAnimationFrame ||
      function (fn) {
        return setTimeout(fn, 1000 / 60);
      };
    window.cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
  },
  methods: {
    btClick(mod) {
      this.bits.throttle = false;
      let btText = this.btText;
      let dot = this.$refs.dot;
      let dotAF = this.$refs.dotAF;

      if (mod == "rest") {
        btText.ind = 0;

        if (dot.classList.contains("dotStart")) {
          dot.classList.remove("dotStart");
        }

        setTimeout(() => {
          dot.style.animationDuration = "0s";
          dot.style.animationPlayState = "paused";
        }, 0);

        this.records.lastRotate = 0;
        dotAF.style.transform = "rotateZ(0deg)";
        return false;
      }

      if (btText.ind == 0) {
        btText.ind = 1;
        if (!dot.classList.contains("dotStart")) {
          dot.classList.add("dotStart");
        }

        dot.style.animationDuration = (60 / this.bits.rowNum) * 4 + "s";
        dot.style.animationPlayState = "running";
        this.records.rotateTime = new Date();

        this.reWTime();
      } else {
        btText.ind = 0;

        dot.style.animationPlayState = "paused";
        let reg = RegExp(/.*rotateZ\(([0-9]{1,}\.?[0-9]{1,})deg\).*/g);
        let deg = parseInt(
          dotAF.style.transform.toString().replace(reg, "$1") % 360
        );
        this.records.lastRotate = deg;
      }
    },
    reWTime() {
      if (this.btText.ind == 0) {
        return false;
      }

      let nowTime = new Date();
      let dotAF = this.$refs.dotAF;
      let { lastRotate, rotateTime } = this.records;

      let degPerBit = 360 / 4 / (60 / this.bits.rowNum); //每秒多少量
      let showRotate = (nowTime - rotateTime) / 1000; //上次变速到现在的累计时间
      let deg = (degPerBit * showRotate + lastRotate) % 360; //本次转动结果量

      dotAF.style.transform = "rotateZ(" + deg + "deg)";

      requestAnimationFrame(this.reWTime);
    },
  },
};
</script>

<style lang="scss">
html,
body {
  padding: 0;
  margin: 0;
}
</style>
<style lang="scss" scoped >
$colorT: rgb(10, 100, 180);
$colorR: rgb(120, 230, 255);
$colorB: rgb(77, 200, 255);
$colorL: rgb(45, 150, 240);
.noselect {
  -webkit-touch-callout: none; /* iOS Safari */
  -webkit-user-select: none; /* Chrome/Safari/Opera */
  -khtml-user-select: none; /* Konqueror */
  -moz-user-select: none; /* Firefox */
  -ms-user-select: none; /* Internet Explorer/Edge */
  user-select: none; /* Non-prefixed version, currently not supported by any browser */
}

.dotDiv {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;

  background: linear-gradient(
    to right top,
    rgba(121, 187, 255, 0.37),
    rgba(121, 187, 255, 0.12)
  );

  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  align-items: center;
}

.dotInnerDiv {
  position: relative;
  width: 80vw;
  height: auto;

  display: grid;
  grid-template-columns: 50% 50%;
  grid-template-rows: 20vmin 15vmin;
}
.dot,
.dotAF {
  position: relative;
  width: 0;
  height: 0;
  left: calc(50% - 5vmin);
  top: calc(50% - 5vmin);

  box-shadow: 0 0 24vmax skyblue;
  border-style: solid;
  border-width: 3vmin 7vmin 7vmin 3vmin;
  border-color: $colorT $colorR $colorB $colorL;

  perspective: 500;
  backface-visibility: visible;
  transform-style: preserve-3d;
}
.dotStart {
  animation: move 2s infinite linear;
  animation-duration: 0s;
  animation-play-state: paused;
}
.TexAnimat,
.TexReAF {
  position: relative;
  display: block;
  width: 100%;

  color: rgba(0, 0, 0, 0.7);
  font-size: 0.8rem;
  text-align: center;
}

.formDiv {
  position: relative;
  width: 100vw;
  height: 30vh;
  margin-top: 7vh;

  display: flex;
  flex-flow: column nowrap;
  justify-content: space-evenly;
  align-items: center;
}
.btDiv {
  width: 30vw;

  display: flex;
  flex-flow: row nowrap;
  justify-content: space-evenly;
}
.el-button {
  margin-top: 3vh;
}
.slider-demo-block {
  width: 70vw;
  min-width: 80vmin;

  display: flex;
  align-items: center;
}
.bpmTex {
  margin-top: 3vh;
  color: rgba(0, 0, 0, 0.7);
  font-size: 1.7rem;
}
.minTex,
.maxTex {
  margin: 0 3vw;
  color: rgba(0, 0, 0, 0.7);
  font-size: 0.7rem;
  text-align: center;
}
.Tex {
  margin-top: 3vh;
  color: rgba(77, 77, 77, 0.7);
  font-size: 0.8rem;
  text-align: center;
}

@keyframes move {
  0% {
    transform: rotateZ(0deg);
  }
  100% {
    transform: rotateZ(360deg);
  }
}
</style>
标签: 前端 vue.js elementui

本文转载自: https://blog.csdn.net/weixin_44841138/article/details/123956340
版权归原作者 观察蚂蚁的人 所有, 如有侵权,请联系我们删除。

“requestAnimationFrame运动框架实现播放中连续变速动画效果”的评论:

还没有评论