使用 Vue + Vite + TypeScript 实现无限视差滚动效果

1. 结果展示

先来看一下最终效果:在向下滚动时,图片逐渐向上偏移并显现,而在向上滚动时,图片逐渐向下偏移并显现

无限视差滚动

2. 介绍

无限视差滚动效果是一种在网页设计中常用的技术,通过以不同速度移动页面上的不同层次元素,创造出动态的视觉效果。它可以应用于各种网站和页面,包括故事性网站、产品展示页面、创意型网站以及导航和交互设计中。这种效果能够增强用户体验、提升页面吸引力,并为网站注入更多创意和个性。

3. 实现原理与步骤

3.1 布局结构搭建

准备一些图片,并编写一个滚动区域,其中包含三个<div>元素,每个<div>内包含一个<img>元素。

  • 效果示意图:
    可以看到,不管怎么滚动,页面上自始至终都只有3张图片
    在这里插入图片描述
  • 基本代码结构:初始化图片路径,通过css将三张图片重合在一起。
<template>
  <div class="scroll-container">
    <div class="prev">
      <img :src="prev" class="item">
    </div>
    <div class="cur">
      <img :src="cur" class="item">
    </div>
    <div class="next">
      <img :src="next" class="item">
    </div>
  </div>
</template>

<script setup lang='ts'>
// 获取 assets/img/rose路径下的所有图片
const imgList = Object.keys(
  import.meta.glob("/src/assets/img/rose/*.{png,jpg,gif,svg,avif}")
);

const prev = ref(imgList[0]);
const cur = ref(imgList[1]);
const next = ref(imgList[2]);

</script>
<style scoped lang="scss">
.scroll-container {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

.item {
  position: absolute;
  left: 0;
  width: 100vw;
  height: 100vh;
  object-fit: cover;
}

.prev,
.cur,
.next {
  position: absolute;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vh;
}
</style>

3.2 监听滚轮事件

为了实现滚动区域的响应式滚动效果,我们需要在滚动区域的容器上添加 @wheel 滚轮事件监听器,并在事件触发时进行相应的处理。具体步骤如下:

  • 在滚动区域的容器上添加@wheel滚轮事件监听器,并绑定相应的处理函数。

  • 在滚轮事件处理函数中,判断滚轮滚动的方向。可以通过事件对象中的deltaY 属性来判断滚动的方向,deltaY > 0表示向下滚动,deltaY < 0 表示向上滚动。

  • 根据滚动方向,动态地为滚动区域的容器添加或移除相应的 class。这些 class 可以用来控制页面元素的过渡效果,如淡入淡出、滑动等。

  • 为了确保滚动事件在过渡完成前只触发一次,需要设置一个变量用于标记是否正在进行过渡动画,在过渡完成前只允许触发一次滚动事件。

滚动区域添加@wheel滚轮事件监听,绑定滚动方向的动态:class属性

<div class="scroll-container" @wheel="onWheel($event)" :class="direction">

滚动事件只能在动画完成后执行,设置滚动方向的动态:class属性

// 是否正在过渡动画中
let isAnimate = false;
// 滚动方向(默认为init)
type Direction = 'up' | 'down' | 'init';
let direction = ref<Direction>('init');

/**
 * 滚轮事件
 * @param event 
 */
const onWheel = (event: unknown) => {
  if (isAnimate) return;
  const deltaY = (event as WheelEvent).deltaY;

  if (Math.abs(deltaY) === 0 || isAnimate) return; // deltaY为0或是已经在动画中时,直接返回

  isAnimate = true;

  if (deltaY > 0) {
    direction.value = 'down';
  } else if (deltaY < 0) {
    direction.value = 'up';
  }
}

3.3 图片切换工具函数

先来实现图片路径的切换,在滑动后,要根据当前显示的图片下标来更新3张图片

const currentIndex = ref(1);
/**
 * 滑动后,刷新图片路径
 * @param {number} index 当前显示图片的下标
 */
const refresh = (index: number) => {
  currentIndex.value = index;
  const prevIndex = index === 0 ? imgList.length - 1 : index - 1;
  const nextIndex = index === imgList.length - 1 ? 0 : index + 1;
  prev.value = imgList[prevIndex];
  cur.value = imgList[index];
  next.value = imgList[nextIndex];
}

3.4 监听过渡结束事件

为了实现无限滚动的效果,我们需要在过渡完成后更新图片

  • 绑定过渡完成事件监听器@transitionend
 <div class="scroll-container" @wheel="onWheel($event)" :class="direction" @transitionend="onTransitionend">
  • 根据滚动方向更新图片,并标记过渡动画已完成。
/**
 * 过渡完成事件
 */
const onTransitionend = () => {
  if (direction.value == 'down') {
    // 下滑时,下标为末尾,设置为0
    const index = currentIndex.value === imgList.length - 1 ? 0 : currentIndex.value + 1
    refresh(index);
  } else if (direction.value == 'up') {
    // 上滑时,下标为0,设置为末尾下标
    const index = currentIndex.value === 0 ? imgList.length - 1 : currentIndex.value - 1
    refresh(index);
  }
  // 过渡完成
  isAnimate = false;
  // 重置滚动区域方向
  direction.value = 'init';
}

3.5 视差效果实现

  • 向下滚动前,父元素底部通过绝对定位固定在视口底部,设置z-index: 1,保证覆盖。图片底部通过绝对定位设置在父元素底部,设置translateY(10%)
    过渡时,父元素的高度从0逐渐增加至整个视口高度。图片设置translateY(0),这样图片有一个上移的效果
    在这里插入图片描述
  • 向上滚动前,父元素通过绝对定位固定在视口顶部,设置z-index: 1,保证覆盖。图片顶部通过绝对定位设置在父元素顶部,设置translateY(-10%)
    过渡时,父元素的高度从0逐渐向下增加至整个视口高度。图片设置translateY(0),这样图片有一个下移的效果。
    在这里插入图片描述
  • css样式
.prev,
.cur,
.next {
  position: absolute;
  left: 0;
  right: 0;
}


.prev {
  top: 0;
  height: 0;
  overflow: hidden;
  z-index: 1;

  img {
    top: 0;
    transform: translateY(-10%);
  }
}

.cur {
  top: 0;
  height: 100vh;

  .item {
    top: 0;
  }
}

.next {
  bottom: 0;
  height: 0;
  overflow: hidden;
  z-index: 1;

  .item {
    bottom: 0;
    transform: translateY(10%);
  }
}

.down .next,
.up .prev {
  height: 100vh;
  transition: 1s ease-in-out;

  .item {
    transform: translateY(0);
    transition: 1s ease-in-out;
  }
}

4. 完整代码参考

test.vue

<template>
  <div class="scroll-container" @wheel="onWheel($event)" :class="direction" @transitionend="onTransitionend">
    <div class="prev">
      <img :src="prev" class="item">
    </div>
    <div class="cur">
      <img :src="cur" class="item">
    </div>
    <div class="next">
      <img :src="next" class="item">
    </div>
  </div>
</template>

<script setup lang='ts'>
// 获取 assets/img/rose路径下的所有图片
const imgList = Object.keys(
  import.meta.glob("/src/assets/img/rose/*.{png,jpg,gif,svg,avif}")
);

const prev = ref(imgList[0]);
const cur = ref(imgList[1]);
const next = ref(imgList[2]);

const currentIndex = ref(1);
/**
 * 滑动后,刷新图片路径
 * @param {number} index 
 */
const refresh = (index: number) => {
  currentIndex.value = index;
  const prevIndex = index === 0 ? imgList.length - 1 : index - 1;
  const nextIndex = index === imgList.length - 1 ? 0 : index + 1;
  prev.value = imgList[prevIndex];
  cur.value = imgList[index];
  next.value = imgList[nextIndex];
}

// 是否正在过渡动画中
let isAnimate = false;
// 滚动方向
type Direction = 'up' | 'down' | 'init';
let direction = ref<Direction>('init');

/**
 * 滚轮事件
 * @param event 
 */
const onWheel = (event: unknown) => {
  if (isAnimate) return;
  const deltaY = (event as WheelEvent).deltaY;

  if (deltaY === 0 || isAnimate) return;

  isAnimate = true;

  if (deltaY > 0) {
    direction.value = 'down';
  } else if (deltaY < 0) {
    direction.value = 'up';
  }
}

/**
 * 过渡完成事件
 */
const onTransitionend = () => {
  if (direction.value == 'down') {
    // 下滑时,下标为末尾,设置为0
    const index = currentIndex.value === imgList.length - 1 ? 0 : currentIndex.value + 1
    refresh(index);
  } else if (direction.value == 'up') {
    // 上滑时,下标为0,设置为末尾下标
    const index = currentIndex.value === 0 ? imgList.length - 1 : currentIndex.value - 1
    refresh(index);
  }
  // 过渡完成
  isAnimate = false;
  // 重置滚动区域方向
  direction.value = 'init';
}
</script>
<style scoped lang="scss">
.scroll-container {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

.item {
  position: absolute;
  left: 0;
  width: 100vw;
  height: 100vh;
  object-fit: cover;
}

.prev,
.cur,
.next {
  position: absolute;
  left: 0;
  right: 0;
}

.prev {
  top: 0;
  height: 0;
  overflow: hidden;
  z-index: 1;

  img {
    top: 0;
    transform: translateY(-10%);
  }
}

.cur {
  top: 0;
  height: 100vh;

  .item {
    top: 0;
  }
}

.next {
  bottom: 0;
  height: 0;
  overflow: hidden;
  z-index: 1;

  .item {
    bottom: 0;
    transform: translateY(10%);
  }
}

.down .next,
.up .prev {
  height: 100vh;
  transition: 1s ease-in-out;

  .item {
    transform: translateY(0);
    transition: 1s ease-in-out;
  }
}
</style>

5. 总结

希望这篇文章能够帮助您更好地理解和实践无限视差滚动,并在您的项目中充分发挥其潜力。如果您有任何疑问或想法,欢迎随时留言分享。感谢您的阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值