傻孩子(GorgonMeducer) · 2022年11月21日 · 北京市

【玩转arm-2d】手把手教你实现抗锯齿的字体

以下文章来源于嵌入式小书虫 ,作者FledgingSu 支离苏

image.png

A4字体是什么

你用过A4纸,那你听说过A4字体吗?

image.png

哈哈哈,在讲A4字体之前,我们先看看平时我们用的普通字体,如下
image.png
字体中的一个点用一位二进制数表示,也就是非黑即白
image.png
这样,画斜线(撇和捺)的时候就会有明显的锯齿是不是,像那种没有锯齿的字体(如下图)是怎么弄的呢?
image.png
别急,我们先把他放大看看,如下
image.png
放大后我们很容易发现,在锯齿边缘还有一些灰色的点(这样斜线的锯齿感就会大大减小),但是问题又来了,这样一个点用一个二进制位来表示就不行了,那怎么办呢?

此时,我们可以采用4位二进制数来表示一个点,这样一个点就可以表示成16种灰度了(* ̄︶ ̄)。这个就是我们今天要讲的A4字体。同样的,一个点用2位二进制数来表示就是A2字体,以此类推,A8字体就是一个点用8位二进制数来表示。

哈哈哈,这也太简单了,那有没有A16字体呢?

image.png

这里A2、A4、A8的A是指Alpha,目前Alpha最大也就8bit,所以最大是A8。不过如果只考虑抗锯齿效果的话,A4字体基本就可以满足了。这样一个字符所占用的存储空间也是原来的4倍了。如果存储空间足够,也可以用A8字体哦。

当然,如果你脑洞够大,也可以试试A16字体,我会在今天的彩蛋环节讲解A16字体的妙用哈(* ̄︶ ̄)

【A4字库的制作】

要显示A4字体,首先要有A4字库,那我们就先制作一个自定义的A4字库,这个字库中包含数字0~9和字符A~F还有一个小数点‘.’共17个字符。

要制作字库,还得有字符素材,素材我们就简单在ppt中输入我们需要的字符并截取成图片就可以了,如下:
image.png
然后用Arm-2D提供的工具img2c.py生成A4字库,命令如下

python3 img2c.py -i digitsfont.png --name myDigitsFont  --dim 15 272
  • 此命令可以帮我们把每一个字符缩放成15*16像素哦,因为我们的字库只有17个字符,所以高度为16*17=272。

打开生成的c文件,我们就可以看到A4字库了,如下:

static const uint8_t c_bmpmyDigitsFontA4Alpha[8*272] = {
/* -0- */
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 
/* -1- */
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 
/* -2- */
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 
/* -3- */
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 
......
};
  • 其实这个工具同时也帮我们生成了A2和A8字库哦

此C文件中也生成了字库所对应的tile,如果你还不知道Tile是什么的,可以看看下面这篇文章:

【玩转Arm-2D】三、资源贴图(Child Tile)和汉字显示

待会我们就可以直接使用这个字库Tile了,(如下)

extern const arm_2d_tile_t c_tilemyDigitsFontA4Mask;

const arm_2d_tile_t c_tilemyDigitsFontA4Mask = {
   ...
};

【显示字库中的字符】

有了自定义的字库,那我们就开始把字库中的字符显示出来。由于我们的字库是自定义的,而不是全部的ASCII码表(在ASCII码表中每一个字符都有固定的位置即映射关系是已知的),如下
image.png
所以,我们首先要解决自定义字库中每个字符所在的位置(即映射关系),位置确定好后就方便我们从字库中找到对应的字符了,此时就需要制作一张字符映射表。

那我们就定义一个结构体用来制作映射表,如下

typedef struct arm_2d_char_idx_t {
    uint8_t chStartCode[4];
    uint16_t hwCount;
    uint16_t hwOffset;
} arm_2d_char_idx_t;
  • chStartCode就是起始字符,比如数字0~9,在ASC码中是连续的,所以我们只需要记录开始的字符“0”就可以了,那问题又来了,一个字符只占一个字节啊,为啥要定义成4个字节呢?这个就是为了兼容其它的编码集,比如汉字字符的GB2312编码集就需要两个字节哦。
  • hwCount就是一共有几个字符,比如0~9就是10个字符。
  • hwOffset就是起始字符在自定义字库中的偏移量,比如字符“0”就在我们字库中的第一个位置,所以偏移量就为0.

有了这个结构体,我们的字符在字库中的映射关系就很容易确定了,下面我们就把我们的自定义字体My_FONT_A4_DIGITS_ONLY和字符映射表确定好,如下

struct My_Fount{
    implement(arm_2d_user_font_t);
    ||定义映射表
    arm_2d_char_idx_t tNumbers;
    arm_2d_char_idx_t tABCDEF;
    arm_2d_char_idx_t tDot;
} My_FONT_A4_DIGITS_ONLY = {
    .use_as__arm_2d_user_font_t = {
        .use_as__arm_2d_font_t = {
            .tileFont = impl_child_tile(
                c_tileMyDigitsFontA4Mask,/* 字库Tile */
                0,          /* x offset */
                0,          /* y offset */
                15,         /* width */
                272         /* height */
            ),
            .tCharSize = {
                .iWidth = 15,
                .iHeight = 16,
            },
            .nCount =  17,  //!< Character count
            .fnGetCharDescriptor = &__digit_font_get_char_descriptor,
            .fnDrawChar = &__digit_font_a4_draw_char,
        },
        .hwCount = 3,
        .hwDefaultCharIndex = 0, /* 默认字符 */
    },
    //初始化字符映射表
    .tNumbers = {
        .chStartCode = {'0'},
        .hwCount = 10,
        .hwOffset = 0,
    },
    
    .tABCDEF = {
        .chStartCode = {'A'},
        .hwCount = 6,
        .hwOffset = 10,
    },
 
    .tDot = {
        .chStartCode = {'.'},
        .hwCount = 1,
        .hwOffset = 16,
    },    
};
  • 我们的字符映射表共3个元素(即tNumbers、tABCDEFtDot),所以第25行的hwCount要设置成3。
  • 第26行hwDefaultCharIndex为默认字符(即遇到字符集不认识的码,都会用默认字符来替代),我们这里设置为0也就是默认显示字符‘0’。当然,这个默认字符最好设置成‘空格’
  • 在我们的自定义字体中还需要两个函数,一个是字符绘制函数__digit_font_a4_draw_char,和字符映射函数__digit_font_get_char_descriptor。

字符绘制函数很简单,我们直接调用Arm-2D的绘制函数就可以,如下

static
IMPL_FONT_DRAW_CHAR(__digit_font_a4_draw_char)
{
    return arm_2d_fill_colour_with_a4_mask_and_opacity(
        ptTile, 
        ptRegion,
        ptileChar,
        (__arm_2d_color_t){tForeColour},
        (uint8_t)chOpacity);
}

字符映射函数如下:

static
IMPL_FONT_GET_CHAR_DESCRIPTOR(__digit_font_get_char_descriptor)
{
    assert(NULL != ptFont);
    assert(NULL != ptDescriptor);
    assert(NULL != pchCharCode);

    arm_2d_user_font_t *ptThis = (arm_2d_user_font_t *)ptFont;
    
    memset(ptDescriptor, 0, sizeof(arm_2d_char_descriptor_t));
    //初始化Descriptor
    ptDescriptor->tileChar.tRegion.tSize = ptFont->tCharSize;
    ptDescriptor->tileChar.ptParent = (arm_2d_tile_t *)&ptFont->tileFont;
    ptDescriptor->tileChar.tInfo.bDerivedResource = true;
    ptDescriptor->chCodeLength = 1;
    //计算字符对应的偏移量
    arm_foreach( arm_2d_char_idx_t, this.tLookUpTable, this.hwCount, ptItem) {
        //在哪个映射表范围区间,是0~9还是A~F还是小数点
        if (    *pchCharCode >= ptItem->chStartCode[0] 
            &&  *pchCharCode < (ptItem->chStartCode[0] + ptItem->hwCount)) {
            //计算偏移量
            int16_t iOffset = *pchCharCode - ptItem->chStartCode[0];
            //根据偏移量计算Y坐标
            ptDescriptor->tileChar.tRegion.tLocation.iY 
                = (ptItem->hwOffset + iOffset) * ptFont->tCharSize.iHeight;
            return ptDescriptor;
        }
    }

    /* 如果字符不在字库中就显示默认字符 */
    ptDescriptor->tileChar.tRegion.tLocation.iY 
        = this.tLookUpTable[this.hwDefaultCharIndex].hwOffset 
        * ptFont->tCharSize.iHeight;

    return ptDescriptor;
}
  • 此函数的功能也很简单,就是根据字符映射表找到字符在字库中的Y坐标,如下图所示
    image.png
  • 这里还有一个变量需要说明一下,就是_ptDescriptor->chCodeLength,_我们在这里设置成1是因为我们的字符使用的是ASCII码,每个字符只占1个字节,如果我们是汉字字符即(GB2312编码),那么此时就需要设置成2,因为每个汉字字符占2个字节.

好了,到这里我们的自定义字体就制作完了,他的使用也很简单,如下

typedef struct My_Fount My_Fount;
extern  My_Fount My_FONT_A4_DIGITS_ONLY;
void test_my_font(){
    arm_lcd_text_set_colour(GLCD_COLOR_RED, GLCD_COLOR_WHITE);
    arm_lcd_text_set_font((arm_2d_font_t *)&My_FONT_A4_DIGITS_ONLY);
    arm_lcd_text_location(1,0);
    arm_lcd_printf("A4%d",1230);
}

如果你还想在屏幕的任意位置显示字符,那只需要指定一个区域就可以了,设置打印区域的函数如下:

  arm_2d_region_t myRegion={
      .tLocation = { .iX = 0,.iY = 110,},
      .tSize = {.iWidth = 69,.iHeight = 69,},//18      
  };  
  arm_lcd_text_set_draw_region(&myRegion);
  arm_lcd_printf("A4");

大家是不是觉得自定义字体很简单,文中的程序是参考官方的代码修改而来,代码地址如下:

| https://github.com/ARM-software/Arm-2D/blob/developing/examples/common/asset/DigitsFont.c |

接下来就是今天的彩蛋环节

image.png

我们这里要讲一下自定义A16字体,看看A16字体到底有什么妙用(* ̄︶ ̄)

首先,我们说的A16字体就是一个点用16位二进制数来表示,如果是灰度图的话就有65536级灰度了,
image.png
弄这么多级灰度意义的确不大,所以我们干脆就把字库做成彩色的(即RGB565),如下图所示
image.png
也就是说我们输入一个“花”字,屏幕上就显示一朵彩色的花,是不是也很有意思。那我们就赶快把这个彩色字体实现一下吧。

首先还是制作自定义字库c_tileMyFontA16Mask,如下

static const uint16_t c_bmpMyFontA16Alpha[22*88] = {
    //flower
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 
...
}

const arm_2d_tile_t c_tileMyFontA16Mask = {
    .tRegion = {
        .tSize = {
            .iWidth = 22,
            .iHeight = 88,
        },
    },
    .tInfo = {
        .bIsRoot = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_RGB565,
        },
    },
    .pchBuffer = (uint8_t *)c_bmpMyFontA16Alpha,
};
  • 备注:每个图片素材为22*22像素,颜色信息chScheme为RGB565。

然后制作字符映射表,如下


struct My_Fount_A16{
    implement(arm_2d_user_font_t);
    //定义映射表
    arm_2d_char_idx_t tFlower;
    arm_2d_char_idx_t tLeaf;
    arm_2d_char_idx_t tWater;
    arm_2d_char_idx_t tStar;
} My_FONT_A16_ONLY = {
    .use_as__arm_2d_user_font_t = {
        .use_as__arm_2d_font_t = {
            .tileFont = impl_child_tile(
                c_tileMyFontA16Mask,/* 字库Tile */
                0,          /* x offset */
                0,          /* y offset */
                22,         /* width */
                88         /* height */
            ),
            .tCharSize = {
                .iWidth = 22,
                .iHeight = 22,
            },
            .nCount =  4,  //!< Character count
            .fnGetCharDescriptor = &__A16_font_get_char_descriptor,
            .fnDrawChar = &__font_a16_draw_char,
        },
        .hwCount = 4,
        .hwDefaultCharIndex = 0, /* tBlank */
    },
    //初始化字符映射表
    .tFlower = {
        .chStartCode = {"花"},
        .hwCount = 1,
        .hwOffset = 0,
    },    
    .tLeaf = {
        .chStartCode = {"叶"},
        .hwCount = 1,
        .hwOffset = 1,
    },
    .tWater = {
        .chStartCode = {"水"},
        .hwCount = 1,
        .hwOffset = 2,
    }, 
    .tStar = {
        .chStartCode = {"星"},
        .hwCount = 1,
        .hwOffset = 3,
    },     
};
  • 注意我们的映射表的每一个元素都只有1个字符,所以hwCount都为1,而hwOffset都加1就可以了。

字符映射函数直接用图片拷贝函数就可以,如下


static
IMPL_FONT_DRAW_CHAR(__font_a16_draw_char)
{  
     return arm_2dp_rgb16_tile_copy_with_colour_keying(
          NULL,
          ptileChar,
          ptTile,
          ptRegion,
          GLCD_COLOR_WHITE,
          ARM_2D_CP_MODE_COPY);
}

最后就是字符映射函数了,如下


static
IMPL_FONT_GET_CHAR_DESCRIPTOR(__A16_font_get_char_descriptor)
{
    assert(NULL != ptFont);
    assert(NULL != ptDescriptor);
    assert(NULL != pchCharCode);
    
    arm_2d_user_font_t *ptThis = (arm_2d_user_font_t *)ptFont;
    
    memset(ptDescriptor, 0, sizeof(arm_2d_char_descriptor_t));
    //初始化Descriptor
    ptDescriptor->tileChar.tRegion.tSize = ptFont->tCharSize;
    ptDescriptor->tileChar.ptParent = (arm_2d_tile_t *)&ptFont->tileFont;
    ptDescriptor->tileChar.tInfo.bDerivedResource = true;
    ptDescriptor->chCodeLength = 2;
    //计算字符对应的偏移量
    arm_foreach( arm_2d_char_idx_t, this.tLookUpTable, this.hwCount, ptItem) {
        //在哪个映射表范围区间
        if (    pchCharCode[0] == ptItem->chStartCode[0] 
            &&  pchCharCode[1] == ptItem->chStartCode[1] ) {
            //计算偏移量
            int16_t iOffset = 0;//pchCharCode[0] - ptItem->chStartCode[0];
            //根据偏移量计算Y坐标
            ptDescriptor->tileChar.tRegion.tLocation.iY 
                = (ptItem->hwOffset + iOffset) * ptFont->tCharSize.iHeight;
            return ptDescriptor;
        }
    }
    /* 如果字符不在字库中就显示默认字符 */
    ptDescriptor->tileChar.tRegion.tLocation.iY 
        = this.tLookUpTable[this.hwDefaultCharIndex].hwOffset 
        * ptFont->tCharSize.iHeight;

    return ptDescriptor;
}
  • 注意:第15行ptDescriptor->chCodeLength的值设置为2,因为我们使用的是GB2312汉字编码集(每个字符占两个字节)。
  • 在19~20行,我们简单和chStartCode比较相等就可以了,因为我们的映射表的每一个元素只有一个字符,即(hwCount = 1),所以只需要和chStartCode比较相等就可以了。如果hwCount不为1,大家要记得修改这里哦。

到这里,我们的A16字体就制作完了,使用也很简单,如下

arm_lcd_text_set_font((arm_2d_font_t *)&My_FONT_A16_ONLY);
arm_lcd_text_location(2,0);
arm_lcd_printf("花叶水星");

大家是不是也发现了,直接用彩色RGB通道做字体没问题,但是意义不大,还不如直接贴图。不过我们讲这个的主要目的是让大家加深对字库中字符映射关系的理解,同时我们也实现了自定义汉字字体(GB2312编码集),相信大家也Get到了(* ̄︶ ̄)

其实我们现在不仅可以实现ASCII码和GB2312编码集,而且可以实现像UTF-8的编码集了,因为我们的chStartCode[4]为4个字节,兼容所有的编码集哦。

下面我用自定义的字体实现了一个类似羊了个羊的小游戏,视频演示如下:

image.png

对了,这个游戏还有一个好玩的需要和大家说明一下,

image.png

就是可以一键开启透明度,有了这个功能我们就可以看到图片下面是什么了,这个功能的制作也很简单,就是使用了下面这个api,如下

arm_2d_rgb565_copy_with_opacity(
     ptileChar,   /* source tile address */
     ptTile,   /* target tile address */
     ptRegion,/* region address */     
     100)  ; /*   alpha */ 
  • 第4个参数alpha就是设置透明度的,大家赶快试试吧
  • 当然,你也可以_@羊了个羊官方开发组_赶快使用Arm-2D添加一个一键开启透明度的功能吧(* ̄︶ ̄)

最后,我们在总结一下自定义字体,如下

image.png

好了,到这里我们的自定义字体就讲完了,下期精彩继续......

原文:嵌入式小书虫
作者:FledgingSu 支离苏

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1480
内容数
118
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息