中北大学 2026 年 8 月,我们把 194 张实拍照片逐张归并,确认了 76 只在册猫咪,然后把它们做成了一片可以漫游、可以抽签、可以导出寻猫海报的 Canvas 星空。本文是这个项目的技术复盘:数据怎么普查把关、纯前端单文件怎么做到零依赖又流畅、星位怎么从"拍脑袋"进化成"手绘边界多边形确定性撒点",以及三个仓库如何用字节级 golden 测试链和 CI 门禁互相咬合。项目已在 GitHub 开源,入口见文末。
一、这是个什么项目
一句话:每只猫是一颗星,整个校园是一片星域。
- 星色 = 毛色。橘猫是暖橘星,狸花是青灰星,三花自带渐变,奶牛是蓝白星。
- 星等 = 人气。一只猫被拍到的次数越多,星等越高,星周环绕的光点也越多。
- 星位 = 出没区域。12 大出没区升格为 12 座星座,点开宿名能听到每座"星官"的故事。
- 档案卡 = 军衔 + 工位 + 小传。中北是"人民兵工第一校",猫学员自然要列队报到:喵校长墩墩、橘少校、设备安全官玄铁(全黑,很难拍)……每只猫都有昵称、军衔和工位。
它是一个纯前端单文件项目:照片和数据全部内嵌,双击 HTML 就能打开,断网、机房、军训拉练都能跑,不产生任何外部请求。同时它有完整的 Web 版托管在 GitHub Pages 上,手机浏览器打开一样玩。
九月开学季,这 76 颗星正好等一届新生来认领——导出一张寻猫海报,把校园走一圈,猫找到了,路也就熟了。这是这个项目最想做的事:让"认识校园"这件事有一个最低压力的入口。
二、先讲数据:194 张照片怎么变成 76 只猫
可视化项目最怕的不是技术翻车,而是数据翻车——猫图鉴里出现两只"同一只猫",这个项目就死了。所以我们在数据上卡得非常严:
- 实地普查:2026 年 8 月,宿舍楼、教学楼、绿化带、食堂后门,见到就拍。一部分自己蹲点,一部分是学长学姐投喂的照片,前后攒了194 张。
- 逐张归并:194 张照片逐张比对,疑似重复、身份存疑的一律不收录。071 号因为夜拍画质不够直接弃用、编号空着;076 和 077 因为同框判读矛盾,双双出局——宁可少一只,不放一只错的。每一条取舍都写进《归并决策摘要.md》,欢迎抽查。
- 证据链:每只猫必须有照片证据链可溯源,置信度低的直接不收;不收录含人脸的照片,不虚构行为故事——档案卡里的 50 字小传只写名册里能看到的内容(军衔、职务、区域、特征、照片数拼装),这是数据红线。
- 隐私:全部为校园实地拍摄,没有 AI 生成的猫像;逐张核验不含人脸;进度数据只存浏览器 localStorage,可随时清除。
这套流程后来被抽象成了12 列名册 CSV 契约(编号、昵称、军衔、工位、毛色、特征描述、代表照片文件、照片数量、出没区域、关联照片编号、置信度、备注),成为整个模板和工厂的数据入口。编码自动探测(utf-8-sig / gb18030 / big5……),空编号、重复编号、照片缺失这类新手坑在构建期就带行号报错,而不是上线后图片 404。
三、星图怎么玩
打开之后,2550 颗粒子先聚成"中北喵星图"五个字,三色极光垂帘在标题后面,然后镜头从猫群深处拉出来,星星按远近依次点亮。之后的事情就随意的:
| 分层 | 功能 |
|---|---|
| 看星 | 拖拽/滚轮/双指漫游缩放;点星开档案卡;按喵名、军衔、岗位、特征搜索(无结果给智能建议);毛色 chips 一键筛选;星图志统计看板;实证搭档猫之间有流动连线的星域网络 |
| 玩起来 | 喵星导览(镜头自动飞过六只旗舰猫);今日喵签(同一天全网抽到同一只);喵图鉴(遇见打卡,10/30/50 只放流星雨);十二星宿;星轨长曝光;星尘变身(星星炸成星尘再聚合成猫照片);本命猫测试;每只猫的数据谱成的专属主题曲(Web Audio 实时合成) |
| 带出门 | 实地寻猫海报(12 出没区 × 识别特征,竖版 PNG);720×1000 分享卡;表情包工坊;喵星护照 + 进度码备份恢复;导览录屏成 WebM 纪录片 |
| 彩蛋 | 桌面端鼠标是彗星;天色按时辰渐变;猫聚簇处漂起对应毛色的星云;秋季金落叶、冬季初雪限定 |
点开任何一颗星,星尘先炸开再缓缓聚合成这只猫的真实照片:
对新生来说,最好用的是实地寻猫:每个出没区列出常驻猫的识别特征,一键导出海报存进手机。为了找墩墩你会走到宿舍楼玻璃门前,为了找玄铁(全黑、夜行、极难拍)你会认识设备区——一圈走下来,校园地形和楼群方位全摸熟了。
四、核心技术实现:零依赖单文件,凭什么能流畅
整个产物是一个 HTML(外加若干 base64 照片分片),没有框架、没有构建产物、没有任何 CDN 依赖,Canvas 2D 手写渲染器。几个关键技术决策:
1. 发光用 sprite 缓存,不用 shadowBlur。shadowBlur在星图这种"满天都是发光体"的场景是帧率杀手。每档星等/星色的光晕预渲染成离屏 sprite,绘制时drawImage贴图,粒子数上去也能稳帧。
2. dt 步进 + 120Hz 兼容 + DPR 上限。
动画全部按dt步进而不是按帧数,高刷屏不会加速;devicePixelRatio 上限压在 2,4K 屏不会白白多画四倍像素。
3. 帧率自适应,不悄悄降级。
每秒实测帧率,低于阈值自动切"省电"档(星系与星尘抽稀)并弹提示告诉用户,手机默认均衡档起步、帧率富余自动升回完整;档位可手动换且会被记住。
4. 照片分片懒加载。
75 张照片 base64 内嵌后切成 ≤1.5MB 的photo-data-NN.js分片,首屏只取底图关键片(约为全量 eager 的 2%),点开档案卡才按需取片解码。两种模式(懒加载 / eager 整包)双击file://都能离线玩。
5. 声音全部代码合成。
星语音效、每只猫的主题曲(毛色定调式、名字定动机),Web Audio 振荡器 + 滤波 + 延迟实时合成,零音频文件,遵守浏览器自动播放策略默认关闭。
6. 兼容性兜底。roundRectpolyfill 兜 Safari 旧版;照片加载失败自动切星夜占位图;EXIF 旋转矫正(手机竖拍照片方向不对是普查阶段踩过的真实坑);浏览器基线 Chrome/Edge 80+、Safari 13.1+。
7. 星轨长曝光的分层。
星系画在独立离屏层,轨迹缓冲只累积星系层(lighter叠加 + 黑色透明衰减),所以长曝光拉出弧形光轨时,地图和 UI 不会被糊成一片。
五、星位是怎么定的:从"拍脑袋"到手绘边界多边形
这是这个项目迭代最多的地方,前后经历了三个阶段:
- 人工校准(v2.8.7 及以前):54 颗星手动拖到真实位置,坐标写死在 HTML 的 CALIB 表里。准,但不可维护——每加一只猫都要手动定位。
- 矩形包络撒点(v2.9.0):星位改为按编号确定性撒在建成区矩形(STAR_BOX)内。可复现、可维护,但矩形里包含了一些"猫不会去"的地方。
- 手绘边界多边形(v2.9.1,当前):直接在底图上手绘校区边界,生成 51 顶点多边形(存进
build_meta.star_zone),撒点算法换成多边形内的确定性拒绝采样——矩形没变、编号没变,但每一颗星都保证落在猫真的会出没的校园轮廓里。上线后实测76/76 全部落形内、0 error。
底图也换了:官方校园平面图经过逐通道颜色拟合,转成了 1920×1121 的手绘风夜空底图(二龙山、太行山、吕梁山都在),生成脚本tools/make_hand_night_map.py入库可复现。
为什么坚持"确定性撒点"而不是随机数?因为同一名册在任何机器上构建两次,产物的字节必须完全一致——这是后面整套质量门禁的地基。同时页面上仍保留"校准位置"模式,拖星导出的 JSON 回传后可以逐颗归位到实测坐标。
六、三个仓库与"渲染真相单源"
一个课设项目一个仓库就够了,为什么会有三个?因为定位不同:
catgalaxy-factory(工厂) = 渲染上游:app/starmap_render.py 是唯一真相 meow-starmap(开源模板) = 门面:模板 + 数据层 + 工具链,vendor 渲染器快照 nuc-cat-starmap(中北正式版) = 实例:产物由工厂渲染器从数据包直出,逐字节一致- 工厂(FastAPI):上传名册 CSV + 照片 → 14 条校验规则 → 毛色/星等/星位推导 → 注入模板 → 在线预览 → 一键打包离线 HTML。带归并工作台(感知哈希给"疑似重复"预排序)、底图标定、主题定制。75 张 266.7MB 原图上传压缩实测 6.35s(多进程并行),内嵌版生成 0.88s、内存峰值 11.4MB(改前是 87.21MB)。
- 开源模板 meow-starmap:
一份名册 CSV + 一个照片文件夹 → 一张全离线单文件星图。工厂的渲染器以vendor 快照方式进入模板仓,tests/test_vendor_snapshot.py用 sha256 钉死——快照和上游差一个字节 CI 就红,杜绝"两边各改各的"。 - 中北正式版:不再手改 HTML,产物由
export_v29_product.py从数据包直出,和模板仓托管的在线版逐字节一致。
跨三仓发版顺序是铁律:工厂 → 模板仓同步快照 → 正式版直出,顺序错了对不上字节。
七、质量门禁:怎么保证 3320 行模板八代迭代不翻车
这套东西最花时间,也最值。当前模板仓149 项 Python 测试 + 全仓 HTML/分片语法门禁,工厂仓749 项测试,GitHub Actions ubuntu + windows 双矩阵(Windows 不是凑数:杀软占文件的 WinError 5/32/33、路径分隔符、GBK 控制台这些坑只在 Windows 上出现,而交付对象恰恰就是 Windows 用户)。几条有意思的机制:
- 字节级 golden 往返:模板由正式版机器提取,把原始数据灌回去必须和正式版逐字节相同。v27→v35 八代各有一份 golden 基线入库,任何一代的规则改动破坏了历史往返,当场变红。
- 托管产物零漂移:CI 现场构建
schools/并与 GitHub Pages 上的产物逐字节比对——"改了模板忘了重建"这种事在仓库内就被拦住。 - 版本串注入不变式:产物里的版本号由数据包
build_meta.json注入模板的__VERSION__token,改版本不动模板、不动 golden。这条是被咬过两次之后(v2.8.4、v2.8.6 两次发版漏同步版本号)固化成测试的。 - 物料打包四重自证:对外发布包打出来后自动断言"打包物 == 仓库产物 == 在线托管"三方 sha 一致、分片逐片一致、清单不多不少、README/SKILL 逐份一致。这条闸门上岗当天就抓住了真实事故:对外包里的 README 是三个月前的旧版,缺了整节许可声明——文档层之前靠人手工同步,没人发现。
- 页面一致性另设仪器:仓库内全绿 ≠ 线上就新。实测碰到过 push 后 CI 绿、但 GitHub legacy Pages 的自动构建没触发(30 次 push 漏 1 次,API 无失败记录),线上停在旧版本。所以另有一条只读核对:直接拉线上 Pages 的字节和本地产物对 sha,退出码非 0 就是漂移。
一句话总结这套体系的价值:它把"忘了同步"、“两边各改各的”、“线上其实没更新"这三类最阴的事故,从"人肉发现"变成了"CI 变红”。
八、把它复刻到你们学校:15 分钟
模板仓把门槛压到了"零前端基础":
gitclone https://github.com/TianWenzzzh/meow-starmap.gitcdmeow-starmap# 用自带示例校(2 只虚构猫)构建uv run--withpillow tools/build.py--pkgschools/示例校--outdist/示例校# 双击 dist/示例校/示例校喵星图.html ✦换成你们学校:复制schools/示例校/→ 填 12 列名册、放照片、换底图 → 同一条命令。普查怎么拍、归并怎么做,docs/普查与拍摄规范.md和工厂的《跨校落地手册》里有完整 SOP。自己构建的产物可以免费部署到 GitHub Pages;想把自己的学校加进模板的多校列表,按docs/PR指南.md提交数据包就行。
协议是双层的:
- 代码与模板:MIT——随便改、随便用,保留版权声明即可,包括商业使用;
- 数据与照片:CC BY-NC-SA 4.0——署名、非商业、相同方式共享。普查数据和照片是这个项目最不可再生的部分,"素材来源网络侵权删"不能免责。
九、踩坑实录(都是真金白银的时间)
Path.write_text在 Windows 上会把整份 LF 文件翻成 CRLF。模板代际脚本一次运行产生 3376 行伪 diff,还破坏了"模板仓侧 LF"的字节基线。现在所有代际脚本一律read_bytes/write_bytes定换行,--check把 CRLF 直接判漂移。- Windows 杀软会占住 pytest 临时目录好几十秒不放手(WinError 5/32/33),CI 上文件竞争超时。解法:写侧重试预算 + CI 排除临时目录扫描。
- GitHub legacy Pages 自动构建会漏触发,且 API 里没有任何失败记录(我们实测 30 次 push 漏了 1 次)。仓库内一致性测试看不见这一层,只能加"拉线上字节对 sha"的外部核对。发版后别只看 CI 绿。
- 版本号字面量别钉进闸门脚本。早期负测把
assert version == "v2.8.6"写死,v2.8.7 一发布,正例先崩。现在一律从build_meta.json取值,并把"允许改动的字段集合"当断言——比钉值更严。 -webkit-backdrop-filter在 Chromium 里 computed 值是none,像素逐位不变。想验证毛玻璃补丁,Chrome 截图说明不了任何问题,只能用 WebKit 内核看,且 headless WebKit 不绘制该滤镜——像素级观感必须真机 iPhone/Mac 确认。"sha 全绿"和"真的生效"是两回事,补丁必须看一行真实产物。- FAT32 U 盘 + 跨平台 git:
core.ignorecase/filemode/symlinks/longpaths四件套要在每个仓库配好,.gitattributes强制文本 LF,否则 Windows 和 Linux 交替开发会把仓库搅成粥。
十、复盘
这个项目从一个很朴素的问题开始:校园里到底有多少只猫?长什么样?住在哪?8 月普查、9 月发版,现在它是 76 只猫的星空档案,也是任何学校都能 15 分钟复刻的开放模板。
回头看,整个项目的技术决策没有一个超出现代前端的基本盘——Canvas 2D、离屏层、分片懒加载、Web Audio、确定性算法——真正花时间的是把每一层都钉死:数据有证据链、渲染有字节基线、发版有闸门、线上有核对。下一个迭代是把同学回传的校准数据合并进去,让 76 颗星逐颗归位到实测坐标。
项目入口:在线体验 https://tianwenzzzh.github.io/meow-starmap/nuc/ | 开源模板在 GitHub 搜索TianWenzzzh/meow-starmap(含 15 分钟复刻教程、普查拍摄规范与跨校落地手册)
本文所有数据(76 只在编、194 张照片、测试项数、性能实测)截至 2026-09 的 v2.9.1 版本。猫咪昵称与军衔均为趣味设定,不涉及任何真实人物。