尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LVGL烟花效果实战:用Canvas与定时器实现高性能粒子动画

LVGL烟花效果实战:用Canvas与定时器实现高性能粒子动画 做嵌入式 GUI 项目时我一度以为“LVGL 放烟火效果”就是准备两张烟花 PNG再叠一层透明度动画。真正在开发板上调起来才发现要做出流畅、自然、还能自由控制节奏的粒子动画核心不是素材而是理解 LVGL 的 Canvas 绘制、刷新机制和定时器驱动。这篇文章从一个可运行的 LVGL 烟花 Demo 入手完整拆解粒子系统设计、绘制流程、完整 C 代码、常见坑点和性能优化方案。无论你是在 STM32、ESP32 上做节日主题 UI还是在 PC 上用模拟器学习 LVGL都可以直接参考。1. LVGL 烟花效果是什么用 Canvas 手写粒子动画1.1 烟花效果的应用场景“放烟火”并不是一个单纯的视觉 Demo它在实际项目里很常见开机动画设备上电时先播放烟花营造氛围后再进入主界面节日主题春节、元宵、跨年等主题 UI 中把按钮点击反馈做成烟花功能演示向客户演示屏幕刷新率和图形渲染能力时烟花属于视觉效果突出又不复杂的效果游戏元素小游戏中的得分反馈、升级特效、道具爆炸效果。在这些场景里开发者需要的不是一张静态图片而是一套能够随机生成、实时运动、最终消失的粒子系统。LVGL 本身提供了丰富的控件和动画 API但烟花效果更偏向“逐像素级”的实时绘图因此需要用到 LVGL 的lv_canvas控件。1.2 技术选型为什么选择 lv_canvas lv_timerLVGL 官方提供了一套动画框架lv_anim它可以很方便地让控件的位置、大小、透明度、角度等属性连续变化。但烟花的粒子数量非常多每个粒子都有独立的运动轨迹、颜色、生命周期如果给每个粒子创建一个lv_obj或者lv_anim资源开销会非常大而且不适合统一管理。所以更合理的做法是用一个lv_canvas作为绘制目标用一个lv_timer作为每帧刷新驱动在定时器回调里自行更新粒子状态再把粒子绘制到 Canvas 上。canvas 的本质是一块内存 bufferLVGL 会把它当作普通图片对象渲染到屏幕上。我们可以直接往 buffer 里写入像素这个特性非常适合做粒子动画、波形显示、简单小游戏等逐像素更新场景。1.3 烟花效果的物理模型一个看起来自然的烟花可以拆成三个阶段发射阶段一个小粒子火箭从屏幕底部向上升空可以轻微左右摆动爆炸阶段火箭到达目标高度后消失同时产生几十个粒子向四周飞散消散阶段爆炸粒子受重力影响下落亮度逐渐减弱直到消失。这三个阶段对应一个简单的状态机。整个烟花效果就是一个状态集合粒子数组、粒子状态、位置、速度、加速度、颜色、生命周期。2. LVGL 动画与 Canvas 绘图底层原理2.1 LVGL 官方动画 lv_anim 的基本用法先看一个最简单的lv_anim示例让一个对象在 1 秒内从 x0 移动到 x100。/* 创建一个普通对象 */ lv_obj_t * obj lv_obj_create(lv_scr_act()); lv_obj_set_size(obj, 50, 50); /* 初始化动画对象 */ lv_anim_t anim; lv_anim_init(anim); lv_anim_set_var(anim, obj); lv_anim_set_exec_cb(anim, (lv_anim_exec_xcb_t)lv_obj_set_x); lv_anim_set_values(anim, 0, 100); lv_anim_set_time(anim, 1000); lv_anim_set_path_cb(anim, lv_anim_path_ease_out); lv_anim_start(anim);这段代码的核心逻辑是lv_anim_set_var指定动画操作的对象lv_anim_set_exec_cb指定动画每帧要修改对象的哪个属性lv_anim_set_values指定起始值和结束值lv_anim_set_time指定动画时长lv_anim_set_path_cb指定缓动曲线例如lv_anim_path_ease_out表示先快后慢。如果你只想让一个图标缓慢旋转或者让一个进度条宽度变化lv_anim是最省事的方案。不过烟花效果里要管理的是一组粒子的位置、速度、生命值而且每个粒子的更新逻辑都不同所以用lv_timer做统一驱动更直观。2.2 为什么烟花效果不用 lv_animlv_anim的表面功能是“属性插值”但实际上它也是基于定时器实现的并且支持回调函数。那为什么本文依然推荐lv_timer主要原因有三个粒子数量多一个烟花爆炸可能产生 40~70 个粒子如果每个粒子单独建动画管理成本太高逻辑不是单纯插值粒子有重力、阻力、生命周期还要在粒子间互相组合适合用一个统一的循环处理绘制集中所有粒子最后都画到同一个 canvas 上不存在“每个粒子一个控件”的需求。使用lv_timer_create(callback, period_ms, user_data)可以创建一个周期性回调这个回调就是我们的主循环。每一帧做三件事更新粒子状态、清空画布、重新绘制粒子。2.3 Canvas buffer 内存模型lv_canvas和普通控件不同它不会自己分配像素内存而是要开发者提供一块 buffer。常见创建方式如下#define CANVAS_W 240 #define CANVAS_H 240 static lv_color_t canvas_buf[LV_CANVAS_BUF_SIZE_TRUE_COLOR(CANVAS_W, CANVAS_H)]; static lv_obj_t * canvas; canvas lv_canvas_create(lv_scr_act()); lv_obj_set_size(canvas, CANVAS_W, CANVAS_H); lv_canvas_set_buffer(canvas, canvas_buf, CANVAS_W, CANVAS_H, LV_IMG_CF_TRUE_COLOR);这里的LV_CANVAS_BUF_SIZE_TRUE_COLOR(W, H)宏会根据当前编译的颜色深度自动计算需要的像素数组大小。比如在 16 位色深RGB565下240×240 的画布需要240 * 240 * 2 112.5KB内存。如果是 RGB8888则需要240 * 240 * 4 225KB。这块 buffer 通常是静态数组或者全局数组不建议放在函数内部的栈上否则很容易导致栈溢出。理解这一点是后面做内存优化和排查问题的基础。3. 环境准备模拟器还是真机3.1 PC 模拟器方案如果你只是想快速验证代码逻辑不一定要先移植到开发板。LVGL 官方提供了 PC 模拟器工程配合 SDL2 可以在 Windows/Linux 上直接运行。常用组合是VS Code CMake SDL2或者 PlatformIO LVGL Library也可以使用 LVGL 官方的 Simulator 仓库。在模拟器里跑通烟花效果后再迁移到 STM32/ESP32开发效率会高很多。搜索“lvgl模拟器”“lvgl vscode开发环境搭建”能找到很多现成教程但要注意 LVGL 8.x 和 9.x 的工程结构不同。3.2 嵌入式平台方案如果你直接基于单片机开发环境准备主要包括在工程中加入 LVGL 源码配置lv_conf.h颜色深度、内存大小、显示缓冲区等对接显示驱动提供lv_disp_flush_cb刷新回调在while(1)或 RTOS 任务里周期调用lv_timer_handler()。STM32 移植 LVGL 是嵌入式 GUI 入门的常见路径网上资料很多。本文的重点不是完整移植而是讲解移植完成后如何实现烟花动画因此代码默认你已经有一个可以正常显示 LVGL 界面的工程。3.3 版本兼容性说明本文示例代码基于 LVGL 8.3 常用 API 编写主要用到的接口在 LVGL 9.x 中同样存在功能LVGL 8.3 APILVGL 9.x API创建画布lv_canvas_createlv_canvas_create设置 bufferlv_canvas_set_bufferlv_canvas_set_buffer部分版本也支持 draw buf 方式填充背景色lv_canvas_fill_bglv_canvas_fill_bg写单个像素lv_canvas_set_pxlv_canvas_set_px创建定时器lv_timer_createlv_timer_create触发重绘lv_obj_invalidatelv_obj_invalidate如果你的工程是 LVGL 9.x并且遇到某个 API 报错建议优先查看官方文档的迁移指南。不同子版本之间可能存在细微差异比如 Canvas buffer 的申请方式但整体思路一致。4. 粒子系统设计把烟花拆成三阶段4.1 粒子数据结构与状态机先定义粒子状态typedef enum { PARTICLE_IDLE 0, /* 空闲可以被复用 */ PARTICLE_RISE, /* 上升阶段 */ PARTICLE_EXPLODE /* 爆炸扩散阶段 */ } particle_state_t;每个粒子需要记录位置、速度、目标高度、颜色、剩余生命等信息typedef struct { particle_state_t state; float x, y; /* 当前坐标 */ float vx, vy; /* 水平/垂直速度 */ float target_y; /* 火箭爆炸的目标高度 */ lv_color_t color; /* 粒子颜色 */ uint8_t life; /* 剩余生命 */ uint8_t max_life; /* 总生命用于计算渐隐比例 */ } particle_t;为什么不直接用整数而用float因为粒子运动的平滑度很关键。在 240×240 的画布上坐标如果只用整数粒子每帧可能只移动 1~2 像素视觉上会显得很“卡顿”。使用浮点坐标再在绘制时取整运动过程会更平滑。4.2 火箭上升阶段火箭从画布底部随机位置生成初始速度向上水平方向可以给一个很小的随机速度让它看起来有一点摆动。核心更新逻辑p-vy -RISE_BASE_SPEED; /* 向上为负 */ p-y p-vy; p-x p-vx; if (p-y p-target_y) { explode_particle(i); }这里的target_y是火箭到达哪个高度后爆炸。为了让效果不单调可以随机生成一个值比如画布高度的 15%~55% 之间。4.3 爆炸扩散阶段爆炸时火箭自身消失同时产生一定数量的粒子。粒子从爆炸点向四周飞散角度均匀随机速度在一个范围内随机。float angle (float)(fr_rand() % 3600) / 10.0f * PI / 180.0f; float speed 1.0f (float)(fr_rand() % 100) / 100.0f * EXPLODE_MAX_SPEED;这样写的好处是角度覆盖 0~360 度速度有大有小粒子的扩散不会是一个标准圆而更像真实烟花的不规则爆裂。4.4 渐隐阶段爆炸粒子每帧都受到重力影响速度逐渐增加同时添加一点点空气阻力让粒子的运动更自然p-vx * 0.99f; /* 空气阻力每次保留 99% 的水平速度 */ p-vy GRAVITY; /* 重力垂直速度每帧增加 */粒子每帧减少life当life降为 0 时回到PARTICLE_IDLE状态等待下一次被复用。这种“对象池 状态机”的设计在嵌入式开发里非常实用。它避免了频繁动态申请和释放内存也符合 LVGL 小内存环境下的开发习惯。5. 完整 LVGL 烟花代码实现5.1 完整代码文件下面是一份可以直接放入 LVGL 工程的完整代码。为了适合阅读我把功能拆成几个函数并加上详细注释。/* 文件lv_firework_demo.c * 演示LVGL Canvas 粒子烟花效果 * 适配LVGL 8.3 / 9.xAPI 略有差异请以实际版本为准 */ #include lvgl.h #include string.h #include math.h /* 画布尺寸 */ #define CANVAS_W 240 #define CANVAS_H 240 /* 最大粒子数 */ #define MAX_PARTICLES 160 /* 物理参数 */ #define GRAVITY 0.22f #define RISE_BASE_SPEED 3.0f #define EXPLODE_MAX_SPEED 4.2f /* 发射间隔timer 30msFIRE_INTERVAL_TICKS20表示 600ms 发射一枚 */ #define FIRE_INTERVAL_TICKS 20 /* 粒子状态 */ typedef enum { PARTICLE_IDLE 0, PARTICLE_RISE, PARTICLE_EXPLODE } particle_state_t; typedef struct { particle_state_t state; float x, y; float vx, vy; float target_y; lv_color_t color; uint8_t life; uint8_t max_life; } particle_t; static lv_obj_t * firework_canvas NULL; static lv_color_t canvas_buf[LV_CANVAS_BUF_SIZE_TRUE_COLOR(CANVAS_W, CANVAS_H)]; static particle_t particles[MAX_PARTICLES]; static lv_timer_t * firework_timer NULL; static uint32_t rand_seed 0x20240101; /* 简单快速的伪随机数生成器不依赖 C 标准库 */ static uint32_t fr_rand(void) { rand_seed rand_seed * 1664525u 1013904223u; return rand_seed; } /* 烟花常用暖色可自行扩展 */ static const lv_color_t fire_colors[] { LV_COLOR_MAKE(255, 120, 0), LV_COLOR_MAKE(255, 200, 0), LV_COLOR_MAKE(255, 60, 60), LV_COLOR_MAKE(255, 255, 120), LV_COLOR_MAKE(255, 160, 255), LV_COLOR_MAKE(120, 220, 255), }; #define FIRE_COLOR_NUM (sizeof(fire_colors) / sizeof(fire_colors[0])) static int find_idle_particle(void) { for (int i 0; i MAX_PARTICLES; i) { if (particles[i].state PARTICLE_IDLE) { return i; } } return -1; } /* 在当前位置生成一圈爆炸粒子 */ static void explode_particle(int idx) { particle_t * rocket particles[idx]; float rx rocket-x; float ry rocket-y; rocket-state PARTICLE_IDLE; uint32_t count 40 fr_rand() % 30; for (uint32_t i 0; i count; i) { int slot find_idle_particle(); if (slot 0) break; particle_t * p particles[slot]; float angle (float)(fr_rand() % 3600) / 10.0f * 3.14159265f / 180.0f; float speed 1.0f (float)(fr_rand() % 100) / 100.0f * EXPLODE_MAX_SPEED; p-state PARTICLE_EXPLODE; p-x rx; p-y ry; p-vx cosf(angle) * speed; p-vy sinf(angle) * speed; p-color fire_colors[fr_rand() % FIRE_COLOR_NUM]; p-life 20 fr_rand() % 30; p-max_life p-life; p-target_y 0; } } /* 发射一枚火箭 */ static void launch_rocket(void) { int slot find_idle_particle(); if (slot 0) return; particle_t * p particles[slot]; p-state PARTICLE_RISE; p-x 20 fr_rand() % (CANVAS_W - 40); p-y CANVAS_H - 4; p-vx ((float)(fr_rand() % 100) - 50.0f) / 300.0f; p-vy -(RISE_BASE_SPEED (float)(fr_rand() % 60) / 60.0f); p-target_y CANVAS_H * (0.15f (float)(fr_rand() % 40) / 100.0f); p-color fire_colors[fr_rand() % FIRE_COLOR_NUM]; p-life 200; p-max_life 200; } /* 更新所有粒子的物理状态 */ static void update_particles(void) { for (int i 0; i MAX_PARTICLES; i) { particle_t * p particles[i]; if (p-state PARTICLE_RISE) { p-x p-vx; p-y p-vy; if (p-y p-target_y) { explode_particle(i); } } else if (p-state PARTICLE_EXPLODE) { p-vx * 0.99f; p-vy GRAVITY; p-x p-vx; p-y p-vy; if (p-life 0) p-life--; if (p-life 0) { p-state PARTICLE_IDLE; } } } } /* 把粒子绘制到 canvas 上 */ static void draw_fireworks(void) { lv_color_t bg lv_color_hex(0x101018); lv_canvas_fill_bg(firework_canvas, bg, LV_OPA_COVER); for (int i 0; i MAX_PARTICLES; i) { particle_t * p particles[i]; if (p-state PARTICLE_IDLE) continue; int px (int)p-x; int py (int)p-y; if (px 0 || px CANVAS_W || py 0 || py CANVAS_H) continue; if (p-state PARTICLE_EXPLODE p-max_life 0) { uint8_t alpha (uint8_t)((p-life * 255) / p-max_life); lv_color_t c lv_color_mix(p-color, bg, alpha); lv_canvas_set_px(firework_canvas, px, py
返回列表