一、起因:测光表不准,备用手机上装了个测光 App
用胶片相机这件事,满打满算一年多了。
我手里这台是台老机械机,别的都还好,就是测光表不太靠谱。光线充足的时候勉强能用,光线一暗,读数就开始飘,照着它拍出来的片子经常欠曝。一卷胶卷三十多块,加上冲扫二十几,废掉一半,心疼是难免的。但老机器就是这样,测光元件年头久了灵敏度下降,这是通病。
后来在备用手机上装了个 Light Meter 测光 App,拍之前对着场景测一下。原理很简单:用手机摄像头当测光表,测出环境亮度换算成 EV 值,再按你选的光圈、快门、ISO 给建议。这东西比机身里那块老表靠谱,装上之后我拍片的底气足了不少。
但用了一阵子也发现了它的局限:测光 App 只管测光。摄影里其他的计算——景深、闪光灯、ND 滤镜等效快门、胶卷互易律——我之前全是靠心算的,公式记在脑子里,现场心算,慢不说,还容易出错。
有一天我忽然想明白:测光原理这么简单,现在 AI 又这么好用,我为什么不自己试试?把摄影里那些"算起来麻烦"的计算全装进一个工具里,该多省事。
二、第一版:跟 DeepSeek 说了一声
我打开 DeepSeek,把需求描述了一遍:帮我写一个网页工具,摄影测光用的。输入测光结果,选好胶片感光度,给我推荐一组光圈快门组合,带曝光补偿。
它问了我几个问题,然后给了一段 HTML 代码。存成文件,用浏览器打开——白底页面,几个输入框,一个按钮,一行结果。填上数值,点一下,推荐的光圈快门就出来了。
第一版很简陋,没有样式,手机上打开字号小得可怜。但我当时挺兴奋——不是因为功能强大,而是因为这个工具是我自己的,想要什么,跟 AI 说一声就能加。
三、模块越加越多,十来天攒出七个
第一版做完,我冒出个念头:既然曝光计算都做了,不如把摄影里其他"自己算很麻烦"的计算全都加进来。于是接下来的十来天里,模块一个接一个长了出来:
光学计算——算视场角、景深、超焦距。拍风景想知道收到 f/11 能不能让近景远景都清楚,输入焦距、光圈、画幅,它直接给出超焦距距离和清晰范围,还画一条景深预览条。更实用的是反算:想让前后某一范围都清楚,它反推该用什么光圈。拍风景、拍合影、街头估焦抓拍都用得上。
闪光灯计算——核心公式是 GN 除以距离等于光圈,公式简单,但数值心算很麻烦:GN 是几十的两位数,距离带小数,除出来往往不在整档上,还要换算成功率百分比(1/2、1/4、1/8 递减)。夜景人像、室内布光,输入型号自动填 GN,直接给光圈和功率。
高级曝光——长曝光的三类计算:ND 滤镜等效快门(ND1000 减十档,1/125 秒变 8 秒,两块叠加更容易算错);胶卷互易律失效补偿(曝光超一秒胶卷感光不线性,要按厂家补偿表加时间);B 门计时(超 30 秒要用 B 门,按着快门还得看表)。
天文计算——星野摄影的 NPF 法则算最长曝光时间,考虑焦距、光圈、传感器像素,还有赤纬和边缘修正;外加目标方位角,输入经纬度和时间,告诉你北极星在哪、某天体几点升起来。
暗房计算——研究胶片冲洗和印放方法的时候发现全是计算:显影温度每差一度时间调 10%,药液 1+1、1+9 配比换算,放大机曝光时间跟倍率平方成正比。
移轴和皮腔——沙姆定律算镜头倾斜角(拍建筑校正透视、控制焦平面),皮腔算有效光圈补偿(拍微距,等效光圈 = 光圈 × (1+倍率),不知道必欠曝)。
七块功能,从测光到移轴、从星空到暗房,基本覆盖了摄影里所有"算起来麻烦"的场景。工具本身的故事到这儿就差不多了——接下来真正花时间的,是跟 AI 一起把代码改对、改稳的过程。那部分占了我这十来天大一半的功夫,也最有意思。
四、和 AI 一起写代码:分工逐渐清晰
一开始我是"让 AI 写、我用人肉测"。测出来不对,把现象描述给 AI,它改,再测。第一版几个模块就是这么磨出来的,那会儿开发记录里还留着一个经典桥段:工具刚做完,我让浏览器自动化帮我跑了两轮验证,抓出两个 bug——一个变量没定义,一个闪光灯自动快门没换算同步速度。那是第一次体验到"让 AI 自己测 AI 写的代码",效率完全不一样。
但随着模块变多,问题来了:改一处,崩三处。有段时间每次改动都像打地鼠。光学模块加了新功能,结果景深近界限算错了;修好近界限,超焦距的显示又不对了。更要命的是有些公式之间的依赖是隐性的——改了一个,另一个模块的结果悄悄变了,不仔细比对根本发现不了。
人肉测试跟不上了,我开始琢磨怎么让 AI 帮我"管住"这些代码。
五、改一处崩三处,于是有了 211 条用例
解决的思路是让 AI 建一套用例测试体系:把每个计算函数的典型输入输出固化成用例,正常值、边界值、甚至故意输 0、输负数、输超大值的都有,一共 211 条。每次改完代码,跑一遍用例,全绿才敢上线;改公式之前,先想清楚这条用例的期望值要不要跟着更新。
这套体系改变了我跟 AI 协作的方式。以前是我说"这里好像不对",AI 改完说"应该好了",我再去试——对没对,凭感觉。现在是:改之前先定好"什么算对",改完跑一遍,211 条全绿才算完事。它把"感觉应该对"变成了"验证过是对的"。
后来这套用例还派上了更重要的用场:后面几轮大的改版——分享功能重写、界面统一、移动端适配——全靠它兜底,改完跑一遍没变化,才敢放心提交。
六、分享卡:两套方案失败,第三套才定稿
工具功能齐了之后,我加了个"保存截图"功能——把当前面板存成图片,方便发朋友圈。这个功能折腾了我很久,前后换了三套方案,是整个过程里最大的一个坑。
第一套方案:SVG 克隆。 把页面内容整个克隆进 SVG 再转图片。结果浏览器出于安全限制,不允许把嵌了网页内容的 SVG 转成图片——导出来全是空白。线上复现,4 组对照变体,全是空白。
第二套方案:html2canvas。 一个把网页渲染成图片的成熟库。这次本地测试完全正常,兴冲冲推上线——结果线上还是空白!本地正常、线上空白,这类问题最折磨人。查了很久才发现根因:页面里有"克隆面板挂到文档底部离屏"的优化策略,这个策略在浏览器安全机制下不成立。改成库的标准用法(直接截当前可见面板,二维码用 canvas 拼上去),才终于出图。
第三套方案,又踩坑。 用了一阵子,截图开始黑框、排版错乱。这次的原因很技术:工具页面用了 CSS 变量、网格布局、内外阴影这些现代 CSS 特性,而 html2canvas 的原理是在 canvas 里把页面"重画"一遍,它认不出 CSS 变量,阴影画不出来,布局也对不上。页面用得越"现代",它越无能为力。
两套方案都死了,最后定稿的方案是:不依赖任何截图库,纯手绘。先把要展示的内容收集起来(标题、数字、表格、警告),用最基础的 SVG 元素——矩形、文字、线条——直接画一张卡片,二维码用矢量图形嵌进去,不带任何网页内容。这样画出来的图不依赖浏览器渲染,怎么导出都不走样。
这个方案的验证也很有意思:浏览器里首次出图还有透明背景黑块的问题,后来直接在 Node 里复用生成卡片的代码,用无头浏览器渲染,再写了个 PNG 解码器做像素级分析——4 个面板、深浅两种主题,黑块 0%、尺寸正确、二维码区域正常,全过才放心上线。
教训我记了很久:当一个功能依赖"把网页截成图"的时候,现代 CSS 是最大的敌人。绕开它,用最笨最直接的办法,反而最稳。
七、一行声明引发的连锁故障
截图功能稳定之后,又遇到一个更隐蔽的 bug,查了大半天。
现象是:分享链接打开后,页面数值不自动重算——网址里明明带着参数,打开却是空的。这个 bug 最折磨人的地方在于,本地怎么测都复现不出来,本地正常,线上不行。
最后是一个模块一个模块地查初始化函数,才找到根因:某个函数里,一行变量的声明位置太靠后,它被使用的时候还没初始化,抛了个错——而这一下,把后面所有模块的初始化全中断了。前面几个模块正常,后面的全哑火,看起来就像"随机失灵"。修法就一行:把声明挪到前面。但找它花了大半天。
这类问题让我长了个记性:多个模块共用全局状态的时候,一个模块初始化失败能拖垮整个页面。 所以后来让 AI 给代码加了个"初始化顺序检查"的机制,每次构建前自动校验,从根上防止再犯。
八、改完不生效——缓存三连坑
还有一类坑,属于"改了、上线了、没生效"。
第一次遇到是改完 JS 推上线,打开一看还是旧版。强制刷新没用,清缓存也没用,反复确认服务器文件已经是最新版,浏览器就是显示旧的。原因很简单:浏览器把 JS 文件缓存了,文件名没变,它就不重新下载。解决办法也简单:给 JS 文件加版本号参数,改一次升一版——ui.js?v=10——文件名变了,浏览器就会重新拉取。
这个坑第一次解决之后,我以为完事了,结果后面又中招两次:一次是内嵌浏览器(App 里的网页视图)有更顽固的启发式缓存,光加版本参数不够,还得在服务器上给页面加"禁止缓存"的响应头;另一次是旧版页面残留在线上,跟新版 JS 版本错配,需要在入口加一个 0 秒跳转页把访问引到新地址。
从那以后我养成了习惯:每次改完前端代码,先确认版本号升了、服务器缓存头没问题,再让用户刷新看效果。
九、手机上字太小——分享卡按手机重做
分享功能上线后用了几天,我自己在手机上打开分享出去的卡片,发现字小得看不清。原版卡片是按电脑屏幕设计的,1800 像素宽,在手机上缩到 400 像素显示,字就剩三四个像素,跟蚂蚁似的。
这次的需求很明确:限制宽度,不限制长度——手机上下滑动看长图。于是把卡片版式整个重做:宽度固定 720 像素(手机上看正好),高度随内容自适应,内容多了往下延伸;字号全部加大,表格 24 号、标题 28 号、主结果 44 号,手机上清清楚楚。
验证照例交给无头浏览器:6 个场景像素级检查——720 宽、黑块 0%、深浅主题、极端超长内容不崩溃——全过,加上 211 条用例全绿,才推上线。
十、验证方法论:怎么证明"改对了"
这十来天的开发,最大的收获不是工具本身,而是一套"怎么证明代码改对了"的方法。整理下来就三条:
- 用例锁定行为:211 条用例固定每个函数的输入输出,改动后行为不变才有保障
- 像素级验证界面:界面类改动靠人眼看不可靠,用无头浏览器渲染 + 程序分析像素(黑块、尺寸、颜色),用数据说话
- 双环境对照:本地正常不代表线上正常,浏览器安全机制、缓存策略都可能让线上表现不同。凡是线上出问题,第一反应不是"代码没错",而是"线上和本地哪里不一样"
这三条看着简单,都是踩坑踩出来的。
十一、尾声:下一站,鸿蒙
这个计算器从冒出念头到定稿,前后不到一个月,改了二十多个版本,如今挂在我的网站上,手机打开就能用。
前阵子我在鸿蒙应用商店里逛了一圈,发现这个生态里好像还没有一款功能齐全的摄影计算软件——测光表有,景深计算器有,但都是单功能的,没有一个把所有场景收进来的。
要说心里没点想法,那是假的。但真移植的话有个现实问题:最早的网页版是"所有代码塞在一个 HTML 文件里"的写法,界面、逻辑、公式全搅在一起,想搬都没法搬。所以过去几个月我一直在做一件事:把架构拆开。现在工具分成了四层——数据层(参数、型号库,纯数据)、引擎层(所有公式,独立纯函数,不碰网页)、界面层(只管显示交互,不含公式)、用例层(那 211 条用例,保证移植后结果和网页版一模一样)。将来迁到鸿蒙,就是把引擎层直接搬过去的事。
当然,这件事急不得,得等有空了才能做。但后路已经铺好了——也许哪天闲下来,我就让 AI 帮我把鸿蒙版做出来。到时候这个网站上,大概会多一篇"鸿蒙版开发记"。