【CSS Tricks】css动画详解

news2024/11/18 15:29:06

目录

  • 引言
  • 动画关键帧序列
  • 动画各属性拆解
    • 1. animation-name
    • 2. animation-duration
    • 3. animation-delay
      • 设置delay为正值
      • 设置delay为负值
    • 4. animation-direction
    • 5. animation-iteration-count
    • 6. animation-fill-mode
    • 7. animation-play-state
    • 8. animation-timing-function
      • 非阶跃函数(三次贝塞尔曲线)
      • 阶跃函数
        • 详细描述下这几个类型的特点
        • 现在还需要了解一些阶跃函数其他的细节。
  • 动画属性简写
  • 总结

引言

CSS animations 使得可以将从一个 CSS 样式配置转换到另一个 CSS 样式配置。

动画包括两个部分:描述动画的样式规则和用于指定动画开始、结束以及中间点样式的关键帧。

相较于传统的脚本实现动画技术,使用 CSS 动画有三个主要优点:

  • 能够非常容易地创建简单动画,你甚至不需要了解 JavaScript 就能创建动画。
  • 动画运行效果良好,甚至在低性能的系统上。渲染引擎会使用跳帧或者其他技术以保证动画表现尽可能的流畅。而使用 JavaScript 实现的动画通常表现不佳(除非经过很好的设计)。
  • 让浏览器控制动画序列,允许浏览器优化性能和效果,如降低位于隐藏选项卡中的动画更新频率。

动画关键帧序列

通过使用 @keyframes 建立两个或两个以上关键帧来实现。每一个关键帧都描述了动画元素在给定的时间点上应该如何渲染。

因为动画的时间设置是通过 CSS 样式定义的,关键帧使用百分比值来指定动画发生的时间点。0% 表示动画的第一时刻,100% 表示动画的最终时刻,根据动画设计的需求可以继续补充中间时刻的关键帧,例如:20%、50%、80%等。因为0%和100%时间点十分重要,所以还可以使用特殊的别名:from 和 to(使用from和to时 不可以 插入其他百分比关键帧)。若 from/0% 或 to/100% 未指定,则浏览器使用计算值开始或结束动画(例如关键帧从20%到80%,动画一开始不动,经历20%动画周期时动画启动,经历80%动画周期时动画停止,直至动画周期执行完毕)。

关键帧示例:

@keyframes blink {
  0% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}
/* 或者 */
@keyframes blink {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

动画各属性拆解

在配置完成 @keyframes 后,需要考虑到如何去播放这个动画关键帧序列。
这就涉及到了animation的子属性:

1. animation-name

必填参数,指定由 @keyframes 描述的关键帧名称。或是填写关键字 none 表示禁用动画。

该属性可以设置一个或多个关键帧名称,多个时用 , 隔开。多个关键帧会共同影响元素动画呈现最终效果,例如:向右的动画加上向下的动画表现形式为斜向右下方的动画。

关键帧命名规范:

  • 由区分大小写的字母 a 到 z、数字 0 到 9、下划线(_)和/或破折号(-)组成。
  • 第一个非破折号字符必须是一个字母。在标识符开头不能有两个破折号。
  • 标识符不能为 none、unset、initial 或 inherit。

2. animation-duration

必填参数,设置动画一个周期的时长。默认值 0s

动画完成一个周期所需的时间。可以用秒(s)或毫秒(ms)指定。值必须是正数或零,单位是必需的。

如果未提供值,则使用默认值 0s,此时动画仍会执行(会触发 animationStart 和 animationEnd 事件)。如果 animation-duration 为 0s 时,动画是否可见取决于 animation-fill-mode 的值,如下所述:

  • 如果 animation-fill-mode 设置为 backwards 或者 both,则在 animation-delay 倒计时期间将显示由 animation-direction 定义的动画的第一帧。
  • 如果 animation-fill-mode 设置为 forwards 或者 both,在 animation-delay 结束后,将显示由 animation-direction 定义的动画的最后一帧。
  • 如果 animation-fill-mode 设置为 none,动画将不会有任何的视觉效果。

3. animation-delay

非必填参数,默认值为 0s,表示动画应立即开始。

设置延时,即从元素加载完成之后到动画序列开始执行的这段时间。可以用秒(s)或毫秒(ms)指定。单位是必需的。

设置delay为正值

表示延迟播放动画,可以想象成延长了动画时长,但是延时的时间段内没有动画关键帧所以画面不动。
在这里插入图片描述

设置delay为负值

表示提前播放动画,可以想象成截取了动画时长,直接跳到延时时间后的那一个时刻的动画关键帧。
在这里插入图片描述

4. animation-direction

非必填参数,默认值为nomal,表示动画正向播放。

设置动画是应正向播放、反向播放还是在正向和反向之间交替播放。对应值为:normalreversealternatealternate-reverse

5. animation-iteration-count

非必填参数,默认值为 1 ,表示动画播放次数。

设置动画序列在停止前应播放的次数:

  • 设置整数以播放完整次数的动画周期。
  • 你可以指定非整数值以播放动画循环的一部分:例如,0.5 将播放动画循环的一半。
  • 特殊值 infinite ,表示无限播放动画。
  • 负值无效。

6. animation-fill-mode

非必选参数,默认值为 none,指定动画执行前后如何为目标元素应用样式。

在元素未赋予动画样式时,应该有一个初始的样式规则。在设置了动画之后,动画有个开始的第一帧样式,和结束最后一帧的样式。

  • 元素在执行动画后面临两个结果,回到一开始的样子,或是保留动画结束的样子。这就是设置 forwards 的结果
  • 再结合了 animation-delay 这个参数后,delay期间的准备阶段,元素应该是什么样子?同样是两个选择,一个是初始化的样子,或是动画第一帧的样子。这就是设置 backwards 的结果
  • 综合前两点,既要delay阶段的第一帧样式,又要结束后最后一帧的样式。那就设置 both

给一个示意图,以实线表示元素初始样式,虚线表示动画赋予元素的样式,主要想表达相对关系,可能动画过程会出现偏差,暂时忽略掉这个问题。能理解意思即可。

  • 当值未 none 时,正常播放完动画回到初始样式。
    在这里插入图片描述

  • 当值为 forwards 时,表示向前继续,即保持动画最后一帧的样式。
    在这里插入图片描述

  • 当值为 backwards 时,表示倒行的,并不是指动画播放完后倒行,而是指在动画播放前准备阶段的样式,要结合 animation-delay 才有意义。
    在这里插入图片描述

  • 当值为 both 时,表示两边都要。
    在这里插入图片描述

7. animation-play-state

非必选参数,默认值为 running ,设置动画是运行还是暂停。

用于暂停和恢复动画播放,一般情况下默认播放。举两个示例:

  1. 动画默认播放,但是需要操作中断。
.box {
  background-color: red;
  width: 100px;
  height: 100px;
  animation-name: move ;
  animation-duration: 2s;
  animation-iteration-count: infinite;
}

.box:hover {
  animation-play-state: paused;
}

@keyframes move {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(100px);
  }
}

  1. 动画默认不播放,需要某个指令开启播放。

还是示例1的例子,修改部分代码

.box {
  animation-play-state: paused;
}

.box:hover {
  animation-play-state: running;
}


这里的变更操作可以是示例中的hover,也可以通过js对box添加额外的样式时加入启动和暂停的属性。

8. animation-timing-function

非必选参数,默认值为 ease ,设置动画速度,即通过建立加速度曲线,设置动画在关键帧之间是如何变化。

有两种方式可以设置动画关键帧之间是缓动函数,分别时 非阶跃函数阶跃函数 。做一个比喻就是非阶跃函数像是过山丘,样式变化起伏非常连贯。阶跃函数就像跳台阶,样式变化非常生硬,某个样式保持的时长仍旧和当前关键帧所占据动画总时长的百分比有关。

非阶跃函数(三次贝塞尔曲线)

以下是一些固定的非阶跃函数关键字:

  • ease 等同于 cubic-bezier(0.25, 0.1, 0.25, 1.0),即默认值,表示动画在中间加速,在结束时减速。

  • linear 等同于 cubic-bezier(0.0, 0.0, 1.0, 1.0),表示动画以匀速运动。

  • ease-in 等同于 cubic-bezier(0.42, 0, 1.0, 1.0),表示动画一开始较慢,随着动画属性的变化逐渐加速,直至完成。

  • ease-out 等同于 cubic-bezier(0, 0, 0.58, 1.0),表示动画一开始较快,随着动画的进行逐渐减速。

  • ease-in-out 等同于 cubic-bezier(0.42, 0, 0.58, 1.0),表示动画属性一开始缓慢变化,随后加速变化,最后再次减速变化。

当然你也可以自行编写 cubic-bezier 的参数,来实现你想要的缓动效果。这样做非常复杂,需要你有很强的数学知识。所以一般情况下,以上的几种固定贝塞尔曲线关键字即可满足绝大部分开发场景。
不排除你非常想自定义贝塞尔曲线的情况,我给大家提供一个网站可以支持可视化调整贝塞尔曲线参数,实时观察动画效果。

网站地址: 贝塞尔曲线在线调试
网站介绍:
在这里插入图片描述

  • 如果你对已经调试好的曲线效果非常满意,建议点击 SAVE TO LIBRARY 将结果保存在右侧 Library 区域。
  • 不用担心刷新页面后数据丢失,数据会保存在 Localstorage 中除非你清空浏览器缓存。
  • 为了更保险的持久化数据,建议在 Library 区域导出你已经调试好的数据(json格式),之后可以通过导入重新获取数据。

阶跃函数

使用语法为:steps(n,jumpterm)

  • ** n** 阶跃次数,每个阶跃的间隔是等长的。
  • jumpterm 表示跳跃方式。
  • 当不传 jumpterm 参数时,默认为 jump-endend

其中jumpterm分为四种类型:jump-startstart(效果一样) 、jump-endend(效果一样) 、jump-nonejump-both
同时衍生出两个特定关键字:

  • step-start 表示 steps(1, jump-start) 或 steps(1, start)。
  • step-end 表示 steps(1, jump-end) 或 steps(1, end)。
详细描述下这几个类型的特点

这里以n=5为例。记住核心点:不论采取哪种跳跃方式,最终都只会跳动五次

  • jump-startstart ,表示一个左连续函数。最开始只有关键字 start 很容易造成混淆,后来新出的关键字 jump-start 其实已经变相解释了这里的跳跃过程。顾名思义就是跳过动画开始阶段。将动画总时长平均分为5份,从第一份的结尾依次阶跃到下一份的结尾。
    在这里插入图片描述
  • jump-endend,表示一个右连续函数。跟start同样的处境。最开始只有关键字 end 很容易造成混淆,后来新出的关键字 jump-end 。顾名思义就是跳过动画结尾阶段。将动画总时长平均分为5份,从第一份的开头依次阶跃到下一份的开头。
    在这里插入图片描述
  • jump-none,这里表示不跳过开头也不跳过结尾,也就是说要从动画周期的第一帧起跳,且最后一跳在动画周期的最后一帧。还记得开头说过的核心点吗?不论什么情况下,都只跳动n次。也就是说这时候就不是把动画周期分为五份了(之前分为五份是因为刚好凑够五个阶跃点)。现在首尾各占用一次,又因为每个阶跃时间间隔相同,所以现在要将动画周期平均分为四份。每一次跳跃的 步长 更长,但是时间相同。
    在这里插入图片描述
  • jump-both,这里表示首尾都跳过,动画周期的第一帧和最后一帧都不参与阶跃。为了凑够五次等长的阶跃周期,现在需要把动画周期平均分为六份。每一次跳跃的 步长 更短,但是时间相同。
    在这里插入图片描述
  • 以上就是阶跃的四种类型的特征,即跳跃方式。
现在还需要了解一些阶跃函数其他的细节。
  1. 当阶跃的 步长 没有横跨两个关键帧时,动画效果的css属性会在每一次阶跃时计算两端关键帧属性的一个过程值,并不是一定等于keyframes的某一个关键帧的属性值,例如:

    • 关键帧from中设置width为100px,to中设置width为200px,假如阶跃函数为:steps(2,start),那么元素的宽度会根据阶跃变换为150px、200px。
      在这里插入图片描述
    • 关键帧from中设置color为红色(red),to中设置color为蓝色(blue),假如阶跃函数为:steps(2,start),那么元素的颜色会根据阶跃变换为洋红(magenta)、蓝色(blue)。可能这里会有些迷惑,如果按照常规的RGB颜色模型作为参照,这个中间态的颜色确实和两端颜色没什么关联。这里采用HSL颜色模型的值作为参考:红色(hsl(0, 100%, 50%))、洋红(hsl(300, 100%, 25%))、蓝色(hsl(240, 100%, 50%))。根据色相角度的变化,(0°等于360°)确实每个阶跃减少了60°的色相角度。至于亮度为何减少我不太明白,有对这方面有研究的朋友指点一下。关于HSL颜色模型我有一篇文档进行了详细介绍,感兴趣的可以参考下:【CSS Tricks】在css中尝试一种新的颜色模型HSL
      在这里插入图片描述
  2. 当阶跃的 步长 覆盖了某个关键帧,这时候就需要提到 左连续函数右连续函数 这两个概念。我这里不对这两个关键词进行数学层面上的定义,这样会让这个知识点变得难以理解。仅从实际感官上表达,结合阶跃的 步长 覆盖了某个关键帧的场景。

    • 左连续函数(jump-startstart):元素会直接突变为下一个阶跃点附近的属性,持续时间是从上一次阶跃点或动画起点到当前阶跃点两点之间的时间。如果阶跃的中间覆盖了一个关键帧,那么元素将先变为关键帧的属性,再变为当前阶跃点附近的属性。两个属性的持续时间跟关键帧在两个阶跃点之间的百分比有关。
    • 右连续函数(jump-endend):元素会在当前阶跃点或动画起点先保持当前的属性值,持续时间是一个阶跃周期,然后突变为下一个阶跃点附近的属性值。如果阶跃中间覆盖了一个关键帧,那么元素会先保持当前阶跃点的属性,再变为关键帧的属性,两个属性的持续时间跟关键帧在两个阶跃点之间的百分比有关。
    • 到这里解释完可能还是有点迷惑,我希望可以通过我写过的实际开发例子来更加形象的解释这一个概念。参考:【VUE3.0】动手做一套像素风的前端UI组件库—Radio
      在这里插入图片描述
      这个案例是设置了阶跃函数为steps(1),keyframe的关键帧序列为:在0%时opacity为1;在50%时opacity为0,100%处可以随便设置,也可以不用设置,因为steps(1)表示steps(1,end),所以元素不会经历100%关键帧处的样式。具体的变化过程即为下图所示:
      在这里插入图片描述
      这个案例示意的是右连续函数,中间插了关键帧时,元素的属性变化过程。左连续函数中间插关键帧时,元素的属性变化过程为先保持50%阶跃周期的opacity为0,再保持50%阶跃周期的opacity为1。
      希望这个案例可以帮助你理解阶跃的 步长 覆盖了某个关键帧这一细节。

动画属性简写

在动画各属性拆解部分,我分别阐述了目前常见的动画属性的使用方法及如何理解各属性值的区别。但是实际开发中并不会每一个属性都用到,也不必分开写这么麻烦。我们一般采取简写的形式,直接使用 animation 属性,以上介绍的所有动画属性都可以按需求写在 animation 属性值中用空格隔开。如果舍弃几个属性不填,则使用该属性的默认值,参考 动画各属性拆解 介绍即可。如果一次性设置多个动画,只需要用 , 隔开每一组动画的属性。

  • name值可能是:none定义keyframe的名字,避免出现name和其他属性的关键字冲突。
  • 时间值秒(s)或毫秒(ms)指定,可能会出现0、1、2次。出现两次时间值,第一个值优先分配给 animation-duration,第二个分配给 animation-delay。如果只出现一次则直接分配给 animation-duration。如果没有时间值则表示动画不播放。
  • 其他属性根据实际需求按需填写即可。

示例:

  animation: move 2s normal infinite steps(1);

总结

css动画是赋予前端页面活力的重要属性,掌握好css动画可以使你的网站脱颖而出。希望本篇可以帮助你深入理解css动画的各类属性以及相关细节,支撑你创造出有趣的页面效果。

再接再厉~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2178874.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【2025】基于Django的鱼类科普网站(源码+文档+调试+答疑)

文章目录 一、基于Django的鱼类科普网站-项目介绍二、基于Django的鱼类科普网站-开发环境三、基于Django的鱼类科普网站-系统展示四、基于Django的鱼类科普网站-代码展示五、基于Django的鱼类科普网站-项目文档展示六、基于Django的鱼类科普网站-项目总结 大家可以帮忙点赞、收…

B3621 枚举元组

1.递归的具体过程&#xff0c;一个dfs1&#xff0c;产生3个dfs2&#xff0c;一个dfs2产生3个dfs3&#xff0c;一共输出9个&#xff08;用n2&#xff0c;k3举例&#xff09; 2.要记得使用return 结束当前递归 #include<bits/stdc.h> using namespace std; int n, k, a[10…

telnet发送邮件教程:安全配置与操作指南?

telnet发送邮件的详细步骤&#xff1f;怎么用telnet命令发邮件&#xff1f; 尽管现代邮件客户端和服务器提供了丰富的功能和安全性保障&#xff0c;但在某些特定场景下&#xff0c;了解如何使用telnet发送邮件仍然是一项有价值的技能。AokSend将详细介绍如何安全配置和操作tel…

英集芯IP5911:集成锂电池充电管理和检测唤醒功能的低功耗8位MCU芯片

英集芯IP5911是一款集成锂电池充电管理、咪头检测唤醒、负载电阻插拔和阻值检测等功能的8bit MCU芯片。其封装采用QFN16&#xff0c;应用时仅需极少的外围器件&#xff0c;就能够有效减小整体方案的尺寸&#xff0c;降低BOM成本&#xff0c;为小型电子设备提供高集成度的解决方…

QT 开发日志:QT 布局管理 —— 如何使用布局器组织 UI 元素

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

探索高效免费的PDF转Word工具,开启便捷办公之旅

无论是为了方便对文档内容进行编辑、修改&#xff0c;还是为了更好地适应不同的工作和学习场景&#xff0c;将 PDF 文档转换为可编辑的 Word 格式都具有重要意义。今天我就分享几款pdf转换成word免费版工具来解决大家的困扰。 1.Foxit PDF转换大师 链接一下>>https://w…

系统架构设计师-知识产权与标准化

目录 一、保护范围与对象 二、保护期限 三、知识产权人确定 四、侵权判断 五、标准化 一、保护范围与对象 知识产权是权利人依法就下列课题享有的专有权利&#xff1a; &#xff08;一&#xff09;作品&#xff08;著作&#xff09; &#xff08;二&#xff09;发明、实用…

通过OpenScada在ARMxy边缘计算网关上实现远程监控

随着工业互联网技术的发展&#xff0c;边缘计算逐渐成为连接物理世界与数字世界的桥梁。在众多边缘计算设备中&#xff0c;ARMxy BL340系列因其强大的性能、灵活的I/O配置及广泛的适用性&#xff0c;成为了工业控制、物联网关等领域的优选方案之一。 一、ARMxy BL340系列概述 …

波导阵列天线 馈电网络2 一种使用有着多反射零点的T型结的毫米波48%带宽高增益3D打印天线阵列

摘要&#xff1a; 一种设计毫米波宽带大规模天线阵列的创新方法被提出了&#xff0c;其使用有着多个反射零点的波导T型结来构建一个H型全公共馈网。通过联合优化反射零点的性质&#xff0c;可以减弱馈网中不期望的小反射的同相叠加&#xff0c;因此提升阵列的带宽。调研了合成有…

04 B-树

目录 常见的搜索结构B-树概念B-树的插入分析B-树的插入实现B树和B*树B-树的应用 1. 常见的搜索结构 种类数据格式时间复杂度顺序查找无要求O(N)二分查找有序O( l o g 2 N log_2N log2​N)二分搜索树无要求O(N)二叉平衡树无要求O( l o g 2 N log_2N log2​N)哈希无要求O(1) 以…

[网络]NAT、代理服务、内网穿透、内网打洞

目录 一、NAT 1.1 NAT 技术背景 1.2 NAT IP 转换过程 1.3 NAPT&#xff08;Network Address Port Translation&#xff09; 1.地址转换表 2. NAPT&#xff08;网络地址端口转换Network Address Port Translation&#xff09; 3. NAT技术的缺陷 二、代理服务器 2.1 正向…

麒麟桌面系统安装和配置Node.js

1.官网下载tar.xz文件 Node.js — 在任何地方运行 JavaScript 2.解压 可以双击直接窗口解压&#xff0c;也可以使用如下命令进行解压&#xff1a; xz -d xxx.tar.xz&#xff1b; tar -xvf xxx.tar 可以解压到usr目录或者其他目录。 3. 配置环境 解压完毕后&#xff0c…

Redis中数据类型的使用(hash和list)

&#xff08;一&#xff09;hash哈希 我们知道redis中的数据都是以键值对的方式存储的&#xff0c;key全部都是string类型&#xff0c;而value可以是不同的数据结构&#xff0c;其中就包括hash&#xff0c;也就是说&#xff0c;key这一层组织完成后到了value仍然是hash 1.Hash…

企业级移动门户的多样化选择:为数字化转型赋能

在当今数字化转型的浪潮中&#xff0c;企业级移动门户&#xff08;Enterprise Mobile Portal&#xff09;被广泛应用于企业的日常运营中。它们为企业提供了一个集中、统一的移动应用与数据访问平台&#xff0c;帮助提升工作效率、增强实时沟通并改善员工体验。随着企业对灵活性…

Qt --- 系统相关---事件、文件操作、多线程编程、网络编程、多媒体

虽然Qt是跨平台的C开发框架&#xff0c;Qt的很多能力其实是操作系统提供的。只不过Qt封装了系统API。程序是运行在操作系统上的&#xff0c;需要系统给我们提供支撑。 事件、文件操作、多线程编程、网络编程、多媒体&#xff08;音频、视频&#xff09;。 一、事件 信号槽&a…

数学期望专题

9.29 - 10.6 更新时间约持续一周 优惠券 Coupons 题目链接&#xff1a;优惠券 Coupons 假设我们某个情况下&#xff0c;我们已经有了 k 种图案&#xff0c;在这个条件下&#xff0c;获得一个新图案需要 天&#xff0c;那我们要求的就是 。由于已经有了 k 种图案&#xff0c…

【热门主题】000002 案例 JavaScript网页设计案例

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【热…

技术速递|Java on Azure Tooling 8月更新 - Java 体验在 Azure 容器应用程序正式发布

作者&#xff1a;Jialuo Gan 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎阅读 Java on Azure 开发者工具8月份更新。在本次更新中&#xff0c;我们将介绍在 IntelliJ IDEA 中 Azure Toolkit 对 Azure App Service 提供托管身份支持&#xff08;Managed Identity&#xf…

AIGC教程:如何用Stable Diffusion+ControlNet做角色设计?

前言 对于生成型AI的画图能力&#xff0c;尤其是AI画美女的能力&#xff0c;相信同行们已经有了充分的了解。然而&#xff0c;对于游戏开发者而言&#xff0c;仅仅是漂亮的二维图片实际上很难直接用于角色设计&#xff0c;因为&#xff0c;除了设计风格之外&#xff0c;角色设…

class 029 重要排序算法的总结

这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。 这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐. https://space.bilibili.com/8888480?spm_id_f…