我把 FaceFizz.fun 的源码公开到了 GitHub。它是一款运行在浏览器里的趣味相机:用户不需要下载 App,也不需要注册账号,允许摄像头后就能实时体验鬼脸变形、镜像分身、贴纸、相框、艺术滤镜、节日装扮和背景场景,最后把照片保存到本机。
这个项目看起来不大,却把产品体验、浏览器媒体能力、实时图像处理、端侧 AI、隐私边界、多语言和响应式设计放进了同一条链路。本文不是功能清单,而是一次从产品目标到工程实现的完整复盘。
最先确定的不是技术,而是体验边界
趣味相机的价值不在于滤镜数量,而在于用户第一次打开页面后,能否快速看到一个有趣结果。为此,FaceFizz 给自己设定了几条边界:
- 打开网页即可使用,不把注册放在相机之前;
- 权限申请前说明用途,关闭拍照窗口时立即停止摄像头;
- 实时画面和生成照片默认不上传服务器;
- 首屏只展示八个精选效果,不用数量制造复杂感;
- 桌面端负责完整探索,移动端保证单手完成授权、选效果、拍照和保存。
这些约束决定了技术方案:FaceFizz 不是“上传照片—服务端处理—等待返回”,而是一条浏览器本地实时渲染链路。
页面效果
桌面端采用编辑式首页,把效果分类、预览拼贴、隐私说明和主操作放在一个页面内。用户可以先浏览,再打开相机。

移动端重新组织了信息密度,而不是简单缩小桌面布局。标题、效果卡片、相机入口和隐私提示依旧保留明确层级。

浏览器中的完整处理链路
FaceFizz 的核心数据流可以概括为:
用户授权摄像头
→ getUserMedia 获取 MediaStream
→ video 提供实时帧
→ 镜像与 cover 裁剪
→ Canvas 2D 效果管线
├─ 变形 / 镜像 / 拼贴
├─ 色彩 / 像素 / 艺术滤镜
├─ 贴纸 / 相框 / 节日元素
└─ MediaPipe 人像分割 → 背景合成
→ requestAnimationFrame 实时预览
→ 三秒倒计时
→ Canvas 导出 JPEG
→ 用户本地下载
浏览器通过 navigator.mediaDevices.getUserMedia 获取前置摄像头,理想分辨率设置为 1280×1280;真正的效果输出统一绘制到 720×720 Canvas。统一内部画布尺寸可以降低每种设备、每种视频比例直接参与效果计算带来的复杂度,也让预览和拍照结果共用同一条渲染管线。
渲染循环使用 requestAnimationFrame 驱动。视频帧先按 cover 规则裁剪,再根据镜像状态写入中间 Canvas,最后由当前效果完成变形、合成或颜色处理。拍照时不重新实现一套滤镜,而是直接从当前输出 Canvas 以 image/jpeg、质量 0.92 导出,避免“预览看到的效果”和“保存下来的照片”不一致。
为什么用 Canvas 2D,而不是先上 WebGL
FaceFizz 的第一阶段目标是验证完整产品链路,而不是打造专业图像引擎。Canvas 2D 已经可以覆盖:
- 区域拉伸与切片偏移;
- 镜像、四格和三联画;
- 像素化、反色和色调处理;
- 图片贴纸、相框和节日素材;
- 多层画布与遮罩合成。
它的优势是 API 简单、调试直接、与 DOM 图片素材配合自然,并且能快速覆盖桌面和移动浏览器。代价也很明确:复杂网格形变、大量像素级处理和高分辨率多层合成会逐渐挤占主线程。
因此当前选择是正确的 MVP 取舍,但不是终局。未来如果效果复杂度继续增长,应把高成本滤镜迁移到 WebGL2 / Shader,把可并行计算移入 Worker,并根据设备能力建立效果降级档位。
MediaPipe 只在需要时进入链路
背景替换需要知道哪些像素属于人像。FaceFizz 在浏览器中加载 MediaPipe ImageSegmenter 和本地 Selfie Segmenter 模型,不把摄像头帧发送给远程推理服务。
初始化时优先使用 GPU delegate;如果失败,再自动回退到 CPU。这比只写一条“使用 GPU”的理想路径更符合真实浏览器环境,因为不同设备、驱动和浏览器对 WebAssembly、SIMD 与 GPU 后端的支持并不一致。
人像分割也没有跟随每一次画面刷新运行。实时画面可以接近显示器刷新频率,但分割计算大约按 80ms 的间隔执行,最近一次遮罩会被复用于中间帧。这样把“画面渲染频率”和“AI 推理频率”分开,能够显著减少端侧模型对主线程和设备温度的压力。
视频帧 ───────────────→ 实时 Canvas 渲染
└─ 每约 80ms → ImageSegmenter → 人像 Mask
↓
背景图片 + 原始画面 + Mask → 合成结果
这个方案仍有优化空间:遮罩边缘平滑、头发细节、低端设备推理延迟,以及自定义背景大图的内存占用,都需要通过真实设备数据继续调整。
隐私不是一句文案,而是资源生命周期
“本地处理”只有落实到代码边界才有意义。FaceFizz 当前的隐私策略包括:
- 不要求账号,不建立云端作品库;
- 摄像头帧、分割遮罩和拍照结果默认只存在于浏览器;
- 用户按下快门后才生成 JPEG;
- 只有用户主动点击保存,图片才进入本地文件;
- 关闭相机时遍历
MediaStreamTrack并调用stop(); - 同时取消动画帧并关闭 MediaPipe 实例,避免摄像头指示灯或推理任务继续运行。
这套边界也降低了后端复杂度:MVP 不需要存储人脸图片、不需要处理删除请求,也不需要为用户作品设计访问控制。隐私设计在这里不是合规完成后的附加项,而是直接减少了系统成本。
效果系统:类型安全先于插件化
当前代码为分类和效果建立了明确的联合类型,例如 CategoryId、EffectId 和 Effect,效果选择、名称、颜色、分类与多语言映射都能在 TypeScript 中检查。精选效果负责首屏体验,更完整的效果分类则进入选择器。
不过,当前实现仍集中在一个较大的客户端页面组件中,效果渲染也主要通过条件分支组织。它适合快速验证,但当模板、素材和多人玩法继续增长时,会出现三个问题:
- 新增效果需要修改核心页面;
- 素材、元数据和渲染算法耦合;
- 无法独立测试、按需加载和远程发布模板。
下一步更合理的演进是声明式效果协议:
type EffectTemplate = {
id: string;
version: number;
category: "warp" | "sticker" | "frame" | "filter" | "background";
preview: string;
assets: string[];
renderer: "canvas2d" | "webgl" | "segmentation";
capture: { aspectRatio: "1:1" | "3:4" | "9:16" };
capability: { webgl2?: boolean; segmentation?: boolean };
};
页面只负责状态和交互,渲染器负责执行效果,模板通过配置注册。这样才能把“开发一个效果”逐步变成“制作并发布一个模板”。
七种语言不是简单复制字符串
FaceFizz 支持简体中文、繁体中文、英语、日语、韩语、泰语和马来语。多语言不仅影响按钮文字,还会影响标题长度、卡片高度、移动端换行、字体回退和 SEO 元数据。
当前项目用类型化字典保存分类、效果名称和界面文案,并把语言选择保存在 localStorage。这种做法对一个单页产品足够轻,但继续增长时应拆分语言包、按语言懒加载,并增加缺失 key 检查和截图回归测试。
项目还提供结构化数据、Sitemap、robots.txt、llms.txt 与 llms-full.txt。它们不会替代产品质量,却让搜索引擎和 AI 检索系统更容易理解页面是什么、能做什么。
工程验证与当前不足
项目采用 React 19、TypeScript、Vinext、Vite、Tailwind CSS 4 和 MediaPipe Tasks Vision,通过 ESLint、构建检查以及渲染 HTML 测试保证基础质量。
但源码公开不等于工程已经完成。接下来最值得投入的是:
- 把大型页面拆成相机控制、效果注册、渲染器和多语言模块;
- 建立 Chrome、Safari 和移动设备的摄像头测试矩阵;
- 记录相机启动成功率、首帧耗时、平均 FPS 和拍照成功率,但不采集原始画面;
- 增加页面进入后台后的暂停与恢复策略;
- 为低端设备提供关闭分割、降低画布尺寸等能力降级;
- 增加键盘操作、焦点管理和屏幕阅读器验证。
我从这个项目得到的判断
第一,端侧 AI 的价值不只是在“节省 API 费用”。它还能缩短反馈链路、减少网络依赖,并建立更容易解释的隐私边界。
第二,实时产品不能只看功能是否可用。摄像头启动、首帧时间、渲染帧率、推理频率、设备温度和资源释放共同决定体验。
第三,MVP 不需要一开始拥有最先进的架构。Canvas 2D、单页状态和本地模型足以验证核心体验;但必须诚实记录规模增长后要拆分的边界。
第四,真正的北极星指标不是访问量,而是用户是否成功生成、保存或愿意分享一张照片。技术最终服务的,仍然是那个让人笑出来的瞬间。
在线体验与源码
许可证说明:截至 2026 年 8 月 11 日,仓库公开了源码,但 README 仍标注 “All rights reserved”,且没有独立的开源许可证。从严格定义看,它目前属于“公开源码”,还不是允许他人复制、修改和分发的正式开源项目。如果希望社区参与,需要补充 MIT、Apache-2.0 等明确许可证。