STM32 U8G2库基础动画效果实现指南
前言:
本文基于u8g2绘图库,用C语言实现了部分基础的动画效果
使用硬件为stm32f103zet6,即正点原子精英开发版;和0.96寸oled屏iic驱动
使用该动画库前,需要读者自行移植u8g2,并配置底层驱动(通信、延时等),为了实现高帧率,推荐采取SPI+DMA的方式驱动屏幕
此外,由于本库只进行了基础的性能优化,对于一些硬件较差的平台,可能效率不佳;代码有不足之处,还请大佬们多多指教
效果展示:
动图中,依次调用了 逐字打印、收缩清屏、闪烁、发散显示、滑动字符串 和 缓动函数控制图形实现弹跳效果。
代码:
1.逐字打印字符串
/*
动效 - 逐字符显示字符串
参数: x/y - 打印位置左下角坐标
*text - 要打印的字符
space - 字符间隔
delay - 打印时间间隔
tip:需要在函数外部设置字体
*/
void PrintStr_CharByChar(u8g2_t *u8g2, uint8_t x, uint8_t y, uint8_t* text,uint8_t space, uint16_t delayMS){
uint8_t text_len;
text_len = strlen(text);
for(uint8_t i = 0; i < text_len; i++){
u8g2_DrawGlyph(u8g2, x + space * i, y, text[i]);
u8g2_SendBuffer(u8g2);
HAL_Delay(delayMS); //这个延时要短一些才有流畅的转换效果
}
}
很基础的动效,需要在调用前先设置气体,space间隔的选取需要考虑到使用的字体宽度。需要注意的是,u8g2的字体库中,并不是 所有的字体 中的字母 都等宽。
调用格式如下:
u8g2_SetFont(&my_u8g2, u8g2_font_8x13B_tr);
PrintStr_CharByChar(&my_u8g2, 10, 10, "test string", 8, 100);
2.滑动清屏
typedef enum{
CLEAR_LEFT,
CLEAR_RIGHT,
CLEAR_UP,
CLEAR_DOWN
} LinearClearDirction;
void LinearClearScreen(u8g2_t *u8g2, LinearClearDirction direction, uint8_t speed, uint16_t delayMS){
int screenWidth = u8g2_GetDisplayWidth(u8g2);
int screenHeight = u8g2_GetDisplayHeight(u8g2);
int x, y;
u8g2_SetDrawColor(u8g2, 0); //设置绘图颜色为黑色
switch (direction) {
case CLEAR_LEFT:
for (x = 0; x < screenWidth; x += speed) {
u8g2_DrawBox(u8g2, 0, 0, x, screenHeight);
u8g2_SendBuffer(u8g2);
HAL_Delay(delayMS);
}
break;
case CLEAR_RIGHT:
for (x = screenWidth - 1; x >= 0; x -= speed) {
u8g2_DrawBox(u8g2, x, 0, screenWidth, screenHeight);
u8g2_SendBuffer(u8g2);
HAL_Delay(delayMS);
}
break;
case CLEAR_UP:
for (y = 0; y < screenHeight; y += speed) {
u8g2_DrawBox(u8g2, 0, 0, screenWidth, y);
u8g2_SendBuffer(u8g2);
HAL_Delay(delayMS);
}
break;
case CLEAR_DOWN:
for (y = screenHeight - 1; y >= 0; y -= speed) {
u8g2_DrawBox(u8g2, 0, y, screenWidth, screenHeight);
u8g2_SendBuffer(u8g2);
HAL_Delay(delayMS);
}
break;
}
u8g2_ClearBuffer(u8g2);
u8g2_SendBuffer(u8g2);
// 恢复正常绘图模式
u8g2_SetDrawColor(u8g2, 1);
}
调用动画前,需要缓冲区存在待清屏的图像。效果就是屏幕一边缘,向另一层逐渐清除屏幕内容
PrintStr_CharByChar(&my_u8g2, 10, 10, "test string", 8, 100); //清屏对象
LinearClearScreen(&my_u8g2, CLEAR_UP, 2, 10);
3.闪烁
typedef void (*DrawFuncCallback)(u8g2_t *u8g2, uint8_t x, uint8_t y, void* params); //带传参的句柄
//画实心矩形
void EasingDrawBox(u8g2_t *u8g2, uint8_t x, uint8_t y, void *params){
uint8_t* box_params = (uint8_t*)params;
uint8_t width = box_params[0];
uint8_t height = box_params[1];
u8g2_DrawBox(u8g2, x, y, width, height);
}
void BlinkDraw(u8g2_t *u8g2, DrawFuncCallback drawFunc, uint8_t x, uint8_t y, void *params, uint8_t times, uint16_t delayMS) {
for (uint8_t i = 0; i < times; i++) {
// 反色绘制
u8g2_SetDrawColor(u8g2, 2); // 设置为反色模式
drawFunc(u8g2, x, y, params);
u8g2_SendBuffer(u8g2);
// 等待
HAL_Delay(delayMS);
// 再次反色绘制(恢复原状态)
drawFunc(u8g2, x, y, params);
u8g2_SendBuffer(u8g2);
// 等待
HAL_Delay(delayMS);
}
// 恢复正常绘图模式
u8g2_SetDrawColor(u8g2, 1);
}
一个比较复杂的动效,按照句柄格式,传入一个封装的绘图函数,这里绘图坐标x/y是固定uint8_t类型,但void *param根据传入的绘图函数不同,其类型和内容都会有所区别;传参时,应该严格参照封装后的绘图函数(以EasingDraw开头)内的解包格式
这里调用了绘制矩形的函数,因此param只是一个数组指针;效果是一个矩形间隔100ms闪烁共5次
uint8_t draw_param[2] = {100, 50};
BlinkDraw(&my_u8g2, EasingDrawBox, 20, 20, draw_param, 5, 100);
4.收缩清屏
/*
动效 - 收缩清屏(黑色绘制法)
参数: speed - 收缩倍率
delay - 收缩的延时
tip:动效会在完成时清空缓冲区
*/
void ShrinkClearScreen(u8g2_t *u8g2, uint8_t speed, uint16_t delayMS){
int screenWidth = u8g2_GetDisplayWidth(u8g2);
int screenHeight = u8g2_GetDisplayHeight(u8g2);
int x = 0, y = 0, width = screenWidth, height = screenHeight;
u8g2_SetDrawColor(u8g2, 0); //设置绘图颜色为黑色
while(width > 0 && height > 0){
// 清除顶部区域
if (y > 0) {
u8g2_DrawBox(u8g2, 0, 0, screenWidth, y);
}
// 清除底部区域
if (y + height < screenHeight) {
u8g2_DrawBox(u8g2, 0, y + height, screenWidth, screenHeight - (y + height));
}
// 清除左侧区域
if (x > 0) {
u8g2_DrawBox(u8g2, 0, y, x, height);
}
// 清除右侧区域
if (x + width < screenWidth) {
u8g2_DrawBox(u8g2, x + width, y, screenWidth - (x + width), height);
}
u8g2_SendBuffer(u8g2); // 发送绘制命令到屏幕
// 更新矩形位置和大小
x += (2 * speed);
y += (1 * speed);
width -= (4 * speed);
height -= (2 * speed);
HAL_Delay(delayMS);
}
u8g2_ClearBuffer(u8g2);
u8g2_SendBuffer(u8g2);
// 恢复正常绘图模式
u8g2_SetDrawColor(u8g2, 1);
}
从屏幕边缘开始,向屏幕中心点以矩形形状收缩,并不断清屏;调用方式同滑动清屏
5.旋转清屏
float tan_table[360] = {1}; //设置第一个元素为1,表示表尚未初始化
typedef enum{
CLEAR_ONE = 360, //一条线清屏360度
CLEAR_TWO = 180, //两条线各180
CLEAR_FOUR = 90 //四条线各90
} SpinClearDirction;
/*
动效 - 旋转清屏(点扫描+查表法)
参数: direction - 方向参数,ONE同时只有一条射线扫描清屏,TWO两条,FOUR四条
speed - 收缩倍率,speed应该是90的因数!
delay - 收缩的延时
tip:动效会在完成时清空缓冲区
*/
void Init_tantable(void){
for (int i = 0; i < 360; i++) {
tan_table[i] = tan(i * M_PI / 180.0);
}
}
// 判断点是否在需要清除的区域内
int is_in_clear_area(SpinClearDirction direction, int x, int y, int centerX, int centerY, int angle, int sector) {
int dx = x - centerX;
int dy = y - centerY;
//原点处理
if(dx == 0 && dy == 0) return 1;
//特殊角度处理
switch (angle){
case 0: if (dy == 0 && dx > 0) return 1;
break;
case 90: if (dx == 0 && dy > 0) return 1;
break;
case 180: if (dy == 0 && dx < 0) return 1;
break;
case 270: if (dx == 0 && dy < 0) return 1;
break;
default: break;
}
//分象限处理
float tan_point = (float)dy / (float)dx;
switch(direction){
case CLEAR_ONE:{
if(tan_point <= tan_table[angle]){
switch(sector){
case 0: if(dx > 0 && dy > 0) return 1;
break;
case 1: if(dx < 0 && dy > 0) return 1;
break;
case 2: if(dx < 0 && dy < 0) return 1;
break;
case 3: if(dx > 0 && dy < 0) return 1;
break;
default: break;
}
}
break;
}
case CLEAR_TWO:{
if(angle > 0 && tan_point <= tan_table[angle]){ //防止angle = 0时,tan90 = inf影响判断
if(tan_table[angle] > 0 && tan_point > 0) return 1;
if(tan_table[angle] < 0 && tan_point < 0) return 1;
}
break;
}
case CLEAR_FOUR:{
if(tan_point > 0){
if(tan_point <= tan_table[angle]) return 1;
}else if(tan_point < 0 && angle != 0){ //防止angle = 0时,tan90 = inf影响判断
if(tan_point <= tan_table[angle + 90]) return 1;
}
break;
}
}
return 0;
}
// 清除区域函数
void clear_sector(u8g2_t *u8g2, SpinClearDirction direction, int width, int height, int centerX, int centerY, int angle, int sector) {
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (is_in_clear_area(direction, x, y, centerX, centerY, angle, sector))
u8g2_DrawPixel(u8g2, x, y);
}
}
}
void SpinClearScreen(u8g2_t *u8g2, SpinClearDirction direction, uint8_t speed, uint16_t delayMS){
int width = u8g2_GetDisplayWidth(u8g2);
int height = u8g2_GetDisplayHeight(u8g2);
int centerX = width / 2, centerY = height / 2;
int sector; //象限
u8g2_SetDrawColor(u8g2, 0); //设置绘图颜色为黑色
if(tan_table[0] == 1) Init_tantable(); //如果未初始化表,初始化
for(int angle = 0; angle < direction; angle += speed){
sector = (float)angle / 90.1; //将90°分给前一象限
clear_sector(u8g2, direction, width, height, centerX, centerY, angle, sector);
u8g2_SendBuffer(u8g2);
HAL_Delay(delayMS);
}
u8g2_ClearBuffer(u8g2);
u8g2_SendBuffer(u8g2);
u8g2_SetDrawColor(u8g2, 1);
}
从屏幕中心做射线,射线旋转并不断清屏,提供了三种子方式,分别是1条清360度、2条各清180、4条各清90度,可以自行尝试
具体实现上,尽管使用了查表法,但是速度还是较慢,如果想要流畅的动画效果,建议延时设为0
6.进度条
/*
动效 - 进度条
参数: x/y - 进度条左上角坐标
width/height - 进度条长宽
percentage - 进度,按百分比输入
reuse - 重新调用标志位,0-第一次调用,1-重复调用
tip:如果是重复调用,置位reuse后,x/y/width/height参数都可以随意输入,函数会调用此前的参数
*/
void ProcessBar(u8g2_t *u8g2, uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t percentage, uint8_t reuse){
static uint8_t pre_x, pre_y, pre_w, pre_h;
//如果是重复调用,则不再绘画进度条外框
if(!reuse){
u8g2_DrawFrame(u8g2, x, y, width, height);
pre_x = x, pre_y = y, pre_w = width, pre_h = height;
}
//绘制填充的部分
uint8_t filledWidth = (pre_w - 2) * percentage / 100;
u8g2_DrawBox(u8g2, pre_x+1, pre_y+1, filledWidth, pre_h-2);
u8g2_SendBuffer(u8g2);
}
动效本身只画出特定百分比完成度的进度条,想要运动效果需要外层进行驱动
第一次调用后就会存储进度条尺寸参数,因此后续调用时,在置位reuse后,就可以只输入当前进度percentage
ProcessBar(&my_u8g2, 10, 10, 100, 10, 0, 0); //初次调用
for(int i = 0; i < 100; i++){
ProcessBar(&my_u8g2, 66, 66, 66, 66, i, 1); //重复调用
}
7.发散显示
void SpreadShow(u8g2_t *u8g2, DrawFuncCallback drawFunc, uint8_t drawX, uint8_t drawY, void *params, uint8_t speed, uint16_t delayMS){
int screen_width = u8g2_GetDisplayWidth(u8g2);
int screen_height = u8g2_GetDisplayHeight(u8g2);
int centerX = screen_width / 2, centerY = screen_height / 2;
int clip_width = 0, clip_height = 0;
while(clip_width < screen_width && clip_height < screen_height){
int x0 = centerX - clip_width/2;
int y0 = centerY - clip_height/2;
int x1 = centerX + clip_width/2;
int y1 = centerY + clip_height/2;
// 防止限制区超出屏幕边界
if(x0 < 0) x0 = 0;
if(y0 < 0) y0 = 0;
if(x1 > screen_width - 1) x1 = screen_width - 1;
if(y1 > screen_height - 1) y1 = screen_height - 1;
u8g2_ClearBuffer(u8g2);
u8g2_SetClipWindow(u8g2, x0, y0, x1, y1);
drawFunc(u8g2, drawX, drawY, params);
u8g2_SendBuffer(u8g2);
clip_width += 2*speed;
clip_height += speed;
HAL_Delay(delayMS);
}
u8g2_SetMaxClipWindow(u8g2);
}
收缩清屏的反向版,但由于需要不断进行重绘,运行速度比较下会更慢
uint8_t draw_param[2] = {100, 50};
SpreadShow(&my_u8g2, EasingDrawBox, 5, 5, draw_param, 1, 0);
8.缓动函数控制图形运动
最复杂的一个函数,由于开发的不是特别深入,在缓动函数的参数意义上有些不明确;如果使用时出现问题,可以留言提问,或者参考其他大佬开发的代码
先看顶层封装
typedef struct{
float t; //时间
float b; //开始位置,即t=0时函数值
float c; //变化量,即系数
float f; //动画单位时间内帧数
} EasingFuncParams;
//句柄类型定义
typedef void (*DrawFuncCallback)(u8g2_t *u8g2, uint8_t x, uint8_t y, void* params); //带传参的句柄
typedef float (*EasingFuncCallback)(EasingFuncParams* params); //缓动函数句柄
//结构体
typedef struct {
EasingFuncCallback easingfuncX; //x轴调用的缓动函数
EasingFuncCallback easingfuncY; //y轴调用的缓动函数
EasingFuncParams* paramX;
EasingFuncParams* paramY;
DrawFuncCallback drawFunc; //绘图函数
uint8_t x; //起始坐标
uint8_t y;
void *params; //绘图函数剩余参数
uint8_t clear; //画一帧后清屏,0-否,1-是
uint16_t frame_cnt; //绘画总帧数
} EasingFuncDrawParams;
/*
动效 - 缓动函数显示
参数: drawparams - 结构体储存的参数
last_pos - uint8_t型指针,绘图最后的位置会在函数调用结束时保存到该地址
delayMS - 延时
*/
void EasingFuncDraw(u8g2_t *u8g2, EasingFuncDrawParams *drawparams, uint8_t* last_pos, uint16_t delayMS){
int x_t = 0, y_t = 0, i = 0;
for(i = 0; i < drawparams->frame_cnt; i++){
drawparams->paramX->t = i;
drawparams->paramY->t = i;
x_t = (int)(drawparams->easingfuncX(drawparams->paramX));
y_t = (int)(drawparams->easingfuncY(drawparams->paramY));
if(drawparams->clear) u8g2_ClearBuffer(u8g2);
drawparams->drawFunc(u8g2, drawparams->x + x_t, drawparams->y + y_t, drawparams->params);
u8g2_SendBuffer(u8g2);
HAL_Delay(delayMS);
}
last_pos[0] = drawparams->x + x_t;
last_pos[1] = drawparams->y + y_t;
}
缓动函数的概念可以自行了解,总之,调用时需要:
传入一个绘图函数,这是要控制的目标;
为x/y方向各传入一个缓动函数,实现具体运动;
传入其他参数,如总帧数,单位时间帧数等
/*线性加速,t>0*/
float Linear(EasingFuncParams* params){
return params->c * params->t / params->f + params->b;
}
/*平方减速,0<t<1,f应等于frame_cnt,c=位移距离*/
float QuadraticEaseOut(EasingFuncParams* params){
float t = params->t;
t /= params->f;
return - params->c *t*(t-2) + params->b;
}
上面是两个缓动函数,他们的使用有所不同
这里t是迭代用的时间参数,f是单位时间内的帧数。理论上,t/f的范围应该在[0,1],但有些缓动函数,超出1后仍然可以使用,如上面的线性缓动函数;而第二个平方减速函数,则不可以,它的系数c等于它[0,1]间位移的距离,b等于起始值,一般置零
为了概念统一,可以如此设置参数:
缓动函数 – c设置为预想的位移距离,b置零,f设置与frame_cnt相同;
顶层函数 – frame_cnt 设置为预期动画总帧数
最后是调用,调用流程比较复杂,请严格按照例程执行
uint8_t draw_param[3] = {5, 5, U8G2_DRAW_ALL}; //绘图函数的额外参数,由于调用的是椭圆绘制函数,因此额外参数有3个
uint8_t last_pos[2]; //存储最后一次画图坐标
//x轴缓动函数参数
EasingFuncParams paramsx = {
.t = 0,
.b = 50,
.c = 0.5,
.f = 0
};
//y轴缓动函数参数
EasingFuncParams paramsy = {
.t = 0,
.b = 0,
.c = 1,
.f = 0
};
//初始化缓动绘图函数参数
EasingFuncDrawParams mydrawbox= {
.easingfuncX = Linear, //x轴调用线性缓动
.easingfuncY = CubicEaseIn, //y轴调用立方加速缓动
.paramX = ¶msx, //为两轴函数赋参数
.paramY = ¶msy,
.drawFunc = EasingDrawEllipse, //调用椭圆绘图函数
.x = 10, //起始坐标(10, 10)
.y = 10,
.params = draw_param, // 指向实际参数的指针
.clear = 1, //绘制一个后清除上一个
.frame_cnt = 50 //总共绘制50帧
};
EasingFuncDraw(&my_u8g2, &mydrawbox, last_pos, 0);
源代码可以在这里获取
百度网盘 请输入提取码
提取码etpt
作者:TPenny68