
1. 项目概述artcraft 到底在做什么artcraft 这个名字听起来很直接——art 加 craft艺术加工艺。但真正上手做这个项目之后我才意识到这两个词合成在一起的分量它不是一个单纯的生成艺术工具也不是一个传统手工艺的数字化文档而是一整套把手工质感转化为可计算逻辑的创作框架。我最初做 artcraft是因为一个很实际的痛点。工作室接到过一批纹理设计需求客户要的是那种看起来像手工编织、自然釉裂、老木纹的视觉素材。用布景拍照的话一组纹理要拍好几个小时而且可控性很差——光线、角度、材料批次都会影响效果。用现成的纹理库呢又容易被别人复用版权上也有隐患。当时我们就在想能不能用代码直接织出这些纹理让每一次生成都带着手工艺的随机性和温度同时又完全归我们控制。artcraft 的定位就这样定下来了一个面向设计师和创意开发者的生成纹理与图案创作工具包。它把传统工艺中的核心生成规则抽象出来——比如编织的经纬交错、陶瓷开片的裂纹扩散、木纹的年轮生长——然后用参数化和随机控制的方式重新实现。跑出来的东西不是简单的 2D 贴图而是带着自然瑕疵、有机走向、甚至某种拙感的视觉资产。这个项目比较适合三类人第一类是平面设计师想快速生成可商用的原创纹理素材第二类是创意编程爱好者想理解自然纹理背后的算法原理第三类是前端开发者需要在一套轻量级代码里实现复杂的图案生成逻辑。不夸张地说只要你能想清楚一种工艺的规则你就能用 artcraft 的思路把它变成代码。2. 整体设计与思路拆解为什么这样构建2.1 设计哲学受控随机与工艺规则优先先说一个我在实际开发中反复体会到的道理生成艺术最大的陷阱不是太随机而是随机太容易了。很多人写生成代码第一步就是Math.random()到处撒。跑出来一看确实什么都有但什么都不可用——因为它没有工艺规则约束。artcraft 的核心哲学是受控随机。拿编织纹理举例真实的一块手工粗布经纬线之间确实有误差——有的地方密一点、有的地方松一点、偶尔有一根线走偏了——但整体上它仍然遵循一上一下交错的基本规则。如果完全随机你得到的就是一坨马赛克只有把随机限制在经线间距 ±5%、纬线张力 ± 2%这样的范围内才能织出那种既规整又有手工感的纹理。所以 artcraft 里所有的生成器都遵循同一个模板工艺规则定义整体结构随机参数定义局部扰动规则永远优先于随机。这种设计哲学带来的好处是输出结果始终在可用的范围内波动而不是在失控的边缘试探。你调整参数时纹理在变但不会突然变得不像它本身。2.2 技术选型与工具链解析artcraft 底层用的是 JavaScript渲染层选的是 Canvas 2D API输出层支持 SVG 和 PNG 序列。好多朋友问我为什么不用 WebGL毕竟 WebGL 性能更强、看起来更高级。我的理由其实很简单这个项目的主要输出是 2D 平面纹理和图案单张画布的分辨率撑死也就 4096×4096这种量级 Canvas 2D 完全扛得住。而且 Canvas 2D 的 API 非常直观——ctx.beginPath()、ctx.moveTo()、ctx.lineTo()、ctx.fill()——每一步都跟真实的绘制行为对应尤其在模拟手工笔触、线迹这一类操作时这种一笔一笔画出来的心智模型特别顺手。引入 WebGL 意味着要管理着色器、缓冲区、绘制管线对纹理生成这个场景来说属于杀鸡用牛刀还把代码复杂度拉高了两个量级。在同一套工具链里SVG 承担了矢量输出。Canvas 负责屏幕上实时预览SVG 负责交付给印刷或矢量编辑软件两者通过一套统一的参数配置驱动。这个组合看起来朴素但稳定、易调试、可爬坡——任何人都能在半小时内看懂代码怎么跑起来的。2.3 从手工到代码三类典型工艺的数字化逻辑艺术创作和手工艺之间有一道很微妙的鸿沟。艺术家关心的是我想表达什么传统匠人关心的是我的手要怎么做而程序员关心的是机器的指令怎么下。artcraft 要做的就是在这三者之间搭一座桥。以编织为例。真实的织物由经线和纬线构成二者的交叉方式决定了织物组织。平纹组织就是一上一下斜纹组织是二上一下递移。这个规律非常结构化非常适合编码。但一条真实的经线是有毛边的——它不完全是一条均匀的直线粗细会变化张力会导致局部弯曲。所以在代码里我会先生成规则的网格交叉结构再在每条线的路径上叠加噪声偏移最后对线迹宽度做随机微调。这样数字化的结构保留着编织的秩序噪声则补上了手工的不完美。陶瓷开片又是另一套逻辑。釉面裂纹本质上是一个应力释放过程釉层在冷却时收缩比胎体大于是产生裂纹。从算法角度去看这近似于 Voronoi 图的边界——一组种子点各自扩张到相邻区域边界就是裂缝。真实的裂纹不是一条干净的分界线它会在扩张过程中产生分岔边沿有细小的崩碎。所以代码实现上需要用噪声扰动种子的扩张速率再让裂缝边界产生次级分支。木纹就更典型了。树木年轮的生成是一个同心圆 生长扰动的过程。树不是正圆的受养分、风向、光照影响每一圈都不同。年轮之间的间距有随机性早材晚材颜色深浅也不同。编码时我先用极坐标生成同心圆再给每个圆的半径加上低频噪声让圆变得不规则最后对环带颜色做渐变填充。这三类案例其实说明了一件事所有工艺纹理的背后都有结构规则 随机扰动双重逻辑。代码的作用不是替代手艺而是把这两层逻辑变成可调节的参数。3. 核心实现拆解与实操要点3.1 随机种子让随机可复现很多人第一次用 artcraft 会感慨每次生成都不一样。这个特性用来找灵感确实不错但设计交付的时候就是灾难——客户看完说这个纹理 OK结果你刷新一下页面纹理变了。你怎么交付所以随机种子系统是在项目第 1 版就必须落地的功能。方案就是实现一个可复现的伪随机数生成器。我推荐 mulberry32实现很短但随机分布质量足够好function mulberry32(seed) { return function() { seed 0x6D2B79F5; let t seed; t Math.imul(t ^ t 15, t | 1); t ^ t Math.imul(t ^ t 7, t | 61); return ((t ^ t 14) 0) / 4294967296; }; }使用方式非常直白生成器接收一个整数 seed返回一个无参随机函数每次调用返回 [0, 1) 区间的值。你看它内部只依赖 seed 的数值和位运算所以只要是同一个 seed不管在哪个机器上跑、跑多少次得到的随机序列都是一模一样的。这个特性在生成艺术里是刚需——你输出给印刷厂之前可以锁定一个 seed 反复微调其他参数最终交付时记录{ seed: 20241101, size: 300, density: 0.8 }对方就能在任意环境里精确复现同一张纹理。实战中有一个容易忽略的细节如果用Math.random()来生成给随机函数的种子那么每次刷新还是会不一样。正确做法是把 seed 暴露成一个独立参数由用户在界面上手动输入或点击随机一个。我习惯的做法是页面加载时默认从一个可预测的地方取值比如当天日期这样打开页面时大家看到的是同一幅基础纹理点随机按钮才更新 seed。3.2 噪声函数自然纹理的基石要让纹理自然不能靠纯随机。真实的自然界——不管是木头的纹理、石材的颗粒还是织物的起伏——都有一个特征相近位置的取值高度相关远处取值才逐渐无关。这就是噪声函数的用武之地。最常用的是 Value Noise原理比 Perlin Noise 简单却足够好用。我在 artcraft 里封装过一个极简版本大概就是在整数网格顶点上放随机值中间用平滑插值填充function valueNoise2D(x, y, rand) { const x0 Math.floor(x), y0 Math.floor(y); const x1 x0 1, y1 y0 1; const xf smoothstep(x - x0); const yf smoothstep(y - y0); const v00 rand(x0, y0), v01 rand(x0, y1); const v10 rand(x1, y0), v11 rand(x1, y1); return lerp(lerp(v00, v10, xf), lerp(v01, v11, xf), yf); }注意这里的rand(x0, y0)必须是根据整数坐标返回固定随机值的函数可以用整数坐标混合后作为种子再调用 mulberry32。smoothstep是典型的缓动函数可以用t*t*(3-2*t)实现它保证了网格点之间过渡连续、没有折痕。Value Noise 在 artcraft 里的使用场景非常多木纹年轮半径的扰动、编织线的走向偏移、釉面裂纹的分岔方向。你只需要把噪声频率和幅度做成可调节的参数就能从均匀的规则图案平滑过渡到扭曲的自然形态。频率控制细节密度幅度控制扭曲强度。这是纹理生成的万金油。3.3 游走画笔手工笔触的模拟canvas 里画一条线太简单了但画一条像手工绘制、带着呼吸感、有起笔收笔轻重变化的线需要专门实现。我在 artcraft 里管这个叫游走画笔walker brush。核心原理是模拟一支笔在画面上移动的状态。笔有自己的位置 (x, y)、速度 (vx, vy)、压力 (pressure)。每帧更新时速度方向被一个随机转向扰动压力则用一个低频率噪声调制。然后把压力映射到线的宽度位置加上一些小幅度摆动function walkerStep(w, rand, noise) { const angle Math.atan2(w.vy, w.vx) (rand() - 0.5) * w.turnFactor; const speed w.speed * (0.9 rand() * 0.2); const nextX w.x Math.cos(angle) * speed; const nextY w.y Math.sin(angle) * speed; const pressure 0.5 0.5 * noise(w.x * 0.02, w.y * 0.02); return { x: nextX, y: nextY, width: w.baseWidth * pressure }; }这种实现有个好处它模拟出的线条不会像 CAD 那样机械均匀而是呈现一种笔尖在纸面上轻微游移的质感。实际操作中turnFactor是最关键的参数取值低时线条接近直线取值高时线条像酗酒的人画的。用来做编织线迹时我一般把 turnFactor 控制在 0.1~0.3 之间刚好能打破直线感又保持整体的走势。3.4 色彩调和系统很多生成工具翻车就翻在色彩上。算法跑得挺顺结构也对但颜色一出就是一股廉价感。调研之后发现问题几乎都出在色彩饱和度太高且色相毫无关联上。artcraft 的解决方案是引入色彩调和系统。它不是一个简单的取色器而是一套基于前期工艺色板的约束规则。每个色板定义了 3~6 个基础色每个基础色派生出一系列的明度变体。生成纹理时颜色不能直接从 RGB 空间乱跳而是通过 HSL 空间在基准色附近小范围偏移偏移量同样由随机种子控制。一个可复制的经验是把色板里的颜色饱和度控制在 20%~45% 之间明度控制在 30%~85% 之间。这区间里的颜色不容易刺眼也不至于灰到没精神。艺术感往往就来自这些不怎么鲜艳的颜色之间的微妙关系。如果你想要更稳的方案可以直接从传统织物或陶瓷的实物色卡上取色然后把这些颜色的 HSL 值输进色板。大自然和传统工艺早就帮你调好了不会出错的组合。3.5 参数面板与导出链路artcraft 的界面层用的是 lil-gui一个轻量级参数面板库。我特意选它有几点考虑零依赖、体积小、配置代码直观而且它天然支持把参数对象序列化成 JSON。这个特性对生成工具尤其重要因为它让你可以把一整组调节好的参数存成一个 preset下次导入即可精确恢复同样效果。参数面板里的每一项都会在每次变更时触发重绘但为了避免拖动滑块时卡顿我用了一个 200ms 的防抖函数。如果你自己实现这行代码能解决很大的体验问题let timer; function scheduleRedraw() { clearTimeout(timer); timer setTimeout(draw, 200); }导出链路的设计上SVG 由一个独立的生成函数负责。Canvas 画一遍全图SVG 会重新遍历同样的绘制指令一一映射到 SVG 元素。这个设计有一个好处——最终的矢量文件保留了每一条线段、每一个节点的信息可以后续在矢量编辑软件里手动调整。为此代码模型上我抽象了一个中间指令列表Canvas 和 SVG 分别通过各自的渲染器解释这份列表。避免Canvas 一套代码、SVG 另一套代码的双维护。4. 实操案例三个可以直接跑起来的示例4.1 经纬编织纹理生成这是 artcraft 里我最常用来做演示的案例因为编织纹理的生成规则足够简单效果却很容易出彩。核心分三块经线组、纬线组、交织效果。首先定义编织密度。假设画布 800×800经纬各 40 条那么每条线的基准间距是 20 像素。为了让线距有手工感我在基准间距上乘以(1 (rand() - 0.5) * 0.1)让每条线的间距在 19~21 像素波动。线的路径不是直线而是沿水平方向走时叠加低频 noise 扰动幅度约 2 像素形成轻微的弯曲。交叉效果是编织纹理的灵魂。我采用一个双重循环绘制每一条纬线时检查它与经线的交点根据坐标的奇偶规律决定浮在经线上还是沉在经线下方。沉下去的部分线的颜色加深且宽度略微缩小模拟被压在下面的阴影感。浮在上方的部分则保持原色并多绘制一两像素的高光。这样跑出来交错的立体感就出来了。关键参数就三个density编织密度、irregularity间距不规则度、curveStrength弯曲强度。我实测下来最常用的组合是密度 40、不规则度 0.08、弯曲强度 2出来的效果非常像手工粗织棉布。4.2 陶瓷开片裂纹纹理陶瓷开片的难点在裂纹的形态——它不是几条简单的线段而是一组互相连通、带分岔的网状结构。artcraft 里用的是 Voronoi 思路。简化的做法是不做完整的 Voronoi 划分而是先在画布上随机撒 20~30 个点作为裂纹源然后让裂纹从每个源点出发以随机方向逐段生长。每一段都记录方向和长度当某条裂纹快要撞上另一条时它会偏向并与之接合形成交叉点。这个避让-接合逻辑能产生自然的分岔效果。为了让裂纹更真实我给每段裂痕添加了边沿细节裂痕不是一条单线而是主缝加两侧随机暗色细线模拟瓷釉崩碎的毛边。背景色我用浅青灰色印象里青瓷开片就是那种暧昧的青白色裂纹色则用深褐灰色。说真的代码跑出来的开片纹理印在亚克力板上效果好到客户以为我们请了景德镇的师傅画了三天。需要留神的一点是这种生长算法是有while循环的如果参数设置不当——比如步长太小、步数太多——会陷入性能泥潭。我的处理办法是每一步都检查当前总节点数超过 10000 个就强制停止保证任何输入都能在几百毫秒内出图。4.3 木纹年轮生成木纹是所有纹理案例里代码量最少、但像不像最容易出问题的一个。很多初版木纹看起来像靶纸就是因为年轮被画成了完美的同心圆。正确的做法是先定义一个极坐标系下的基准半径年轮 i 的半径是r i * ringWidth。然后把每个半径通过 noise 函数扰动扰动随角度变化但不随半径变化这样整棵树的年轮会呈现统一的歪斜感——表现的是树在生长过程中受到的偏压。再多加一个小振幅的高频噪声让年轮边界产生局部的粗糙。木纹的颜色渐变也要讲究。靠近髓心的部分一般是浅色的早材靠近外侧的晚材颜色更深。我在相邻年轮之间做渐变深色晚材宽度约占环宽的 15% 到 25%这个比例直接影响仿真度。最后在背景上叠一层几乎看不出的径向光晕让木纹表面有轻微的反光立体感。这类纹理在商业里最实用——壁纸、地板设计、产品包装的木质质感模拟全是刚需。生成一版不需要拍照设备参数可控且无版权风险。5. 常见问题与排错实录5.1 同一份种子为什么两次结果不一致这是新手最常遇到也最容易措手不及的问题。明明在同一次会话里点击了保存 preset第二天打开回来用同一个 seed 恢复结果图却不一样了。第一个排查对象是Math.random()。很多人在局部代码里图方便直接用了它比如决定色相偏移量时写Math.random() * hueRange。这份子随机序列不归种子系统管所以每次运行都会变化。解决办法就是全局禁用Math.random()所有随机值一律通过种子随机函数产生。第二个排查对象是对象的遍历顺序。JavaScript 的对象键在特定情况下会重新排序尤其是整型键。如果你用for...in或Object.entries()遍历参数对象并依次消耗随机数那么随机数的消耗顺序在不同环境下可能不一致导致生成结果漂移。解决方法是把参数对象转换成一个有序的数组严格按数组顺序消耗随机数。5.2 Canvas 导出 SVG 图形错位Canvas 和 SVG 明明用的同一份指令列表但导出文件就是和屏幕预览错位。这个问题我排查了很久最后发现是坐标系统的差异。Canvas 的坐标原点是画布左上角而 SVG 的默认坐标体系也是左上角单看原点没问题。但实际预览时为了让纹理居中加入了很多位移和缩放变换——比如ctx.scale(2, 2)、ctx.translate(40, 40)——而导出时这些变换没有重新计算到绝对坐标里。正确的做法是在导出 SVG 之前把指令列表里的每个坐标点先做逆变换或者生成时就记录世界坐标和屏幕坐标两套值导出用世界坐标预览用屏幕坐标。还有一个更隐蔽的坑Canvas 的线宽会被变换矩阵 scale 影响你预览时线宽 2 在 scale 2 下实际是 4。导出 SVG 时如果用原始线宽就会出现预览粗、导出细的差异。要确保导出的stroke-width等于 屏幕线宽除以整体缩放量。5.3 渲染性能突然暴跌画布就 800×800操作滑块却卡到 3 帧每秒。遇到这种情况不要急着怀疑 Canvas 2D 的性能先查你的代码里有没有无意中创建的重复对象。我踩过最大的性能坑是把一副纹理的生成函数写成了每帧都重新计算全部噪声。木纹案例里噪声计算是每帧全量重跑。后来我给这功能加了一个缓存层——输入参数不变时直接用cacheKey seed density irregularity做键把生成结果存进 Map只有参数真正变化时才重算。加了这层之后大部分滑块操作都能流畅跟手。另外要检查requestAnimationFrame是否和参数防抖冲突了。如果每次参数变化都同时触发一次setTimeout重绘和一次requestAnimationFrame会产生重复绘制。我最后的统一方案是所有重绘请求都带一个递增的 revision 号重绘前检查 revision 是不是最新过期请求直接丢掉。5.4 色彩输出偏灰偏脏生成结果结构没毛病但颜色总像蒙了一层灰。排查方向有两个。一是颜色运算过程中是不是无意中把 RGB 分量截断了多次。每截断一次色域边缘的细节就丢失一点累积起来画面自然就脏了。建议在调色统一在 HSL 空间做只在渲染最后一步转成 RGB。二是色板里的颜色明度反差太小。所有颜色都挤在中明度区间画面缺少跳动的亮点和收得住的重色。调整办法是在色板里保证存在一个明度低于 40% 的深色和一个明度高于 75% 的浅色中间色作为过渡。这个技巧对任何层次的纹理都有效不只是 artcraft。6. 扩展思路与个人体会6.1 从 2D 纹理到 3D 表面artcraft 做到后面已经不满足于平面纹理了。我们把生成的灰度图导出成 bump 贴图或 normal 贴图直接塞进三维渲染软件里做表面凹凸。编织纹理生成器输出的经纬交叉高度信息取灰度后映射到模型的布料表面陶瓷裂纹的线条在渲染器里通过透明度通道变成了真实的裂纹凹槽。这个玩法之所以可行是因为 artcraft 的生成过程始终保持了结构信息。它不是像截图一样拍一张纹理而是保留了每一条线、每一个节点的空间关系和属性。把线宽换成模型采样用的权重把颜色深浅换成高度值一张 2D 纹理就变成了 3D 表面的 displacement 数据。这让 artcraft 从一个平面素材工具变成了跨维度的材质生成器。6.2 我在维护过程中积累的几点经验一个开源工具项目能做起来技术只是基础更多是靠使用场景的持续验证。我自己最常做的事是把每次案例积累下来的好参数存成预设并且在预设文件里写好注释记录当时是怎么调出来的。这个习惯帮了大忙——三个月后再回来看这个项目不用重新摸索。另一个很实际的经验是一个生成工具的交互界面要减少抽象层次。用户不关心你内部用的是 Value Noise 还是 Perlin Noise他们只关心密度粗细随机程度这种直观指标。把复杂参数命名成普通人能理解的自然语言工具才真正好上手。最后一点也是在维护 artcraft 的过程中被反复验证的任何工艺纹理的数字化都包含结构提取和瑕疵注入两个阶段。结构提取解决像不像瑕疵注入解决真不真。这两个阶段必须清晰地分开设计一旦混在一起参数就会乱掉纹理也会失去秩序感。这条原则不仅仅适用于纹理生成我觉得放在任何创作类工具的设计里都是一个有效的指导框架。