注册
web

前端实现:页面滚动时,元素缓慢上升效果

效果


2024-08-11 13.58.04.gif


实现方式



  1. 自定义指令
  2. 封装组件

两种方式均可以在SSR页面中使用


方式1:自定义指令实现


import Vue from 'vue';

const DISTANCE = 100; // y轴移动距离
const DURATION = 1000; // 动画持续时间
const THRESHOLD_FOR_TRIGGERING_ANIMATION = 0.1; // 当元素一部分可见时触发动画

const animationMap = new WeakMap();

function handleIntersection(entries, observer) { // IntersectionObserver 回调函数, 处理元素的可见性变化
for (const entry of entries) { // 遍历所有观察目标
if (entry.isIntersecting) { // 如果目标可见
const animation = animationMap.get(entry.target); // 获取动画对象
if (animation) {
animation.play(); // 播放动画
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
entry.target.classList.add('active');
}
observer.unobserve(entry.target); // 播放一次后停止监听
}
}
}

let ob;
if ('IntersectionObserver' in window) { // 如果浏览器支持 IntersectionObserver
ob = new IntersectionObserver(handleIntersection, { // 创建 IntersectionObserver 对象
threshold: THRESHOLD_FOR_TRIGGERING_ANIMATION // 当元素一部分可见时触发动画
});
} else {
// 回退机制:如果不支持 IntersectionObserver
ob = {
observe(el) { // IntersectionObserver 接口的 observe 方法
el.__onScroll__ = () => { // 监听元素的滚动事件
if (isInViewport(el)) { // 如果元素在视窗内
const animation = animationMap.get(el); // 获取动画对象
if (animation) {
animation.play();
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
el.classList.add('active');
}
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
}
};
window.addEventListener('scroll', el.__onScroll__); // 监听元素的滚动事件
},
unobserve(el) { // IntersectionObserver 接口的 unobserve 方法
if (el.__onScroll__) { // 如果元素有滚动事件监听
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
delete el.__onScroll__; // 清理引用
}
}
};
}

function isBelowViewport(el) { // 判断元素是否在视窗下方
const rect = el.getBoundingClientRect();
return rect.top > window.innerHeight;
}

function isInViewport(el) { // 判断元素是否在视窗内
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}

const directive = {
name: 'slide-in',
inserted(el, binding) { // 元素插入到 DOM 时触发
if (!isBelowViewport(el)) { // 如果元素在视窗下方,则不执行动画
console.log('Element is not below viewport');
return;
}

const duration = binding.value && binding.value.duration ? binding.value.duration : DURATION; // 动画持续时间
const animationOptions = { // 动画选项: 目标位置、持续时间、缓动函数
duration: duration,
easing: binding.value && binding.value.easing ? binding.value.easing : 'ease'
};

// 检查是否支持 Web Animations API
let animation;
if (el.animate) { // 如果支持 Web Animations API
animation = el.animate([ // 创建动画对象
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5
},
{
transform: 'translateY(0)',
opacity: 1
}
], animationOptions);
animation.pause(); // 初始化时暂停动画
animationMap.set(el, animation); // 保存动画对象
} else {
// 如果不支持 Web Animations API,则添加 CSS 动画回退类
el.classList.add('animate-fallback'); // animate-fallback在下面SCSS中有定义
}

ob.observe(el); // 开始监听元素的可见性变化
},
unbind(el) { // 元素从 DOM 中移除时触发
ob.unobserve(el); // 停止监听元素的可见性变化
}
};

Vue.directive(directive.name, directive);


注册指令


image.png


directives/index.js


import './slide-in' // 元素缓慢上升效果

main.js


import './directives'

在页面中使用


<template>
<div class="boxs .scroll-container">
<div class="slide-box" v-slide-in="{ duration: 500, easing: 'ease-in-out' }">0 - slide-directives</div>
<div class="slide-box" v-slide-in>1 - slide-directives</div>
<div class="slide-box" v-slide-in>2 - slide-directives</div>
<div v-slide-in>3 - slide-directives</div>
<div v-slide-in="{ duration: 500, easing: 'linear' }">4 - slide-directives</div>
<div v-slide-in>5 - slide-directives</div>
<div v-slide-in="{ duration: 500 }">6 - slide-directives</div>
</div>
</template>

<style lang="scss" scoped>
.boxs {
div {
text-align: center;
width: 800px;
height: 300px;
background-color: #f2f2f2;
margin: 0 auto;
margin-top: 20px;
}
}

<!-- 兼容性处理(可放到全局style中) -->
.animate-fallback {
opacity: 0;
transform: translateY(100px);
transition: transform 1s ease, opacity 1s ease;
}

.animate-fallback.active {
opacity: 1;
transform: translateY(0);
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateY(100px);
}

to {
opacity: 1;
transform: translateY(0);
}
}

.animate-fallback-keyframes {
opacity: 0;
animation: slideIn 1s ease forwards;
}
</style>

方式2: 封装为组件


<template>
<div ref="animatedElement" :style="computedStyle">
<slot></slot>
</div>
</template>

<script>
export default {
name: 'slideIn',
props: {
duration: { // 动画持续时间
type: Number,
default: 1000
},
easing: { // 动画缓动效果
type: String,
default: 'ease'
},
distance: { // 动画距离
type: Number,
default: 100
}
},
data() {
return {
hasAnimated: false // 是否已经动画过
}
},
computed: {
computedStyle() {
return {
opacity: this.hasAnimated ? 1 : 0,
transform: this.hasAnimated ? 'translateY(0)' : `translateY(${this.distance}px)`,
transition: `transform ${this.duration}ms ${this.easing}, opacity ${this.duration}ms ${this.easing}`
}
}
},
mounted() {
if (typeof window !== 'undefined' && 'IntersectionObserver' in window) { // 检测是否支持IntersectionObserver
this.createObserver() // 创建IntersectionObserver
} else {
// 如果不支持IntersectionObserver,则使用scroll事件来实现动画
this.observeScroll()
}
},
methods: {
createObserver() {
const observer = new IntersectionObserver(entries => { // IntersectionObserver回调函数
entries.forEach(entry => { // 遍历每个观察目标
if (entry.isIntersecting && !this.hasAnimated) { // 如果目标进入视口并且没有动画过
this.hasAnimated = true // 标记动画过
observer.unobserve(entry.target) // 停止观察
}
})
}, { threshold: 0.1 }) // 观察阈值,表示目标在视口的百分比
observer.observe(this.$refs.animatedElement) // 观察目标
},
observeScroll() {
const onScroll = () => { // scroll事件回调函数
if (this.isInViewport(this.$refs.animatedElement) && !this.hasAnimated) { // 如果目标在视口并且没有动画过
this.hasAnimated = true // 标记动画过
window.removeEventListener('scroll', onScroll) // 停止监听scroll事件
}
}
window.addEventListener('scroll', onScroll) // 监听scroll事件
},
isInViewport(el) { // 判断目标是否在视口
const rect = el.getBoundingClientRect()
return rect.top < window.innerHeight && rect.bottom > 0
}
}
}
</script>

页面使用


<div class="text-slide-in-vue">
<slide-comp v-for="(s ,idx) in list" :key="idx">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>

<div class="level-slide">
<slide-comp v-for="(s, idx) in list" :key="idx" :duration="500 * idx + 500">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>

<style>
.text-slide-in-vue {
p {
text-align: center;
width: 400px;
height: 200px;
background-color: goldenrod;
margin: 0 auto;
margin-top: 20px;
}
}

.level-slide {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 20px;
p {
text-align: center;
width: 200px;
height: 200px;
background-color: blueviolet;
margin: 0 auto;
margin-top: 20px;
}
}
</style>

作者:Blue啊
来源:juejin.cn/post/7401042923490836480

0 个评论

要回复文章请先登录注册