为什么值得讲这个
「粉笔笔刷」听起来是绘画软件里最简单的东西之一 —— 不就是白线加点噪点吗。 但绝大多数一眼看上去就假的数字粉笔, 都不是败在美术上,而是败在几个很具体的工程判断上。 这些判断彼此独立,任何一个做错,整支笔就废了。 下面六条,不需要编程背景也能看懂。
坑一:把随机做进笔尖,会被平均成一片死灰
先说一个基本事实:屏幕上的一笔不是一条被画出来的线。 它是沿着你手指走过的路径,隔一小段就盖一个印, 印与印大量重叠,连成一条。走得慢,一段路上盖几十个印。
现在假设每个印自己带一套随机噪点 —— 这是最自然的想法,也是最常见的错误。 第一个印的噪点是这个花纹,第二个印是另一个花纹, 它们叠在一起,两边的孔洞互相填补。 叠到第十个,孔基本被填光了。 结果就是:你画得越用力、越慢,它越平, 最后得到一条中间死灰、只有两边毛一点的线。
这和真实完全相反。真粉笔反复涂同一处, 凸起的地方越来越实,凹的地方始终是空的 —— 因为凹凸是板的性质,不会因为你多涂几遍就改变位置。
所以正确的做法是:把粗糙度纹理钉在画布上, 每个印按它落在画布的哪个位置去查这张纹理。 这样每一个经过同一处的印,读到的都是同一块粗糙度, 它们互相加强而不是互相抵消。 Flying Chalk 用的是两层:一层细噪点做颗粒本身, 再叠一层大尺度的缓慢起伏,让颗粒成团—— 少了第二层,颗粒会均匀得像撒了盐,一样假。
坑二:干性笔迹是被咬出洞的实块,不是堆起来的点
第二个常见做法:把笔迹当成很多个半透明的小点, 点铺得越密,颜色越接近实。
这个模型对喷枪是对的,对粉笔是错的。 它做出来的粉笔像烟:整体雾蒙蒙, 永远达不到「这一块是实的,那一块完全没有」的干脆感。
真实的干性材料更接近另一个描述: 先有一层实心的沉积,然后被板面的粗糙从里面咬掉很多洞。 所以模型应该是做减法,而且要有一个明确的门槛: 沉积量不到门槛就完全不着色,过了门槛就是实的。 这个门槛的锐利程度本身就是一个可调的参数。
有一个很好认的症状:如果一支笔的基础不透明度被调得太低, 它永远跨不过沉积门槛,那支笔看起来就一直是烟, 怎么调噪点都救不回来。
坑三:笔立起来的时候,方位角是不可信的
要做出粉笔侧棱那种细长笔迹,需要两个数据: 笔杆放得多平(决定接触面被拉多长), 以及笔杆在屏幕上朝哪个方向(决定这道细长的形状躺向哪边)。
问题出在第二个。当笔接近垂直时, 笔杆在屏幕上的投影几乎是一个点 —— 它其实没有明确的方向。 硬件还是会报一个角度出来,但那个角度在相邻两次采样之间可能剧烈跳变。
直接拿这个原始角度去旋转笔迹的 App, 会让你把笔立起来写字时,笔迹肉眼可见地抖。 用户完全不知道发生了什么,只觉得「这个 App 的笔有点毛病」。
处理办法是把它当成一个方向而不是一个数: 用单位向量来表示和插值, 这样在两个采样之间过渡时走的是最短的那条弧, 不会出现从 359 度跳到 1 度时笔尖原地转一整圈的情况。
坑四:快笔会被采样丢掉,塌成折线
高刷新率屏幕上,触摸的采样频率比画面刷新频率还高 —— 系统一帧之内可能收集到好几个笔的位置,然后成批交给 App。
如果 App 只取这批里的最后一个位置, 那你在这一帧里画的其余轨迹就被扔掉了。 慢慢画的时候看不出来(一帧里手也没动多远), 但飞快甩一笔画个圆,出来的是三四段带折角的直边。
用户会把这个叫做「延迟」,其实它是采样不足。 要拿到完整的那一批,App 必须走能访问到这些额外采样的底层路径, 而不是图省事用上层的手势接口 —— 后者拿不到。 Flying Chalk 的画布就是为了这个建在底层视图上的。
坑五:速度不影响笔迹,就永远没有飞白
真粉笔甩快了会打滑,笔迹变淡、断开; 慢慢描,粉才堆积成实的。 书法里叫飞白,写板书的人都熟。
在盖印模型里,这件事其实很好办: 速度快 → 印之间被拉开 + 每个印本身变淡变细。 关键是必须主动去做—— 一支不看速度的笔,无论怎么调噪点,都出不了飞白, 因为飞白的成因本来就是速度。
这里还藏着一个容易翻车的细节: 「快」要怎么度量? 如果用屏幕像素来算,那么当你把画布放大之后, 每两个采样之间画布上其实没移动多少, 引擎会认为你一直在慢慢画 —— 于是恰恰在你放大抠细节的时候,飞白效果消失了。 正确的做法是拿当前笔径做尺度去衡量速度, 这样同一个手上动作在任何缩放倍率下含义相同。
顺带一提:这也是为什么手指画画依然能用。 手指报不出压感、报不出倾角、报不出方向 —— 它唯一能提供的表现力就是速度。 一支认真做了速度响应的笔,用手指画照样有飞白。
坑六:手指没有角度,就不能假装它有
接着上一条。手指的倾角和方位角读出来是 0, 而「高度角为 0」的含义恰恰是笔杆完全贴平在玻璃上—— 最极端的侧锋。
不检查触摸类型、直接把这些值往下传的 App, 会把每一道指尖笔迹拉成一条朝向随机的极端糊线。 这是自己写笔刷引擎时最常见的一个 bug, 而修法非常简单:读那些字段之前,先确认这次触摸确实是笔。
正确的降级方式是:手指画出正圆的笔迹, 压感取一个固定的中间值,全部表现力交给速度。 诚实地做成圆,比伪造一个倾角好看得多, 而且用户一眼就能理解发生了什么。
最后:存点,不存像素
把上面几条做对之后,还有一个架构层面的决定, 它决定了这个 App 后面能做什么、不能做什么。
一幅画可以存成一张图(每个像素什么颜色), 也可以存成一串笔画(每一笔走过哪些点,加上一个随机种子)。 后者要在每次打开时把这些点重新放一遍才能看到画。 听起来更麻烦,但它换来几件存图做不到的事:
- 撤销是精确的。撤销五次再重做五次, 每一粒颗粒落回原来的位置 —— 因为随机种子被存下来了, 重放会掷出完全一样的随机。基于位图的画布做不到,除非每一步都存整张图。
- 画完还能换板。 既然笔画是被重放的,那把底下的板材换成牛皮纸或者宣纸, 整幅画就在新的粗糙度上重新长一遍。现实里当然做不到。
- 图层几乎不要钱。 一个图层只是给每一笔记一个数字, 而不是像位图那样每层都要占一整屏的内存。
- 文件很小。存的是一串坐标。
还有一个附带的整齐之处:既然一切都是「沿点列盖印」, 那么几何图形也可以走同一条路—— 直线、矩形、椭圆、箭头、三角、菱形、星形、多边形 在 Flying Chalk 里都是先被采样成点列再交给同一套引擎的。 结果是画出来的矩形和随手画的线拥有同样的颗粒和飞白, 而不是一条突兀的、光滑的矢量边。 分步涂鸦模板也是同样存成点列,一步一笔, 而且笔顺是人工编排的 —— 矢量文件导出的路径顺序并不等于人下笔的顺序。
常见问题
为什么很多 App 的粉笔笔刷涂多了就变成一片灰?
因为噪点被做进了笔尖印章里。每个印带各自的随机花纹,重叠之后互相填补孔洞,叠够多次就平了。粗糙度必须钉在画布坐标上,重复经过同一处的印才会互相加强。
为什么有的数字粉笔看起来像烟?
因为它把笔迹当成半透明小点的堆积。干性材料更接近一层实心沉积被表面咬掉很多洞,需要一个明确的沉积门槛。基础不透明度调得太低的笔永远跨不过这个门槛。
笔立起来写字笔迹发抖是怎么回事?
笔接近垂直时,笔杆在屏幕上的投影几乎是一个点,方位角本身就是不可信的,相邻采样之间会剧烈跳变。把它当成单位向量插值而不是原始角度,就能稳住。
飞快画一笔为什么会变成几段直线?
系统在一帧内会成批送来多个触摸采样,App 只取最后一个就丢掉了其余轨迹。这是采样不足,不是延迟,用户在设置里改什么都没用。
放大之后飞白效果消失了,是 bug 吗?
通常是。如果引擎用屏幕像素衡量速度,放大后画布上的位移变小,它会误以为你在慢慢画。用当前笔径做尺度去衡量速度,同一个手上动作在任何倍率下含义就一致了。
为什么存笔画的点比存图片好?
撤销重做能精确复现同样的颗粒,画完之后还能换底下的板材,图层只需一个数字而不是一整屏内存,文件也小得多。代价是每次打开要把笔画重放一遍。
相关指南
拿一块黑板试试
Flying Chalk(飞白)是 iPad 与 iPhone 上的免费粉笔黑板画板。支持 Apple Pencil 压感与倾角,8 支粉笔家族的笔、11 块画板。无广告、免注册、无内购。