--[[ @module ws2812_fonts @summary WS2812 演示使用的字模数据与文本条带构建(中文 22×22 / 英文 22×11 / ASCII 5×7) @version 1.0 @date 2026.08.17 @usage 适用产品:合宙 Air1780P / Air1780H / Air1780HV。 字模尺寸与灯板 22×22 强绑定,灯板尺寸非 22×22 时需重新生成字模。 本模块由 ws2812_scroll_task 调用,提供: WS2812_FONT5x7 5×7 ASCII 小字模(列优先,bit0=顶行),放大 2× 后渲染 WS2812_FONT_CN 中文 22×22 字模(欢迎使用),行优先字符串 WS2812_FONT_EN22 英文半宽 22×11 大字模(LuatOS),行优先字符串 ws2812_build_strip(text) 将 UTF-8 文本打包为列掩码数组 最终输出"列掩码数组" columns:每个元素是一个 22-bit 整数, 第 y 位为 1 表示该列第 y 行的 LED 点亮。scroll_task 按列偏移遍历即可渲染。 ]] -- ====== 5×7 ASCII 字模(列优先,bit0=顶行) ====== -- 格式说明: -- 每个字符是一个含 5 个字节的数组,分别对应 5 列; -- 每个字节的 bit0~bit6 对应该列自顶向下的 7 行, -- 即 bit0=最顶行、bit6=最底行,置 1 表示该像素点亮。 -- 渲染时会通过 expand_small_col 将 7 行放大为 14 行(每行翻倍), -- 再垂直居中下移 EN_VOFFSET_SMALL 排,最终得到约 14 像素高、10 像素宽的字符。 -- 键名使用 ASCII 码(如 65 = 'A')。 local WS2812_FONT5x7 = { [65] = {0x7E,0x09,0x09,0x09,0x7E}, -- A [66] = {0x7F,0x49,0x49,0x49,0x36}, -- B [67] = {0x3E,0x41,0x41,0x41,0x22}, -- C [68] = {0x7F,0x41,0x41,0x22,0x1C}, -- D [69] = {0x7F,0x49,0x49,0x49,0x41}, -- E [70] = {0x7F,0x09,0x09,0x09,0x01}, -- F [71] = {0x3E,0x49,0x49,0x49,0x7A}, -- G [72] = {0x7F,0x08,0x08,0x08,0x7F}, -- H [73] = {0x00,0x41,0x7F,0x41,0x00}, -- I [74] = {0x20,0x41,0x41,0x3F,0x00}, -- J [75] = {0x7F,0x08,0x14,0x22,0x41}, -- K [76] = {0x7F,0x40,0x40,0x40,0x40}, -- L [77] = {0x7F,0x02,0x04,0x02,0x7F}, -- M [78] = {0x7F,0x04,0x08,0x10,0x7F}, -- N [79] = {0x3E,0x41,0x41,0x41,0x3E}, -- O [80] = {0x7F,0x09,0x09,0x09,0x06}, -- P [81] = {0x3E,0x41,0x51,0x21,0x5E}, -- Q [82] = {0x7F,0x09,0x19,0x29,0x46}, -- R [83] = {0x46,0x49,0x49,0x49,0x31}, -- S [84] = {0x01,0x01,0x7F,0x01,0x01}, -- T [85] = {0x3F,0x40,0x40,0x40,0x3F}, -- U [86] = {0x1F,0x20,0x40,0x20,0x1F}, -- V [87] = {0x3F,0x40,0x38,0x40,0x3F}, -- W [88] = {0x63,0x14,0x08,0x14,0x63}, -- X [89] = {0x07,0x08,0x70,0x08,0x07}, -- Y [90] = {0x71,0x69,0x55,0x4D,0x47}, -- Z [97] = {0x78,0x14,0x14,0x14,0x78}, -- a [116] = {0x04,0x04,0x7F,0x44,0x44}, -- t [117] = {0x3C,0x40,0x40,0x40,0x7C}, -- u } -- ====== 中文 22×22 字模(行优先字符串,# = 亮,. = 灭) ====== -- 格式说明: -- 每个字符由 22 个字符串组成,每个字符串固定 22 个字符宽; -- 字符串数组顺序是行序(第 1 个字符串 = 最顶行), -- 字符串内从左到右是列序,'#' 表示该像素亮、'.' 表示灭。 -- 渲染时按列扫描:col_mask(glyph, col, voffset) 对每一行取该列字符, -- 为 '#' 就把对应行位置 1,最终拼成一个 22-bit 的列掩码。 -- 键名是 UTF-8 中文字符串(占 3 字节),build_strip 中用 string.sub 取出 3 字节作为键。 local WS2812_FONT_CN = { ["欢"] = { "......................", "............##........", "...........###........", "...........##.........", "..#######..##.........", "..#######..#########..", ".......##.##########..", ".......##.##......##..", "..###..#..##.##..##...", "...##.##.##..##.......", "....####.....##.......", "....###......##.......", ".....###.....###......", "....####.....###......", "....#####...####......", "...###.##...##.##.....", "...##...##.###.###....", "..##......###....###..", ".###.....###.....####.", "........###.......###.", "........##............", "......................", }, ["迎"] = { "......................", "...#.......#..........", "..##....############..", "..###..###...#######..", "...##..##....##...##..", "...##..##....##...##..", ".......##....##...##..", ".......##....##...##..", ".####..##....##...##..", ".####..##....##...##..", "...##..##....##...##..", "...##..##....##...##..", "...##..##....##...##..", "...##..##..#.##...##..", "...##..#####.##..###..", "...##..####..##.###...", "...##..##....##.......", "...##........##.......", "...###.......##.......", "..#######.............", ".###..###############.", ".##.......###########.", }, ["使"] = { "......................", ".....#......##........", "....##......##........", "....##......#.........", "....##.##############.", "...##...############..", "...##........#........", "..###...############..", "..###...############..", ".####...#....#....##..", ".##.#...#....#....##..", "....#...############..", "....#...###########...", "....#...##..##........", "....#...###.##........", "....#....#####........", "....#.....####........", "....#......###........", "....#....########.....", "....#...###...#######.", "....#..###.......###..", "......................", }, ["用"] = { "......................", "......................", "...################...", "...################...", "...##.....##.....##...", "...##.....##.....##...", "...##.....##.....##...", "...################...", "...################...", "...##.....##.....##...", "...##.....##.....##...", "...##.....##.....##...", "...################...", "...################...", "...##.....##.....##...", "...##.....##.....##...", "...##.....##.....##...", "..##......##.....##...", "..##......##....###...", ".###......##...####...", "..#...................", "......................", }, } -- ====== 英文半宽大字模 22 行 × 11 列(行优先字符串) ====== -- 格式说明: -- 每个字符由 22 个字符串组成,每个字符串宽 11 字符(半宽,约为中文的一半); -- 行序、'#'/'.' 含义与中文字模一致。 -- 渲染时通过 col_mask(big, col, 2) 扫描每一列,voffset=2 下移 2 排, -- 使其与 22×22 中文在视觉基线上对齐(英文上下留白较多)。 -- 键名使用 ASCII 码(如 76 = 'L'),仅包含本演示用到的 L/u/a/t/O/S。 local WS2812_FONT_EN22 = { [76] = { -- L "...........","...........","..##.......","..##.......","..##.......", "..##.......","..##.......","..##.......","..##.......","..##.......", "..##.......","..##.......","..##.......","..##.......","..########.", "..########.","...........","...........","...........","...........", "...........","...........", }, [117] = { -- u "...........","...........","...........","...........","...........", ".##.....##.",".##.....##.",".##.....##.",".##.....##.",".##.....##.", ".##.....##.",".##.....##.",".##....###.",".###..####.","..########.", "..#####.##.","...........","...........","...........","...........", "...........","...........", }, [97] = { -- a "...........","...........","...........","...........","...........", "...#####...","..#######..","..#....###.","........##.","...#######.", ".#########.",".###....##.",".##.....##.",".###..####.",".#########.", "..#####.##.","...........","...........","...........","...........", "...........","...........", }, [116] = { -- t "...........","...........","...##......","...##......","...##......", "##########.","##########.","...##......","...##......","...##......", "...##......","...##......","...##......","...###.....","....######.", ".....#####.","...........","...........","...........","...........", "...........","...........", }, [79] = { -- O "...........","...........","...#####...","..########.",".###...###.", ".##.....###","###......##","##.......##","##.......##","##.......##", "##.......##","##.......##","###.....###",".###...###.",".########..", "...#####...","...........","...........","...........","...........", "...........","...........", }, [83] = { -- S "...........","...........","...######..","..#######..",".###....#..", ".##........",".###.......",".####......","..#####....","....#####..", ".......###.","........##.","........##.",".#.....###.",".#########.", "..######...","...........","...........","...........","...........", "...........","...........", }, } -- ====== 条带尺寸常量 ====== -- 条带虚拟高度(与 ws2812_config 中的 LED_H 对齐,均为 22 行)。 -- 列掩码最多使用 bit0~bit21,超过 STRIP_H 的位会被 col_mask 丢弃。 local STRIP_H = 22 -- 5×7 小字模放大 2× 后垂直居中下移的排数。 -- 计算方式:5×7 字形高 7 像素,放大 2× 后高 14 像素; -- 在 22 像素高的条带里垂直居中,需要下移 (22-14)/2 = 4 排。 local EN_VOFFSET_SMALL = 4 -- 5×7 英文放大 2× 后下移居中 -- ====== 5×7 列字节 → 放大 2× 后的位掩码 ====== --- 将 5×7 字模的单列字节(7 像素)放大为 14 像素高的位掩码 -- 放大规则:每个原始像素在垂直方向翻倍占两行(2× 缩放)。 -- 例:原 bit r 为 1,则在结果掩码中置位 (r*2+voff) 和 (r*2+voff+1) 两个 bit。 -- 使用 (3 << ((r*2)+voff)):3 的二进制是 11,左移后恰好是相邻两个 1。 -- @param byte integer 5×7 字模某一列的字节(bit0=顶行) -- @return integer 放大并垂直居中后的 22-bit 列掩码 local function expand_small_col(byte) local mask = 0 -- 遍历 7 行 for r = 0, 6 do -- (byte >> r) & 1 取出第 r 位 if (byte >> r) & 1 == 1 then -- 3 = 二进制 11,左移 (r*2)+EN_VOFFSET_SMALL 位: -- 原始第 r 像素 → 放大后占第 (r*2+voff) 和 (r*2+voff+1) 两排 -- |= 把这两个 bit 合并进总掩码 mask = mask | (3 << ((r * 2) + EN_VOFFSET_SMALL)) end end return mask end -- ====== 行优先字模 → 单列位掩码 ====== --- 从行优先字符串字模中取出某一列,拼成列掩码 -- 双向越界保护:r + off 可能小于 0(voffset 为负、字形靠上) -- 或大于等于 STRIP_H(字形靠下超出 22 行),这两种情况都跳过, -- 避免位运算产生无效 bit 或写到条带范围外。 -- @param glyph table 字模表(22 个字符串,行序) -- @param col integer 要取的列号(0 起始) -- @param voffset integer 垂直偏移(可正可负,nil 视为 0) -- @return integer 22-bit 列掩码 local function col_mask(glyph, col, voffset) local mask = 0 local h = #glyph local off = voffset or 0 -- 遍历字形的每一行 for r = 0, h - 1 do -- 取出第 r 行、第 col 列的字符;Lua 字符串索引从 1 开始故 +1 local ch = string.sub(glyph[r + 1], col + 1, col + 1) if ch == "#" then -- 该像素亮:计算它落在条带中的实际行号 local bit = r + off -- 双向越界保护:bit>=0 防止负偏移(左移负数位未定义), -- bit= 0 and bit < STRIP_H then mask = mask | (1 << bit) end end end return mask end -- ====== UTF-8 文本 → 列掩码数组 ====== --- 将 UTF-8 文本打包成滚动条带用的列掩码数组 -- UTF-8 编码要点: -- * ASCII 字符(0x00~0x7F)占 1 字节 -- * 0x80 及以上的字节是多字节序列的起始或后续字节 -- * 常用中文在 UTF-8 中占 3 字节(1110xxxx 10xxxxxx 10xxxxxx) -- 为什么用 while 而不是 for: -- Lua 数值 for 循环在循环开始时就固化了起始/终止/步长, -- 循环体内对 i 的赋值(如 i=i+3)不会改变下一次迭代值, -- 无法实现"中文跳 3 字节、ASCII 走 1 字节"的变长步进; -- 而 while + 手动 i=i+n 可以精确控制每次前进的字节数。 -- ASCII 字符的字模优先级: -- 1) 先查 WS2812_FONT_EN22(22×11 大尺寸英文字模,最清晰) -- 2) 查不到再回退到 WS2812_FONT5x7(5×7 小字模,放大 2× 后使用) -- @param text string UTF-8 编码的文本 -- @return table 列掩码数组(每元素是 22-bit 整数,含字间距空白列) function ws2812_build_strip(text) local columns = {} local i = 1 -- while 手动步进:i 在中文分支 +3,ASCII 分支 +1 while i <= #text do local byte = string.byte(text, i) if byte >= 0x80 then -- ====== 多字节分支:按中文 3 字节处理 ====== -- 取当前位置起连续 3 个字节,得到一个完整的 UTF-8 中文字符串, -- 直接用作 WS2812_FONT_CN 的键 local full = string.sub(text, i, i + 2) local glyph = WS2812_FONT_CN[full] -- 中文字模标准宽度 22 列;查不到字模时也保留 22 列空白占位, -- 避免后续列错位(不会因为缺字把后面的字符挤到前面) local gw = glyph and #glyph[1] or 22 for c = 0, gw - 1 do -- voffset=0:中文 22×22 正好占满条带高度,无需垂直偏移 columns[#columns + 1] = glyph and col_mask(glyph, c, 0) or 0 end -- 中文占 3 字节,前进 3 i = i + 3 else -- ====== ASCII 分支:1 字节 ====== -- 优先尝试 22×11 大字模 local big = WS2812_FONT_EN22[byte] if big then local ew = #big[1] for c = 0, ew - 1 do -- voffset=2:英文字模整体下移 2 排, -- 让英文视觉基线与旁边的中文对齐(英文字形上下留白较多) columns[#columns + 1] = col_mask(big, c, 2) end else -- 回退到 5×7 小字模:放大 2× 后渲染 local glyph = WS2812_FONT5x7[byte] if glyph then -- 5×7 共 5 列 for c = 0, 4 do local m = expand_small_col(glyph[c + 1]) -- 水平也放大 2×:每列重复写两次,得到 10 像素宽 columns[#columns + 1] = m columns[#columns + 1] = m end end -- 若 5×7 也没有该字符,则什么都不输出(不占列、字符直接消失) end -- ASCII 占 1 字节,前进一步 i = i + 1 end -- ====== 字间距:每个字符后追加 2 列空白 ====== -- 0 表示该列没有任何像素点亮,形成字符间的可见间隔, -- 避免中英文挤在一起难以辨认 columns[#columns + 1] = 0 columns[#columns + 1] = 0 end return columns end -- 导出条带高度给 scroll_task 使用(渲染时遍历 0~WS2812_STRIP_H-1 行) WS2812_STRIP_H = STRIP_H