我的博客一直没有加载动画,每次打开网页内容都是直接蹦出来,总觉得不太美观。为了彰显我的技术力厨力,我就想自己动手,做一个旋转的红白阴阳图来当加载动画。

这图看上去挺简单的:一个完整的圆,由两条对称的阴阳鱼组成,每条鱼里还有个相反颜色的小圆点。
可光是怎么实现,我就想了了两天。

一开始我异想天开,打算用四个伪元素(::before、::after)来拼出这个图形。结果在CSS里调来调去,各种对不齐,左边矫正了,右边就不知道偏到哪里去了。
​硬着头皮试了两个小时,只能放弃。

然后就是到处查资料,参考别人的加载动画——说来也怪,我愣是没找到一个用太极图当加载动画的例子。
最后,我想到可以用三层结构来搭建这个复杂的图形。


第一层:基础圆形 + 左右分色

1
2
3
4
5
6
.bagua {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(to right, #fff 50%, #e74c3c 50%);
}
  • 使用 border-radius: 50% 创建完美圆形
  • 使用 linear-gradient 实现左右分色(左白右红)

第二层:上下半圆创建阴阳鱼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.bagua::before {
/* 上半圆 - 红色 */
top: 0;
left: 50%;
width: 50%;
height: 50%;
background: #e74c3c;
}

.bagua::after {
/* 下半圆 - 白色 */
bottom: 0;
left: 50%;
width: 50%;
height: 50%;
background: #fff;
}
  • 使用 ::before::after 伪元素创建两个半圆

  • 分别定位在上下位置

  • 颜色与所在区域的背景色相反,形成阴阳鱼形状

第三层:鱼眼(小圆点)

1
2
3
4
5
6
7
8
9
10
11
.bagua-inner::before {
/* 上方鱼眼 - 白色 */
top: 25px;
background: #fff;
}

.bagua-inner::after {
/* 下方鱼眼 - 红色 */
bottom: 25px;
background: #e74c3c;
}
  • 使用额外的容器元素(bagua-inner)来承载鱼眼
  • 两个小圆点放在对应鱼形的中心
  • 颜色与所在鱼形相反

关键技巧

1.相对定位计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 所有尺寸都基于父元素百分比,确保缩放一致性 */
.bagua::before {
width: 50%; /* 半圆宽度为父元素一半 */
height: 50%; /* 半圆高度为父元素一半 */
left: 25%; /* 左偏移25%,使其居中 */
top: 0; /* 贴顶部 */
}

/* 鱼眼的位置也使用相对计算 */
.bagua-inner::before {
width: 16.67%; /* 20px/120px ≈ 16.67% */
height: 16.67%;
left: 41.67%; /* 50px/120px ≈ 41.67% */
top: 16.67%; /* 20px/120px ≈ 16.67% */
}
  • 使用百分比确保在不同尺寸下保持比例
  • 精确计算位置,使图形对称美观

2.旋转动画

1
2
3
4
5
6
7
8
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.bagua {
animation: rotate 3s linear infinite;
}
  • 对整个太极图应用旋转动画
  • 线性动画确保匀速旋转
  • 无限循环创建持续动感

CSS代码总算写出来了,效果看着挺满意。
​可我的博客用的是Butterfly主题,样式文件是.styl格式,纯CSS不能直接往里扔。于是又花了几个小时,吭哧吭哧地重新移植了一遍。

没想到,等我把加载动画部署好的时候,发现它一闪就没了,根本起不到遮丑的作用。
检查了好几遍fullpage-loading.pug,代码明明是对的,命令行也没报错。缓存清了一次又一次,还是老样子。
没辙了,只能开始漫长的排查。查了两天,什么也没发现。

实在没办法,我又回去翻店长的旧教程(新版本已经不适配了)。看了一半,突然看到了夜间背景切换的教程,突然悟了,我把背景魔改了。
原版的Butterfly没法检测到我那个自定义背景是否加载完成,所以只要md文档一加载好,它就认为页面准备好了,加载动画也就立刻结束了。

都折腾到这地步了,不继续那我写的加载动画不白瞎了吗?只好又硬着头皮,从背景加载的逻辑开始重新啃。
说起来都怪自己,当初图省事,魔改背景的时候完全是照抄,一行注释都没加,现在看自己的代码像看天书一样。
偷懒果然没好下场

又花了几天时间,一边理解一边加注释,等到终于调通的时候,注释也差不多写满了,整个主题文件的大小估计涨了四分之一。

今天总算全部搞定了。我把核心的CSS样式贴在下面,万一有朋友能用得上呢。


完整代码

fullpage-loading.pug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#loading-box
.loading-left-bg
.loading-right-bg
.spinner-box
.bagua
.bagua-inner
.loading-word 少女祈祷中...

script.
(()=>{
const $loadingBox = document.getElementById('loading-box')
const $body = document.body
const $spinnerBox = document.querySelector('.spinner-box')

const preloader = {
endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = ''
$loadingBox.classList.add('loaded')
},
initLoading: () => {
$body.style.overflow = 'hidden'
$loadingBox.classList.remove('loaded')
// 移除淡入效果,为下次加载做准备
if ($spinnerBox) {
$spinnerBox.classList.remove('fade-in')
}
},
fadeInContent: () => {
// 添加淡入效果
if ($spinnerBox && !$spinnerBox.classList.contains('fade-in')) {
$spinnerBox.classList.add('fade-in')
}
}
}

preloader.initLoading()

// 函数:检测当前body的背景图是否加载完成
function checkBackgroundImage() {
return new Promise((resolve) => {
// 直接获取当前body的背景图
const computedStyle = window.getComputedStyle(document.body)
const bgImage = computedStyle.backgroundImage

// 提取URL
const urlMatch = bgImage.match(/url\(['"]?(.*?)['"]?\)/)

// 如果没有背景图,直接返回
if (!urlMatch || !urlMatch[1]) {
console.log('没有检测到背景图,跳过加载检测')
resolve(false)
return
}

const bgUrl = urlMatch[1]
console.log('检测到背景图URL:', bgUrl)

// 创建Image对象预加载背景图
const img = new Image()
img.onload = () => {
console.log('背景图加载完成')
resolve(true)
}
img.onerror = () => {
console.log('背景图加载失败')
resolve(false)
}
img.src = bgUrl

if (img.complete) {
console.log('背景图已缓存')
resolve(true)
}
})
}

// 主加载逻辑 - 确保至少显示3秒
async function startLoading() {
// 记录开始时间
const startTime = Date.now()
const MINIMUM_DISPLAY_TIME = 3000 // 最小显示时间:3秒

// 先触发淡入动画(延迟500ms让黑幕先显示)
setTimeout(() => {
preloader.fadeInContent()
}, 500)

// 等待DOM加载完成
if (document.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve)
})
}

console.log('DOM加载完成,开始检测背景图')

// 等待当前背景图加载
const bgLoadResult = await checkBackgroundImage()

// 计算已用时间
const elapsedTime = Date.now() - startTime
const remainingTime = MINIMUM_DISPLAY_TIME - elapsedTime

console.log(`背景图检测${bgLoadResult ? '成功' : '失败'},已用时 ${elapsedTime}ms`)

// 如果已经超过3秒,立即结束;否则等待剩余时间
if (remainingTime > 0) {
console.log(`需要等待额外 ${remainingTime}ms 达到3秒最小显示时间`)
await new Promise(resolve => setTimeout(resolve, remainingTime))
} else {
console.log('已超过3秒最小显示时间,立即结束')
}

// 结束加载
preloader.endLoading()
}

// 启动加载检测
startLoading().catch((error) => {
console.error('加载检测出错:', error)
// 如果出错,至少显示2秒
setTimeout(preloader.endLoading, 2000)
})

// 超时保护:8秒后强制结束
setTimeout(preloader.endLoading, 8000)

// PJAX支持
if (!{theme.pjax && theme.pjax.enable}) {
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
btf.addGlobalFn('pjaxComplete', async () => {
// 每次PJAX完成后重新触发淡入效果
setTimeout(() => {
preloader.fadeInContent()
}, 300)

// 然后重新检测背景图并至少显示3秒
const startTime = Date.now()
const MINIMUM_DISPLAY_TIME = 3000

try {
const bgLoadResult = await checkBackgroundImage()

const elapsedTime = Date.now() - startTime
const remainingTime = MINIMUM_DISPLAY_TIME - elapsedTime

console.log(`PJAX背景图检测${bgLoadResult ? '成功' : '失败'},已用时 ${elapsedTime}ms`)

if (remainingTime > 0) {
setTimeout(preloader.endLoading, remainingTime)
} else {
setTimeout(preloader.endLoading, 300)
}
} catch (error) {
console.error('PJAX背景图检测出错:', error)
setTimeout(preloader.endLoading, 300)
}
}, 'preloader_end')
}
})()

loading.styl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
if hexo-config('preloader.enable') && hexo-config('preloader.source') == 1
.loading-bg
position: fixed
z-index: 1000
width: 50%
height: 100%
background-color: #000

#loading-box
.loading-left-bg
@extend .loading-bg

.loading-right-bg
@extend .loading-bg
right: 0

.spinner-box
position: fixed
z-index: 1001
display: flex
flex-direction: column
justify-content: center
align-items: center
width: 100%
height: 100vh

/* 初始状态:透明 */
opacity: 0
transition: opacity 0.5s ease-out 0.1s

/* 红白八卦图 */
.bagua
width: 120px
height: 120px
position: relative
border-radius: 50%
background: linear-gradient(to right, #fff 50%, #e74c3c 50%)
animation: rotate 3s linear infinite
box-shadow: 0 0 20px rgba(231, 76, 60, 0.7)
margin-bottom: 40px

.bagua::before,
.bagua::after
content: ''
position: absolute
border-radius: 50%

.bagua::before
width: 60px
height: 60px
top: 0
left: 30px
background: #e74c3c

.bagua::after
width: 60px
height: 60px
bottom: 0
left: 30px
background: #fff

.bagua-inner::before,
.bagua-inner::after
content: ''
position: absolute
border-radius: 50%
z-index: 1

.bagua-inner::before
width: 20px
height: 20px
top: 20px
left: 50px
background: #fff

.bagua-inner::after
width: 20px
height: 20px
bottom: 20px
left: 50px
background: #e74c3c

/* 文字样式 */
.loading-word
position: relative
color: #fff
font-size: 18px
font-weight: bold
text-shadow:
0 0 5px #e74c3c,
0 0 10px #e74c3c,
0 0 15px #e74c3c
letter-spacing: 3px
text-align: center
margin-top: 20px
animation: text-glow 2s ease-in-out infinite alternate

/* 淡入后的状态 */
&.fade-in
opacity: 1

&.loaded
.loading-left-bg
transition: all 1s
transform: translate(-100%, 0)

.loading-right-bg
transition: all 1s
transform: translate(100%, 0)

.spinner-box
opacity: 0
visibility: hidden
transition: opacity 0.5s ease, visibility 0.5s ease

@keyframes rotate
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)

@keyframes text-glow
0%
text-shadow:
0 0 5px #e74c3c,
0 0 10px #e74c3c,
0 0 15px #e74c3c
opacity: 0.8
100%
text-shadow:
0 0 10px #e74c3c,
0 0 20px #e74c3c,
0 0 30px #e74c3c
opacity: 1