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

使用 GFX 编写 X11 颜色选择器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2022年 4月 20日

MIT

15分钟阅读

viewsIcon

6585

downloadIcon

63

在您的 ESP32 WROVER 或 WROOM 物联网应用中实现一个漂亮的颜色选择器。

ESP32 Color Picker

引言

这篇文章差点没写出来。我匆忙得出结论,认为没人需要物联网拾色器,然后却偶然发现了自己对一个的需求。

最重要的是,这篇文章是一个探索 GFX 最新功能的机会,例如用户级别的动态批处理和 HSV 色彩模型支持。

最终,我决定给予它完整的处理,尽管我开发它的初衷更多是出于好奇,但最终它也获得了自己的文章。

背景

GFX 是一个功能齐全的图形库,用于小型微控制器。它填补了其他产品在面向 ESP32 和 Atmel SAMD 等中型物联网设备方面的空白,提供了 JPG 和 TrueType 等更高级别的功能,这些功能可以让这些功能稍强的 MCU 成功利用。

ESP32 WROVER 是一款小型的 32 位双核 MCU,最高可运行频率为 240MHz。它拥有 512KB 的 SRAM - 其中约 300KB 可供用户使用。此外,它还拥有不少于 4MB 的 PSRAM,通过内部 80MHz SPI 总线连接,每 4 个时钟周期即可获取一次数据,或者差不多,这实际上相当合理。

这对我们的应用来说有点过量了,但它们是比功能较弱的 Arduino 产品更受欢迎的替代品。需要注意的是,这段代码可以轻松移植到 ESP32 WROOM(它缺少额外的 4MB RAM),只需将所需的 TTF 字体嵌入为头文件而不是加载到 PSRAM 中。您可以使用 GFX 附带的 fontgen 工具来创建头文件。另一种选择是使用项目附带的 20+KB 的小型字体文件。它可以加载到 SRAM 中。

GFX 不仅限于 Arduino 框架,但 Arduino 框架提供了更多的设备支持,并且与 ESP-IDF 目前提供的相比,对 TFT 设备具有更快的 SPI 通信速度,这就是为什么我将重点放在使用 Arduino 框架与 ESP32。

我们将通过 VS Code 使用 Platform IO 作为我们的开发 IDE。GFX 也可以在其他一些环境中使用,但有了 Platform IO,就可以即插即用了。

注意:在首次运行此项目之前,请务必上传文件系统镜像(在任务下),以便将文件放入 SPIFFS 的 /data 目录下。

理解这段乱码

实际应用

我提供了一个简短的 Youtube 视频 这里,让您对我们正在构建的内容有更直观的了解。

高层概念

为了使拾色器更易于人类使用,也更易于我们渲染,我们将使用 HSV 色彩模型 而不是 RGB。在颜色绘制方面,例如色调条和渐变,大部分繁重的工作可以通过简单地使用 HSV 而不是 RGB 来完成。

我们将把色调选择器渲染为触摸显示器底部的一条简单的水平条。显示器的大部分区域将包含一个双轴渐变,其中饱和度是 Y 值,亮度是 X 值。

渲染色调条只是一个在从左到右移动时将颜色的色调通道从 0% 增加到 100% 的过程。

为给定的色调值渲染渐变意味着我们必须在沿 Y 轴和 X 轴移动时依次渲染不断增加的饱和度和亮度通道。

获取颜色名称稍微棘手一些,但除了为 140 种不同的命名 X11 颜色准备大量样板代码外,并没有太多令人费解的地方 - 实际过程再简单不过了。我们只需获取颜色,使用调色板的 nearest() 函数匹配最近的调色板颜色,从而获得该颜色的 X11 调色板索引,然后将其输入到名称字符串表中。

提高性能

一如既往,渲染文本是这个小怪物工作的重头戏。对于这些机器来说,True Type 并不是一件容易的事。为了在一定程度上加快速度并保持灵活性,我们的字体存储在 SPIFFS 分区上的 TTF 和 OTF 文件中。与其尝试直接从 SPIFFS 使用它们(这会非常慢),不如在启动时将文件复制到 PSRAM 中。然后,当我们想渲染它时,我们只需从缓冲区中重构字体,这几乎是瞬时的。如果我们使用的是 WROOM,我们就必须将字体文件嵌入为头文件并从那里进行渲染。

另一个可能耗时很多的工作是渲染渐变。在此应用程序中,总共需要渲染 44,800 个像素。要向 ILI9341 绘制一个像素需要 7 次 SPI 事务。一定有更好的办法。

过去我们可以做的一件事是创建一个临时的 320x140 位图,在上面绘制,然后一次性将其写入显示器,这也会奏效,但这需要大量工作。这也意味着您必须有足够的内存可用,如果没有,就无法进行折衷。

借助最新版本的 GFX,您现在可以使用用户级别的批处理。它的作用是允许您设置一个矩形区域。然后,您可以按从上到下、从左到右的顺序向该区域写入像素,而无需为每个像素指定坐标。它不仅会自动为您使用上述位图技术,而且如果内存不足且驱动程序支持,它还会回退到驱动程序级别的批处理,而 ILI9341 恰好支持。听起来很复杂。但使用起来非常简单。我们稍后会讲到。

编写这个混乱的程序

接线

我们将显示器和触摸控制器连接到同一个 SPI 总线。我们使用了 MOSI 引脚 23、MISO 引脚 19 和 SCLK 引脚 18。LCD 的 CS 线是引脚 5。触摸线的 CS 线是引脚 15。LCD 的 DC 线是引脚 2,RST 是 4,BL/LED 是 14。触摸 IRQ 线未连接。

Platformio.ini

在软件方面,让我们从这个项目的 platformio.ini 开始。

[env:esp32-ili9341]
platform = espressif32
board = node32s
framework = arduino
monitor_speed = 115200
upload_speed = 921600
build_unflags=-std=gnu++11
build_flags=-std=gnu++14
            -DBOARD_HAS_PSRAM
            -mfix-esp32-psram-cache-issue
lib_deps = 
    codewitch-honey-crisis/htcw_ili9341
    codewitch-honey-crisis/htcw_xpt2046
lib_ldf_mode = deep

这将为我们准备一个通用的 ESP32 WROVER 开发板,该板连接到 ILI9341 显示屏,并配有 XP2046 触摸控制器。它包括我的 ILI9341 驱动程序,该驱动程序也调用 GFX,以及我编写的触摸驱动程序库。lib_ldf_mode = deep 可防止 Platform IO 对 GFX 所依赖的依赖项感到困惑。

构建标志之所以存在,是因为 GFX 需要 C++14 或更高版本才能编译,而 Arduino 框架环境通常使用 GNU C++11。Node32s 只是一个适用于任何 ESP32(S2/S3/C3 系列除外)的通用开发板。PSRAM 行是启用对 4MB PSRAM 访问所必需的。

Main.cpp

现在我们进入正题。

// Arduino ESP32 headers
#include <Arduino.h>
#include <SPIFFS.h>
// bus framework header
#include <tft_io.hpp>
// driver header
#include <ili9341.hpp>
// touch header
#include <xpt2046.hpp>
// gfx for C++14 header
#include <gfx_cpp14.hpp>
// our x11 stuff
#include "x11_palette.hpp"
#include "x11_names.hpp"
// import the namespace for the drivers
using namespace arduino;
// and for GFX
using namespace gfx;

// both devices share the SPI bus:
#define HOST VSPI

// wiring is as follows for the touch and display
// MOSI 23
// MISO 19
// SCLK 18
// VCC 3.3v
// see below for additional pins:
#define LCD_CS 5
#define LCD_DC 2
#define LCD_RST 4
#define LCD_BL 14

#define TOUCH_CS 15
// you may need to change this to 1 if your screen is upside down
#define LCD_ROTATION 3
// if you don't see any backlight, or any display 
// try changing this to false
#define LCD_BL_HIGH true

// use the default pins for the SPI bus
using bus_t = tft_spi<HOST,LCD_CS>;
// set up the display
using lcd_t = ili9341<LCD_DC,LCD_RST,LCD_BL,bus_t,LCD_ROTATION,LCD_BL_HIGH>;
// set up the touch driver
using touch_t = xpt2046<TOUCH_CS>;

这里的绝大多数内容都是不言自明的,直到最后几行。

GFX 驱动程序通常使用我的 htcw_tft_io 解耦总线库以获得更好的性能,并能够独立于总线的实际性质(例如,是 I2C、SPI 还是并行)。ILI9341 驱动程序也不例外。因此,我们为此声明了一个 TFT SPI 总线类型,通过 tft_spi<> 行传递 HOST 和连接的 LCD(在此情况下为 ILI9341)的 CS 线。

最后,我们可以声明我们的 ILI9341 驱动程序,并附带它所需的各种引脚和设置,所有这些都通过预处理器宏传入。

最后一件事是声明触摸驱动程序,使用它的 CS 线。请注意,我们没有为此驱动程序使用 tft_spi<> 总线声明。并非所有驱动程序都使用 TFT IO 框架,而且在大多数情况下,这仅限于显示器。由于总线框架不控制 CS 线,它会自行处理,因此在声明它时必须将其传入。

您会注意到这段代码大量使用了模板。GFX 相关的东西倾向于以这种方式运行,并且它带来了许多优势。GFX 和驱动程序大量使用“模板实例静态变量”(我不确定这个概念的官方名称),但基本上 GFX 和相关代码依赖于这样一个事实:模板类的静态变量是每个实例独有的,也就是说,如果我声明了两个不同的 ILI9341 设备,因为我的 ESP32 连接了两个,那么它们各自的静态变量将彼此不同,但相对于自身仍然是静态的。正因为如此,您可以使用此设置同时驱动多个任何类型的显示器,而不是像 Adafruit_GFX 和 TFT_eSPI 那样进行传统的操作。

总之,既然我们已经处理了一些样板代码,让我们继续。

// declare the display
lcd_t lcd;
// declare the touch. The touch takes a reference to an SPIClass
// However, devices that share a bus must share the same instance.
// Always retrieving the SPIClass from spi_container ensures the same
// instance for the same host (requires the htcw_tft_io lib)
// Since the touch and the LCD share a bus, we want to use
// the same instance. spi_container<HOST>::instance() retrieves that
// in a cross platform manner.
touch_t touch(spi_container<HOST>::instance());

lcd 声明很简单。传递给 touch 构造函数的表达式可能需要一些解释。注释中有说明,但我会在此重申。令人失望的是,对于给定的“SPI 主机”,不存在跨平台的方法来获取 SPI 实例,即使许多运行 Arduino 框架的设备都有多个主机。此外,其中大多数(如果不是全部)平台都要求共享 SPI 总线的任何设备也共享一个 SPIClass 实例,这就是问题的症结所在。除非您已经知道您的平台,否则很难检索它,即使这样,对于 ESP32 等平台,您仍然需要为每个需要的宿主保留一个全局的 SPIClass 实例。这很麻烦。我的 htcw_tft_io 包含了一个解决方案。spi_container 是一个模板,它以一个数字化的、从零开始的宿主作为参数,并返回一个驱动该宿主的 SPIClass 的单一共享实例。还有一个 i2c_container 模板,作用类似。

在这里,我们使用它来获取 tft_spi<> 声明在内部使用的同一个 SPIClass 实例。

需要注意的是,我通常将我的设备声明为我的代码的全局变量,因为在物理上它们在电路中是全局可访问的,所以我想让驱动程序也遵循这一点。

接下来的两行只是为我们提供了一种方便的方式来访问一些 X11 颜色以及我们将稍后探索的 X11 颜色调色板。

// easy access to the X11 colors for our display
using color_t = color<typename lcd_t::pixel_type>;
// easy access to our X11 palette mapped to 24-bit RGB
using x11_t = x11_palette<rgb_pixel<24>>;

color<> 伪枚举以您指定的任何颜色模型和像素格式呈现 140 种命名的 X11 颜色。如果您想要“旧蕾丝”颜色作为 24 位 RGB 像素,您可以使用 color<rgb_pixel<24>>::old_lace。在这里,我们传递 lcd_t::pixel_type 以使用 ILI9341 使用的相同像素格式(16 位 RGB)。

调色板是包含 140 种颜色的调色板,每种 X11 颜色有一个条目。在这种情况下,调色板将每种 X11 颜色映射到一个具有 RGB 颜色模型的 24 位像素。正如我之前所说,在此应用程序中,我们主要在 HSV 中操作,但计算颜色距离时,使用 RGB 会比使用 HSV 得到更符合预期的结果。

// you can try one of the other fonts if you like.
const char* font_path =  "/Ubuntu.ttf"; 
//"/Telegrama.otf"; // "/Bungee.otf";
const char* font_name = font_path+1;
uint8_t* font_buffer;
size_t font_buffer_len;

我随项目附带了 3 种字体,包括注释掉的两种。您也可以从 fontsquirrel.com 等网站下载更多字体。

总之,这些是我们用于字体的全局变量。它包含文件名、名称(仅文件名,不带前导 /)、PSRAM 中的一个缓冲区用于存储字体,以及缓冲区的长度。

// holds the currently selected hue value
float current_hue;

像素的实数/浮点值始终按 0 到 1 的范围缩放,其中 0 是 0%,1 是 100%。以这种方式处理 HSV 更容易,所以我们这样做。current_hue 值是我们正在选择的 HSV 像素的 H 通道。它通过屏幕最底部的色调条选择。

接下来是 calibrate() 函数,它用于向用户显示校准屏幕,因为这些廉价的触摸显示器在使用前需要校准。此例程可以选择将校准数据写入 SPIFFS,以便您以后加载,而无需每次都进行校准。我们现在来探讨一下。

// calibrates the screen, optionally writing the calibration file to SPIFFS
void calibrate(bool write=true) {
  touch.initialize();
  File file;
  if(write) {
    file = SPIFFS.open("/calibration","wb");
  }
  int16_t values[8];
  uint16_t x,y;
  srect16 sr(0,0,15,15);
  ssize16 ssr(8,8);
  // top left
  lcd.fill(lcd.bounds(),color_t::white);
  // reconstitute our font stream from PSRAM
  const_buffer_stream cbs(font_buffer,font_buffer_len);
  open_font fnt;
  // attempt to open the font (already checked in setup)
  open_font::open(&cbs,&fnt);
  float scale = fnt.scale(30);
  const char* text = "Touch the corners\nas indicated";
  ssize16 fsz = fnt.measure_text({32767,32767},{0,0},text,scale).inflate(2,2);
  srect16 tr = fsz.bounds().center((srect16)lcd.bounds());
  draw::text(lcd,tr,{0,0},text,fnt,scale,color_t::black);

  draw::filled_rectangle(lcd,ssr.bounds().offset(sr.top_left()),color_t::sky_blue);
  draw::filled_ellipse(lcd,sr,color_t::sky_blue);
  while(!touch.calibrate_touch(&x,&y)) delay(1);
  values[0]=x;values[1]=y;
  if(write) {
    file.write((uint8_t*)&x,2);
    file.write((uint8_t*)&y,2);
  }
  lcd.fill((rect16)sr,color_t::white);
  delay(1000); // debounce
...
  touch.calibrate(lcd.dimensions().width,lcd.dimensions().height,values);
  if(write) {
    file.close();
  }
}

为了简洁起见,我省略了一些重复的代码。我们做的第一件事是初始化触摸驱动程序。我们不一定非要这样做,因为它在首次使用时会自动初始化,但我这样做感觉更好。

接下来,如果指定了 write,我们便打开文件。然后,我们声明我们的校准点数组 values,该数组包含每个 x,y 坐标的两个 int16_t 条目,这些坐标以顺时针顺序从左上角开始指定。

之后,我们填充屏幕,从缓冲区获取字体,以 30 像素的字体高度在屏幕中央写下一些说明,然后逐个在每个角落绘制小水滴,等待您触摸它们,记录 touch.calibrate_touch() 检索到的设备点,然后将其擦除并绘制下一个角落,直到每个值都被存储在数组中,并在指定的情况下写入文件。

完成后,我们将 values 数组传递给 touch.calibrate(),以使用这些数据校准屏幕。

水滴只是一个圆圈,其中一个角落有一个正方形,重叠在上面。这真的很简单。

接下来是一个函数,它读取 SPIFFS 中的校准文件(如果存在),并使用这些值校准设备。这是我们之前编写的同一个文件,我们用从文件中读取的数据来校准显示器,而不是提示用户输入。

// read the calibration from SPIFFS
bool read_calibration() {
  if(SPIFFS.exists("/calibration")) {
    File file = SPIFFS.open("/calibration","rb");
    int16_t values[8];
    uint16_t x,y;
    for(int i = 0;i<8;i+=2) {
      if(2!=file.readBytes((char*)&x,2)) { file.close(); return false; }
      if(2!=file.readBytes((char*)&y,2)) { file.close(); return false; }
      values[i]=x;
      values[i+1]=y;
    }
    file.close();
    return touch.calibrate(lcd.dimensions().width,lcd.dimensions().height,values);
  }
  return false;
}

现在我们终于开始处理一些实际的主要应用程序图形了!

// draw a 90deg linear gradient from HSV(0%,100%,100%) to HSV(100%,100%,100%)
void draw_hue_bar(rect16 rect) {
  int w = (float)rect.width()/
          (float)((hsv_pixel<24>::channel_by_name<channel_name::H>::max
                  -hsv_pixel<24>::channel_by_name<channel_name::H>::min)+1);
  if(w==0)
    w=1;

  for(int x = rect.left();x <= rect.right(); ++x) {
    hsv_pixel<24> px(true,(((float)(x-rect.left()))/(rect.width()-1)),1,1);
    draw::filled_rectangle(lcd,srect16(x,rect.top(),x+w-1,rect.bottom()),px);
  }
}

这实际上非常简单。最复杂的部分是获取每个色调值的宽度(w)。对于我们的显示器,w 应该最终为 1。

我们从矩形的左侧循环到右侧。请注意,这不是 x1 到 x2,因为矩形可能水平翻转。对于位置,我们将 x 缩放到 0 到 1 之间的值,然后将其馈送到我们像素的色调通道。请注意,像素的构造函数接受 4 个参数。第一个是一个虚拟的布尔值,当您指定实数时必须传递它。否则,构造函数将期望未缩放的整数值。开头的布尔值可以区分重载。

接下来,我们有绘制实际选定颜色以及旁边最近匹配的 X11 颜色的例程。

// draw the color match bar (exact and nearest x11 color)
void draw_color(hsv_pixel<24> color) {
  draw::filled_rectangle(lcd,srect16(0,140,159,159),color);
  x11_t pal;
  typename x11_t::pixel_type px;
  typename x11_t::mapped_pixel_type cpx;
  convert(color,&cpx);
  pal.nearest(cpx,&px);
  pal.map(px,&cpx);
  draw::filled_rectangle(lcd,srect16(160,140,319,159),cpx);
}

我们在这里所做的是绘制第一种颜色。然后,我们将颜色转换为 RGB,并使用我们之前声明的调色板,将颜色匹配到最接近的调色板颜色,在大多数情况下(包括本例)都使用欧几里德/笛卡尔距离算法来确定哪个像素最接近。我们在 RGB 空间中进行此操作,以避免使用 HSV 进行此操作时出现一些不理想的结果。

绘制颜色名称相对直接。第一部分有点像上面,因为我们将颜色映射到调色板中最接近的 X11 颜色。

// draw the name of the color
void draw_color_name(hsv_pixel<24> color) {
  x11_t pal;
  typename x11_t::pixel_type ipx;
  typename x11_t::mapped_pixel_type cpx;
  convert(color,&cpx);
  pal.nearest(cpx,&ipx);
  const char* name = x11_names[ipx.template channel<0>()];
  // reconstitute our font stream from PSRAM
  const_buffer_stream cbs(font_buffer,font_buffer_len);
  open_font fnt;
  // attempt to open the font (already checked in setup)
  open_font::open(&cbs,&fnt);
  float scale = fnt.scale(30);
  ssize16 fsz = fnt.measure_text({32767,32767},{0,0},name,scale).inflate(2,2);
  srect16 tr = fsz.bounds().center({0,160,319,208});
  draw::filled_rectangle(lcd,srect16(0,160,319,208),color_t::white);
  draw::text(lcd,tr,{0,0},name,fnt,scale,color_t::black);
}

然后,我们从调色板中获取一个索引像素,并将其索引用作查找 140 个条目字符串数组(其中包含颜色名称)的索引。

一旦我们有了它,就只需要从缓冲区中重构字体,测量文本,然后绘制背景,然后绘制文本本身。

现在让我们进入绘制渐变的部分,因为其中有一个重要的技术。

void draw_frame(float hue) {
  // draw a linear gradient on the HSV axis, where h is fixed at "hue"
  // and S and V are along the Y and X axes, respectively
  hsv_pixel<24> px(true,hue,1,1);
  auto px2 = px;
  // batching is the fastest way
  auto b = draw::batch(lcd,srect16(0,0,319,139));
  for(int y = 0;y<140;++y) {
    px2.template channelr<channel_name::S>(((double)y)/139.0);
    for(int x = 0;x<320;++x) {
      px2.template channelr<channel_name::V>(((double)x)/319.0);
      b.write(px2);
    }
  }
  // commit what we wrote
  b.commit();
  // draw the color bar
  draw_color(px);
  // draw the color name
  draw_color_name(px);
}

我们在这里主要做的是绘制渐变。我们首先创建一个指定色调的 HSV 像素。然后,我们使用 draw::batch<>() 为批处理写入做准备,并为其提供目标矩形。

当我们沿着 Y 轴和 X 轴移动时,我们调整 px 的 S 和 V 通道,每次都将其写入批处理。

完成后,我们提交批处理,然后再绘制颜色条和名称。

使用批处理通常比仅使用 draw::point<>() 快几个数量级。速度有多快取决于显示控制器最终的能力以及您有多少可用 SRAM。

现在,该我们使用老式的 Arduino setup() 函数了。

void setup() {
  Serial.begin(115200);
  SPIFFS.begin(true);
  lcd.initialize();
  touch.initialize();

  File file = SPIFFS.open(font_path,"rb");
  if(!file)  {
    Serial.printf("Asset %s not found. Halting.",font_name);
    while(true) delay(1000);
  }
  // get the file length
  file.seek(0,fs::SeekMode::SeekEnd);
  size_t len = file.position();
  file.seek(0);
  if(len==0) {
    Serial.printf("Asset %s not found. Halting.",font_name);
    while(true) delay(1000);
  }
  // allocate the buffer
  font_buffer = (uint8_t*)ps_malloc(len);
  if(!font_buffer)  {
    Serial.printf("Unable to allocate PSRAM for asset %s. Halting.",font_name);
    while(true) delay(1000);
  }
  // copy the file into the buffer
  file.readBytes((char*)font_buffer,len);
  // don't need the file anymore
  file.close();
  font_buffer_len = len;
  // test the font to make sure it's good (avoiding checks later)
  // first wrap the buffer w/ a stream
  const_buffer_stream cbs(font_buffer,font_buffer_len);
  open_font fnt;
  // attempt to open the font
  gfx_result r=open_font::open(&cbs,&fnt);
  if(r!=gfx_result::success) {
    Serial.printf("Unable to load asset %s. Halting.",font_name);
    while(true) delay(1000);
  }

  if(!read_calibration() || !touch.calibrated()) {
    calibrate(true);
  }
  current_hue = 0;
  // draw the hue bar at the bottom
  draw_hue_bar({0,210,319,239});
  // draw the initial frame 
  draw_frame(0);
}

细心的读者可能会注意到,在某些情况下,触摸可能会被初始化多次。这不会有什么坏处,因为如果已经初始化,它就不会再次初始化。这些初始化也不是必需的,但它们在我测试时帮助了我,所以我把它们留下了。我所有的设备驱动程序都会在首次使用时自动初始化,除非初始化有任何不可能性。

例程的大部分代码是将字体复制到 PSRAM 并加载以确保其有效。

之后,如有必要,我们进行校准,然后设置为当前色调,绘制色调条,然后绘制带有渐变的边框。

现在,loop() 函数。

void loop() {
  uint16_t x=0,y=0;
  // touched?
  if(touch.calibrated_xy(&x,&y)) {
    // hue bar?
    if(y>=210) {
      current_hue = ((double)x)/319.0;
      draw_frame(current_hue);
    } else if(y<140) { // gradient area
      double s = ((double)y)/139.0;
      double v = ((double)x)/319.0;
      // get our HSV pixel
      hsv_pixel<24> px(true,current_hue,s,v);
      // update the screen with it
      draw_color(px);
      draw_color_name(px);
    }
  }
}

这个例程几乎是微不足道的。我们只是轮询触摸事件,如果有事件,我们就确定触摸的位置。如果小于 140,则是渐变区域,如果大于或等于 210,则是色调条。在第一种情况下,我们重新计算颜色并重绘屏幕的该部分。在后一种情况下,我们必须重新计算色调并重绘整个边框。

x11_palette.hpp

好吧,我作弊了。我使用我在 C# 中编写的一个工具生成了大部分这个文件以及名称头文件,该工具通过抓取 System.Drawing.Color 来获取所有 X11 颜色和名称。调色板提供了两个函数。一个函数仅根据索引像素返回颜色。该例程非常庞大且是自动生成的。

另一个例程只是用于查找最近颜色的样板代码。它比较调色板中每种颜色与比较值的距离,并找到最接近的一个。

结论

就是这样!希望这能给您一些关于如何在自己的项目中使用 GFX 的想法。请记住,您可以在 https://honeythecodewitch.com/gfx 上了解最新的文档和源代码。

历史

  • 2022年4月20日 - 初始提交
© . All rights reserved.