← 返回作品

个人项目 · 独立设计与开发 · 2026

Carb Kun Carb

把相机对准盘子,它会变成一张贴纸回来,营养素也一并附上。

开发进行中
30 秒速览

要解决的问题

饮食记录死在搜索框上。没有人愿意在小贩中心打出菜名再从九个结果里挑一个,习惯自然活不过第一周。

我做了什么

把记录设计成一个动作——对准相机,得到一张附带营养素的贴纸——并从这张贴纸推演出整套视觉识别。

结果如何

2026 年持续开发中:识别系统已完成,从第一个提交起就是双语,本页展示可交互复刻。

Carb Kun Carb 标志组合:一台头顶米饭碗的相机,旁边是文字标志

灵感来源

Strava 让一次跑步变成值得发布的内容,Instagram 让照片本身成为主角。Carb Kun Carb 想为你盘中的食物做同样的事:拍下这道菜,它会被从背景中抠出来,「噗」地一声化成贴纸,落进你当天的记录里,营养素早已附在上面——记录本身成了乐趣,而不是为了乐趣付出的代价。它专为亚洲的食物而做,也刻意做得可爱:一份小贩中心的饭,应该和一杯蛋白粉一样好记录。

项目概述

一套 Expo / React Native 代码库同时驱动 iOS 与 Android,TypeScript 严格模式,从第一次提交起就同时支持英文与简体中文。相机、抠图与动画运行在客户端;一个轻量 Node 服务负责分割、菜品识别与回顾视频渲染;Supabase Postgres 与 Storage 则由行级安全策略守护。这虽是个人项目,但我完全按生产级产品的方式来做——设计令牌自动生成、模型有评测集、访问规则有测试来证明。

我的角色

全部:品牌与视觉识别、交互设计、React Native 应用、后端服务、数据库结构与安全策略,以及从唯一数据源重新生成整套设计系统的工具链。

背后的设计决策

贴纸是这个点子本身,但它真正成立的原因,是一系列关于「摩擦」的决策——哪里该拿掉,哪里留一点反而值得。

01

一个动作完成记录

把相机对准盘子。没有搜索框,没有份量选择器,也没有秤。记录流程里每少一步,这个习惯就多活一天——而搜索正是杀死它的那一步:没有人愿意在小贩中心里打出「炒粿条」,再从九个结果里挑一个。

02

营养素要分主次,而不是并列

先是展示级字号的热量,然后是作为「这一餐的形状」的分段条,最后才是各个标签。四个数字平铺出来是一张表格;分了主次,才是一眼就懂。分段条只是辅助,绝不单独承载信息——每个标签都仍然带着自己的名称和数值。

03

从第一个提交起就是双语

英文与简体中文从一开始就是一等公民,而不是最后再补的本地化,因为这个应用里有一半的菜名是中日韩文字。这约束了字体栈、行长与标签宽度——其中任何一项事后再改,都意味着整个版式要重画。

04

一路设计到最小的那个尺寸

这个图标是对着 29 像素的应用图标调的,而不是对着主视觉标准组合;每一个导航图标都活在 44dp 的点击区域里,并带着一个作为该控件唯一名称的无障碍标签。如果一个设计只在你画它的尺寸上成立,那它就还没做完。

标志:一台头顶午饭的相机

相机说明这是什么产品,饭碗说明它关注什么,而那张脸决定了整体调性——年轻人不觉得幼稚,长辈也不觉得疏远。

  • 像帽子一样戴着的一碗饭

    它是主角,不是点缀。碗的宽度必须远大于饭堆——这个比例是它在 29px 下不被看成一朵蘑菇的唯一保障。

  • 会回望你的镜头

    脸长在镜头里,于是相机和角色是同一件东西,而不是一个吉祥物站在产品旁边。

  • 那颗闪光就是闪光灯

    它同时代表贴纸弹出的那一刻——应用里唯一的「小确幸」,被图标借用了过来。

  • 先有描边,再谈填充

    奶油色镜头压在柿橙机身上只有 2.57:1。真正拉开层次的是可可色轮廓,所以它永远不细于 1.5dp。

  • 文字标志在暗色下要翻转

    可可色压在浓缩咖啡色上只有 1.47:1——字会直接消失。暗色版组合把文字换成奶白色,而标志本身不变,因为它的可可色描边压在自己的亮色填充上依然清晰。这也是为什么暗色组合是一个独立文件,而不是一层 CSS 滤镜。

缩小之后依然成立

下面每一个尺寸都来自同一个文件。小于 40px 之后,脸只剩一个暗示,真正起作用的是轮廓——碗、机身、闪光。

180px
120px
87px
60px
40px
29px

iOS 会施加自己的圆角遮罩,因此交付的图标是不含透明通道的满幅方图。Android 拿到的是分层的自适应图标(标志与底色各一层),外加一个单色主题图标。

目前已完成的部分

01

贴纸制造机

照片 → 主体从背景中抠出 → 像素尘埃消散动画 → 命名并保存。三级抠图方案层层降级而非直接失败:服务端返回裁好的贴纸与配准的遮罩;端上通过 ML Kit 与 Apple Vision 离线完成;两者都不可用时则保留原图。所有原生模块都在带缓存的 try/catch 后惰性加载,因此同一份 JS 包也能跑在未链接原生模块的二进制上。

02

藏在接缝之后的菜品识别

由视觉大模型识别菜品并附上营养素,它被封装在一个带兜底实现的接口之后,因此供应商随时可换——而这确实必要。我按实测可用率而非版本号来选模型:一个更新更快的模型在八次探测中只答了两次。推理型模型还会先花光 token 预算再作答,并在结束思考标签前后把答案输出两遍;这两点都在解析层处理掉,而不是假装不存在。

03

用数据说话,而不是靠争论

我用自己拍摄并标注的食物照片建了一套评测集,因为不同的错误代价并不相同:名字错了只是表面问题,用户重打一遍即可;而菜品 ID 错了,会把另一道菜的营养素悄悄记到别人的食物上。置信度低于 0.72 时,界面会询问而不是断言——绝不把对他人饮食的猜测当成事实呈现。

04

本地优先,再同步

Zustand 与 AsyncStorage 是设备上的唯一真相来源;Supabase Postgres 与 Storage 在后台备份。行级安全策略把每一行数据锁定到它的归属者,并由一套 SQL 测试来证明可见性规则,而不是默认它成立。此外还有 Google 登录、换新设备后的下拉恢复,以及服务端的账号导出与删除——删除的执行顺序至关重要。

05

可以分享的一天

服务端用 sharp 把当天的贴纸拼成画面,再用 ffmpeg 渲染成视频,最后经签名令牌从对象存储分发——一段关于你今天吃了什么的短回顾,天生就是用来分享出去的。

菜品「噗」地变成贴纸

应用里唯一的「小确幸」,也是代码库中约束最多的部分。与页面同色的实心圆点在照片上依次弹出,把背景一点点吃掉,而抠好的菜品静静待在上面一层,纹丝不动。下面是它的网页版复刻——滚动到此处会自动播放,也可以点击重播。

  • 只用纯色视图

    总共两张图片,加约一百个小圆点。不用 Skia,不用图片切片——几百个由图片构成的视图会拖垮中端 GPU,动画在几帧之后就直接跳到终态。

  • 每个圆点都有自己的时钟

    没有共享的进度值。Reanimated 基于时间戳,一个中央驱动会让整个网格在一次卡顿后一起跳变。一百个独立计时则会优雅降级——掉一帧只让某个圆点晚几毫秒,别的毫无影响。

  • 圆点本身就是背景

    它们的颜色必须与组件背后的页面完全一致。哪怕差一个色阶都会露馅,因为这些圆点不是尘埃——它们是提前抵达的背景。

  • 严格只做一次分割

    揭晓过程中的第二次推理会把渲染管线饿死。这个问题让动画重写了三次,最后才发现真正的元凶在上游的拍摄处理里。

时间线:每个圆点在 800ms 的散布窗口内随机起跑,各自用 350ms 弹出,因此最后一个在 1150ms 落定。随后照片在圆点之下淡出,作为覆盖率的安全网——圆点不必自己做到完美覆盖——最后再停留片刻,让贴纸独自待一会儿,画面才继续。

视觉识别,运行起来的样子

由应用自己的设计令牌与图标几何构建,因此不会与真正交付的界面脱节。

⚠ 复刻界面,并非真机截图

今天
8 月 6 日 星期四
1,385 千卡
共 2 道菜
碳 155g蛋 40g脂 66g纤 7g
炒粿条
12:40 · 745 千卡
碳 76g蛋 23g
拍一道菜
今天
8 月 6 日 星期四
1,385 千卡
共 2 道菜
碳 155g蛋 40g脂 66g纤 7g
炒粿条
12:40 · 745 千卡
碳 76g蛋 23g
拍一道菜
留住你的贴纸
G 使用 Google 继续
或
密码
邮箱验证码
邮箱
you@example.com
密码
••••••••
登录

导航按钮为 44dp,并且独占一行。四个按钮加上间距约 200dp,在 360dp 的手机上无法与一个完整日期共处一行而不被截断——而日期是更有用的信息。

Google 按钮刻意游离于整套配色之外。其品牌规范要求标志出现在白色或近黑色表面上,柿橙并非选项。正是这个例外,让它一眼就被读作「那个 Google 按钮」——屏幕上其余的按钮都是我们自己的。

设计系统是生成的,而非手工维护

一个令牌文件是色彩、字体与贴纸几何的唯一真相来源。构建脚本据此生成应用的主题模块、SVG 标志与图标集,以及两大应用商店所需的每一张 PNG——因此视觉识别与真正运行的应用不会各走各的。主题文件与品牌 SVG 一律不手工修改。

奶白打底,只留一个高声调

奶白 Milk

#FAF4EA

主要底色

燕麦 Oat

#EFE1CB

下沉表面、贴纸台纸

可可 Cocoa

#4A3B32

文字与贴纸描边 · 奶白上 9.78:1

柿橙 Persimmon

#FF6B4A

唯一的高声调 · 填充与主要操作

抹茶 Matcha

#7FB069

膳食纤维与蔬菜

紫薯 Ube

#9B7EDE

蛋白质

黄油 Butter

#FFC94D

脂肪,以及那颗闪光

樱花 Sakura

#FF9EB5

仅作装饰 · 绝不用于文字

米色负责承托,柿橙负责发声。柿橙在奶白上只有 2.57:1——作为填充按钮恰到好处,作为正文则无法辨读——所以每个亮色都配有一个 -ink 变体,在两种浅色底上都达到 WCAG AA。

每一道菜都会变成一张贴纸

保存下来的菜品沿用与标志相同的构造——可可色描边、奶油色模切边、偏移投影——因此贴纸与应用图标看上去属于同一类物件。

椰浆饭640 千卡
  • 碳水 79g
  • 蛋白质 17g
  • 脂肪 28g
  • 纤维 4g
炒粿条745 千卡
  • 碳水 76g
  • 蛋白质 23g
  • 脂肪 38g
  • 纤维 3g
拉茶130 千卡
  • 碳水 21g
  • 蛋白质 3g
  • 脂肪 4g

此处数值仅作示意,并非真实营养数据。

四个图标,手工绘制而非采购

一套图标字体为了用其中四个字形要付出几百 KB,而且没有任何现成图标集里的「贴纸乐园」能一眼被读懂。它们用 react-native-svg 在 24 的网格上绘制,遵循与标志相同的规则。

日历
22 · 16 · 12
贴纸乐园
22 · 16 · 12
回顾
22 · 16 · 12
账号
22 · 16 · 12
  • 日历 — 月视图网格。刻意填满三天,因为那个页面想说的正是:有些日子有贴纸,有些没有。
  • 贴纸乐园 — 翻滚中的散落贴纸——是那个陀螺仪玩具,而不是一颗泛泛的星光。
  • 回顾 — 圆角框里的播放三角:是当天的一段视频,而不是又一台相机。
  • 账号 — 四个之中,唯一一个现成图标集也能做对的。

只用描边,绝不填充。它们要放进 44dp 的圆形按钮里,而填充式图标在 22dp 下会丢掉内部留白,糊成一团。描边宽度随图标缩放,但下限锁定在 1.5dp——与贴纸描边同一个下限,理由也相同:撑起形状的是轮廓,不是填充。

只有图标意味着无障碍标签就是这个控件唯一的名字。因此它是必填属性而非可选项——少了它,读屏软件会连续念四次「按钮」,导航彻底不可用。

圆润到可以当贴纸

标题

ui-rounded 800

Char Kway Teow 炒粿条

正文

system-ui

把相机对准你的盘子。Carb Kun Carb 会把这道菜抠出来、叫出它的名字,并把营养素归档——不用搜索,也不用称重。

数据

ui-monospace, 等宽数字

640 千卡 · 79 碳 · 17 蛋 · 28 脂

ui-rounded 是真实存在的 CSS 通用字体族,在 Apple 平台上直接映射到 SF Pro Rounded,零下载成本。Android 则必须自带字体:首选 M PLUS Rounded 1c——一款圆润无衬线体,且完整覆盖假名与汉字,而这个应用里有一半菜名是中日韩文字。Baloo 2 则把同一套圆润骨架延伸到天城文、孟加拉文与泰米尔文。

从设计过程中沉淀下来的原则

  • 颜色只是强化,绝不是唯一信号

    碳水、蛋白质与纤维三者的相对亮度相差不到 0.09,在红绿色盲下无法区分,因此每一枚营养素标签都同时带着名称与数值。

  • 置信度低于 0.72 时,询问而不是断言

    绝不把对他人饮食的猜测当成事实呈现。这个阈值首先是产品决策,其次才是模型问题。

  • 文案只存在于一个地方

    所有面向用户的字符串都放在多语言文件中,页面代码里不含任何文案。繁体中文语言环境刻意回退到英文——把简体字形呈现给繁体读者,看起来只会显得敷衍,而非贴心。

当前进度

Carb Kun Carb 仍在开发中,尚未上架。目前以开发版运行:拍摄、抠图、消散动画、菜品识别、当日视图、日历、回顾视频与同步都已完整跑通。剩下的,大多是不那么光鲜的另一半。

刻意列出的已知缺口

  • 还没有信息流。数据结构与安全策略已经支持「公开」与「仅好友可见」,但让它真正社交起来的浏览界面还不存在。
  • 营养数值是占位数据。真实数字需要有授权的区域性数据库——新加坡 HPB 的成分表、马来西亚的 MyFCD,或商业平台——而不是一张写死的列表。
  • 还没有内容审核工具,而在除我之外的任何人往里放照片之前,它必须先存在。
  • Android 上仍找不到既圆润、又能完整覆盖简体中文的展示字体,而这套视觉识别恰恰依赖它。
下一个案例: Blipflip