1. 固定步长的动画
// JS动画的原理:通过定时器去不断修改一个元素的某个属性,因为定时器每隔一段时间就会执行一次,元素的属性每隔一段时间就会被修改一次,元素就去了新的位置,人的眼睛看到的就是动画效果;
// 固定步长的动画:每次定时器执行让这个元素改变固定的步长。
const { win, css } = window.utils;
let box = document.getElementById('box');
let step = 10; // 设置步长
let maxL = win('clientWidth') - css(box, 'width');
let timer = setInterval(() => {
// 固定步长的动画,在原有的基础上累加上步长,然后再设置回去
let preLeft = css(box, 'left'); // 获取原有的 left 值
let curLeft = preLeft + step;
// 动画过界判断: 判断元素是否运动到边界了,如果到达超过边界,先把定时器清除了,停止动画,并且把 curLeft 改成最大值
if (curLeft >= maxL) {
clearInterval(timer);
curLeft = maxL;
}
css(box, 'left', curLeft); // curLeft 是当前元素因下一帧的位置,要把 curLeft 设置给 box 的
// left 属性,box 才会去 curLeft 的位置
}, 16);
2. 指定时长的动画
/*
* 目标:
* 1. 理解指定时长的动画原理
* 2. 比较指定时长和指定步长动画的区别
*
* */
// 固定时长的动画:在指定的时间内,从某个位置运动到指定的位置;
// 在 3s 内运动到 left 的值是800;
const {css, win} = window.utils;
let time = 3000; // 3s 指定时间 3s
let target = 800; // 目标位置
// 路程 = 目标位置 - 起点位置;
let begin = css(box, 'left');
let change = target - begin; // 路程
// 速度 = 路程 / 时间
let speed = change / time; // 速度
// console.log(speed);
let curT = 0; // 记录从动画开始后经过的时间
// 使用定时器开启动画
let timer = setInterval(() => {
curT += 16; // curT经过的时间
console.log(curT);
if (curT >= time) { // 动画过界判断:如果 curT 大于3000,就应该修正成3000
clearInterval(timer);
curT = time;
}
let curLeft = curT * speed; // 在 curT 时间内走过的路程
let total = begin + curLeft; // 经过 curT 时间后,元素应该所处的位置
css(box, 'left', total);
}, 16);
3. utils库
// 封装一个工具集:增强代码的可复用性,提升开发效率;
// utils 工具包,这里面提供了常用的方法;
window.utils = (function () {
/**
* @desc 类数组转数组
* @param arrLike 类数组对象
* @returns {Array} 类数组对象转成的数组
*/
function arrLikeToAry(arrLike) {
try {
return Array.from(arrLike);
} catch (e) {
var ary = []; //
for (var i = 0; i < arrLike.length; i++) {
ary.push(arrLike[i]);
}
return ary;
}
}
/**
* @desc JSON格式字符串转对象
* @param jsonstr JSON格式字符串
* @returns {Object} 对象
*/
function toJSON(jsonstr) {
if ('JSON' in window) { // 'JSON' in window 返回 false 表示 JSON 的方法不可以用
return JSON.parse(jsonstr);
} else {
return eval('(' + jsonstr + ')');
}
}
/**
* @desc 获取 documentElement、document.body 的盒子模型属性
* @param attr 盒子模型属性名
* @param val 设置的值
* @returns {*} 获取的盒子模型属性值
*/
function win(attr, val) {
if (typeof val === 'undefined') {
// 如果 val 是 undefined,证明第二个参数没传,没传就是获取
return document.documentElement[attr] || document.body[attr] // 如果函数法返回值是表达
式,它会等着表达式求值,把求出来的值作为返回值返回
}
document.documentElement[attr] = document.body[attr] = val;
}
/**
* @desc 获取当前元素相对于 body 的左上角点坐标()
* @param ele 当前元素
* @returns {{left: number, top: number}} left:元素左外边到 body 左内边的距离; top: 元素的上
外边距离 body 上内边的距离
*/
function offset(ele) {
let left = ele.offsetLeft; // 当前元素的 offsetLeft
let top = ele.offsetTop; // 当前元素的 offsetTop
let parent = ele.offsetParent; // 获取当前元素的 offsetParent
while (parent && parent.nodeName !== 'BODY') {
left += parent.clientLeft + parent.offsetLeft;
top += parent.clientTop + parent.offsetTop;
parent = parent.offsetParent;
}
return {
left,
top
}
}
/**
* @desc 获取元素的计算生效的样式值
* @param ele 元素对象
* @param attr css属性
* @returns {*} css样式计算生效后的值
*/
function getCss(ele, attr) {
var value;
// 1. 判断是否是 IE 浏览器
if ('getComputedStyle' in window) { // 判断 window 对象上有 getComputedStyle 吗
value = window.getComputedStyle(ele, null)[attr];
} else {
// 执行 else 的时候说明是 IE 低版本,使用 currentStyle 属性
value = ele.currentStyle[attr];
}
// 把单位去掉:把数字且带单位的,把单位去掉
var reg = /^-?\d+(\.\d+)?px|rem|em|pt$/g;
if (reg.test(value)) {
value = parseFloat(value);
}
return value
}
/**
* @desc 设置元素对象的样式
* @param ele 元素对象
* @param attr CSS属性
* @param val 样式的值
*/
function setCss(ele, attr, val) {
let reg = /^(fontSize|width|height|(margin|padding)?(top|right|bottom|left)?)$/i;
if (reg.test(attr)) {
if (!isNaN(val)) val += 'px';
}
ele.style[attr] = val;
}
/**
* @desc 批量设置CSS样式
* @param ele
* @param cssBatch
*/
function setBatchCss(ele, cssBatch) {
// 批量设置 css 样式就是遍历传入的 CSS 对象,把样式和值依次设置给元素对象即可
// 检验 cssBatch 是不是一个对象
if (typeof cssBatch !== 'object') {
throw TypeError('cssBatch is not a object')
}
for (let key in cssBatch) {
// hasOwnProperty() 检测某个属性是不是对象私有的,是则 true,不是返回 false
if (cssBatch.hasOwnProperty(key)) { // 去复习for in循环、面向对象
setCss(ele, key, cssBatch[key]);
}
}
}
/**
* @desc 封装一个 css 的方法,根据参数不同有不同的功能
* @param ele 元素
* @param param CSS 样式或者 CSS 样式对象
* @param val CSS 样式值
* @returns {*} 获取时是 CSS 样式值
*/
function css(ele, param, val) {
// 根据传参的不同调用不同的方法
// 第二个参数是字符串类型,不传 val 时,就是获取
// 第二个参数是字符串时,并且第三个参数传了,就是设置单个样式
// 第二个参数是对象时,就是批量设置 CSS 样式
if (typeof param === 'string' && typeof val === 'undefined') {
return getCss(ele, param);
}
if (typeof param === 'string' && typeof val !== 'undefined') {
setCss(ele, param, val);
return;
}
if (typeof param === 'object') {
setBatchCss(ele, param);
}
}
/**
* @desc 判断当前元素有没有某个类名
* @param ele 元素对象
* @param className 类名
* @returns {boolean} 是否有类名
*/
function hasClass(ele, className) {
let cN = className.trim();
// return ele.className.includes(cN);
return ele.className.indexOf(cN) !== -1;
}
/**
* @desc 为元素添加类名
* @param ele 元素对象
* @param className 类名
*/
function addClass(ele, className) {
let cN = className.trim();
if (hasClass(ele, cN)) return;
// 优化:如果原来的类名末尾有空格,就可以不拼接空格,如果没有时再拼接
let reg = / $/g;
// 'box '
if (reg.test(ele.className)) {
ele.className += `${cN}`;
} else {
ele.className += ` ${cN}`;
}
}
/**
* @desc 移除类名
* @param ele 元素对象
* @param className 类名
*/
function removeClass(ele, className) {
let cN = className.trim();
let reg = new RegExp(cN, 'g');
ele.className = ele.className.replace(reg, '');
}
return {
arrLikeToAry,
toJSON,
win,
offset,
getCss,
setCss,
setBatchCss,
css,
hasClass,
addClass,
removeClass
}
})();
案例:回到顶部
css部分
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
.box {
height: 5000px;
background: -webkit-linear-gradient(top left, red, blue, green, orange);
}
.btn {
position: fixed;
right: 50px;
bottom: 50px;
border-radius: 50%;
width: 80px;
height: 80px;
background: #999;
line-height: 80px;
text-align: center;
color: #fff;
-webkit-user-select: none;
user-select: none;
cursor: pointer;
text-decoration: none;
}
</style>
</head>
<body>
<div id="box" class="box"></div>
<div class="btn" id="btn">回到顶部</div>
<!--<a href="#box" class="btn">回到顶部</a>-->
<!--锚点定位:把 a 标签的 href 设置成某个元素的 id# 元素 id 点击 a 标签就可以回到顶部 -->
<script src="js/utils.js"></script>
<script src="js/4-回到顶部.js"></script>
</body>
</html>
js部分
const {win} = window.utils; // 从 utils 中解构 win 方法
// 写一个动画,让页面缓慢的回到顶部
// 让页面回到顶部:需要操作盒子模型的 scrollTop 属性(页面滚动条卷去的距离)
let winScrollTop = win('scrollTop'); // win 方法获取页面滚动条卷去的高度;
let btn = document.getElementById('btn');
// 点击的时候才会触发滚动的行为
// 1. 瞬间回去
/*
btn.onclick = function () {
win('scrollTop', 0);
};
*/
// 2. 动画着回去:用一个定时器不断的修改页面的 scrollTop 值
/*let time = 2000; // 指定时间回去; 需要计算速度
let isRun = false; // 标识符:标识当前的滚动条是否正在运动
btn.onclick = function () {
if (isRun) return; // 如果 isRun 是 true ,说明现在有正在执行的动画,为了避免动画累加,后面的代
码不能执行;
// 1. 计算速度
let winScrollTop = win('scrollTop'); // 点击时页面滚动条卷去的高度
let speed = winScrollTop / time; // 计算速度
let curT = 0; // curT 记录经过的时间
isRun = true; // 下一行就要开启定时器了,所以在这里把 isRun 置为 true
let timer = setInterval(() => {
curT += 16; // 让时间累加
if (curT >= time) { // 当到大于等于 time 时,应该滚动到底了
clearInterval(timer);
curT = time;
isRun = false; // 动画结束后把 isRun 置为 false
}
let change = 16 * speed; // 在 curT 时间内走过的路程
winScrollTop -= change; // 经过 curT 时间后,页面滚动条的位置
win('scrollTop', winScrollTop); // 设置回去
}, 16);
};*/
// 避免动画累加的第一种方案:设置标识符,初始值是 false,表示当前没有正在执行的动画。如果开始动画,我们就把这个标识符置为 true;当我们再次开启动画之前,如果这个标识符是 true,表示当前有正在执行到的动画,就不能再次开启相同的动画;而且注意当动画结束后,把标识符修改为 false;
// 第二种解决动画累加的解决方案:在开启新的动画之前把之前的动画清除掉(把之前的定时器清除了);
let time = 2000;
// let timerId = null; 一般我们不用全局变量记录定时器 id,一般使用自定义属性的方式来记录定时器的 ID;
btn.onclick = function () {
// 1. 计算速度
let winScrollTop = win('scrollTop'); // 点击时页面滚动条卷去的高度
if (winScrollTop <= 0) return; // 如果卷去的高度为小于等于0,不开启动画
let speed = winScrollTop / time; // 计算速度
let curT = 0; // curT 记录经过的时间
if (this.timerId) {
// 如果 timerId 不是 null 就说明之前已经有动画了,在开启新的动画之前把原来的动画清除掉
clearInterval(this.timerId);
}
this.timerId = setInterval(() => {
curT += 16; // 让时间累加
if (curT >= time) { // 当到大于等于 time 时,应该滚动到底了
clearInterval(this.timerId);
curT = time;
}
let change = 16 * speed; // 在 curT 时间内走过的路程
winScrollTop -= change; // 经过 curT 时间后,页面滚动条的位置
win('scrollTop', winScrollTop); // 设置回去
}, 16);
};