把一块 1.28 寸的圆形屏幕接到 ESP32 上,然后让动漫里的写轮眼在上面转起来——这活儿听起来挺中二,但做完你就会发现,它几乎覆盖了嵌入式图形开发的所有基础技能:SPI 屏幕驱动适配、几何绘图、动画主循环、帧率控制。项目本身不复杂,用的是常见的 GC9D01/GC9A01 240x240 圆屏模组和一块 30 引脚的 ESP32 DevKit,工具链就是 Arduino IDE,从零开始五步就能跑出带虹膜渐变和三勾玉旋转的写轮眼动画。
这篇文章我会把每一步的“为什么”也讲清楚:为什么圆屏要用 GC9A01 驱动、为什么驱动库初始化那几行代码决定你屏幕能不能亮、勾玉的逗号形状是怎么用几个圆叠出来的、动画转起来之后又该怎么调帧率和避免闪烁。哪怕你之前只点过 OLED,也完全可以照着做。
1. 项目整体思路:圆屏、写轮眼和动画的三重适配
1.1 为什么选 ESP32 + GC9A01 这个组合
先说主控。ESP32 在这个项目里其实属于“杀鸡用牛刀”,一块 STM32 或者 RP2040 也能干,但 ESP32 有它不可替代的优势:Arduino 生态成熟,库和例程多,出问题好搜;双核 240MHz 的算力让逐帧重绘毫无压力,哪怕像我这样把整屏反复 fillCircle 也跑得动;而且带 WiFi 和蓝牙,做完动画之后想加个手机遥控、配网校时都是现成的,不用换板子。
再说屏幕。市面上 1.28 寸、240x240 的圆屏模组,绝大多数用的都是 GC9A01 驱动 IC,一小部分标的是 GC9D01。这个型号差异我在模块丝印和商家描述里都见过,实际使用中寄存器兼容,驱动库基本都是按 GC9A01 初始化,点亮效果没有区别。圆屏这种东西,正面看是完整的多边形裁剪出来的圆形显示区,边缘没有方屏那种“边框”,非常适合做表盘、仪表、眼睛这类圆形视觉元素。写轮眼本身就是圆的,用圆屏算是最对味的载体。
1.2 写轮眼动画的“静态+动态”拆解
动画不能一上来就写代码。我习惯先把画面拆成静态构图和动态节奏两部分。静态构图决定“像不像”,动态节奏决定“活不活”。
写轮眼的静态特征很清晰:红色虹膜、黑色瞳孔、围绕瞳孔均匀分布的三个黑色勾玉,勾玉的头部朝外、尾巴顺着旋转方向弯曲。这个构图在 240x240 的圆屏上,只需要圆心坐标、半径参数和三个角度就能完全描述。动态部分我做了三件事:勾玉绕瞳孔旋转、开眼瞬间虹膜从暗红渐亮、转起来之后保持稳定速度。旋转用角度变量每帧累加,渐变用颜色值插值,脉动用 sin 函数驱动参数,都是嵌入式动画最基础的手法。
| 动画元素 | 实现方式 | 需要掌握的要点 |
|---|---|---|
| 红色虹膜 | fillCircle | 圆心、半径、RGB565 颜色 |
| 黑色瞳孔与外环 | fillCircle + drawCircle | 图层覆盖顺序 |
| 三勾玉 | 多个 fillCircle 叠加 | 坐标旋转公式 |
| 旋转动画 | 角度变量逐帧累加 | 弧度制、帧率控制 |
| 开眼渐变 | 颜色值线性插值 | RGB565 红色通道 |
| 脉动呼吸 | sin() 驱动参数微调 | 时间基准 |
这套拆解思路可以迁移到任何动画项目:先定构图,再定动作,最后才是写代码。
2. 硬件准备与接线:先避开供电和引脚的两个大坑
2.1 器件清单
这个项目需要的硬件很少,绝大多数人手上就有:
- ESP32 DevKit 开发板一块,常见 WROOM-32 模组、30 引脚版本就行;
- GC9A01/GC9D01 1.28 寸圆屏模块一块,240x240,带 SPI 接口;
- 杜邦线若干,最好用母对母,长度别超过 10cm;
- 面包板一块,方便临时改线;
- 供电直接用电脑 USB 也行,但别用劣质 USB 集线器。
关于屏幕选型多说一句:别光看标题和图片,下单前确认驱动 IC 是 GC9A01 或者兼容型号,否则买回来可能要用别的初始化序列。有些卖家会把 GC9A01 圆屏和 ST7789 圆屏混着卖,收到货以后先看 FPC 软排线上的丝印,再找卖家要一份初始化代码,能省去后面排查驱动的时间。
2.2 接线图和供电注意
ESP32 DevKit 默认的硬件 SPI 是 VSPI:SCK 对应 GPIO18,MOSI 对应 GPIO23。为了让代码里能用默认 SPI,我按下面的表接线:
| 屏幕引脚 | 接到 ESP32 | 说明 |
|---|---|---|
| VCC | 3.3V | 模组逻辑供电,别接 5V |
| GND | GND | 共地必须接 |
| SCL / SCK | GPIO18 | SPI 时钟 |
| SDA / MOSI | GPIO23 | SPI 数据输出 |
| RES / RST | GPIO4 | 复位,低有效 |
| DC | GPIO2 | 数据/命令选择 |
| CS | GPIO5 | 片选,低有效 |
| BLK / BL | 3.3V | 背光,也可接 GPIO 做 PWM 调光 |
这里有两个最常见的坑。第一个是供电:圆屏模块的逻辑部分几乎都是 3.3V,只有极少数带稳压芯片的模块允许从 5V 引脚供电。如果你不确定,用万用表量一下模块上的稳压芯片型号,或者干脆一律接 3.3V,背光也接 3.3V。第二个坑是 CS 和 DC 千万别接反,接反以后屏幕大概率只有背光亮、没有任何内容输出,而且很难定位,因为代码看起来完全没问题。
接线顺序我建议屏幕先不接,先把 ESP32 单独用 USB 线连电脑,确认串口能识别、能烧录,再接屏幕。这样一旦没点亮,能快速排除是不是屏幕接线的问题。
3. 驱动适配指南:GC9A01/GC9D01 点亮的核心逻辑
3.1 先分清驱动 IC:GC9A01、GC9D01 与 ST7789
圆屏的驱动适配,核心是“初始化序列要对”。驱动 IC 上电后不会自己告诉主机它是什么型号,需要主机按照它的寄存器手册发一串初始化命令。GC9A01 和 ST7789 虽然都是 240x240 级别的 SPI 屏,但初始化命令和地址偏移都不一样,用错驱动就会花屏或者显示区域偏移。
GC9D01 和 GC9A01 的关系我在前面提过,寄存器基本兼容,Arduino 的库里没有单独的 GC9D01 条目,统一按 GC9A01 处理即可。如果你手里的模块丝印写的是 GC9D01,直接选 GC9A01 驱动试,十有八九能亮。
怎么确认驱动 IC?三个办法:看 FPC 丝印,丝印上通常会直接标;看卖家的资料页,正规一点的店铺会给出初始化代码;实在不行就分别烧 GC9A01 和 ST7789 的初始化代码试,看哪个能全屏正常显示。这个“试错法”在驱动适配里是最实用的。
3.2 Adafruit 路线:代码最短,适合入门
我最推荐新手用 Adafruit 系列库,因为封装好、出错概率低。打开 Arduino 库管理器,搜索并安装两个库:Adafruit GFX Library 和 Adafruit GC9A01A。
点亮代码非常简单:
#include <SPI.h> #include <Adafruit_GFX.h> #include <Adafruit_GC9A01A.h> #define TFT_CS 5 #define TFT_DC 2 #define TFT_RST 4 Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RST); void setup() { tft.begin(); // 上电初始化,走默认 VSPI tft.setRotation(0); tft.fillScreen(0xF800); // 全屏红色,验证显示 } void loop() {}这段代码如果烧进去屏幕全红,说明驱动适配已经完成。Adafruit 库在 ESP32 上默认使用硬件 SPI,也就是 GPIO18 和 GPIO23,所以构造函数只需要传 CS、DC、RST 三个引脚。如果你想把 SPI 速率调高减少重绘时间,可以改成tft.begin(40000000),40MHz 是这个芯片常用的极限值;如果出现花屏就降到 26MHz 或者 20MHz。这个速率参数在屏大、动画复杂时会明显影响帧率,也是后面优化动画的关键。
3.3 TFT_eSPI 路线:更快,但不细心就踩坑
如果你想追求更高的刷新速度,或者想用 Sprite(帧缓冲)做无闪烁动画,TFT_eSPI 是更合适的选择。这个库对 GC9A01 有专门支持,关键就在 User_Setup.h 这个文件里。
安装 TFT_eSPI 之后,进入libraries/TFT_eSPI/User_Setup.h,把下面这些行改好:
#define GC9A01_DRIVER #define TFT_MOSI 23 #define TFT_SCLK 18 #define TFT_CS 5 #define TFT_DC 2 #define TFT_RST 4 #define SPI_FREQUENCY 40000000TFT_eSPI 会自动把 GC9A01 识别为 240x240 圆屏,不需要手动设宽高。这里要注意两件事:一是这个文件是仓库自带的,如果你用库管理器安装,改了之后下次更新库会被覆盖,最好备份一份;二是如果画面方向不对,用tft.setRotation(1)之类的调用调整,不要改 TFT_WIDTH/TFT_HEIGHT,圆屏这两个值固定是 240。
TFT_eSPI 的 API 和 Adafruit 略有不同,但画圆的基本函数名字一致:tft.fillCircle(cx, cy, r, color)。后面动画代码里的绘图函数,在两个库之间切换几乎不用改,只有初始化部分不一样。我自己是这么选:调试用 Adafruit,正式做动画产品用 TFT_eSPI + Sprite。
3.4 点亮后的三色自检
点亮之后别急着画写轮眼,先做一个三色自检。把 fillScreen 里的颜色分别改成纯红0xF800、纯绿0x07E0、纯蓝0x001F,每 2 秒切换一次。这一步能同时验证三件事:屏幕是否全屏正常显示、RGB 顺序是否正确、是否有显示区域偏移。
如果红蓝互换,说明屏幕是 BGR 像素顺序。Adafruit 库可以通过给 GC9A01 发 MADCTL 命令切换位序,比如tft.sendCommand(0x36, 0x08)和tft.sendCommand(0x36, 0x00)来回切;TFT_eSPI 则直接改#define TFT_RGB_ORDER TFT_BGR,编译一次就能看出区别。如果只有中间一块有内容、四周黑边或者内容串位,那大概率是驱动 IC 选错了,回到 3.1 用试错法重新确认。
4. 五步跑通:从环境搭建到写轮眼转起来
4.1 Step 1:环境与接线
这部分其实在第 2 章已经完成了大半。具体再收个尾:Arduino IDE 里装 ESP32 开发板支持包,在“开发板管理器”搜索 esp32 安装“esp32 by Espressif Systems”,然后选择开发板“ESP32 Dev Module”。
装完之后先烧一个最简单的点灯程序,确认开发环境和串口都没问题。很多人一上来就烧动画,结果开发板支持包没装好,或者串口号选错,浪费半小时还以为是屏幕的问题。这一步 5 分钟就能做完,别跳过。
4.2 Step 2:驱动点亮
按第 3.2 节的代码烧录,确认屏幕能全屏显示颜色。在这个阶段把白屏、偏色、花屏的问题全部处理掉,后面进入绘图阶段会非常顺畅。如果用的是 TFT_eSPI,只需要把 3.3 节的头文件配置好,再写两行tft.init(); tft.fillScreen(TFT_RED);就能验证。
这里我想多说一句:驱动点亮是整个项目里最容易让人劝退的一步,因为它对硬件细节非常敏感。遇到问题不要急着怀疑代码,先量电压、再查接线、再看波形。没有示波器就把 SPI 频率降到 1MHz 慢速跑,很多时候花屏会变成正常画面,那就说明是速率问题而不是接线问题。
4.3 Step 3:静态构图,先把写轮眼“画”出来
现在进入核心绘图部分。屏幕 240x240,圆心设在 (120, 120)。虹膜半径 100,瞳孔半径 20,勾玉轨道半径 48,勾玉头部半径 13。这些参数我调过好几轮,最终组合在圆屏上视觉最平衡:虹膜离屏幕边缘留了 20 像素的黑边,勾玉到瞳孔和外圈都有足够间距。
勾玉是整个绘制里最需要解释的部分。勾玉不是圆,也不是椭圆,而是一个“圆头带尾巴”的逗号。用像素级别的贝塞尔曲线去画当然可以,但 240x240 的屏幕上根本不需要那么精细,我用三个圆叠加再加一个背景色圆“挖槽”,就能得到非常像样的勾玉。
具体逻辑是:在轨道上取一个角度 angle,算出勾玉头部圆心 (hx, hy);头部画一个大圆;沿切线方向偏移 0.72 倍半径的位置画一个中圆,再偏移 1.18 倍半径的位置画一个小圆,这三个圆组合出“头粗尾细”的感觉;最后在头部朝向屏幕中心的方向,用一个背景色圆挖掉一块,凹槽就出来了,整体轮廓接近逗号。你可以在纸上画一下这四个圆的位置关系,会比我用文字描述更直观。
转 120 度重复三次,三个勾玉就均匀分布在瞳孔周围:
void drawTomoe(float angle, uint16_t bg) { int16_t hx = CX + (int16_t)(ORBIT_R * cos(angle)); int16_t hy = CY + (int16_t)(ORBIT_R * sin(angle)); float tangent = angle + PI / 2; tft.fillCircle(hx, hy, TOMOE_S, BLACK); tft.fillCircle(hx + (int16_t)(TOMOE_S * 0.72 * cos(tangent)), hy + (int16_t)(TOMOE_S * 0.72 * sin(tangent)), (int16_t)(TOMOE_S * 0.52), BLACK); tft.fillCircle(hx + (int16_t)(TOMOE_S * 1.18 * cos(tangent)), hy + (int16_t)(TOMOE_S * 1.18 * sin(tangent)), (int16_t)(TOMOE_S * 0.28), BLACK); tft.fillCircle(hx + (int16_t)(TOMOE_S * 0.55 * cos(angle + PI)), hy + (int16_t)(TOMOE_S * 0.55 * sin(angle + PI)), (int16_t)(TOMOE_S * 0.52), bg); }上面代码里 bg 是当前虹膜颜色。挖槽的圆必须用背景色而不是固定的红色,这样后面做虹膜渐变时,勾玉的凹槽才不会穿帮。
完整的静态绘制函数就是把虹膜、瞳孔、勾玉按顺序叠上去:
void drawSharingan(float baseAngle, uint16_t bg) { tft.fillScreen(BLACK); tft.fillCircle(CX, CY, IRIS_R, bg); tft.drawCircle(CX, CY, IRIS_R, BLACK); for (int i = 0; i < 3; i++) { drawTomoe(baseAngle + i * (TWO_PI / 3), bg); } tft.drawCircle(CX, CY, PUPIL_R + 2, BLACK); tft.drawCircle(CX, CY, PUPIL_R + 1, BLACK); tft.fillCircle(CX, CY, PUPIL_R, BLACK); tft.fillCircle(CX + 7, CY - 9, 3, WHITE); // 高光点,可选 }绘制顺序就是图层顺序:先铺红底,再画黑边和瞳孔,最后画勾玉和高光。顺序反了,后面的图形会盖住前面的内容。
4.4 Step 4:旋转动画,让勾玉动起来
静态图能显示,动画就只差一个“角度变量”。把三个勾玉的公共角度 baseAngle 放在 loop 里每帧递增,每一帧都重新调用 drawSharingan,勾玉就会绕瞳孔旋转。角度是弧度制,0 到 2π 一圈,三个勾玉的相对角度分别是 0、2π/3、4π/3。
帧率控制上,我在每帧末尾加delay(30),也就是大约 33fps。这个刷新率对动画来说足够流畅,而且给 ESP32 留了余量,不会因为持续满速重绘导致看门狗复位。如果你的动画内容更复杂,可以试试把 delay 缩短到 20ms、同时把 SPI 频率拉到 40MHz;如果出现撕裂或闪烁,就回到延时和频率的组合上重新调。
这里有个经验之谈:动画主循环里尽量少调用fillScreen以外的耗时操作,更不要放Serial.print在每帧里。串口打印在 115200 波特率下虽然快,但每帧多那么几毫秒,帧率从 30fps 掉到 24fps 你是能肉眼察觉出来的。
4.5 Step 5:开眼特效、脉动和最终整合
第五步是锦上添花,但也是动画“活起来”的关键。我做了一个开眼效果:上电前 2 秒,虹膜颜色从暗红线性变到纯红,同时勾玉转得慢一点,开眼完成后再提速。实现方式是用一个 redLv 变量在前几十帧里递增,再用一个函数把 0~255 的红色强度转成 RGB565。
RGB565 的红色通道占 5 bit,直接拿 redLv 左移 11 位就行。注意这里要做redLv >> 3再左移,因为 8 bit 的红色值要先变成 5 bit。如果不做这个转换,颜色会偏绿甚至发紫,这是新手画图最容易踩的颜色坑之一。
最终整合代码如下:
#include <SPI.h> #include <Adafruit_GFX.h> #include <Adafruit_GC9A01A.h> #define TFT_CS 5 #define TFT_DC 2 #define TFT_RST 4 Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RST); #define BLACK 0x0000 #define WHITE 0xFFFF const int16_t CX = 120; const int16_t CY = 120; const int16_t IRIS_R = 100; const int16_t PUPIL_R = 20; const int16_t ORBIT_R = 48; const int16_t TOMOE_S = 13; uint16_t red565(uint8_t r) { return (uint16_t)((r >> 3) << 11); } void drawTomoe(float angle, uint16_t bg) { int16_t hx = CX + (int16_t)(ORBIT_R * cos(angle)); int16_t hy = CY + (int16_t)(ORBIT_R * sin(angle)); float tangent = angle + PI / 2; tft.fillCircle(hx, hy, TOMOE_S, BLACK); tft.fillCircle(hx + (int16_t)(TOMOE_S * 0.72 * cos(tangent)), hy + (int16_t)(TOMOE_S * 0.72 * sin(tangent)), (int16_t)(TOMOE_S * 0.52), BLACK); tft.fillCircle(hx + (int16_t)(TOMOE_S * 1.18 * cos(tangent)), hy + (int16_t)(TOMOE_S * 1.18 * sin(tangent)), (int16_t)(TOMOE_S * 0.28), BLACK); tft.fillCircle(hx + (int16_t)(TOMOE_S * 0.55 * cos(angle + PI)), hy + (int16_t)(TOMOE_S * 0.55 * sin(angle + PI)), (int16_t)(TOMOE_S * 0.52), bg); } void drawSharingan(float baseAngle, uint16_t bg) { tft.fillScreen(BLACK); tft.fillCircle(CX, CY, IRIS_R, bg); tft.drawCircle(CX, CY, IRIS_R, BLACK); for (int i = 0; i < 3; i++) { drawTomoe(baseAngle + i * (TWO_PI / 3), bg); } tft.drawCircle(CX, CY, PUPIL_R + 2, BLACK); tft.drawCircle(CX, CY, PUPIL_R + 1, BLACK); tft.fillCircle(CX, CY, PUPIL_R, BLACK); tft.fillCircle(CX + 7, CY - 9, 3, WHITE); } void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(0); tft.fillScreen(BLACK); delay(100); Serial.println("GC9A01 OK"); } void loop() { static float baseAngle = 0.0f; static uint8_t redLv = 40; static bool activated = false; static uint32_t lastMs = 0; if (!activated && millis() - lastMs > 30) { lastMs = millis(); redLv += 8; if (redLv >= 255) { redLv = 255; activated = true; } } drawSharingan(baseAngle, red565(redLv)); baseAngle += activated ? 0.045f : 0.015f; if (baseAngle > TWO_PI) baseAngle -= TWO_PI; delay(30); }如果想加脉动效果,把 ORBIT_R 改成ORBIT_R * (1.0f + 0.03f * sin(millis() / 200.0f)),再更新勾玉和瞳孔的大小关系,整个眼睛会有轻微“呼吸感”。脉动的幅度别超过 5%,否则勾玉会碰到瞳孔或者外圈,看起来像破功。
5. 常见问题速查:白屏、花屏、偏色和闪烁
5.1 白屏或黑屏,背光亮但没内容
这是最常遇到的问题。排查顺序我建议是:先确认电源——用万用表量屏幕 VCC 是不是 3.3V,背光 BLK 是否拉到高电平;再确认复位——RES 引脚不能浮空,必须接到 GPIO 或者直接接 3.3V 上拉,浮空的 RES 可能导致屏幕一直卡在复位状态;最后确认 DC 和 CS 有没有接反,这两个引脚接反后通常就是“背光亮、内容无”的典型表现。
5.2 花屏或只显示一部分
花屏大概率是驱动 IC 选错,GC9A01 的初始化发给了 ST7789 芯片,或者反过来。可以先试 3.1 的试错法。如果型号没问题还是花,那就把 SPI 频率降下来试试。劣质长杜邦线在 26MHz 以上会出现信号反射,我在面包板上用 10cm 以上的母对母杜邦线跑到 40MHz 就偶尔花屏,换成粗短的线或者直接焊接后就很稳定。
5.3 颜色发蓝发绿或者红蓝互换
这和驱动 IC 无关,是像素 RGB/BGR 顺序不对。GC9A01 通过 MADCTL 寄存器的 bit3 控制 RGB/BGR 交换,Adafruit 库可以用tft.sendCommand(0x36, 0x08)和tft.sendCommand(0x36, 0x00)切换;如果用 TFT_eSPI,直接在 User_Setup.h 里加#define TFT_RGB_ORDER TFT_BGR。另外,如果你用pushImage或 Sprite 推整幅图像,还要在 Adafruit 里调用tft.setSwapBytes(true),否则图像的字节序是反的。
5.4 动画卡顿、闪烁
动画卡顿先看两处:SPI 频率是否已经拉高,以及每帧是否有多余的耗时操作。如果用的是 TFT_eSPI,可以用 Sprite 把整帧画在内存里再一次推送到屏幕,消除“边画边显示”造成的撕裂感。一个 240x240 的 RGB565 帧缓冲是 115200 字节,ESP32 的 320KB SRAM 放得下,但会显著挤压堆空间,所以 Sprite 更适合配合 PSRAM 的模组使用。
如果你在这个项目里开了 WiFi 或者蓝牙,动画偶尔会卡一下。SPI 和射频共存时,无线中断的优先级更高,会抢占传输带宽。动画循环里如果不需要联网,就别初始化无线功能,这也是我推荐先做纯动画再考虑联网功能的原因。
5.5 烧录失败或反复重启
烧录失败先看端口和开发板型号,确保选的“ESP32 Dev Module”和实际模组一致。部分 DevKit 需要按住 BOOT 键再点烧录,烧完按一下 EN 键复位。反复重启大概率是供电不足,USB 集线器供电不稳定时,ESP32 启动瞬间电流大,屏幕背光再一拉,压降到 3.0V 以下就会触发低电压复位。解决办法是换直连电脑的 USB 口,或者单独给屏幕供电,别让背光和主控抢同一路电。
6. 进阶扩展:写轮眼只是开始
6.1 从写轮眼到万花筒:换几何图案
写轮眼只是这套绘图方法的一个案例。万花筒写轮眼的图案本质上是“旋转对称 + 特定几何形”,把三勾玉换成多边形、扇形、圆弧组合,就能演化出各种款式。圆屏在这个场景下特别占便宜,因为圆心的极坐标计算直接对应屏幕的圆形边界,你不需要做任何边界裁剪。
6.2 从动画到完整应用:加交互与联网
动画能跑之后,可以往前走几步:加一个按钮切换眼睛形态;用 ESP32 的无线功能获取时间,让圆屏变成一个动漫风表盘;或者接个麦克风,让勾玉随着音量脉动。ESP32 最大的优势就在这——动画、交互、联网是同一块芯片里的事情,不用换平台。
6.3 几条实操心得
最后分享几条我做完这个项目后的体会。第一,调参数时把代码里的常量单独拎出来定义,半径、速度、颜色全部做成宏或 const,改起来不用满文件找数字;第二,动画开发一定要做版本存档,我调脉动幅度时把 0.03 改成 0.08,结果勾玉直接和瞳孔重叠,幸好存了上一版能很快回退;第三,别迷信高帧率,30fps 对这类视觉动画已经足够,把 CPU 留给扩展功能更划算。
个人而言,我最喜欢这个项目的地方是它把“画图”这件事从 PC 搬到了嵌入式屏幕上,而且难度曲线非常友好。你学会的不只是让一个眼睛转起来,而是一整套“如何在资源受限的屏幕上做动画”的思考方式。下次再看到别人做的圆屏小项目,你也能一眼看出它的构图参数和动画逻辑。