65.9K
CodeProject 正在变化。 阅读更多。
Home

GFX 第三部分:绘图图元

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2021 年 4 月 25 日

MIT

14分钟阅读

viewsIcon

5939

downloadIcon

113

探索 GFX IoT 库提供的基本绘图功能。

gfx 3

引言

基于前几篇文章,我们现在开始在之前探讨的位图上进行绘制。使用此库的绘图功能,我们可以绘制各种颜色、各种类型的图形,并将它们裁剪到我们定义的矩形窗口内。

更新:现在将使用 C++14 进行构建,这使得在某些框架和工具链中使用起来更加方便。

构建这个大杂烩

此代码使用 clang 11 和 gcc 10,并以 -std=c++17-std=c++14 进行测试。

由于 C++ 没有定义表示 C++17 标准的 __cplusplus 宏值,因此有两个头文件:gfx_color.hppgfx_color_cpp14.hpp。前者适用于 C++17 及更高版本。后者适用于 C++14。它们对 color<> 模板的定义不同,因为这两个标准在这方面不兼容。使用方法相同,但声明代码因标准而异。包含相应的头文件即可访问 color<> 模板。

MSVC 的最新版本无法编译此代码。坦白说,我不在乎,因为我遇到的任何东西都无法与 MSVC 交叉编译,而此代码的主要目的是用于无法轻易使用 MSVC 定位的 IoT 设备。此代码中没有特定于 Linux 的内容,我相信它只是符合标准的 C++,但也可能是我在不知情的情况下使用了一些 gcc 和 clang 特有的功能。

要构建的主文件是 /reference 目录下的 demo.cpp

/tools 目录下有 fontgen.cpp,可用于生成字体。

使用这个烂摊子

绘图图元非常简单。您可以直接在 bitmap<> 模板类型或任何具有兼容方法的类型上绘制。一切的核心是 draw 类,它公开了几个执行基本绘图操作的模板方法。

  • point<>() - 在指定位置、使用指定颜色绘制一个点,并可选择使用指定的剪裁矩形。
  • line<>() - 从 rect 的 (x1,y1) 绘制一条线到 (x2,y2),使用指定的颜色,并可选择使用指定的剪裁矩形。
  • rectangle<>() - 使用指定的边界、指定的颜色绘制一个矩形,并可选择使用指定的剪裁矩形。
  • filled_rectangle<>() - 使用指定的边界、指定的颜色绘制一个填充矩形,并可选择使用指定的剪裁矩形。
  • ellipse<>() - 使用指定的边界、指定的颜色绘制一个椭圆,并可选择使用指定的剪裁矩形。
  • filled_ellipse<>() - 使用指定的边界、指定的颜色绘制一个填充椭圆,并可选择使用指定的剪裁矩形。
  • arc<>() - 使用指定的边界绘制一个 90 度圆弧,边界也指示方向,使用指定的颜色,并可选择使用指定的剪裁矩形。
  • filled_arc<>() - 使用指定的边界绘制一个填充的 90 度圆弧,边界也指示方向,使用指定的颜色,并可选择使用指定的剪裁矩形。
  • rounded_rectangle<>() - 使用指定的边界、指定的 x 和 y 角比率,使用指定的颜色绘制一个带圆角的矩形,并可选择使用指定的剪裁矩形。
  • filled_rounded_rectangle<>() - 使用指定的边界、指定的 x 和 y 角比率,使用指定的颜色绘制一个填充的带圆角的矩形,并可选择使用指定的剪裁矩形。
  • bitmap<>() - 将位图的选定部分绘制到目标,使用指定的 d 目标边界、源位图(或兼容类型)、源矩形、选项和剪裁矩形。内部有一个“快速模式”,在可能的情况下使用原始 blt,这取决于复制选项。
  • text<>() - 使用指定的边界、字体、颜色和可选的背景色绘制文本,以及一个可选的剪裁矩形。

矩形

我将再次讨论矩形,因为它们对这些绘图操作至关重要。rectx<> 模板允许您声明任何数值元素类型的矩形,但通常您会为有符号版本使用 rect16srect16

大多数绘图操作都接收矩形。即使是 line<>() 也要用矩形来指定线段的端点。这样做的原因是因为矩形公开了一个广泛的接口来操作它们。因此,接收一个矩形而不是两个端点更具灵活性,因为您可以执行诸如 flip_horizontal() 矩形的操作,从而改变其方向。让我们谈谈为什么您可能想执行像翻转矩形这样的操作。

在绘制线段时,您可以轻松地绘制一条线,沿着任一轴翻转矩形,然后使用新的翻转矩形再次绘制,以制作一个“X”。

在绘制圆弧或位图时,矩形的方向决定了绘图操作的方向。例如,如果一个矩形垂直翻转,位图将倒置显示。圆弧也是如此。当然,水平翻转也具有相同的属性。

裁剪

最初,我计划了一个方案,您将创建一个可以包含其他画布的画布,将所有绘图剪裁到画布,并将子画布剪裁到它们的父级(如果存在)。问题在于这些设备确实没有太多 RAM,虽然每个画布占用的内存不多,但如果有大量的画布,累加起来会很多。此外,剪裁也需要一些 CPU 时间。除此之外,这种设施对于复杂的动态和可组合布局来说更有用,就像在 HTML 或 WPF 中找到的那样。这对于 IoT 设备来说不太合适。

话虽如此,即使我们不使用上述画布组合,一些剪裁功能仍然非常有益。我选择的折衷方案是允许为每个绘图图元指定一个剪裁矩形。结合 rectx<>crop() 等功能,您可以根据需要轻松创建绘图区域(画布)的组合。每个绘图操作的最后一个参数称为 clip,它是一个指向定义剪裁矩形的 srect16 结构的指针,或者为 nullptr 表示不剪裁。

位图

bitmap<>() 方法包含了相当多的功能。您可以绘制位图的全部或部分,同时可选地进行拉伸、缩小、翻转以及/或转换为不同的像素格式,所有这些都在一个方法中完成。需要注意的是,利用翻转、转换和调整大小功能比不需要它们时要慢得多。目前调整大小使用的是最近邻算法,但我打算在将来添加(更慢的)双线性插值以减少“锯齿状”的调整大小。本质上,此绘图操作将在可能的情况下使用 bitmap<>blt() 方法。否则,它必须逐像素绘制位图。请注意,目标矩形的方向决定了位图的方向,因此通过翻转该矩形,您可以翻转位图,就像通过调整其大小一样,您可以根据 options 拉伸或裁剪位图。

文本和字体

这是最大的部分。事实上,这几乎是整篇文章。绘制文本会引出很多问题。首先,您需要字体。

一旦有了字体,实际的绘制就变得很简单。请注意,常见的空格字符(如制表符和换行符)都会被识别,文本会换行,但不会按单词换行。

这就剩下字体了。font 类有很多成员。在讨论它们之前,我们先退一步,谈谈如何获取字体。

此库允许您导入 .FON 文件,这些文件是非常老的字体文件,通常与 Windows 3.1 一起使用。在我放弃 TrueType 和其他更现代的选项后,我选择此格式有几个原因。原因是我受限于点阵字体,除非我想大幅增加代码大小——我不想。TTF 字体和所有这些的问题是,即使它们是点阵的,文件格式也更复杂,甚至点阵字体也有很多功能,如字偶间距和高级字符间距,而 IoT 认为这些功能不太需要或没有处理能力。FON 文件最初是为 16 位系统设计的,这使其非常适合我们的用途。它们可能很难找到,但我已经包含了用于测试的来自这里的一些文件以及来自这里的一个文件。

版权归功:Simon Tatham 编写了我在源代码中包含的两个 Python 脚本,用于将 .FON 文件转换为文本文件以及从文本文件转换回来。我从他编写的 Python 代码中借鉴了一些字体加载代码,尽管现在已经不太容易辨认了,因为它们依赖的底层机制完全不同。例如,脚本处理的是一个数组,而我处理的是一个流,我存储字体数据的方式也不同,但他的代码为我提供了解析这些文件的基础——这些信息在其他地方很难找到,因此他应该为此获得赞誉。他的网站在这里:这里

免责声明:字体在 32 位和 64 位系统上最多为 64 像素宽,在 8 位和 16 位系统上最多为 32 像素宽。字体作为资源嵌入到 Windows 可执行文件中,这些文件只是一个带有资源的存根。它们基本上是扩展名已重命名的 16 位 .exe 文件。它们有时也可能以 32 位可执行格式而不是较旧的 16 位格式嵌入。我还没有实现 32 位格式,仅仅是因为我还没有找到使用它的字体文件,所以无法进行测试。因此,此代码将无法加载这些字体。此代码应支持可变宽度点阵字体,我已尽力确保这一点,但有一些绘图代码我迫切需要测试,而且我还没有可变宽度字体可以尝试。我还需要在大端系统上进行测试。由于 Microsoft 决定以这种方式分发嵌入在可执行文件中的字体,因此杀毒软件可能会将其标记为病毒。我分发的任何字体中都没有病毒。这些字体文件中没有有意义的可执行代码,只有存根。您仍然需要重命名它们才能尝试运行它们,并且您必须在 Windows 3.1 上这样做,因为它们在您的系统上无法运行。我发誓,它们只是字体。

检索字体

在您做其他任何事情之前,您都需要获取一些字体数据。根据您的需求以及目标系统的限制,有两种方法可以实现这一点。第一种方法是从可查找、可读的流(如 io::file_stream)加载 .FON 文件。第二种方法是将字体数据直接嵌入到您的源代码中。前者在某些方面更灵活。您可以随时从多种不同来源加载不同的字体,但这需要堆内存才能这样使用。后者在支持“PROGMEM”的系统上不需要堆内存,它将静态数据嵌入闪存而不是加载到 RAM 中。数据访问速度稍慢,但 RAM 的节省可能至关重要,尤其是在最低端的设备上。使用后一种方法,字体数据将被直接编译到您的二进制文件中。要生成字体数据,您可以使用附带的 fontgen.cpp 工具,并将字体文件名作为参数传递给它。您可能需要稍微修改输出——即更改 #include <gfx_font.hpp> 行以反映您的环境,并在使用 PROGMEM 和 Arduino 框架时可能需要添加 #include <Arduino.h>。它创建的字体被声明为 static const,其名称派生自文件名。

字体信息

font 类提供了有关它所代表的字体的大量信息。

这里的大部分数据您可能不需要,但其中一些非常关键。

  • height() - 表示字体的像素高度。
  • resolution() - 表示每英寸的水平和垂直分辨率(dpi)。
  • external_leading() - 表示由 height() 定义的边界内的行距,以像素为单位。重音符号可能会出现在该区域。可能为零。
  • external_leading() - 表示字体行之间的像素数。目前未使用。可能为零。(译者注:此条目与上一条目重复,且英文描述不同,可能为原文错误或有不同含义。)
  • ascent() - 表示字体的基线。
  • point_size() - 表示字体的磅值大小。
  • style() - 表示指定 italic(斜体)、underline(下划线)和/或 strikeout(删除线)的 3 位字段。
  • weight() - 表示字体的粗细。默认值为 400,粗体字体可能更高。
  • first_char() - 表示字体所代表的第一个字符。
  • last_char() - 表示字体所代表的最后一个字符。
  • default_char() - 表示在无法映射输入时要映射的字符。
  • break_char() - 表示用于单词分隔的字符。目前未使用,即使实现了单词分隔,可能也不会使用。
  • charset() - 表示字符集代码。
  • average_width() - 表示此字体中字符的平均宽度。用于计算制表符间距。
  • width() - 表示给定字符的宽度。
  • operator[]() - 检索给定字符的 width()data()
  • read() - 从流中读取字体,给定字体集中的可选字体索引、第一个字符提示和最后一个字符提示。您也可以使用构造函数,但此方法返回一个可以检查的错误结果。构造函数则不能。
  • measure_text() - 测量当给定文本在指定尺寸内布局时,将包围该文本的边界框的有效尺寸。基本上,您传递一些文本和一些尺寸。尺寸代表您的文本可以使用的整个可用空间。文本将在该空间中布局,并在必要时换行以适应,然后返回包围文本的边界框的尺寸。返回的尺寸将等于或小于传入的尺寸。这对于执行诸如文本居中之类的操作非常有用。

演示

让我们来看一些实际代码。以下代码已省略其嵌入的字体数据,因为它很长。

#define HTCW_LITTLE_ENDIAN
#include <stdio.h>
#include <string.h>
#include "../src/gfx_bitmap.hpp"
#include "../src/gfx_drawing.hpp"
#include "../src/gfx_color.hpp"

using namespace gfx;

#ifndef PROGMEM
        #define PROGMEM
#endif

// embedded terminal font data
// generated with the fontgen tool.
static const uint8_t terminal_fon_char_data[] PROGMEM = {
        0x06, 0x00, 0x00, 0x00, ...
        };

static const ::gfx::font terminal_fon(
        13,
        6,
        12,
        11,
        ::gfx::point16(100, 100),
        '\0',
        '\xFF',
        '\0',
        ' ',
        { 0, 0, 0 },
        400,
        255,
        0,
        0,
        terminal_fon_char_data);

// prints a bitmap as 4-bit grayscale ASCII
template <typename BitmapType>
void dump_bitmap(const BitmapType& bmp) {
    static const char *col_table = " .,-~;*+!=1%O@$#";
    using gsc4 = pixel<channel_traits<channel_name::L,4>>;
    for(int y = 0;y<bmp.dimensions().height;++y) {
        for(int x = 0;x<bmp.dimensions().width;++x) {
            const typename BitmapType::pixel_type px = bmp[point16(x,y)];
            const auto px2 = px.template convert<gsc4>();
            size_t i =px2.template channel<0>();
            printf("%c",col_table[i]);
        }
        printf("\r\n");
    }
}

int main(int argc, char** argv) {

    // use whatever bit depth you like.
    // the only difference really is that
    // it might fudge the colors a bit
    // if you go down really low
    // and that can change the ascii
    // output, although with what
    // we draw below it can be as
    // low as 3 with what we draw
    // below without impacting
    // anything. Byte aligned things
    // are quicker, so multiples of 8
    // are good.
    static const size_t bit_depth = 24;

    // our type definitions
    // this makes things easier
    using bmp_type = bitmap<rgb_pixel<bit_depth>>;
    using color = color<typename bmp_type::pixel_type>;

    static const size16 bmp_size(32,32);

    // declare the bitmap
    uint8_t bmp_buf[bmp_type::sizeof_buffer(bmp_size)];
    bmp_type bmp(bmp_size,bmp_buf);

    // draw stuff
    bmp.clear(bmp.bounds()); // comment this out and check out the uninitialized RAM.
                             // It looks neat.

    // bounding info for the face
    srect16 bounds(0,0,bmp_size.width-1,(bmp_size.height-1)/(4/3.0));
    rect16 ubounds(0,0,bounds.x2,bounds.y2);

    // draw the face
    draw::filled_ellipse(bmp,bounds,color::yellow);

    // draw the left eye
    srect16 eye_bounds_left(spoint16(bounds.width()/5,bounds.height()/5),
                            ssize16(bounds.width()/5,bounds.height()/3));
    draw::filled_ellipse(bmp,eye_bounds_left,color::black);

    // draw the right eye
    srect16 eye_bounds_right(
        spoint16(
            bmp_size.width-eye_bounds_left.x1-eye_bounds_left.width(),
            eye_bounds_left.y1
        ),eye_bounds_left.dimensions());
    draw::filled_ellipse(bmp,eye_bounds_right,color::black);

    // draw the mouth
    srect16 mouth_bounds=bounds.inflate(-bounds.width()/7,-bounds.height()/8).normalize();
    // we need to clip part of the circle we'll be drawing
    srect16 mouth_clip(mouth_bounds.x1,mouth_bounds.y1+
            mouth_bounds.height()/(float)1.6,mouth_bounds.x2,mouth_bounds.y2);
    draw::ellipse(bmp,mouth_bounds,color::black,&mouth_clip);

    // now blt the bitmaps
    const size_t count  =3; // 3 faces
    // our second bitmap. Not strictly necessary because one
    // can draw to the same bitmap they copy from
    // but doing so can cause garbage if the regions
    // overlap. This way is safer but takes more RAM
    using bmp2_type = bitmap<typename bmp_type::pixel_type>;
    static const size16 bmp2_size(128,64);
    uint8_t buf2[bmp2_type::sizeof_buffer(bmp2_size)];
    bmp2_type bmp2(bmp2_size,buf2);

    // if we don't do the following, we'll get uninitialized garbage.
    // it looks neat though:
    bmp2.clear(bmp2.bounds());
    srect16 r = bounds;
    // how much to shrink each iteration:
    spoint16 shrink(-bounds.width()/8,-bounds.height()/8);
    for(size_t i = 0;i<count;++i) {
        // draw the bitmap
        draw::bitmap(bmp2,r,bmp,ubounds,bitmap_flags::resize);
        // move the rect, flip and shrink it
        r=r.offset(r.width(),0);
        r=r.inflate(shrink.x,shrink.y);
        // rect orientation dictates bitmap orientation
        r=r.flip_vertical();
    }
    // we can load fonts from a file, but that requires heap
    // while PROGMEM arrays do not (at least on devices
    // that use flash memory)
    // io::file_stream dynfs("./fonts/Bm437_ToshibaSat_8x8.FON");
    // font dynf(&dynfs);
    // store the rightmost extent
    // so we have something to
    // center by
    int er = r.right();
    // now let's draw some text
    // choose the font you want to use below:
    const font& f =
        terminal_fon // use embedded font
        // dynf // use dynamic font
    ;

    const char* str = "Have a nice day!";
    // create the bounding rectangle for our
    // proposed text
    r=srect16(0,bounds.height(),er+1,bmp2_size.height);
    // now "shrink" the bounding rectangle to our
    // actual text size:
    r=srect16(r.top_left(),f.measure_text(r.dimensions(),str));
    // center the bounding rect:
    int16_t s = (er+1)-r.width();
    if(0>s)
        s=0;
    r=r.offset(s/2,0);

    // now draw
    draw::text(bmp2,r,str,f,color::white);

    // display our mess
    dump_bitmap(bmp2);
    return 0;
}

我们首先定义字节序。它默认为小端字节序,但我喜欢明确声明,因为在交叉编译时,这是一个很好的设置提醒。然后我们包含一些内容,特别是 gfx_bitmap.hppgfx_drawing.hpp。您可能认为绘图代码会依赖位图代码,但事实并非如此。draw 对位图没有特殊了解,它会使用任何公开类似成员的类型。接下来,我们添加 using 关键字来使用 gfx 命名空间,以节省输入。

现在我们有一个 PROGMEM 哨兵,基本上是为了在当前环境不支持 PROGMEM 属性时抑制编译器警告。

接下来有一个非常大的数组,我在上面的文本中省略了大部分内容。那是我们的字体数据,它是通过在命令行上使用 ./fontgen ./fonts/terminal.fon 0 生成的。Fontgen 通过编译 fontgen.cpp 并运行它来使用。之后是一个 font 声明,它由相同的工具生成。这是我们默认使用的字体,尽管您可以加载其他字体,或运行 fontgen 来为它们创建嵌入式 C++ 代码。

之后,有一个 dump_bitmap<>() 函数,它将位图渲染为 4 位灰度,然后映射到 ASCII 并打印到控制台。这样我们就不必编写实际的设备驱动程序或使用其构建周期较长的 IoT 设备。我保证下一部分我们将渲染到实际的图形显示器。

在我们的 main() 例程中,首先,我们有一些 consts 和一些类型别名来设置我们的位图和像素“形状”。

在此之后,我们使用一个字节缓冲区初始化位图,该缓冲区的尺寸已经计算完毕。

接下来是清除位图,尽管如果您不这样做,输出看起来会有点像有趣的、类似《黑客帝国》的效果,因为未初始化的内存被映射到了像素数据。

现在我们进行一系列的矩形操作和椭圆绘制来绘制一个笑脸。唯一有点奇怪的地方是如何绘制嘴巴。基本上,我们使用剪裁矩形覆盖一个椭圆,这样我们只绘制了它的大约下半部分。

在我们将笑脸绘制到初始位图后,我们就开始做一些“猴子戏”。我们声明一个第二、更大的位图,在其中我们将刚刚为笑脸制作的位图进行 blt,然后缩小它,垂直翻转它,向右移动它,然后再 blt 一次,我们重复这样做一次。

之后,我们测量文本,然后通过一些矩形操作将其居中,然后在绘制到目标。

您的结果应该如下所示:

           @@@@@@@@@@
        @@@@@@@@@@@@@@@@
      @@@@@@@@@@@@@@@@@@@@
     @@@@@@@@@@@@@@@@@@@@@@               @@@@@@@@@@@@
   @@@@    @@@@@@@@@@    @@@@           @@@@@@@@@@@@@@@
  @@@@      @@@@@@@@      @@@@          @@@@@      @@@@@
  @@@@      @@@@@@@@      @@@@        @@@@@  @@@@@@  @@@@@          @@@@@@@@
 @@@@@      @@@@@@@@      @@@@@      @@@  @@@@@@@@@@@@ @@@         @@@@@@@@@@@
 @@@@@      @@@@@@@@      @@@@@      @@@@@@@@@@@@@@@@@@ @@@      @@   @@@@   @@
@@@@@@      @@@@@@@@      @@@@@@     @@@@@@@@@@@@@@@@@@ @@@      @@   @@@@   @@@
@@@@@@      @@@@@@@@      @@@@@@    @@@@@@@@@@@@@@@@@@@@@@@     @@@   @@@@   @@@
@@@@@@@    @@@@@@@@@@    @@@@@@@    @@@@@@@@@@@@@@@@@@@@@@@     @@@@  @@@@@  @@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@@   @@@@@@@@   @@@@     @@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@     @@@@@@    @@@@      @@@@@@@@@@@@ @@
@@@@ @@@@@@@@@@@@@@@@@@@@@@ @@@@     @@@     @@@@@@    @@@@      @@ @@@@@@@@ @@
 @@@@ @@@@@@@@@@@@@@@@@@@@ @@@@      @@@     @@@@@@    @@@@       @@@ @@@@  @@@
 @@@@ @@@@@@@@@@@@@@@@@@@@ @@@@      @@@     @@@@@@    @@@         @@@@@@@@@@
  @@@@  @@@@@@@@@@@@@@@@  @@@@        @@@   @@@@@@@@   @@@          @@@@@@@@
  @@@@@@ @@@@@@@@@@@@@@ @@@@@@          @@@@@@@@@@@@@@@@
   @@@@@@   @@@@@@@@   @@@@@@             @@@@@@@@@@@@
     @@@@@@@        @@@@@@@                 @@@@@@@@
      @@@@@@@@@@@@@@@@@@@@
        @@@@@@@@@@@@@@@@
           @@@@@@@@@@


#   #                                                                       #               #
#   #                                             #                         #               #
#   #                                                                       #               #
#   #  ###  #   #  ###         ###        # ##   ##    ###   ###         ####  ###  #   #   #
#####     # #   # #   #           #       ##  #   #   #   # #   #       #   #     # #   #   #
#   #  #### #   # #####        ####       #   #   #   #     #####       #   #  #### #   #   #
#   # #   #  # #  #           #   #       #   #   #   #     #           #   # #   # #  ##   #
#   # #  ##  # #  #   #       #  ##       #   #   #   #   # #   #       #   # #  ##  ## #
#   #  ## #   #    ###         ## #       #   #  ###   ###   ###         ####  ## #     #   #
                                                                                    #   #
                                                                                     ###      

我真心希望您能做到。

下一步

在下一部分中,我们终于要介绍一些实际的显示驱动程序,这些驱动程序使我们能够直接在 TFT、LED 和 OLED 屏幕上绘图,而不仅仅是在位图和 ASCII 上。

历史

  • 2021 年 4 月 25 日 - 首次提交
  • 2021 年 4 月 25 日 - 使其与 C++14 兼容
© . All rights reserved.