上期已介绍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>
版权归原作者 观察蚂蚁的人 所有, 如有侵权,请联系我们删除。