Harmony os 技术实战|拼豆制图27:用单字符编码承载 50 张 70×70 图纸

标签:Harmony os、ArkTS、资源编码、数据校验、像素图纸

一张 70×70 拼豆图有 4900 个位置。如果把每个位置直接写成包含 rowcolcolorIdcolorCodehexisEmpty 的对象,资源文件会迅速膨胀,人工审查也几乎不可能。工程采用了更紧凑的形式:每行是一段字符串,每个字符代表空格或色板索引,加载时再展开为完整 BeadCell[]

当前资源层包含 50 张图,每张都有 70 行施工图、16 行预览和最多 16 个颜色。仅施工图就有 245,000 个字符位置。单字符编码解决了"源码如何保存"的问题,却没有自动解决运行时内存、非法字符、行宽不一致和色板越界。本篇将编码表、解码过程与资源校验放在同一条链路中分析。

一、资源对象只保存三类信息

每张资源图由颜色数组、完整图纸行和预览行组成:

typescript 复制代码
export interface AssetChart {
  colors: string[];
  chartRows: string[];
  previewRows: string[];
}

当前每个 chartRows 含 70 个长度为 70 的字符串,previewRows 含 16 个长度为 16 的字符串。以 50 张图计算:

text 复制代码
完整图纸字符数 = 50 × 70 × 70 = 245,000
预览字符数     = 50 × 16 × 16 = 12,800
合计位置数     = 257,800

这些数字还没有计入引号、逗号和换行,但已经能说明编码规模。若每个位置都写成对象,同一个 colorId、行列坐标和字段名会重复数十万次;字符串行则把重复信息推迟到加载阶段生成。

二、一个字符刚好覆盖 16 色与空格

解码表采用三段规则:

字符 含义 返回索引
. 空格 -1
0---9 色板前 10 色 0---9
A---F 色板后 6 色 10---15

对应实现保持为纯整数映射:

typescript 复制代码
private static assetColorIndex(token: string): number {
  if (token === '.') {
    return -1;
  }
  const digitIndex = '0123456789'.indexOf(token);
  if (digitIndex >= 0) {
    return digitIndex;
  }
  const letterIndex = 'ABCDEF'.indexOf(token);
  if (letterIndex >= 0) {
    return 10 + letterIndex;
  }
  return -1;
}

因此 0 不是"无颜色",而是第 0 个颜色;只有 . 表示空格。小写 a、字母 G、空格字符和其他符号都会返回 -1。这个约定让一个 UTF-16 代码单元就能表达一个格子,也把色板上限明确固定为 16。

三、色板顺序就是文件协议的一部分

资源文件只保存十六进制颜色,没有保存色号。仓库解码时按固定顺序补上业务色号:

typescript 复制代码
const codes: string[] = [
  'H7', 'G1', 'D9', 'A12', 'F1', 'D11', 'H6', 'H2',
  'H5', 'E1', 'M12', 'H14', 'F16', 'F17', 'M5', 'G18'
];

于是字符与色号的关系由数组位置决定:0 -> H79 -> E1A -> M12F -> G18。如果只调整 colors 顺序,却没有同步重新编码所有行,图形轮廓仍然完整,颜色却会整体错位。这类错误比解析失败更隐蔽,因为页面仍然能正常渲染。

可以把三者看作一个不可拆分的版本:

text 复制代码
字符 token -> palette index -> color code / hex

任何一段改变,都应重新核对固定像素样本,而不是只验证数组长度。

四、解码时才创建完整 BeadCell

createCellsFromRows() 按行和列遍历字符串,取出一个字符,解析色板索引,再生成业务格子:

typescript 复制代码
private static createCellsFromRows(
  id: string,
  palette: PaletteColor[],
  rows: string[]
): BeadCell[] {
  const cells: BeadCell[] = [];
  for (let row = 0; row < rows.length; row++) {
    const sourceRow = rows[row];
    for (let col = 0; col < sourceRow.length; col++) {
      const token = sourceRow.substring(col, col + 1);
      const colorIndex = PatternRepository.assetColorIndex(token);
      const color = colorIndex < 0 || colorIndex >= palette.length
        ? null
        : palette[colorIndex];
      cells.push(PatternRepository.createCell(id, row, col, color));
    }
  }
  return cells;
}

展开后的每个格子拥有稳定坐标和完整颜色信息,页面、统计服务与 PNG 导出服务都不再关心资源字符。这是很有价值的边界:紧凑格式只存在于资源层,业务层始终处理统一模型。

五、非法字符被当作空格,容错也可能掩盖错误

当前实现把两种情况都转换为 null 颜色:

  1. 字符无法识别,例如 G 或小写 a
  2. 字符能得到索引,但索引超出当前色板长度。

最终生成的格子会是 isEmpty=true。运行时因此不会崩溃,但资源录入错误会表现为图案缺一颗豆,而不是明确的异常。若缺口位于大面积背景中,人工预览很难发现。

更适合资源构建阶段的策略是"严格校验,运行时保底":

typescript 复制代码
interface AssetIssue {
  row: number;
  col: number;
  token: string;
  reason: string;
}

开发时扫描全部行并报告精确坐标;正式运行时仍可保留空格回退,避免单个坏字符让整个图库无法打开。两层策略各自解决不同问题。

六、行宽不一致会让二维坐标悄悄错位

图纸宽度当前取第一行长度:

typescript 复制代码
private static rowWidth(rows: string[]): number {
  if (rows.length === 0) {
    return 0;
  }
  return rows[0].length;
}

解码循环却按每一行自己的 sourceRow.length 追加格子。假设宽度记录为 70,而第 20 行只有 69 个字符,从该行之后,扁平数组按 width=70 重新分组时就会整体左移;若某行多了一个字符,后续又会整体右移。

这不是单行少一格,而是从错误点开始污染余下所有行。因此资源校验至少要满足:

text 复制代码
chartRows.length == 70
every chartRows[row].length == 70
previewRows.length == 16
every previewRows[row].length == 16
colors.length <= 16

宽高如果未来不固定,也应先从元数据得到期望值,再验证每一行,而不是默认相信第一行。

七、紧凑资源不等于低运行时内存

一次 PatternRepository.getPatterns() 会创建全部 50 张完整图和预览图:

text 复制代码
每张格子对象数 = 70 × 70 + 16 × 16 = 5,156
50 张对象数     = 5,156 × 50 = 257,800

资源文件确实紧凑,但调用后仍会展开为 257,800 个 BeadCell 对象。每个对象还携带字符串 ID、色号和颜色值,真实堆占用远高于字符数量。

这说明"存储格式"和"加载策略"是两个独立维度:

  • 单字符行减少源码体积与资源维护成本。
  • 按需解码或缓存最近访问项,才减少首屏对象分配。

若首页只展示缩略图,可以先解码 16×16 预览;进入编号页时再解码对应 70×70 图纸。这样仍然复用同一套资源协议,却不必在应用初始化时展开所有完整矩阵。

八、50 个 if 分支可以继续收敛,但先保留静态类型

当前 PatternAssetCharts.get(id) 通过 50 个明确的 ID 分支返回资源。优点是结构直观、构建时可获得类型约束;缺点是文件超过五千行,查找、合并和人工校对成本高。

可演进为按分类拆分,再用显式映射汇总:

typescript 复制代码
const animeCharts: Record<string, AssetChart> = { /* ... */ };
const gameCharts: Record<string, AssetChart> = { /* ... */ };

static get(id: string): AssetChart | null {
  return animeCharts[id] ?? gameCharts[id] ?? null;
}

重点不是换一种语法,而是让每个资源仍然经过同一校验入口。若直接读取外部 JSON,还需要处理字段缺失、字符编码和类型收窄,不能因为文件更短就省略边界验证。

九、资源扫描应输出可复算摘要

对 50 张图执行资源扫描时,建议每张至少输出以下摘要:

text 复制代码
id=anime-1
palette=16
chart=70x70
preview=16x16
invalidTokens=0
raggedRows=0
usedColorIndexes=0..15

总览再核对:

  • 图案 ID 是否与仓库中的 50 个种子一一对应。
  • 是否存在资源但没有种子,或种子没有资源。
  • 字符索引是否超出色板。
  • 空格比例是否异常突变。
  • 预览与完整图的主色分布是否相近。

这些摘要比逐行阅读 70 个长字符串更有效,也能在资源更新后快速发现结构变化。

十、编码上限变化时必须显式升级协议

0-9A-F 的优势是一个字符覆盖 16 色。如果未来需要 20 色,不能随意把 G-J 接在后面而不记录版本,因为旧解码器会把新字符当作空格。可以选择:

  1. 明确扩展字母表并增加资源版本。
  2. 改用两字符定长索引。
  3. 使用调色板压缩后的二进制资源。

对当前 16 色业务,单字符方案足够简单且可读。真正需要提前设计的是版本识别和失败方式,而不是为了假设中的规模立即引入复杂格式。

小结

Harmony os 应用中的图纸资源可以很紧凑:用 . 表示空格、0-9A-F 表示 16 个色板索引,50 张 70×70 图与 16×16 预览共承载 257,800 个字符位置;加载后再统一展开为 BeadCell[],让页面、统计和导出服务不感知底层编码。

这套方案真正的工程边界有三条:色板顺序属于协议,不能单独调整;每行宽度必须在解码前验证;紧凑存储不能替代按需加载。把资源扫描、运行时回退和对象展开分开设计,单字符编码才能同时获得可维护性、可定位性与稳定渲染结果。

相关推荐
皮卡丘不断更1 小时前
手机优先的 Personal Ledger:把账目、学习和复盘放到同一个入口
数据库·sqlite·fastapi·开源项目·个人效率
CarIise2 小时前
CSS选择器与样式关联
前端·css·tensorflow
保卫大狮兄2 小时前
设备管理从台账到报废,完整生命周期一次讲清
数据库·设备管理·设备
数据安全技术观察3 小时前
数据动态脱敏:让脱敏策略跟着数据目录走
大数据·网络·数据库
里欧跑得慢3 小时前
CSS 模块化架构的演进:BEM、CSS Modules 到 CSS-in-JS 的反思
前端·css·flutter·web·css-in-js
IT_陈寒3 小时前
Vue的computed属性把我坑惨了,原来我一直用错姿势
前端·人工智能·后端
小席是个热心肠3 小时前
Redis的自我学习
数据库·redis·学习
小灰灰搞电子4 小时前
Rust+Slint 实现温度计源码分享
前端·rust·slint
2601_965798474 小时前
Salient Theme Setup Guide for Fast Creative WordPress Sites
数据库·web3·php·wordpress