实现一个好看的滚动文字渐入效果
我们来实现一个还比较不错的前端显示效果。目前已完成一个简单demo,大家可以在我的个人博客blog.jimmyxuexue.top中切换切换文章,大家可以滚动下看看。
前言
在上次直播时,猫哥给我看了一个网站,说这个效果挺不错的,直播现在也没啥人看,要不研究一下怎么实现它给博客也加一下吧。
效果网站 isux.tencent.com/articles/in...
效果就是随着我们滚动鼠标向下滚动时,文字会由下往上的有个缓慢的渐出效果,在这种文字比较多的网站里,这种效果还是相对比较优雅的。所以我们今天也来实现一下。
🐱哥是一个群友,欢迎大家加入我的一个前端群呀~
我这实现的是按照我的思路来实现的,可能不能做到1比1还原效果,不过应该是八九不离十
思路
主要有这么几个需要思考的点
动画
首先文字渐出的效果这个应该是一个动画,所以我们得实现这个动画。
第一想法是这个东西我们找个动画库就好了,看看有没有类似的,用一下就好了,于是我上了 Animate.css ,找到了好几个类似的效果,最终我们选择 animate__fadeInUp
这个效果。
当我们想要有这个效果的元素进入可视窗口时,加上这个动画类
实现
-
scroll
监听滚动条事件,判断元素在视口了就加上这个类
这个方案被比较早就被排除了,因为滚动时触发比较频繁的事件,性能会比较差,如果加上了防抖或者节流,整个效果又延迟,所以不行。
-
IntersectionObserver
使用js比较新的观察者api,api优雅,且性能更高
综合下来使用这个IntersectionObserver
这个api是最佳选择。
image-20231023110210478
怎么介入 vitepress 工程
因为博客是基于vitepress来实现的,它最终的打包效果是将每个markdown文件都打包成单独的html,最糟糕的视线方式是给打包后的每个html文件都加上一些脚本。
上面的方案肯定是不可行的,麻烦,且耗时,不益于后续的扩展。
利用vue的机制
Vitepress 也是有基于vue3进行开发的,所以我们只要能有接口来介入vue的工程里面,就可以比较高效的视线了。思路大概就是:
-
页面初始化时,执行脚本,页面元素在滚动到视口时加上我们动画类。
onMounted
-
当路由变化时,再次执行脚本,最新的页面元素在滚动到视口时加上我们动画类。
Watch
我们在docs/.vitepress/theme/index.js
这个文件下进行代码注入即可。
这个文件是vitepress暴露给我们的接口,可以写我们写
setup()
函数,有了它就能用onMounted
、watch
哪些元素是我们需要效果的呢
这里我们就需要分析一下网站的dom结构了。
image-20231023112200288
经过我们分析,发现其实就是 .vp-doc > div
下的所有子元素是需要加这个效果的,所以我们只需要给这些元素加上观察者即可,当元素滚动到视口时,加上我们的动画类。
代码
具体的一些代码实现
一些工具函数
-
检查元素是否在视口
dartconst isElementInViewport = element => { var rect = element.getBoundingClientRect() const isInViewport = rect.top >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) return isInViewport }
刚打开页面时,本来就在视口的元素是不需要加入动画类的,只有通过滚动才出现的元素才需要加上这个类
-
检查元素是否需要加动画类
kotlinconst checkHasAttribute = element => { return !!element.getAttribute('snow_is_show') }
这里我们通过给元素添加自定义属性的方式,来实现,当元素有这个属性了,就不需要加动画类了。
这个操作主要是为了防止一直上线滚动,频繁出现动画的问题,动画只需要加一次就好。
全部代码
javascript
const observers = [] // 用于存储所有观察者 -> 收集起来主要是为了当路由变化时效果之前的观察者。
export default {
// ... 省略
setup() {
const route = useRoute()
onMounted(() => {
initFirstScreen() // 初始化 -> 给首次渲染就在视口的元素加上自定义属性,这些元素永远不用加动画类
animateFn() // 执行核心脚本
})
// 元素是否在视口
const isElementInViewport = element => {
var rect = element.getBoundingClientRect()
const isInViewport =
rect.top >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight)
return isInViewport
}
// 检查是否有自定义属性
const checkHasAttribute = element => {
return !!element.getAttribute('snow_is_show')
}
// 初始化函数
const initFirstScreen = () => {
const main = document.querySelector('.vp-doc>div') || []
const paragraphs = [...(main?.children || [])]
paragraphs.forEach(item => {
if (isElementInViewport(item)) {
item.setAttribute('snow_is_show', true)
}
})
}
// 核心脚本
const animateFn = () => {
const main = document.querySelector('.vp-doc>div') || []
const paragraphs = [...(main?.children || [])]
paragraphs.forEach(item => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && !checkHasAttribute(item)) {
// 元素进入视口
item.classList.add('animate__animated')
item.classList.add('animate__fadeInUp')
item.setAttribute('snow_is_show', true)
}
})
})
observer.observe(item)
observers.push(observer)
})
}
// 清空所有 observer 的函数
const destructionObserver = () => {
observers.forEach(observe => {
observe.disconnect()
})
observers.length = 0
}
watch(
() => route.path,
() =>
nextTick(() => {
destructionObserver() // 先清空所有的观察者
initFirstScreen() // 再初始化一次 类似onMounted
animateFn() // 再次执行核心函数
})
)
},
Layout,
}
总结
效果我们已经实现完成啦,其实也还是有优化点的:
- 初始化时可以加一些其他的动画
- 直接获取
.vp-doc > div
的所有一级子元素有点粗暴,可能它的下面还有子元素,细分下能让动画能加优雅一点 - ......
欢迎大家加我的vx:ysh15120,进我们前端交流群。我会不定期直播,一起交流。