持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情
本篇文章我们会实现一个图片加载特效,特效的效果是让图片从模糊的状态随着加载进度的推进,逐渐变得清晰,效果图如下:
如果让你实现这样的特效,你会如何实现呢?先别往下看,先思考一下甚至可以动手尝试一下,再接着看下去看看我和你的思路有什么不同吧,欢迎把你的思路在评论区留言分享出来
1. 思考实现思路
- 首先我们肯定要有一个背景元素,然后还要有一个进度文本元素
- 给背景图片设置一个模糊度,并且这个模糊度应当随着进度的推进而不断变得清晰,这是从效果图中能够明显看出来的
- 还有文字也应该设置一下透明度,需要随着进度的推进而不断从不透明变成透明,也就是
opacity
要从1
变成0
主要就是做到以上三点,很显然,核心就在于如何将图片加载进度和背景图片模糊度建立一个映射关系,比如图片加载进度为0
的时候,背景图片的模糊度应当为30px
,而当图片加载进度推进,这个模糊度应当逐步下降,最终下降至0
还需要建立图片加载进度和加载文字不透明度之间的映射,也就是说当加载进度为0
的时候,加载文字的不透明度应当为1
,随着图片加载进度推进,这个不透明都逐渐增大,直到加载进度为100%
时,让加载文字的不透明度变为0
,也就是不再显示
2. HTML 结构
明确了以上的思路,我们首先来实现最简单的HTML
结构,代码如下:
<div class="background"></div>
<p class="loading-text">0%</p>
没错,就只需要简单的一个div
作为背景元素,一个p
作为加载文本元素即可
3. CSS 样式
3.1 加载图片居中显示
为了让加载文字水平垂直居中显示,将body
设置为flex
布局,并且主轴和交叉轴方向上的对齐方式都是center
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 0;
}
3.2 背景图片样式
为了让背景图片不遮挡文字的显示,我们应当用绝对定位让其脱离文档流,并且z-index
设置为-1
使其不要遮挡加载文本的显示
其次,为了让图片能够居中显示,使用background-position
的center
属性
同时为了防止图片比例被破坏,我们可以使用background-size
的cover
属性
以上属性可以简写到background
中,即background: url('xxx') center center/cover
.background {
position: absolute;
width: 100vw;
height: 100vh;
z-index: -1;
/* background 中写 center/cover 等价于 background-size */
/* background-size: cover; */
background: url('https://unsplash.com/photos/QMwkGYFDjiE/download?ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjU2NTExNTgw&force=true')
no-repeat center center/cover;
filter: blur(30px);
}
此时的效果如下: 很明显可以看到四周是有白边的,这个问题该怎么解决呢?
3.3 解决 blur 带来的白边问题
由于blur
模糊后会让原来的背景颜色,也就是这里的body
的#fff
白色背景显示出来,所以这个“白边”就是从body
的白色背景来的,既然知道问题的所在,那么就好解决了
由于我们设置刚开始的模糊半径为30px
,所以我们可以让背景图片绝对定位在top: -30px; left: -30px
的位置,将左上角区域的白边遮盖住,但是这样就导致右下角的白边变得更加大了,本来就有30px
的模糊半径带来的白边,现在又有top
和left
偏移30px
带来的白边,一共就是60px
的白边了
所以我们还需要将宽高在原来的基础上加上60px
去抵消这个白边的范围,不过这样一来就会导致背景图片溢出整个body
容器元素的宽高,导致出现水平和垂直的滚动条,所以我们还应该给body
加上overflow: hidden
的设定
综上,我们修改一下CSS
样式:
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 0;
+ overflow: hidden;
}
.background {
position: absolute;
+ inset: -30px auto auto -30px;
- width: 100vw;
- height: 100vh;
+ width: calc(100vw + 60px);
+ height: calc(100vh + 60px);
z-index: -1;
/* background 中写 center/cover 等价于 background-size */
/* background-size: cover; */
background: url("https://unsplash.com/photos/QMwkGYFDjiE/download?ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjU2NTExNTgw&force=true")
no-repeat center center/cover;
filter: blur(30px);
}
现在模糊效果带来的白边就消失了
4. JS 建立加载进度和模糊半径、加载文字透明度之间的映射
根据前面的思路分析,我们已经知道我们应当建立两个映射:
- 加载进度与模糊半径之间的映射:当加载进度从
0 ~ 100
变化的时候,模糊半径也应当相应地在30 ~ 0
之间变化 - 加载进度与加载文字不透明度之间的映射:当加载进度从
0 ~ 100
变化的时候,加载文字的不透明度应当从1 ~ 0
之间变化
为了建立这个映射关系,我们可以实现一个辅助函数mapNumberRange
,将一个数值从某一个范围的输入映射到另一个范围的输出
比如我将加载进度变量progress
输入,这个变量的范围是从0 ~ 100
,然后我希望它的输出是从30 ~ 0
,比如调用:
mapNumberRange(progress, {
srcStart: 0,
srcEnd: 100,
destStart: 30,
destEnd: 0,
})
那么当progress
为0
的时候,我希望得到的是一个30
,而当progress
为1
的时候,我希望得到的是29
以此类推,你可以思考一下mapNumberRange
应当怎么实现,这里我直接给出实现,感兴趣的读者可以自己推导一下
/**
* @description 将数值按照一定范围进行映射
* @param {number} num 待映射的数
* @param {{srcStart: number, srcEnd: number, destStart: number, destEnd: number}} options
*/
const mapNumberRange = (num, options) => {
const { srcStart, srcEnd, destStart, destEnd } = options
return (
((num - srcStart) * (destEnd - destStart)) / (srcEnd - srcStart) + destStart
)
}
由于该函数不涉及外部变量的修改,不会产生副作用,所以它也算是一个纯函数
5. 使用 setInterval 模拟加载进度
这里为了简单起见,直接使用setInterval
进行模拟,使用定时器修改加载进度变量,并且让图片背景模糊半径和加载文字的不透明度跟随加载进度变量的变化而变化
5.1 获取目标元素
这个很容易,由于我们的html
结构很简单,所以获取目标元素就是背景和加载文字
/** @type HTMLDivElement */
const oBackground = document.querySelector('.background')
/** @type HTMLParagraphElement */
const oLoadingText = document.querySelector('.loading-text')
为了能够获得方便的类型提示,我使用了jsdoc
的类型注释(别杠为啥不直接用ts
,因为这是原生js
练手专题)
5.2 实现图片加载特效函数
实现一个名为blurAnimation
的函数用于实现加载特效
const blurAnimation = () => {
// 加载进度
let progress = 0
let timer = setInterval(() => {
progress++
if (progress === 100) {
// 加载完毕则清除定时器
clearInterval(timer)
}
// 设置加载文字的数值
oLoadingText.innerText = `${progress}%`
// 设置加载文字的不透明度 -- 随加载进度变化
oLoadingText.style.opacity = mapNumberRange(progress, {
srcStart: 0,
srcEnd: 100,
destStart: 1,
destEnd: 0,
})
// 设置背景图片的模糊半径 -- 随加载进度变化
oBackground.style.filter = `blur(${mapNumberRange(progress, {
srcStart: 0,
srcEnd: 100,
destStart: 30,
destEnd: 0,
})}px)`
}, 30)
}
每次定时器中的回调函数执行时,都会让进度加1
,并且每次修改进度后,都会动态修改:
- 加载文字的数值
- 加载文字的不透明度,借助前面实现的
mapNumberRange
辅助纯函数实现 - 背景图片的模糊半径,借助前面实现的
mapNumberRange
辅助纯函数实现
直到加载进度到达100
,就清除定时器,完成图片加载过程,至此整个从模糊到清晰的图片加载特效就实现完成啦,效果还是有点酷炫的吧!
6. 完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
<title>blury-loading</title>
</head>
<body>
<div class="background"></div>
<p class="loading-text">0%</p>
<script src="index.js"></script>
</body>
</html>
css
* {
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
.background {
position: absolute;
inset: -30px auto auto -30px;
width: calc(100vw + 60px);
height: calc(100vh + 60px);
z-index: -1;
/* background 中写 center/cover 等价于 background-size */
/* background-size: cover; */
background: url('https://unsplash.com/photos/QMwkGYFDjiE/download?ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjU2NTExNTgw&force=true')
no-repeat center center/cover;
filter: blur(30px);
}
.loading-text {
font-size: 128px;
color: #fff;
}
js
;(() => {
/** @type HTMLDivElement */
const oBackground = document.querySelector('.background')
/** @type HTMLParagraphElement */
const oLoadingText = document.querySelector('.loading-text')
/**
* @description 将数值按照一定范围进行映射
* @param {number} num 待映射的数
* @param {{srcStart: number, srcEnd: number, destStart: number, destEnd: number}} options
*/
const mapNumberRange = (num, options) => {
const { srcStart, srcEnd, destStart, destEnd } = options
return (
((num - srcStart) * (destEnd - destStart)) / (srcEnd - srcStart) +
destStart
)
}
const blurAnimation = () => {
// 加载进度
let progress = 0
let timer = setInterval(() => {
progress++
if (progress === 100) {
// 加载完毕则清除定时器
clearInterval(timer)
}
// 设置加载文字的数值
oLoadingText.innerText = `${progress}%`
// 设置加载文字的不透明度 -- 随加载进度变化
oLoadingText.style.opacity = mapNumberRange(progress, {
srcStart: 0,
srcEnd: 100,
destStart: 1,
destEnd: 0,
})
// 设置背景图片的模糊半径 -- 随加载进度变化
oBackground.style.filter = `blur(${mapNumberRange(progress, {
srcStart: 0,
srcEnd: 100,
destStart: 30,
destEnd: 0,
})}px)`
}, 30)
}
const init = () => {
blurAnimation()
}
init()
})()