前言
已经半个月没干活了,每天都在掘金里混日子,最近对 canvas 突然来了兴趣,看到大佬写的那种优美的效果,我也忍不住想整一个。
没想到花了我整整一个工作日的时间,才令我满意。
思路分解
我们看到这个时钟拥有边框,刻度,数字,指针四部分组成,首先指针是动态的,那必然得用 canvas 来绘制,毋庸置疑的吧;
其次边框,刻度,数字都是静止的,边框很简单,用 html + css 就能实现,刻度和数字也能用绝对定位实现,但刻度和数字那么多,得写多少 css 的绝对定位啊,那不得累成狗。因此,数字和刻度也使用 canvas 来绘制。
绘制边框
思路很明确了,创建边框使用 html + css 。
- 背景要设置成黑色,程序猿最爱的黑底,没毛病吧;
- 需要两个边框容器,一个外边框,一个内边框,使用 border-radius 等于 50% 的宽高让容器成为圆形;
- 我们注意到这里的边框是有阴影的,可以使用 box-shadow 创建一个边框阴影,外边框就用外阴影,内边框就用内阴影。 很简单吧,那看看代码。
// css
<style>
html,
body {
overflow: hidden;
}
.canvasPlay {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: black;
overflow: hidden;
}
.canvasPlayWrapper {
transform: scale(0.5, 0.5);
width: 440px;
height: 440px;
min-width: 440px;
min-height: 440px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: white;
box-shadow: 0px 0px 24px gray;
}
.canvasTarget {
box-shadow: 0px 0px 24px gray inset;
background-color: white;
border-radius: 50%;
}
</style>
// html
<div class="canvasPlay">
<div class="canvasPlayWrapper">
<canvas
id="canvasClock"
width="400px"
height="400px"
class="canvasTarget"
></canvas>
</div>
</div>
至此,我们就创建了一个好看的环形边框了,来看看效果。
创建 canvas 并定位
接下来是关键了,我们要获取到 canvas 的节点,然后在这个节点上绘制我们的图形。接下来定位到 canvas 中心点作为坐标原点,并在这个原点绘制一个实心圆,作为钟表的束缚点(这里我不知道叫啥[捂脸]),直接上代码。
/**
* @desc: 创建中心点
* @param {*}
* @return {*}
*/
const createCenterPointer = (ctx) => {
// 画中间的小圆
ctx.beginPath();
ctx.arc(0, 0, 3, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
};
const canvasTarget = document.getElementById("canvasClock");
if (canvasTarget) {
const ctx = canvasTarget.getContext("2d");
// 设置中心点,此时100,100变成了坐标的0,0
ctx.translate(100, 100);
createCenterPointer(ctx);
}
现在看看效果咋样啦~
像不像是多了一颗痣 😂!
绘制 刻度 & 数字
作为一个完美的时钟,当然要支持刻度和数字两种视觉参考啦。
绘制刻度
这里我们看到,刻度的话分成大小两种刻度,大刻度分成12份,小刻度分成60份,这样我们很容易想到,得分两次遍历绘制才行,然后用到一个很重要的偏转角度的函数 - rotate
。
小刻度宽度为1,我们使用 canvas 的画线函数便很容易实现,配合 rotate 函数偏转对应的值就好了,看看代码:
ctx.lineWidth = 1;
for (let i = 0; i < 60; i += 1) {
ctx.rotate((2 * Math.PI) / 60);
ctx.beginPath();
ctx.moveTo(90, 0);
ctx.lineTo(100, 0);
ctx.stroke();
ctx.closePath();
}
接下来绘制大刻度,会稍微难一点,因为大刻度是一个不规则形状,就像下面这个图一样
把这个形状画好之后再给它填充一下颜色,就得到了单个刻度,然后使用 rotate 函数偏转对应的值就好了,看看代码:
ctx.lineWidth = 1;
for (let i = 0; i < 12; i += 1) {
ctx.rotate((2 * Math.PI) / 12);
ctx.beginPath();
ctx.lineTo(89, 0);
ctx.lineTo(90, 2);
ctx.lineTo(100, 2);
// ctx.lineTo(100, 0);
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, (2 * Math.PI) / 250);
ctx.lineTo(100, -2);
ctx.lineTo(90, -2);
ctx.lineTo(89, 0);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
}
那么问题来了,当我们画完小刻度之后,这时候x轴已经发生了偏移,这时候肯定要让x轴恢复到原来的模样,我们才能保证接下来绘制大刻度的时候不受影响。
那么save和restore
就派上用场了,save是把ctx当前的状态打包压入栈中,restore是取出栈顶的状态并赋值给ctx
,save可多次,但是restore取状态的次数必须等于save次数
,那么看看完整的函数吧。
/**
* @desc: 绘制刻度
* @param {*}
* @return {*}
*/
const drawScale = (ctx) => {
// 保存上一次的状态
ctx.save();
// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1;
for (let i = 0; i < 60; i += 1) {
ctx.rotate((2 * Math.PI) / 60);
ctx.beginPath();
ctx.moveTo(90, 0);
ctx.lineTo(100, 0);
ctx.stroke();
ctx.closePath();
}
// 恢复成上一次保存的状态
ctx.restore();
ctx.save();
ctx.lineWidth = 1;
for (let i = 0; i < 12; i += 1) {
ctx.rotate((2 * Math.PI) / 12);
ctx.beginPath();
ctx.lineTo(89, 0);
ctx.lineTo(90, 2);
ctx.lineTo(100, 2);
// ctx.lineTo(100, 0);
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, (2 * Math.PI) / 250);
ctx.lineTo(100, -2);
ctx.lineTo(90, -2);
ctx.lineTo(89, 0);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
}
ctx.restore();
};
看看效果咋样~
绘制数字
数字怎么整上去?这里我们要用 canvas 绘制文本,就涉及到两个核心的方法。
- font - 定义字体
- fillText(text,x,y) - 在 canvas 上绘制实心的文本 这就简单了,方法如下:
- 数字时钟为 1-12,很明显用 for 循环绘制即可,遍历使用 fillText 的方法绘制,text 就用遍历 index 即可;
- x,y 的坐标怎么定位?把我们的初中知识用起来吧 - 三角函数,使用 radius * sin(偏移角度) 就得到了 x 轴的位置,使用 radius * cos(偏移角度);
- 由于数字本身具有宽高,因此,x 和 y 的位置需要在本身的基础上减去 数字宽高 / 2;
- 为了保证函数之间的状态不受影响,所以我们同样要为函数加入一对 save和restore;
话不多说,上代码
/**
* @desc: 绘制数字
* @param {*}
* @return {*}
*/
const drawScaleNumber = (ctx) => {
ctx.save();
// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1;
const textRadius = 80; // 设置文字半径为80,与边界相差20个像素
for (let i = 0; i < 12; i += 1) {
ctx.font = "16px Arial";
ctx.fillText(
i + 1,
textRadius * Math.sin((Math.PI * (i + 1)) / 6) -
(Math.ceil(i / 8) * 8) / 2,
-(textRadius * Math.cos((Math.PI * (i + 1)) / 6) - 12 / 2)
);
}
ctx.restore();
};
这块的三角函数需要自己画图才能理解,还有就是文字偏移量有时候不一定准,所以具体需要调~
还是看看效果吧~
嗯,还算可以吧!自吹一波。
绘制指针
在我的心中,指针是个六边菱形,就像下面这样
因此,方案来了。
- 使用 canvas 的 moveTo&lineTo 的画线工具,然后填充一下颜色;
- 使用 shadowColor 为指针添加阴影,这样显得有层级感;
- 获取系统时间,计算指针偏转的角度,使用 rotate 实现角度偏转;
- 由于每次绘制指针之后 x 轴都会发生偏移,所以在绘制完成之后都需要使用 save 保存状态,并使用 restore 取出上一次的状态。
很明显,计算指针角度是最复杂的,我们拿时针举例,时针总共被分成12份,那么 (2 * Math.PI) / 12) * hour
就是当前时针转的角度,当然,还得加上分针所转过的角度才能是时针整体应该转过的角度,所以应该再加上 (2 * Math.PI) / 12) * (min / 60)
,由于坐标系和我们的视野正好颠倒了 180°,所以还应该再减去 Math.PI / 2
,因此我们就得到了
- 时针转过的角度为
((2 * Math.PI) / 12) * (hour + min / 60) - Math.PI / 2
; - 同理,我们得到了分针转过的角度为
((2 * Math.PI) / 60) * (min + sec / 60) - Math.PI / 2
; - 秒针转过的角度为
((2 * Math.PI) / 60) * (sec - 1 + milliSec / 1000) - Math.PI / 2
;
看看代码吧:
/**
* @desc: 获取当前时间
* @param {*}
* @return {*}
*/
const getCurrentTime = () => {
// 获取当前时,分,秒,毫秒
const time = new Date();
const hour = time.getHours() % 12;
const min = time.getMinutes();
const sec = time.getSeconds();
const milliSec = time.getMilliseconds();
return {
hour,
min,
sec,
milliSec,
};
};
/**
* @desc: 创建指针
* @param {*}
* @return {*}
*/
const createPointer = (ctx) => {
const { hour, min, sec, milliSec } = getCurrentTime();
// 保存上一次的状态
ctx.save();
// 时针
ctx.rotate(((2 * Math.PI) / 12) * (hour + min / 60) - Math.PI / 2);
ctx.beginPath();
// moveTo设置画线起点
ctx.moveTo(-3, 0);
// lineTo设置画线经过点
ctx.lineTo(0, 3);
ctx.lineTo(45, 3);
ctx.lineTo(50, 0);
ctx.lineTo(45, -3);
ctx.lineTo(0, -3);
ctx.lineTo(-3, 0);
// 设置线宽
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.stroke();
// 创建黑色阴影,模糊级数是 4
ctx.shadowBlur = 4;
ctx.shadowColor = "black";
// 填充颜色
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
// 恢复成上一次保存的状态
ctx.restore();
ctx.save();
// 分针
ctx.rotate(((2 * Math.PI) / 60) * (min + sec / 60) - Math.PI / 2);
ctx.beginPath();
// moveTo设置画线起点
ctx.moveTo(-2, 0);
// lineTo设置画线经过点
ctx.lineTo(0, 2);
ctx.lineTo(52, 2);
ctx.lineTo(60, 0);
ctx.lineTo(52, -2);
ctx.lineTo(0, -2);
ctx.lineTo(-2, 0);
ctx.lineWidth = 1;
ctx.strokeStyle = "#1e80ff";
ctx.stroke();
// 创建蓝色阴影,模糊级数是 3
ctx.shadowBlur = 3;
ctx.shadowColor = "#1e80ff";
// 填充颜色
ctx.fillStyle = "#1e80ff";
ctx.fill();
ctx.closePath();
ctx.restore();
ctx.save();
// 秒针
ctx.rotate(
((2 * Math.PI) / 60) * (sec - 1 + milliSec / 1000) - Math.PI / 2
);
ctx.beginPath();
// moveTo设置画线起点
ctx.moveTo(-1, 0);
ctx.lineTo(0, 1);
ctx.lineTo(60, 1);
ctx.lineTo(70, 0);
ctx.lineTo(60, -1);
ctx.lineTo(0, -1);
ctx.lineTo(-1, 0);
ctx.strokeStyle = "#e9686b";
ctx.stroke();
// 创建红色阴影,模糊级数是 2
ctx.shadowBlur = 2;
ctx.shadowColor = "#e9686b";
// 填充颜色
ctx.fillStyle = "#e9686b";
ctx.fill();
ctx.closePath();
ctx.restore();
};
绘制完成之后,我们的静态时钟也就完成了,看看效果~
渐变边框
最开始我们使用了 css 的 box-shadow 绘制了边框,但我们也可以使用 canvas 的渐变属性绘制一个边框,canvas 渐变可以填充在矩形, 圆形, 线条, 文本等等, 各种形状可以自己定义不同的颜色。
以下有两种不同的方式来设置Canvas渐变:
- createLinearGradient(x,y,x1,y1) - 创建线条渐变
- createRadialGradient(x,y,r,x1,y1,r1) - 创建一个径向/圆渐变
到这里很明显,我们能使用径向/圆渐变实现阴影的效果,上代码:
/**
* @desc: 创建渐变内环
* @param {*}
* @return {*}
*/
const createRadialGradient = (ctx) => {
// 创建渐变
const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, 100);
grd.addColorStop(0, "white");
grd.addColorStop(0.92, "white");
grd.addColorStop(0.99, "#BDBDBD");
grd.addColorStop(1, "#D8D8D8");
// 填充渐变
ctx.fillStyle = grd;
ctx.arc(0, 0, 100, 0, 2 * Math.PI);
ctx.fill();
};
经过对比发现,这样的边框效果并没有 css 的 box-shadow 美观,因此我们依然选择 css 来绘制边框。
让指针运动起来
当我们把时钟都绘制完成了,最后一步就是让它动起来,也就是要更新视图,需要注意的是,每次更新视图的时候都要把上一次的画布清除,再开始画新的视图,不然就会出现千手观音的景象, 更新视图我们可以使用两种方案:
-
- 定时器
setInterval
,以固定频率刷新视图;
- 定时器
-
- 使用window.requestAnimationFrame,以浏览器内置频率刷新视图;
用过这两种方案的小伙伴应该很清楚,setInterval 并不能和浏览器刷新频率统一,更新频率过快造成浪费,更新过慢会导致视觉卡顿,呈现效果并不好,因此我们选择 window.requestAnimationFrame 来更新视图。 代码如下:
// 渲染函数
const animloop = () => {
drawClock(ctx, canvasTarget);
// console.log('canvas');
requestAnimationFrame(animloop);
};
animloop();
这样我们的时钟就动起来了,配上一张 gif 来表示一下效果吧。
防止视图放大之后模糊
用 canvas 绘图有个很明显的缺点就是 canvas 被放大之后,图像会变得模糊起来,这是由于设备独立像素和物理像素不一致,设备像素比=物理像素/设备独立像素,大部分设备像素比为2,这意味着100px的图像要放在200px中才可以正常显示,因此,我们需要
- 先把 canvas 图像放大两倍,用
ctx.scale(2, 2)
便可以实现; - 然后将canvas 外部容器缩小两倍,用 css 的
transform: scale(0.5, 0.5)
便可以实现。 看看代码修改吧。
// css
.canvasPlayWrapper {
+ transform: scale(0.5, 0.5);
width: 440px;
height: 440px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: white;
box-shadow: 0px 0px 24px gray;
}
// js
/**
* @desc: 创建时钟
* @param {*}
* @return {*}
*/
const createClock = () => {
const canvasTarget = document.getElementById("canvasClock");
if (canvasTarget) {
const ctx = canvasTarget.getContext("2d");
// 放大两倍,容器再缩小两倍,防止放大的时候模糊
+ ctx.scale(2, 2);
if (ctx) {
// 渲染函数
const animloop = () => {
drawClock(ctx, canvasTarget);
// console.log('canvas');
requestAnimationFrame(animloop);
};
animloop();
}
}
};
至此,我们的时钟就完成啦,并且放大还不会模糊。
最终代码
最后附上一下最终代码,拿到浏览器里就能用~
<!--
* @Descripttion: canvas 时钟
* @version: 1.0.0
* @Author: jiaxiantao
* @Date: 2021-08-20 14:12:03
* @LastEditors: jiaxiantao
* @LastEditTime: 2021-08-23 16:29:26
-->
<!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" />
<title>这是一个时钟</title>
<style>
html,
body {
overflow: hidden;
}
.canvasPlay {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: black;
overflow: hidden;
}
.canvasPlayWrapper {
transform: scale(0.5, 0.5);
width: 440px;
height: 440px;
min-width: 440px;
min-height: 440px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: white;
box-shadow: 0px 0px 24px gray;
}
.canvasTarget {
box-shadow: 0px 0px 24px gray inset;
background-color: white;
border-radius: 50%;
}
</style>
</head>
<body>
<div class="canvasPlay">
<div class="canvasPlayWrapper">
<canvas
id="canvasClock"
width="400px"
height="400px"
class="canvasTarget"
></canvas>
</div>
</div>
<script>
/**
* @desc: 获取当前时间
* @param {*}
* @return {*}
*/
const getCurrentTime = () => {
// 获取当前时,分,秒,毫秒
const time = new Date();
const hour = time.getHours() % 12;
const min = time.getMinutes();
const sec = time.getSeconds();
const milliSec = time.getMilliseconds();
return {
hour,
min,
sec,
milliSec,
};
};
/**
* @desc: 创建中心点
* @param {*}
* @return {*}
*/
const createCenterPointer = (ctx) => {
// 画中间的小圆
ctx.beginPath();
ctx.arc(0, 0, 3, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
};
/**
* @desc: 创建渐变内环
* @param {*}
* @return {*}
*/
const createRadialGradient = (ctx) => {
// 创建渐变
const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, 100);
grd.addColorStop(0, "white");
grd.addColorStop(0.92, "white");
grd.addColorStop(0.99, "#BDBDBD");
grd.addColorStop(1, "#D8D8D8");
// 填充渐变
ctx.fillStyle = grd;
ctx.arc(0, 0, 100, 0, 2 * Math.PI);
ctx.fill();
};
/**
* @desc: 绘制刻度
* @param {*}
* @return {*}
*/
const drawScale = (ctx) => {
// 保存上一次的状态
ctx.save();
// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1;
for (let i = 0; i < 60; i += 1) {
ctx.rotate((2 * Math.PI) / 60);
ctx.beginPath();
ctx.moveTo(90, 0);
ctx.lineTo(100, 0);
ctx.stroke();
ctx.closePath();
}
// 恢复成上一次保存的状态
ctx.restore();
ctx.save();
ctx.lineWidth = 1;
for (let i = 0; i < 12; i += 1) {
ctx.rotate((2 * Math.PI) / 12);
ctx.beginPath();
ctx.lineTo(89, 0);
ctx.lineTo(90, 2);
ctx.lineTo(100, 2);
// ctx.lineTo(100, 0);
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, (2 * Math.PI) / 250);
ctx.lineTo(100, -2);
ctx.lineTo(90, -2);
ctx.lineTo(89, 0);
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
}
ctx.restore();
};
/**
* @desc: 绘制数字
* @param {*}
* @return {*}
*/
const drawScaleNumber = (ctx) => {
ctx.save();
// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1;
const textRadius = 80; // 设置文字半径为80,与边界相差20个像素
for (let i = 0; i < 12; i += 1) {
ctx.font = "16px Arial";
ctx.fillText(
i + 1,
textRadius * Math.sin((Math.PI * (i + 1)) / 6) -
(Math.ceil(i / 8) * 8) / 2,
-(textRadius * Math.cos((Math.PI * (i + 1)) / 6) - 12 / 2)
);
}
ctx.restore();
};
/**
* @desc: 创建指针
* @param {*}
* @return {*}
*/
const createPointer = (ctx) => {
const { hour, min, sec, milliSec } = getCurrentTime();
// 保存上一次的状态
ctx.save();
// 时针
ctx.rotate(((2 * Math.PI) / 12) * (hour + min / 60) - Math.PI / 2);
ctx.beginPath();
// moveTo设置画线起点
ctx.moveTo(-3, 0);
// lineTo设置画线经过点
ctx.lineTo(0, 3);
ctx.lineTo(45, 3);
ctx.lineTo(50, 0);
ctx.lineTo(45, -3);
ctx.lineTo(0, -3);
ctx.lineTo(-3, 0);
// 设置线宽
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.stroke();
// 创建黑色阴影,模糊级数是 4
ctx.shadowBlur = 4;
ctx.shadowColor = "black";
// 填充颜色
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
// 恢复成上一次保存的状态
ctx.restore();
ctx.save();
// 分针
ctx.rotate(((2 * Math.PI) / 60) * (min + sec / 60) - Math.PI / 2);
ctx.beginPath();
// moveTo设置画线起点
ctx.moveTo(-2, 0);
// lineTo设置画线经过点
ctx.lineTo(0, 2);
ctx.lineTo(52, 2);
ctx.lineTo(60, 0);
ctx.lineTo(52, -2);
ctx.lineTo(0, -2);
ctx.lineTo(-2, 0);
ctx.lineWidth = 1;
ctx.strokeStyle = "#1e80ff";
ctx.stroke();
// 创建蓝色阴影,模糊级数是 3
ctx.shadowBlur = 3;
ctx.shadowColor = "#1e80ff";
// 填充颜色
ctx.fillStyle = "#1e80ff";
ctx.fill();
ctx.closePath();
ctx.restore();
ctx.save();
// 秒针
ctx.rotate(
((2 * Math.PI) / 60) * (sec - 1 + milliSec / 1000) - Math.PI / 2
);
ctx.beginPath();
// moveTo设置画线起点
ctx.moveTo(-1, 0);
ctx.lineTo(0, 1);
ctx.lineTo(60, 1);
ctx.lineTo(70, 0);
ctx.lineTo(60, -1);
ctx.lineTo(0, -1);
ctx.lineTo(-1, 0);
ctx.strokeStyle = "#e9686b";
ctx.stroke();
// 创建红色阴影,模糊级数是 2
ctx.shadowBlur = 2;
ctx.shadowColor = "#e9686b";
// 填充颜色
ctx.fillStyle = "#e9686b";
ctx.fill();
ctx.closePath();
ctx.restore();
};
/**
* @desc: 绘制时钟
* @param {*} ctx
* @return {*}
*/
const drawClock = (ctx, target) => {
if (ctx) {
ctx.save();
// 保存清除状态
ctx.clearRect(0, 0, 200, 200);
// 设置中心点,此时100,100变成了坐标的0,0
ctx.translate(100, 100);
// 创建中心点
createCenterPointer(ctx);
// 创建渐变内环
// createRadialGradient(ctx);
// 绘制刻度
drawScale(ctx);
// 绘制数字
drawScaleNumber(ctx);
// 创建指针
createPointer(ctx);
ctx.restore();
}
};
/**
* @desc: 创建时钟
* @param {*}
* @return {*}
*/
const createClock = () => {
const canvasTarget = document.getElementById("canvasClock");
if (canvasTarget) {
const ctx = canvasTarget.getContext("2d");
// 放大两倍,容器再缩小两倍,防止放大的时候模糊
ctx.scale(2, 2);
if (ctx) {
// 渲染函数
const animloop = () => {
drawClock(ctx, canvasTarget);
// console.log('canvas');
requestAnimationFrame(animloop);
};
animloop();
}
}
};
createClock();
</script>
</body>
</html>
这是我在掘金里的第一篇水文,喜欢的小伙伴请点个赞哦~