生成艺术工具 artcraft:打造可控随机的矢量图形流水线

发布时间:2026/10/19 11:00:12
生成艺术工具 artcraft:打造可控随机的矢量图形流水线 年初有位老朋友要过生日我想送张贺卡。手绘是不可能手绘的我连一条顺滑的直线都画不利索于是理所当然地打开编辑器准备用代码来画一张。半小时后我得到了一团随机散点外加几条歪歪扭扭的曲线说它是“抽象”都算客气。那段经历让我想明白一件事问题不在于我不懂画而在于我没有一套能将“脑海里的画面”翻译成“程序参数”的流水线。也就是从那时起我断断续续搭了一个代号叫artcraft的生成艺术小工具。它不是一个完整的产品更像是我自己的一张工作台——输入种子、调一批参数输出可复现的矢量图形或位图。这篇文章没有产品宣传只有完整的架构思路、算法细节、实战环节和踩坑记录适合正在玩 p5.js/Processing但总觉得作品差一口气的开发者也适合对生成艺术感兴趣、却不知道怎么把这些随机数组织成“作品”的设计师。1. 为什么我会做这样一个工具1.1 手画不行纯随机又没灵魂先说最初的痛点。前几年我想做一组壁纸试过直接用随机函数在画布上撒点、连线、画圈。刚开始还觉得挺有“数据艺术感”但看久了就会发现纯随机生成的东西有一个致命毛病没有组织感。随机点会扎堆线条会乱窜整个画面像一台坏掉的电视在出雪花而不是一件“作品”。那段时间我也试过现成的纹理生成器和滤镜。它们的问题是太固定了本质是别人预设好的规则集合改几个滑块只能微调很难调出自己真正想要的画面。我从这条路里得到的核心教训是生成艺术不该是“让电脑随便画”而是设计一个系统让电脑在这个系统里“有纪律地随机”。这就像掷骰子——你接受结果是随机的但骰子必须是公平的六面体而不是随便一团废纸。于是我很自然地把目光转回了代码。既然市面上找不到一个满足我要求的工具那就自己做一个。这就是 artcraft 最早的动机。1.2 我要的是“可控的偶然”“可控的偶然”是我后来一直在用的设计原则。大多数随机生成失败不是随机用得不够而是随机用错了地方。举一个具体的分层场景全局随机整幅画的构图方向、视觉重心落在哪比如“偏左下角组织线条”这种随机是艺术层面的选择。局部随机一条线条的抖动程度、一个圆点的半径变化这种随机让画面产生手感和生命力。禁止随机海报上的文字、签名、固定尺寸的边缘、需要精确对齐的元素这些地方必须完全确定。artcraft 整个架构都是围绕这三层随机展开的。全局随机由种子决定局部随机由种子派生出来的子生成器控制禁止随机的部分直接用绝对坐标。所有随机决策都必须可复现——这一点后文会专门展开。1.3 定位不是绘画软件是一条生成流水线我一开始也想过干脆把 artcraft 做成一个类似画板的软件左边工具栏右边画布用户手动画。但很快放弃了因为市面上成熟的绘画工具太多了我没有必要也不该去和它们竞争。artcraft 的定位是一条生成流水线输入是“种子 参数组”输出是 SVG/PNG/GIF中间是三个不透明的阶段——生成器负责造出基础元素合成器负责把元素组织到画布上渲染器负责最终输出。整个过程可以一键重放。打个比方它不像绘画软件更像一套胶片冲印流程。参数是底片种子是显影配方。同样的底片配不同配方冲出的照片气质完全不同但每张照片都有迹可循。2. artcraft 的核心架构把“艺术直觉”翻译成“程序参数”2.1 四层流水线的职责划分artcraft 的内部结构并不复杂我把整个项目拆成了四层种子层一个整数种子会生成一个可复现的伪随机数流。每个生成器从主随机流里申请一个独立子流这样互不干扰程序行为可预期。生成器层根据种子和参数生成基础元素包括点、线、多边形、噪声场。这一层只管“造东西”不管“摆东西”。合成器层负责把所有元素组织到画布上。它处理图层顺序、坐标变换、裁剪区域、混合模式。如果把生成器比作生产零件的工厂合成器就是装配线。渲染器层把合成后的结构化对象输出成 SVG、PNG 或其他格式同时处理 DPI、尺寸、背景色等问题。用代码表达大概是这种感觉const artcraft createArtcraftClient(); const project artcraft.createProject({ seed: 20241117, width: 1200, height: 800, output: svg }); project.addLayer(ContourLayer); project.addLayer(DotLayer); project.addLayer(ColorFieldLayer); project.render();层与层之间不直接互相调用中间的通信通过统一数据格式带属性标注的路径集合。这样做的好处是任何一个环节出了问题都能单独替换。比如我想把渲染器从 SVG 换成 Canvas 2D完全不需要动生成器和合成器。2.2 参数设计让调参像调色而不是改源码早期版本的 artcraft 参数表直接暴露底层函数参数比如noiseOctaves: 7、maxDistance: 38.5。结果我自己过两天回来看都一脸懵根本不知道调到什么程度是合理的。后来我换了一套思路所有参数必须能直接映射到“你眼睛看到的东西”。举个例子。用户不应该理解分形噪声的 octave 是什么他只知道自己想要“更细腻的纹理”。于是我把底层参数包了一层detailLevel0 到 1内部映射到噪声叠加次数和频率缩放。wobble线条抖动量内部映射到贝塞尔控制点的高斯扰动方差。sparseness点的疏密度内部映射到泊松圆盘采样的最小距离。命名上我尽量用“人话”避免直接用底层术语。这听起来是小事但特别重要。工具一旦超过两天不用你就会忘掉那些抽象参数的含义而一套“所见即所得”的参数表能让任何基础的人坐下来玩十分钟就知道怎么调整。参数还要分组。artcraft 的参数面板大致分五组构图、纹理、线条、颜色、输出。每组下面才是具体旋钮。有些组合参数可以打包成“预设”比如“柔和印刷风”“荧光噪点风”一个预设就是一整套参数快照。这样新手可以直接从预设开始改而不用面对一张空空的参数表单。2.3 为什么默认输出 SVG而不是直接给 PNG很多人问过生成艺术不是应该直接输出位图吗为什么要绕一圈先出 SVG再转位图原因有三。第一SVG 是可缩放、可再编辑的。一次生成的矢量结果放到名片上、墙体海报上都能保持清晰甚至还能在矢量软件里继续调整路径。第二SVG 文件通常比同尺寸 PNG 小一个数量级。一张 4K 尺寸、包含大量圆点和曲线的作品PNG 可能要几十 MB而 SVG 往往只有几 MB。第三SVG 是“结构化”的这意味着我可以对结果做程序化后处理批量改颜色、替换线条宽度、裁剪局部区域全部不需要重新生成。所以在 artcraft 里底层数据模型是一批带属性的矢量路径对象光栅化成 PNG 只是渲染流水线的一个输出端口。3. 关键算法与实现细节让生成艺术告别“电脑味”3.1 分形噪声铺底所有柔和的纹理都从它开始生成纹理、云层、地形、渐变背景我几乎都离不开分形噪声。它的原理可以简化成一句话把多个不同频率、不同振幅的平滑噪声叠加起来。单独一层平滑噪声太“均匀”了像一块完全平滑的丝绸而把大山套小山、小山套石头的波形加起来才接近自然界的复杂纹理。我用的是一个简化版的分形布朗运动实现function fbm(x, y, octaves 5, lacunarity 2, gain 0.5) { let amplitude 0.5; let frequency 1; let sum 0; let norm 0; for (let i 0; i octaves; i) { sum amplitude * noise.perlin2(x * frequency, y * frequency); norm amplitude; amplitude * gain; frequency * lacunarity; } return sum / norm; }几个关键参数的作用octaves叠加层数。更多层意味着更多细节但计算量也线性增加。lacunarity频率放大倍数通常取 2。数值越大高频细节被放大得越快。gain振幅衰减比例通常小于 1。它保证高频层的贡献越来越小最终叠加结果收敛不会爆掉。单人项目里完全不用实现复杂的噪声算法直接找一个可靠的梯度噪声实现就能用。关键是理解 fbm 的组合方式而不是从零写一个哈希梯度函数。3.2 让随机点看起来“有组织感”泊松圆盘采样与黄金角随机点本身不是问题问题在于没有约束的随机点会在视觉上扎堆。现实里你不会希望一张海报上的圆点左边挤成一团、右边空出一大片。想让点阵既自然又均匀我主要用两种方案。第一种是泊松圆盘采样。它保证任意两个点之间的距离不小于某个最小值同时又能保留随机性。实现核心是用网格加速距离查询把画布切成若干个单元格每个点只落在一个格子里检测冲突时只要查相邻格子就行然后通过一个活跃点列表不断往外生长。function poissonDisk(width, height, radius, rng) { const cellSize radius / Math.sqrt(2); const cols Math.ceil(width / cellSize); const grid new Array(cols * Math.ceil(height / cellSize)); const points []; const active []; // 随机初始点 const start { x: rng() * width, y: rng() * height }; // ...后续生成与冲突检测 return points; }第二种是黄金角螺旋。以 137.5 度为角度增量生成的点列会自然形成向日葵花盘、星系旋臂这类构图视觉上均匀却没有网格感。这个角度在圆上的含义是连续旋转后永远趋近黄金分割比例所以不会出现明显的直线对齐痕迹。这两种方案我通常结合使用大面积铺点用泊松圆盘需要漩涡或放射状构图时用黄金角螺旋。3.3 颜色调和把色彩理论直接写进代码刚开始我尝试用 RGB 空间做随机颜色结果所有颜色都像儿童糖果过于鲜艳、互不协调。后来我切换到 HSL 空间才发现问题出在最基本的地方。HSL 的好处是三个维度在视觉上相对独立色相决定颜色倾向饱和度决定鲜艳程度明度决定深浅。做调和方案时我只需要固定一个基准色相然后按规则偏移就很容易得到协调的整套配色。artcraft 里内置了常见的调和规则写出来并不复杂function buildPalette(baseHue, scheme analogous, count 8, rng) { const rules { monochrome: [0], analogous: [-30, 30], complementary: [180], splitComplementary: [150, 210], triadic: [120, 240], }; const offsets rules[scheme]; const palette []; for (let i 0; i count; i) { const offset offsets[i % offsets.length]; const hue (baseHue offset rng() * 12 - 6 360) % 360; const saturation clamp(60 rng() * 30, 0, 100); const lightness clamp(55 rng() * 25, 0, 100); palette.push({ hue, saturation, lightness }); } return palette; }注意中间的抖动值rng() * 12 - 6。纯按公式来会显得非常机械像电脑配色加了这个 12 以内的抖动颜色才“活”起来像人挑出来的。3.4 笔触模拟一条有生命的手绘线是怎么来的我想让曲线的边缘有一点点手绘的味道可以把一条数学上完美的曲线拆成控制点再给每个控制点加高斯扰动。最简单的方式是取相邻点的中点做偏移然后把它们拼成贝塞尔曲线function wobblyLine(pts, wobble 4, rng) { const controls []; for (let i 0; i pts.length - 1; i) { const p pts[i]; const next pts[i 1]; const mid { x: (p.x next.x) / 2 (rng() - 0.5) * wobble, y: (p.y next.y) / 2 (rng() - 0.5) * wobble, }; controls.push(mid); } // 拼成 SVG path return M ${pts[0].x} ${pts[0].y} C ...; }wobble参数背后用的是高斯扰动而不是平均分布因为平均分布的偏转容易产生呆板的锯齿感。更进阶的做法是给线条叠加粗细渐变线条两端逐渐变细模拟铅笔和毛笔的物理笔触。这一步直接决定了最终作品是“软件画的”还是“手绘的”。4. 实战一张等高线与圆点海报的完整生成链路4.1 先定方向再写代码理论讲了一堆实际跑一遍最有价值。我在 artcraft 里做了一个自测项目生成一张 A4 竖版海报风格定为山体等高线叠加渐变圆点。A4 竖版意味着画布是 2480×3508 像素300 DPI。我先定下参考参数const config { seed: 817204, page: { width: 2480, height: 3508, margin: 160 }, background: { gradient: true, colors: [#e9e2d0, #b2c6d8] }, contours: { count: 24, strokeWidth: 2, opacity: 0.35 }, dots: { count: 620, minR: 6, maxR: 48, scheme: splitComplementary }, }; const project artcraft.createProject(config);这里有两个细节值得说明。第一margin: 160是出血和装订线的空间做印刷品构图时元素不能贴到边缘。第二配色用#e9e2d0到#b2c6d8一个偏暖的沙色、一个偏冷的雾蓝配合分裂互补色的小圆点整体层次很容易拉开。4.2 实现过程从噪声场到等值线再到圆点整个生成链路分五步初始化项目根据种子派生多个随机子流生成构图重心位置。生成背景用 fbm 噪声生成一个柔和渐变场作为整个画面的底色。提取等高线在噪声场上画 24 条等值线。等值线的算法我用的是经典的网格边线插值思路遍历每一个小格判断四个角的值与阈值的相对关系连出线段再把所有网格的线段拼起来。这个过程类似地形图的山体等高线只不过这里的“高度”来自噪声场。生成圆点用泊松圆盘采样在画布上撒 620 个不重叠的点每个点的半径从 6 到 48 随机分布颜色从调色板里取。合成分层背景在最底层等高线在中层圆点放在前景最终渲染成 SVG再由离屏画布光栅化成 PNG。落地之后我还会执行一次命令行式生成把 seed 和 config 都作为参数传入node make-poster.js --config poster.art.json --seed 817204这样每次跑出的海报都是同一张。想换风格只需要替换 config 文件。4.3 输出阶段的三个关键检查点输出阶段不只是点一下导出按钮我实操中总结出三个高频检查点尺寸与 DPI生成 SVG 时要带上正确的viewBox按 1:1 对应 2480×3508 像素如果后续要印刷再按目标纸张尺寸换算成毫米。出血位我会故意把背景色块多画一圈盖过画布边缘防止切割时出现白边。透明度图层SVG 里几十个半透明图层叠加在屏幕上看着没问题但印刷前必须做扁平化否则打印解析时又慢又容易出错。这一步做完一张可以直接发朋友圈的生成海报就出来了。算法复杂度不高但每层之间的配合才真正决定了画面的品质。5. 踩坑复盘种子、性能、参数与印刷现场的真实教训5.1 种子管理可复现是生成艺术的底线这是 artcraft 发展过程中最大的教训。早期版本里我直接在代码各处调用Math.random()结果每次运行都生成一张完全不同的图。调出一个满意的效果想回头调整几个参数结果整个画面变成了另一幅画完全没法迭代。后来我立了一个铁规矩每一个随机决策都必须从同一个种子派生绝不能调用全局随机函数。代码里所有随机源都从一个可复现的伪随机对象里获取。我还会为每一次“出图”保存一个 manifest 文件里面记录 seed 和完整参数快照{ seed: 817204, config: { background: { gradient: true, colors: [#e9e2d0, #b2c6d8] }, contours: { count: 24, strokeWidth: 2, opacity: 0.35 }, dots: { count: 620, minR: 6, maxR: 48 } } }每次实验都保留这个 JSON就好比摄影师记录拍摄参数。遇到满意的结果可以随时找回遇到翻车的也能精确定位是哪一步出了问题。这事听着麻烦实际一劳永逸。5.2 性能优化从“跑十分钟”到“秒出图”等值线提取的时候我第一次直接在 2480×3508 的整图层上逐像素计算结果跑了十几分钟才出一张图简直没法用。换了个思路先在低分辨率上算出噪声场比如整幅图的四分之一再做双线性插值放大。噪声本身是低频纹理插值带来的视觉损失几乎看不出来性能从十几秒直接降到两秒以内。第二个性能坑来自 SVG 体量。等高线加几千个圆点一次性写到 SVG 文件里文件可能堆到十几 MB浏览器打开都卡。对策是合并重复路径、删除密集但视觉上无感知的多余点。大部分情况下一条等值线根本不需要几百个点缩减到七八十个就足够平滑。第三个坑是内存。4K 画布每个像素按 RGBA 算要 34 MB如果我同时保留三层中间结果内存会迅速吃掉几百 MB。我的做法是能即时渲染的就即时渲染不保留完整像素缓冲渲染完成马上释放。5.3 参数爆炸与默认值设计生成工具最大的困境是“旋钮太多人容易晕”。artcraft 早期有上百个可调参数我自己都记不住更别提其他人。后来我把参数按功能和可视化效果重新分组每组只保留一个主旋钮加两个微调项其余参数全部设好安全默认值。安全默认值的定义是就算用户把它调到极端程序也不应该崩溃或者输出一张明显破损的图。为此我写了统一钳制函数保证每个参数都在安全范围内function clamp(v, min, max) { return Math.min(Math.max(v, min), max); }别小看这个函数。少了它用户把密度调到 100%程序可能瞬间生成几十万条路径直接把浏览器拖垮。有了钳制行为就变成“密度再高也只在一个合理范围内”。5.4 打印现场的一个具体翻车密集圆点的摩尔纹做那批海报的时候我第二次打印就翻了车。样张显示圆点密集区域出现了明显的波纹像两排纱窗叠在一起产生的干涉条纹。这个现象叫摩尔纹本质是规则网点阵和打印栅格之间发生了空间频率干涉。我当时的第一反应是“生成精度不够”后来查了印刷经验才明白规则排列的等间距网点在特定缩放比例下几乎必出摩尔纹。解决方式是给圆点的位置和半径都加入 5% 到 10% 的随机微调打破网点阵的周期性。同时把密度整体下调一点并为印刷单独保留一份“低密度版本”。这个坑让我明白生成作品在屏幕上看着舒服和印刷出来舒服是两套完全不同的验收标准。6. 再往前走动效、AI 协作与 artcraft 的生态位6.1 把静态图变成时间轴动画artcraft 的下一步我打算做时间轴化。思路很简单把参数组中的每一个值看作一条随时间变化的曲线每帧读一次曲线当前的参数值然后走一遍生成流水线输出一帧图像。连续帧拼起来就是一段没有任何关键帧动画师介入的生成动画。这比单独生成静态图的难点在于必须保证参数变化是平滑的否则帧与帧之间会跳变。解决办法是在粒子系统里加入“缓存池”让每个粒子在帧间的颜色、位置都做线性插值而不是每帧重新随机。6.2 和 AI 生成模型结合的思路很多人最近都在讨论 AI 生成图像我的观点是大模型擅长“描述”但不擅长“精确控制几何”。artcraft 的价值恰好相反擅长精确控制但缺少一点点“文字直觉”。所以我的思路是双向的一方面用自然语言描述想要的画面气质让模型输出一组 artcraft 参数建议另一方面让 artcraft 把参数精确渲染成矢量图形再由人微调。比如输入一句“安静的等高线海报女儿红配色”模型给出一组初始baseHue、contourCount等参数然后我打开预览动两下旋钮就能拿到一张完整作品。这不是魔法只是把原来 prompt 工程里的“抽卡”变成了“调可感知参数”。6.3 有一些方向还是要克制一点工具做到现在我最大的体会是“边界感”很重要。artcraft 不会去做的三件事不做位图滤镜。成熟商业软件在这条路上积累太深不是一个小项目该碰的领域。不做商业排版系统。文字排版、网格设计、复杂版式已经有非常专业的工具。不做重物理模拟。粒子物理、流体模拟、刚体碰撞每一个都是大坑放进生成艺术工具里只会让项目无限膨胀。artcraft 的舒适区只有一个快速生成可复现的实验性图案而且参数要好玩、可沉浸式调节。在这个窄口里做深比什么都想做却都做不深要实际得多。artcraft 目前还是我脚本集合里的一堆 JavaScript 文件离一个完整的作品还有距离。但如果再让我做一次我一定会从第一天就把种子归档和参数验证写进核心流程而不是等踩了坑再补。生成艺术的核心从来不是“随机”而是“规则与随机之间的对话”。你如果也想做类似的小工具不用急着从零搭架构先把一个噪声函数、一堆受约束的点和一条矢量输出管线跑通后面所有事情都会顺很多。