我们在日常前端开发时在动画的选择上基本都是css,通过css的animation即可满足大部分的开发场景,如果遇到了特殊而比较不容易实现的效果就会考虑到用js来实现,而本次的主题,就是围绕用js来做一个比较不常见的特殊动画效果。
假设我们要做一个进度条控制方块移动的动画,进度条开始方块在左边,进度条结束方块在右边。
是不是大脑里已经有了方案,看起来是一个容易实现的需求。
大致的思路:通过监听滚动条,滚动条0方块的left为0,滚动条100%方块的left100%。
当我们完成了这番代码后得出了以下的效果:
看起来是符合了自己的预期,但哪里感觉不太对。
感觉上这个效果还不够灵动?缺少某种缓动的动画效果。
在这一步,是卡了我非常久的地方,经过一番漫长的探索,终于发现了其中的奥秘。
我们看看带有缓动效果后的样子:
原来的方案速率是一致的,而通过调整后,方块移动的速率就不一样了。
那么这里的重点就在于一个公式:插值公式。
我们先看定义:
插值公式是数学中用于在离散数据的基础上补插连续函数的方法,使得这条连续曲线通过全部给定的离散数据点。插值是离散函数逼近的重要方法,通过函数在有限个点处的取值状况,估算出函数在其他点处的近似值。插值公式广泛应用于填充图像变换时像素之间的空隙,以及在计算力学等领域。
就当前这个动画来说,插值能在动画执行的过程中不断的计算下一次要移动的速率。
这两个动画有一个明显区别,当滚动条停下时,无插值的动画会立刻停下,而有插值的动画会在滚动条停下后依旧有缓动的效果再逐渐停下,从而形成一种动画感。
接下来是代码的实现过程:
<!--首页-->
<template>
<div id="home" style="height:2000px">
<div class="squar" ref="squars">方块</div>
</div>
</template>
<script setup>
import {ref} from "vue";
//动画render
let prg = 0;
let lastPrg = 0;//上一次滚动条进度
let lerpValue = 0.05;
const init = ()=>{
// 初始化函数
prg = document.documentElement.scrollTop || document.body.scrollTop;
lastPrg = document.documentElement.scrollTop || document.body.scrollTop;
}
//动画数据动画
const SetAnimData = (prg)=>{
squars.value.style.left = prg+'px'
}
//插值函数
//x:当前位置 y:目标位置 t:插值参数百分比
const lerp = (x, y, t) =>{
return (1 - t) * x + t * y;
}
let animateFrameId = '';
const animate = ()=>{
animateFrameId = requestAnimationFrame(animate);
prg = document.documentElement.scrollTop || document.body.scrollTop;
if(prg>1000)prg=1000;//如果当前进度条大于整组动画进度最大值
//如果滑动值比上次滑动值 大于lerpValue
if (Math.abs(prg - lastPrg) > lerpValue)
{
//进行插值运算
lastPrg = prg//lerp(lastPrg, prg, lerpValue);
//执行动画数据动画
SetAnimData(lastPrg);
}
// 小于插值 直接进行赋值,否则会无限的插值
else
{
//直接赋值
lastPrg = prg;
//执行动画数据动画
SetAnimData(lastPrg);
}
}
let container = null
const squars = ref(null)
onMounted(()=>{
container = document.getElementById("home");
if(process.client && container.clientHeight){
console.log(squars.value)
init();
animate();
}
})
</script>
<style scoped lang="less">
.squar{
width: 100px;
height: 100px;
background: red;
position: fixed;
top: 0;
left: 0;
}
</style>
接下来是一个思考题,现在我们能做到的是滚动条从0-100% 方块从左到右的移动。
那么假设我们希望0-50%方块从左到右移动 50%-100%从上到下移动,这样该怎么做呢?
在实际的场景中,我们往往会需要动画有好几个阶段不同的形式进行插值动画,(让一组从左至右,下一组从上至下)。
如下图案例:(先上往下,再左到右)
像这个多组动画的形式,我采用的是for循环,循环多组动画,每组动画设置起始的进度和结束的进度条以及起始的进度值和结束的进度值:
//动画数据动画
let animates = {
// 在0-1200进度条值的范围内 元素会从左到右0-1000的移动。
'left':[
{
beginPrg:0,//起始进度条值
endPrg:1200,//结束进度条值
begin:0,//起始进度条 元素的起始值
end:1000,//结束进度条 元素的结束值
},
],
'top':[
{
beginPrg:1200,
endPrg:2400,
begin:0,
end:1000,
},
],
}
const SetAnimData = (prg)=>{
// squars.value.style.left = prg+'px'
for(let i in animates){
for(let j in animates[i]){
let animateOne = animates[i][j];
console.log(animateOne,'animaO')
//如果滚动条值在 起始滚动条和结束滚动条范围内
// console.log(animateOne,'sssssss',prg,animateOne[0].beginPrg,)
var prgValue = clamp((prg - animateOne.beginPrg) / (animateOne.endPrg - animateOne.beginPrg), 0, 1);
if ((1 - prgValue) <= 0.01){
prgValue = 1;
}
if ((prgValue) <= 0.01){
prgValue = 0;
}
let curValue= lerp(animateOne.begin, animateOne.end, prgValue);//通过插值计算当前模型要变化的值
if (prg >= animateOne.beginPrg && prg < animateOne.endPrg)
{
squars.value.style[i] = curValue+'px'
}
}
}
}
这样就能做出多组不同形式的动画效果了。
再往后就是多个元素同时的移动,以及细节方面的优化,考虑到篇幅会过长,这里就不再继续延申下去了。
完整的代码实现过程:
<!--首页-->
<template>
<div id="home" style="height:2000px">
<div class="squar" ref="squars">方块</div>
</div>
</template>
<script setup>
import {ref} from "vue";
import {clamp} from 'lodash-es'
//动画render
let prg = 0;
let lastPrg = 0;//上一次滚动条进度
let lerpValue = 0.05;
const init = ()=>{
// 初始化函数
prg = document.documentElement.scrollTop || document.body.scrollTop;
lastPrg = document.documentElement.scrollTop || document.body.scrollTop;
}
//动画数据动画
let animates = {
// 在0-1200进度条值的范围内 元素会从左到右0-1000的移动。
'left':[
{
beginPrg:0,//起始进度条值
endPrg:1200,//结束进度条值
begin:0,//起始进度条 元素的起始值
end:1000,//结束进度条 元素的结束值
},
],
'top':[
{
beginPrg:1200,
endPrg:2400,
begin:0,
end:1000,
},
],
}
const SetAnimData = (prg)=>{
// squars.value.style.left = prg+'px'
for(let i in animates){
for(let j in animates[i]){
let animateOne = animates[i][j];
console.log(animateOne,'animaO')
//如果滚动条值在 起始滚动条和结束滚动条范围内
// console.log(animateOne,'sssssss',prg,animateOne[0].beginPrg,)
var prgValue = clamp((prg - animateOne.beginPrg) / (animateOne.endPrg - animateOne.beginPrg), 0, 1);
if ((1 - prgValue) <= 0.01){
prgValue = 1;
}
if ((prgValue) <= 0.01){
prgValue = 0;
}
let curValue= lerp(animateOne.begin, animateOne.end, prgValue);//通过插值计算当前模型要变化的值
if (prg >= animateOne.beginPrg && prg < animateOne.endPrg)
{
squars.value.style[i] = curValue+'px'
}
}
}
}
//插值函数
//x:当前位置 y:目标位置 t:插值参数百分比
const lerp = (x, y, t) =>{
return (1 - t) * x + t * y;
}
let animateFrameId = '';
const animate = ()=>{
animateFrameId = requestAnimationFrame(animate);
prg = document.documentElement.scrollTop || document.body.scrollTop;
if(prg>2400)prg=2400;//如果当前进度条大于整组动画进度最大值
//如果滑动值比上次滑动值 大于lerpValue
if (Math.abs(prg - lastPrg) > lerpValue)
{
//进行插值运算
lastPrg = lerp(lastPrg, prg, lerpValue);
//执行动画数据动画
SetAnimData(lastPrg);
}
// 小于插值 直接进行赋值,否则会无限的插值
else
{
//直接赋值
lastPrg = prg;
//执行动画数据动画
SetAnimData(lastPrg);
}
}
let container = null
const squars = ref(null)
onMounted(()=>{
container = document.getElementById("home");
if(process.client && container.clientHeight){
console.log(squars.value)
init();
animate();
}
})
</script>
<style scoped lang="less">
.squar{
width: 100px;
height: 100px;
background: red;
position: fixed;
top: 0;
left: 0;
}
</style>
以上就是本次的分享,感谢观看!