先来看看b站的一键三连是什么效果:

2022-08-09 13.51.36.gif

不难观察出以下几个特点:

  1. 长按点赞出现抖动动画
  2. 长按点赞时关联按钮会有圆环进度条效果
  3. 长按超过一段时间后放开则一次实现三个动作并且有个绽放特效

接下来我们要做的就是逐步实现这些步骤,如何开始呢?这就需要介绍今天的主角:谷歌扩展插件。

创建一个Chrome插件

Chrome插件是一个用Web技术开发、用来增强浏览器功能的软件,它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包。可以通过 chrome-plugin-demo 这个项目了解更多,这里我们直接讲如何使用:

首先在谷歌浏览器直接打开地址 chrome://extensions/ 进入扩展程序,并打开右上角开发者模式,这时就可以加载我们的插件了:

image.png

展程序会以 manifest.json 这个文件来识别并加载插件:

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
{
"manifest_version": 2,
"name": "掘金一键三连小助手",
"version": "1.0",
"description": "通过Chrome插件实现的一键三连效果,长按点赞3秒即可点赞+收藏+关注",
"author": "ShawnPhang",
"icons": {
"48": "icon.png",
"128": "icon.png"
},
"page_action": {
"default_icon": "icon.png",
"default_title": "我是pageAction",
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["https://juejin.cn/*"],
"js": ["mojs.js", "inject.js"],
"css": ["like.css"]
}
],
"background": {
"scripts": ["background.js"]
},
"web_accessible_resources": []
}

加载后效果 状态栏效果
image.png image.png

在这个json文件中最主要看 content_scripts 这段配置,它表示了插件会向网页注入的JS文件和CSS文件,前面说了谷歌插件既是由一系列网页文件构成的,那么接下来就可以正式开始我们的效果实现了~

长按抖动

这是最容易实现的一个效果了,这里我定义了一个 shaking 的类,重复执行一段css动画,主要就是利用 translate 属性重新定位元素,就可以做到抖动的效果,下面的css也是非常随意写的,效果还可以。虽然只是上下左右位移却使用了 translate3d,是为了触发css的3d加速性能会更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.article-suspended-panel > .shaking {
animation: shake 350ms linear;
animation-iteration-count: infinite;
animation-direction: reverse;
}

@keyframes shake {
10%, 90% { transform: translate3d(-1px, 0, 0); }
15%, 85% { transform: translate3d(0, -1px, 0); }
20%, 80% { transform: translate3d(+2px, 0, 0); }
25%, 75% { transform: translate3d(0, +2px, 0); }
30%, 70% { transform: translate3d(-2.5px, 0, 0); }
35%, 65% { transform: translate3d(0, -2.5px, 0); }
2.50%, 60% { transform: translate3d(+2.5px, 0, 0); }
2.55%, 55% { transform: translate3d(0, +2.5px, 0); }
50% { transform: translate3d(-2.5px, -2.5px, 0); }
}

接下来在 inject.js 中只需要捕获文章页面点赞按钮,为其添加 mousedown 的监听事件,在点下鼠标的时候动态添加上 shaking 这个class,动画就开始执行了:

1
2
3
4
5
6
7
8
const likeBtn = document.querySelector('.article-suspended-panel .panel-btn')

likeBtn?.addEventListener('mousedown', () => {
if (likeBtn.className.includes('active')) {
return
}
likeBtn.classList.add('shaking')
})

2022-08-09 22.12.14.gif

圆环进度条

这个效果开始有点难度了,需要分两个Div来画,我们都知道一个完整的圆是这样:

1
2
3
4
5
6
7
.circle {
width: 4rem;
height: 4rem;
border: 2px solid red;
border-radius: 50%;
box-sizing: border-box;
}

image.png

这时我们先把红色边改为透明,然后只显示其中两条,并旋转一个角度,这就得到了半圆效果:

1
2
3
4
5
6
7
.circle {
....
border: 2px solid transparent;
border-top: 2px solid #1e80ff;
border-right: 2px solid #1e80ff;
transform: rotate(-135deg);
}

image.png

现在以这个半圆我们先来绘制右半部分的圆环,在这个 circle 元素父级添加一个外层元素,使结构如下:

1
2
3
<div class="wrapper right">
<div class="circle"></div>
</div>

外层的 wrapper 高度和圆环高度一致,宽度则为一半,绝对定位到右边,此时效果是这样的:

1
2
3
4
5
6
7
.wrapper {
width: 2rem;
height: 4rem;
position: absolute;
right: 0;
background: yellow;
}

image.png

这时我们先写个css动画让 circle 转起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
.circle .rightcircle {
right: 0;
animation: circle 3s linear infinite;
}
@keyframes circle {
0% {
transform: rotate(-135deg);
}
50%,
100% {
transform: rotate(45deg);
}
}

2022-08-09 22.42.10.gif

此时黄色区域为圆环的父级元素,如果我们将该区域视为可视区,那么只需要设置溢出隐藏:

1
2
3
4
.wrapper {
.....
overflow: hidden;
}

效果就出来了:

2022-08-09 22.42.36.gif

同样的方法绘制左半圆,叠加在一块就形成了环形进度条动画,核心在于两个Div的动画执行时间是一致的,也就是完整跑完一个360°的旋转周期,只不过各自都有一半被遮住,从而形成了最终效果,下面有请码上掘金为我们演示完整代码:

代码片段

回到我们刚刚插件中,在点赞按钮点下的事件中我们需要批量添加上面这段DOM到相应的操作按钮中,然后绝对定位在左上角(0,0)处即可,查看网页源代码可知按钮宽高为 4rem,颜色我们则取全局变量中的蓝色 var(--juejin-brand-1-normal),将相关CSS写到 like.css 文件中后,JS中定义一个创建DOM的函数:

1
2
3
4
5
6
7
8
9
10
11
function createCircle() {
const fragment = document.createElement('div')
fragment.classList.add('circle_process')
fragment.innerHTML = `<div class="wrapper right">
<div class="circle rightcircle"></div>
</div>
<div class="wrapper left">
<div class="circle leftcircle"></div>
</div>`
return fragment
}

因为这段结构还是有点多的,就不一一使用Element片段去创建了,创建完最外层的Div之后直接使用innerHtml写入两个半圆的结构,函数返回这个DOM片段。

下面就是在点击事件中处理相关动作,用时间戳判断长按时间是否持续3秒,用一个数组存储好创建的片段,鼠标抬起时循环调用其.remove()方法来移除DOM,b站的效果当然更加复杂,在这里并不是直接终止动画而是逆向运行动画,由于我们是使用css实现的,就折腾不了这种细节了,直接上代码:

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
let doms = []
let timeStamp = 0

const likeBtn = document.querySelector('.article-suspended-panel .panel-btn')
likeBtn?.addEventListener('mousedown', () => {
if (likeBtn.className.includes('active')) {
return
}
timeStamp = new Date().getTime() / 1000
likeBtn.classList.add('shaking')
doms.push(createCircle(), createCircle())
document.getElementsByClassName('panel-btn')[0].appendChild(doms[0])
document.getElementsByClassName('panel-btn')[2].appendChild(doms[1])
})
likeBtn?.addEventListener('mouseup', () => {
likeBtn.click()
const now = new Date().getTime() / 1000
const pass = now - timeStamp > 2.9
likeBtn.classList.remove('shaking') // 移除震动
for (const iterator of doms) {
// 移除圆环
iterator.remove()
}
if (likeBtn.className.includes('active')) {
return
}
if (pass) {
// TODO: follow and collect btn action
}
})

2022-08-09 23.11.58.gif

粒子绽放效果

这个效果用css实现难度更高了,刚好我今天在掘金看了一篇文章,介绍了一个轻量级动画库 mojs,里面的爆裂(Burst)效果就很适合这个场景,于是把umd文件下载来引入页面中。

inject.js 中定义一个函数执行来动画:

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
function play(parent, cb) {
new window.mojs.Burst({
// 爆裂范围 {从多大 : 到多大}
radius: { 0: 50 },
// 动画挂载的父元素, 如果不填默认挂载到 <body>
parent,
// 动画延迟的贝塞尔曲线函数
easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
// 动画延迟时间
duration: 1500,
// 在动画动之前等待的时间 (这里一般设置150ms方便减少低端机型可能会存在的卡顿)
delay: 300,
// 扩散的粒子配置
children: {
duration: 750,
// 粒子大小变换 {从多大 : 到多大}
// rand(from, to) rand函数可以帮我们随机出一个区间的值
radius: { 0: 'rand(5, 25)' },
// 形状选择, 这里我们选择了 “圆形”
shape: 'circle',
// 粒子可选的填充色
fill: ['#1abc9c', '#2ecc71', '#00cec9', '#3498db', '#9b59b6', '#fdcb6e', '#f1c40f', '#e67e22', '#e74c3c', '#e84393'],
},
// 透明度
opacity: 0.6,
// 生成的粒子数量
count: 12,
onStart() {
// 动画触发前的钩子函数
},
onComplete() {
// 动画完成后的钩子函数
cb && cb()
},
}).play()
}

接着在 mouseup 事件中调用:

1
2
3
4
5
6
7
8
9
..........
likeBtn?.addEventListener('mouseup', () => {
.............
if (pass) {
play(likeBtn, () => { // 这里只有两个按钮所以直接用回调函数来排列动画了
play(document.getElementsByClassName('panel-btn')[2])
})
}
})

完整效果展示(后面长按时间调整到2s):

2022-08-10 17.22.52.gif

插件完整代码地址:chromePlugin-juejin-oneThree