artcraft 这个名字,是我给一个个人数字创作项目起的,art 加 craft 的组合,直白一点理解就是“艺术与手艺的融合体”。它做的是把传统手工艺里的耐心、可控和打磨,和计算机图形里的批量生成、参数调整结合起来。整个项目目前跑在浏览器里,包含像素画编辑、体素建模、程序化纹理生成、调色板管理、多格式导出等一整套轻量创作工具,素材和最终作品都可以一键落地。做这件事的初衷很简单:很多搞创作的朋友并不需要打开大型软件去画一张小头像或做一套游戏图标,他们的真正需求是一个“上手快、能导出、可复现”的轻量工作台。这篇文章主要服务三类人:对像素艺术和体素建模感兴趣的新手创作者、想在简历里放一个完整工具链作品的前端开发者,以及做独立游戏想先跑通素材管线的开发者。
1. 先想清楚 artcraft 到底要解决什么问题
1.1 名字与定位:它的核心不是“生成”,而是“可控地制造”
名字本身已经说明了它的气质。我见过太多工具把艺术创作变成“点一下生成”的黑盒,用户抽卡似的不断刷新,得到一堆看起来炫酷但无法修整的结果。而我想要的创作方式,更像是传统手工匠人做事:把材料准备好了,再一步步雕刻、调整。所以 artcraft 里大量操作是“半自动”:程序负责批量生成或铺底,人负责选择和修改。这种设计哲学直接决定了功能的取舍:每个生成器后面都跟着一个可编辑结果面板,而不是一个不可控的随机结果导出按钮。
从功能形态上说,它不是一个单一应用,而是多个小工具的组合。我最初只写了一个像素画板,后续发现纹理生成、调色板、体素这些环节是同一批人在创作中都会遇到的,于是把每个工具都做成了相对独立的模块。模块之间通过统一的项目文件格式沟通,这个格式就是 artcraft 的“工程文件”,顶层是一个 JSON,里面记录画布尺寸、图层列表、画笔记录、调色板索引和元数据。这个设计在后来的迭代里帮了大忙:不同的工具可以共用同一份调色板,也可以把像素画直接映射到体素侧面。
很多人会问,这种工具和直接调格式刷、调色板插件有什么区别。区别在于项目文件本身是结构化的,创作者可以把“过程”也保存下来。比如一个纹理的生成参数,我可以用一段描述性配置完整复现,而不是只留下一张最终图片。这种可复现性对平时做素材管理很有价值——下次项目要用同一套背景风格,不需要再靠记忆去碰运气调参数。
1.2 为什么坚持浏览器端:一个创作者工具的形态决定了门槛
项目最开始是用桌面框架做的,但用了不到两个星期就放弃了。原因很现实:目标用户里有一半是拿着平板或普通笔记本做素材的人,让他们为了画 16x16 的图标专门装一个安装包,门槛太高。换到浏览器端之后,整个体验变成了“打开链接就能用”,对作品分享也友好得多。基础技术选了 TypeScript + Canvas 2D,部分模块用到 WebGL。这里有一个容易被忽略的点:Canvas 2D 其实比很多人以为的更强。对于像素画和纹理生成来说,绝大多数操作是逐像素写入像素缓冲区,再通过 putImageData 上屏,这比用 DOM 节点拼格子快几个数量级。再加上 OffscreenCanvas 可以在 Web Worker 里计算,生成 1024x1024 的噪声纹理时,主线程完全不会卡顿。WebGL 只用在体素模型的实时预览,因为那一块确实需要深度缓冲和三维变换,用 2D 画布去模拟投影会非常别扭。
工程结构上我坚持“单一画布 + 统一事件总线”的架构。每个工具拥有自己的画布和状态机,但所有跨模块操作,比如“把当前调色板应用到头像底色”,都走事件总线。这样做的好处是接插件化非常容易,后面加新工具都不用改旧代码。举个例子,体素模块是后来加的,我只需要注册一个独立的画布实例,再监听总线里的“调色板变更”事件,就能立刻复用像素画模块已经调好的颜色管理能力。这个架构对一个人开发的工具型项目来说,比引入重型框架要合适得多。
1.3 目标用户与真实使用场景:独立游戏、头像素材、教学演示
artcraft 的目标不是取代专业软件,而是解决中间地带的需求。比如独立游戏开发早期的素材定调、社区活动的头像批量制作、课程里给学生演示“程序化图形如何生成”、甚至个人作品集里放一组像素风格的界面设计稿。我一个实际用得很多的场景是给开源项目画 logo 和横幅:先用像素画工具画出主图形,再用纹理生成器做背景质感,最后用调色板工具统一全图配色。整个过程不需要切换软件,单浏览器就能完成。
假设一个独立游戏开发者想要一套森林主题的按钮图标,他的工作流可以是这样的:在调色板模块里用“色轮分割”生成一组以绿色系为主的候选色,锁定其中三五个;然后打开像素画编辑器,把画布设置成 32x32,用对称画笔快速画出树叶轮廓;再用纹理生成器生成一张 256x256 的噪点纹理当作背景材质,叠加一层低透明度渐变后导出 PNG。整个过程不到十分钟,得到的素材风格统一、可复现、不依赖网络资源。对前端开发者来说,这个项目本身也是一个很好的技术练兵场,它包含了 Canvas 绘图、状态管理、颜色科学、Web Worker、IndexedDB、GLTF 导出等多类常见问题,麻雀虽小五脏俱全。
2. 核心模块拆解:四个最容易踩坑的创作环节
2.1 像素画编辑器:数据模型、撤销重做与对称画笔
像素画编辑器是整个项目里最基础也最重要的模块。它的核心数据结构非常简单:一个宽度 x 高度的二维数组,每个格子里存放的是调色板索引而非颜色值。这是很多人忽略的一个关键设计。如果格子里直接存 #ff00ff 这样的颜色字符串,后续换主题色、批量替换颜色会很麻烦;存索引则意味着调色板替换只改一个映射表,所有像素自动整体变色。这个思路其实和现代游戏引擎里的索引贴图如出一辙:内存占用小,颜色替换快,数据也更适合压缩和序列化。
撤销重做机制我选了快照方案,而不是命令模式。原因也简单:像素画的单步操作代价很小,但操作频次极高,拖动画笔一笔可能跨几百上千格。命令模式需要记录每个格子的旧值和新值,反而更加复杂。快照方案是每完成一次“笔触”或“一次工具操作”,就把整个二维数组序列化压栈。为了节省内存,项目里做了一个 16 层的环形压缩缓冲,相邻快照会自动合并中间态。实测 64x64 的画布,几百步撤销完全不会卡顿。这个方案有一个边界条件要处理好:如果不小心画了很长的线条导致单步操作覆盖大量区域,内存占用会暴涨,所以需要在压栈前判断差异格子的数量,超过阈值时强制压缩一步。
画笔这边除了基础铅笔,核心是“镜像画笔”:以中心轴为基准,把一次绘制同时映射到上下左右四个方向,这样画对称图案非常快。实现上不复杂,只要在绘图时对坐标做三次对称变换然后落到同一个临时画布上,最后统一提交一次状态。注意顺序问题:必须先对坐标变换后的点做边界裁剪,再写入像素区,否则容易越界。另外还有一种“抖动画笔”比较好用:每次绘制时在原始坐标上附加一个 1~2 像素的随机偏移,模仿铅笔手绘的质感。这里的随机偏移同样要用种子驱动,否则撤销后重画会得到完全不同的笔迹。
2.2 程序化纹理:从值噪声到分形叠加的确定性生成
纹理生成器是 artcraft 里最有算法浓度的模块。我一开始想着集成复杂的卷积神经网络风格迁移,后来发现这是个典型的过度设计。对于纹理合成来说,程序化噪声和分形叠加已经能覆盖大部分需求。最基础的实现是值噪声:先在整数格点上随机生成一系列灰度值,然后对坐标范围内的点做双线性插值。为了让画面更自然,会从一层噪声叠到多层:每升一层,频率翻倍、振幅减半,最后把多层结果相加,这就是常见分形噪声,也叫 fBm。
代码上最关键的地方是不要把随机数函数做成闭包状态,而要使用“种子驱动的确定型随机”:同一个整数种子,无论在哪台机器、哪个浏览器上运行,生成结果都一样。这样用户可以把种子和参数存进一个链接,分享给任何人时能精确复现同一个纹理,对协作和教学都特别有价值。实现确定型随机有一个小技巧:用整数哈希函数,比如将坐标和种子做位运算扰动后乘以一个大质数,再取小数部分,比很多语言自带随机函数要稳定得多。
在实际开发里我收敛得很快的一个经验:所有噪声参数统一走参数面板,但参数面板的数据结构要刻意区分“即时执行参数”和“需要重新生成参数”。比如频率、振幅变化可以实时重绘;一旦改成六边形扰动或者新的噪声类型,就要整图重新计算。如果混在一起,容易在拖动滑块时触发大量昂贵计算。参数面板我设计成一个纯描述对象,每项都带 key、name、min、max、step、default,生成器函数根据这个对象动态读取参数。这样以后加新噪声类型,不需要改表单代码,只要注册一个生成器描述就够了。
2.3 颜色系统:RGB、HSL、OKLCH 与你必须知道的感知均匀性
颜色这一块是最多坑、但前期最不起眼的模块。界面里用户看到的都是色块,但底层要做到 RGB、HSL、OKLCH 三个空间自由互转。RGB 到 HSL 有标准公式,核心是先把 RGB 分量归一化,找最大值最小值和差值,再根据最大值所在通道算出色调,这里最容易踩的是色调区间计算错误以及饱和度分母为零的情况。为了避免写错,我直接用成熟的转换函数并加了大量单元测试。测试用例不能只覆盖标准色,还要覆盖灰色、接近黑色的暗色,这些边界条件最容易暴露出实现缺陷。
调色板生成算法也是几种的组合。第一种是“色轮分割”:把色相均匀切成若干份,再固定饱和度和明度,生成一组和谐色。第二种是“随机漫步”:从种子色出发,每次在色相环上小步移动,适合生成有流动感的渐变组。第三种是“图片提取”:用 k-means 聚类的简化版本,把图片像素颜色聚成 8 到 16 类,用来从设计稿或摄影作品里提取主题色。聚类时不能直接对 RGB 距离做 k-means,否则结果会很脏;应该先转换到感知均匀的 OKLCH 或 Lab 空间再计算距离,这样聚出来的颜色才符合人眼判断。
针对像素画场景,我额外加了一个“色差检查”功能:分析两个相邻颜色在 Lab 空间里的色差 ΔE,如果低于阈值会标出警告,因为很多屏幕低清渲染下这类颜色会糊成一团。这个功能虽然只是一个小提示,但对像素画可读性的提升非常明显。在做体素表面着色时也有类似的概念:两个面如果色差很小,合并成统一颜色后视觉上几乎无损,这也是后面体素导出能做面合并的依据之一。
2.4 体素模型构建器:数据结构、拾取交互与 glTF 导出
体素模块是后来加进来的,最初只是想给像素画多一个展示维度。数据结构上,模型可以用稀疏存储:一个调色板数组加一组“坐标到颜色索引”的映射。实际用 Int32Array 保存,每个 voxel 占用四个整数分别表示 x、y、z 和颜色索引,读取和遍历都比较快。编辑交互用的是射线拾取:从鼠标点出发构造射线,与包围盒做 DDA 遍历,命中体素后高亮显示,点击时增删体素或在表面涂刷。这里一定要把拾取的结果和渲染结果分开缓存,否则每帧都重新遍历模型会导致明显卡顿。
说一个交互上的反直觉点:体素编辑器里“删除”比“添加”更难做。因为在旋转视角下用户想删的往往是背面的体素,直接点击可能选中前面的方块。我做了两套模式:默认模式只能编辑面向用户的表面体素,另一个模式允许穿透编辑,按住修饰键后射线会继续往里走,命中更深处的目标。这个细节在只用了几分钟的人那里没什么感觉,但长时间编辑时效率差距很大。体素模型的导出是重头戏。要导出标准 glTF,需要把体素数组转成三角网。最朴素的做法是每个体素生成 6 个面 12 个三角形,但这种方式会产生大量内部隐藏面,文件体积和加载性能都不可接受。我实现了一个简化面剔除:把一个体素的每个面只保留相邻体素为空的那些面,然后用面元合并算法把相邻同色面合并成更大矩形。这样导出同一个 16x16x16 模型,顶点数能降低 60% 以上。合并矩形部分的实现思路类似“种子填充的前缀和”,先判断哪些面可以合并成行,再把行合并成块。
3. 实操过程:从零跑通一个可用的创作闭环
3.1 工程搭建:最小闭环优先,别急着铺架构
很多人做个人项目容易一上来就铺大架构,结果三个月后还在搭框架。我自己的经验是先跑通一个最窄的核心闭环:一个页面、一块画布、一支画笔、一次导出。第一版我大概用了两个晚上就完成,之后就一直在上面迭代加功能。具体工程上,我用 Vite 快速初始化,框架选了 Vue 3,原因没有太多魔法,就是这个项目本身需要大量细粒度响应式面板,组合式 API 写起来顺手。状态管理完全没引入第三方库,自己写了一个极简 Store:核心是一个不可变状态对象加 emit 事件。对工具类应用来说,这比鬼复杂的中间件灵活得多。
目录结构按功能拆:components、lib、stores 三层,lib 里都是纯函数模块,这样可以单测。一个值得复用的技巧是画布组件封装。凡是绘制型工具,我都会建一个通用的 CanvasSurface 组件,它负责画布初始化、尺寸适配、鼠标/触控事件归一化,对外只抛抽象事件 like pointDown / pointMove / pointUp。业务代码永远不需要关心事件的坐标究竟是来自鼠标还是手指,也不用关心画布是在普通屏幕还是高 DPI 屏幕。高 DPI 适配用 canvas.width = clientWidth * devicePixelRatio 解决,绘制逻辑里统一用逻辑坐标,导出时再处理物理像素。
3.2 像素画板核心逻辑:一块 32x32 画布从事件到像素
这里给出一个迷你版实现思路,足够撑起一个最小画板。先初始化画布,设置逻辑尺寸 32x32,物理尺寸乘 devicePixelRatio。内存里维护一个 Uint8Array,长度等于宽乘高,每个元素存调色板索引。鼠标事件里,把 clientX 和 clientY 转换成画布逻辑坐标,再取整得到格子坐标,然后 setPixel。setPixel 的修改不要直接写到主缓冲区,而是先写到一个临时改动集,一次 mousemove 期间的所有改动集中批量提交,并触发重绘。重绘时循环格子,从调色板取颜色,逐像素写入 ImageData,最后 putImageData。32x32 画布这样写完全没问题,即使到 128x128 也能流畅。
如果还觉得慢,可以用局部重绘:只把 touched 的格子所在行重绘。不过这个优化收益在普通尺寸下不明显,反而增加复杂度,建议先不做。绘制过程中有一个容易忽略的细节:不要直接对 ImageData 里的每个元素做颜色字符串到 RGB 的解析,那会非常慢。正确的做法是在调色板每次变更时,预生成一个 Uint8Array 形式的 RGBA 缓冲,绘制时直接从预生成缓冲里拷贝。下面是一个简化的 TypeScript 片段,展示这种索引画布的最小实现:
type IndexBuffer = Uint8Array; function putIndexToImageData( imageData: ImageData, pixels: IndexBuffer, paletteRGBA: Uint8Array, width: number, height: number ) { const data = imageData.data; for (let i = 0; i < pixels.length; i++) { const colorOffset = pixels[i] * 4; const targetOffset = i * 4; data[targetOffset] = paletteRGBA[colorOffset]; data[targetOffset + 1] = paletteRGBA[colorOffset + 1]; data[targetOffset + 2] = paletteRGBA[colorOffset + 2]; data[targetOffset + 3] = paletteRGBA[colorOffset + 3]; } }这个写法避免了重复解析颜色字符串,性能会比直觉快很多。加上简单防抖后,拖动鼠标连续绘制也能保持流畅。
3.3 纹理生成接入:三层预览策略让大图不再卡死主线程
接入纹理生成器时最大的问题是性能和交互体验的平衡。我的做法是“三层预览”:最外层的参数面板是一个普通响应式表单,滑块拖动时只更新数值;中间层用 requestAnimationFrame 做合并,确保 100ms 内的连续变化只触发一次重绘;最底层则是一个 OffscreenCanvas 的 Worker 实例,真正耗时的噪声计算全部放在 Worker 里执行,完成后通过 postMessage 把 ImageData 传回主线程再上屏。这几乎是目前浏览器端处理这种任务的标准姿势。
这里需要强调一个坑:OffscreenCanvas 不是所有浏览器都默认支持。兼容做法是先检查 typeof OffscreenCanvas,不支持就退到主线程计算,只是 UI 会有肉眼可见的抖动。代码里用了两套路径,虽然只多几十行,却让项目的用户覆盖面扩大了不少。参数面板的数据结构我设计成一个 plain object,每一项都带 key、name、min、max、step、default。生成器函数根据这个对象动态读取参数,这样以后加新噪声类型,不需要新增表单代码,只要注册一个生成器描述就够了。
在 Worker 里计算纹理时,还要注意传递数据的耗材。postMessage 一个 ImageData 对象涉及结构化克隆,对象越大耗时越长。一种优化是直接把 OffscreenCanvas 的上下文也放到 Worker 里,让 Worker 自己 putImageData 到自己的画布上,再通过 transfer 把画布位图交给主线程,这样能避免一次大对象的克隆。实测 1024x1024 的纹理,从 30 毫秒降到了 5 毫秒左右,提升非常明显。
3.4 导出流程:像素放大与体素模型文件落地的细节
导出功能是工具类项目容易被低估的环节。像素画编辑器里,用户绘制的是 32x32 的逻辑尺寸,直接导出会得到一张很小的图。不能直接把 canvas 放大,那样生成的是默认抗锯齿模糊结果。正确做法是自己写一个最近邻放大:按 scale 倍数创建目标 ImageData,每个目标像素直接采样源像素坐标。也可以加上更高级的逐行插值算法,不过对像素画来说,最近邻最正宗,能保留硬边缘。如果用户需要更好的放大质量,可以再提供一步可选的分层平滑处理。
体素导出的流程我上面已经提过:先体素数组转成三角网,再做面剔除和面合并,然后按 glTF 格式拼成 JSON。这里最大的坑是 glTF 的坐标系统:模型坐标系是 Y 轴向上的右手系,而体素编辑器内部习惯用 Z 轴当高度。导出前要做一个坐标换轴,且要处理索引绕序,否则导入到其他软件后模型会左右翻转或者法线反向。这个问题排查了很久,后来是同时导出一个带线框的预览版做对比才定位的。还有一个容易漏掉的细节:导出 glTF 时要显式声明颜色空间和 Alpha 模式,否则有些引擎会把半透明材质渲染成黑色。
4. 常见问题与排查技巧:把踩过的坑整理成速查
4.1 画布性能:先看主线程占用,再谈换技术栈
像素画做到 256x256 时,如果每次都全量 putImageData,普通笔记本会掉到 30 帧以下。很多人第一反应是上 WebGL 或者用 offscreen canvas,其实先可以做两件收益很高的小事:第一,把绘制循环从事件回调里抽出来,放进 requestAnimationFrame 调度,让浏览器决定何时刷新;第二,把 ImageData 的写入从“按需创建”改成常驻缓冲,只在画布尺寸变化时重新分配。这两步做完,大部分普通尺寸的卡顿问题都能解决。如果还要更大画布,就需要分块渲染或者只用可视区域重绘。
但 256x256 对像素画已经是超大尺寸,更多时候卡顿来源于别的模块占住主线程,比如纹理生成在 Worker 里如果写得不好,会反复传大对象。所以排查时一定要用性能面板看“主线程占用”和“消息拷贝”两个指标,不要只看帧率。我遇到过一种情况:Worker 里计算很快,但每次 postMessage 传回一个大型 ArrayBuffer,主线程在接收时反而卡住。后来改成 transfer 转移所有权,问题立刻消失。所以优化性能时,传输语义与计算开销同样重要。
4.2 跨域图片与 Canvas 污染:导出黑屏的经典场景
纹理生成里允许用户上传参考图,这一功能本身不难,但图一旦画到画布上,Canvas 就会被标记为“被污染”。这时调用 toDataURL 会直接抛安全错误,toBlob 会得到空对象。这个问题只在浏览器环境下出现,本地保存或跨域配置不当都会触发。我的解决方案很简单:上传的参考图在进入主画布前,先经过一个“下载到本地并转成 Blob 对象再创建对象 URL”的步骤。如果来源不是本地文件而是远程 URL,就必须确保服务器返回正确的 CORS 头,否则宁可拒绝加载。这个处理逻辑虽然多绕了一圈,但能给用户清晰的错误提示,而不是莫名变成黑图。
这里还要考虑一种常见的操作习惯:用户从浏览器拖一张图片到上传区,他以为图片已经“本地化”了,但浏览器可能仍然保留着远程页面的源信息。所以我在上传区落地的第一件事就是把文件读成 ArrayBuffer,再基于本地内容创建 Blob,确保所有后续绘制都使用这个本地 Blob 的对象 URL。这样从源头避免了污染问题。
4.3 颜色空间陷阱:无彩色、Gamma 与视觉发灰
颜色模块是我单元测试写得最多的部分。RGB 到 HSL 的公式里,当 max 等于 min 时色相是没有意义的,但很多实现会返回一个 0,这在某些场景下会造成调色板排序错乱。处理方式是单独标记无彩色像素,在色相排序时它们永远排在最后。另一个坑是“直觉”和“公式”对不上。普通显示器在处理颜色时做的是非线性 Gamma 空间,但很多颜色距离公式直接套在线性空间或者 sRGB 空间上,结果会偏向某几种通道。特别是在做体素表面着色和纹理叠加时,如果两图层颜色都按“数字平均”去混合,视觉上会发灰。项目里统一把颜色转成线性空间做混合,再转回 sRGB 显示,这一步对质感提升非常重要。
这个问题的直观解释可以类比为“两个朋友各说一半的话,你用算术平均去理解,往往会丢失语气和重音”。颜色也是一样,暗部和高光在感知上并不是均匀分布的,用简单的算术混合容易抹掉层次。正确做法是先解码 Gamma,在线性空间里做混合,再重新编码回显示空间。为了不让这个逻辑污染所有业务代码,我封装了一个 color 工具库,所有模块统一走它提供的接口,避免有人直接拿 hex 字符串做加法。
4.4 体素编辑体验:拾取遮挡、历史记录与文件体积
体素编辑器的第一版我用的是教科书式的“鼠标点击添加方块”。实际测试时,有朋友抱怨“不知道为什么越编辑越乱”,我意识到问题出在立体感上:没有辅助网格时,用户很难判断当前视角下方块与方块的前后遮挡关系。后来我加了三件套:地面网格、半透明外框高亮、每一步编辑的轻微自动旋转提示。这些都不是“算法”问题,而是交互问题,但对编辑体验的影响比算法更大。另外,体素项目文件的体积会随着编辑历史快速增长。我给 undo 历史做了一个限制:只保留最近 50 步,超过后自动丢弃最旧快照。对于体素这种每步操作可能涉及多区块的情况,这个限制能保证项目文件不会无限膨胀。
还有一个体验优化值得记录:当用户在当前视角下鼠标悬浮到某个体素表面时,我会把这个体素所在面用半透明材质高亮,但不改变实际颜色数据。这个“预览高亮”是独立于模型数据之外的一层渲染,所以用户撤销或者截图时都不会留下痕迹。它就像是问你“你确定要在这里操作吗?”,在 3D 场景里特别有用。
5. 下一步扩展方向与我的个人体会
5.1 真正能提升价值的两个功能方向
项目做到现在,我自己的判断是“功能数量已经够了,缺的是创作社区化的外皮”。后续比较理想的扩展有两个方向:一个是把调色板、纹理参数、像素画工程文件做成可分享链接,让用户之间能直接交换创作流程;另一个是给体素模型加简单的骨骼或关键帧动画,让它可以导出 Web 端可播放的轻量动画格式。前者的价值甚至可能大于后者,因为对创作工具来说,案例和模板的流动才是增长引擎。如果一个用户看到别人分享的“森林地面纹理生成参数”,可以一键导入并改成自己的画布尺寸,工具就不再只是工具,而是变成了传播创作方法的媒介。
第二个方向其实已经在内部做过很小规模的实验。我给一种纹理生成器加了一个“渐变扰动”参数,允许用户在最底层噪声之上叠加一个方向渐变的遮罩。很多用户拿到之后开始做“光照模拟”:把渐变的亮部当作光源,暗部当作阴影,再叠加一层细节噪声,得到的效果特别像传统手绘里先铺大调子再画细节的步骤。这个功能很简单,但它验证了一件事:参数化的创作流程如果想被复用,参数的命名、分组和默认值必须非常讲究,要让人觉得“这不是一堆滑杆,而是一套有逻辑的创作步骤”。
5.2 做这个项目最大的收获:打磨比堆叠更接近“craft”
最后说一个我在实际开发中反复验证的经验:个人项目千万不要一开始就追求“全平台、多语言、一键部署”,先把一个最窄的闭环做到好用,再逐步加功能。artcraft 的很多模块都是踩过坑之后才补上的,比如颜色空间的统一、跨域图片处理、体素面剔除等等。如果一开始就把这些全部设计进去,项目大概率会胎死腹中。现在回头看,最值得骄傲的不是代码量,而是每一步迭代都解决了一个真实问题,这正是“craft”这个词想表达的:不是堆砌,而是打磨。
另一个小技巧是给项目里的每类文件都留一套“诊断导出”逻辑。比如导出像素画时同时导出一份带网格线的调试版,导出体素时同时导出一份线框版。这些调试文件在排查问题时作用很大,平时却完全不打扰用户。把调试能力留到项目内部,而不是等到出了问题再临时加代码,是减少挫败感很有效的一招。如果你也在做类似的创作工具或图像处理项目,我建议你从一开始就把这个习惯保留进来,后面会发现它远比临时抱佛脚有效。