鸿蒙国庆节特辑:用华为云码道打造「五星红旗 · 标准制图室」,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技能完成建仓:
建仓完成,仓库地址落定:https://atomgit.com/2501_92427005/flag-drafting-lab
第二步,开发。单独开会话,把完整需求喂给 Agent——核心是 GB 12982 的几何参数表,我直接把星位坐标、直径比例、旋转角约束全写死在提示词里,不给模型「自由发挥」的空间:
红线清单也一并给出:禁 any/unknown、禁旧作换皮(日晷/节气/流星雨/假期打卡全点名)、公式永不进组件事件、提交语义化:
2. 需求拆解:一根滑杆、一面旗、五颗可点的星
应用功能一句话讲完:
拖滑杆改旗高,屏幕上的五星红旗按国标重画,点星星看它为什么在那个位置。
具体三层:
| 层 | 职责 |
|---|---|
| domain | 由旗高推出全部几何量:网格尺寸、五颗星圆心、内外半径、旋转角 |
| ui | Canvas 绘制旗面/网格/星形,滑杆与开关,星位命中检测 |
| 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.etsexportconstBIG_STAR=Object.freeze({centerXUnits:5,centerYUnits:5,diameterRatio:3/10,});exportconstSMALL_STARS:ReadonlyArray<{readonlycenterXUnits:number;readonlycenterYUnits:number;}>=[{centerXUnits:10,centerYUnits:2},{centerXUnits:12,centerYUnits:4},{centerXUnits:12,centerYUnits:7},{centerXUnits:10,centerYUnits:9},];exportconstSMALL_STAR_DIAMETER_RATIO=1/10;Object.freeze+readonly双保险,常量不可能在运行期被改写。
3.2 旋转角:小星「看向」大星
四颗小星的旋转角不是查表查出来的,是算出来的:
exportfunctionpointToAngleRad(fromX:number,fromY:number,toX:number,toY:number):number{returnMath.atan2(toX-fromX,-(toY-fromY));}注意第二个参数取负:屏幕坐标 y 向下,而「角尖朝上」对应-y方向。这个符号坑过不少人,Agent 一次写对。
3.3 五角星顶点:外内交替的 10 个点
constINNER_OUTER_RATIO=Math.sin(Math.PI/10)/Math.sin((7*Math.PI)/10);exportfunctionstarPolygonPoints(star:StarInfo):Array<{x:number;y:number}>{constpoints:Array<{x:number;y:number}>=[];conststep=Math.PI/5;for(leti=0;i<10;i++){constr=i%2===0?star.outerRadius:star.innerRadius;consta=star.rotationRad+i*step;points.push({x:star.centerX+r*Math.sin(a),y:star.centerY-r*Math.cos(a),});}returnpoints;}内外半径比sin(18°)/sin(126°)是标准五角星的几何性质,写成一个常量而不是魔法数字。
3.4 输入消毒:滑杆值也要过安检
exportfunctionsanitizeHeightPx(input:unknown,fallback:number):number{constn=toFiniteNumber(input);if(n===null)returnfallback;returnMath.max(100,Math.min(300,n));}空串、null、NaN、字符串数字、越界值各有归宿。calcFlagLayout内部先调它,所以即使 UI 层忘了 sanitize,domain 层也不会产出 NaN 布局。
4. UI:Canvas 绘制与命中检测
4.1 绘制流程
privatedrawFlag():void{constlayout=calcFlagLayout(this.heightPx);// 1. 居中偏移constoffsetX=(ctx.width-layout.widthPx)/2;constoffsetY=(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(constsoflayout.smallStars)this.drawStar(ctx,s,offsetX,offsetY);}旗面红#DE2910、星黄#FFDE00是国标色值,写进resources/base/element/color.json而非硬编码。
4.2 命中检测
exportfunctionnearestStar(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 组随机旗高全链路有限不抛错。
$npmtestℹ tests7ℹ pass7ℹ fail0中间踩过一个坑值得记录:最初转译脚本输出.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 内仍能命中。
小结
这篇是系列里技术密度最高的一篇,但代码量反而最小——因为国标把所有「决策」都做完了,剩下的只是忠实翻译。三点心得:
规范即需求。GB 12982 把坐标、比例、朝向全定死了,提示词可以直接写成「常量表 + 公式」,Agent 没有自由发挥的空间,输出反而最稳。这比写「帮我画一面好看的国旗」靠谱一万倍。
可测试性来自几何的确定性。
(5,5)格、3/10直径、atan2旋转角,每个数都有手算答案,单元测试的「尺子」随手就能写。反过来,如果需求里有「大概」「差不多」「看起来」,测试就废了。踩坑要变成下一条提示词。上篇踩的「中文路径」「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 条单元测试全绿