门户网站第二弹:实现滚动加载 - 滚动驱动的动画

最近在开发官网的过程中,涉及到 UI 动画的制作,其中滚动效果的使用比较频繁,特此整理一下,以便查询和温习。

平滑向上过渡动画

这种往下滚动过渡渐变显示的动画是最常见的,比如:向上滚动平滑过渡案例

本文实现demo效果展示:

实现步骤

  • 利用 IntersectionObserver API 来监听各个元素是否进入视口
  • 当进入视口时播放进入的过渡动画
  • 播放过一次动画后关闭 IntersectionObserver 的监听
  • 在刷新页面且保持的滚动状态的时候,只监听当前滚动位置之下的元素

其中 IntersectionObserver 可以这样定义:

js 复制代码
const map = new WeakMap();

const ob = new IntersectionObserver(entries => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      // 当在视口内的时候,取出当前的元素,播放动画
      // 具体怎么记录 map,在下面的实现中会提到
      const animation = map.get(entry.target);
      if (animation) {
        animation.play();
        ob.unobserve(entry.target);
      }
    }
  }
});

vue 实现

首先定义一个指令 v-slide:

js 复制代码
const vSlide = {
  mounted(el) {
    // 判断不是在视口之下的元素,不需要添加动画
    if (!isBelowViewport(el)) {
      return;
    }
    const animation = el.animate([
      {
        transform: `translateY(${SLIDE_FADE_DISTANCE}px)`,
        opacity: 0.1
      },
      {
        transform: `translateY(0)`,
        opacity: 1
      }
    ], {
      duration: SLIDE_FADE_DURATION,
      easing: 'ease-in-out',
      fill: 'forwards' // 当动画完成后,保留最后一个关键帧的样式
    });

    animation.pause();
    map.set(el, animation);
    ob.observe(el);

    // return animation;
  },
  unmounted(el) {
    ob.unobserve(el);
  }
}

代码解释:利用元素自带的 animate API,给指令挂载的元素添加动画。在一开始全部都暂停,并将当前元素作为唯一key,存入动画对象到 map 中。最后开启 IntersectionObserver 监听。

模板定义如下:

html 复制代码
<div v-slide v-for="item in list" class="block" :style="{'background-color': item.bg}">...</div>

源码可参看:门户网站平滑滚动动画-vue

react 实现

实现原理与 vue 相似,只是没有自定义指令可用了,需要封装高阶组件来包裹需要监听的元素,在每一个子块上,使用 ref 来获取 dom 元素,并添加 observer。

源码参考:门户网站平滑滚动动画-react

进阶:复杂滚动动画

上面的加载方式是传统意义的动画,就是以时间为自变量,图形的长宽、颜色、位置等为因变量的一系列状态的变化。在门户网站里还有一种滚动动画,是把自变量改为滚动的位置,而其余的设置不变,动画随滚动位置的不同而不同。

参考案例:daisyui 官网,效果如下:

这种动画随着滚动只播放进度,但定位在屏幕中间一段时间的效果,是通过粘性定位来实现的,看一下原理图:

上图中,蓝色区域就是动画执行的滚动区域。

我们针对这个效果来做简单的实现。

首先来创建元素结构:

html 复制代码
// 蓝色区域
<div class="animation-container">
  <div class="header">我是顶部内容</div>
  
  // 黄色动画区域
  <div class="panel">
    <div class="item">1</div>  
    <div class="item">2</div>  
    <div class="item">3</div>  
    <div class="item">4</div>  
    <div class="item">5</div>  
  </div>
  <div class="other">我是其他区域内容</div>
</div>

加入基础样式后,效果如下:

然后通过 js 函数实现动画。

其实 css 已经支持设置动画方式为滚动了,只是存在兼容性问题,不得已采取这种兼容方式。

通过 js 批量操作 css 变化,最好的方式就是使用 css 变量:

css 复制代码
.panel {
  --rotate-rate: 1;
  --translate-y: 0;
  --translate-x: 0;
  --opacity: 1;
  position: sticky;
  top: 30%;
  margin: 0 auto;
  height: 200px;
  width: 250px;
  border: 1px solid blue;
  box-shadow: 1px 1px 2px #ccc;
  transform: rotateX(calc(20deg * var(--rotate-rate))) rotateZ(calc(-20deg * var(--rotate-rate))) skewY(calc(8deg * var(--rotate-rate)));
}

.panel > .item {
  color: white;
  height: 30px;
  width: 30px;
  border: 1px dashed red;
  background-color: blueviolet;
  box-shadow: 4px 4px 6px #ccc;
  margin-bottom: 10px;
  opacity: var(--opacity);
  transform: translate(var(--translate-x), var(--translate-y));
}

这样只需要 js 获取滚动位置,配置位置与各个变量之间的映射,并实时刷新映射,写入变量就可以了。

我们先写一个通用函数,接受滚动的开始、结束位置,开始和结束的值:

js 复制代码
function createAnimation(
  scrollStart,
  scrollEnd,
  startValue,
  endValue
) {
  return function(x) {
    // 控制边界
    if (x < scrollStart) {
      return startValue;
    }

    if (x > scrollEnd) {
      return endValue;
    }

    // 滚动位置占所有可滚动区域的百分比
    const progress = (x - scrollStart) / (scrollEnd - scrollStart);

    // 返回当前滚动位置累加出来的值(其实就是百分比计算)
    return startValue + (endValue - startValue) * progress;
  }
}

这个函数返回的值一定在 startValue 和 endValue 之间,返回高阶函数,接受当前滚动位置,其通过滚动位置均匀地计算中间位置的样式值。我们以透明度为例:

js 复制代码
const animationMap = new Map();
const container = document.querySelector('.animation-container');
const items = document.querySelectorAll('.item');

// 初始化执行一遍即可
function updateMap() {
  const containerRect = container.getBoundingClientRect();
  
  // 根据滚动容器(蓝色区域)高度计算起始、结束区域
  const scrollStart = containerRect.top;
  const scrollEnd = containerRect.bottom - window.innerHeight;

  for (const item of items) {
    animationMap.set(item, getDomAnimation(item, scrollStart, scrollEnd));
  }
}

// 返回 css 变量计算好的值
function getDomAnimation(dom, scrollStart, scrollEnd) {
  // 这里从不透明变化到 0.2,
  const opacityAnimation = createAnimation(scrollStart, scrollEnd, 1, 0.2);

  return {
    '--opacity': function(x) {
      return opacityAnimation(x);
    },
  }
}

定义一个 map 来维护滚动位置与样式的映射关系,执行 updateMap 函数后,透明度参数就写进 map 里了。

接下来就是将 map 更新进样式里:

js 复制代码
// 获取动画区域。写入变量
const panel = document.querySelector('.panel');

function updateStyles() {
  const scrollY = window.scrollY; // 如果要计算某个容器内的滚动量,请使用 ele.scrollTop

  for (const [dom, animations] of animationMap) {
    for (const prop in animations) {
      // 每一个 animations 就是上边返回的 function(x){...} 函数,接受参数是当前的滚动位置
      // prop 是 getDomAnimation 返回的对象的每一个 key 值,这里是 css 变量标识
      panel.style.setProperty(prop, animations[prop](scrollY));
    }
  }
}

如此,基础的框架就搭好了,补上必须的 css,查看效果:

可以看到,我们示例中配置的透明度也变化了。

全部源码:复杂滚动案例

滚动动画实战:视差滚动动画

视差滚动也是随着滚动位置的不同而改变的动画,他是背景图片位置的变化。

视差滚动指的是块盒在整体滚动的时候,内容滚动距离与背景图滚动距离不一致(一般是背景图滚动慢于内容),从而产生视觉的差异性效果,让背景图片随着滚动有一定的溢出效果。这种效果常常用于官网的产品展示板块。

效果如图所示:

实现步骤:

  1. 当元素的上边框出现在视口中时,开始播放动画。
  2. 当元素下边框在视口中消失时,停止动画。
  3. 动画设置:往上滚动设置背景图片往下平移,往下滚动设置背景图片往下平移。平移的量的范围是 正负浏览器视口高度一半。

我们设置 html 元素:

html 复制代码
<section>
  <h1>111</h1>
</section>
<section>
  <h1>222</h1>
</section>
...

为了节省代码量,直接使用第三方动画库 gsap 配置插件 scrolltrigger 来实现。

核心实现代码就是一个滚动动画配置:

js 复制代码
// 遍历各个 section,添加按照滚动量计算的动画
sections.forEach(item => {
  gsap.fromTo(item, {
    backgroundPositionY: `-${window.innerHeight / 2}px`,
    ease: 'none'
  }, {
    backgroundPositionY: `${window.innerHeight / 2}px`,
    ease: 'none',
    scrollTrigger: {
      trigger: item,
      scrub: true, // 动画根据滚动位置决定
    }
  })
});

源码可参考 视差滚动案例 demo - js实现

CSS 实现滚动加载

新版本的 chrome 支持 css 来实现滚动驱动:animation-timeline: scroll()

不作为主要方法讲述,是因为他还只是实验性功能,存在兼容性问题。当兼容性问题解决后,这个方式将成为滚动动画的主流方式。

我们创建元素:

html 复制代码
<div id="container">
  <div id="header">我是顶部内容</div>

  <div id="stretcher">
    我是动画区域
    <div id="square"></div>
  </div>

  <div id="other">我是底部内容</div>
</div>

其中 stretcher 是滚动区域。

我们定义需要动画的元素样式:

css 复制代码
#square {
  position: sticky;
  top: calc(50% - 50px);
  width: 100px;
  height: 100px;
  animation-name: myAnimation;
  animation-timeline: scroll();
}

#stretcher {
  height: 2000px;
}

其中,animation-timeline 声明是滚动驱动,滚动的方向为从下往上,相对滚动容器为父容器 stretcher,滚动的起始点就是动画的开始点。同时声明 sticky 定位,让滚动容器 stretcher 在滚动过程中,内部执行动画的元素能够一直显示在视窗中。

我们定义一下动画 myAnimation:

css 复制代码
@keyframes myAnimation {
  0% {
    opacity: 0;
    transform: translateX(0) scale(0.5);
  }
  100% {
    opacity: 1;
    transform: translateX(300px) scale(1.5);
  }
}

展示效果如下:

如果你想控制动画播放的起始结束时间,不想让其覆盖滚动的全周期,可以使用 animation-range-start 和 animation-range-end 属性来定义。

源码:css 滚动案例


完结撒花 ✿✿ヽ(°▽°)ノ✿ ~~

相关推荐
沈梦研1 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
轻口味2 小时前
Vue.js 组件之间的通信模式
vue.js
光头程序员4 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me4 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者4 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
fmdpenny5 小时前
Vue3初学之商品的增,删,改功能
开发语言·javascript·vue.js
涔溪5 小时前
有哪些常见的 Vue 错误?
前端·javascript·vue.js
VillanelleS7 小时前
React进阶之高阶组件HOC、react hooks、自定义hooks
前端·react.js·前端框架
亦黑迷失7 小时前
vue 项目优化之函数式组件
前端·vue.js·性能优化
计算机-秋大田8 小时前
基于SpringBoot的高校教师科研的设计与实现(源码+SQL脚本+LW+部署讲解等)
java·vue.js·spring boot·后端·课程设计