跟手动画原理

624 阅读3分钟

跟手动画原理

前置知识

Intersection Observer

Intersection Observer API提供了一种异步检测目标元素与祖先元素或视口(可统称为根元素)相交情况变化的方法。

IntersectionObserver接口

juejin.cn/post/714644…

const target = document.querySelector(".target")

 //IntersectionObserver构造器的callback函数
const callback = (entries) =>{
  console.log(entries[0].intersectionRatio)
  if (entries[0].intersectionRatio > 0) {
    document.body.style.backgroundColor = '#333';
    // 
    // observer.disconnect()
  } else {
    document.body.style.backgroundColor = '#fff'
  }
}

  //IntersectionObserver构造器的option函数
 const option = {
     root:root //用作边界的视口,默认为视口
     rootMargin:"0px 0px 0px 0px" //改变边界视口的检测范围,类似于margin
     threshold:[0,0.1]或者0 //数组或具体值,当相交比例越过这些值,触发callback函数
 }

 //通过IntersectionObserver构造器构造一个新的IntersectionObserver对象
let observer = new IntersectionObserver(callback,option)

 //该对象拥有四个方法
observer.observe(target)//绑定需要检测的元素target,可绑定多个
observer.observe(target2)
observer.unobserve(target)//解绑对应元素
observer.takeRecords()// 返回监听的数组
observer.disconnected()//解除所有的监听

UML 图 (2).jpg intersectionRect属性,会返回target与检测视口相交的高度。

Emittery

Emittery : 简单而现代的异步事件发射器

关于异步实现的原理和优点待补充

EventEmitter(同步发射器)的异同点待补充

Emittery简介

Emittery常用方法

on(eventName | eventName[], listener)绑定监听

const Emittery = require('emittery');

const emitter = new Emittery();

emitter.on('🦄', data => {
        console.log(data);
});
// emitter对象的on方法,eventName监听的事件名,listener回调函数

emitter.on(['🦄', '🐶'], data => {
        console.log(data);
});

emitter.emit('🦄', '🌈'); // 输出 '🌈' x2
emitter.emit('🐶', '🍖'); // 输出 '🍖' 

off(eventName | eventName[], listener)解除监听

const Emittery = require('emittery');

const emitter = new Emittery();

const listener = data => console.log(data);
(async () => {
        emitter.on(['🦄', '🐶', '🦊'], listener);
        await emitter.emit('🦄', 'a'); //输出a
        await emitter.emit('🐶', 'b'); //输出b
        await emitter.emit('🦊', 'c'); //输出c
        emitter.off('🦄', listener);
        emitter.off(['🐶', '🦊'], listener);
        await emitter.emit('🦄', 'a'); // nothing happens
        await emitter.emit('🐶', 'b'); // nothing happens
        await emitter.emit('🦊', 'c'); // nothing happens
})();

once(eventName | eventName[]).then(listener)监听执行后解除监听

const Emittery = require('emittery');

const emitter = new Emittery();

emitter.once('🦄').then(data => {
        console.log(data);
        //=> '🌈'
});
emitter.once(['🦄', '🐶']).then(data => {
        console.log(data);
});

emitter.emit('🦄', '🌈'); // log => '🌈' x2
emitter.emit('🐶', '🍖'); // 监听已经触发过,nothing happens
emitter.emit('🦄', '🌈'); // 监听已经触发过,nothing happens

resize-observer-polyfill

resize-observer-polyfill能立即检测元素何时调整大小并将准确的大小信息返回给回调函数

仓库地址:github.com/juggle/resi…

import { ResizeObserver } from '@juggle/resize-observer';

const ro = new ResizeObserver((entries, observer) => {
 //entries对象属性待补充
console.log('Body has resized!');
observer.disconnect(); // Stop observing

});

ro.observe(document.body); // Watch dimension changes on body

跟手动画实现-Scroom

Scroom仓库地址

主要原理:

    // 跟手动画的容器高度一般是大于视口的
    <div class="wrapper" style="height: 3000px">
    //播放动画元素通过sticky布局在滚动中固定位置
      <div
        class="player"
        style="width: 80%; position: sticky; top: 0; margin: 0 auto"
      ></div>
    </div>

上文已经介绍intersectionRect属性(target与检测视口相交的高度),但intersectionRect最大交叉高度小于监测视口&Target高度,滑动处在下图位置时,intersectionRect的值将固定为600px(监测视口),此时动画的进度无法计算

UML 图 (3).jpg 利用rootMargin属性,将监测视口扩充到与Target高度一致,就可以实现滚动过程intersectionRect实时变化,能够完整监测滚动过程.

计算目标与默认浏览器视口的差值:

const bottomMargin = target.clientHeight - window.innerHeight;

rootMargin = `0px 0px ${bottomMargin }px 0px`;

UML 图 (4).jpg

由于跟手动画触发要在容器划出监测视口触发,简单的判断方式就是,相交区域target top小于target元素的top,证明target已经有一部分划出监测视口。

isIntersecting boolean值代表target是否还在监测视口未完全划出

isProgressing=``boundingClientRect.top < intersectionRect.top`` && isIntersecting;

具体实现

调用

let target = document.querySelectorAll(".wrapper")

const sc = createScroom({
    target: target ,
    // offset调整检测视口偏移,当offset为1时,progress在浏览器视口底部触发
    //为0时,progrss在浏览器视口顶部触发
    offset: 0.5,
   });
   //绑定progress异步触发器
    sc.on("progress", (e) => {
      console.log(e);
 });

核心代码

function createScroom (options)
{
  const { target, offset = 0.5, threshold = 4, direction = 'vertical' } = options;
  const emitter = new Emittery();

  // observer for intersecting
  let intersectionObserver = createIntersectionObserver();

  // resizeObserver 生成了一个ResizeObserver对象在target尺寸变化时触发resizeHandler
  let resizeObserver = createResizeObserver();
  function createResizeObserver() {
    const observer = new ResizeObserver(resizeHandler);
    return observer;
  }
  function resizeHandler() {
  //此回调函数在target尺寸变化时触发,销毁所有滚动监听,并重新绑定
    intersectionObserver.disconnect();
    intersectionObserver = createIntersectionObserver();
    intersectionObserver.observe(target);
  }

  function createIntersectionObserver() {
     .....//生成Intersection Observer核心代码,下文分析
    return observer;
  }


  function destroy() {
    intersectionObserver.disconnect();
    resizeObserver.disconnect();
    emitter.clearListeners();
    window.removeEventListener('resize', resizeHandler);
  }

    // 利用上文resize-observer-polyfill监视target尺寸变化
  resizeObserver.observe(target);
    // 监视浏览器视口的变化
  window.addEventListener('resize', resizeHandler);

    //返回对象带有的两个对象,四个方法
  return {
    target,
    observer: intersectionObserver,
    destroy,
    on: emitter.on.bind(emitter),
    once: emitter.once.bind(emitter),
    off: emitter.off.bind(emitter),
  };
}

**IntersectionObserver**对象生成函数

//const { target, offset = 0.5, threshold = 4, direction = 'vertical' }需要参数
function createIntersectionObserver() {
    let rootMargin: string;
    let thresholdStep: number;
    //根据浏览器视口高度及offset调节监测视口
    const m1 = -offset * window.innerHeight;
    const m2 = (offset - 1) * window.innerHeight + target.clientHeight;
    rootMargin = `${m1}px 0px ${m2}px 0px`;
    
    //IntersectionObserver对象回调函数的触发时机,
    //即threshold(属性),可以理解为每thresholdpx(参数)触发一次,(命名重复)
    thresholdStep = threshold / target.clientHeight;

    let isIntersectingLastTick = false;
    let progressLastTick = -1;

    let isFirstEnter = true;
    setTimeout(() => {
      isFirstEnter = false;
    });

    const observer = new IntersectionObserver(
      ([entry]) => {
        // boundingClientRect
        const { 
        //target绑定元素的尺寸对象
        boundingClientRect: client, 
        //target绑定元素与浏览器相交的尺寸对象
        intersectionRect: rect, 
        //元素是否在监测视口
        isIntersecting } = entry;
        
        let isProgressing = false;
        let progress = 0;

        // 当检测的元素上边界完全划过视口 && 元素未完全消失isProgressing=true
        //client.top < rect.top保证了容器在划出视口时进入Progressing
        isProgressing = client.top < rect.top && isIntersecting;
        // 根据相交高度及元素总高度计算进度
        progress = 1 - rect.height / client.height;

        isIntersectingLastTick = isProgressing;

        const emitProgress = (progress) => {
          if (progressLastTick !== progress) {
          //异步触发器的触发,需要在调用处.on绑定progress触发器
            emitter.emit('progress', {
              target,
              progress,
            });
          }
          progressLastTick = progress;
        };

        if (isProgressing && progressLastTick !== progress) {
          emitProgress(progress);
        }
        
      },
      {
        rootMargin,
        threshold: genRange(0, 1, thresholdStep),
      },
    );
    return observer;
  }