跟手动画原理
前置知识
Intersection Observer
Intersection Observer API提供了一种异步检测目标元素与祖先元素或视口(可统称为根元素)相交情况变化的方法。
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()//解除所有的监听
intersectionRect属性,会返回target与检测视口相交的高度。
Emittery
Emittery : 简单而现代的异步事件发射器
关于异步实现的原理和优点待补充
与EventEmitter(同步发射器)的异同点待补充
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能立即检测元素何时调整大小并将准确的大小信息返回给回调函数
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
主要原理:
// 跟手动画的容器高度一般是大于视口的
<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(监测视口),此时动画的进度无法计算
利用
rootMargin属性,将监测视口扩充到与Target高度一致,就可以实现滚动过程intersectionRect实时变化,能够完整监测滚动过程.
计算目标与默认浏览器视口的差值:
const bottomMargin = target.clientHeight - window.innerHeight;
rootMargin = `0px 0px ${bottomMargin }px 0px`;
由于跟手动画触发要在容器划出监测视口触发,简单的判断方式就是,相交区域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;
}