动画原理
animate API
效果预览
完整源码
<script setup>
import vSlideIn from './directives/vSlideIn.js';
</script>
<template>
<div class="container">
<div v-slide-in class="item">1</div>
<div v-slide-in class="item">2</div>
<div v-slide-in class="item">3</div>
<div v-slide-in class="item">4</div>
<div v-slide-in class="item">5</div>
<div v-slide-in class="item">6</div>
<div v-slide-in class="item">7</div>
<div v-slide-in class="item">8</div>
<div v-slide-in class="item">9</div>
<div v-slide-in class="item">10</div>
</div>
</template>
<style scoped>
.item {
color: #fff;
width: 60%;
height: 400px;
line-height: 400px;
text-align: center;
background: orange;
margin: 20px auto;
font-size: 50px;
}
</style>
const DISTANCE = 150;
const DURATION = 1000;
// 存放元素与动画对象的映射关系
const animationMap = new WeakMap();
const ob = new IntersectionObserver((entries) => {
// console.log(entries);
for (let entry of entries) {
// 如果和视口有重叠
if (entry.isIntersecting) {
const animation = animationMap.get(entry.target);
animation.play();
// 播放完成后,就没有必要再播放了
ob.unobserve(entry.target);
}
}
});
// 判断元素是否在视口之下
const isBelowViewport = (el) => {
const rect = el.getBoundingClientRect();
return rect.top > window.innerHeight;
};
export default {
mounted(el) {
if (!isBelowViewport(el)) {
return;
}
const animation = el.animate(
[
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5,
},
{
transform: 'translateY(0px)',
opacity: 1,
},
],
{
duration: DURATION,
easing: 'ease',
}
);
animation.pause();
ob.observe(el);
animationMap.set(el, animation);
},
unmounted(el) {
ob.unobserve(el);
},
};
实现解析
第一步
使用animation API实现过渡动画
const DISTANCE = 150;
const DURATION = 1000;
export default {
mounted(el) {
const animation = el.animate(
[
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5,
},
{
transform: 'translateY(0px)',
opacity: 1,
},
],
{
duration: DURATION,
easing: 'ease',
}
);
},
};
第二步
const DISTANCE = 150;
const DURATION = 1000;
// 存放元素与动画对象的映射关系
const animationMap = new WeakMap();
const ob = new IntersectionObserver((entries) => {
// console.log(entries);
for (let entry of entries) {
// 如果和视口有重叠
if (entry.isIntersecting) {
const animation = animationMap.get(entry.target);
animation.play();
// 播放完成后,就没有必要再播放了
ob.unobserve(entry.target);
}
}
});
export default {
mounted(el) {
const animation = el.animate(
[
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5,
},
{
transform: 'translateY(0px)',
opacity: 1,
},
],
{
duration: DURATION,
easing: 'ease',
}
);
// 先暂停动画
animation.pause();
// 然后观察该元素
ob.observe(el);
// 最后建立 Dom 和 animation 的映射关系
animationMap.set(el, animation);
},
unmounted(el) {
ob.unobserve(el);
},
};
- IntersectionObserver 实例可以通过 observe 方法添加监听的元素,也可以通过 unobserve 解除监听
- 在创建 IntersectionObserver 实例的时候,可以传入一个回调函数,这个回调函数在监听的元素进入视口或者离开视口的时候调用,然后回调函数的参数是一个数组,数组项通过 isIntersecting 判断是否进入视口,数组项通过 target 属性可以拿到监听的 Dom
- 为什么要使用 WeakMap
第三步
在视口以下的元素才有动画
const DISTANCE = 150;
const DURATION = 1000;
// 存放元素与动画对象的映射关系
const animationMap = new WeakMap();
const ob = new IntersectionObserver((entries) => {
// console.log(entries);
for (let entry of entries) {
// 如果和视口有重叠
if (entry.isIntersecting) {
const animation = animationMap.get(entry.target);
animation.play();
// 播放完成后,就没有必要再播放了
ob.unobserve(entry.target);
}
}
});
// 判断元素是否在视口之下
const isBelowViewport = (el) => {
const rect = el.getBoundingClientRect();
return rect.top > window.innerHeight;
};
export default {
mounted(el) {
if (!isBelowViewport(el)) {
return;
}
const animation = el.animate(
[
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5,
},
{
transform: 'translateY(0px)',
opacity: 1,
},
],
{
duration: DURATION,
easing: 'ease',
}
);
animation.pause();
ob.observe(el);
animationMap.set(el, animation);
},
unmounted(el) {
ob.unobserve(el);
},
};
- 获取元素的尺寸
- getComputedStyle(dom).width,这个尺寸读取的是 CSSOM 树,也就是整个浏览器绘制管线的第二个步骤,这种尺寸一般情况下不会读取,因为拿到的尺寸不一定是界面上的尺寸,比如我设置宽度为 100px,那么在界面上就不一定是 100px,比如加了 padding 或者 border。所以读取元素尺寸的时候最好不要用
- dom.style.width 这里读取的 dom 元素上的 style 属性的值,如果没有这个属性就读取不到,也不建议使用这种方式去读取元素尺寸
- clientWidth 包含元素的 content +padding,不包含边框和滚动条。读取的是 layout tree 的尺寸
- offsetWidth 包含元素的 content + padding + scroll + border。读取的是 layout tree 的尺寸
- scrollWidth 不是元素本身的尺寸,表示元素内部可滚动区域的宽度,如果没有滚动条和 clientWidth 的值一样。读取的是 layout tree 的尺寸
- const rect = dom.getBoundingClientRect(),读取的是 GPU 进程绘制出来的尺寸,是实际在界面中展示的尺寸