傻孩子(GorgonMeducer) · 2021年11月24日

【例说Arm-2D界面设计】任意尺寸的圆角矩形(上)

image.png

【书接上回】

在上篇文章《【例说Arm-2D界面设计】做剪影风也太简单了8!》中我们介绍了使用透明蒙版的方法来实现“性冷淡风”图标显示的方法。其中,我们提到了使用透明蒙版的三个好处:

  • 允许我们利用图形的剪影来重新染色;
  • 边缘处自带抗锯齿效果;
  • 在使用剪影风来构建界面时,仅存储透明蒙版要比存储完整的RGBA8888图像节省75%的存储空间。

然而,单纯使用前文介绍的方法也有以下几个明显的限制:

  • 图形的大小固定
  • 当处理面积较大的剪影时,透明蒙版仍然会占据可观的存储空间,比如100*100的蒙版就要消耗掉10K的ROM;

正如我们在文章《【例说Arm-2D界面设计】从不规则图标的显示说起》中所提出的那样:现代界面设计中圆角矩形是一个不可或缺的图形要素。例如:在下面的界面“概念设计”中,我们很容易注意到,圆角矩形所构成的透明蒙版成功构建出了GUI设计的常见要素:面板、按钮、文本框、列表……

image.png

这里就引入了一个很具体的问题:

  • 不同的圆角矩形拥有不同的形状和面积;
  • 为每一个可能用到圆角矩形的地方都保存一个固定尺寸的透明蒙版会占用大量的存储空间;

那么有没有一种方法可以同时解决上述问题——以极小的代价在资源高度受限的嵌入式环境下提供任意尺寸圆角矩形(透明蒙版)的方案呢?

答案是肯定的。

不过别着急,让我们先从“圆角”的实现说起。

【如何获取一个“圆角”】

在上一篇文章中,我们已经通过“PPT”加“img2c.py脚本”的方式生成了一个圆形的透明蒙版 circle.c——不清楚创建资源方式的小伙伴,可以单击这里来阅读对应的内容。

打开 circle.c,可以看到它描述了一个 60 * 60 的透明蒙版:


extern const arm_2d_tile_t c_tileCircleMask;
const arm_2d_tile_t c_tileCircleMask = {
    .tRegion = {
        .tSize = {
            .iWidth = 60,
            .iHeight = 60,
        },
    },
    .tInfo = {
        .bIsRoot = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_8BIT,
        },
    },
    .pchBuffer = (uint8_t *)c_bmpCircleAlpha,
};

通过下面的代码,我们可以轻松的将其显示在背景图片上:

extern const arm_2d_tile_t c_tileCircleMask;

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    //! 绘制背景图片
    arm_2d_rgb16_tile_copy(&c_tileBackground, ptFrameBuffer, NULL, ARM_2D_CP_MODE_COPY);
    //! 计算居中区域
    arm_2d_align_centre(*ptFrameBuffer, c_tileCircleMask.tRegion.tSize) {
        arm_2d_rgb565_fill_colour_with_mask_and_opacity(  
              ptFrameBuffer,    //!< 显示缓冲区
              &__centre_region, //!< 显示缓冲区的中心
              &c_tileCircleMask,//!< 透明蒙版
              
              //! 利用透明蒙版将指定范围染成白色
              (arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
              64 );             //!< 25% 的不透明度
        arm_2d_op_wait_async(NULL);
    }

}

在 320 * 240 屏幕上显示的效果是这样的:

image.png

目前为止都还算是复述上一篇文章所介绍的内容,并无什么特别的难度。

根据简单的几何知识,我们知道:圆是一个中心对称图形。借助这一特性,实际上我们只需要保存1/4的扇形区域,就可以通过镜像的方法还原出整个圆形(如下图所示):

image.png

获取1/4个圆的方法很多:

  • 通过算法专门生成一个,并将其填充;
  • 利用PPT+img2c.py的方式构建一个专门的素材;
  • 从已有的圆素材中派生一个子素材;

相信对很多朋友来说前两种方法是显而易见的,而第三种方式却可能稍微陌生——“从一个已有素材中截取局部来派生出一个新素材”是Arm-2D特别引入的一个功能。比如,我们可以在 c_tileCircleMask 的基础上截取出一个扇形,派生出一个新的tile:


extern const arm_2d_tile_t c_tileCircleQuaterMask;
const arm_2d_tile_t c_tileCircleQuaterMask = {
    .tRegion = {
        .tLocation = {
            .iX = 30,
            .iY = 0,
        },
        .tSize = {
            .iWidth = 30,
            .iHeight = 30,
        },
    },
    .tInfo = {
        .bIsRoot = false,
        .bDerivedResource = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_8BIT,
        },
    },
    .ptParent = (arm_2d_tile_t *)&c_tileCircleMask,
};

这里需要简单的解释一下:

  • 新的素材贴图是从已有的透明蒙版 c_tileCircleMask基础上派生而来,因此:
  • 21行ptParent指针就指向了父贴图 c_tileCircleMask
  • 14行bIsRoot被设置为false,因为它是派生而来,并不直接携带位图数据;
  • 15行bDerivedResource明确的被设置为true,标明它是一个派生资源(这点很重要,不能漏掉
  • 新的贴图资源必须拥有和父贴图资源相同的颜色信息(这一信息不能省略);
  • 由于我们截取的是原贴图中右上角的部分,因此:
  • 原来的图形尺寸是 60 * 60,1/4圆的尺寸就是 30 * 30;
  • 右上角所代表的起始坐标是 (30,0);

获得了上述资源后,我们可以像普通贴图那样直接在程序中使用:

extern const arm_2d_tile_t c_tileCircleQuaterMask;

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    arm_2d_rgb16_tile_copy(&c_tileBackground, ptFrameBuffer, NULL, ARM_2D_CP_MODE_COPY);

    arm_2d_align_centre(*ptFrameBuffer, c_tileCircleQuaterMask.tRegion.tSize) {
        arm_2d_rgb565_fill_colour_with_mask_and_opacity(  
                ptFrameBuffer, 
                &__centre_region, 
                &c_tileCircleQuaterMask, 
                (arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
                128 );    //!< 50% 的不透明度
        arm_2d_op_wait_async(NULL);
    }
}

通过显示效果,可以清晰的验证:我们的确成功的获得了右上角的扇形;

image.png

这里选择右上角的扇形并没有任何特殊的意义,只是为了展示我们可以取一个已有资源的任意位置而已。实际应用中,我们往往为了偷懒,通常会选择左上角,而不是需要我们“略加思索、计算所得”的右上角,比如:

extern const arm_2d_tile_t c_tileCircleQuaterMask;
const arm_2d_tile_t c_tileCircleQuaterMask = {
    .tRegion = {
        //.tLocation = {
        //    .iX = 0,
        //    .iY = 0,
        //},
        .tSize = {
            .iWidth = 30,
            .iHeight = 30,
        },
    },
    .tInfo = {
        .bIsRoot = false,
        .bDerivedResource = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_8BIT,
        },
    },
    .ptParent = (arm_2d_tile_t *)&c_tileCircleMask,
};

无论选哪个角,用起来都是一样的。

【镜像!镜像!再镜像!】

拿到了四分之一圆以后,要想还原出整个圆,就需要进行以下的镜像方式:

  • X轴镜像,获得左上角的扇形;
  • Y轴镜像,获得右下角的扇形;
  • X轴Y轴同时镜像,获得左下角的扇形。

为了保存这些镜像后的蒙版,我们需要在RAM中开辟一段专门的区域:

static ARM_NOINIT uint8_t c_bmpCorner[30 * 30];
static const arm_2d_tile_t c_tileCorner = {
    .tRegion = {
        .tSize = {
            .iWidth = 30,
            .iHeight = 30,
        },
    },
    .tInfo = {
        .bIsRoot = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_8BIT,
        },
    },
    .pchBuffer = c_bmpCorner,
};

接下来,就要借助 arm_2d_cbit8_tile_copy()函数在素材拷贝的同时完成镜像操作:

  • X镜像:

    arm_2d_c8bit_tile_copy(&c_tileCircleQuaterMask,
                       &c_tileCorner,
                       NULL,
                       ARM_2D_CP_MODE_COPY      |
                       ARM_2D_CP_MODE_X_MIRROR  );
  • Y镜像:
arm_2d_c8bit_tile_copy(&c_tileCircleQuaterMask,
                       &c_tileCorner,
                       NULL,
                       ARM_2D_CP_MODE_COPY      |
                       ARM_2D_CP_MODE_Y_MIRROR  );
  • XY镜像:
arm_2d_c8bit_tile_copy(&c_tileCircleQuaterMask,
                       &c_tileCorner,
                       NULL,
                       ARM_2D_CP_MODE_COPY      |
                       ARM_2D_CP_MODE_X_MIRROR  |
                       ARM_2D_CP_MODE_Y_MIRROR  );

借助上述方法,我们可以重新拼接出完整的圆形:

extern const arm_2d_tile_t c_tileCircleQuaterMask;




static ARM_NOINIT uint8_t c_bmpCorner[30 * 30];
static const arm_2d_tile_t c_tileCorner = {
    .tRegion = {
        .tSize = {
            .iWidth = 30,
            .iHeight = 30,
        },
    },
    .tInfo = {
        .bIsRoot = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_8BIT,
        },
    },
    .pchBuffer = c_bmpCorner,
};

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    arm_2d_rgb16_tile_copy(&c_tileBackground, ptFrameBuffer, NULL, ARM_2D_CP_MODE_COPY);
    
    arm_2d_align_centre(*ptFrameBuffer, c_tileCircleQuaterMask.tRegion.tSize) {
        
        arm_2d_rgb565_fill_colour_with_mask_and_opacity(  
                            ptFrameBuffer, 
                            &__centre_region, 
                            &c_tileCircleQuaterMask, 
                            (arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
                            128 );
                            
        //! 绘制左上角
        do {
            arm_2d_c8bit_tile_copy(&c_tileCircleQuaterMask,
                                   &c_tileCorner,
                                   NULL,
                                   ARM_2D_CP_MODE_COPY      |
                                   ARM_2D_CP_MODE_X_MIRROR  );
                                
            arm_2d_op_wait_async(NULL);
            //! 增量式更新__centre_region使其覆盖左上角
            __centre_region.tLocation.iX -= c_tileCorner.tRegion.tSize.iWidth;
            
            arm_2d_rgb565_fill_colour_with_mask_and_opacity(  
                                ptFrameBuffer, 
                                &__centre_region, 
                                &c_tileCorner, 
                                (arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
                                128 );
        } while(0);
        
        //! 绘制左下角
        do {
            arm_2d_c8bit_tile_copy(&c_tileCircleQuaterMask,
                                   &c_tileCorner,
                                   NULL,
                                   ARM_2D_CP_MODE_COPY      |
                                   ARM_2D_CP_MODE_X_MIRROR  |
                                   ARM_2D_CP_MODE_Y_MIRROR  );
                                
            arm_2d_op_wait_async(NULL);
            //! 增量式更新 __centre_region 使其覆盖左下角      
            __centre_region.tLocation.iY += c_tileCorner.tRegion.tSize.iHeight;
            
            arm_2d_rgb565_fill_colour_with_mask_and_opacity(  
                                ptFrameBuffer, 
                                &__centre_region, 
                                &c_tileCorner, 
                                (arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
                                128 );
        } while(0);

        //! 绘制右下角
        do {
            arm_2d_c8bit_tile_copy(&c_tileCircleQuaterMask,
                                   &c_tileCorner,
                                   NULL,
                                   ARM_2D_CP_MODE_COPY      |
                                   ARM_2D_CP_MODE_Y_MIRROR  );
                                
            arm_2d_op_wait_async(NULL);
            //! 增量式更新 __centre_region 使其覆盖右下角      
            __centre_region.tLocation.iX += c_tileCorner.tRegion.tSize.iWidth;
            
            arm_2d_rgb565_fill_colour_with_mask_and_opacity(  
                                ptFrameBuffer, 
                                &__centre_region, 
                                &c_tileCorner, 
                                (arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
                                128 );
        } while(0);
                            
        arm_2d_op_wait_async(NULL);
    }
}

整个绘制过程如下图所示:
image.png

【任意尺寸的圆角矩形】

至此,我们已经有能力根据仅仅1/4个圆就实现圆角矩形所有4个角的效果。实际上,如下图所示:对于一个任意给定尺寸的圆角矩形,我们都可以用四个角加色块填充的方式来实现。

image.png

实践中,对于类似320*240这类的屏幕(甚至稍微大一点,比如480*272这种),通常半径为7像素的圆角就已经绰绰有余这意味我们只需要保存 7 * 7 = 49个字节的透明模板,就可以绘制几乎任意尺寸的圆角矩形(当然,width和height都必须>=7)。

更具体的,我们不妨设计一个专门的函数,来绘制指定尺寸、颜色和透明度的圆角矩形:

void draw_round_corner_box( const arm_2d_tile_t *ptTarget, 
                            const arm_2d_region_t *ptRegion,
                            uint16_t hwColour,
                            uint8_t chOpacity);

其中:

  • ptTarget:     一个指针,指向目标缓冲区;
  • ptRegion:    一个指针,指向目标缓冲区内的给定位置;
  • hwColour:   圆角矩形的颜色(RGB565)
  • chOpacity:  圆角矩形的不透明度

比方说,这样的效果:

image.png

就可以通过对draw_round_corner_box()的简单调用来实现:

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    arm_2d_rgb16_tile_copy(&c_tileBackground, ptFrameBuffer, NULL, ARM_2D_CP_MODE_COPY);
    
    arm_2d_align_centre(*ptFrameBuffer, 
                        150,    //!< 圆角矩形宽度 150
                        100) {  //!< 圆角矩形高度 100
        
        //! 用白色在屏幕的中央绘制圆角矩形
        draw_round_corner_box(
            ptFrameBuffer,     //!< 目标缓冲区
            &__centre_region,  //!< 中心区域
            GLCD_COLOR_WHITE,  //!< 白色
            64);               //!< 不透明度 25%

        arm_2d_op_wait_async(NULL);
    }
}

要想编写这样一个函数,除了四个圆角可以使用我们前面介绍过的方法生成外,中间矩形的透明部分则直接借助带(不)透明信息的颜色填充函数arm_2d_rgb565_fil__colour_with_opacity() 来全权负责。它的等效函数原型为:

ARM_NONNULL(1)
extern
arm_fsm_rt_t arm_2dp_rgb565_fill_colour_with_opacity( 
          const arm_2d_tile_t *ptTarget,
          const arm_2d_region_t *ptRegion,
          arm_2d_color_rgb565_t tColour,
          uint_fast8_t chOpacity);

其中:

  • ptTarget:     一个指针,指向目标缓冲区;
  • ptRegion:    一个指针,指向目标缓冲区内的给定位置;
  • tColour:       圆角矩形的颜色(arm_2d_color_rgb565_t)
  • chOpacity:  圆角矩形的不透明度

是不是与我们的draw_round_corner_box()如出一辙?唯一需要注意的是,这里 uint16_t的颜色与arm_2d_color_rgb565_t可以通过下述的方式进行转换:

(arm_2d_color_rgb565_t){<uint16_t类型的颜色>}

具体的代码可以在 examples/benchmark/controls/shape_round_corner_box.c 中找到,这里就不再赘述了。方便打开github的朋友也可以复制下面的链接到浏览器后进行访问:

https://github.com/ARM-software/EndpointAI/blob/b89a04b0f1db4cdcc1a1cb26dd6ca0646eca7d0d/Kernels/Research/Arm-2D/examples/benchmark/controls/shape_round_corner_box.c#L69

【一个简单的界面设计】

还记得本文一开始时候所提出的一个界面概念么?

image.png

借助draw_round_corner_box() 的帮助,我们可以轻松的将其变为现实:

image.png

怎么样?除了还没有填充文字和图标外,是不是已经有那个味儿了?

参考代码如下:

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    //! 绘制背景图片
    arm_2d_rgb16_tile_copy(&c_tileBackground, ptFrameBuffer, NULL, ARM_2D_CP_MODE_COPY);
    
    //! 绘制白色的工具栏                
    draw_round_corner_box(
          ptFrameBuffer,
          (const arm_2d_region_t []) {{
                    .tLocation = {
                        .iX = 20,
                        .iY = 20,
                    },
                    .tSize = {
                        .iWidth = 280,
                        .iHeight = 40,
                    },
                }},
          GLCD_COLOR_WHITE,
          64);

    //! 绘制工具栏上一个 40*40 的被选中的图标背景色               
    draw_round_corner_box(
        ptFrameBuffer,
        (const arm_2d_region_t []) {{
                  .tLocation = {
                      .iX = 60,
                      .iY = 20,
                  },
                  .tSize = {
                      .iWidth = 40,
                      .iHeight = 40,
                  },
              }},
        GLCD_COLOR_WHITE,
        128);
                                
    //! 绘制蓝色的透明面板
    arm_2dp_rgb565_fill_colour_with_opacity(  NULL,
        ptFrameBuffer, 
        (const arm_2d_region_t []) {{
            .tLocation = {
                .iX = 0,
                .iY = 80,
            },
            .tSize = {
                .iWidth = APP_SCREEN_WIDTH,
                .iHeight = 140,
            },
        }},
        (arm_2d_color_rgb565_t){__RGB(   0x3D,   0xAA, 0xD6  )},
        200);       
        
    //! 面板上左半边的白色不透明文本框      
    draw_round_corner_box(
        ptFrameBuffer,
        (const arm_2d_region_t []) {{
                  .tLocation = {
                      .iX = 10,
                      .iY = 90,
                  },
                  .tSize = {
                      .iWidth = 140,
                      .iHeight = 120,
                  },
              }},
        GLCD_COLOR_WHITE,
        255);

    //! 面板上右边半透明的文本框
    draw_round_corner_box(
        ptFrameBuffer,
        (const arm_2d_region_t []) {{
                  .tLocation = {
                      .iX = 170,
                      .iY = 90,
                  },
                  .tSize = {
                      .iWidth = 140,
                      .iHeight = 120,
                  },
              }},
        GLCD_COLOR_WHITE,
        128);
                                
}

千万别忘了,我们只用了一个50字节不到的、7*7大小的素材哦!

【说在后面的话】

在小资源环境下,如何对已有资源进行物尽其用是最能体现设计师聪明才智的地方。利用本文介绍的两种方式:

  • 在已有素材基础上进行派生;
  • 对已有的素材进行镜像操作

可以在在占用极小ROM的情况下实现令人惊艳的效果。这当然也离不开对透明模板的灵活使用和活用各类带透明效果的贴图和颜色填充函数。说来你也许不信,本文实际上用到的arm-2d API函数实际上只有三类:

  • tile-copy:支持贴图、填充、镜像
  • fill-colour-with-opacity:在指定的区域内填充颜色,并带有透明效果
  • fill-colour-with-mask-and-opacity:根据透明蒙版进行颜色填充,并可以额外指定(不)透明度

设想一下,在一个资源极其有限的系统中,借助 PFB Helper我们可以使用8*8 的PFB以128字节 RAM的代价完成整个屏幕的刷新以49个字节 ROM的代价实现任意大小的圆角矩形——这可以为成本敏感的现代IoT设备来充分的竞争力。

首发:裸机思维
作者: GorgonMeducer 傻孩子

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。
推荐阅读
关注数
1462
内容数
107
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息