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

最近在开发官网的过程中,涉及到 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 滚动案例


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

相关推荐
开心工作室_kaic2 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育2 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博2 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
isSamle2 小时前
使用Vue+Django开发的旅游路书应用
前端·vue.js·django
ss2733 小时前
基于Springboot + vue实现的汽车资讯网站
vue.js·spring boot·后端
武昌库里写JAVA3 小时前
浅谈怎样系统的准备前端面试
数据结构·vue.js·spring boot·算法·课程设计
TttHhhYy4 小时前
uniapp+vue开发app,蓝牙连接,蓝牙接收文件保存到手机特定文件夹,从手机特定目录(可自定义),读取文件内容,这篇首先说如何读取,手机目录如何寻找
开发语言·前端·javascript·vue.js·uni-app
飞翔的渴望4 小时前
antd3升级antd5总结
前端·react.js·ant design
CodeChampion6 小时前
61.基于SpringBoot + Vue实现的前后端分离-在线动漫信息平台(项目+论文)
java·vue.js·spring boot·后端·node.js·maven·idea
╰つ゛木槿7 小时前
深入了解 React:从入门到高级应用
前端·react.js·前端框架