鸿蒙国庆节特辑:用华为云码道打造「五星红旗 · 标准制图室」,GB 12982 几何规范一屏看懂

与其做一个节日工具,不如把节日里最庄严的那个符号本身做成作品。五星红旗的画法不是「凭感觉」,而是白纸黑字写在 GB 12982《国旗》国家标准里的——30×20 网格、大星圆心 (5,5) 格、四颗小星各有一尖正对大星中心。我让码道 Agent 把这些几何规范落成一个鸿蒙「制图室」应用:滑杆调旗高,Canvas 实时重绘,点任意一颗星看它的坐标和旋转角。

仓库地址:https://atomgit.com/2501_92427005/flag-drafting-lab

在这里插入图片描述
建仓提示词发送

0. 缘起:为什么是它

纯旗面

国庆节刷到升旗视频,弹幕里总有人争论「四颗小星到底是怎么摆的」。其实答案在 GB 12982 里写得清清楚楚:

  • 旗面 3:2,划分为 30 行 × 20 列的网格;
  • 大五角星外接圆直径 = 旗高 3/10,圆心在左上方 5×5 格正方形中心;
  • 四颗小五角星外接圆直径 = 旗高 1/10,圆心分别在 (10,2)、(12,4)、(12,7)、(10,9) 格;
  • 每颗小星都有一个角尖正对大星中心点——这意味着四颗小星各有各的旋转角,不是复制粘贴。

这套几何天然适合做成「可交互的画法几何教具」,也天然适合考验码道 Agent:坐标全是分数与三角函数,错一点视觉上立刻穿帮。同时它跟前几篇(日晷、卫星、流星雨、中秋小游戏)完全不撞题。

照例先立六条硬约束,写进给 Agent 的提示词:

  • 纯本地、零权限:requestPermissions 保持 [],不联网、不存储、不定位;
  • ArkTS + ArkUI 声明式,API 12+,DevEco Studio 5.0 直接打开可构建;
  • domain 层纯净:几何计算不 import 任何 @ohos.*,能在 Node 里跑单元测试;
  • 数值必经 sanitize:滑杆输入先过 sanitizeHeightPx,空串/NaN/越界全部有兜底;
  • 中文文案全走资源:resources/base|dark/element,深浅色双主题;
  • 测试必须真绿:npm test 用 Node 的 --experimental-strip-types 直接跑。

1. 给 Agent 的提示词:先建仓,再开发

沿用系列验证过的「两步走」:

第一步,建仓。 让码道在 AtomGit 创建 flag-drafting-lab 仓库,只写 README 标题 + MIT License + .gitignore(含 .hvigor/、*.hap 这些鸿蒙特有忽略项),不写任何业务代码:

建仓提示词发送

Agent 思考后选择 AtomGit 平台,调 ag-cli 技能完成建仓:

Agent 选择平台

建仓完成,仓库地址落定:https://atomgit.com/2501_92427005/flag-drafting-lab

建仓完成

第二步,开发。 单独开会话,把完整需求喂给 Agent——核心是 GB 12982 的几何参数表,我直接把星位坐标、直径比例、旋转角约束全写死在提示词里,不给模型「自由发挥」的空间:

开发提示词发送

红线清单也一并给出:禁 any/unknown、禁旧作换皮(日晷/节气/流星雨/假期打卡全点名)、公式永不进组件事件、提交语义化:

红线与交付物

2. 需求拆解:一根滑杆、一面旗、五颗可点的星

应用功能一句话讲完:

拖滑杆改旗高,屏幕上的五星红旗按国标重画,点星星看它为什么在那个位置。

具体三层:

层职责
domain由旗高推出全部几何量:网格尺寸、五颗星圆心、内外半径、旋转角
uiCanvas 绘制旗面/网格/星形,滑杆与开关,星位命中检测
resources深浅色色板、全部中文文案、免责声明

交互细节:

  • 旗高滑杆 100–300 px,步进 1 px,实时重绘;
  • 「网格辅助线」开关:叠 30×20 半透明网格,教学场景专用;
  • 「显示坐标」开关:画布上方打出 H/W/格值;
  • 点击画布 → 命中检测 → 下方卡片显示该星圆心、外接圆半径;小星额外显示旋转角(度)。

3. 架构:domain 层是全部精华

工程结构(Agent 产出,我未改分层):

entry/src/main/ets/
├── domain/
│   └── flagGeometry.ets   纯几何:常量表 + 纯函数,零 @ohos.* import
├── entryability/
│   └── EntryAbility.ets
└── pages/
    └── Index.ets          @Entry:Canvas + 滑杆 + 命中卡片

3.1 常量表:把国标翻译成代码

// domain/flagGeometry.ets
export const BIG_STAR = Object.freeze({
  centerXUnits: 5,
  centerYUnits: 5,
  diameterRatio: 3 / 10,
});

export const SMALL_STARS: ReadonlyArray<{
  readonly centerXUnits: number;
  readonly centerYUnits: number;
}> = [
  { centerXUnits: 10, centerYUnits: 2 },
  { centerXUnits: 12, centerYUnits: 4 },
  { centerXUnits: 12, centerYUnits: 7 },
  { centerXUnits: 10, centerYUnits: 9 },
];

export const SMALL_STAR_DIAMETER_RATIO = 1 / 10;

Object.freeze + readonly 双保险,常量不可能在运行期被改写。

3.2 旋转角:小星「看向」大星

四颗小星的旋转角不是查表查出来的,是算出来的:

export function pointToAngleRad(
  fromX: number, fromY: number,
  toX: number, toY: number
): number {
  return Math.atan2(toX - fromX, -(toY - fromY));
}

注意第二个参数取负:屏幕坐标 y 向下,而「角尖朝上」对应 -y 方向。这个符号坑过不少人,Agent 一次写对。

3.3 五角星顶点:外内交替的 10 个点

const INNER_OUTER_RATIO = Math.sin(Math.PI / 10) / Math.sin((7 * Math.PI) / 10);

export function starPolygonPoints(star: StarInfo): Array<{ x: number; y: number }> {
  const points: Array<{ x: number; y: number }> = [];
  const step = Math.PI / 5;
  for (let i = 0; i < 10; i++) {
    const r = i % 2 === 0 ? star.outerRadius : star.innerRadius;
    const a = star.rotationRad + i * step;
    points.push({
      x: star.centerX + r * Math.sin(a),
      y: star.centerY - r * Math.cos(a),
    });
  }
  return points;
}

内外半径比 sin(18°)/sin(126°) 是标准五角星的几何性质,写成一个常量而不是魔法数字。

3.4 输入消毒:滑杆值也要过安检

export function sanitizeHeightPx(input: unknown, fallback: number): number {
  const n = toFiniteNumber(input);
  if (n === null) return fallback;
  return Math.max(100, Math.min(300, n));
}

空串、null、NaN、字符串数字、越界值各有归宿。calcFlagLayout 内部先调它,所以即使 UI 层忘了 sanitize,domain 层也不会产出 NaN 布局。

4. UI:Canvas 绘制与命中检测

4.1 绘制流程

private drawFlag(): void {
  const layout = calcFlagLayout(this.heightPx);
  // 1. 居中偏移
  const offsetX = (ctx.width - layout.widthPx) / 2;
  const offsetY = (ctx.height - layout.heightPx) / 2;
  // 2. 旗面
  ctx.fillStyle = '#DE2910';
  ctx.fillRect(offsetX, offsetY, layout.widthPx, layout.heightPx);
  // 3. 可选网格
  if (this.showGrid) { /* 30×20 半透明线 */ }
  // 4. 五颗星
  this.drawStar(ctx, layout.bigStar, offsetX, offsetY);
  for (const s of layout.smallStars) this.drawStar(ctx, s, offsetX, offsetY);
}

旗面红 #DE2910、星黄 #FFDE00 是国标色值,写进 resources/base/element/color.json 而非硬编码。

4.2 命中检测

export function nearestStar(
  layout: FlagLayout, x: number, y: number
): { kind: 'big' | 'small'; index: number; distancePx: number } | null {
  // 遍历五颗星,距离 ≤ 外接圆半径 + 4px 容差即命中,返回最近者
}

容差 4 px 是给手指触控留的——纯几何命中区域对触摸来说太小。

5. 单元测试:Node 直跑 ArkTS 子集

沿用上一篇验证过的路子:tests/transpile.mjs 把 domain/*.ets 改 import 路径、转存为 .ts,Node 22+ 的 --experimental-strip-types 直接执行,零第三方依赖。

"scripts": {
  "pretest": "node tests/transpile.mjs",
  "test": "node --experimental-strip-types --test tests/*.test.mjs"
}

7 条用例覆盖:

  • 手算尺子:heightPx=300 → gridSize=10、大星中心 (50,50)、外接圆半径 45、小星①中心 (100,20);
  • 比例一致性:任意旗高,宽 = 高 × 1.5 恒成立;
  • 旋转角正确性:四颗小星 rotationRad 与 pointToAngleRad 重算值误差 < 1e-9;
  • 星形顶点数 = 10,首顶点距圆心 = 外接圆半径;
  • 命中检测:大星中心命中 big/0,空白点返回 null;
  • sanitize:空串/null/NaN/-5/999/150 各断言;
  • 随机扫描:10000 组随机旗高全链路有限不抛错。
$ npm test
ℹ tests 7
ℹ pass 7
ℹ fail 0

中间踩过一个坑值得记录:最初转译脚本输出 .mjs,Node 的 --experimental-strip-types 只处理 .ts 后缀,.mjs 里的类型注解直接炸 SyntaxError: Missing initializer in const declaration。改成输出 .ts 后立刻全绿。这个教训已写进提示词方法论的备忘录。

6. DevEco 实测:四个状态

工程在 DevEco Studio 5.0 里 Sync 一次过(这次 Agent 把 AppScope/、hvigor/ 骨架补齐了,上一篇踩的坑没复发),Pura X 模拟器直接 Run:

默认首屏,旗高 200 px:

首屏

网格 + 坐标全开,旗高 277 px——教学场景下能逐格核对星位:

网格与坐标

关网格、开坐标,旗高 148 px 的小旗形态:

小旗

纯旗面展示,旗高 238 px——四颗小星的朝向差异在这个尺寸下肉眼可辨:

纯旗面

边界实测:

  • 滑杆拉到最小 100 px / 最大 300 px,星形不变形、不溢出;
  • 快速来回拖滑杆,Canvas 重绘无闪烁(@State 驱动,声明式渲染);
  • 点旗面空白处,星位卡片收起;点小星边缘 4 px 内仍能命中。

小结

这篇是系列里技术密度最高的一篇,但代码量反而最小——因为国标把所有「决策」都做完了,剩下的只是忠实翻译。三点心得:

  1. 规范即需求。GB 12982 把坐标、比例、朝向全定死了,提示词可以直接写成「常量表 + 公式」,Agent 没有自由发挥的空间,输出反而最稳。这比写「帮我画一面好看的国旗」靠谱一万倍。

  2. 可测试性来自几何的确定性。(5,5) 格、3/10 直径、atan2 旋转角,每个数都有手算答案,单元测试的「尺子」随手就能写。反过来,如果需求里有「大概」「差不多」「看起来」,测试就废了。

  3. 踩坑要变成下一条提示词。上篇踩的「中文路径」「AppScope 缺失」这次提前写进提示词就没复发;这次踩的「.mjs 不吃 --experimental-strip-types」已经进了我的提示词备忘录。每一篇的坑都是下一篇的护城河。

下一个想做的方向:既然旗的几何做完了,不如把「时间」加进来——天安门广场每日升旗时刻 = 当日北京日出时刻,背后是太阳赤纬 + 均时差 + 大气折射的一整套天文算法,正好接上系列前六篇的天文科普人设。如果下期挑战赛还在周期内,继续更。

  • 应用名称:Flag Drafting Lab(五星红旗 · 标准制图室)
  • 仓库地址:https://atomgit.com/2501_92427005/flag-drafting-lab
  • 技术栈:HarmonyOS ArkTS + ArkUI 声明式 · API 12+ · Canvas 2D · Node node:test(--experimental-strip-types)
  • 核心特性:零权限、纯本地、GB 12982 几何内核、30×20 教学网格、星位命中检测、深浅色双主题、7 条单元测试全绿
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐